diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000000..052f3fb147c --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,83 @@ +{ + "name": "native", + "image": "mcr.microsoft.com/devcontainers/base:bookworm", + + "customizations": { + "codespaces": { + "openFiles": [ + "docs/setup/codespaces.md", + "README.md" + ] + } + }, + "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" + }, + "ghcr.io/devcontainers/features/sshd:1": { + "version": "latest" + }, + "ghcr.io/devcontainers/features/github-cli:1": { + "version": "latest" + } + }, + + "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 + } + }, + + "onCreateCommand": "sh .devcontainer/on-create.sh", + "postCreateCommand": "sh .devcontainer/post-create.sh", + "postStartCommand": "sh .devcontainer/post-start.sh", + "postAttachCommand": { + "server": "bin/rails server", + "sidekiq": "bundle exec sidekiq" + }, + + "customizations": { + "codespaces": { + "repositories": { + "department-of-veterans-affairs/vets-api-mockdata": { + "permissions": { + "contents": "read", + "pull_requests": "write" + } + } + } + }, + "vscode": { + "extensions": ["Shopify.ruby-lsp"] + } + } +} diff --git a/.devcontainer/on-create.sh b/.devcontainer/on-create.sh new file mode 100644 index 00000000000..0e45dd7d73b --- /dev/null +++ b/.devcontainer/on-create.sh @@ -0,0 +1,80 @@ +#!/bin/sh + +# this runs as part of pre-build + +echo "on-create start" +echo "$(date +'%Y-%m-%d %H:%M:%S') on-create start" >> "$HOME/status" + +# Homebrew/asdf paths to zsh +{ + echo "export PATH=\"/home/linuxbrew/.linuxbrew/bin:/home/linuxbrew/.linuxbrew/sbin:\$PATH\"" + echo "source \"\$HOME/.asdf/asdf.sh\"" +} >> ~/.zshrc + +export PATH="${HOME}/.asdf/shims:${HOME}/.asdf/bin:${PATH}" +asdf install ruby $( cat .ruby-version ) +asdf global ruby $( cat .ruby-version ) + +# Clone needed repos +git clone https://github.com/department-of-veterans-affairs/vets-api-mockdata.git /workspaces/vets-api-mockdata + +# Install dependencies +sudo apt-get update +sudo apt-get install -y libpq-dev pdftk shared-mime-info postgresql-15-postgis-3 tmux xclip + +# only run apt upgrade on pre-build +if [ "$CODESPACE_NAME" = "null" ] +then + sudo apt-get update + sudo apt-get upgrade -y + sudo apt-get autoremove -y + sudo apt-get clean -y +fi + +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 + +# Start redis +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 & + +# Start postgres +sudo /etc/init.d/postgresql restart +pg_isready -t 60 +sudo -u root sudo -u postgres psql -c "ALTER USER postgres PASSWORD 'password';" + +# Install gems and setup DB +./bin/setup + +# Prewarm Bootsnap +bundle exec bootsnap precompile --gemfile app/ lib/ + +echo "on-create complete" +echo "$(date +'%Y-%m-%d %H:%M:%S') on-create complete" >> "$HOME/status" diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh new file mode 100644 index 00000000000..88c4240d446 --- /dev/null +++ b/.devcontainer/post-create.sh @@ -0,0 +1,22 @@ +#!/bin/sh + +# this runs at Codespace creation - not part of pre-build + +echo "post-create start" +echo "$(date) post-create start" >> "$HOME/status" + +# update the repos +git -C /workspaces/vets-api-mockdata pull +git -C /workspaces/vets-api pull + +mkdir /workspaces/vets-api/.vscode +{ +{ + "rubyLsp.rubyVersionManager": "none" +} +} >> /workspaces/vets-api/.vscode/settings.json + +bundle install + +echo "post-create complete" +echo "$(date +'%Y-%m-%d %H:%M:%S') post-create complete" >> "$HOME/status" 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 4b87774c30c..dc78d981a1a 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 @@ -72,7 +73,6 @@ app/controllers/v0/evss_claims_async_controller.rb @department-of-veterans-affai app/controllers/v0/evss_claims_controller.rb @department-of-veterans-affairs/benefits-management-tools-be @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group app/controllers/v0/example_controller.rb @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group app/controllers/v0/feature_toggles_controller.rb @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group -app/controllers/v0/financial_status_reports_controller.rb @department-of-veterans-affairs/vsa-debt-resolution @department-of-veterans-affairs/backend-review-group app/controllers/v0/folders_controller.rb @department-of-veterans-affairs/vfs-health-modernization-initiative @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group app/controllers/v0/form1010cg/attachments_controller.rb @department-of-veterans-affairs/vfs-10-10 @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group app/controllers/v0/form1095_bs_controller.rb @department-of-veterans-affairs/vfs-1095-b @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group @@ -149,6 +149,7 @@ app/controllers/v1/profile/ @department-of-veterans-affairs/vfs-authenticated-ex app/controllers/v1/supplemental_claims @department-of-veterans-affairs/benefits-decision-reviews-be @department-of-veterans-affairs/backend-review-group app/controllers/v1/apidocs_controller.rb @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group app/controllers/v1/notice_of_disagreements_controller.rb @department-of-veterans-affairs/Benefits-Team-1 @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group +app/controllers/v1/pension_ipf_callbacks_controller.rb @department-of-veterans-affairs/pensions @department-of-veterans-affairs/backend-review-group app/controllers/v1/sessions_controller.rb @department-of-veterans-affairs/octo-identity app/controllers/v1/supplemental_claims_controller.rb @department-of-veterans-affairs/benefits-decision-reviews-be @department-of-veterans-affairs/backend-review-group app/controllers/v1/supplemental_claims @department-of-veterans-affairs/benefits-decision-reviews-be @department-of-veterans-affairs/backend-review-group @@ -244,7 +245,6 @@ app/models/form1010cg/submission.rb @department-of-veterans-affairs/vfs-10-10 @d app/models/form1095_b.rb @department-of-veterans-affairs/vfs-1095-b @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group app/models/form526_job_status.rb @department-of-veterans-affairs/Disability-Experience @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group app/models/form526_submission.rb @department-of-veterans-affairs/Disability-Experience @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group -app/models/form5655_submission.rb @department-of-veterans-affairs/vsa-debt-resolution @department-of-veterans-affairs/backend-review-group app/models/form_attachment.rb @department-of-veterans-affairs/Benefits-Team-1 @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group app/models/form_profile.rb @department-of-veterans-affairs/vfs-authenticated-experience-backend @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group app/models/form_profiles @department-of-veterans-affairs/my-education-benefits @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group @@ -288,6 +288,7 @@ app/models/rate_limited_search.rb @department-of-veterans-affairs/va-api-enginee app/models/saml_request_tracker.rb @department-of-veterans-affairs/octo-identity app/models/saved_claim.rb @department-of-veterans-affairs/Benefits-Team-1 @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group app/models/session.rb @department-of-veterans-affairs/octo-identity +app/models/saved_claim/burial.rb @department-of-veterans-affairs/benefits-non-disability @department-of-veterans-affairs/pensions @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group app/models/saved_claim/pension.rb @department-of-veterans-affairs/pensions @department-of-veterans-affairs/backend-review-group app/models/saved_claim/veteran_readiness_employment_claim.rb @department-of-veterans-affairs/benefits-non-disability @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group app/models/sign_in @department-of-veterans-affairs/octo-identity @@ -397,8 +398,6 @@ app/serializers/receive_application_serializer.rb @department-of-veterans-affair app/serializers/saved_claim_serializer.rb @department-of-veterans-affairs/Benefits-Team-1 @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group app/serializers/search_serializer.rb @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group app/serializers/service_history_serializer.rb @department-of-veterans-affairs/Disability-Experience @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group -app/serializers/sign_in @department-of-veterans-affairs/octo-identity -app/serializers/sign_in/introspect_serializer.rb @department-of-veterans-affairs/octo-identity app/serializers/submit_disability_form_serializer.rb @department-of-veterans-affairs/Disability-Experience @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group app/serializers/supporting_documentation_attachment_serializer.rb @department-of-veterans-affairs/Disability-Experience @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group app/serializers/supporting_evidence_attachment_serializer.rb @department-of-veterans-affairs/Disability-Experience @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group @@ -775,6 +774,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 @@ -786,8 +786,8 @@ lib/bgs @department-of-veterans-affairs/Benefits-Team-1 @department-of-veterans- lib/bid @department-of-veterans-affairs/Disability-Experience @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group lib/bip_claims @department-of-veterans-affairs/Benefits-Team-1 @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group lib/carma @department-of-veterans-affairs/vfs-10-10 @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group -lib/caseflow @department-of-veterans-affairs/lighthouse-banana-peels -lib/central_mail @department-of-veterans-affairs/lighthouse-banana-peels +lib/caseflow @department-of-veterans-affairs/lighthouse-banana-peels @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group +lib/central_mail @department-of-veterans-affairs/lighthouse-banana-peels @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group lib/chip @department-of-veterans-affairs/vsa-healthcare-health-quest-1-backend @department-of-veterans-affairs/patient-check-in @department-of-veterans-affairs/backend-review-group lib/claim_letters @department-of-veterans-affairs/benefits-management-tools-be @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group lib/common/client/base.rb @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group @@ -881,9 +881,10 @@ lib/pagerduty @department-of-veterans-affairs/va-api-engineers @department-of-ve lib/pdf_fill @department-of-veterans-affairs/vsa-debt-resolution @department-of-veterans-affairs/backend-review-group @department-of-veterans-affairs/vfs-10-10 @department-of-veterans-affairs/vfs-1095-b lib/pdf_fill/forms/pdfs/21P-527EZ.pdf @department-of-veterans-affairs/pensions @department-of-veterans-affairs/backend-review-group lib/pdf_fill/forms/va21p527ez.rb @department-of-veterans-affairs/pensions @department-of-veterans-affairs/backend-review-group -lib/pdf_info.rb @department-of-veterans-affairs/lighthouse-banana-peels -lib/pdf_utilities @department-of-veterans-affairs/lighthouse-banana-peels +lib/pdf_info.rb @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group +lib/pdf_utilities @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group lib/pension_burial @department-of-veterans-affairs/mbs-core-team @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group +lib/pension_21p527ez @department-of-veterans-affairs/pensions @department-of-veterans-affairs/backend-review-group lib/periodic_jobs.rb @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group lib/preneeds @department-of-veterans-affairs/mbs-core-team @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group lib/rx @department-of-veterans-affairs/mobile-api-team @department-of-veterans-affairs/vfs-mhv-medications @department-of-veterans-affairs/backend-review-group @@ -923,7 +924,7 @@ lib/virtual_regional_office @department-of-veterans-affairs/Benefits-Team-1 @dep lib/vre @department-of-veterans-affairs/benefits-non-disability @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group lib/webhooks @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group modules/accredited_representatives @department-of-veterans-affairs/accredited-representation-management -modules/appeals_api @department-of-veterans-affairs/lighthouse-banana-peels +modules/appeals_api @department-of-veterans-affairs/lighthouse-banana-peels @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group modules/apps_api @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/lighthouse-pivot modules/ask_va_api @department-of-veterans-affairs/ask-va-team @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group modules/avs @department-of-veterans-affairs/after-visit-summary @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group @@ -943,11 +944,11 @@ modules/my_health @department-of-veterans-affairs/vfs-mhv-secure-messaging @depa modules/representation_management @department-of-veterans-affairs/backend-review-group @department-of-veterans-affairs/accredited-representation-management modules/simple_forms_api @department-of-veterans-affairs/platform-va-product-forms @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group modules/test_user_dashboard @department-of-veterans-affairs/octo-identity @department-of-veterans-affairs/qa-standards -modules/va_forms @department-of-veterans-affairs/lighthouse-banana-peels +modules/va_forms @department-of-veterans-affairs/lighthouse-banana-peels @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group modules/va_notify @department-of-veterans-affairs/va-notify-write @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group modules/vaos @department-of-veterans-affairs/vfs-vaos @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group modules/vaos/app/services/vaos/v2 @department-of-veterans-affairs/vfs-vaos @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group -modules/vba_documents @department-of-veterans-affairs/lighthouse-banana-peels +modules/vba_documents @department-of-veterans-affairs/lighthouse-banana-peels @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group modules/veteran @department-of-veterans-affairs/lighthouse-pivot @department-of-veterans-affairs/accredited-representation-management public @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group modules/veteran_confirmation @department-of-veterans-affairs/lighthouse-ninjapigs @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group @@ -973,6 +974,7 @@ rakelib/mockdata_synchronize.rake @department-of-veterans-affairs/octo-identity rakelib/pension_burial.rake @department-of-veterans-affairs/mbs-core-team @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group rakelib/piilog_repl @department-of-veterans-affairs/backend-review-group @department-of-veterans-affairs/va-api-engineers rakelib/prod @department-of-veterans-affairs/vfs-vaos @department-of-veterans-affairs/backend-review-group @department-of-veterans-affairs/va-api-engineers +rakelib/prod/user_credential.rake @department-of-veterans-affairs/octo-identity rakelib/remove_va1995s_records.rake @department-of-veterans-affairs/govcio-vfep-codereviewers @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group rakelib/routes_csv.rake @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group rakelib/rswag.rake @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group @@ -1009,7 +1011,6 @@ spec/controllers/v0/evss_claims_async_controller_spec.rb @department-of-veterans spec/controllers/v0/evss_claims_controller_spec.rb @department-of-veterans-affairs/vfs-authenticated-experience-backend @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/controllers/v0/example_controller_spec.rb @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/controllers/v0/feature_toggles_controller_spec.rb @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group -spec/controllers/v0/financial_status_reports_controller_spec.rb @department-of-veterans-affairs/vsa-debt-resolution @department-of-veterans-affairs/backend-review-group spec/controllers/v0/form1010cg @department-of-veterans-affairs/vfs-10-10 @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/controllers/v0/form1010cg/attachments_controller_spec.rb @department-of-veterans-affairs/vfs-10-10 @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/controllers/v0/forms_controller_spec.rb @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group @department-of-veterans-affairs/vfs-public-websites-frontend @@ -1163,7 +1164,7 @@ spec/fixtures/hca @department-of-veterans-affairs/vfs-authenticated-experience-b spec/fixtures/form1010_ezr @department-of-veterans-affairs/vfs-10-10 @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/fixtures/identity_dashboard @department-of-veterans-affairs/octo-identity spec/fixtures/idme @department-of-veterans-affairs/octo-identity -spec/fixtures/json/detailed_schema_errors_schema.json @department-of-veterans-affairs/lighthouse-banana-peels +spec/fixtures/json/detailed_schema_errors_schema.json @department-of-veterans-affairs/lighthouse-banana-peels @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/fixtures/json/evss_with_poa.json @department-of-veterans-affairs/backend-review-group spec/fixtures/json/get_active_rxs.json @department-of-veterans-affairs/vfs-mhv-medications spec/fixtures/json/get_determination_not_eligible.json @department-of-veterans-affairs/vfs-mhv-medications @@ -1193,13 +1194,13 @@ spec/fixtures/pdf_fill/28-8832 @department-of-veterans-affairs/Benefits-Team-1 @ spec/fixtures/pdf_fill/5655 @department-of-veterans-affairs/vsa-debt-resolution spec/fixtures/pdf_fill/686C-674 @department-of-veterans-affairs/Benefits-Team-1 @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/fixtures/pdf_fill/extras.pdf @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group -spec/fixtures/pdf_utilities @department-of-veterans-affairs/lighthouse-banana-peels +spec/fixtures/pdf_utilities @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/fixtures/pension @department-of-veterans-affairs/pensions @department-of-veterans-affairs/backend-review-group spec/fixtures/preneeds @department-of-veterans-affairs/mbs-core-team @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/fixtures/pssg @department-of-veterans-affairs/vfs-facilities-frontend @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/fixtures/sign_in @department-of-veterans-affairs/octo-identity spec/fixtures/va_profile @department-of-veterans-affairs/vfs-authenticated-experience-backend @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group -spec/fixtures/vba_documents @department-of-veterans-affairs/lighthouse-banana-peels +spec/fixtures/vba_documents @department-of-veterans-affairs/lighthouse-banana-peels @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/fixtures/vbms @department-of-veterans-affairs/benefits-dependents-management @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/sidekiq/account_login_statistics_job_spec.rb @department-of-veterans-affairs/octo-identity spec/sidekiq/benefits_intake_status_job_spec.rb @department-of-veterans-affairs/platform-va-product-forms @department-of-veterans-affairs/Disability-Experience @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group @@ -1237,7 +1238,8 @@ spec/sidekiq/identity @department-of-veterans-affairs/octo-identity spec/sidekiq/income_limits @department-of-veterans-affairs/vfs-public-websites-frontend @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/sidekiq/in_progress_form_cleaner_spec.rb @department-of-veterans-affairs/vfs-authenticated-experience-backend @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/sidekiq/kms_key_rotation @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group -spec/sidekiq/lighthouse @department-of-veterans-affairs/lighthouse-banana-peels @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group +spec/sidekiq/lighthouse @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group +spec/sidekiq/lighthouse/pension_benefit_intake_job_spec.rb @department-of-veterans-affairs/pensions @department-of-veterans-affairs/backend-review-group spec/sidekiq/mhv @department-of-veterans-affairs/vfs-mhv-medical-records @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/sidekiq/pager_duty @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/sidekiq/preneeds @department-of-veterans-affairs/mbs-core-team @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group @@ -1263,8 +1265,8 @@ spec/lib/bid @department-of-veterans-affairs/Disability-Experience @department-o spec/lib/bip_claims @department-of-veterans-affairs/Benefits-Team-1 @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/lib/breakers @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/lib/carma @department-of-veterans-affairs/vfs-10-10 @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group -spec/lib/caseflow @department-of-veterans-affairs/lighthouse-banana-peels -spec/lib/central_mail @department-of-veterans-affairs/lighthouse-banana-peels +spec/lib/caseflow @department-of-veterans-affairs/lighthouse-banana-peels @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group +spec/lib/central_mail @department-of-veterans-affairs/lighthouse-banana-peels @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/lib/chip @department-of-veterans-affairs/vsa-healthcare-health-quest-1-backend @department-of-veterans-affairs/patient-check-in @department-of-veterans-affairs/backend-review-group spec/lib/claim_status_tool @department-of-veterans-affairs/benefits-management-tools-be @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/lib/common/client/concerns/mhv_fhir_session_client_spec.rb @department-of-veterans-affairs/vfs-mhv-medical-records @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group @@ -1336,8 +1338,8 @@ spec/lib/okta/directory_service_spec.rb @department-of-veterans-affairs/lighthou spec/lib/olive_branch_patch_spec.rb @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/lib/pagerduty @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/lib/pdf_fill @department-of-veterans-affairs/backend-review-group -spec/lib/pdf_info/metadata_spec.rb @department-of-veterans-affairs/lighthouse-banana-peels -spec/lib/pdf_utilities @department-of-veterans-affairs/lighthouse-banana-peels +spec/lib/pdf_info/metadata_spec.rb @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group +spec/lib/pdf_utilities @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/lib/pension_burial @department-of-veterans-affairs/mbs-core-team @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/lib/preneeds @department-of-veterans-affairs/mbs-core-team @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/lib/rx @department-of-veterans-affairs/vfs-mhv-medications @@ -1367,7 +1369,7 @@ spec/lib/slack/service_spec.rb @department-of-veterans-affairs/va-api-engineers spec/lib/sm @department-of-veterans-affairs/vfs-mhv-secure-messaging @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/lib/string_helpers_spec.rb @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/lib/tasks/support/schema_camelizer_spec.rb @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group -spec/lib/token_validation @department-of-veterans-affairs/lighthouse-dash @department-of-veterans-affairs/lighthouse-banana-peels +spec/lib/token_validation @department-of-veterans-affairs/lighthouse-dash @department-of-veterans-affairs/lighthouse-banana-peels @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/lib/user_profile_attribute_service_spec.rb @department-of-veterans-affairs/vfs-authenticated-experience-backend @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/lib/va_profile @department-of-veterans-affairs/vfs-authenticated-experience-backend @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/lib/vbs @department-of-veterans-affairs/vsa-debt-resolution @department-of-veterans-affairs/backend-review-group @@ -1381,9 +1383,9 @@ spec/mailers/create_staging_spool_files_mailer_spec.rb @department-of-veterans-a spec/mailers/dependents_application_failure_mailer_spec.rb @department-of-veterans-affairs/vfs-authenticated-experience-backend @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/mailers/direct_deposit_mailer_spec.rb @department-of-veterans-affairs/vfs-authenticated-experience-backend @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/mailers/hca_submission_failure_mailer_spec.rb @department-of-veterans-affairs/vfs-authenticated-experience-backend @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group -spec/mailers/previews/appeals_api_daily_error_report_mailer_preview.rb @department-of-veterans-affairs/lighthouse-banana-peels +spec/mailers/previews/appeals_api_daily_error_report_mailer_preview.rb @department-of-veterans-affairs/lighthouse-banana-peels @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/mailers/previews/appeals_api_decision_review_preview.rb @department-of-veterans-affairs/lighthouse-banana-peels @department-of-veterans-affairs/vfs-authenticated-experience-backend @department-of-veterans-affairs/backend-review-group -spec/mailers/previews/appeals_api_weekly_error_report_mailer_preview.rb @department-of-veterans-affairs/lighthouse-banana-peels +spec/mailers/previews/appeals_api_weekly_error_report_mailer_preview.rb @department-of-veterans-affairs/lighthouse-banana-peels @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/mailers/previews/ch31_submissions_report_mailer_preview.rb @department-of-veterans-affairs/benefits-non-disability @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/mailers/previews/claims_api_submission_report_mailer_preview.rb @department-of-veterans-affairs/lighthouse-dash spec/mailers/previews/claims_api_unsuccessful_report_mailer_preview.rb @department-of-veterans-affairs/lighthouse-dash @@ -1422,7 +1424,6 @@ spec/models/form1010cg @department-of-veterans-affairs/vfs-10-10 @department-of- spec/models/form1095_b_spec.rb @department-of-veterans-affairs/vfs-1095-b @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/models/form526_job_status_spec.rb @department-of-veterans-affairs/Disability-Experience @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/models/form526_submission_spec.rb @department-of-veterans-affairs/Disability-Experience @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group -spec/models/form5655_submission_spec.rb @department-of-veterans-affairs/vsa-debt-resolution @department-of-veterans-affairs/backend-review-group spec/models/form_attachment_spec.rb @department-of-veterans-affairs/vfs-authenticated-experience-backend @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/models/form_profile_spec.rb @department-of-veterans-affairs/vfs-authenticated-experience-backend @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/models/form_submission_spec.rb @department-of-veterans-affairs/platform-va-product-forms @department-of-veterans-affairs/Disability-Experience @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group @@ -1460,7 +1461,7 @@ spec/models/user_session_form_spec.rb @department-of-veterans-affairs/octo-ident spec/models/user_spec.rb @department-of-veterans-affairs/octo-identity spec/models/user_verification_spec.rb @department-of-veterans-affairs/octo-identity spec/models/va_profile_redis @department-of-veterans-affairs/vfs-authenticated-experience-backend @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group -spec/models/webhooks @department-of-veterans-affairs/lighthouse-banana-peels +spec/models/webhooks @department-of-veterans-affairs/lighthouse-banana-peels @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/policies/appeals_policy_spec.rb @department-of-veterans-affairs/backend-review-group spec/policies/bgs_policy_spec.rb @department-of-veterans-affairs/benefits-dependents-management @department-of-veterans-affairs/backend-review-group @department-of-veterans-affairs/va-api-engineers spec/policies/ch33_dd_policy_spec.rb @department-of-veterans-affairs/vfs-authenticated-experience-backend @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group @@ -1570,8 +1571,6 @@ spec/serializers/lighthouse @department-of-veterans-affairs/vfs-facilities spec/serializers/message_serializer_spec.rb @department-of-veterans-affairs/vfs-authenticated-experience-backend @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/serializers/personal_information_serializer_spec.rb @department-of-veterans-affairs/vfs-authenticated-experience-backend @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/serializers/receive_application_serializer_spec.rb @department-of-veterans-affairs/mbs-core-team @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group -spec/serializers/sign_in @department-of-veterans-affairs/octo-identity -spec/serializers/sign_in/introspect_serializer_spec.rb @department-of-veterans-affairs/octo-identity spec/serializers/triage_team_serializer_spec.rb @department-of-veterans-affairs/vfs-mhv-secure-messaging @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/serializers/user_serializer_spec.rb @department-of-veterans-affairs/octo-identity spec/serializers/user_verification_serializer_spec.rb @department-of-veterans-affairs/octo-identity @@ -1799,8 +1798,8 @@ spec/support/vcr_cassettes/bgs/benefit_claim @department-of-veterans-affairs/ben spec/support/vcr_cassettes/bid @department-of-veterans-affairs/Disability-Experience @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/support/vcr_cassettes/brd @department-of-veterans-affairs/lighthouse-dash @department-of-veterans-affairs/dbex-trex @department-of-veterans-affairs/Disability-Experience spec/support/vcr_cassettes/carma @department-of-veterans-affairs/vfs-10-10 @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group -spec/support/vcr_cassettes/central_mail @department-of-veterans-affairs/lighthouse-banana-peels -spec/support/vcr_cassettes/caseflow @department-of-veterans-affairs/lighthouse-banana-peels +spec/support/vcr_cassettes/central_mail @department-of-veterans-affairs/lighthouse-banana-peels @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group +spec/support/vcr_cassettes/caseflow @department-of-veterans-affairs/lighthouse-banana-peels @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/support/vcr_cassettes/check_in @department-of-veterans-affairs/vsa-healthcare-health-quest-1-backend @department-of-veterans-affairs/patient-check-in @department-of-veterans-affairs/backend-review-group @department-of-veterans-affairs/va-api-engineers spec/support/vcr_cassettes/claims_api @department-of-veterans-affairs/lighthouse-dash spec/support/vcr_cassettes/complex_interaction @department-of-veterans-affairs/octo-identity @@ -1875,7 +1874,7 @@ spec/support/vcr_cassettes/slack/slack_bot_notify.yml @department-of-veterans-af spec/support/vcr_cassettes/sm_client @department-of-veterans-affairs/vfs-mhv-secure-messaging @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/support/vcr_cassettes/spec/support @department-of-veterans-affairs/octo-identity spec/support/vcr_cassettes/staccato @department-of-veterans-affairs/vfs-10-10 @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group -spec/support/vcr_cassettes/token_validation @department-of-veterans-affairs/lighthouse-dash @department-of-veterans-affairs/lighthouse-banana-peels +spec/support/vcr_cassettes/token_validation @department-of-veterans-affairs/lighthouse-dash @department-of-veterans-affairs/lighthouse-banana-peels @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/spupport/vcr_cassettes/user/get_facilities_empty.yml @department-of-veterans-affairs/vfs-facilities-frontend @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/support/vcr_cassettes/va_forms @department-of-veterans-affairs/platform-va-product-forms @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/support/vcr_cassettes/va_notify @department-of-veterans-affairs/va-notify-write @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group diff --git a/.github/scripts/validate-datadog-yaml/datadog-schema2.2.json b/.github/scripts/validate-datadog-yaml/datadog-schema2.2.json new file mode 100644 index 00000000000..1884127ac02 --- /dev/null +++ b/.github/scripts/validate-datadog-yaml/datadog-schema2.2.json @@ -0,0 +1,208 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://github.com/DataDog/schema/tree/main/service-catalog/v2.2/schema.json", + "title": "Service Definition Schema v2.2", + "description": "A service definition for providing additional service metadata and integrations v2.2", + "type": "object", + "properties": { + "schema-version": { + "description": "Schema version being used", + "examples": ["v2.2"], + "type": "string", + "default": "v2.2", + "enum": ["v2.2"] + }, + "dd-service": { + "description": "Unique identifier of the service. Must be unique across all services, and is used to match with a service in Datadog", + "examples": ["my-service"], + "type": "string" + }, + "team": { + "description": "Team that owns the service. It is used to locate a team defined in Datadog Teams if it exists", + "examples": ["my-team"], + "type": "string" + }, + "application": { + "description": "Identifier for a group of related services serving a product feature, which the service is a part of", + "examples": ["my-app"], + "type": "string" + }, + "description": { + "description": "A short description of the service", + "examples": ["My app description"], + "type": "string" + }, + "tier": { + "description": "Importance of the service", + "examples": ["1", "High"], + "type": "string" + }, + "lifecycle": { + "description": "The current life cycle phase of the service.", + "examples": ["sandbox", "staging", "production", "deprecated"], + "type": "string" + }, + "type": { + "description": "The type of service", + "examples": ["web", "db", "cache", "function", "browser", "mobile", "custom"], + "type": "string", + "enum": ["web", "db", "cache", "function", "browser", "mobile", "custom"] + }, + "languages": { + "description": "The service's programming language. See examples for a list of recognizable languages", + "examples": [["dotnet", "go", "java", "js", "php", "python", "ruby", "c++"]], + "type": "array", + "items": { + "type": "string" + } + }, + "contacts": { + "description": "A list of contacts related to the services. ", + "type": "array", + "items": { + "$ref": "#/$defs/contact" + } + }, + "links": { + "description": "A list of links related to the services. ", + "type": "array", + "items": { + "$ref": "#/$defs/link" + } + }, + "tags": { + "description": "A set of custom tags", + "examples": [["my:tag"]], + "type": "array", + "items": { + "type": "string" + } + }, + "integrations": { + "description": "Third party integrations that Datadog supports", + "type": "object", + "properties": { + "pagerduty": { + "description": "Pagerduty integration for the service", + "type": "object", + "properties": { + "service-url": { + "description": "Pagerduty Service URL", + "examples": ["https://my-org.pagerduty.com/service-directory/PMyService"], + "type": "string", + "pattern": "^(https?://)?[a-zA-Z\\d_\\-.]+\\.pagerduty\\.com/service-directory/(P[a-zA-Z\\d_\\-]+)/?$" + } + }, + "required": ["service-url"], + "additionalProperties": false + }, + "opsgenie": { + "description": "Opsgenie integration for the service", + "type": "object", + "properties": { + "service-url": { + "description": "Opsgenie Service URL", + "examples": ["https://www.opsgenie.com/service/123e4567-e89b-12d3-a456-426614174000"], + "type": "string", + "pattern": "^(https?://)?[a-zA-Z\\d_\\-.]+\\.opsgenie\\.com/service/([a-zA-Z\\d_\\-]+)/?$" + }, + "region": { + "description": "Opsgenie Instance Region", + "type": "string", + "examples": ["US", "EU"], + "enum": ["US", "EU"] + } + }, + "required": ["service-url"], + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "extensions": { + "description": "Custom extensions", + "type": "object", + "additionalProperties": true + }, + "ci-pipeline-fingerprints": { + "description": "A set of CI pipeline fingerprints related to the service", + "examples": [["j88xdEy0J5lc", "eZ7LMljCk8vo"]], + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false, + "required": ["schema-version", "dd-service"], + "$defs": { + "link": { + "additionalProperties": false, + "type": "object", + "properties": { + "name": { + "description": "Link name", + "examples": ["Runbook", "Dashboard"], + "type": "string" + }, + "type": { + "description": "Link type. See examples for a list of recognizable types", + "examples": ["runbook", "doc", "repo", "dashboard", "other"], + "type": "string", + "default": "other" + }, + "url": { + "description": "Link url", + "examples": ["https://my-runbook"], + "type": "string", + "format": "uri" + }, + "provider": { + "description": "Link provider. See examples for a list of recognizable providers", + "examples": ["Github", "Confluence"], + "type": "string" + } + }, + "required": [ + "name", + "type", + "url" + ] + }, + "contact": { + "additionalProperties": false, + "type": "object", + "properties": { + "name": { + "description": "Contact name", + "examples": ["Oncall Slack", "Team Email"], + "type": "string", + "minLength": 2 + }, + "type": { + "description": "Contact type. See examples for a list of recognizable types", + "examples": ["email", "slack", "microsoft-teams"], + "type": "string" + }, + "contact": { + "description": "Contact value", + "examples": ["contact@datadoghq.com", "https://my-org.slack.com/archives/my-channel"], + "type": "string" + } + }, + "if": { + "properties": {"type": {"const": "email"}} + }, + "then": { + "properties": {"contact": {"format": "email"}} + }, + "else": { + "properties": {"contact": {"format": "uri"}} + }, + "required": [ + "type", + "contact" + ] + } + } +} diff --git a/.github/workflows/build_atlas_codeowners.yml b/.github/workflows/build_atlas_codeowners.yml index e84b16c11f8..30f6d5c2eb8 100644 --- a/.github/workflows/build_atlas_codeowners.yml +++ b/.github/workflows/build_atlas_codeowners.yml @@ -30,7 +30,7 @@ jobs: - name: Check codeowner changes id: check-codeowner-changes - uses: tj-actions/changed-files@v43 # v36.3.0 + uses: tj-actions/changed-files@v44 # v36.3.0 with: files: | .github/CODEOWNERS diff --git a/.github/workflows/datadog_service_catalog_update.yml b/.github/workflows/datadog_service_catalog_update.yml index 22c4f350a92..25d59c0f88b 100644 --- a/.github/workflows/datadog_service_catalog_update.yml +++ b/.github/workflows/datadog_service_catalog_update.yml @@ -45,7 +45,7 @@ jobs: cmd: yq '.' ${{matrix.file}} - name: 'Update Service Catalog in DataDog' run: | - curl --location --request POST 'https://api.ddog-gov.com/api/v2/services/definitions?schema_version=v2.1' \ + curl --location --request POST 'https://api.ddog-gov.com/api/v2/services/definitions?schema_version=v2.2' \ --header 'Accept: application/json' \ --header 'DD-API-KEY: ${{ secrets.DD_API_KEY }}' \ --header 'DD-APPLICATION-KEY: ${{ secrets.DD_APP_KEY }}' \ diff --git a/.github/workflows/validate-datadog-changes.yml b/.github/workflows/validate-datadog-changes.yml index 592a499727f..6c7660399f5 100644 --- a/.github/workflows/validate-datadog-changes.yml +++ b/.github/workflows/validate-datadog-changes.yml @@ -31,4 +31,4 @@ jobs: working-directory: ./.github/scripts/validate-datadog-yaml run: | files=("${{steps.get_filenames.outputs.changedFiles}}") - python validate_yaml.py -s datadog-schema2.1.json -F $files + python validate_yaml.py -s datadog-schema2.2.json -F $files diff --git a/.gitignore b/.gitignore index defd29c8e9c..53bf694cd23 100644 --- a/.gitignore +++ b/.gitignore @@ -91,9 +91,10 @@ test_users.csv # Ignore .DS_STORE files created by Mac OS **/.DS_STORE -# Ignore VIM artifact files +# Ignore VIM/vscode artifact files .ignore *.swp +.vscode # Ignore node_modules for development tools node_modules diff --git a/.rubocop_explicit_enables.yml b/.rubocop_explicit_enables.yml index 09a6d631d60..96e84e4a523 100644 --- a/.rubocop_explicit_enables.yml +++ b/.rubocop_explicit_enables.yml @@ -284,11 +284,11 @@ FactoryBot/FactoryNameStyle: # new in 2.16 Enabled: true FactoryBot/SyntaxMethods: # new in 2.7 Enabled: true -RSpec/Rails/AvoidSetupHook: # new in 2.4 +RSpecRails/AvoidSetupHook: # new in 2.4 Enabled: true -RSpec/Rails/HaveHttpStatus: # new in 2.12 +RSpecRails/HaveHttpStatus: # new in 2.12 Enabled: true -RSpec/Rails/InferredSpecType: # new in 2.14 +RSpecRails/InferredSpecType: # new in 2.14 Enabled: true Layout/EmptyLinesAroundAttributeAccessor: diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index c96b9f3c6d0..9ec0afa9657 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -750,7 +750,7 @@ RSpec/PendingWithoutReason: # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: ResponseMethods. # ResponseMethods: response, last_response -RSpec/Rails/HaveHttpStatus: +RSpecRails/HaveHttpStatus: Exclude: - 'modules/appeals_api/spec/controllers/application_controller_spec.rb' - 'modules/appeals_api/spec/requests/higher_level_reviews/v0/higher_level_reviews_controller_spec.rb' @@ -837,7 +837,7 @@ RSpec/Rails/HaveHttpStatus: # Offense count: 264 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: Inferences. -RSpec/Rails/InferredSpecType: +RSpecRails/InferredSpecType: Enabled: false # Offense count: 26 diff --git a/Gemfile b/Gemfile index e1e589bd20f..a2181798caf 100644 --- a/Gemfile +++ b/Gemfile @@ -20,6 +20,7 @@ path 'modules' do gem 'facilities_api' gem 'health_quest' gem 'income_limits' + gem 'ivc_champva' gem 'meb_api' gem 'mobile' gem 'mocked_authentication' @@ -174,7 +175,7 @@ end group :test do gem 'apivore', git: 'https://github.com/department-of-veterans-affairs/apivore', tag: 'v2.0.0.vsp' - gem 'fakeredis' + gem 'mock_redis' gem 'pdf-inspector' gem 'rspec_junit_formatter' gem 'rspec-retry' @@ -215,7 +216,7 @@ group :development, :test do gem 'rubocop-rails' gem 'rubocop-rspec' gem 'rubocop-thread_safety' - gem 'sidekiq', '>= 6.4.0' + gem 'sidekiq', '~> 7.2.0' gem 'timecop' gem 'webmock' gem 'yard' diff --git a/Gemfile.lock b/Gemfile.lock index c2eec663b29..d517fa8e2d1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -73,10 +73,10 @@ GIT GIT remote: https://github.com/department-of-veterans-affairs/vets-json-schema - revision: d6918c7e0dce104ae9b06382a44df5deb56e2adf + revision: 0832461353f871b688b0c231ecf9bdbfb1f9145a branch: master specs: - vets_json_schema (21.5.2) + vets_json_schema (22.0.3) multi_json (~> 1.0) script_utils (= 0.0.4) @@ -104,6 +104,7 @@ PATH facilities_api (0.1.0) health_quest (0.1.0) income_limits (0.1.0) + ivc_champva (0.1.0) meb_api (0.1.0) mobile (0.1.0) dry-validation @@ -133,12 +134,13 @@ PATH GEM remote: https://enterprise.contribsys.com/ specs: - sidekiq-ent (2.5.3) - einhorn (>= 0.7.4) - sidekiq (>= 6.5.0, < 7) - sidekiq-pro (>= 5.5.0, < 6) - sidekiq-pro (5.5.8) - sidekiq (~> 6.0, >= 6.5.6) + sidekiq-ent (7.2.2) + einhorn (~> 1.0) + gserver + sidekiq (>= 7.2.0, < 8) + sidekiq-pro (>= 7.2.0, < 8) + sidekiq-pro (7.2.0) + sidekiq (>= 7.2.0, < 8) GEM remote: https://rubygems.org/ @@ -241,8 +243,8 @@ GEM attr_extras (7.1.0) awesome_print (1.9.2) aws-eventstream (1.3.0) - aws-partitions (1.899.0) - aws-sdk-core (3.191.4) + aws-partitions (1.903.0) + aws-sdk-core (3.191.5) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.651.0) aws-sigv4 (~> 1.8) @@ -250,7 +252,7 @@ GEM aws-sdk-kms (1.78.0) aws-sdk-core (~> 3, >= 3.191.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.146.0) + aws-sdk-s3 (1.146.1) aws-sdk-core (~> 3, >= 3.191.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.8) @@ -282,7 +284,7 @@ GEM bundler (>= 1.2.0, < 3) thor (~> 1.0) byebug (11.1.3) - carrierwave (3.0.6) + carrierwave (3.0.7) activemodel (>= 6.0.0) activesupport (>= 6.0.0) addressable (~> 2.6) @@ -415,10 +417,8 @@ GEM factory_bot_rails (6.4.3) factory_bot (~> 6.4) railties (>= 5.0.0) - faker (3.2.3) + faker (3.3.1) i18n (>= 1.8.11, < 2) - fakeredis (0.9.2) - redis (~> 4.8) faraday (2.9.0) faraday-net_http (>= 2.0, < 3.2) faraday-follow_redirects (0.3.0) @@ -438,7 +438,7 @@ GEM typhoeus (~> 1.4) faraday_curl (0.0.2) faraday (>= 0.9.0) - fastimage (2.3.0) + fastimage (2.3.1) ffi (1.16.3) ffi-compiler (1.0.1) ffi (>= 1.0.0) @@ -509,7 +509,7 @@ GEM thor (>= 0.20, < 2.a) google-cloud-env (2.1.1) faraday (>= 1.0, < 3.a) - google-protobuf (4.26.0) + google-protobuf (4.26.1) rake (>= 13) googleauth (1.11.0) faraday (>= 1.0, < 3.a) @@ -573,7 +573,7 @@ GEM jmespath (1.6.2) json (2.7.1) json (2.7.1-java) - json-schema (4.2.0) + json-schema (4.3.0) addressable (>= 2.8) json_schemer (2.2.1) base64 @@ -637,6 +637,7 @@ GEM mini_mime (1.1.5) mini_portile2 (2.8.5) minitest (5.22.3) + mock_redis (0.44.0) msgpack (1.7.2) msgpack (1.7.2-java) multi_json (1.15.0) @@ -662,7 +663,7 @@ GEM nio4r (2.7.0) nio4r (2.7.0-java) no_proxy_fix (0.1.2) - nokogiri (1.16.3) + nokogiri (1.16.4) mini_portile2 (~> 2.8.2) racc (~> 1.4) nori (2.6.0) @@ -694,7 +695,7 @@ GEM os (1.1.4) ox (2.14.18) parallel (1.24.0) - parallel_tests (4.5.2) + parallel_tests (4.6.1) parallel parser (3.3.0.5) ast (~> 2.4.1) @@ -813,9 +814,12 @@ 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 (5.1.0) + redis-client (>= 0.17.0) + redis-client (0.20.0) + connection_pool redis-namespace (1.11.0) redis (>= 4) regexp_parser (2.9.0) @@ -827,7 +831,7 @@ GEM uber (< 0.2.0) request_store (1.6.0) rack (>= 1.4) - restforce (7.3.0) + restforce (7.3.1) faraday (>= 1.1.0, < 2.10.0) faraday-follow_redirects (<= 0.3.0, < 1.0.0) faraday-multipart (>= 1.0.0, < 2.0.0) @@ -889,7 +893,7 @@ GEM rswag-ui (2.13.0) actionpack (>= 3.1, < 7.2) railties (>= 3.1, < 7.2) - rubocop (1.62.1) + rubocop (1.63.1) json (~> 2.3) language_server-protocol (>= 3.17.0) parallel (~> 1.10) @@ -907,15 +911,18 @@ GEM rubocop-factory_bot (2.25.1) rubocop (~> 1.41) rubocop-junit-formatter (0.1.4) - rubocop-rails (2.24.0) + rubocop-rails (2.24.1) activesupport (>= 4.2.0) rack (>= 1.1) rubocop (>= 1.33.0, < 2.0) rubocop-ast (>= 1.31.1, < 2.0) - rubocop-rspec (2.27.1) + rubocop-rspec (2.29.1) rubocop (~> 1.40) rubocop-capybara (~> 2.17) rubocop-factory_bot (~> 2.22) + rubocop-rspec_rails (~> 2.28) + rubocop-rspec_rails (2.28.2) + rubocop (~> 1.40) rubocop-thread_safety (0.5.1) rubocop (>= 0.90.0) ruby-progressbar (1.13.0) @@ -948,7 +955,7 @@ GEM rake (>= 10.0) semantic_logger (4.15.0) concurrent-ruby (~> 1.0) - sentry-ruby (5.17.1) + sentry-ruby (5.17.2) bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) shellany (0.0.1) @@ -957,10 +964,11 @@ GEM shrine (3.5.0) content_disposition (~> 1.0) down (~> 5.1) - sidekiq (6.5.12) - connection_pool (>= 2.2.5, < 3) - rack (~> 2.0) - redis (>= 4.5.0, < 5) + sidekiq (7.2.2) + concurrent-ruby (< 2) + connection_pool (>= 2.3.0) + rack (>= 2.2.4) + redis-client (>= 0.19.0) sidekiq_alive (2.4.0) gserver (~> 0.0.1) sidekiq (>= 5, < 8) @@ -1126,7 +1134,6 @@ DEPENDENCIES facilities_api! factory_bot_rails faker - fakeredis faraday (~> 2.9) faraday-follow_redirects faraday-httpclient @@ -1158,6 +1165,7 @@ DEPENDENCIES ice_nine income_limits! iso_country_codes + ivc_champva! json json-schema json_schemer @@ -1174,6 +1182,7 @@ DEPENDENCIES mimemagic mini_magick mobile! + mock_redis mocked_authentication! my_health! net-sftp @@ -1238,7 +1247,7 @@ DEPENDENCIES sentry-ruby shoulda-matchers shrine - sidekiq (>= 6.4.0) + sidekiq (~> 7.2.0) sidekiq-ent! sidekiq-pro! sidekiq_alive 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..5ead02cc210 100644 --- a/app/controllers/v0/burial_claims_controller.rb +++ b/app/controllers/v0/burial_claims_controller.rb @@ -6,20 +6,42 @@ module V0 class BurialClaimsController < ClaimsBaseController service_tag 'burial-application' + def show + submission_attempt = determine_submission_attempt + if submission_attempt + state = submission_attempt.aasm_state == 'failure' ? 'failure' : 'success' + render(json: { data: { attributes: { state: } } }) + elsif central_mail_submission + render(json: central_mail_submission) + else + Rails.logger.error("ActiveRecord::RecordNotFound: Claim submission not found for claim_id: #{params[:id]}") + render(json: { data: { attributes: { state: 'not found' } } }, status: :not_found) + end + rescue => e + Rails.logger.error(e.to_s) + render(json: { data: { attributes: { state: 'error processing request' } } }, status: :unprocessable_entity) + end + 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 @@ -31,5 +53,17 @@ def short_name def claim_class SavedClaim::Burial end + + private + + def determine_submission_attempt + claim = claim_class.find_by(guid: params[:id]) + form_submission = claim&.form_submissions&.last + form_submission&.form_submission_attempts&.last + end + + def central_mail_submission + CentralMailSubmission.joins(:central_mail_claim).find_by(saved_claims: { guid: params[:id] }) + end end 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/decision_review_evidences_controller.rb b/app/controllers/v0/decision_review_evidences_controller.rb index bcf6cf946ee..f1129baa18f 100644 --- a/app/controllers/v0/decision_review_evidences_controller.rb +++ b/app/controllers/v0/decision_review_evidences_controller.rb @@ -17,8 +17,6 @@ class DecisionReviewEvidencesController < ApplicationController def save_attachment_to_cloud! common_log_params = { key: :evidence_upload_to_s3, - # Will have to update this when NOD and SC using same LH API version. The beginning of that work is ticketed in - # https://github.com/department-of-veterans-affairs/va.gov-team/issues/66514. form_id: get_form_id_from_request_headers, user_uuid: current_user.uuid, downstream_system: 'AWS S3', @@ -41,7 +39,7 @@ def get_form_id_from_request_headers # - vets-website/src/platform/startup/setup.js (setUpCommonFunctionality) # - vets-website/src/platform/startup/index.js (startApp) source_app_name = request.headers['Source-App-Name'] - # The higher-level review form (966) is not included in this list because it does permit evidence uploads. + # The higher-level review form (996) is not included in this list because it does not permit evidence uploads. form_id = { '10182-board-appeal' => '10182', '995-supplemental-claim' => '995' diff --git a/app/controllers/v0/financial_status_reports_controller.rb b/app/controllers/v0/financial_status_reports_controller.rb deleted file mode 100644 index 86a884d9041..00000000000 --- a/app/controllers/v0/financial_status_reports_controller.rb +++ /dev/null @@ -1,163 +0,0 @@ -# frozen_string_literal: true - -require 'debts_api/v0/financial_status_report_service' - -module V0 - class FinancialStatusReportsController < ApplicationController - service_tag 'financial-report' - before_action { authorize :debt, :access? } - - rescue_from ::DebtsApi::V0::FinancialStatusReportService::FSRNotFoundInRedis, with: :render_not_found - - def create - render json: service.submit_financial_status_report(fsr_form) - end - - def download_pdf - send_data( - service.get_pdf, - type: 'application/pdf', - filename: 'VA Form 5655 - Submitted', - disposition: 'attachment' - ) - end - - private - - def render_not_found - render json: nil, status: :not_found - end - - def full_name - %i[first middle last] - end - - def address - %i[ - addressline_one - addressline_two - addressline_three - city - state_or_province - zip_or_postal_code - country_name - ] - end - - def name_amount - %i[name amount] - end - - # rubocop:disable Metrics/MethodLength - def fsr_form - params.permit( - streamlined: %i[ - value - type - ], - personal_identification: %i[fsr_reason ssn file_number], - personal_data: [ - :telephone_number, - :email, - :date_of_birth, - :married, - { ages_of_other_dependents: [], - veteran_full_name: full_name, - address:, - spouse_full_name: full_name, - employment_history: [ - :veteran_or_spouse, - :occupation_name, - :from, - :to, - :present, - :employer_name, - { employer_address: address } - ] } - ], - income: [ - :veteran_or_spouse, - :monthly_gross_salary, - :total_deductions, - :net_take_home_pay, - :total_monthly_net_income, - { deductions: [ - :taxes, - :retirement, - :social_security, - { other_deductions: name_amount } - ], - other_income: name_amount } - ], - expenses: [ - :rent_or_mortgage, - :food, - :utilities, - :other_living_expenses, - :expenses_installment_contracts_and_other_debts, - :total_monthly_expenses, - { other_living_expenses: name_amount } - ], - discretionary_income: %i[ - net_monthly_income_less_expenses - amount_can_be_paid_toward_debt - ], - assets: [ - :cash_in_bank, - :cash_on_hand, - :trailers_boats_campers, - :us_savings_bonds, - :stocks_and_other_bonds, - :real_estate_owned, - :total_assets, - { automobiles: %i[make model year resale_value], - other_assets: name_amount } - ], - installment_contracts_and_other_debts: [ - :creditor_name, - :date_started, - :purpose, - :original_amount, - :unpaid_balance, - :amount_due_monthly, - :amount_past_due, - { creditor_address: address } - ], - total_of_installment_contracts_and_other_debts: %i[ - original_amount - unpaid_balance - amount_due_monthly - amount_past_due - ], - additional_data: [ - :additional_comments, - { bankruptcy: %i[ - has_been_adjudicated_bankrupt - date_discharged - court_location - docket_number - ] } - ], - applicant_certifications: %i[ - veteran_signature - ], - selected_debts_and_copays: [ - :current_ar, - :debt_type, - :deduction_code, - :p_h_amt_due, - :p_h_dfn_number, - :p_h_cerner_patient_id, - :resolution_comment, - :resolution_option, - { station: [:facilit_y_num] } - ] - ).to_hash - end - # rubocop:enable Metrics/MethodLength - - def service - DebtsApi::V0::FinancialStatusReportService.new(current_user) - end - end -end diff --git a/app/controllers/v0/pension_claims_controller.rb b/app/controllers/v0/pension_claims_controller.rb index 79f039d112d..90f90c67eb6 100644 --- a/app/controllers/v0/pension_claims_controller.rb +++ b/app/controllers/v0/pension_claims_controller.rb @@ -1,5 +1,8 @@ # frozen_string_literal: true +require 'pension_21p527ez/tag_sentry' +require 'pension_21p527ez/monitor' + module V0 class PensionClaimsController < ClaimsBaseController service_tag 'pension-application' @@ -12,64 +15,50 @@ def claim_class SavedClaim::Pension end - # rubocop:disable Metrics/MethodLength def show - claim = claim_class.find_by!({ guid: params[:id] }) # will raise ActiveRecord::NotFound - + claim = claim_class.find_by!(guid: params[:id]) # will raise ActiveRecord::NotFound form_submission = claim.form_submissions&.order(id: :asc)&.last submission_attempt = form_submission&.form_submission_attempts&.order(created_at: :asc)&.last - if submission_attempt # this is to satisfy frontend check for successful submission state = submission_attempt.aasm_state == 'failure' ? 'failure' : 'success' - response = { - data: { - id: claim.id, - form_id: claim.form_id, - guid: claim.guid, - attributes: { - state:, - benefits_intake_uuid: form_submission.benefits_intake_uuid, - form_type: form_submission.form_type, - attempt_id: submission_attempt.id, - aasm_state: submission_attempt.aasm_state - } - } - } + response = format_show_response(claim, state, form_submission, submission_attempt) end - render(json: response) + rescue ActiveRecord::RecordNotFound => e + pension_monitor.track_show404(params[:id], current_user, e) + render(json: { error: e.to_s }, status: :not_found) + rescue => e + pension_monitor.track_show_error(params[:id], current_user, e) + raise e end - # rubocop:enable Metrics/MethodLength # 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 - Rails.logger.info("Begin #{claim.class::FORM} Submission", { guid: claim.guid, user_uuid: }) + pension_monitor.track_create_attempt(claim, current_user) in_progress_form = current_user ? InProgressForm.form_for_user(claim.form_id, current_user) : nil claim.itf_datetime = in_progress_form.created_at if in_progress_form unless claim.save - StatsD.increment("#{stats_key}.failure") + pension_monitor.track_create_error(in_progress_form, claim, current_user) log_validation_error_to_metadata(in_progress_form, claim) - Rails.logger.error("Submit #{claim.class::FORM} Failed for user_id: #{user_uuid}", - { in_progress_form_id: in_progress_form&.id, errors: claim&.errors&.errors }) raise Common::Exceptions::ValidationErrors, claim.errors end - claim.upload_to_lighthouse + claim.upload_to_lighthouse(current_user) - StatsD.increment("#{stats_key}.success") - Rails.logger.info("Submit #{claim.class::FORM} Success", - { confirmation_number: claim.confirmation_number, user_uuid: }) + pension_monitor.track_create_success(in_progress_form, claim, current_user) clear_saved_form(claim.form_id) render(json: claim) + rescue => e + pension_monitor.track_create_error(in_progress_form, claim, current_user, e) + raise e end private @@ -81,5 +70,26 @@ def log_validation_error_to_metadata(in_progress_form, claim) metadata['submission']['error_message'] = claim&.errors&.errors&.to_s in_progress_form.update(metadata:) end + + def format_show_response(claim, state, form_submission, submission_attempt) + { + data: { + id: claim.id, + form_id: claim.form_id, + guid: claim.guid, + attributes: { + state:, + benefits_intake_uuid: form_submission.benefits_intake_uuid, + form_type: form_submission.form_type, + attempt_id: submission_attempt.id, + aasm_state: submission_attempt.aasm_state + } + } + } + end + + def pension_monitor + Pension21p527ez::Monitor.new + end end end diff --git a/app/controllers/v0/profile/direct_deposits_controller.rb b/app/controllers/v0/profile/direct_deposits_controller.rb new file mode 100644 index 00000000000..010be5c416a --- /dev/null +++ b/app/controllers/v0/profile/direct_deposits_controller.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +require 'lighthouse/service_exception' +require 'lighthouse/direct_deposit/client' +require 'lighthouse/direct_deposit/error_parser' +require 'lighthouse/direct_deposit/payment_account' +require 'lighthouse/direct_deposit/control_information' + +module V0 + module Profile + class DirectDepositsController < ApplicationController + service_tag 'direct-deposit' + before_action { authorize :lighthouse, :direct_deposit_access? } + before_action :payment_account, only: :update + before_action :control_information, only: :update + after_action :log_sso_info, only: :update + + rescue_from(*Lighthouse::ServiceException::ERROR_MAP.values) do |exception| + error = { status: exception.status_code, body: exception.errors.first } + response = Lighthouse::DirectDeposit::ErrorParser.parse(error) + + render status: response.status, json: response.body + end + + def show + response = client.get_payment_info + + render status: response.status, + json: response.body, + serializer: DisabilityCompensationsSerializer + end + + def update + response = client.update_payment_info(@payment_account) + send_confirmation_email + + render status: response.status, + json: response.body, + serializer: DisabilityCompensationsSerializer + end + + private + + def client + @client ||= DirectDeposit::Client.new(@current_user.icn) + end + + def payment_account + @payment_account ||= Lighthouse::DirectDeposit::PaymentAccount.new(payment_account_params) + end + + def control_information + @control_information ||= Lighthouse::DirectDeposit::ControlInformation.new(control_info_params) + end + + def payment_account_params + params.require(:payment_account) + .permit(:account_type, + :account_number, + :routing_number) + end + + def control_info_params + params.require(:control_information) + .permit(:can_update_direct_deposit, + :is_corp_available, + :is_edu_claim_available, + :is_corp_rec_found, + :has_no_bdn_payments, + :has_index, + :is_competent, + :has_mailing_address, + :has_no_fiduciary_assigned, + :is_not_deceased, + :has_payment_address, + :has_indentity) + end + + def send_confirmation_email + VANotifyDdEmailJob.send_to_emails(current_user.all_emails, 'comp_and_pen') + end + end + end +end diff --git a/app/controllers/v0/rated_disabilities_discrepancies_controller.rb b/app/controllers/v0/rated_disabilities_discrepancies_controller.rb index f6229836a96..75daaf5a43d 100644 --- a/app/controllers/v0/rated_disabilities_discrepancies_controller.rb +++ b/app/controllers/v0/rated_disabilities_discrepancies_controller.rb @@ -15,12 +15,9 @@ def show lh_response = get_lh_rated_disabilities evss_response = get_evss_rated_disabilities - lh_response_length = lh_response.dig('data', 'attributes', 'individual_ratings').length - evss_response_length = evss_response.rated_disabilities.length - - if lh_response_length != evss_response_length - log_length_discrepancy((lh_response_length - evss_response_length).abs) - end + lh_ratings = lh_response.dig('data', 'attributes', 'individual_ratings') + evss_ratings = evss_response.rated_disabilities + log_length_discrepancy(evss_ratings, lh_ratings) if lh_ratings.length != evss_ratings.length # This doesn't need to return anything at the moment render json: nil @@ -28,12 +25,16 @@ def show private - def log_length_discrepancy(difference) - message = "Discrepancy of #{difference} disability ratings" + def log_length_discrepancy(evss_ratings, lh_ratings) + message = 'Discrepancy between Lighthouse and EVSS disability ratings' ::Rails.logger.info(message, { message_type: 'lh.rated_disabilities.length_discrepancy', - revision: 4 + evss_length: evss_ratings.length, + evss_rating_ids: evss_ratings.pluck('rated_disability_id'), + lighthouse_length: lh_ratings.length, + lighthouse_rating_ids: lh_ratings.pluck('disability_rating_id'), + revision: 5 }) end diff --git a/app/controllers/v0/sign_in_controller.rb b/app/controllers/v0/sign_in_controller.rb index c03ede2794a..bcc43b4e6e5 100644 --- a/app/controllers/v0/sign_in_controller.rb +++ b/app/controllers/v0/sign_in_controller.rb @@ -223,12 +223,6 @@ def logingov_logout_proxy render json: { errors: e }, status: :bad_request end - def introspect - render json: @current_user, serializer: SignIn::IntrospectSerializer, status: :ok - rescue SignIn::Errors::StandardError => e - render json: { errors: e }, status: :unauthorized - end - private def validate_authorize_params(type, client_id, acr, operation) @@ -318,7 +312,9 @@ def create_login_code(state_payload, user_info, credential_level) # rubocop:disa user_attributes = auth_service(state_payload.type, state_payload.client_id).normalized_attributes(user_info, credential_level) verified_icn = SignIn::AttributeValidator.new(user_attributes:).perform - user_code_map = create_user_code_map(user_attributes, state_payload, verified_icn, request.remote_ip) + user_code_map = SignIn::UserCodeMapCreator.new( + user_attributes:, state_payload:, verified_icn:, request_ip: request.remote_ip + ).perform context = { type: state_payload.type, @@ -345,16 +341,6 @@ def create_login_code(state_payload, user_info, credential_level) # rubocop:disa content_type: 'text/html' end - def create_user_code_map(user_attributes, state_payload, verified_icn, request_ip) - klass = if state_payload.client_id == Settings.sign_in.arp_client_id - AccreditedRepresentativePortal::RepresentativeUserCreator - else - SignIn::UserCreator - end - - klass.new(user_attributes:, state_payload:, verified_icn:, request_ip:).perform - end - def refresh_token_param params[:refresh_token] || token_cookies[SignIn::Constants::Auth::REFRESH_TOKEN_COOKIE_NAME] end diff --git a/app/controllers/v0/terms_of_use_agreements_controller.rb b/app/controllers/v0/terms_of_use_agreements_controller.rb index 7b40deca77c..dcdc7012ee5 100644 --- a/app/controllers/v0/terms_of_use_agreements_controller.rb +++ b/app/controllers/v0/terms_of_use_agreements_controller.rb @@ -42,8 +42,10 @@ def update_provisioning mpi_gcids: current_user.mpi_gcids) if provisioner.perform create_cerner_cookie + Rails.logger.info('[TermsOfUseAgreementsController] update_provisioning success', { icn: current_user.icn }) render json: { provisioned: true }, status: :ok else + Rails.logger.error('[TermsOfUseAgreementsController] update_provisioning error', { icn: current_user.icn }) render_error('Failed to provision') end rescue TermsOfUse::Errors::ProvisionerError => e diff --git a/app/controllers/v1/pension_ipf_callbacks_controller.rb b/app/controllers/v1/pension_ipf_callbacks_controller.rb new file mode 100644 index 00000000000..20e5cd322e8 --- /dev/null +++ b/app/controllers/v1/pension_ipf_callbacks_controller.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +require 'decision_review_v1/utilities/logging_utils' + +module V1 + class PensionIpfCallbacksController < ApplicationController + include ActionController::HttpAuthentication::Token::ControllerMethods + include DecisionReviewV1::Appeals::LoggingUtils + + service_tag 'pension-ipf-callbacks' + + skip_before_action :verify_authenticity_token, only: [:create] + skip_before_action :authenticate, only: [:create] + skip_after_action :set_csrf_header, only: [:create] + before_action :authenticate_header, only: [:create] + + STATUSES_TO_IGNORE = %w[sent delivered temporary-failure].freeze + + def create + return render json: nil, status: :not_found unless Flipper.enabled? :pension_ipf_callbacks_endpoint + + payload = JSON.parse(request.body.string) + + # save encrypted request body in database table for non-successful notifications + payload_status = payload['status']&.downcase + if STATUSES_TO_IGNORE.exclude? payload_status + begin + PensionIpfNotification.create!(payload:) + rescue ActiveRecord::RecordInvalid => e + log_formatted(**log_params(payload).merge(is_success: false), params: { exception_message: e.message }) + return render json: { message: 'failed' } + end + end + + log_formatted(**log_params(payload).merge(is_success: true)) + render json: { message: 'success' } + end + + private + + def authenticate_header + authenticate_user_with_token || authenticity_error + end + + def authenticate_user_with_token + Rails.logger.info('pension-ipf-callbacks-69766 - Received request, authenticating') + authenticate_with_http_token do |token| + # TODO: Temp logging for debugging Staging issue. Remove after testing + Rails.logger.info("pension-ipf-callbacks-69766 - Expecting #{bearer_token_secret}") + Rails.logger.info("pension-ipf-callbacks-69766 - Length: #{bearer_token_secret.length}") + Rails.logger.info("pension-ipf-callbacks-69766 - Received #{token}") + Rails.logger.info("pension-ipf-callbacks-69766 - Length: #{token.length}") + return false if bearer_token_secret.nil? + + Rails.logger.info("pension-ipf-callbacks-69766 - Is equal?: #{token == bearer_token_secret}") + token == bearer_token_secret + end + end + + def authenticity_error + Rails.logger.info('pension-ipf-callbacks-69766 - Failed to authenticate request') + render json: { message: 'Invalid credentials' }, status: :unauthorized + end + + def bearer_token_secret + Settings.dig(:pension_ipf_vanotify_status_callback, :bearer_token) + end + + def log_params(payload) + { + key: :callbacks, + form_id: '21P-527EZ', + user_uuid: nil, + upstream_system: 'VANotify', + body: payload.merge('to' => '') # scrub PII from logs + } + end + end +end diff --git a/app/controllers/v1/sessions_controller.rb b/app/controllers/v1/sessions_controller.rb index bf080f29087..e0ebb14ceee 100644 --- a/app/controllers/v1/sessions_controller.rb +++ b/app/controllers/v1/sessions_controller.rb @@ -231,8 +231,7 @@ def login_params(type) url_service.login_url( 'logingov', [IAL::LOGIN_GOV_IAL2, AAL::LOGIN_GOV_AAL2], - AuthnContext::LOGIN_GOV, - AuthnContext::MINIMUM + AuthnContext::LOGIN_GOV ) when 'logingov_signup' url_service.logingov_signup_url([IAL::LOGIN_GOV_IAL1, AAL::LOGIN_GOV_AAL2]) diff --git a/app/models/concerns/form526_claim_fast_tracking_concern.rb b/app/models/concerns/form526_claim_fast_tracking_concern.rb index c48e89e5877..d09d1793f8d 100644 --- a/app/models/concerns/form526_claim_fast_tracking_concern.rb +++ b/app/models/concerns/form526_claim_fast_tracking_concern.rb @@ -13,10 +13,16 @@ module Form526ClaimFastTrackingConcern MAX_CFI_STATSD_KEY_PREFIX = 'api.max_cfi' EP_MERGE_STATSD_KEY_PREFIX = 'worker.ep_merge' - DISABILITIES_WITH_MAX_CFI = [ClaimFastTracking::DiagnosticCodes::TINNITUS].freeze EP_MERGE_BASE_CODES = %w[010 110 020 030 040].freeze EP_MERGE_SPECIAL_ISSUE = 'EMP' - OPEN_STATUSES = ['CLAIM RECEIVED', 'UNDER REVIEW', 'GATHERING OF EVIDENCE', 'REVIEW OF EVIDENCE'].freeze + OPEN_STATUSES = [ + 'CLAIM RECEIVED', + 'UNDER REVIEW', + 'GATHERING OF EVIDENCE', + 'REVIEW OF EVIDENCE', + 'CLAIM_RECEIVED', + 'INITIAL_REVIEW' + ].freeze def send_rrd_alert_email(subject, message, error = nil, to = Settings.rrd.alerts.recipients) RrdAlertMailer.build(self, subject, message, error, to).deliver_now @@ -85,6 +91,10 @@ def disabilities form.dig('form526', 'form526', 'disabilities') end + def increase_disabilities + disabilities.select { |disability| disability['disabilityActionType']&.upcase == 'INCREASE' } + end + def increase_only? disabilities.all? { |disability| disability['disabilityActionType']&.upcase == 'INCREASE' } end @@ -117,14 +127,17 @@ def prepare_for_ep_merge! pending_eps = open_claims.select do |claim| EP_MERGE_BASE_CODES.include?(claim['base_end_product_code']) && OPEN_STATUSES.include?(claim['status']) end - StatsD.distribution("#{EP_MERGE_STATSD_KEY_PREFIX}.pending_ep_count", pending_eps.count) + Rails.logger.info('EP Merge total open EPs', id:, count: pending_eps.count) return unless pending_eps.count == 1 date = Date.strptime(pending_eps.first['date'], '%m/%d/%Y') days_ago = (Time.zone.today - date).round - StatsD.distribution("#{EP_MERGE_STATSD_KEY_PREFIX}.pending_ep_age", days_ago) - - if Flipper.enabled?(:disability_526_ep_merge_api, User.find(user_uuid)) + feature_enabled = Flipper.enabled?(:disability_526_ep_merge_api, User.find(user_uuid)) + Rails.logger.info( + 'EP Merge open EP eligibility', + { id:, feature_enabled:, pending_ep_age: days_ago, pending_ep_status: pending_eps.first['status'] } + ) + if feature_enabled save_metadata(ep_merge_pending_claim_id: pending_eps.first['id']) add_ep_merge_special_issue! end @@ -179,18 +192,19 @@ def update_form_with_classification_code(classification_code) end def log_max_cfi_metrics_on_submit - DISABILITIES_WITH_MAX_CFI.intersection(diagnostic_codes).each do |diagnostic_code| - next unless disabilities.any? do |dis| - diagnostic_code == dis['diagnosticCode'] - end + user = User.find(user_uuid) + max_cfi_enabled = Flipper.enabled?(:disability_526_maximum_rating, user) ? 'on' : 'off' + ClaimFastTracking::DiagnosticCodesForMetrics::DC.each do |diagnostic_code| + next unless max_rated_diagnostic_codes_from_ipf.include?(diagnostic_code) - next unless max_rated_disabilities_from_ipf.any? do |dis| - diagnostic_code == dis['diagnostic_code'] - end + disability_claimed = diagnostic_codes.include?(diagnostic_code) - user = User.find(user_uuid) - max_cfi_enabled = Flipper.enabled?(:disability_526_maximum_rating, user) ? 'on' : 'off' - StatsD.increment("#{MAX_CFI_STATSD_KEY_PREFIX}.#{max_cfi_enabled}.submit.#{diagnostic_code}") + if disability_claimed + StatsD.increment("#{MAX_CFI_STATSD_KEY_PREFIX}.#{max_cfi_enabled}.submit.#{diagnostic_code}") + end + Rails.logger.info('Max CFI form526 submission', + id:, max_cfi_enabled:, disability_claimed:, diagnostic_code:, + total_increase_conditions: increase_disabilities.count) end rescue => e # Log the exception but but do not fail, otherwise form will not be submitted @@ -235,6 +249,10 @@ def max_rated_disabilities_from_ipf end end + def max_rated_diagnostic_codes_from_ipf + max_rated_disabilities_from_ipf.pluck('diagnostic_code') + end + # Fetch and memoize all of the veteran's open EPs. Establishing a new EP will make the memoized # value outdated if using the same Form526Submission instance. def open_claims diff --git a/app/models/form5655_submission.rb b/app/models/form5655_submission.rb deleted file mode 100644 index f3ea67c89cd..00000000000 --- a/app/models/form5655_submission.rb +++ /dev/null @@ -1,62 +0,0 @@ -# frozen_string_literal: true - -require 'user_profile_attribute_service' - -class Form5655Submission < ApplicationRecord - class StaleUserError < StandardError; end - - enum state: { unassigned: 0, in_progress: 1, submitted: 2, failed: 3 } - - validates :user_uuid, presence: true - belongs_to :user_account, dependent: nil, optional: true - has_kms_key - has_encrypted :form_json, :metadata, key: :kms_key, **lockbox_options - - def kms_encryption_context(*) - { - model_name: model_name.to_s, - model_id: id - } - end - - scope :streamlined, -> { where("(public_metadata -> 'streamlined' ->> 'value')::boolean") } - scope :not_streamlined, -> { where.not("(public_metadata -> 'streamlined' ->> 'value')::boolean") } - scope :streamlined_unclear, -> { where("(public_metadata -> 'streamlined') IS NULL") } - scope :streamlined_nil, lambda { - where("(public_metadata -> 'streamlined') IS NOT NULL and " \ - "(public_metadata -> 'streamlined' ->> 'value') IS NULL") - } - - def public_metadata - super || {} - end - - def form - @form_hash ||= JSON.parse(form_json) - end - - def user_cache_id - user = User.find(user_uuid) - raise StaleUserError, user_uuid unless user - - UserProfileAttributeService.new(user).cache_profile_attributes - end - - def submit_to_vba - DebtsApi::V0::Form5655::VBASubmissionJob.perform_async(id, user_cache_id) - end - - def submit_to_vha - DebtsApi::V0::Form5655::VHASubmissionJob.perform_async(id, user_cache_id) - end - - def register_failure(message) - failed! - update(error_message: message) - Rails.logger.error('Form5655Submission failed', message) - end - - def streamlined? - public_metadata.dig('streamlined', 'value') == true - end -end diff --git a/app/models/form_attachment.rb b/app/models/form_attachment.rb index 979087c2612..9230cb8f322 100644 --- a/app/models/form_attachment.rb +++ b/app/models/form_attachment.rb @@ -13,7 +13,7 @@ class FormAttachment < ApplicationRecord def set_file_data!(file, file_password = nil) attachment_uploader = get_attachment_uploader - file = unlock_pdf(file, file_password) if file_password.present? + file = unlock_pdf(file, file_password) if File.extname(file).downcase == '.pdf' && file_password.present? attachment_uploader.store!(file) self.file_data = { filename: attachment_uploader.filename }.to_json rescue CarrierWave::IntegrityError => e @@ -36,15 +36,13 @@ def get_file private def unlock_pdf(file, file_password) - return file unless File.extname(file) == '.pdf' - pdftk = PdfForms.new(Settings.binaries.pdftk) tmpf = Tempfile.new(['decrypted_form_attachment', '.pdf']) begin pdftk.call_pdftk(file.tempfile.path, 'input_pw', file_password, 'output', tmpf.path) rescue PdfForms::PdftkError => e - file_regex = %r{/(?:\w+/)*[\w-]+\.pdf\b} + file_regex = %r{/(?:\w+/)*[\w-]+\.pdf\b}i password_regex = /(input_pw).*?(output)/ sanitized_message = e.message.gsub(file_regex, '[FILTERED FILENAME]').gsub(password_regex, '\1 [FILTERED] \2') log_message_to_sentry(sanitized_message, 'warn') 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/in_progress_form.rb b/app/models/in_progress_form.rb index 24b08e1bc70..d4e938b9d64 100644 --- a/app/models/in_progress_form.rb +++ b/app/models/in_progress_form.rb @@ -129,7 +129,7 @@ def days_till_expires def default_expires_after case form_id - when '21-526EZ', '21P-527EZ' + when '21-526EZ', '21P-527EZ', '21P-530V2' 1.year else 60.days diff --git a/app/models/pension_ipf_notification.rb b/app/models/pension_ipf_notification.rb new file mode 100644 index 00000000000..8daccfc8c0d --- /dev/null +++ b/app/models/pension_ipf_notification.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require 'json_marshal/marshaller' + +class PensionIpfNotification < ApplicationRecord + serialize :payload, coder: JsonMarshal::Marshaller + + has_kms_key + has_encrypted :payload, key: :kms_key, **lockbox_options + + validates(:payload, presence: true) + + before_save :serialize_payload + + private + + def serialize_payload + self.payload = payload.to_json unless payload.is_a?(String) + end +end diff --git a/app/models/personal_information_log.rb b/app/models/personal_information_log.rb index 16c628a3588..c62c30516ee 100644 --- a/app/models/personal_information_log.rb +++ b/app/models/personal_information_log.rb @@ -2,14 +2,9 @@ class PersonalInformationLog < ApplicationRecord scope :last_week, -> { where('created_at >= :date', date: 1.week.ago) } - validates(:data, :error_class, presence: true) - # TODO: utility method for working with data persisted by logger middleware - # consider removing once we have determined how we are going to analyze the data - def decoded_data - return data unless data.key?('request_body') && data.key?('response_body') + has_kms_key + has_encrypted :data, migrating: true, type: :json, key: :kms_key, **lockbox_options - data.merge('request_body' => Base64.decode64(data['request_body']), - 'response_body' => Base64.decode64(data['response_body'])) - end + validates :error_class, presence: true end diff --git a/app/models/prescription_details.rb b/app/models/prescription_details.rb index 8c54dbcebe9..71c128d88ca 100644 --- a/app/models/prescription_details.rb +++ b/app/models/prescription_details.rb @@ -27,6 +27,10 @@ class PrescriptionDetails < Prescription attribute :tracking, Boolean attribute :orderable_item, String attribute :sorted_dispensed_date + attribute :shape, String + attribute :color, String + attribute :back_imprint, String + attribute :front_imprint, String def sorted_dispensed_date has_refills = try(:rx_rf_records).present? diff --git a/app/models/saved_claim.rb b/app/models/saved_claim.rb index 79498d6f1e4..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 @@ -102,6 +102,10 @@ def update_form(key, value) self.form = JSON.generate(application) end + def business_line + '' + end + private def attachment_keys diff --git a/app/models/saved_claim/burial.rb b/app/models/saved_claim/burial.rb index 141c77704b8..fd9b1f858f2 100644 --- a/app/models/saved_claim/burial.rb +++ b/app/models/saved_claim/burial.rb @@ -5,12 +5,26 @@ 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)) files.find_each { |f| f.update(saved_claim_id: id) } - - CentralMail::SubmitSavedClaimJob.new.perform(id) + if Flipper.enabled?(:central_mail_benefits_intake_submission) + Lighthouse::SubmitBenefitsIntakeClaim.new.perform(id) + else + CentralMail::SubmitSavedClaimJob.new.perform(id) + end end def regional_office @@ -18,10 +32,38 @@ def regional_office end def attachment_keys - %i[transportationReceipts deathCertificate].freeze + %i[transportationReceipts deathCertificate militarySeparationDocuments additionalEvidence].freeze end 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 process_pdf(pdf_path, timestamp = nil, form_id = nil) + processed_pdf = CentralMail::DatestampPdf.new(pdf_path).run( + text: 'Application Submitted on va.gov', + x: 400, + y: 675, + text_only: true, # passing as text only because we override how the date is stamped in this instance + timestamp:, + page_number: 6, + template: "lib/pdf_fill/forms/pdfs/#{form_id}.pdf", + multistamp: true + ) + renamed_path = "tmp/pdfs/#{form_id}_#{id}_final.pdf" + File.rename(processed_pdf, renamed_path) # rename for vbms upload + renamed_path # return the renamed path + end + + def business_line + 'NCA' + end end diff --git a/app/models/saved_claim/education_career_counseling_claim.rb b/app/models/saved_claim/education_career_counseling_claim.rb index a2cc5a262e2..4ca69e12edf 100644 --- a/app/models/saved_claim/education_career_counseling_claim.rb +++ b/app/models/saved_claim/education_career_counseling_claim.rb @@ -29,4 +29,8 @@ def process_attachments! CentralMail::SubmitSavedClaimJob.new.perform(id) end + + def business_line + 'EDU' + end end diff --git a/app/models/saved_claim/pension.rb b/app/models/saved_claim/pension.rb index a91a7d0e7cc..9b73d3c1207 100644 --- a/app/models/saved_claim/pension.rb +++ b/app/models/saved_claim/pension.rb @@ -37,11 +37,11 @@ def send_confirmation_email # Send this Pension claim to the Lighthouse Benefit Intake API # https://developer.va.gov/explore/api/benefits-intake/docs # @see Lighthouse::PensionBenefitIntakeJob - def upload_to_lighthouse + def upload_to_lighthouse(current_user = nil) refs = attachment_keys.map { |key| Array(open_struct_form.send(key)) }.flatten files = PersistentAttachment.where(guid: refs.map(&:confirmationCode)) files.find_each { |f| f.update(saved_claim_id: id) } - Lighthouse::PensionBenefitIntakeJob.perform_async(id) + Lighthouse::PensionBenefitIntakeJob.perform_async(id, current_user&.uuid) end end diff --git a/app/models/saved_claim/veteran_readiness_employment_claim.rb b/app/models/saved_claim/veteran_readiness_employment_claim.rb index 590db719eae..e6ce564465b 100644 --- a/app/models/saved_claim/veteran_readiness_employment_claim.rb +++ b/app/models/saved_claim/veteran_readiness_employment_claim.rb @@ -111,10 +111,7 @@ def add_office_location(updated_form) end def send_to_vre(user) - if user&.participant_id.blank? - log_message_to_sentry('Participant id is blank when submitting VRE claim', :warn) - send_to_central_mail!(user) - else + if user&.participant_id begin upload_to_vbms send_vbms_confirmation_email(user) @@ -129,6 +126,9 @@ def send_to_vre(user) log_exception_to_sentry(e, { uuid: user.uuid }) end end + else + log_message_to_sentry('Participant id is blank when submitting VRE claim', :warn) + send_to_central_mail!(user) end send_vre_email_form(user) @@ -156,7 +156,6 @@ def send_to_central_mail!(user) form_copy['vaFileNumber'] = parsed_form.dig('veteranInformation', 'VAFileNumber') update!(form: form_copy.to_json) - log_message_to_sentry(guid, :warn, { attachment_id: guid }, { team: 'vfs-ebenefits' }) @sent_to_cmp = true log_to_statsd('cmp') do @@ -223,7 +222,11 @@ def process_attachments! files = PersistentAttachment.where(guid: refs.map(&:confirmationCode)) files.find_each { |f| f.update(saved_claim_id: id) } - CentralMail::SubmitSavedClaimJob.new.perform(id) + Lighthouse::SubmitBenefitsIntakeClaim.new.perform(id) + end + + def business_line + 'VRE' end private diff --git a/app/policies/lighthouse_policy.rb b/app/policies/lighthouse_policy.rb index 4f9aab8637f..656ccdfeb9b 100644 --- a/app/policies/lighthouse_policy.rb +++ b/app/policies/lighthouse_policy.rb @@ -5,7 +5,7 @@ def access? user.icn.present? && user.participant_id.present? end - def access_disability_compensations? + def direct_deposit_access? user.loa3? && allowed_providers.include?(user.identity.sign_in[:service_name]) && user.icn.present? && user.participant_id.present? @@ -17,6 +17,7 @@ def access_update? user.icn.present? && user.participant_id.present? end + alias_method :access_disability_compensations?, :direct_deposit_access? alias_method :mobile_access?, :access_update? alias_method :rating_info_access?, :access? diff --git a/app/policies/mhv_messaging_policy.rb b/app/policies/mhv_messaging_policy.rb index 2dd3e0532af..d1e8d83266c 100644 --- a/app/policies/mhv_messaging_policy.rb +++ b/app/policies/mhv_messaging_policy.rb @@ -1,12 +1,18 @@ # frozen_string_literal: true +require 'sm/client' + MHVMessagingPolicy = Struct.new(:user, :mhv_messaging) do def access? + return false unless user.mhv_correlation_id + client = SM::Client.new(session: { user_id: user.mhv_correlation_id }) validate_client(client) end def mobile_access? + return false unless user.mhv_correlation_id + client = Mobile::V0::Messaging::Client.new(session: { user_id: user.mhv_correlation_id }) validate_client(client) end diff --git a/app/serializers/sign_in/introspect_serializer.rb b/app/serializers/sign_in/introspect_serializer.rb deleted file mode 100644 index 420e28cc02d..00000000000 --- a/app/serializers/sign_in/introspect_serializer.rb +++ /dev/null @@ -1,39 +0,0 @@ -# frozen_string_literal: true - -module SignIn - class IntrospectSerializer < ActiveModel::Serializer - attributes :uuid, :first_name, :middle_name, :last_name, :birth_date, - :email, :gender, :birls_id, :authn_context, - :icn, :edipi, :active_mhv_ids, :sec_id, :vet360_id, - :participant_id, :cerner_id, :cerner_facility_ids, :idme_uuid, - :vha_facility_ids, :id_theft_flag, :verified, :logingov_uuid - - delegate :uuid, to: :object - delegate :first_name, to: :object - delegate :middle_name, to: :object - delegate :last_name, to: :object - delegate :birth_date, to: :object - delegate :logingov_uuid, to: :object - delegate :idme_uuid, to: :object - delegate :email, to: :object - delegate :gender, to: :object - delegate :birls_id, to: :object - delegate :icn, to: :object - delegate :edipi, to: :object - delegate :active_mhv_ids, to: :object - delegate :sec_id, to: :object - delegate :vet360_id, to: :object - delegate :participant_id, to: :object - delegate :cerner_id, to: :object - delegate :cerner_facility_ids, to: :object - delegate :vha_facility_ids, to: :object - delegate :id_theft_flag, to: :object - delegate :authn_context, to: :object - - def id; end - - def verified - object.loa3? - end - end -end diff --git a/app/services/claim_fast_tracking/diagnostic_codes.rb b/app/services/claim_fast_tracking/diagnostic_codes.rb index 347d00ef0d8..e52944a7adb 100644 --- a/app/services/claim_fast_tracking/diagnostic_codes.rb +++ b/app/services/claim_fast_tracking/diagnostic_codes.rb @@ -2,8 +2,15 @@ module ClaimFastTracking module DiagnosticCodes + LIMITED_MOTION_OF_WRIST = 5215 + LIMITATION_OF_MOTION_OF_INDEX_OR_LONG_FINGER = 5229 + LIMITATION_OF_EXTENSION_OF_THE_THIGH = 5251 + FLATFOOT_ACQUIRED = 5276 + HALLUX_VALGUS_UNILATERAL = 5280 + TINNITUS = 6260 ASTHMA = 6602 HYPERTENSION = 7101 - TINNITUS = 6260 + SCARS_GENERAL = 7805 + MIGRAINES = 8100 end end diff --git a/app/services/claim_fast_tracking/diagnostic_codes_for_metrics.rb b/app/services/claim_fast_tracking/diagnostic_codes_for_metrics.rb new file mode 100644 index 00000000000..0873bcb1138 --- /dev/null +++ b/app/services/claim_fast_tracking/diagnostic_codes_for_metrics.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module ClaimFastTracking + module DiagnosticCodesForMetrics + include DiagnosticCodes + + DC = [ + LIMITED_MOTION_OF_WRIST, + LIMITATION_OF_MOTION_OF_INDEX_OR_LONG_FINGER, + LIMITATION_OF_EXTENSION_OF_THE_THIGH, + FLATFOOT_ACQUIRED, + HALLUX_VALGUS_UNILATERAL, + TINNITUS, + SCARS_GENERAL, + MIGRAINES + ].freeze + end +end diff --git a/app/services/claim_fast_tracking/max_cfi_metrics.rb b/app/services/claim_fast_tracking/max_cfi_metrics.rb index 06d97132d8e..4ed9d136485 100644 --- a/app/services/claim_fast_tracking/max_cfi_metrics.rb +++ b/app/services/claim_fast_tracking/max_cfi_metrics.rb @@ -82,17 +82,21 @@ def max_rated_disabilities_diagnostic_codes max_rated_disabilities.map { |dis| dis['diagnosticCode'] || dis['diagnostic_code'] } end + def diagnostic_codes_for_logging_metrics + ClaimFastTracking::DiagnosticCodesForMetrics::DC.intersection(max_rated_disabilities_diagnostic_codes) + end + private def log_init_metric StatsD.increment("#{MAX_CFI_STATSD_KEY_PREFIX}.#{flipper_enabled_state}.526_started") - max_rated_disabilities_diagnostic_codes.each do |dc| + diagnostic_codes_for_logging_metrics.each do |dc| StatsD.increment("#{MAX_CFI_STATSD_KEY_PREFIX}.#{flipper_enabled_state}.526_started.#{dc}") end end def log_cfi_metric - max_rated_disabilities_diagnostic_codes.each do |dc| + diagnostic_codes_for_logging_metrics.each do |dc| StatsD.increment("#{MAX_CFI_STATSD_KEY_PREFIX}.#{flipper_enabled_state}.rated_disabilities.#{dc}") end end diff --git a/app/services/claim_fast_tracking/max_rating_annotator.rb b/app/services/claim_fast_tracking/max_rating_annotator.rb index b0cf0725415..3c921c2a7ec 100644 --- a/app/services/claim_fast_tracking/max_rating_annotator.rb +++ b/app/services/claim_fast_tracking/max_rating_annotator.rb @@ -4,28 +4,16 @@ module ClaimFastTracking class MaxRatingAnnotator - def self.annotate_disabilities(rated_disabilities_response) - if Flipper.enabled?(:disability_526_maximum_rating_api) - annotate_disabilities_api_enabled(rated_disabilities_response) - else - annotate_disabilities_api_disabled(rated_disabilities_response) - end - end + SELECT_DISABILITIES = [ClaimFastTracking::DiagnosticCodes::TINNITUS].freeze - def self.annotate_disabilities_api_disabled(rated_disabilities_response) - tinnitus = ClaimFastTracking::DiagnosticCodes::TINNITUS - rated_disabilities_response.rated_disabilities.each do |disability| - disability.maximum_rating_percentage = 10 if disability.diagnostic_code == tinnitus - end - end - - def self.annotate_disabilities_api_enabled(rated_disabilities_response) + def self.annotate_disabilities(rated_disabilities_response) return if rated_disabilities_response.rated_disabilities.blank? diagnostic_codes = rated_disabilities_response.rated_disabilities .compact # filter out nil entries in rated_disabilities .map(&:diagnostic_code) # map to diagnostic_code field in rating .select { |dc| dc.is_a?(Integer) } # select only integer values + .select { |dc| eligible_for_request?(dc) } # select only eligible return rated_disabilities_response if diagnostic_codes.empty? ratings = get_ratings(diagnostic_codes) @@ -49,6 +37,10 @@ def self.get_ratings(diagnostic_codes) nil end - private_class_method :annotate_disabilities_api_disabled, :annotate_disabilities_api_enabled, :get_ratings + def self.eligible_for_request?(dc) + Flipper.enabled?(:disability_526_maximum_rating_api_all_conditions) || SELECT_DISABILITIES.include?(dc) + end + + private_class_method :get_ratings, :eligible_for_request? end end diff --git a/app/services/sign_in/constants/statsd.rb b/app/services/sign_in/constants/statsd.rb index ad973d9a968..0836d34d39e 100644 --- a/app/services/sign_in/constants/statsd.rb +++ b/app/services/sign_in/constants/statsd.rb @@ -13,8 +13,6 @@ module Statsd STATSD_SIS_REFRESH_FAILURE = 'api.sis.refresh.failure' STATSD_SIS_REVOKE_SUCCESS = 'api.sis.revoke.success' STATSD_SIS_REVOKE_FAILURE = 'api.sis.revoke.failure' - STATSD_SIS_INTROSPECT_SUCCESS = 'api.sis.introspect.success' - STATSD_SIS_INTROSPECT_FAILURE = 'api.sis.introspect.failure' STATSD_SIS_LOGOUT_SUCCESS = 'api.sis.logout.success' STATSD_SIS_LOGOUT_FAILURE = 'api.sis.logout.failure' STATSD_SIS_REVOKE_ALL_SESSIONS_SUCCESS = 'api.sis.revoke_all_sessions.success' diff --git a/app/services/sign_in/user_creator.rb b/app/services/sign_in/user_code_map_creator.rb similarity index 68% rename from app/services/sign_in/user_creator.rb rename to app/services/sign_in/user_code_map_creator.rb index e35967d0c18..5e4dd7c20f2 100644 --- a/app/services/sign_in/user_creator.rb +++ b/app/services/sign_in/user_code_map_creator.rb @@ -1,15 +1,11 @@ # frozen_string_literal: true module SignIn - class UserCreator + class UserCodeMapCreator attr_reader :state_payload, :idme_uuid, :logingov_uuid, - :authn_context, - :current_ial, - :max_ial, :credential_email, - :multifactor, :verified_icn, :edipi, :mhv_correlation_id, @@ -21,11 +17,7 @@ def initialize(user_attributes:, state_payload:, verified_icn:, request_ip:) @state_payload = state_payload @idme_uuid = user_attributes[:idme_uuid] @logingov_uuid = user_attributes[:logingov_uuid] - @authn_context = user_attributes[:authn_context] - @current_ial = user_attributes[:current_ial] - @max_ial = user_attributes[:max_ial] @credential_email = user_attributes[:csp_email] - @multifactor = user_attributes[:multifactor] @edipi = user_attributes[:edipi] @mhv_correlation_id = user_attributes[:mhv_correlation_id] @verified_icn = verified_icn @@ -35,7 +27,6 @@ def initialize(user_attributes:, state_payload:, verified_icn:, request_ip:) end def perform - create_authenticated_user create_credential_email create_user_acceptable_verified_credential create_terms_code_container if needs_accepted_terms_of_use? @@ -45,10 +36,6 @@ def perform private - def create_authenticated_user - user - end - def create_credential_email Login::UserCredentialEmailUpdater.new(credential_email:, user_verification:).perform @@ -80,17 +67,6 @@ def user_verifier_object icn: verified_icn }) end - def user_identity_for_user_creation - @user_identity_for_user_creation ||= UserIdentity.new({ idme_uuid:, - logingov_uuid:, - icn: verified_icn, - loa:, - sign_in:, - email: credential_email, - multifactor:, - authn_context: }) - end - def user_code_map @user_code_map ||= UserCodeMap.new(login_code:, type: state_payload.type, @@ -111,14 +87,6 @@ def sign_in } end - def loa - @loa ||= { current: ial_to_loa(current_ial), highest: ial_to_loa(max_ial) } - end - - def ial_to_loa(ial) - ial == Constants::Auth::IAL_TWO ? Constants::Auth::LOA_THREE : Constants::Auth::LOA_ONE - end - def user_uuid @user_uuid ||= user_verification.backing_credential_identifier end @@ -146,18 +114,5 @@ def terms_code @terms_code ||= SecureRandom.uuid end - - def user - @user ||= begin - user = User.new - user.instance_variable_set(:@identity, user_identity_for_user_creation) - user.uuid = user_uuid - user_identity_for_user_creation.uuid = user_uuid - user.last_signed_in = Time.zone.now - user.fingerprint = request_ip - user.save && user_identity_for_user_creation.save - user - end - end end end diff --git a/app/services/terms_of_use/provisioner.rb b/app/services/terms_of_use/provisioner.rb index 67a00c4786a..00b61189816 100644 --- a/app/services/terms_of_use/provisioner.rb +++ b/app/services/terms_of_use/provisioner.rb @@ -24,7 +24,11 @@ def initialize(icn:, first_name:, last_name:, mpi_gcids:) def perform response = update_provisioning - raise(Errors::ProvisionerError, 'Agreement not accepted') if response[:agreement_signed].blank? + + if response[:agreement_signed].blank? + Rails.logger.error('[TermsOfUse] [Provisioner] update_provisioning error', { icn:, response: }) + raise(Errors::ProvisionerError, 'Agreement not accepted') + end ActiveModel::Type::Boolean.new.cast(response[:agreement_signed]) rescue Common::Client::Errors::ClientError => e diff --git a/app/services/users/profile.rb b/app/services/users/profile.rb index f2dded83b7e..f13e9b0d263 100644 --- a/app/services/users/profile.rb +++ b/app/services/users/profile.rb @@ -63,6 +63,7 @@ def account nil end + # rubocop:disable Metrics/MethodLength def profile { email: user.email, @@ -79,9 +80,17 @@ def profile sign_in: user.identity.sign_in, authn_context: user.authn_context, inherited_proof_verified: user.inherited_proof_verified, - claims: + claims:, + icn: user.icn, + birls_id: user.birls_id, + edipi: user.edipi, + sec_id: user.sec_id, + logingov_uuid: user.logingov_uuid, + idme_uuid: user.idme_uuid, + id_theft_flag: user.id_theft_flag } end + # rubocop:enable Metrics/MethodLength def claims if Flipper.enabled?(:profile_user_claims, user) @@ -113,6 +122,7 @@ def vet360_contact_information return {} if person.blank? { + vet360_id: user.vet360_id, email: person.email, residential_address: person.residential_address, mailing_address: person.mailing_address, @@ -138,9 +148,12 @@ def mpi_profile gender: user.gender_mpi, given_names: user.given_names, is_cerner_patient: !user.cerner_id.nil?, + cerner_id: user.cerner_id, + cerner_facility_ids: user.cerner_facility_ids, facilities: user.va_treatment_facility_ids.map { |id| facility(id) }, va_patient: user.va_patient?, - mhv_account_state: user.mhv_account_state + mhv_account_state: user.mhv_account_state, + active_mhv_ids: user.active_mhv_ids } else scaffold.errors << Users::ExceptionHandler.new(user.mpi_error, 'MVI').serialize_error diff --git a/app/services/users/services.rb b/app/services/users/services.rb index 11b0ec35049..c07a5a0f36a 100644 --- a/app/services/users/services.rb +++ b/app/services/users/services.rb @@ -18,7 +18,7 @@ def initialize(user) # def authorizations @list << BackendServices::RX if user.authorize :mhv_prescriptions, :access? - @list << BackendServices::MESSAGING if user.authorize :legacy_mhv_messaging, :access? + @list << BackendServices::MESSAGING if user.authorize messaging_service, :access? @list << BackendServices::MEDICAL_RECORDS if user.authorize :mhv_medical_records, :access? @list << BackendServices::HEALTH_RECORDS if user.authorize :mhv_health_records, :access? @list << BackendServices::EVSS_CLAIMS if user.authorize :evss, :access? @@ -30,6 +30,7 @@ def authorizations @list << BackendServices::ID_CARD if user.can_access_id_card? @list << BackendServices::IDENTITY_PROOFED if user.loa3? @list << BackendServices::VET360 if user.can_access_vet360? + @list end @@ -44,5 +45,9 @@ def auth_free_services BackendServices::FORM_PREFILL ] end + + def messaging_service + Flipper.enabled?(:mhv_sm_session_policy, user) ? :mhv_messaging : :legacy_mhv_messaging + end end end diff --git a/app/sidekiq/central_mail/submit_saved_claim_job.rb b/app/sidekiq/central_mail/submit_saved_claim_job.rb index 60ce5ca6649..b4c688c4ffd 100644 --- a/app/sidekiq/central_mail/submit_saved_claim_job.rb +++ b/app/sidekiq/central_mail/submit_saved_claim_job.rb @@ -59,7 +59,11 @@ def perform(saved_claim_id) def send_claim_to_central_mail(saved_claim_id) @claim = SavedClaim.find(saved_claim_id) - @pdf_path = process_record(@claim) + @pdf_path = if @claim.form_id == '21P-530V2' + process_record(@claim, @claim.created_at, @claim.form_id) + else + process_record(@claim) + end @attachment_paths = @claim.persistent_attachments.map do |record| process_record(record) @@ -97,16 +101,33 @@ def to_faraday_upload(file_path) ) end - def process_record(record) + # rubocop:disable Metrics/MethodLength + def process_record(record, timestamp = nil, form_id = nil) pdf_path = record.to_pdf stamped_path1 = CentralMail::DatestampPdf.new(pdf_path).run(text: 'VA.GOV', x: 5, y: 5) - CentralMail::DatestampPdf.new(stamped_path1).run( + stamped_path2 = CentralMail::DatestampPdf.new(stamped_path1).run( text: 'FDC Reviewed - va.gov Submission', - x: 429, + x: 400, y: 770, text_only: true ) + if form_id.present? && ['21P-530V2'].include?(form_id) + CentralMail::DatestampPdf.new(stamped_path2).run( + text: 'Application Submitted on va.gov', + x: 425, + y: 675, + text_only: true, # passing as text only because we override how the date is stamped in this instance + timestamp:, + page_number: 5, + size: 9, + template: "lib/pdf_fill/forms/pdfs/#{form_id}.pdf", + multistamp: true + ) + else + stamped_path2 + end end + # rubocop:enable Metrics/MethodLength def get_hash_and_pages(file_path) { diff --git a/app/sidekiq/decision_review/nod_email_loader_job.rb b/app/sidekiq/decision_review/nod_email_loader_job.rb index 76e8dc7636e..eedae525a78 100644 --- a/app/sidekiq/decision_review/nod_email_loader_job.rb +++ b/app/sidekiq/decision_review/nod_email_loader_job.rb @@ -15,7 +15,7 @@ class NodEmailLoaderJob user_uuid: nil }.freeze - def perform(file_name, template_id, s3_config = Settings.decision_review.s3) + def perform(file_name, template_id, s3_config) csv_file = get_csv(file_name, s3_config) line_num = 1 @@ -36,9 +36,9 @@ def perform(file_name, template_id, s3_config = Settings.decision_review.s3) # returns StringIO def get_csv(file_name, s3_config) - credentials = Aws::Credentials.new(s3_config.aws_access_key_id, s3_config.aws_secret_access_key) - s3 = Aws::S3::Client.new(region: s3_config.region, credentials:) - s3.get_object(bucket: s3_config.bucket, key: file_name).body + credentials = Aws::Credentials.new(s3_config[:aws_access_key_id], s3_config[:aws_secret_access_key]) + s3 = Aws::S3::Client.new(region: s3_config[:region], credentials:) + s3.get_object(bucket: s3_config[:bucket], key: file_name).body rescue => e raise "Error fetching #{file_name}: #{e}" end diff --git a/app/sidekiq/decision_review/submit_upload.rb b/app/sidekiq/decision_review/submit_upload.rb index b5370bb1827..74a73489d31 100644 --- a/app/sidekiq/decision_review/submit_upload.rb +++ b/app/sidekiq/decision_review/submit_upload.rb @@ -31,7 +31,7 @@ def perform(appeal_submission_upload_id) when 'NOD' handle_notice_of_disagreement(appeal_submission_upload, file_number_or_ssn, sanitized_file) when 'SC' - handle_supplemental_claim(appeal_submission, file_number_or_ssn, sanitized_file) + handle_supplemental_claim(appeal_submission_upload, file_number_or_ssn, sanitized_file) else raise "Unknown appeal type (#{type})" end.body.dig('data', 'id') @@ -53,10 +53,10 @@ def get_sanitized_file!(form_attachment:) # rubocop:disable Metrics/MethodLength appeal_submission = appeal_submission_upload.appeal_submission # For now, I'm limiting our new `log_formatted` style of logging to the NOD form. In the near future, we will # expand this style of logging to every Decision Review form. - is_nod_submission = appeal_submission.type_of_appeal == 'NOD' + form_id = appeal_submission.type_of_appeal == 'NOD' ? '10182' : '995' common_log_params = { key: :evidence_upload_retrieval, - form_id: '10182', + form_id:, user_uuid: appeal_submission.user_uuid, upstream_system: 'AWS S3', params: { @@ -67,10 +67,10 @@ def get_sanitized_file!(form_attachment:) # rubocop:disable Metrics/MethodLength begin sanitized_file = form_attachment.get_file - log_formatted(**common_log_params.merge(is_success: true)) if is_nod_submission + log_formatted(**common_log_params.merge(is_success: true)) sanitized_file rescue => e - log_formatted(**common_log_params.merge(is_success: false, response_error: e)) if is_nod_submission + log_formatted(**common_log_params.merge(is_success: false, response_error: e)) raise e end end @@ -104,16 +104,30 @@ def handle_notice_of_disagreement(appeal_submission_upload, file_number_or_ssn, upload_url_response end - def handle_supplemental_claim(appeal_submission, file_number_or_ssn, sanitized_file) + # Handle supplemental claims appeal type. Make a request to Lighthouse to get the URL where we can upload the + # file, then get the file from S3 and send it to Lighthouse + # + # @param appeal_submission_upload [AppealSubmissionUpload] + # @param file_number_or_ssn [String] Veteran's SSN or File # + # @param sanitized_file [CarrierWave::SanitizedFile] The sanitized file from S3 + # @return [Faraday::Env] The response from Lighthouse + def handle_supplemental_claim(appeal_submission_upload, file_number_or_ssn, sanitized_file) Sentry.set_tags(source: '20-0995-supplemental-claim') + appeal_submission = appeal_submission_upload.appeal_submission + user_uuid = appeal_submission.user_uuid + appeal_submission_upload_id = appeal_submission_upload.id upload_url_response = get_dr_svc.get_supplemental_claim_upload_url( sc_uuid: appeal_submission.submitted_appeal_uuid, - file_number: file_number_or_ssn + file_number: file_number_or_ssn, + user_uuid:, + appeal_submission_upload_id: ) upload_url = upload_url_response.body.dig('data', 'attributes', 'location') get_dr_svc.put_supplemental_claim_upload(upload_url:, file_upload: sanitized_file, - metadata_string: appeal_submission.upload_metadata) + metadata_string: appeal_submission.upload_metadata, + user_uuid:, + appeal_submission_upload_id:) upload_url_response end end diff --git a/app/sidekiq/education_form/templates/1995.erb b/app/sidekiq/education_form/templates/1995.erb index 6268d53e867..78f99e3717a 100644 --- a/app/sidekiq/education_form/templates/1995.erb +++ b/app/sidekiq/education_form/templates/1995.erb @@ -1,8 +1,12 @@ +<% if Settings.vsp_environment.eql?('production') -%> <%= header %> +<% else -%> +<%= parse_with_template_path('header_1995') %> +<% end -%> <% if Settings.vsp_environment.eql?('production') -%> <%= form_type %> <% else -%> -<%= @applicant.benefitAppliedFor %> +<%= @applicant.benefitAppliedFor&.titleize %> <% end -%> *START* VA Form 22-1995 @@ -13,10 +17,11 @@ OMB Control #: 2900-0074 <% end -%> - REQUEST FOR CHANGE OF PROGRAM OR PLACE OF TRAINING <% if Settings.vsp_environment.eql?('production') -%> + REQUEST FOR CHANGE OF PROGRAM OR PLACE OF TRAINING FOR VETERANS, SERVICEPERSONS & MEMBERS OF THE SELECTED RESERVE <% else -%> + REQUEST FOR CHANGE OF BENEFIT, PROGRAM OR PLACE OF TRAINING FOR VETERANS, SERVICEPERSONS, DEPENDENTS & MEMBERS OF THE SELECTED RESERVE <% end -%> ------------------------------------- @@ -25,6 +30,17 @@ FOR VETERANS, SERVICEPERSONS, DEPENDENTS & MEMBERS OF THE SELECTED RESERVE --------------------- SSN: <%= value_or_na(@applicant.veteranSocialSecurityNumber) %> VA File Number: <%= value_or_na(@applicant.vaFileNumber) %> +<% unless Settings.vsp_environment.eql?('production') -%> +<% if @applicant.minorHighSchoolQuestions -%> + +Applicant has graduated high school or received GED? <%= @applicant.minorHighSchoolQuestions.minorHighSchoolQuestion %> + +<% grad_date = @applicant.minorHighSchoolQuestions.highSchoolGedGradDate if @applicant.minorHighSchoolQuestions.highSchoolGedGradDate -%> +<% grad_date = @applicant.minorHighSchoolQuestions.highSchoolGedExpectedGradDate unless @applicant.minorHighSchoolQuestions.highSchoolGedGradDate -%> +<% date_label = @applicant.minorHighSchoolQuestions.highSchoolGedGradDate ? "Date graduated:" : "Date expected to graduate:" -%> +<%= date_label %> <%= grad_date %> +<% end -%> +<% end -%> <% unless Settings.vsp_environment.eql?('production') -%> Sex: <%= @applicant.applicantGender %> Date of Birth: <%= @applicant.dateOfBirth %> @@ -44,6 +60,19 @@ Preferred Method of Contact: <%= @applicant.preferredContactMethod %> <%= parse_with_template_path('bank_account') %> <% else -%> <%= parse_with_template_path('bank_account_no_stop') %> +<% end -%> +<% unless Settings.vsp_environment.eql?('production') -%> +<% if @applicant.benefitUpdate.eql?('chapter35') || @applicant.benefitAppliedFor.eql?('chapter35') -%> + + SERVICE MEMBER INFORMATION + -------------------------- + +Name: <%= full_name(@applicant.sponsorFullName) %> + +SSN: <%= @applicant.sponsorSocialSecurityNumber %> + +VA File Number: <%= value_or_na(@applicant.vaFileNumber) %> +<% end -%> <% end -%> TYPE AND PROGRAM OF EDUCATION OR TRAINING @@ -58,7 +87,7 @@ Benefit You Are Receiving: <%= form_benefit %> <% end -%> <% unless Settings.vsp_environment.eql?('production') -%> -Benefit Being Applied For: <%= @applicant.benefitAppliedFor %> +Benefit Being Applied For: <%= @applicant.benefitAppliedFor&.titleize %> <% end -%> <% if Settings.vsp_environment.eql?('production') -%> Type of Education or Training: <%= @applicant.educationType&.titleize %> @@ -66,7 +95,7 @@ Type of Education or Training: <%= @applicant.educationType&.titleize %> Type of Education or Training: <%= @applicant.educationTypeUpdate&.titleize %> <% end -%> Education or Career Goal: <%= @applicant.educationObjective %> - +<% if Settings.vsp_environment.eql?('production') -%> New School or Training Establishment: <%= school_name_and_addr(@applicant.newSchool) %> @@ -75,16 +104,54 @@ Current/Prior School or Training Establishment: Date You Stopped Training: <%= @applicant.trainingEndDate %> Reason for Change: <%= @applicant.reasonForChange %> - +<% end -%> ACTIVE DUTY SERVICE INFORMATION ------------------------------- +<% unless Settings.vsp_environment.eql?('production') -%> + +Served in the armed forces?: <%= @applicant.applicantServed %> + +Are You Now On Active Duty?: <%= yesno(@applicant.isActiveDuty) %> + +Do you have any new periods of service to record since you last applied for +education benefits? <%= yesno(@applicant.toursOfDuty.present?) %> Date Entered Date Separated Service Component <% @applicant&.toursOfDuty&.each do |tour| -%> <%= to_date(tour.dateRange&.from) %> <%= to_date(tour.dateRange&.to) %> <%= tour.serviceBranch %> -<% end %> +<% end -%> + +<% else -%> +Date Entered Date Separated Service Component +<% @applicant&.toursOfDuty&.each do |tour| -%> +<%= to_date(tour.dateRange&.from) %> <%= to_date(tour.dateRange&.to) %> <%= tour.serviceBranch %> +<% end -%> + +<% end -%> +<% unless Settings.vsp_environment.eql?('production') -%> +<% if @applicant.minorHighSchoolQuestions -%> + GUARDIAN INFORMATION + -------------------- +First name of Parent, Guardian or Custodian: <%= @applicant.minorQuestions.guardianFirstName %> + +Middle name of Parent, Guardian or Custodian: <%= @applicant.minorQuestions.guardianMiddleName %> + +Last name of Parent, Guardian or Custodian: <%= @applicant.minorQuestions.guardianLastName %> + +Address of Parent, Guardian or Custodian: + Country: <%= @applicant.minorQuestions.guardianAddress.country %> + Street: <%= @applicant.minorQuestions.guardianAddress.street %> + Street address line 2: <%= @applicant.minorQuestions.guardianAddress.street2 %> + City: <%= @applicant.minorQuestions.guardianAddress.city %> + State: <%= @applicant.minorQuestions.guardianAddress.state %> + Postal code: <%= @applicant.minorQuestions.guardianAddress.postalCode %> + Mobile phone number: <%= @applicant.minorQuestions.guardianMobilePhone %> + Home phone number: <%= @applicant.minorQuestions.guardianHomePhone %> + Email address: <%= @applicant.minorQuestions.guardianEmail %> +<% end -%> +<% end -%> <% if Settings.vsp_environment.eql?('production') %> ENTITLEMENT TO AND USAGE OF ADDITIONAL TYPES OF ASSISTANCE ---------------------------------------------------------- @@ -115,9 +182,14 @@ Signature of Applicant Date Signature/Title/Branch of Armed Forces Education Service Officer Date <% else -%> <% if @applicant.isActiveDuty -%> -As an active-duty service member, you have consulted with an Education Service Officer (ESO) regarding your education program. +As an active-duty service member, you have consulted with an Education Service +Officer (ESO) regarding your education program. +<% else -%> +<% if @applicant.minorHighSchoolQuestions -%> + You are the parent, guardian, or custodian of the applicant <% else -%> Certification and Signature of Applicant +<% end -%> Signature of Applicant Date <% end -%> <% end -%> diff --git a/app/sidekiq/education_form/templates/header_1995.erb b/app/sidekiq/education_form/templates/header_1995.erb new file mode 100644 index 00000000000..3f6fa950b88 --- /dev/null +++ b/app/sidekiq/education_form/templates/header_1995.erb @@ -0,0 +1,9 @@ +*INIT* +<%= applicant_name&.first&.upcase&.strip %> +<%= applicant_name&.middle&.upcase&.strip %> +<%= applicant_name&.last&.upcase&.strip %> +<%= @applicant.sponsorSocialSecurityNumber&.gsub(/[^\d]/, '') %> +<%= @applicant.veteranSocialSecurityNumber&.gsub(/[^\d]/, '') %> +<%= header_form_type %> + +<%= school_name %> diff --git a/app/sidekiq/lighthouse/pension_benefit_intake_job.rb b/app/sidekiq/lighthouse/pension_benefit_intake_job.rb index e75b27be4f6..3a70a325447 100644 --- a/app/sidekiq/lighthouse/pension_benefit_intake_job.rb +++ b/app/sidekiq/lighthouse/pension_benefit_intake_job.rb @@ -3,6 +3,8 @@ require 'benefits_intake_service/service' require 'central_mail/datestamp_pdf' require 'simple_forms_api_submission/metadata_validator' +require 'pension_21p527ez/tag_sentry' +require 'pension_21p527ez/monitor' module Lighthouse class PensionBenefitIntakeJob @@ -19,9 +21,13 @@ class PensionBenefitIntakeError < StandardError; end # retry for one day sidekiq_options retry: 14, queue: 'low' sidekiq_retries_exhausted do |msg| - Rails.logger.error('Lighthouse::PensionBenefitIntakeJob Exhausted!', - { saved_claim_id: @saved_claim_id, error: msg }) - StatsD.increment("#{STATSD_KEY_PREFIX}.exhausted") + pension_monitor = Pension21p527ez::Monitor.new + begin + claim = SavedClaim::Pension.find(msg['args'].first) + rescue + claim = nil + end + pension_monitor.track_submission_exhaustion(msg, claim) end # Process claim pdfs and upload to Benefits Intake API @@ -32,17 +38,15 @@ class PensionBenefitIntakeError < StandardError; end # # @param [Integer] saved_claim_id # rubocop:disable Metrics/MethodLength - def perform(saved_claim_id) + def perform(saved_claim_id, user_uuid = nil) + Pension21p527ez::TagSentry.tag_sentry + @user_uuid = user_uuid @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 @lighthouse_service = BenefitsIntakeService::Service.new(with_upload_location: true) - Rails.logger.info('Lighthouse::PensionBenefitIntakeJob Attempt', { - claim_id: @claim.id, - benefits_intake_uuid: @lighthouse_service.uuid, - confirmation_number: @claim.confirmation_number - }) + pension_monitor.track_submission_begun(@claim, @lighthouse_service, @user_uuid) form_submission_polling @@ -57,25 +61,14 @@ def perform(saved_claim_id) attachments: @attachment_paths.map(&method(:split_file_and_path)) } - Rails.logger.info('Lighthouse::PensionBenefitIntakeJob Upload', { - file: payload[:file], - attachments: payload[:attachments], - claim_id: @claim.id, - benefits_intake_uuid: @lighthouse_service.uuid, - confirmation_number: @claim.confirmation_number - }) + pension_monitor.track_submission_attempted(@claim, @lighthouse_service, @user_uuid, payload) response = @lighthouse_service.upload_doc(**payload) check_success(response) rescue => e - Rails.logger.warn('Lighthouse::PensionBenefitIntakeJob FAILED!', - { error: e.message, - claim_id: @claim&.id, - benefits_intake_uuid: @lighthouse_service&.uuid, - confirmation_number: @claim&.confirmation_number }) - StatsD.increment("#{STATSD_KEY_PREFIX}.failure") + pension_monitor.track_submission_retry(@claim, @lighthouse_service, @user_uuid, e) @form_submission_attempt&.fail! - raise + raise e ensure cleanup_file_paths end @@ -140,11 +133,7 @@ def generate_form_metadata_lh # @param [Object] response def check_success(response) if response.success? - Rails.logger.info('Lighthouse::PensionBenefitIntakeJob Succeeded!', - { claim_id: @claim.id, - benefits_intake_uuid: @lighthouse_service.uuid, - confirmation_number: @claim.confirmation_number }) - StatsD.increment("#{STATSD_KEY_PREFIX}.success") + pension_monitor.track_submission_success(@claim, @lighthouse_service, @user_uuid) @claim.send_confirmation_email if @claim.respond_to?(:send_confirmation_email) else @@ -170,6 +159,15 @@ def form_submission_polling def cleanup_file_paths Common::FileHelpers.delete_file_if_exists(@form_path) if @form_path @attachment_paths&.each { |p| Common::FileHelpers.delete_file_if_exists(p) } + rescue => e + pension_monitor.track_file_cleanup_error(@claim, @lighthouse_service, @user_uuid, e) + raise e + end + + private + + def pension_monitor + Pension21p527ez::Monitor.new end end end diff --git a/app/sidekiq/lighthouse/submit_benefits_intake_claim.rb b/app/sidekiq/lighthouse/submit_benefits_intake_claim.rb new file mode 100644 index 00000000000..34553145270 --- /dev/null +++ b/app/sidekiq/lighthouse/submit_benefits_intake_claim.rb @@ -0,0 +1,157 @@ +# frozen_string_literal: true + +require 'central_mail/service' +require 'central_mail/datestamp_pdf' +require 'pension_burial/tag_sentry' +require 'benefits_intake_service/service' +require 'simple_forms_api_submission/metadata_validator' +require 'pdf_info' + +module Lighthouse + class SubmitBenefitsIntakeClaim + include Sidekiq::Job + include SentryLogging + class BenefitsIntakeClaimError < StandardError; end + + FOREIGN_POSTALCODE = '00000' + STATSD_KEY_PREFIX = 'worker.lighthouse.submit_benefits_intake_claim' + + # Sidekiq has built in exponential back-off functionality for retries + # A max retry attempt of 14 will result in a run time of ~25 hours + RETRY = 14 + + sidekiq_options retry: RETRY + + sidekiq_retries_exhausted do |msg, _ex| + Rails.logger.error( + "Failed all retries on Lighthouse::SubmitBenefitsIntakeClaim, last error: #{msg['error_message']}" + ) + StatsD.increment("#{STATSD_KEY_PREFIX}.exhausted") + end + + # rubocop:disable Metrics/MethodLength + def perform(saved_claim_id) + @claim = SavedClaim.find(saved_claim_id) + + @lighthouse_service = BenefitsIntakeService::Service.new(with_upload_location: true) + @pdf_path = if @claim.form_id == '21P-530V2' + process_record(@claim, @claim.created_at, @claim.form_id) + else + process_record(@claim) + end + @attachment_paths = @claim.persistent_attachments.map do |record| + process_record(record) + end + + create_form_submission_attempt + + payload = { + upload_url: @lighthouse_service.location, + file: split_file_and_path(@pdf_path), + metadata: generate_metadata.to_json, + attachments: @attachment_paths.map(&method(:split_file_and_path)) + } + + response = @lighthouse_service.upload_doc(**payload) + + if response.success? + Rails.logger.info('Lighthouse::SubmitBenefitsIntakeClaim succeeded', generate_log_details) + @claim.send_confirmation_email if @claim.respond_to?(:send_confirmation_email) + else + raise BenefitsIntakeClaimError, response.body + end + rescue => e + Rails.logger.warn('Lighthouse::SubmitBenefitsIntakeClaim failed, retrying...', generate_log_details(e)) + raise + ensure + cleanup_file_paths + end + + # rubocop:enable Metrics/MethodLength + def generate_metadata + form = @claim.parsed_form + veteran_full_name = form['veteranFullName'] + address = form['claimantAddress'] || form['veteranAddress'] + + metadata = { + 'veteranFirstName' => veteran_full_name['first'], + 'veteranLastName' => veteran_full_name['last'], + 'fileNumber' => form['vaFileNumber'] || form['veteranSocialSecurityNumber'], + 'zipCode' => address['postalCode'], + 'source' => "#{@claim.class} va.gov", + 'docType' => @claim.form_id, + 'businessLine' => @claim.business_line + } + + SimpleFormsApiSubmission::MetadataValidator.validate(metadata, zip_code_is_us_based: check_zipcode(address)) + end + + # rubocop:disable Metrics/MethodLength + def process_record(record, timestamp = nil, form_id = nil) + pdf_path = record.to_pdf + stamped_path1 = CentralMail::DatestampPdf.new(pdf_path).run(text: 'VA.GOV', x: 5, y: 5) + stamped_path2 = CentralMail::DatestampPdf.new(stamped_path1).run( + text: 'FDC Reviewed - va.gov Submission', + x: 400, + y: 770, + text_only: true + ) + if form_id.present? && ['21P-530V2'].include?(form_id) + CentralMail::DatestampPdf.new(stamped_path2).run( + text: 'Application Submitted on va.gov', + x: 425, + y: 675, + text_only: true, # passing as text only because we override how the date is stamped in this instance + timestamp:, + page_number: 5, + size: 9, + template: "lib/pdf_fill/forms/pdfs/#{form_id}.pdf", + multistamp: true + ) + else + stamped_path2 + end + end + + # rubocop:enable Metrics/MethodLength + def split_file_and_path(path) + { file: path, file_name: path.split('/').last } + end + + private + + def generate_log_details(e = nil) + details = { + claim_id: @claim.id, + benefits_intake_uuid: @lighthouse_service.uuid, + confirmation_number: @claim.confirmation_number + } + details['error'] = e.message if e + details + end + + def create_form_submission_attempt + Rails.logger.info('Lighthouse::SubmitBenefitsIntakeClaim job starting', { + claim_id: @claim.id, + benefits_intake_uuid: @lighthouse_service.uuid, + confirmation_number: @claim.confirmation_number + }) + form_submission = FormSubmission.create( + form_type: @claim.form_id, + form_data: @claim.to_json, + benefits_intake_uuid: @lighthouse_service.uuid, + saved_claim: @claim + ) + @form_submission_attempt = FormSubmissionAttempt.create(form_submission:) + end + + def cleanup_file_paths + Common::FileHelpers.delete_file_if_exists(@pdf_path) if @pdf_path + @attachment_paths&.each { |p| Common::FileHelpers.delete_file_if_exists(p) } + end + + def check_zipcode(address) + address['country'].upcase.in?(%w[USA US]) + end + end +end 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/app/sidekiq/terms_of_use/sign_up_service_updater_job.rb b/app/sidekiq/terms_of_use/sign_up_service_updater_job.rb index bca51e83b0b..307797272e8 100644 --- a/app/sidekiq/terms_of_use/sign_up_service_updater_job.rb +++ b/app/sidekiq/terms_of_use/sign_up_service_updater_job.rb @@ -27,6 +27,8 @@ def perform(attr_package_key) @version = attrs[:version] terms_of_use_agreement.accepted? ? accept : decline + + Sidekiq::AttrPackage.delete(attr_package_key) end private diff --git a/app/swagger/swagger/requests/financial_status_reports.rb b/app/swagger/swagger/requests/financial_status_reports.rb index 80024600b35..2bc6c94b8fd 100644 --- a/app/swagger/swagger/requests/financial_status_reports.rb +++ b/app/swagger/swagger/requests/financial_status_reports.rb @@ -5,7 +5,7 @@ module Requests class FinancialStatusReports include Swagger::Blocks - swagger_path '/v0/financial_status_reports' do + swagger_path '/debts_api/v0/financial_status_reports' do operation :post do key :summary, 'Submits Form VA-5655 data to the Debt Management Center' key :description, "Submits Form VA-5655 to the Debt Management Center. @@ -32,7 +32,7 @@ class FinancialStatusReports end end - swagger_path '/v0/financial_status_reports/download_pdf' do + swagger_path '/debts_api/v0/financial_status_reports/download_pdf' do operation :get do key :summary, 'Downloads the filled copy of VA-5655 Financial Status Report' key :operationId, 'getFinancialStatusReport' diff --git a/app/swagger/swagger/requests/medical_copays.rb b/app/swagger/swagger/requests/medical_copays.rb index 1cde0d8ebb0..de248da30fd 100644 --- a/app/swagger/swagger/requests/medical_copays.rb +++ b/app/swagger/swagger/requests/medical_copays.rb @@ -255,12 +255,20 @@ class MedicalCopays parameter do key :name, :statements key :in, :body - key :description, 'New statement data' + key :description, 'An array of statement data sent as a base64 json file' key :required, true schema do - key :type, :object + key :type, :array key :required, [:statements] + items do + key :type, :object + property :veteranIdentifier, type: :string, example: '123456789' + property :identifierType, type: :string, example: 'edipi' + property :facilityNum, type: :string, example: '123' + property :facilityName, type: :string, example: 'VA Medical Center' + property :statementDate, type: :string, example: '01/01/2023' + end end end diff --git a/app/swagger/swagger/requests/sign_in.rb b/app/swagger/swagger/requests/sign_in.rb index 2929031050a..72ad1a8767c 100644 --- a/app/swagger/swagger/requests/sign_in.rb +++ b/app/swagger/swagger/requests/sign_in.rb @@ -178,24 +178,6 @@ class SignIn end end - swagger_path '/v0/sign_in/introspect' do - operation :get do - key :description, 'Sign in Service user introspection.' - key :operationId, 'getSignInIntrospect' - key :tags, %w[authentication] - - key :produces, ['application/json'] - key :consumes, ['application/json'] - - parameter :optional_authorization - - response 200 do - key :description, 'Access token validated, user attributes are serialized and rendered to client.' - schema { key :$ref, :UserAttributesResponse } - end - end - end - swagger_path '/v0/sign_in/revoke' do operation :post do key :description, 'Sign in Service session destruction.' diff --git a/app/swagger/swagger/schemas/contacts.rb b/app/swagger/swagger/schemas/contacts.rb index d19585b9085..e6a01bf6276 100644 --- a/app/swagger/swagger/schemas/contacts.rb +++ b/app/swagger/swagger/schemas/contacts.rb @@ -10,7 +10,7 @@ class Contacts key :required, [:data] property :data, type: :array do items do - property :id, type: :string, example: 'dbbf9a58-41e5-40c0-bdb5-fc1407aa1f05' + property :id, type: :string property :type, type: :string property :attributes do key :$ref, :Contact @@ -21,7 +21,11 @@ class Contacts swagger_schema :Contact do key :required, %i[contact_type given_name family_name primary_phone] - property :contact_type, type: :string, enum: VAProfile::Models::AssociatedPerson::CONTACT_TYPES + property( + :contact_type, + type: :string, + enum: VAProfile::Models::AssociatedPerson::PERSONAL_HEALTH_CARE_CONTACT_TYPES + ) property :given_name, type: %i[string null] property :family_name, type: %i[string null] property :relationship, type: %i[string null] diff --git a/app/swagger/swagger/schemas/health/prescriptions.rb b/app/swagger/swagger/schemas/health/prescriptions.rb index 48626ef04b7..c502c58a6e3 100644 --- a/app/swagger/swagger/schemas/health/prescriptions.rb +++ b/app/swagger/swagger/schemas/health/prescriptions.rb @@ -61,7 +61,7 @@ class Prescriptions property :quantity, type: :integer property :expiration_date, type: :string, format: :date property :dispensed_date, type: %i[string null], format: :date - property :sorted_dispensed_date, type: :string, format: :date + property :sorted_dispensed_date, type: %i[string null], format: :date property :station_number, type: :string property :is_refillable, type: :boolean property :is_trackable, type: :boolean diff --git a/config/features.yml b/config/features.yml index cf3b21130a6..1b218358ef1 100644 --- a/config/features.yml +++ b/config/features.yml @@ -73,12 +73,10 @@ features: actor_type: user description: Enables access to the 10-10EZR application in prod for the purposes of conducting user reasearch enable_in_development: true - cerner_transition_556_t30: + ezr_tera_enabled: actor_type: user - description: This will control the content that will be in effect 30 days prior to transition. - cerner_transition_556_t5: - actor_type: user - description: This will control the content that will be in effect 5 days prior to transition. + description: Enables Toxic Exposure questions for 10-10EZR applicants. + enable_in_development: true cerner_override_653: actor_type: user description: This will show the Cerner facility 653 as `isCerner`. @@ -158,18 +156,10 @@ features: actor_type: user description: Uses the refactored code for Travel Claim Redis client to fetch attributes enable_in_development: true - check_in_experience_travel_claim_increase_timeout: - actor_type: user - description: Increases the timeout for BTSSS submit claim request to 120 seconds - enable_in_development: true claim_letters_access: actor_type: user description: Enables users to access the claim letters page enable_in_development: true - cst_use_lighthouse: - actor_type: user - description: When enabled, claims status tool uses the Lighthouse API instead of EVSS - enable_in_development: true cst_use_lighthouse_5103: actor_type: user description: When enabled, claims status tool uses the Lighthouse API for the 5103 endpoint @@ -190,14 +180,14 @@ features: actor_type: user description: When enabled, the Download Decision Letters feature includes 5103 letters enable_in_development: true - cst_use_new_claim_cards: - actor_type: user - description: When enabled, claims status tool uses the new claim card designs - enable_in_development: true cst_use_claim_details_v2: actor_type: user description: When enabled, claims status tool uses the new claim details design enable_in_development: true + cst_use_dd_rum: + actor_type: user + description: When enabled, claims status tool uses DataDog's Real User Monitoring logging + enable_in_development: false coe_access: actor_type: user description: Feature gates the certificate of eligibility application @@ -330,9 +320,6 @@ features: decision_review_delay_evidence: actor_type: user description: Ensures that NOD and SC evidence is not received in Central Mail before the appeal itself - decision_review_use_appeal_submitted_job: - actor_type: user - description: Uses the revised AppealSubmittedJob instead of AppealReceivedJob to send emails for newly submitted Decision Reviews dependency_verification: actor_type: user description: Feature gates the dependency verification modal for updating the diaries service. @@ -365,9 +352,9 @@ features: actor_type: user description: enables displaying a short education blurb alongside rated disabilities already at maximum rating. enable_in_development: true - disability_526_maximum_rating_api: + disability_526_maximum_rating_api_all_conditions: actor_type: user - description: enables sending disability diagnostic codes to VRO Max CFI API to get the corresponding maximum ratings. + description: enables calls to VRO Max CFI API for all conditions, otherwise only calls for select conditions are made enable_in_development: false disability_526_ep_merge_api: actor_type: user @@ -479,6 +466,10 @@ features: actor_type: user description: Enables flagging feature for Find a Representative frontend enable_in_development: true + representative_status_enabled: + actor_type: user + description: Enables flagging feature for Find a Representative frontend + enable_in_development: true form526_legacy: actor_type: user description: If true, points controllers to the legacy EVSS Form 526 instance. If false, the controllers will use the Dockerized instance running in DVP. @@ -677,6 +668,10 @@ features: actor_type: user description: Enables/disables Secure Messaging Cerner Transition Pilot environment on VA.gov enable_in_development: true + mhv_sm_session_policy: + actor_type: user + description: changes secure messaging policy to use sm sessions endpoint for authorization + enable_in_development: true mhv_medical_records_allow_txt_downloads: actor_type: user description: Allows users to download Medical Records data in TXT format @@ -760,6 +755,10 @@ features: actor_type: user description: changes secure messaging policy to use sm sessions endpoint for authorization enable_in_development: true + mobile_v1_lighthouse_facilities: + actor_type: user + description: change mobile lighthouse facility calls to use new v1 endpoint + enable_in_development: true multiple_address_10_10ez: actor_type: cookie_id description: > @@ -1028,10 +1027,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 @@ -1108,10 +1103,6 @@ features: actor_type: user enable_in_development: true description: Toggle for proof of concept to help Veteran contact a facility when the type of care is not available - va_online_scheduling_required_schedulable_param: - actor_type: user - enable_in_development: true - description: Toggle that requires the inclusion of a new 'schedulable' boolean param for fetching facilities va_online_scheduling_after_visit_summary: actor_type: user enable_in_development: true @@ -1128,6 +1119,14 @@ features: actor_type: user enable_in_development: true description: Allows appointment cancellations to be routed to Oracle Health sites. + va_online_scheduling_enable_OH_eligibility: + actor_type: user + enable_in_development: true + description: Toggle for routing eligibility requests to the VetsAPI Gateway Service(VPG) instead of vaos-service + va_online_scheduling_enable_OH_slots_search: + actor_type: user + enable_in_development: true + description: Toggle for routing slots search requests to the VetsAPI Gateway Service(VPG) instead of vaos-service va_online_scheduling_datadog_RUM: actor_type: user description: Enables datadog Real User Monitoring. @@ -1143,6 +1142,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. @@ -1202,6 +1204,10 @@ features: actor_type: user description: Flag to use begin rescue block for BGS call enable_in_development: true + toe_light_house_dgi_direct_deposit: + actor_type: user + description: Uses lighthouse api for direct deposit information in TOE. + enable_in_development: true move_form_back_button: actor_type: user description: Test moving form back button to the top of the page @@ -1262,6 +1268,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 @@ -1289,6 +1298,10 @@ features: actor_type: user description: NOD VANotify notification callbacks endpoint enable_in_development: true + pension_ipf_callbacks_endpoint: + actor_type: user + description: Pension IPF VANotify notification callbacks endpoint + enable_in_development: true hlr_browser_monitoring_enabled: actor_type: user description: HLR Datadog RUM monitoring @@ -1318,6 +1331,9 @@ features: pension_claim_submission_to_lighthouse: actor_type: user description: Pension claim submission uses Lighthouse API + central_mail_benefits_intake_submission: + actor_type: user + description: Enable central mail claims submission uses Benefits Intake API virtual_agent_enable_param_error_detection: actor_type: user description: If enabled, Allows for the detection of errors in the chatbot params @@ -1332,12 +1348,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/config/initializers/01_redis.rb b/config/initializers/01_redis.rb index bd43c3a719d..fcf0ffd2bd5 100644 --- a/config/initializers/01_redis.rb +++ b/config/initializers/01_redis.rb @@ -4,6 +4,10 @@ REDIS_CONFIG = Rails.application.config_for(:redis).freeze # set the current global instance of Redis based on environment specific config -$redis = Redis.new(REDIS_CONFIG[:redis].to_hash) - -Redis.exists_returns_integer = true +$redis = + if Rails.env.test? + require 'mock_redis' + MockRedis.new(url: REDIS_CONFIG[:redis][:url]) + else + Redis.new(REDIS_CONFIG[:redis].to_h) + end diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index b2852d54867..41fb273cc5c 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -59,6 +59,8 @@ end # Remove the default error handler - config.error_handlers.delete_if { |handler| handler.is_a?(Sidekiq::ExceptionHandler::Logger) } + config.error_handlers.delete(Sidekiq::Config::ERROR_HANDLER) end + + Sidekiq.strict_args!(false) end diff --git a/config/locales/en.yml b/config/locales/en.yml index 99be4cfaf7a..690ace033e8 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -23,6 +23,7 @@ en: time: formats: pdf_stamp: "%Y-%m-%d" + pdf_stamp4010007: "%m/%d/%Y" dependency_claim_failure_mailer: subject: We can’t process your dependents application body_html: > diff --git a/config/redis.yml b/config/redis.yml index c3444f1575c..3cde49a7e0b 100644 --- a/config/redis.yml +++ b/config/redis.yml @@ -197,6 +197,9 @@ test: <<: *defaults redis: inherit_socket: true + url: <%= Settings.redis.app_data.url %> + sidekiq: + url: <%= Settings.redis.sidekiq.url %> production: <<: *defaults diff --git a/config/routes.rb b/config/routes.rb index 07b2b34536b..ebf6efc99a0 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -18,7 +18,6 @@ post '/v0/sign_in/refresh', to: 'v0/sign_in#refresh' post '/v0/sign_in/revoke', to: 'v0/sign_in#revoke' post '/v0/sign_in/token', to: 'v0/sign_in#token' - get '/v0/sign_in/introspect', to: 'v0/sign_in#introspect' get '/v0/sign_in/logout', to: 'v0/sign_in#logout' get '/v0/sign_in/logingov_logout_proxy', to: 'v0/sign_in#logingov_logout_proxy' get '/v0/sign_in/revoke_all_sessions', to: 'v0/sign_in#revoke_all_sessions' @@ -93,12 +92,6 @@ end get 'benefits_reference_data/*path', to: 'benefits_reference_data#get_data' - resources :financial_status_reports, only: %i[create] do - collection do - get :download_pdf - end - end - post '/mvi_users/:id', to: 'mpi_users#submit' resource :decision_review_evidence, only: :create @@ -299,6 +292,7 @@ resource :military_occupations, only: :show # Lighthouse + resource :direct_deposits, only: %i[show update] namespace :direct_deposits do resource :disability_compensations, only: %i[show update] end @@ -437,6 +431,7 @@ scope format: false do resources :nod_callbacks, only: [:create] + resources :pension_ipf_callbacks, only: [:create] end end @@ -462,6 +457,7 @@ mount DebtsApi::Engine, at: '/debts_api' mount DhpConnectedDevices::Engine, at: '/dhp_connected_devices' mount FacilitiesApi::Engine, at: '/facilities_api' + mount IvcChampva::Engine, at: '/ivc_champva' mount RepresentationManagement::Engine, at: '/representation_management' mount SimpleFormsApi::Engine, at: '/simple_forms_api' mount HealthQuest::Engine, at: '/health_quest' diff --git a/config/settings.yml b/config/settings.yml index fa10b33c6c4..ce557b9f454 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -79,6 +79,8 @@ sign_in: vaweb_client_id: vaweb vamobile_client_id: vamobile arp_client_id: arp + sts_client: + key_path: spec/fixtures/sign_in/sts_client.pem terms_of_use: current_version: v1 @@ -617,7 +619,7 @@ ask_va_api: client_id: client_id client_secret: secret resource: resource - veis_api_path: veis/vagov.lob.ava/api + veis_api_path: eis/vagov.lob.ava/api tenant_id: abcdefgh-1234-5678-12345-11e8b8ce491e ocp_apim_subscription_key: subscription_key redis_token_expiry: 3540 # 59 minutes @@ -1672,6 +1674,7 @@ travel_pay: auth_url: https://login.microsoftonline.us subscription_key: ~ base_url: ~ + client_number: ~ service_name: BTSSS-API xlsx_file_fetcher: diff --git a/config/settings/test.yml b/config/settings/test.yml index 7fb55bde072..989de93e139 100644 --- a/config/settings/test.yml +++ b/config/settings/test.yml @@ -107,14 +107,10 @@ bgs: client_station_id: 281 client_username: VAgovAPI url: https://internal-dsva-vagov-dev-fwdproxy-1893365470.us-gov-west-1.elb.amazonaws.com:4447 - # To create a new VCR cassette: - # 1. Uncomment these lines - # 2. Create an empty cassette file - # 3. Run the test against that file, VCR should fill it - # 4. Replace localhost in the file with the url above - # 5. Re-comment these lines: - # url: https://localhost:4447 - # ssl_verify_mode: "none" +# You can use this in `config/settings/test.local.yml`. +# bgs: +# url: https://localhost:4447 +# ssl_verify_mode: "none" flipper: github_organization: "organization" @@ -417,3 +413,18 @@ ask_va: nod_vanotify_status_callback: bearer_token: bearer_token_secret + +travel_pay: + veis: + client_id: 'client_id' + client_secret: 'client_secret' + resource: 'resource_id' + tenant_id: 'tenant_id' + auth_url: 'https://auth.veis.gov' + subscription_key: 'api_key' + base_url: 'https://btsss.gov' + client_number: '12345' + service_name: 'BTSSS-API' + +pension_ipf_vanotify_status_callback: + bearer_token: bearer_token_secret diff --git a/datadog-service-catalog/datadog-service-catalog.yml b/datadog-service-catalog/datadog-service-catalog.yml index 50186a24781..c0a6fa2e688 100644 --- a/datadog-service-catalog/datadog-service-catalog.yml +++ b/datadog-service-catalog/datadog-service-catalog.yml @@ -1,5 +1,5 @@ --- -schema-version: v2.1 +schema-version: v2.2 dd-service: dependent-change team: benefits application: 686c/674 @@ -16,7 +16,7 @@ links: url: https://vagov.ddog-gov.com/dashboard/vad-969-xqc/benefits---dependents-686674 integrations: {} --- -schema-version: v2.1 +schema-version: v2.2 dd-service: mobile-application tier: '1' lifecycle: production @@ -35,7 +35,7 @@ links: url: https://vagov.ddog-gov.com/dashboard/pzz-7x2-4cs/mobile-api-dashboard integrations: {} --- -schema-version: v2.1 +schema-version: v2.2 dd-service: claim-status team: benefits application: claim status tool application @@ -53,7 +53,7 @@ links: url: https://vagov.ddog-gov.com/dashboard/uc8-jnr-zhm/benefits---claim-status-tool integrations: {} --- -schema-version: v2.1 +schema-version: v2.2 dd-service: gibill-application team: vfs-education tier: '1' @@ -64,7 +64,7 @@ contacts: contact: https://dsva.slack.com/archives/CG6N59X40 integrations: {} --- -schema-version: v2.1 +schema-version: v2.2 dd-service: healthcare-application team: 1010-health-apps application: health @@ -81,7 +81,7 @@ links: url: https://vagov.ddog-gov.com/dashboard/p5g-fys-epz/1010-health-apps integrations: {} --- -schema-version: v2.1 +schema-version: v2.2 dd-service: profile team: auth-experience-profile tier: '1' @@ -92,7 +92,7 @@ contacts: contact: https://dsva.slack.com/archives/C909ZG2BB integrations: {} --- -schema-version: v2.1 +schema-version: v2.2 dd-service: education-forms team: vfs-education tier: '1' @@ -103,7 +103,7 @@ contacts: contact: https://dsva.slack.com/archives/CG6N59X40 integrations: {} --- -schema-version: v2.1 +schema-version: v2.2 dd-service: ask-va team: octo-engineers tier: '2' @@ -114,7 +114,7 @@ contacts: contact: https://dsva.slack.com/archives/C05A2F6DEAE integrations: {} --- -schema-version: v2.1 +schema-version: v2.2 dd-service: evidence-upload team: benefits application: Notice of Disagreement, Supplemental Claims @@ -133,7 +133,7 @@ links: url: https://vagov.ddog-gov.com/dashboard/u78-itq-2yt integrations: {} --- -schema-version: v2.1 +schema-version: v2.2 dd-service: appeal-application team: benefits application: Supplemental Claims @@ -151,7 +151,7 @@ links: url: https://vagov.ddog-gov.com/dashboard/uc7-8ai-6c3/benefits---supplemental-claims integrations: {} --- -schema-version: v2.1 +schema-version: v2.2 dd-service: direct-deposit team: auth-experience-profile tier: '1' @@ -167,7 +167,7 @@ links: url: https://vagov.ddog-gov.com/dashboard/gra-npe-h52/authenticated-experience-cp-direct-deposit integrations: {} --- -schema-version: v2.1 +schema-version: v2.2 dd-service: avs team: After Visit Summary description: Displays after visit summaries to veterans. @@ -177,7 +177,7 @@ contacts: contact: https://dsva.slack.com/archives/C04UBETRY8N integrations: {} --- -schema-version: v2.1 +schema-version: v2.2 dd-service: disability-application team: benefits application: disability benefits application @@ -195,7 +195,7 @@ links: url: https://vagov.ddog-gov.com/dashboard/ygg-v6d-nza/form-526-disability-compensation integrations: {} --- -schema-version: v2.1 +schema-version: v2.2 dd-service: appeal-status team: benefits application: Appeal Status - part of Claim Status Tool @@ -213,7 +213,7 @@ links: url: https://vagov.ddog-gov.com/dashboard/cpt-6qy-rbc integrations: {} --- -schema-version: v2.1 +schema-version: v2.2 dd-service: homepage team: vfs-websites tier: '1' @@ -226,7 +226,7 @@ tags: - homepage integrations: {} --- -schema-version: v2.1 +schema-version: v2.2 dd-service: caregiver-application team: 1010-health-apps tier: '1' @@ -242,7 +242,7 @@ links: url: https://vagov.ddog-gov.com/dashboard/p5g-fys-epz/1010-health-apps integrations: {} --- -schema-version: v2.1 +schema-version: v2.2 dd-service: feature-flag team: platform application: platform @@ -258,7 +258,7 @@ links: url: https://vagov.ddog-gov.com/dashboard/sbb-id5-kwh/feature-toggles-500-errors integrations: {} --- -schema-version: v2.1 +schema-version: v2.2 dd-service: financial-report team: vfs-debt tier: '1' @@ -268,7 +268,7 @@ contacts: contact: https://dsva.slack.com/archives/CPE4AJ6Q0 integrations: {} --- -schema-version: v2.1 +schema-version: v2.2 dd-service: search team: vfs-websites tier: '1' @@ -278,7 +278,7 @@ contacts: contact: https://dsva.slack.com/archives/C011K1VSTC3 integrations: {} --- -schema-version: v2.1 +schema-version: v2.2 dd-service: save-in-progress team: platform application: platform @@ -289,7 +289,7 @@ contacts: contact: https://dsva.slack.com/archives/CBU0KDSB1 integrations: {} --- -schema-version: v2.1 +schema-version: v2.2 dd-service: authentication team: platform-identity tier: '1' @@ -303,7 +303,7 @@ links: url: https://vagov.ddog-gov.com/dashboard/52g-hyg-wcj/vsp-identity-monitor-dashboard integrations: {} --- -schema-version: v2.1 +schema-version: v2.2 dd-service: mobile-app team: vfs-mobile tier: '1' @@ -323,7 +323,7 @@ links: url: https://vagov.ddog-gov.com/dashboard/pzz-7x2-4cs/mobile-api-dashboard integrations: {} --- -schema-version: v2.1 +schema-version: v2.2 dd-service: pension-application team: benefits application: Pension / form 21P-527EZ @@ -341,7 +341,7 @@ links: url: https://vagov.ddog-gov.com/dashboard/vk2-6zi-zzu/benefits---form-527-pension-benefits integrations: {} --- -schema-version: v2.1 +schema-version: v2.2 dd-service: education-enrollment-verification team: vfs-education tier: '1' @@ -352,7 +352,22 @@ contacts: contact: https://dsva.slack.com/archives/CG6N59X40 integrations: {} --- -schema-version: v2.1 +schema-version: v2.2 +dd-service: mhv-appointments +team: mhv-appointments +application: health +tier: '1' +description: Veteran medical appointment scheduling service. +lifecycle: production +contacts: + - name: "#appointments-team" + type: slack + contact: https://dsva.slack.com/archives/CMNQT72LX +type: web +languages: + - ruby +--- +schema-version: v2.2 dd-service: mhv-messaging team: vfs-myhealth application: health @@ -369,7 +384,7 @@ links: url: https://vagov.ddog-gov.com/dashboard/5r5-ra2-qga/mhv-secure-messaging integrations: {} --- -schema-version: v2.1 +schema-version: v2.2 dd-service: intent-to-file team: benefits application: disability-app @@ -387,7 +402,7 @@ links: url: https://vagov.ddog-gov.com/dashboard/9c7-45v-3fx integrations: {} --- -schema-version: v2.1 +schema-version: v2.2 dd-service: mhv-medications team: vfs-myhealth application: health @@ -398,7 +413,7 @@ contacts: contact: https://dsva.slack.com/archives/C04PRFEJQTY integrations: {} --- -schema-version: v2.1 +schema-version: v2.2 dd-service: find-a-form team: vfs-websites tier: '1' diff --git a/datadog-service-catalog/debt-resolution.yml b/datadog-service-catalog/debt-resolution.yml index 7aa506f5f70..1c00fa1ff07 100644 --- a/datadog-service-catalog/debt-resolution.yml +++ b/datadog-service-catalog/debt-resolution.yml @@ -1,5 +1,5 @@ --- -schema-version: v2.1 +schema-version: v2.2 dd-service: debt-resolution team: vfs-debt tier: '1' diff --git a/db/migrate/20240313211432_add_status_to_inprogress_form.rb b/db/migrate/20240313211432_add_status_to_inprogress_form.rb new file mode 100644 index 00000000000..166ebcf4d11 --- /dev/null +++ b/db/migrate/20240313211432_add_status_to_inprogress_form.rb @@ -0,0 +1,5 @@ +class AddStatusToInprogressForm < ActiveRecord::Migration[7.0] + def change + add_column :in_progress_forms, :status, :integer + end +end diff --git a/db/migrate/20240313211541_add_status_to_inprogress_form_default.rb b/db/migrate/20240313211541_add_status_to_inprogress_form_default.rb new file mode 100644 index 00000000000..44620cb1bbc --- /dev/null +++ b/db/migrate/20240313211541_add_status_to_inprogress_form_default.rb @@ -0,0 +1,5 @@ +class AddStatusToInprogressFormDefault < ActiveRecord::Migration[7.0] + def change + change_column_default :in_progress_forms, :status, from: nil, to: 0 + end +end diff --git a/db/migrate/20240402195838_add_encrypted_data_to_personal_information_logs.rb b/db/migrate/20240402195838_add_encrypted_data_to_personal_information_logs.rb new file mode 100644 index 00000000000..f135f377f39 --- /dev/null +++ b/db/migrate/20240402195838_add_encrypted_data_to_personal_information_logs.rb @@ -0,0 +1,6 @@ +class AddEncryptedDataToPersonalInformationLogs < ActiveRecord::Migration[7.1] + def change + add_column :personal_information_logs, :data_ciphertext, :text + add_column :personal_information_logs, :encrypted_kms_key, :text + end +end diff --git a/db/migrate/20240408152120_add_service_levels_and_credential_service_providers_to_client_configs.rb b/db/migrate/20240408152120_add_service_levels_and_credential_service_providers_to_client_configs.rb new file mode 100644 index 00000000000..fc2ae97d190 --- /dev/null +++ b/db/migrate/20240408152120_add_service_levels_and_credential_service_providers_to_client_configs.rb @@ -0,0 +1,6 @@ +class AddServiceLevelsAndCredentialServiceProvidersToClientConfigs < ActiveRecord::Migration[7.1] + def change + add_column :client_configs, :service_levels, :string, array: true, default: %w[ial1 ial2 loa1 loa3 min] + add_column :client_configs, :credential_service_providers, :string, array: true, default: %w[logingov idme dslogon mhv] + end +end diff --git a/db/migrate/20240410212414_remove_fkeys_from_accredited_organizations_accredited_representatives.rb b/db/migrate/20240410212414_remove_fkeys_from_accredited_organizations_accredited_representatives.rb new file mode 100644 index 00000000000..e117c8103cd --- /dev/null +++ b/db/migrate/20240410212414_remove_fkeys_from_accredited_organizations_accredited_representatives.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class RemoveFkeysFromAccreditedOrganizationsAccreditedRepresentatives < ActiveRecord::Migration[7.1] + def change + remove_foreign_key :accredited_organizations_accredited_representatives, :accredited_representatives, + column: :accredited_representative_id, if_exists: true + remove_foreign_key :accredited_organizations_accredited_representatives, :accredited_organizations, + column: :accredited_organization_id, if_exists: true + end +end diff --git a/db/migrate/20240410212440_drop_accredited_organizations_accredited_representatives.rb b/db/migrate/20240410212440_drop_accredited_organizations_accredited_representatives.rb new file mode 100644 index 00000000000..0eb2cb0c4bc --- /dev/null +++ b/db/migrate/20240410212440_drop_accredited_organizations_accredited_representatives.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class DropAccreditedOrganizationsAccreditedRepresentatives < ActiveRecord::Migration[7.1] + def change + drop_table :accredited_organizations_accredited_representatives, if_exists: true # rubocop:disable Rails/ReversibleMigration + end +end diff --git a/db/migrate/20240410212508_drop_accredited_organizations.rb b/db/migrate/20240410212508_drop_accredited_organizations.rb new file mode 100644 index 00000000000..3cce9757e53 --- /dev/null +++ b/db/migrate/20240410212508_drop_accredited_organizations.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class DropAccreditedOrganizations < ActiveRecord::Migration[7.1] + def change + drop_table :accredited_organizations, if_exists: true # rubocop:disable Rails/ReversibleMigration + end +end diff --git a/db/migrate/20240410212528_drop_accredited_representatives.rb b/db/migrate/20240410212528_drop_accredited_representatives.rb new file mode 100644 index 00000000000..e557818c486 --- /dev/null +++ b/db/migrate/20240410212528_drop_accredited_representatives.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class DropAccreditedRepresentatives < ActiveRecord::Migration[7.1] + def change + drop_table :accredited_representatives, if_exists: true # rubocop:disable Rails/ReversibleMigration + end +end diff --git a/db/migrate/20240410212702_drop_accredited_claims_agents.rb b/db/migrate/20240410212702_drop_accredited_claims_agents.rb new file mode 100644 index 00000000000..0bfd4f06741 --- /dev/null +++ b/db/migrate/20240410212702_drop_accredited_claims_agents.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class DropAccreditedClaimsAgents < ActiveRecord::Migration[7.1] + def change + drop_table :accredited_claims_agents, if_exists: true # rubocop:disable Rails/ReversibleMigration + end +end diff --git a/db/migrate/20240410212727_drop_accredited_attorneys.rb b/db/migrate/20240410212727_drop_accredited_attorneys.rb new file mode 100644 index 00000000000..73ab0e0273b --- /dev/null +++ b/db/migrate/20240410212727_drop_accredited_attorneys.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class DropAccreditedAttorneys < ActiveRecord::Migration[7.1] + def change + drop_table :accredited_attorneys, if_exists: true # rubocop:disable Rails/ReversibleMigration + end +end diff --git a/db/schema.rb b/db/schema.rb index 71001dcade6..92987f8f693 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2024_03_19_141429) do +ActiveRecord::Schema[7.1].define(version: 2024_04_10_212727) do # These are extensions that must be enabled in order to support this database enable_extension "btree_gin" enable_extension "pg_stat_statements" @@ -339,6 +339,8 @@ t.text "terms_of_use_url" t.text "enforced_terms" t.boolean "shared_sessions", default: false, null: false + t.string "service_levels", default: ["ial1", "ial2", "loa1", "loa3", "min"], array: true + t.string "credential_service_providers", default: ["logingov", "idme", "dslogon", "mhv"], array: true t.index ["client_id"], name: "index_client_configs_on_client_id", unique: true end @@ -727,6 +729,7 @@ t.text "form_data_ciphertext" t.text "encrypted_kms_key" t.uuid "user_account_id" + t.integer "status", default: 0 t.index ["form_id", "user_uuid"], name: "index_in_progress_forms_on_form_id_and_user_uuid", unique: true t.index ["user_account_id"], name: "index_in_progress_forms_on_user_account_id" t.index ["user_uuid"], name: "index_in_progress_forms_on_user_uuid" @@ -841,6 +844,8 @@ t.string "error_class", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.text "data_ciphertext" + t.text "encrypted_kms_key" t.index ["created_at"], name: "index_personal_information_logs_on_created_at" t.index ["error_class"], name: "index_personal_information_logs_on_error_class" end diff --git a/db/seeds/development.rb b/db/seeds/development.rb index 00a1512f296..aa5195e17ac 100644 --- a/db/seeds/development.rb +++ b/db/seeds/development.rb @@ -110,7 +110,7 @@ access_token_audience: 'http://localhost:3978/api/messages', access_token_user_attributes: ['icn'], access_token_duration: SignIn::Constants::ServiceAccountAccessToken::VALIDITY_LENGTH_SHORT_MINUTES, - certificates: [File.read('spec/fixtures/sign_in/sample_service_account.crt')] + certificates: [File.read('spec/fixtures/sign_in/sts_client.crt')] ) # Create config for accredited_representative_portal @@ -124,3 +124,14 @@ access_token_attributes: %w[first_name last_name email], refresh_token_duration: SignIn::Constants::RefreshToken::VALIDITY_LENGTH_SHORT_MINUTES, logout_redirect_uri: 'http://localhost:3001/representatives') + +# Create Service Account Config for BTSSS +btsss = SignIn::ServiceAccountConfig.find_or_initialize_by(service_account_id: 'bbb5830ecebdef04556e9c430e374972') +btsss.update!( + description: 'BTSSS', + scopes: [], + access_token_audience: 'http://localhost:3000', + access_token_user_attributes: ['icn'], + access_token_duration: SignIn::Constants::ServiceAccountAccessToken::VALIDITY_LENGTH_SHORT_MINUTES, + certificates: [File.read('spec/fixtures/sign_in/sts_client.crt')] +) diff --git a/docker-compose-deps.yml b/docker-compose-deps.yml index 7a630300ffb..78f564a5b42 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.test.yml b/docker-compose.test.yml index 4c45e51b195..49f7f798f3e 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -35,7 +35,7 @@ x-app: &common services: redis: - image: redis:5.0-alpine + image: redis:6.2-alpine ports: - 6379:6379 postgres: diff --git a/docker-compose.yml b/docker-compose.yml index 06238577f5e..a19def5b318 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -35,7 +35,7 @@ services: volumes: - shared-vol:/vets-api 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/central_mail/datestamp_pdf.rb b/lib/central_mail/datestamp_pdf.rb index fc5c83ee886..25681203567 100644 --- a/lib/central_mail/datestamp_pdf.rb +++ b/lib/central_mail/datestamp_pdf.rb @@ -13,7 +13,7 @@ def initialize(file_path, append_to_stamp: nil) def run(settings) stamp_path = Common::FileHelpers.random_file_path generate_stamp(stamp_path, settings[:text], settings[:x], settings[:y], settings[:text_only], settings[:size], - settings[:timestamp], settings[:page_number], settings[:template]) + settings[:timestamp], settings[:page_number], settings[:template], @file_path) stamp(@file_path, stamp_path, multistamp: settings[:multistamp]) ensure Common::FileHelpers.delete_file_if_exists(stamp_path) if defined?(stamp_path) @@ -21,10 +21,15 @@ def run(settings) # rubocop:disable Metrics/ParameterLists # rubocop:disable Metrics/MethodLength - def generate_stamp(stamp_path, text, x, y, text_only, size = 10, timestamp = nil, page_number = nil, template = nil) + def generate_stamp(stamp_path, text, x, y, text_only, size = 10, timestamp = nil, page_number = nil, + template = nil, file_path = nil) timestamp ||= Time.zone.now unless text_only - text += " #{I18n.l(timestamp, format: :pdf_stamp)}" + text += if file_path == 'tmp/vba_40_10007-stamped.pdf' + " #{I18n.l(timestamp, format: :pdf_stamp4010007)}" + else + " #{I18n.l(timestamp, format: :pdf_stamp)}" + end text += ". #{@append_to_stamp}" if @append_to_stamp end 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/common/models/redis_store.rb b/lib/common/models/redis_store.rb index 755a92a5980..fa89590beb1 100644 --- a/lib/common/models/redis_store.rb +++ b/lib/common/models/redis_store.rb @@ -48,6 +48,8 @@ def initialize(attributes = {}, persisted = false) end def self.find(redis_key = nil) + return nil if redis_key.nil? + response = redis_namespace.get(redis_key) return nil unless response 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/decision_review_v1/appeals/supplemental_claim_services.rb b/lib/decision_review_v1/appeals/supplemental_claim_services.rb index 48302264d0c..53174039848 100644 --- a/lib/decision_review_v1/appeals/supplemental_claim_services.rb +++ b/lib/decision_review_v1/appeals/supplemental_claim_services.rb @@ -3,11 +3,14 @@ require 'decision_review_v1/utilities/form_4142_processor' require 'decision_review_v1/utilities/helpers' require 'decision_review_v1/utilities/constants' +require 'decision_review_v1/utilities/logging_utils' module DecisionReviewV1 module Appeals + # rubocop:disable Metrics/ModuleLength module SupplementalClaimServices include DecisionReviewV1::Appeals::Helpers + include DecisionReviewV1::Appeals::LoggingUtils ## # Returns all of the data associated with a specific Supplemental Claim. @@ -36,9 +39,15 @@ def create_supplemental_claim(request_body:, user:) with_monitoring_and_error_handling do request_body = request_body.to_json if request_body.is_a?(Hash) headers = create_supplemental_claims_headers(user) + common_log_params = { key: :overall_claim_submission, form_id: '995', user_uuid: user.uuid, + downstream_system: 'Lighthouse' } response, bm = run_and_benchmark_if_enabled do perform :post, 'supplemental_claims', request_body, headers + rescue => e + log_formatted(**common_log_params.merge(is_success: false, response_error: e)) + raise e end + log_formatted(**common_log_params.merge(is_success: true, status_code: response.status, body: '[Redacted]')) raise_schema_error_unless_200_status response.status validate_against_schema json: response.body, schema: SC_CREATE_RESPONSE_SCHEMA, append_to_error_class: ' (SC_V1)' @@ -82,7 +91,17 @@ def get_supplemental_claim_contestable_issues(user:, benefit_type:) with_monitoring_and_error_handling do path = "contestable_issues/supplemental_claims?benefit_type=#{benefit_type}" headers = get_contestable_issues_headers(user) - response = perform :get, path, nil, headers + common_log_params = { key: :get_contestable_issues, form_id: '995', user_uuid: user.uuid, + upstream_system: 'Lighthouse' } + begin + response = perform :get, path, nil, headers + log_formatted(**common_log_params.merge(is_success: true, status_code: response.status, body: '[Redacted]')) + rescue => e + # We can freely log Lighthouse's error responses because they do not include PII or PHI. + # See https://developer.va.gov/explore/api/decision-reviews/docs?version=v1. + log_formatted(**common_log_params.merge(is_success: false, response_error: e)) + raise e + end raise_schema_error_unless_200_status response.status validate_against_schema( json: response.body, @@ -100,10 +119,28 @@ def get_supplemental_claim_contestable_issues(user:, benefit_type:) # @param file_number [Integer] The file number or ssn # @return [Faraday::Response] # - def get_supplemental_claim_upload_url(sc_uuid:, file_number:) + def get_supplemental_claim_upload_url(sc_uuid:, file_number:, user_uuid: nil, appeal_submission_upload_id: nil) + common_log_params = { + key: :get_lighthouse_evidence_upload_url, + form_id: '995', + user_uuid:, + upstream_system: 'Lighthouse', + downstream_system: 'Lighthouse', + params: { + sc_uuid:, + appeal_submission_upload_id: + } + } with_monitoring_and_error_handling do - perform :post, 'supplemental_claims/evidence_submissions', { sc_uuid: }, - { 'X-VA-SSN' => file_number.to_s.strip.presence } + response = perform :post, 'supplemental_claims/evidence_submissions', { sc_uuid: }, + { 'X-VA-SSN' => file_number.to_s.strip.presence } + log_formatted(**common_log_params.merge(is_success: true, status_code: response.status, body: response.body)) + response + rescue => e + # We can freely log Lighthouse's error responses because they do not include PII or PHI. + # See https://developer.va.gov/explore/api/decision-reviews/docs?version=v2 + log_formatted(**common_log_params.merge(is_success: false, response_error: e)) + raise e end end @@ -116,7 +153,9 @@ def get_supplemental_claim_upload_url(sc_uuid:, file_number:) # # @return [Faraday::Response] # - def put_supplemental_claim_upload(upload_url:, file_upload:, metadata_string:) + # rubocop:disable Metrics/MethodLength + def put_supplemental_claim_upload(upload_url:, file_upload:, metadata_string:, user_uuid: nil, + appeal_submission_upload_id: nil) content_tmpfile = Tempfile.new(file_upload.filename, encoding: file_upload.read.encoding) content_tmpfile.write(file_upload.read) content_tmpfile.rewind @@ -130,8 +169,25 @@ def put_supplemental_claim_upload(upload_url:, file_upload:, metadata_string:) # when we upgrade to Faraday >1.0 # params = { metadata: Faraday::FilePart.new(json_tmpfile, Mime[:json].to_s, 'metadata.json'), # content: Faraday::FilePart.new(content_tmpfile, Mime[:pdf].to_s, file_upload.filename) } + common_log_params = { + key: :evidence_upload_to_lighthouse, + form_id: '995', + user_uuid:, + downstream_system: 'Lighthouse', + params: { + upload_url:, + appeal_submission_upload_id: + } + } with_monitoring_and_error_handling do - perform :put, upload_url, params, { 'Content-Type' => 'multipart/form-data' } + response = perform :put, upload_url, params, { 'Content-Type' => 'multipart/form-data' } + log_formatted(**common_log_params.merge(is_success: true, status_code: response.status, body: '[Redacted]')) + response + rescue => e + # We can freely log Lighthouse's error responses because they do not include PII or PHI. + # See https://developer.va.gov/explore/api/decision-reviews/docs?version=v2 + log_formatted(**common_log_params.merge(is_success: false, response_error: e)) + raise e end ensure content_tmpfile.close @@ -139,6 +195,7 @@ def put_supplemental_claim_upload(upload_url:, file_upload:, metadata_string:) json_tmpfile.close json_tmpfile.unlink end + # rubocop:enable Metrics/MethodLength ## # Returns all of the data associated with a specific Supplemental Claim Evidence Submission. @@ -192,5 +249,6 @@ def submit_form4142(form_data:) CentralMail::Service.new.upload(processor.request_body) end end + # rubocop:enable Metrics/ModuleLength end end diff --git a/lib/decision_review_v1/service.rb b/lib/decision_review_v1/service.rb index 86eee1cf5ef..4c246bd91e6 100644 --- a/lib/decision_review_v1/service.rb +++ b/lib/decision_review_v1/service.rb @@ -37,7 +37,15 @@ class Service < Common::Client::Base def create_higher_level_review(request_body:, user:) with_monitoring_and_error_handling do headers = create_higher_level_review_headers(user) - response = perform :post, 'higher_level_reviews', request_body, headers + common_log_params = { key: :overall_claim_submission, form_id: '996', user_uuid: user.uuid, + downstream_system: 'Lighthouse' } + begin + response = perform :post, 'higher_level_reviews', request_body, headers + log_formatted(**common_log_params.merge(is_success: true, status_code: response.status, body: '[Redacted]')) + rescue => e + log_formatted(**common_log_params.merge(is_success: false, response_error: e)) + raise e + end raise_schema_error_unless_200_status response.status validate_against_schema json: response.body, schema: HLR_CREATE_RESPONSE_SCHEMA, append_to_error_class: ' (HLR_V1)' @@ -72,7 +80,17 @@ def get_higher_level_review_contestable_issues(user:, benefit_type:) with_monitoring_and_error_handling do path = "contestable_issues/higher_level_reviews?benefit_type=#{benefit_type}" headers = get_contestable_issues_headers(user) - response = perform :get, path, nil, headers + common_log_params = { key: :get_contestable_issues, form_id: '996', user_uuid: user.uuid, + upstream_system: 'Lighthouse' } + begin + response = perform :get, path, nil, headers + log_formatted(**common_log_params.merge(is_success: true, status_code: response.status, body: '[Redacted]')) + rescue => e + # We can freely log Lighthouse's error responses because they do not include PII or PHI. + # See https://developer.va.gov/explore/api/decision-reviews/docs?version=v1. + log_formatted(**common_log_params.merge(is_success: false, response_error: e)) + raise e + end raise_schema_error_unless_200_status response.status validate_against_schema( json: response.body, 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/form1010_ezr/service.rb b/lib/form1010_ezr/service.rb index ee795553758..d43ee9ab577 100644 --- a/lib/form1010_ezr/service.rb +++ b/lib/form1010_ezr/service.rb @@ -48,6 +48,9 @@ def submit_sync(parsed_form) # @param [HashWithIndifferentAccess] parsed_form JSON form data def submit_form(parsed_form) + # Log the 'veteranDateOfBirth' to ensure the frontend validation is working as intended + # REMOVE THE FOLLOWING TWO LINES OF CODE ONCE THE DOB ISSUE HAS BEEN DIAGNOSED - 3/27/24 + @unprocessed_user_dob = parsed_form['veteranDateOfBirth'].clone parsed_form = configure_and_validate_form(parsed_form) if Flipper.enabled?(:ezr_async, @user) @@ -92,6 +95,14 @@ def validate_form(parsed_form) validation_errors = JSON::Validator.fully_validate(schema, parsed_form) if validation_errors.present? + # REMOVE THE FOLLOWING SIX LINES OF CODE ONCE THE DOB ISSUE HAS BEEN DIAGNOSED - 3/27/24 + if validation_errors.find { |error| error.include?('veteranDateOfBirth') }.present? + PersonalInformationLog.create!( + data: @unprocessed_user_dob, + error_class: "Form1010Ezr 'veteranDateOfBirth' schema failure" + ) + end + log_validation_errors(parsed_form) Rails.logger.error('10-10EZR form validation failed. Form does not match schema.') diff --git a/lib/in_progress_form_status_default.rb b/lib/in_progress_form_status_default.rb new file mode 100644 index 00000000000..7ec746d57d4 --- /dev/null +++ b/lib/in_progress_form_status_default.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module DataMigrations + module InProgressFormStatusDefault + module_function + + def run + InProgressForm.where(status: nil).update_all(status: 'pending') # rubocop:disable Rails/SkipsModelValidations + end + end +end diff --git a/lib/lgy/service.rb b/lib/lgy/service.rb index 34e6b3461fd..3ddc4e59957 100644 --- a/lib/lgy/service.rb +++ b/lib/lgy/service.rb @@ -167,7 +167,7 @@ def post_grant_application(payload:) :post, "#{grant_manager_end_point}/application/createGrantApplication", payload.to_json, - request_headers + sahsha_request_headers ) end rescue Common::Client::Errors::ClientError => e @@ -180,6 +180,14 @@ def request_headers } end + def sahsha_request_headers + { + Authorization: "api-key { \"appId\":\"#{Settings.lgy_sahsha.app_id}\", \"apiKey\": \"#{ + Settings.lgy_sahsha.api_key + }\"}" + } + end + def pdf_headers { 'Accept' => 'application/octet-stream', 'Content-Type' => 'application/octet-stream' @@ -193,7 +201,7 @@ def end_point end def grant_manager_end_point - "#{Settings.lgy.base_url}/grant-manager/api/grants" + "#{Settings.lgy_sahsha.base_url}/grant-manager/api/grants" end end end diff --git a/lib/lighthouse/benefits_claims/service.rb b/lib/lighthouse/benefits_claims/service.rb index 3e4313cf0e1..46f200a43ea 100644 --- a/lib/lighthouse/benefits_claims/service.rb +++ b/lib/lighthouse/benefits_claims/service.rb @@ -38,6 +38,14 @@ def get_claim(id, lighthouse_client_id = nil, lighthouse_rsa_key_path = nil, opt raise BenefitsClaims::ServiceException.new(e.response), 'Lighthouse Error' end + def get_power_of_attorney(lighthouse_client_id = nil, lighthouse_rsa_key_path = nil, options = {}) + config.get("#{@icn}/power-of-attorney", lighthouse_client_id, lighthouse_rsa_key_path, options).body + rescue Faraday::TimeoutError + raise BenefitsClaims::ServiceException.new({ status: 504 }), 'Lighthouse Error' + rescue Faraday::ClientError, Faraday::ServerError => e + raise BenefitsClaims::ServiceException.new(e.response), 'Lighthouse Error' + end + def submit5103(user, id, options = {}) params = {} is_dependent = SponsorResolver.dependent?(user) diff --git a/lib/lighthouse/direct_deposit/control_information.rb b/lib/lighthouse/direct_deposit/control_information.rb index 893938a41dc..4ed67551aeb 100644 --- a/lib/lighthouse/direct_deposit/control_information.rb +++ b/lib/lighthouse/direct_deposit/control_information.rb @@ -5,18 +5,57 @@ module DirectDeposit class ControlInformation include ActiveModel::Model - attr_accessor :can_update_direct_deposit, - :is_corp_available, - :is_corp_rec_found, - :has_no_bdn_payments, - :has_identity, - :has_index, - :is_competent, - :has_mailing_address, - :has_no_fiduciary_assigned, - :is_not_deceased, - :has_payment_address, - :is_edu_claim_available + ACTIONS = [:can_update_direct_deposit].freeze + USAGES = %i[is_corp_available is_edu_claim_available].freeze + RESTRICTIONS = %i[ + is_corp_rec_found + has_no_bdn_payments + has_identity + has_index + is_competent + has_mailing_address + has_no_fiduciary_assigned + is_not_deceased + has_payment_address + ].freeze + + attr_accessor(*(ACTIONS + USAGES + RESTRICTIONS)) + attr_reader :errors + + alias :comp_and_pen? is_corp_available + alias :edu_benefits? is_edu_claim_available + + def account_updatable? + @can_update_direct_deposit && restrictions.size.zero? + end + + def benefit_type? + comp_and_pen? || edu_benefits? + end + + def restrictions + RESTRICTIONS.reject { |name| send(name) } + end + + def clear_restrictions + @can_update_direct_deposit = true + RESTRICTIONS.each { |name| send("#{name}=", true) } + end + + def valid? + @errors = [] + + error = 'Has restrictions. Account should not be updatable.' + errors << error if @can_update_direct_deposit && restrictions.any? + + error = 'Has no restrictions. Account should be updatable.' + errors << error if !@can_update_direct_deposit && restrictions.empty? + + error = 'Missing benefit type. Must be either CnP or EDU benefits.' + errors << error unless benefit_type? + + errors.size.zero? + end end end end diff --git a/lib/lighthouse/direct_deposit/error_parser.rb b/lib/lighthouse/direct_deposit/error_parser.rb index 5539399340a..954a0b7ebe8 100644 --- a/lib/lighthouse/direct_deposit/error_parser.rb +++ b/lib/lighthouse/direct_deposit/error_parser.rb @@ -44,29 +44,35 @@ def self.parse_detail(body) end def self.parse_code(detail) # rubocop:disable Metrics/MethodLength - return 'cnp.payment.api.rate.limit.exceeded' if detail.include? 'API rate limit exceeded' - return 'cnp.payment.api.gateway.timeout' if detail.include? 'Did not receive a timely response' - return 'cnp.payment.invalid.authentication.creds' if detail.include? 'Invalid authentication credentials' - return 'cnp.payment.invalid.token' if detail.include? 'Invalid token' - return 'cnp.payment.invalid.scopes' if detail.include? 'scopes are not configured' - return 'cnp.payment.icn.not.found' if detail.include? 'No data found for ICN' - return 'cnp.payment.icn.invalid' if detail.include? 'getDirectDeposit.icn size' - return 'cnp.payment.account.number.invalid' if detail.include? 'payment.accountNumber.invalid' - return 'cnp.payment.account.type.invalid' if detail.include? 'payment.accountType.invalid' - return 'cnp.payment.account.number.fraud' if detail.include? 'Flashes on record' - return 'cnp.payment.routing.number.invalid.checksum' if detail.include? 'accountRoutingNumber.invalidCheckSum' - return 'cnp.payment.routing.number.invalid' if detail.include? 'payment.accountRoutingNumber.invalid' - return 'cnp.payment.routing.number.fraud' if detail.include? 'Routing number related to potential fraud' - return 'cnp.payment.restriction.indicators.present' if detail.include? 'restriction.indicators.present' - return 'cnp.payment.day.phone.number.invalid' if detail.include? 'Day phone number is invalid' - return 'cnp.payment.day.area.number.invalid' if detail.include? 'Day area number is invalid' - return 'cnp.payment.night.phone.number.invalid' if detail.include? 'Night phone number is invalid' - return 'cnp.payment.night.area.number.invalid' if detail.include? 'Night area number is invalid' - return 'cnp.payment.mailing.address.invalid' if detail.include? 'field not entered for mailing address update' - return 'cnp.payment.potential.fraud' if detail.include? 'GUIE50041' - return 'cnp.payment.unspecified.error' if detail.include? 'GUIE50022' + return "#{prefix}.api.rate.limit.exceeded" if detail.include? 'API rate limit exceeded' + return "#{prefix}.api.gateway.timeout" if detail.include? 'Did not receive a timely response' + return "#{prefix}.invalid.authentication.creds" if detail.include? 'Invalid authentication credentials' + return "#{prefix}.invalid.token" if detail.include? 'Invalid token' + return "#{prefix}.invalid.scopes" if detail.include? 'scopes are not configured' + return "#{prefix}.icn.not.found" if detail.include? 'No data found for ICN' + return "#{prefix}.icn.invalid" if detail.include? 'getDirectDeposit.icn size' + return "#{prefix}.account.number.invalid" if detail.include? 'payment.accountNumber.invalid' + return "#{prefix}.account.type.invalid" if detail.include? 'payment.accountType.invalid' + return "#{prefix}.account.number.fraud" if detail.include? 'Flashes on record' + return "#{prefix}.routing.number.invalid.checksum" if detail.include? 'accountRoutingNumber.invalidCheckSum' + return "#{prefix}.routing.number.invalid" if detail.include? 'payment.accountRoutingNumber.invalid' + return "#{prefix}.routing.number.fraud" if detail.include? 'Routing number related to potential fraud' + return "#{prefix}.restriction.indicators.present" if detail.include? 'restriction.indicators.present' + return "#{prefix}.day.phone.number.invalid" if detail.include? 'Day phone number is invalid' + return "#{prefix}.day.area.number.invalid" if detail.include? 'Day area number is invalid' + return "#{prefix}.night.phone.number.invalid" if detail.include? 'Night phone number is invalid' + return "#{prefix}.night.area.number.invalid" if detail.include? 'Night area number is invalid' + return "#{prefix}.mailing.address.invalid" if detail.include? 'field not entered for mailing address update' + return "#{prefix}.potential.fraud" if detail.include? 'GUIE50041' + return "#{prefix}.unspecified.error" if detail.include? 'GUIE50022' - 'cnp.payment.generic.error' + "#{prefix}.generic.error" + end + + def self.prefix + return 'direct.deposit' if Flipper.enabled?(:profile_show_direct_deposit_single_form) + + 'cnp.payment' end def self.data_source diff --git a/lib/lighthouse/facilities/v1/client.rb b/lib/lighthouse/facilities/v1/client.rb new file mode 100644 index 00000000000..60296a7d7c7 --- /dev/null +++ b/lib/lighthouse/facilities/v1/client.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'common/client/base' +require_relative 'response' +require_relative '../configuration' + +module Lighthouse + module Facilities + module V1 + # Documentation located at: + # https://developer.va.gov/explore/facilities/docs/facilities + class Client < Common::Client::Base + configuration Lighthouse::Facilities::Configuration + + ## + # Request a list of facilities matching the params provided + # @param params [Hash] a hash of parameter objects that must include bbox, ids, or lat and long + # see https://developer.va.gov/explore/facilities/docs/facilities for more options + # @example client.get_facilities(bbox: [60.99, 10.54, 180.00, 20.55]) + # @example client.get_facilities(ids: 'vha_358,vba_358') + # @example client.get_facilities(lat: 10.54, long: 180.00, per_page: 50, page: 2) + # @return [Array] + # + def get_facilities(params) + response = perform(:get, '/services/va_facilities/v1/facilities', params) + facilities = Lighthouse::Facilities::V1::Response.new(response.body, response.status).facilities + facilities.reject!(&:mobile?) if params['exclude_mobile'] + facilities + end + end + end + end +end diff --git a/lib/lighthouse/facilities/v1/response.rb b/lib/lighthouse/facilities/v1/response.rb new file mode 100644 index 00000000000..f67e0368be3 --- /dev/null +++ b/lib/lighthouse/facilities/v1/response.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'common/models/base' +require_relative '../facility' + +module Lighthouse + module Facilities + module V1 + class Response < Common::Base + attribute :body, String + attribute :current_page, Integer + attribute :data, Object + attribute :links, Object + attribute :meta, Object + attribute :per_page, Integer + attribute :status, Integer + attribute :total_entries, Integer + + def initialize(body, status) + super() + self.body = body + self.status = status + parsed_body = JSON.parse(body) + self.data = parsed_body['data'] + self.meta = parsed_body['meta'] + self.links = parsed_body['links'] + if meta + self.current_page = meta['pagination']['currentPage'] + self.per_page = meta['pagination']['perPage'] + self.total_entries = meta['pagination']['totalEntries'] + end + end + + def facilities + facilities = data.each_with_index.map do |facility, index| + facility['attributes'] = facility['attributes'].transform_keys(&:underscore) + fac = Lighthouse::Facilities::Facility.new(facility) + fac.distance = meta['distances'][index]['distance'] if meta['distances'].present? + fac + end + + WillPaginate::Collection.create(current_page, per_page) do |pager| + pager.replace(facilities) + pager.total_entries = total_entries + end + end + + def facility + Lighthouse::Facilities::Facility.new(data) + end + end + end + end +end diff --git a/lib/lighthouse/service_exception.rb b/lib/lighthouse/service_exception.rb index 45d7ae831e8..f57b5d899c0 100644 --- a/lib/lighthouse/service_exception.rb +++ b/lib/lighthouse/service_exception.rb @@ -86,12 +86,19 @@ def self.transform_error_keys(error_body, status, title, detail, code) .transform_keys(&:to_sym) end - # sends errors to sentry! + # log errors def self.send_error_logs(error, service_name, lighthouse_client_id, url) - base_key_string = "#{lighthouse_client_id} #{url} Lighthouse Error" + # Faraday error may contain request data + error.response.delete(:request) + Rails.logger.error( - error.response, - base_key_string + service_name, + { + url:, + lighthouse_client_id:, + status: error.response[:status], + body: error.response[:body] + } ) extra_context = Sentry.set_extras( diff --git a/lib/medical_records/client.rb b/lib/medical_records/client.rb index 5a4078aaeb3..df9f4d1adf1 100644 --- a/lib/medical_records/client.rb +++ b/lib/medical_records/client.rb @@ -89,11 +89,8 @@ def get_patient_by_identifier(fhir_client, identifier) def list_allergies bundle = fhir_search(FHIR::AllergyIntolerance, - { - search: { parameters: { patient: patient_fhir_id, 'clinical-status': 'active', - 'verification-status:not': 'entered-in-error' } }, - headers: { 'Cache-Control': 'no-cache' } - }) + search: { parameters: { patient: patient_fhir_id, 'clinical-status': 'active', + 'verification-status:not': 'entered-in-error' } }) sort_bundle(bundle, :recordedDate, :desc) end @@ -103,10 +100,7 @@ def get_allergy(allergy_id) def list_vaccines bundle = fhir_search(FHIR::Immunization, - { - search: { parameters: { patient: patient_fhir_id, 'status:not': 'entered-in-error' } }, - headers: { 'Cache-Control': 'no-cache' } - }) + search: { parameters: { patient: patient_fhir_id, 'status:not': 'entered-in-error' } }) sort_bundle(bundle, :occurrenceDateTime, :desc) end @@ -139,11 +133,8 @@ def get_condition(condition_id) def list_clinical_notes loinc_codes = "#{PHYSICIAN_PROCEDURE_NOTE},#{DISCHARGE_SUMMARY},#{CONSULT_RESULT}" bundle = fhir_search(FHIR::DocumentReference, - { - search: { parameters: { patient: patient_fhir_id, type: loinc_codes, - 'status:not': 'entered-in-error' } }, - headers: { 'Cache-Control': 'no-cache' } - }) + search: { parameters: { patient: patient_fhir_id, type: loinc_codes, + 'status:not': 'entered-in-error' } }) # Sort the bundle of notes based on the date field appropriate to each note type. sort_bundle_with_criteria(bundle, :desc) do |resource| @@ -283,7 +274,11 @@ def fhir_search(fhir_model, params) # @return [FHIR::ClientReply] # def fhir_search_query(fhir_model, params) + default_headers = { 'Cache-Control': 'no-cache' } + params[:headers] = default_headers.merge(params.fetch(:headers, {})) + params[:search][:parameters].merge!(_count: DEFAULT_COUNT) + result = fhir_client.search(fhir_model, params) handle_api_errors(result) if result.resource.nil? result 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-527EZ.pdf b/lib/pdf_fill/forms/pdfs/21P-527EZ.pdf index 251ba71df27..ecc7198e6f5 100644 Binary files a/lib/pdf_fill/forms/pdfs/21P-527EZ.pdf and b/lib/pdf_fill/forms/pdfs/21P-527EZ.pdf differ 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..66c7e245884 Binary files /dev/null and b/lib/pdf_fill/forms/pdfs/21P-530V2.pdf differ diff --git a/lib/pdf_fill/forms/va21p527ez.rb b/lib/pdf_fill/forms/va21p527ez.rb index 62ff987147a..905bd9f89b3 100644 --- a/lib/pdf_fill/forms/va21p527ez.rb +++ b/lib/pdf_fill/forms/va21p527ez.rb @@ -1343,7 +1343,7 @@ def expand_veteran_service_information @form_data['serviceBranch'] = @form_data['serviceBranch']&.select { |_, value| value == true } @form_data['pow'] = to_radio_yes_no(@form_data['powDateRange'].present?) - if @form_data['pow'].zero? + if @form_data['pow'] == 1 @form_data['powDateRange'] ||= {} @form_data['powDateRange']['from'] = split_date(@form_data.dig('powDateRange', 'from')) @form_data['powDateRange']['to'] = split_date(@form_data.dig('powDateRange', 'to')) @@ -1372,10 +1372,10 @@ def expand_pension_information ) # If "YES," skip question 4B - @form_data['medicalCondition'] = 'Off' if @form_data['socialSecurityDisability'].zero? + @form_data['medicalCondition'] = 'Off' if @form_data['socialSecurityDisability'] == 1 # If "NO," skip question 4D - @form_data['medicaidStatus'] = 'Off' if @form_data['nursingHome'] == 1 + @form_data['medicaidStatus'] = 'Off' if @form_data['nursingHome'] == 2 @form_data['vaTreatmentHistory'] = to_radio_yes_no(@form_data['vaTreatmentHistory']) @form_data['federalTreatmentHistory'] = to_radio_yes_no(@form_data['federalTreatmentHistory']) @@ -1392,7 +1392,7 @@ def expand_employment_history }) end - @form_data['currentEmployers'] = nil if @form_data['currentEmployment'] == 1 + @form_data['currentEmployers'] = nil if @form_data['currentEmployment'] == 2 end # SECTION VI: MARITAL STATUS @@ -1575,7 +1575,7 @@ def expand_income_and_assets end @form_data['transferredAssets'] = to_radio_yes_no(@form_data['transferredAssets']) @form_data['homeOwnership'] = to_radio_yes_no(@form_data['homeOwnership']) - if (@form_data['homeOwnership']).zero? + if (@form_data['homeOwnership']) == 1 @form_data['homeAcreageMoreThanTwo'] = to_radio_yes_no(@form_data['homeAcreageMoreThanTwo']) @form_data['landMarketable'] = to_radio_yes_no(@form_data['landMarketable']) end @@ -1711,7 +1711,7 @@ def to_checkbox_on_off(obj) end def to_radio_yes_no(obj) - obj ? 0 : 1 + obj ? 1 : 2 end end end diff --git a/lib/pdf_fill/forms/va21p530v2.rb b/lib/pdf_fill/forms/va21p530v2.rb new file mode 100644 index 00000000000..d65a4fe91dd --- /dev/null +++ b/lib/pdf_fill/forms/va21p530v2.rb @@ -0,0 +1,773 @@ +# 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" + }, + 'middleInitial' => { + 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" + }, + 'middleInitial' => { + 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: 14, + question_suffix: 'D', + question_text: 'GRADE, RANK OR RATING, ORGANIZATION AND BRANCH OF SERVICE', + limit: 31 + }, + 'unit' => { + key: "form1[0].#subform[82].GRADE_RANK_OR_RATING_UNIT[#{ITERATOR}]", + question_num: 14, + question_suffix: 'D', + question_text: 'UNIT', + limit: 0 + } + }, + '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 + }, + 'placeAndLocation' => { + limit: 75, + question_num: 16, + question_text: "PLEASE PROVIDE VETERAN'S SPECIFIC PLACE OF DEATH INCLUDING THE NAME AND LOCATION OF THE NURSING HOME, VA MEDICAL CENTER OR STATE VETERAN FACILITY.", + key: 'form1[0].#subform[37].DeathOccurredPlaceAndLocation[1]' + } + }, + '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 sanitize_phone(phone) + phone.gsub('-', '') + end + + def split_phone(hash, key) + phone = hash[key] + return if phone.blank? + + phone = sanitize_phone(phone) + 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? + + if location_of_death[location_of_death['location']].present? && location_of_death['location'] != 'other' + options = location_of_death[location_of_death['location']] + location_of_death['placeAndLocation'] = "#{options['facilityName']} - #{options['facilityLocation']}" + end + + 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 + + def format_currency_spacing + return if @form_data['amountGovtContribution'].blank? + + @form_data['amountGovtContribution'] = @form_data['amountGovtContribution'].rjust(5) + end + + def set_state_to_no_if_national + national = @form_data['nationalOrFederal'] + @form_data['cemetaryLocationQuestion'] = 'none' if national + 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') + } + + # special case for transportation being the only option selected. + final_resting_place = @form_data.dig('finalRestingPlace', 'location') + if final_resting_place.present? + @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') + } + end + + 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 + set_state_to_no_if_national + 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 + + format_currency_spacing + + %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/monitor.rb b/lib/pension_21p527ez/monitor.rb new file mode 100644 index 00000000000..a7e2b52fe0a --- /dev/null +++ b/lib/pension_21p527ez/monitor.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +module Pension21p527ez + class Monitor + CLAIM_STATS_KEY = 'api.pension_claim' + SUBMISSION_STATS_KEY = 'worker.lighthouse.pension_benefit_intake_job' + + def track_show404(confirmation_number, current_user, e) + Rails.logger.error('21P-527EZ submission not found', + { confirmation_number:, user_uuid: current_user&.uuid, message: e&.message }) + end + + def track_show_error(confirmation_number, current_user, e) + Rails.logger.error('21P-527EZ fetching submission failed', + { confirmation_number:, user_uuid: current_user&.uuid, message: e&.message }) + end + + def track_create_attempt(claim, current_user) + StatsD.increment("#{CLAIM_STATS_KEY}.attempt") + Rails.logger.info('21P-527EZ submission to Sidekiq begun', + { confirmation_number: claim&.confirmation_number, user_uuid: current_user&.uuid }) + end + + def track_create_error(in_progress_form, claim, current_user, e = nil) + StatsD.increment("#{CLAIM_STATS_KEY}.failure") + Rails.logger.error('21P-527EZ submission to Sidekiq failed', + { confirmation_number: claim&.confirmation_number, user_uuid: current_user&.uuid, + in_progress_form_id: in_progress_form&.id, errors: claim&.errors&.errors, + message: e&.message }) + end + + def track_create_success(in_progress_form, claim, current_user) + StatsD.increment("#{CLAIM_STATS_KEY}.success") + Rails.logger.info('21P-527EZ submission to Sidekiq success', + { confirmation_number: claim&.confirmation_number, user_uuid: current_user&.uuid, + in_progress_form_id: in_progress_form&.id }) + end + + def track_submission_begun(claim, lighthouse_service, user_uuid) + StatsD.increment("#{SUBMISSION_STATS_KEY}.begun") + Rails.logger.info('Lighthouse::PensionBenefitIntakeJob submission to LH begun', + { + claim_id: claim&.id, + benefits_intake_uuid: lighthouse_service&.uuid, + confirmation_number: claim&.confirmation_number, + user_uuid: + }) + end + + def track_submission_attempted(claim, lighthouse_service, user_uuid, payload) + StatsD.increment("#{SUBMISSION_STATS_KEY}.attempt") + Rails.logger.info('Lighthouse::PensionBenefitIntakeJob submission to LH attempted', { + claim_id: claim&.id, + benefits_intake_uuid: lighthouse_service&.uuid, + confirmation_number: claim&.confirmation_number, + user_uuid:, + file: payload[:file], + attachments: payload[:attachments] + }) + end + + def track_submission_success(claim, lighthouse_service, user_uuid) + StatsD.increment("#{SUBMISSION_STATS_KEY}.success") + Rails.logger.info('Lighthouse::PensionBenefitIntakeJob submission to LH succeeded', { + claim_id: claim&.id, + benefits_intake_uuid: lighthouse_service&.uuid, + confirmation_number: claim&.confirmation_number, + user_uuid: + }) + end + + def track_submission_retry(claim, lighthouse_service, user_uuid, e) + StatsD.increment("#{SUBMISSION_STATS_KEY}.failure") + Rails.logger.warn('Lighthouse::PensionBenefitIntakeJob submission to LH failed, retrying', { + claim_id: claim&.id, + benefits_intake_uuid: lighthouse_service&.uuid, + confirmation_number: claim&.confirmation_number, + user_uuid:, + message: e&.message + }) + end + + def track_submission_exhaustion(msg, claim = nil) + StatsD.increment("#{SUBMISSION_STATS_KEY}.exhausted") + Rails.logger.error('Lighthouse::PensionBenefitIntakeJob submission to LH exhausted!', { + claim_id: msg['args'].first, + confirmation_number: claim&.confirmation_number, + message: msg, + user_uuid: msg['args'].length <= 1 ? nil : msg['args'][1] + }) + end + + def track_file_cleanup_error(claim, lighthouse_service, user_uuid, e) + Rails.logger.error('Lighthouse::PensionBenefitIntakeJob cleanup failed', + { + error: e&.message, + claim_id: claim&.id, + benefits_intake_uuid: lighthouse_service&.uuid, + confirmation_number: claim&.confirmation_number, + user_uuid: + }) + end + end +end 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/rx/client.rb b/lib/rx/client.rb index c3a9fe4780f..830efd47923 100644 --- a/lib/rx/client.rb +++ b/lib/rx/client.rb @@ -18,6 +18,7 @@ class Client < Common::Client::Base client_session Rx::ClientSession CACHE_TTL = 3600 * 1 # 1 hour cache + CACHE_TTL_ZERO = 0 def request(method, path, params = {}, headers = {}, options = {}) super(method, path, params, headers, options) @@ -31,7 +32,7 @@ def request(method, path, params = {}, headers = {}, options = {}) # @return [Common::Collection[Prescription]] # def get_active_rxs - Common::Collection.fetch(::Prescription, cache_key: cache_key('getactiverx'), ttl: CACHE_TTL) do + Common::Collection.fetch(::Prescription, cache_key: cache_key('getactiverx'), ttl: CACHE_TTL_ZERO) do perform(:get, 'prescription/getactiverx', nil, token_headers).body end end @@ -53,7 +54,7 @@ def get_active_rxs_with_details # @return [Common::Collection[Prescription]] # def get_history_rxs - Common::Collection.fetch(::Prescription, cache_key: cache_key('gethistoryrx'), ttl: CACHE_TTL) do + Common::Collection.fetch(::Prescription, cache_key: cache_key('gethistoryrx'), ttl: CACHE_TTL_ZERO) do perform(:get, 'prescription/gethistoryrx', nil, token_headers).body end end @@ -123,10 +124,7 @@ def get_tracking_history_rx(id) # @return [Faraday::Env] # def post_refill_rxs(ids) - if (result = perform(:post, 'prescription/rxrefill', ids, token_headers)) - Common::Collection.bust([cache_key('getactiverx'), cache_key('gethistoryrx')]) - end - result + perform(:post, 'prescription/rxrefill', ids, token_headers) end ## @@ -137,7 +135,8 @@ def post_refill_rxs(ids) # def post_refill_rx(id) if (result = perform(:post, "prescription/rxrefill/#{id}", nil, token_headers)) - Common::Collection.bust([cache_key('getactiverx'), cache_key('gethistoryrx')]) + keys = [cache_key('getactiverx'), cache_key('gethistoryrx')].compact + Common::Collection.bust(keys) unless keys.empty? end result end diff --git a/lib/sidekiq/attr_package.rb b/lib/sidekiq/attr_package.rb index e8d1749065d..4692b775d90 100644 --- a/lib/sidekiq/attr_package.rb +++ b/lib/sidekiq/attr_package.rb @@ -27,13 +27,20 @@ def find(key) json_value = redis.get(key) return nil unless json_value - JSON.parse(json_value, symbolize_names: true).tap do - redis.del(key) - end + JSON.parse(json_value, symbolize_names: true) rescue => e raise AttrPackageError.new('find', e.message) end + # Delete an attribute package by key + # @param key [String] the key of the attribute package + # @return [Integer] the number of keys deleted + def delete(key) + redis.del(key) + rescue => e + raise AttrPackageError.new('delete', e.message) + end + private def redis 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/sidekiq/semantic_logging.rb b/lib/sidekiq/semantic_logging.rb index 822699bd393..b6043e958ca 100644 --- a/lib/sidekiq/semantic_logging.rb +++ b/lib/sidekiq/semantic_logging.rb @@ -4,6 +4,11 @@ require 'sidekiq/job_logger' class Sidekiq::SemanticLogging < Sidekiq::JobLogger + def initialize + logger = Rails.logger + super(logger) + end + def call(_worker, item, queue) logger_tags = { class: item['class'], diff --git a/lib/simple_forms_api_submission/s3.rb b/lib/simple_forms_api_submission/s3.rb index 065bf4ef680..7ac5567f7b2 100644 --- a/lib/simple_forms_api_submission/s3.rb +++ b/lib/simple_forms_api_submission/s3.rb @@ -14,13 +14,22 @@ def initialize(region:, access_key_id:, secret_access_key:, bucket_name:) @bucket_name = bucket_name end - def upload_file(key, file) - obj = resource.bucket(bucket_name).object(key) - obj.upload_file(file) + def put_object(key, file, metadata = {}) + Datadog::Tracing.trace('S3 Put File(s)') do + # Convert nil values to empty strings in the metadata + metadata&.transform_values! { |value| value || '' } - { success: true } - rescue => e - { success: false, error_message: "S3 Upload failure for #{file}: #{e.message}" } + client.put_object({ + bucket: Settings.ivc_forms.s3.bucket, + key:, + body: File.read(file), + metadata:, + acl: 'public-read' + }) + { success: true } + rescue => e + { success: false, error_message: "S3 PutObject failure for #{file}: #{e.message}" } + end end private @@ -32,9 +41,5 @@ def client secret_access_key: Settings.ivc_forms.s3.aws_secret_access_key ) end - - def resource - @resource ||= Aws::S3::Resource.new(client:) - end end end 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/models/associated_person.rb b/lib/va_profile/models/associated_person.rb index 9f9f72c25bc..c100e0e1d33 100644 --- a/lib/va_profile/models/associated_person.rb +++ b/lib/va_profile/models/associated_person.rb @@ -10,19 +10,27 @@ class AssociatedPerson < Base OTHER_EMERGENCY_CONTACT = 'Other emergency contact' PRIMARY_NEXT_OF_KIN = 'Primary Next of Kin' OTHER_NEXT_OF_KIN = 'Other Next of Kin' + DESIGNEE = 'Designee' + POWER_OF_ATTORNEY = 'Power of Attorney' - CONTACT_TYPES = [ + PERSONAL_HEALTH_CARE_CONTACT_TYPES = [ EMERGENCY_CONTACT, OTHER_EMERGENCY_CONTACT, PRIMARY_NEXT_OF_KIN, OTHER_NEXT_OF_KIN ].freeze + CONTACT_TYPES = [ + *PERSONAL_HEALTH_CARE_CONTACT_TYPES, + DESIGNEE, + POWER_OF_ATTORNEY + ].freeze + attribute :contact_type, String attribute :given_name, Common::TitlecaseString attribute :middle_name, Common::TitlecaseString attribute :family_name, Common::TitlecaseString - attribute :relationship, Common::TitlecaseString + attribute :relationship, String attribute :address_line1, String attribute :address_line2, String attribute :address_line3, String 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..1b333021400 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,45 @@ require 'va_profile/models/associated_person' require 'va_profile/models/message' -module VAProfile::Profile::V3 - class HealthBenefitBioResponse < VAProfile::Response - attr_reader :body - - 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 +module VAProfile + module Profile + module V3 + class HealthBenefitBioResponse < VAProfile::Response + attribute :contacts, Array[VAProfile::Models::AssociatedPerson] + attribute :messages, Array[VAProfile::Models::Message] + attribute :va_profile_tx_audit_id, String + + 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'] + va_profile_tx_audit_id = response.response_headers['vaprofiletxauditid'] + super(response.status, { contacts:, messages:, va_profile_tx_audit_id: }) + end + + def debug_data + { + status:, + message:, + va_profile_tx_audit_id: + } + end + + private + + def valid_contact_types + VAProfile::Models::AssociatedPerson::PERSONAL_HEALTH_CARE_CONTACT_TYPES + end + + def message + m = messages&.first + return '' unless m - def metadata - { status:, messages: } + "#{m.code} #{m.key} #{m.text}" + end + end end end end diff --git a/lib/va_profile/profile/v3/service.rb b/lib/va_profile/profile/v3/service.rb index 8c26e61e49f..8b06ccc5f59 100644 --- a/lib/va_profile/profile/v3/service.rb +++ b/lib/va_profile/profile/v3/service.rb @@ -26,8 +26,10 @@ def initialize(user) def get_health_benefit_bio oid = MPI::Constants::VA_ROOT_OID path = "#{oid}/#{ERB::Util.url_encode(icn_with_aaid)}" - response = perform(:post, path, { bios: [{ bioPath: 'healthBenefit' }] }) - VAProfile::Profile::V3::HealthBenefitBioResponse.new(response) + service_response = perform(:post, path, { bios: [{ bioPath: 'healthBenefit' }] }) + response = VAProfile::Profile::V3::HealthBenefitBioResponse.new(service_response) + Sentry.set_extras(response.debug_data) unless response.ok? + response end def get_military_info diff --git a/modules/accredited_representative_portal/app/controllers/accredited_representative_portal/services/fetch_poa_requests.rb b/modules/accredited_representative_portal/app/controllers/accredited_representative_portal/services/fetch_poa_requests.rb new file mode 100644 index 00000000000..c251e8bd22f --- /dev/null +++ b/modules/accredited_representative_portal/app/controllers/accredited_representative_portal/services/fetch_poa_requests.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module AccreditedRepresentativePortal + module Services + # The FetchPoaRequests service is responsible for retrieving Power of Attorney (POA) request records + # based on provided Power of Attorney codes. This class currently reads from a JSON file as a temporary + # source of data. In the future, this service will be updated to fetch POA requests directly from the + # Lighthouse API once the appropriate endpoint is ready. + # + # This service is a part of the interim solution to support development and testing of the Accredited + # Representative portal. The use of a static JSON file allows for the simulation of interacting with + # an API and facilitates the frontend development process. + # + # Example usage: + # fetcher = AccreditedRepresentativePortal::Services::FetchPoaRequests.new(['A1Q', '091']) + # result = fetcher.call + # puts result # => { 'records': [...], 'meta': { 'totalRecords': '...' } } + # + # TODO: This class is slated for update to use the Lighthouse API once the appropriate endpoint + # is available. For more information on the transition plan, refer to: + # https://app.zenhub.com/workspaces/accredited-representative-facing-team-65453a97a9cc36069a2ad1d6/issues/gh/department-of-veterans-affairs/va.gov-team/80195 + class FetchPoaRequests + # Initializes the FetchPoaRequests service with the given POA codes. + # @param poa_codes [Array] an array of POA codes to filter the POA requests. + def initialize(poa_codes) + @poa_codes = poa_codes + end + + # Fetches POA request records filtered by the initialized POA codes. + # Currently reads from a static JSON file as a data source. + # @return [Hash] A hash containing the filtered records and metadata. + def call + file_path = Rails.root.join('modules', 'accredited_representative_portal', 'spec', 'fixtures', + 'poa_records.json') + file_data = File.read(file_path) + all_records_json = JSON.parse(file_data) + all_records = all_records_json['records'] + + filtered_records = all_records.select do |record| + @poa_codes.include?(record['attributes']['poaCode']) + end + + { 'records' => filtered_records, 'meta' => { 'totalRecords' => filtered_records.count.to_s } } + rescue => e + Rails.logger.error "Failed to fetch POA requests: #{e.message}" + { 'data' => [], 'meta' => { 'totalRecords' => '0' } } + end + end + end +end diff --git a/modules/accredited_representative_portal/app/controllers/accredited_representative_portal/v0/power_of_attorney_requests_controller.rb b/modules/accredited_representative_portal/app/controllers/accredited_representative_portal/v0/power_of_attorney_requests_controller.rb index 743545dce95..6c5aef3a7da 100644 --- a/modules/accredited_representative_portal/app/controllers/accredited_representative_portal/v0/power_of_attorney_requests_controller.rb +++ b/modules/accredited_representative_portal/app/controllers/accredited_representative_portal/v0/power_of_attorney_requests_controller.rb @@ -4,10 +4,77 @@ module AccreditedRepresentativePortal module V0 class PowerOfAttorneyRequestsController < ApplicationController def accept - # TODO: The ID will be either a veteran_id or a poa_id - # id = params[:id] - # NOTE: the below is a placeholder for the acceptance logic - render json: { message: 'Accepted' }, status: :ok + id = params[:proc_id] + result = update_poa_request(id, 'Accepted') + + if result[:success] + render json: { message: 'Accepted' }, status: :ok + else + render json: { error: result[:error] }, status: :unprocessable_entity + end + end + + def decline + id = params[:proc_id] + result = update_poa_request(id, 'Declined') + + if result[:success] + render json: { message: 'Declined' }, status: :ok + else + render json: { error: result[:error] }, status: :unprocessable_entity + end + end + + def index + poa_codes = permitted_params[:poa_codes]&.split(',') || [] + + return render json: { error: 'POA codes are required' }, status: :bad_request if poa_codes.blank? + + poa_requests = AccreditedRepresentativePortal::Services::FetchPoaRequests.new(poa_codes).call + + render json: { records: poa_requests['records'], records_count: poa_requests['meta']['totalRecords'].to_i }, + status: :ok + end + + private + + def permitted_params + params.permit(:poa_codes) + end + + # TODO: This class is slated for update to use the Lighthouse API once the appropriate endpoint + # is available. For more information on the transition plan, refer to: + # https://app.zenhub.com/workspaces/accredited-representative-facing-team-65453a97a9cc36069a2ad1d6/issues/gh/department-of-veterans-affairs/va.gov-team/80195 + def update_poa_request(proc_id, action) + # TODO: Update the below to use the RepresentativeUser's profile data + # representative = { + # first_name: 'John', + # last_name: 'Doe' + # } + + # Simulating the interaction with an external service to update POA. + # In real implementation, this method will make an actual API call. + # service_response = ClaimsApi::ManageRepresentativeService.new.update_poa_request( + # representative:, + # proc_id: + # ) + + if %w[Accepted Declined].include?(action) + { + success: true, + response: { + proc_id:, + action:, + status: 'updated', + dateRequestActioned: Time.current.iso8601, + secondaryStatus: action == 'Accepted' ? 'obsolete' : 'cancelled' + } + } + else + { success: false, error: 'Invalid action' } + end + rescue => e + { success: false, error: e.message } end end end diff --git a/modules/accredited_representative_portal/app/models/accredited_representative_portal/representative_user.rb b/modules/accredited_representative_portal/app/models/accredited_representative_portal/representative_user.rb index 82e5dde64fb..b4e58fcee9a 100644 --- a/modules/accredited_representative_portal/app/models/accredited_representative_portal/representative_user.rb +++ b/modules/accredited_representative_portal/app/models/accredited_representative_portal/representative_user.rb @@ -6,19 +6,22 @@ class RepresentativeUser < Common::RedisStore redis_ttl REDIS_CONFIG[:representative_user_store][:each_ttl] redis_key :uuid - attribute :uuid + # in alphabetical order + attribute :authn_context attribute :email + attribute :fingerprint attribute :first_name - attribute :last_name attribute :icn - alias_attribute :mhv_icn, :icn attribute :idme_uuid - attribute :logingov_uuid - attribute :fingerprint + attribute :last_name attribute :last_signed_in - attribute :authn_context attribute :loa + attribute :logingov_uuid + attribute :ogc_number + attribute :poa_codes attribute :sign_in + attribute :uuid + alias_attribute :mhv_icn, :icn validates :uuid, :email, :first_name, :last_name, :icn, presence: true end diff --git a/modules/accredited_representative_portal/app/services/accredited_representative_portal/representative_user_creator.rb b/modules/accredited_representative_portal/app/services/accredited_representative_portal/representative_user_creator.rb deleted file mode 100644 index efedd2bd9a9..00000000000 --- a/modules/accredited_representative_portal/app/services/accredited_representative_portal/representative_user_creator.rb +++ /dev/null @@ -1,143 +0,0 @@ -# frozen_string_literal: true - -module AccreditedRepresentativePortal - class RepresentativeUserCreator - attr_reader :state_payload, - :idme_uuid, - :logingov_uuid, - :authn_context, - :current_ial, - :max_ial, - :credential_email, - :multifactor, - :verified_icn, - :edipi, - :mhv_correlation_id, - :request_ip, - :first_name, - :last_name - - def initialize(user_attributes:, state_payload:, verified_icn:, request_ip:) - @state_payload = state_payload - @idme_uuid = user_attributes[:idme_uuid] - @logingov_uuid = user_attributes[:logingov_uuid] - @authn_context = user_attributes[:authn_context] - @current_ial = user_attributes[:current_ial] - @max_ial = user_attributes[:max_ial] - @credential_email = user_attributes[:csp_email] - @multifactor = user_attributes[:multifactor] - @edipi = user_attributes[:edipi] - @mhv_correlation_id = user_attributes[:mhv_correlation_id] - @verified_icn = verified_icn - @request_ip = request_ip - @first_name = user_attributes[:first_name] - @last_name = user_attributes[:last_name] - end - - def perform - create_authenticated_user - create_credential_email - create_user_acceptable_verified_credential - create_code_container - user_code_map - end - - private - - def create_authenticated_user - user - end - - def create_credential_email - Login::UserCredentialEmailUpdater.new(credential_email:, - user_verification:).perform - end - - def create_user_acceptable_verified_credential - Login::UserAcceptableVerifiedCredentialUpdater.new(user_account: user_verification.user_account).perform - end - - def create_code_container - SignIn::CodeContainer.new(code: login_code, - client_id: state_payload.client_id, - code_challenge: state_payload.code_challenge, - user_verification_id: user_verification.id, - credential_email:, - user_attributes: access_token_attributes).save! - end - - def user_verifier_object - @user_verifier_object ||= OpenStruct.new({ idme_uuid:, - logingov_uuid:, - sign_in:, - edipi:, - mhv_correlation_id:, - icn: verified_icn }) - end - - def user_code_map - @user_code_map ||= SignIn::UserCodeMap.new(login_code:, - type: state_payload.type, - client_state: state_payload.client_state, - client_config:, - terms_code: nil) - end - - def user_verification - @user_verification ||= Login::UserVerifier.new(user_verifier_object).perform - end - - def sign_in - @sign_in ||= { - service_name: state_payload.type, - auth_broker: SignIn::Constants::Auth::BROKER_CODE, - client_id: state_payload.client_id - } - end - - def loa - @loa ||= { current: ial_to_loa(current_ial), highest: ial_to_loa(max_ial) } - end - - def ial_to_loa(ial) - ial == SignIn::Constants::Auth::IAL_TWO ? SignIn::Constants::Auth::LOA_THREE : SignIn::Constants::Auth::LOA_ONE - end - - def user_uuid - @user_uuid ||= user_verification.backing_credential_identifier - end - - def access_token_attributes - { first_name:, - last_name:, - email: credential_email }.compact - end - - def client_config - @client_config ||= SignIn::ClientConfig.find_by!(client_id: state_payload.client_id) - end - - def login_code - @login_code ||= SecureRandom.uuid - end - - def user - @user ||= begin - user = RepresentativeUser.new - user.uuid = user_uuid - user.icn = verified_icn - user.email = credential_email - user.idme_uuid = idme_uuid - user.logingov_uuid = logingov_uuid - user.first_name = first_name - user.last_name = last_name - user.fingerprint = request_ip - user.last_signed_in = Time.zone.now - user.authn_context = authn_context - user.loa = loa - user.sign_in = sign_in - user.save - end - end - end -end diff --git a/modules/accredited_representative_portal/app/services/accredited_representative_portal/representative_user_loader.rb b/modules/accredited_representative_portal/app/services/accredited_representative_portal/representative_user_loader.rb index dfe2514186e..7a7a93bac16 100644 --- a/modules/accredited_representative_portal/app/services/accredited_representative_portal/representative_user_loader.rb +++ b/modules/accredited_representative_portal/app/services/accredited_representative_portal/representative_user_loader.rb @@ -4,6 +4,8 @@ module AccreditedRepresentativePortal class RepresentativeUserLoader attr_reader :access_token, :request_ip + class RepresentativeNotFoundError < StandardError; end + def initialize(access_token:, request_ip:) @access_token = access_token @request_ip = request_ip @@ -64,6 +66,18 @@ def user_verification @user_verification ||= session.user_verification end + def get_poa_codes + rep = Veteran::Service::Representative.find_by(representative_id: ogc_number) + # TODO-ARF 80297: Determine how to get ogc_number into RepresentativeUserLoader + # raise RepresentativeNotFoundError unless rep + + rep&.poa_codes + end + + def ogc_number + # TODO-ARF 80297: Determine how to get ogc_number into RepresentativeUserLoader + end + def current_user return @current_user if @current_user.present? @@ -77,6 +91,8 @@ def current_user user.authn_context = authn_context user.loa = loa user.logingov_uuid = user_verification.logingov_uuid + user.ogc_number = ogc_number # TODO-ARF 80297: Determine how to get ogc_number into RepresentativeUserLoader + user.poa_codes = get_poa_codes user.idme_uuid = user_verification.idme_uuid || user_verification.backing_idme_uuid user.last_signed_in = session.created_at user.sign_in = sign_in diff --git a/modules/accredited_representative_portal/config/routes.rb b/modules/accredited_representative_portal/config/routes.rb index 7d53d843c0f..6f348e335fb 100644 --- a/modules/accredited_representative_portal/config/routes.rb +++ b/modules/accredited_representative_portal/config/routes.rb @@ -2,9 +2,10 @@ AccreditedRepresentativePortal::Engine.routes.draw do namespace :v0, defaults: { format: :json } do - resources :power_of_attorney_requests, only: [] do + resources :power_of_attorney_requests, only: [:index] do member do post :accept + post :decline end end diff --git a/modules/accredited_representative_portal/spec/factories/representative_user.rb b/modules/accredited_representative_portal/spec/factories/representative_user.rb index 3dd401ab597..89ce83812e8 100644 --- a/modules/accredited_representative_portal/spec/factories/representative_user.rb +++ b/modules/accredited_representative_portal/spec/factories/representative_user.rb @@ -13,6 +13,8 @@ last_signed_in { Time.zone.now } authn_context { LOA::IDME_LOA3_VETS } loa { { current: LOA::THREE, highest: LOA::THREE } } + ogc_number { '123456789' } + poa_codes { %w[1234 5678] } sign_in { { service_name: SignIn::Constants::Auth::IDME, client_id: SecureRandom.uuid, auth_broker: SignIn::Constants::Auth::BROKER_CODE } diff --git a/modules/accredited_representative_portal/spec/fixtures/poa_records.json b/modules/accredited_representative_portal/spec/fixtures/poa_records.json new file mode 100644 index 00000000000..956a084a366 --- /dev/null +++ b/modules/accredited_representative_portal/spec/fixtures/poa_records.json @@ -0,0 +1,1117 @@ +{ + "records": [ + { + "procId": "9942820247", + "type": "powerOfAttorneyRequest", + "attributes": { + "poaCode": "A1Q", + "secondaryStatus": "pending", + "dateRequestReceived": "2024-03-28", + "dateRequestActioned": "2024-05-05", + "declinedReason": null, + "healthInfoAuth": "Y", + "changeAddressAuth": "Y", + "claimant": { + "firstName": "Dale", + "lastName": "Hills", + "city": "Haleyland", + "state": "TN", + "zip": "73538", + "country": "Albania", + "militaryPO": null, + "militaryPostalCode": null, + "participantID": "7158421739", + "relationship": "Friend" + }, + "veteran": { + "firstName": "Refugio", + "lastName": "Pollich", + "middleName": "Erdman", + "participantID": "2152444888", + "sensitivityLevel": "Low" + }, + "VSORepresentative": { + "email": "clarita@marvin.example", + "firstName": "Cinderella", + "lastName": "Baumbach" + } + } + }, + { + "procId": "4206885703", + "type": "powerOfAttorneyRequest", + "attributes": { + "poaCode": "091", + "secondaryStatus": "pending", + "dateRequestReceived": "2024-03-10", + "dateRequestActioned": "2024-04-13", + "declinedReason": null, + "healthInfoAuth": "N", + "changeAddressAuth": "N", + "claimant": { + "firstName": "Whitney", + "lastName": "Stoltenberg", + "city": "Lake Evita", + "state": "AL", + "zip": "27906", + "country": "Antigua and Barbuda", + "militaryPO": null, + "militaryPostalCode": null, + "participantID": "9440452403", + "relationship": "Child" + }, + "veteran": { + "firstName": "Bob", + "lastName": "McDermott", + "middleName": "Herman", + "participantID": "8635486958", + "sensitivityLevel": "High" + }, + "VSORepresentative": { + "email": "leon.fritsch@kulas.example", + "firstName": "Andres", + "lastName": "Spencer" + } + } + }, + { + "procId": "6538972520", + "type": "powerOfAttorneyRequest", + "attributes": { + "poaCode": "A1Q", + "secondaryStatus": "pending", + "dateRequestReceived": "2024-03-23", + "dateRequestActioned": "2024-04-28", + "declinedReason": null, + "healthInfoAuth": "Y", + "changeAddressAuth": "Y", + "claimant": { + "firstName": "Bernie", + "lastName": "Kuphal", + "city": "North Theo", + "state": "IN", + "zip": "57624", + "country": "Niue", + "militaryPO": null, + "militaryPostalCode": null, + "participantID": "7820124603", + "relationship": "Parent" + }, + "veteran": { + "firstName": "Cristopher", + "lastName": "Heaney", + "middleName": "Jakubowski", + "participantID": "5751314148", + "sensitivityLevel": "Low" + }, + "VSORepresentative": { + "email": "belle@collier-zieme.test", + "firstName": "Bobby", + "lastName": "Robel" + } + } + }, + { + "procId": "1545078041", + "type": "powerOfAttorneyRequest", + "attributes": { + "poaCode": "091", + "secondaryStatus": "pending", + "dateRequestReceived": "2024-03-16", + "dateRequestActioned": "2024-04-21", + "declinedReason": null, + "healthInfoAuth": "N", + "changeAddressAuth": "N", + "claimant": { + "firstName": "Miguel", + "lastName": "Stracke", + "city": "East Marisolbury", + "state": "KY", + "zip": "38747", + "country": "Greenland", + "militaryPO": null, + "militaryPostalCode": null, + "participantID": "1900423684", + "relationship": "Parent" + }, + "veteran": { + "firstName": "Elidia", + "lastName": "Lehner", + "middleName": "Gulgowski", + "participantID": "8469999212", + "sensitivityLevel": "Medium" + }, + "VSORepresentative": { + "email": "jewell@okon.test", + "firstName": "Jordan", + "lastName": "Pagac" + } + } + }, + { + "procId": "2740225884", + "type": "powerOfAttorneyRequest", + "attributes": { + "poaCode": "A1Q", + "secondaryStatus": "pending", + "dateRequestReceived": "2024-03-11", + "dateRequestActioned": "2024-05-01", + "declinedReason": null, + "healthInfoAuth": "Y", + "changeAddressAuth": "Y", + "claimant": { + "firstName": "Haydee", + "lastName": "Robel", + "city": "North Marcelinoside", + "state": "WV", + "zip": "37916", + "country": "Thailand", + "militaryPO": null, + "militaryPostalCode": null, + "participantID": "4355283680", + "relationship": "Child" + }, + "veteran": { + "firstName": "Nery", + "lastName": "Reilly", + "middleName": "Hessel", + "participantID": "3103541588", + "sensitivityLevel": "High" + }, + "VSORepresentative": { + "email": "chong@terry.example", + "firstName": "Darrick", + "lastName": "Keeling" + } + } + }, + { + "procId": "1852457399", + "type": "powerOfAttorneyRequest", + "attributes": { + "poaCode": "091", + "secondaryStatus": "pending", + "dateRequestReceived": "2024-04-01", + "dateRequestActioned": "2024-04-28", + "declinedReason": null, + "healthInfoAuth": "N", + "changeAddressAuth": "N", + "claimant": { + "firstName": "Brenda", + "lastName": "Schowalter", + "city": "Port Harry", + "state": "NE", + "zip": "25128", + "country": "Mozambique", + "militaryPO": null, + "militaryPostalCode": null, + "participantID": "6406503016", + "relationship": "Friend" + }, + "veteran": { + "firstName": "Roxy", + "lastName": "Leuschke", + "middleName": "Morissette", + "participantID": "7156298434", + "sensitivityLevel": "Medium" + }, + "VSORepresentative": { + "email": "major@carter.example", + "firstName": "Annette", + "lastName": "Rodriguez" + } + } + }, + { + "procId": "4081328722", + "type": "powerOfAttorneyRequest", + "attributes": { + "poaCode": "A1Q", + "secondaryStatus": "pending", + "dateRequestReceived": "2024-03-14", + "dateRequestActioned": "2024-04-27", + "declinedReason": null, + "healthInfoAuth": "Y", + "changeAddressAuth": "Y", + "claimant": { + "firstName": "Larry", + "lastName": "Kessler", + "city": "New Yukiko", + "state": "UT", + "zip": "87363-8157", + "country": "Jersey", + "militaryPO": null, + "militaryPostalCode": null, + "participantID": "8901124888", + "relationship": "Spouse" + }, + "veteran": { + "firstName": "Effie", + "lastName": "Reilly", + "middleName": "Schmeler", + "participantID": "4267563093", + "sensitivityLevel": "Medium" + }, + "VSORepresentative": { + "email": "winford_swaniawski@altenwerth.test", + "firstName": "Mariam", + "lastName": "Abernathy" + } + } + }, + { + "procId": "9954357683", + "type": "powerOfAttorneyRequest", + "attributes": { + "poaCode": "091", + "secondaryStatus": "pending", + "dateRequestReceived": "2024-03-11", + "dateRequestActioned": "2024-04-27", + "declinedReason": null, + "healthInfoAuth": "N", + "changeAddressAuth": "N", + "claimant": { + "firstName": "Steven", + "lastName": "Fay", + "city": "Lake Mai", + "state": "TX", + "zip": "81365-6236", + "country": "Cabo Verde", + "militaryPO": null, + "militaryPostalCode": null, + "participantID": "2155221172", + "relationship": "Spouse" + }, + "veteran": { + "firstName": "Boyd", + "lastName": "Herman", + "middleName": "Graham", + "participantID": "5323184877", + "sensitivityLevel": "High" + }, + "VSORepresentative": { + "email": "angelita.kuhlman@corkery-collins.example", + "firstName": "Geoffrey", + "lastName": "Willms" + } + } + }, + { + "procId": "5254792179", + "type": "powerOfAttorneyRequest", + "attributes": { + "poaCode": "A1Q", + "secondaryStatus": "pending", + "dateRequestReceived": "2024-04-03", + "dateRequestActioned": "2024-04-20", + "declinedReason": null, + "healthInfoAuth": "Y", + "changeAddressAuth": "Y", + "claimant": { + "firstName": "Edmundo", + "lastName": "Purdy", + "city": "Port Jeromyville", + "state": "MA", + "zip": "36198-3784", + "country": "Cambodia", + "militaryPO": null, + "militaryPostalCode": null, + "participantID": "4405853933", + "relationship": "Spouse" + }, + "veteran": { + "firstName": "Hassan", + "lastName": "Bernier", + "middleName": "Christiansen", + "participantID": "4143003298", + "sensitivityLevel": "Medium" + }, + "VSORepresentative": { + "email": "antonio@murazik.test", + "firstName": "Mose", + "lastName": "Gulgowski" + } + } + }, + { + "procId": "9622796898", + "type": "powerOfAttorneyRequest", + "attributes": { + "poaCode": "091", + "secondaryStatus": "pending", + "dateRequestReceived": "2024-03-14", + "dateRequestActioned": "2024-04-12", + "declinedReason": null, + "healthInfoAuth": "N", + "changeAddressAuth": "N", + "claimant": { + "firstName": "Noah", + "lastName": "Langosh", + "city": "Kertzmannburgh", + "state": "IA", + "zip": "69720", + "country": "Comoros", + "militaryPO": null, + "militaryPostalCode": null, + "participantID": "7967611020", + "relationship": "Friend" + }, + "veteran": { + "firstName": "Krystin", + "lastName": "Nicolas", + "middleName": "Smitham", + "participantID": "5205712033", + "sensitivityLevel": "Low" + }, + "VSORepresentative": { + "email": "ethelene.bins@kirlin.example", + "firstName": "Nestor", + "lastName": "Goodwin" + } + } + }, + { + "procId": "8754621135", + "type": "powerOfAttorneyRequest", + "attributes": { + "poaCode": "A1Q", + "secondaryStatus": "pending", + "dateRequestReceived": "2024-03-08", + "dateRequestActioned": "2024-04-27", + "declinedReason": null, + "healthInfoAuth": "Y", + "changeAddressAuth": "Y", + "claimant": { + "firstName": "Juana", + "lastName": "Grady", + "city": "North Scarlet", + "state": "MO", + "zip": "69137", + "country": "Spain", + "militaryPO": null, + "militaryPostalCode": null, + "participantID": "2954642877", + "relationship": "Child" + }, + "veteran": { + "firstName": "Dorothea", + "lastName": "Nikolaus", + "middleName": "Feil", + "participantID": "1359697723", + "sensitivityLevel": "Medium" + }, + "VSORepresentative": { + "email": "harris.predovic@hickle.example", + "firstName": "Lindsey", + "lastName": "Green" + } + } + }, + { + "procId": "3217671507", + "type": "powerOfAttorneyRequest", + "attributes": { + "poaCode": "091", + "secondaryStatus": "pending", + "dateRequestReceived": "2024-03-28", + "dateRequestActioned": "2024-04-30", + "declinedReason": null, + "healthInfoAuth": "N", + "changeAddressAuth": "N", + "claimant": { + "firstName": "Lawerence", + "lastName": "Miller", + "city": "New Nicolasshire", + "state": "VA", + "zip": "54181", + "country": "Saint Pierre and Miquelon", + "militaryPO": null, + "militaryPostalCode": null, + "participantID": "4447702721", + "relationship": "Child" + }, + "veteran": { + "firstName": "Erwin", + "lastName": "Zboncak", + "middleName": "Walsh", + "participantID": "7496852392", + "sensitivityLevel": "High" + }, + "VSORepresentative": { + "email": "jefferson@grimes-hegmann.test", + "firstName": "Man", + "lastName": "Ritchie" + } + } + }, + { + "procId": "6109624522", + "type": "powerOfAttorneyRequest", + "attributes": { + "poaCode": "A1Q", + "secondaryStatus": "pending", + "dateRequestReceived": "2024-03-08", + "dateRequestActioned": "2024-04-21", + "declinedReason": null, + "healthInfoAuth": "Y", + "changeAddressAuth": "Y", + "claimant": { + "firstName": "Gale", + "lastName": "Moen", + "city": "Starkberg", + "state": "OH", + "zip": "78205-5340", + "country": "Greece", + "militaryPO": null, + "militaryPostalCode": null, + "participantID": "6033466916", + "relationship": "Child" + }, + "veteran": { + "firstName": "Leon", + "lastName": "Dach", + "middleName": "Smitham", + "participantID": "2248396374", + "sensitivityLevel": "High" + }, + "VSORepresentative": { + "email": "noe@yundt.test", + "firstName": "Shirl", + "lastName": "Kohler" + } + } + }, + { + "procId": "3546805044", + "type": "powerOfAttorneyRequest", + "attributes": { + "poaCode": "091", + "secondaryStatus": "pending", + "dateRequestReceived": "2024-03-16", + "dateRequestActioned": "2024-04-08", + "declinedReason": null, + "healthInfoAuth": "N", + "changeAddressAuth": "N", + "claimant": { + "firstName": "Reed", + "lastName": "Ruecker", + "city": "South Rosannetown", + "state": "TN", + "zip": "77931-8169", + "country": "Papua New Guinea", + "militaryPO": null, + "militaryPostalCode": null, + "participantID": "6557732976", + "relationship": "Child" + }, + "veteran": { + "firstName": "Tessie", + "lastName": "Corkery", + "middleName": "Sporer", + "participantID": "6032892024", + "sensitivityLevel": "Low" + }, + "VSORepresentative": { + "email": "marcelina_keebler@schaden.example", + "firstName": "Claudio", + "lastName": "Moore" + } + } + }, + { + "procId": "5157897360", + "type": "powerOfAttorneyRequest", + "attributes": { + "poaCode": "A1Q", + "secondaryStatus": "pending", + "dateRequestReceived": "2024-03-10", + "dateRequestActioned": "2024-05-01", + "declinedReason": null, + "healthInfoAuth": "Y", + "changeAddressAuth": "Y", + "claimant": { + "firstName": "Morris", + "lastName": "Rippin", + "city": "Port Rosiotown", + "state": "AR", + "zip": "56024-8810", + "country": "Saint Vincent and the Grenadines", + "militaryPO": null, + "militaryPostalCode": null, + "participantID": "6590052993", + "relationship": "Parent" + }, + "veteran": { + "firstName": "Zandra", + "lastName": "Greenfelder", + "middleName": "Rippin", + "participantID": "5779628693", + "sensitivityLevel": "High" + }, + "VSORepresentative": { + "email": "carrol_hauck@paucek-oberbrunner.test", + "firstName": "Latashia", + "lastName": "Graham" + } + } + }, + { + "procId": "7680796959", + "type": "powerOfAttorneyRequest", + "attributes": { + "poaCode": "091", + "secondaryStatus": "pending", + "dateRequestReceived": "2024-03-28", + "dateRequestActioned": "2024-04-06", + "declinedReason": null, + "healthInfoAuth": "N", + "changeAddressAuth": "N", + "claimant": { + "firstName": "Geoffrey", + "lastName": "Schroeder", + "city": "Rickeyview", + "state": "MN", + "zip": "94866-8087", + "country": "Saint Pierre and Miquelon", + "militaryPO": null, + "militaryPostalCode": null, + "participantID": "9909988926", + "relationship": "Child" + }, + "veteran": { + "firstName": "Gonzalo", + "lastName": "Miller", + "middleName": "Rosenbaum", + "participantID": "1396051298", + "sensitivityLevel": "Low" + }, + "VSORepresentative": { + "email": "willow@white.test", + "firstName": "Joseph", + "lastName": "Robel" + } + } + }, + { + "procId": "1503143583", + "type": "powerOfAttorneyRequest", + "attributes": { + "poaCode": "A1Q", + "secondaryStatus": "pending", + "dateRequestReceived": "2024-03-11", + "dateRequestActioned": "2024-04-18", + "declinedReason": null, + "healthInfoAuth": "Y", + "changeAddressAuth": "Y", + "claimant": { + "firstName": "Leigh", + "lastName": "Bergnaum", + "city": "West George", + "state": "NV", + "zip": "49718-4159", + "country": "Senegal", + "militaryPO": null, + "militaryPostalCode": null, + "participantID": "1748245535", + "relationship": "Child" + }, + "veteran": { + "firstName": "Joan", + "lastName": "MacGyver", + "middleName": "Cruickshank", + "participantID": "3077111270", + "sensitivityLevel": "Medium" + }, + "VSORepresentative": { + "email": "tory@mraz.test", + "firstName": "Irma", + "lastName": "Kub" + } + } + }, + { + "procId": "8489633597", + "type": "powerOfAttorneyRequest", + "attributes": { + "poaCode": "091", + "secondaryStatus": "pending", + "dateRequestReceived": "2024-03-24", + "dateRequestActioned": "2024-04-21", + "declinedReason": null, + "healthInfoAuth": "N", + "changeAddressAuth": "N", + "claimant": { + "firstName": "Tuyet", + "lastName": "Skiles", + "city": "South Kendrick", + "state": "KY", + "zip": "89464", + "country": "Portugal", + "militaryPO": null, + "militaryPostalCode": null, + "participantID": "4324476034", + "relationship": "Friend" + }, + "veteran": { + "firstName": "Verona", + "lastName": "Abbott", + "middleName": "Toy", + "participantID": "4793172236", + "sensitivityLevel": "Medium" + }, + "VSORepresentative": { + "email": "jeremiah_ryan@heller-blick.test", + "firstName": "Shane", + "lastName": "Stamm" + } + } + }, + { + "procId": "9238210701", + "type": "powerOfAttorneyRequest", + "attributes": { + "poaCode": "A1Q", + "secondaryStatus": "pending", + "dateRequestReceived": "2024-03-06", + "dateRequestActioned": "2024-04-28", + "declinedReason": null, + "healthInfoAuth": "Y", + "changeAddressAuth": "Y", + "claimant": { + "firstName": "Serita", + "lastName": "Gislason", + "city": "East Cyrusmouth", + "state": "WY", + "zip": "61149-8042", + "country": "Syrian Arab Republic", + "militaryPO": null, + "militaryPostalCode": null, + "participantID": "3346818434", + "relationship": "Friend" + }, + "veteran": { + "firstName": "Sherilyn", + "lastName": "McClure", + "middleName": "Windler", + "participantID": "9956501498", + "sensitivityLevel": "Medium" + }, + "VSORepresentative": { + "email": "georgie@crooks-stoltenberg.example", + "firstName": "Andy", + "lastName": "Gerhold" + } + } + }, + { + "procId": "2385422076", + "type": "powerOfAttorneyRequest", + "attributes": { + "poaCode": "091", + "secondaryStatus": "pending", + "dateRequestReceived": "2024-03-06", + "dateRequestActioned": "2024-05-04", + "declinedReason": null, + "healthInfoAuth": "N", + "changeAddressAuth": "N", + "claimant": { + "firstName": "Bryon", + "lastName": "Boehm", + "city": "Feestside", + "state": "LA", + "zip": "66933-3057", + "country": "Guinea-Bissau", + "militaryPO": null, + "militaryPostalCode": null, + "participantID": "8279129687", + "relationship": "Spouse" + }, + "veteran": { + "firstName": "Kristopher", + "lastName": "Kovacek", + "middleName": "Lueilwitz", + "participantID": "4008725460", + "sensitivityLevel": "High" + }, + "VSORepresentative": { + "email": "lynna.romaguera@borer.example", + "firstName": "Min", + "lastName": "Homenick" + } + } + }, + { + "procId": "5385001943", + "type": "powerOfAttorneyRequest", + "attributes": { + "poaCode": "A1Q", + "secondaryStatus": "pending", + "dateRequestReceived": "2024-03-21", + "dateRequestActioned": "2024-04-22", + "declinedReason": null, + "healthInfoAuth": "Y", + "changeAddressAuth": "Y", + "claimant": { + "firstName": "Lynwood", + "lastName": "Hamill", + "city": "South Felipehaven", + "state": "ID", + "zip": "79379", + "country": "United States of America", + "militaryPO": null, + "militaryPostalCode": null, + "participantID": "4207789506", + "relationship": "Parent" + }, + "veteran": { + "firstName": "Ellsworth", + "lastName": "Hintz", + "middleName": "Treutel", + "participantID": "3417319641", + "sensitivityLevel": "Low" + }, + "VSORepresentative": { + "email": "dewayne.goyette@krajcik.test", + "firstName": "Les", + "lastName": "Barrows" + } + } + }, + { + "procId": "4290146937", + "type": "powerOfAttorneyRequest", + "attributes": { + "poaCode": "091", + "secondaryStatus": "pending", + "dateRequestReceived": "2024-04-02", + "dateRequestActioned": "2024-04-16", + "declinedReason": null, + "healthInfoAuth": "N", + "changeAddressAuth": "N", + "claimant": { + "firstName": "Phillip", + "lastName": "Lesch", + "city": "South Susannehaven", + "state": "NC", + "zip": "74990-8341", + "country": "Djibouti", + "militaryPO": null, + "militaryPostalCode": null, + "participantID": "7646626729", + "relationship": "Child" + }, + "veteran": { + "firstName": "Jeanna", + "lastName": "Prohaska", + "middleName": "Spencer", + "participantID": "6014125455", + "sensitivityLevel": "Medium" + }, + "VSORepresentative": { + "email": "paul_hamill@hoppe.test", + "firstName": "Faustino", + "lastName": "Kreiger" + } + } + }, + { + "procId": "8254468195", + "type": "powerOfAttorneyRequest", + "attributes": { + "poaCode": "A1Q", + "secondaryStatus": "pending", + "dateRequestReceived": "2024-03-28", + "dateRequestActioned": "2024-04-30", + "declinedReason": null, + "healthInfoAuth": "Y", + "changeAddressAuth": "Y", + "claimant": { + "firstName": "Alva", + "lastName": "Kovacek", + "city": "Danielbury", + "state": "ND", + "zip": "72835", + "country": "Estonia", + "militaryPO": null, + "militaryPostalCode": null, + "participantID": "9067297261", + "relationship": "Friend" + }, + "veteran": { + "firstName": "Vicky", + "lastName": "Emard", + "middleName": "Hartmann", + "participantID": "5103708939", + "sensitivityLevel": "High" + }, + "VSORepresentative": { + "email": "shanda_terry@walter.example", + "firstName": "Brandon", + "lastName": "Mann" + } + } + }, + { + "procId": "1385844527", + "type": "powerOfAttorneyRequest", + "attributes": { + "poaCode": "091", + "secondaryStatus": "pending", + "dateRequestReceived": "2024-03-09", + "dateRequestActioned": "2024-04-15", + "declinedReason": null, + "healthInfoAuth": "N", + "changeAddressAuth": "N", + "claimant": { + "firstName": "Izetta", + "lastName": "Bode", + "city": "Tiannaview", + "state": "LA", + "zip": "60072", + "country": "Colombia", + "militaryPO": null, + "militaryPostalCode": null, + "participantID": "5961069692", + "relationship": "Spouse" + }, + "veteran": { + "firstName": "Bryon", + "lastName": "Auer", + "middleName": "Nikolaus", + "participantID": "1214142631", + "sensitivityLevel": "Low" + }, + "VSORepresentative": { + "email": "asley.morar@jakubowski.test", + "firstName": "Marty", + "lastName": "Crooks" + } + } + }, + { + "procId": "7013291891", + "type": "powerOfAttorneyRequest", + "attributes": { + "poaCode": "A1Q", + "secondaryStatus": "pending", + "dateRequestReceived": "2024-03-24", + "dateRequestActioned": "2024-05-01", + "declinedReason": null, + "healthInfoAuth": "Y", + "changeAddressAuth": "Y", + "claimant": { + "firstName": "Clifford", + "lastName": "Olson", + "city": "West Karly", + "state": "PA", + "zip": "68378", + "country": "Kenya", + "militaryPO": null, + "militaryPostalCode": null, + "participantID": "3750399936", + "relationship": "Parent" + }, + "veteran": { + "firstName": "Cortez", + "lastName": "Abernathy", + "middleName": "O'Connell", + "participantID": "9462383093", + "sensitivityLevel": "Low" + }, + "VSORepresentative": { + "email": "margrett@krajcik.example", + "firstName": "Dominque", + "lastName": "Dibbert" + } + } + }, + { + "procId": "4913724953", + "type": "powerOfAttorneyRequest", + "attributes": { + "poaCode": "091", + "secondaryStatus": "obsolete", + "dateRequestReceived": "2024-03-30", + "dateRequestActioned": "2024-05-02", + "declinedReason": "Expedita consequatur temporibus dicta.", + "healthInfoAuth": "N", + "changeAddressAuth": "N", + "claimant": { + "firstName": "Grant", + "lastName": "Graham", + "city": "Lake Ellen", + "state": "MN", + "zip": "79618-0070", + "country": "Benin", + "militaryPO": null, + "militaryPostalCode": null, + "participantID": "1160880217", + "relationship": "Friend" + }, + "veteran": { + "firstName": "Alfredo", + "lastName": "Ziemann", + "middleName": "Skiles", + "participantID": "2733143615", + "sensitivityLevel": "Medium" + }, + "VSORepresentative": { + "email": "kisha@ortiz.example", + "firstName": "Emerson", + "lastName": "Bradtke" + } + } + }, + { + "procId": "3825558136", + "type": "powerOfAttorneyRequest", + "attributes": { + "poaCode": "A1Q", + "secondaryStatus": "canceled", + "dateRequestReceived": "2024-03-21", + "dateRequestActioned": "2024-04-19", + "declinedReason": "Quaerat deserunt ratione officiis.", + "healthInfoAuth": "Y", + "changeAddressAuth": "Y", + "claimant": { + "firstName": "Alec", + "lastName": "Kub", + "city": "Torphychester", + "state": "AK", + "zip": "39971-6644", + "country": "Lesotho", + "militaryPO": null, + "militaryPostalCode": null, + "participantID": "4566622083", + "relationship": "Child" + }, + "veteran": { + "firstName": "Bernardo", + "lastName": "Witting", + "middleName": "Feeney", + "participantID": "3241314281", + "sensitivityLevel": "Medium" + }, + "VSORepresentative": { + "email": "dahlia_harris@welch-gislason.test", + "firstName": "Marshall", + "lastName": "Willms" + } + } + }, + { + "procId": "7002867330", + "type": "powerOfAttorneyRequest", + "attributes": { + "poaCode": "091", + "secondaryStatus": "canceled", + "dateRequestReceived": "2024-03-19", + "dateRequestActioned": "2024-05-05", + "declinedReason": "Ratione placeat velit aspernatur.", + "healthInfoAuth": "N", + "changeAddressAuth": "N", + "claimant": { + "firstName": "Galen", + "lastName": "Leffler", + "city": "North Kristineside", + "state": "OR", + "zip": "52077-7654", + "country": "Finland", + "militaryPO": null, + "militaryPostalCode": null, + "participantID": "8415906652", + "relationship": "Friend" + }, + "veteran": { + "firstName": "Cristobal", + "lastName": "Harvey", + "middleName": "Pouros", + "participantID": "8418705577", + "sensitivityLevel": "Medium" + }, + "VSORepresentative": { + "email": "stacy@king.example", + "firstName": "Barton", + "lastName": "Jones" + } + } + }, + { + "procId": "1647587150", + "type": "powerOfAttorneyRequest", + "attributes": { + "poaCode": "A1Q", + "secondaryStatus": "canceled", + "dateRequestReceived": "2024-03-30", + "dateRequestActioned": "2024-04-26", + "declinedReason": "Eos nobis ut ipsum.", + "healthInfoAuth": "Y", + "changeAddressAuth": "Y", + "claimant": { + "firstName": "Mackenzie", + "lastName": "Reynolds", + "city": "Lowellbury", + "state": "WA", + "zip": "64999-3531", + "country": "Cocos (Keeling) Islands", + "militaryPO": null, + "militaryPostalCode": null, + "participantID": "8115232468", + "relationship": "Friend" + }, + "veteran": { + "firstName": "Glennis", + "lastName": "Buckridge", + "middleName": "Koch", + "participantID": "7817266608", + "sensitivityLevel": "Low" + }, + "VSORepresentative": { + "email": "dalia.harris@strosin.test", + "firstName": "Herschel", + "lastName": "Wolf" + } + } + }, + { + "procId": "6307005458", + "type": "powerOfAttorneyRequest", + "attributes": { + "poaCode": "091", + "secondaryStatus": "obsolete", + "dateRequestReceived": "2024-03-25", + "dateRequestActioned": "2024-05-05", + "declinedReason": "Ea voluptate in ad.", + "healthInfoAuth": "N", + "changeAddressAuth": "N", + "claimant": { + "firstName": "Jerry", + "lastName": "Kutch", + "city": "Millietown", + "state": "MN", + "zip": "66052-8399", + "country": "Republic of Korea", + "militaryPO": null, + "militaryPostalCode": null, + "participantID": "3372098647", + "relationship": "Friend" + }, + "veteran": { + "firstName": "Val", + "lastName": "Ledner", + "middleName": "Reichel", + "participantID": "1914871616", + "sensitivityLevel": "High" + }, + "VSORepresentative": { + "email": "ralph.satterfield@schuppe-koch.example", + "firstName": "Donald", + "lastName": "Kuhlman" + } + } + } + ], + "meta": { + "totalRecords": "30" + } +} \ No newline at end of file diff --git a/modules/accredited_representative_portal/spec/requests/accredited_representative_portal/v0/power_of_attorney_requests_controller_spec.rb b/modules/accredited_representative_portal/spec/requests/accredited_representative_portal/v0/power_of_attorney_requests_controller_spec.rb index 8aba1739ef4..98403db53ea 100644 --- a/modules/accredited_representative_portal/spec/requests/accredited_representative_portal/v0/power_of_attorney_requests_controller_spec.rb +++ b/modules/accredited_representative_portal/spec/requests/accredited_representative_portal/v0/power_of_attorney_requests_controller_spec.rb @@ -12,12 +12,64 @@ end describe 'POST /accept' do + let(:proc_id) { '123' } + it 'returns a successful response with an accepted message' do - id = '123' - post "/accredited_representative_portal/v0/power_of_attorney_requests/#{id}/accept" + post "/accredited_representative_portal/v0/power_of_attorney_requests/#{proc_id}/accept" expect(response).to have_http_status(:ok) json = JSON.parse(response.body) expect(json['message']).to eq('Accepted') end end + + describe 'POST /decline' do + let(:proc_id) { '123' } + + it 'returns a successful response with a declined message' do + post "/accredited_representative_portal/v0/power_of_attorney_requests/#{proc_id}/decline" + expect(response).to have_http_status(:ok) + json = JSON.parse(response.body) + expect(json['message']).to eq('Declined') + end + end + + describe 'GET /index' do + context 'when valid POA codes are provided' do + it 'returns a successful response with matching POA requests' do + get '/accredited_representative_portal/v0/power_of_attorney_requests', params: { poa_codes: '091,A1Q' } + expect(response).to have_http_status(:ok) + json = JSON.parse(response.body) + expect(json['records']).to be_an_instance_of(Array) + expect(json['records_count']).to eq(json['records'].size) + end + end + + context 'when no POA codes are provided' do + it 'returns a bad request status with an error message' do + get '/accredited_representative_portal/v0/power_of_attorney_requests' + expect(response).to have_http_status(:bad_request) + json = JSON.parse(response.body) + expect(json['error']).to eq('POA codes are required') + end + end + + context 'when POA codes parameter is empty' do + it 'returns a bad request status with an error message' do + get '/accredited_representative_portal/v0/power_of_attorney_requests', params: { poa_codes: '' } + expect(response).to have_http_status(:bad_request) + expect(JSON.parse(response.body)['error']).to eq('POA codes are required') + end + end + + context 'when there are no records for the provided POA codes' do + it 'returns an empty records array and zero records count' do + get '/accredited_representative_portal/v0/power_of_attorney_requests', params: { poa_codes: 'XYZ,ABC' } + expect(response).to have_http_status(:ok) + json = JSON.parse(response.body) + expect(json['records']).to be_an_instance_of(Array) + expect(json['records']).to be_empty + expect(json['records_count']).to eq(0) + end + end + end end diff --git a/modules/accredited_representative_portal/spec/services/accredited_representative_portal/representative_user_creator_spec.rb b/modules/accredited_representative_portal/spec/services/accredited_representative_portal/representative_user_creator_spec.rb deleted file mode 100644 index f09cd76ce15..00000000000 --- a/modules/accredited_representative_portal/spec/services/accredited_representative_portal/representative_user_creator_spec.rb +++ /dev/null @@ -1,97 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' -require 'timecop' - -RSpec.describe AccreditedRepresentativePortal::RepresentativeUserCreator do - describe '#perform' do - subject(:representative_user_creator) do - described_class.new(user_attributes:, state_payload:, verified_icn:, request_ip:) - end - - let(:user_attributes) do - { - logingov_uuid:, - loa:, - csp_email:, - current_ial:, - max_ial:, - multifactor:, - authn_context:, - first_name:, - last_name: - } - end - - let(:state_payload) do - create(:state_payload, - client_state:, - client_id:, - code_challenge:, - type:) - end - - let(:logingov_uuid) { SecureRandom.hex } - let(:authn_context) { service_name } - let(:csp_email) { 'some-csp-email' } - let(:first_name) { 'Jane' } - let(:last_name) { 'Doe' } - let(:request_ip) { '127.0.0.1' } - let(:loa) { { current: SignIn::Constants::Auth::LOA_THREE, highest: SignIn::Constants::Auth::LOA_THREE } } - let(:current_ial) { SignIn::Constants::Auth::IAL_TWO } - let(:max_ial) { SignIn::Constants::Auth::IAL_TWO } - let(:multifactor) { true } - let(:service_name) { SignIn::Constants::Auth::LOGINGOV } - let(:verified_icn) { 'verified-icn' } - let!(:user_verification) { create(:logingov_user_verification, logingov_uuid:) } - let(:user_uuid) { user_verification.backing_credential_identifier } - let(:login_code) { 'some-login-code' } - let(:expected_last_signed_in) { '2023-1-1' } - let(:client_state) { SecureRandom.alphanumeric(SignIn::Constants::Auth::CLIENT_STATE_MINIMUM_LENGTH) } - let(:client_config) { create(:client_config) } - let(:client_id) { client_config.client_id } - let(:code_challenge) { 'some-code-challenge' } - let(:type) { SignIn::Constants::Auth::LOGINGOV } - let(:sign_in) do - { - service_name:, - auth_broker: SignIn::Constants::Auth::BROKER_CODE, - client_id: - } - end - - before do - allow(SecureRandom).to receive(:uuid).and_return(login_code) - Timecop.freeze(expected_last_signed_in) - end - - after do - Timecop.return - end - - it 'creates a RepresentativeUser with expected attributes' do - representative_user_creator.perform - - representative_user = AccreditedRepresentativePortal::RepresentativeUser.find(user_uuid) - expect(representative_user).to be_a(AccreditedRepresentativePortal::RepresentativeUser) - expect(representative_user.uuid).to eq(user_uuid) - expect(representative_user.icn).to eq(verified_icn) - expect(representative_user.email).to eq(csp_email) - expect(representative_user.idme_uuid).to eq(nil) - expect(representative_user.logingov_uuid).to eq(logingov_uuid) - expect(representative_user.first_name).to eq(first_name) - expect(representative_user.last_name).to eq(last_name) - expect(representative_user.fingerprint).to eq(request_ip) - expect(representative_user.last_signed_in).to eq(expected_last_signed_in) - expect(representative_user.authn_context).to eq(authn_context) - expect(representative_user.loa).to eq(loa) - expect(representative_user.sign_in).to eq(sign_in) - end - - it 'sets terms code to nil' do - user_code_map = representative_user_creator.perform - - expect(user_code_map.terms_code).to be_nil - end - end -end diff --git a/modules/accredited_representative_portal/spec/services/accredited_representative_portal/representative_user_loader_spec.rb b/modules/accredited_representative_portal/spec/services/accredited_representative_portal/representative_user_loader_spec.rb index 2bfaabf1f3f..c7579507b92 100644 --- a/modules/accredited_representative_portal/spec/services/accredited_representative_portal/representative_user_loader_spec.rb +++ b/modules/accredited_representative_portal/spec/services/accredited_representative_portal/representative_user_loader_spec.rb @@ -6,8 +6,14 @@ describe '#perform' do subject(:representative_user_loader) { described_class.new(access_token:, request_ip:) } + let(:reloaded_user) { representative_user_loader.perform } + let(:access_token) { create(:access_token, user_uuid: user.uuid, session_handle:) } - let!(:user) { create(:representative_user, uuid: user_uuid, icn: user_icn, loa: user_loa) } + let(:ogc_number) { '123456' } # TODO-ARF 80297: Determine how to get ogc_number into RepresentativeUserLoader + let(:poa_codes) { %w[A1 B2 C3] } + let!(:user) do + create(:representative_user, uuid: user_uuid, icn: user_icn, loa: user_loa) + end let(:user_uuid) { user_account.id } let(:user_account) { create(:user_account) } let(:user_verification) { create(:idme_user_verification, user_account:) } @@ -16,6 +22,14 @@ let(:session) { create(:oauth_session, user_account:, user_verification:) } let(:session_handle) { session.handle } let(:request_ip) { '123.456.78.90' } + let!(:representative) do + FactoryBot.create(:representative, first_name: 'Bob', last_name: 'Smith', representative_id: ogc_number, + poa_codes:) + end + + before do + allow_any_instance_of(described_class).to receive(:ogc_number).and_return(ogc_number) + end shared_examples 'reloaded user' do context 'and associated session cannot be found' do @@ -49,8 +63,6 @@ end it 'reloads user object with expected attributes' do - reloaded_user = representative_user_loader.perform - expect(reloaded_user).to be_a(AccreditedRepresentativePortal::RepresentativeUser) expect(reloaded_user.uuid).to eq(user_uuid) expect(reloaded_user.email).to eq(email) @@ -59,6 +71,8 @@ expect(reloaded_user.icn).to eq(user_icn) expect(reloaded_user.idme_uuid).to eq(idme_uuid) expect(reloaded_user.logingov_uuid).to eq(nil) + expect(reloaded_user.ogc_number).to eq(ogc_number) + expect(reloaded_user.poa_codes).to eq(poa_codes) expect(reloaded_user.fingerprint).to eq(request_ip) expect(reloaded_user.last_signed_in).to eq(session.created_at) expect(reloaded_user.authn_context).to eq(authn_context) @@ -85,5 +99,31 @@ it_behaves_like 'reloaded user' end + + describe '#get_poa_codes' do + before do + user.destroy + end + + context 'when reloading a user' do + it 'sets the poa_codes based on the ogc_number' do + expect(reloaded_user.poa_codes).to match_array(poa_codes) + end + end + + # context 'when no representative is found for the ogc_number' do + # let(:non_existent_ogc_number) { 'non-existent-number' } + + # before do + # allow_any_instance_of(described_class).to receive(:ogc_number).and_return(non_existent_ogc_number) + # end + + # it 'raises a RepresentativeNotFoundError' do + # expect do + # reloaded_user + # end.to raise_error(described_class::RepresentativeNotFoundError) + # end + # end + end end end diff --git a/modules/accredited_representative_portal/spec/support/poa_record_generator.rb b/modules/accredited_representative_portal/spec/support/poa_record_generator.rb new file mode 100644 index 00000000000..5da28c938d9 --- /dev/null +++ b/modules/accredited_representative_portal/spec/support/poa_record_generator.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +# The module provides a method to save generated data to a JSON file, +# useful for seeding databases or loading mock data during development. +# +# Using the generator in the Rails console: +# 1. Start the console: `bundle exec rails c`. +# 2. Load the generator: +# `require Rails.root.join('modules/accredited_representative_portal', +# 'spec/support/poa_record_generator.rb')`. +# 3. Generate POA records: `PoaRecordGenerator.generate(num_records: 30)`. +# 4. Save to a file: `PoaRecordGenerator.generate_and_save_to_file(num_records: 30)`. +module PoaRecordGenerator + class << self + require 'faker' + require 'json' + + # Generates a hash with POA request records and associated metadata. + # @param num_records [Integer] The number of POA records to generate. + # @return [Hash] A hash containing the generated records and metadata. + def generate(num_records: 30) + Faker::UniqueGenerator.clear + + records = num_records.times.map do |i| + status = i < 25 ? 'pending' : %w[obsolete expired canceled].sample + { + procId: Faker::Number.unique.number(digits: 10).to_s, + type: 'powerOfAttorneyRequest', + attributes: generate_attributes(i, status) + } + end + + { records:, meta: { totalRecords: num_records.to_s } } + end + + # Generates POA request records and saves them to a specified JSON file. + # @param num_records [Integer] The number of POA records to generate. + # @param file_path [String] The file path to write the JSON data to. + def generate_and_save_to_file(num_records: 30, + file_path: 'modules/accredited_representative_portal/spec/fixtures/poa_records.json') + poa_data = generate(num_records:) + File.write(file_path, JSON.pretty_generate(poa_data)) + end + + private + + def generate_attributes(index, status) + { + poaCode: index.even? ? 'A1Q' : '091', + secondaryStatus: status, + dateRequestReceived: Faker::Date.backward(days: 30).iso8601, + dateRequestActioned: Faker::Date.forward(days: 30).iso8601, + declinedReason: status == 'pending' ? nil : Faker::Lorem.sentence, + healthInfoAuth: index.even? ? 'Y' : 'N', + changeAddressAuth: index.even? ? 'Y' : 'N', + claimant: generate_claimant, + veteran: generate_veteran, + VSORepresentative: generate_representative + } + end + + def generate_claimant + { + firstName: Faker::Name.first_name, + lastName: Faker::Name.last_name, + city: Faker::Address.city, + state: Faker::Address.state_abbr, + zip: Faker::Address.zip, + country: Faker::Address.country, + militaryPO: nil, + militaryPostalCode: nil, + participantID: Faker::Number.unique.number(digits: 10).to_s, + relationship: %w[Spouse Child Parent Friend].sample + } + end + + def generate_veteran + { + firstName: Faker::Name.first_name, + lastName: Faker::Name.last_name, + middleName: Faker::Name.middle_name, + participantID: Faker::Number.unique.number(digits: 10).to_s, + sensitivityLevel: %w[Low Medium High].sample + } + end + + def generate_representative + { + email: Faker::Internet.email, + firstName: Faker::Name.first_name, + lastName: Faker::Name.last_name + } + end + end +end diff --git a/modules/appeals_api/app/controllers/appeals_api/metadata_controller.rb b/modules/appeals_api/app/controllers/appeals_api/metadata_controller.rb index 2eedce8d9bd..d826ece2c81 100644 --- a/modules/appeals_api/app/controllers/appeals_api/metadata_controller.rb +++ b/modules/appeals_api/app/controllers/appeals_api/metadata_controller.rb @@ -10,6 +10,8 @@ class MetadataController < ::ApplicationController skip_before_action(:authenticate) before_action :set_default_headers + WARNING_EMOJI = ':warning:' + def decision_reviews render json: { meta: { @@ -37,11 +39,46 @@ def appeals_status def healthcheck render json: { description: 'Appeals API health check', - status: 'UP', + status: 'pass', time: Time.zone.now.to_formatted_s(:iso8601) } end + # Treat s3 as an internal resource as opposed to an upstream service per VA + def healthcheck_s3 + http_status_code = 200 + s3_heathy = s3_is_healthy? + unless s3_heathy + begin + http_status_code = 503 + slack_details = { + class: self.class.name, + warning: "#{WARNING_EMOJI} Appeals API healthcheck failed: unable to connect to AWS S3 bucket." + } + AppealsApi::Slack::Messager.new(slack_details).notify! + rescue => e + Rails.logger.error("Appeals API S3 failed Healthcheck slack notification failed: #{e.message}", e) + end + end + render json: { + description: 'Appeals API health check', + status: s3_heathy ? 'pass' : 'fail', + time: Time.zone.now.to_formatted_s(:iso8601) + }, status: http_status_code + end + + def s3_is_healthy? + # Internally, appeals defers to Benefits Intake for s3 ops, so we check the BI buckets + # using the BI Settings + s3 = Aws::S3::Resource.new(region: Settings.vba_documents.s3.region, + access_key_id: Settings.vba_documents.s3.aws_access_key_id, + secret_access_key: Settings.vba_documents.s3.aws_secret_access_key) + s3.client.head_bucket({ bucket: Settings.vba_documents.s3.bucket }) + true + rescue + false + end + def mail_status_upstream_healthcheck mail_status_code = proc do health_checker.mail_services_are_healthy? ? 200 : 503 diff --git a/modules/appeals_api/app/models/appeals_api/higher_level_review.rb b/modules/appeals_api/app/models/appeals_api/higher_level_review.rb index 55bce5e3fe4..0a6251cda7a 100644 --- a/modules/appeals_api/app/models/appeals_api/higher_level_review.rb +++ b/modules/appeals_api/app/models/appeals_api/higher_level_review.rb @@ -256,21 +256,7 @@ def update_status(status:, code: nil, detail: nil, raise_on_error: false) return if auth_headers.blank? # Go no further if we've removed PII if status == 'submitted' && email_present? - if Flipper.enabled? :decision_review_use_appeal_submitted_job - AppealsApi::AppealSubmittedJob.perform_async(id, self.class.name, appellant_local_time.iso8601) - else - AppealsApi::AppealReceivedJob.perform_async( - { - receipt_event: 'hlr_received', - email_identifier:, - first_name:, - date_submitted: veterans_local_time.iso8601, - guid: id, - claimant_email: claimant.email, - claimant_first_name: claimant.first_name - }.deep_stringify_keys - ) - end + AppealsApi::AppealReceivedJob.perform_async(id, self.class.name, appellant_local_time.iso8601) end end end diff --git a/modules/appeals_api/app/models/appeals_api/notice_of_disagreement.rb b/modules/appeals_api/app/models/appeals_api/notice_of_disagreement.rb index f1fdd642b2c..31a07d91ec8 100644 --- a/modules/appeals_api/app/models/appeals_api/notice_of_disagreement.rb +++ b/modules/appeals_api/app/models/appeals_api/notice_of_disagreement.rb @@ -269,21 +269,7 @@ def update_status(status:, code: nil, detail: nil, raise_on_error: false) return if auth_headers.blank? # Go no further if we've removed PII if status == 'submitted' && email_present? - if Flipper.enabled? :decision_review_use_appeal_submitted_job - AppealsApi::AppealSubmittedJob.perform_async(id, self.class.name, appellant_local_time.iso8601) - else - AppealsApi::AppealReceivedJob.perform_async( - { - receipt_event: 'nod_received', - email_identifier:, - first_name: veteran_first_name, - date_submitted: veterans_local_time.iso8601, - guid: id, - claimant_email: claimant.email, - claimant_first_name: claimant.first_name - }.deep_stringify_keys - ) - end + AppealsApi::AppealReceivedJob.perform_async(id, self.class.name, appellant_local_time.iso8601) end end end diff --git a/modules/appeals_api/app/models/appeals_api/supplemental_claim.rb b/modules/appeals_api/app/models/appeals_api/supplemental_claim.rb index 302acc72af8..2bfafbf9bae 100644 --- a/modules/appeals_api/app/models/appeals_api/supplemental_claim.rb +++ b/modules/appeals_api/app/models/appeals_api/supplemental_claim.rb @@ -255,21 +255,7 @@ def update_status(status:, code: nil, detail: nil, raise_on_error: false) return if auth_headers.blank? # Go no further if we've removed PII if status == 'submitted' && email_present? - if Flipper.enabled? :decision_review_use_appeal_submitted_job - AppealsApi::AppealSubmittedJob.perform_async(id, self.class.name, appellant_local_time.iso8601) - else - AppealsApi::AppealReceivedJob.perform_async( - { - receipt_event: 'sc_received', - email_identifier:, - first_name: veteran.first_name, - date_submitted: appellant_local_time.iso8601, - guid: id, - claimant_email: claimant.email, - claimant_first_name: claimant.first_name - }.deep_stringify_keys - ) - end + AppealsApi::AppealReceivedJob.perform_async(id, self.class.name, appellant_local_time.iso8601) end end end diff --git a/modules/appeals_api/app/sidekiq/appeals_api/appeal_received_job.rb b/modules/appeals_api/app/sidekiq/appeals_api/appeal_received_job.rb index 28fb96a93e3..d9577e8c866 100644 --- a/modules/appeals_api/app/sidekiq/appeals_api/appeal_received_job.rb +++ b/modules/appeals_api/app/sidekiq/appeals_api/appeal_received_job.rb @@ -9,152 +9,82 @@ class AppealReceivedJob STATSD_KEY_PREFIX = 'api.appeals.received' STATSD_CLAIMANT_EMAIL_SENT = "#{STATSD_KEY_PREFIX}.claimant.email.sent".freeze - # @param [Hash] opts - # @option opts [String] :receipt_event The callback indicating which appeal was received. Required. - # @option opts [Hash] :email_identifier The values identifying the receiving email address. Required - # @option email_identifier [String] :id_value Either the email or - # ICN (Integration Control Number - generated by the Master Patient Index)associated with the appellant. Required. - # @option email_identifier [String] :id_type The type of id value provided: 'email' or 'ICN'. Required. - # @option opts [String] :first_name First name of Veteran associated with the appeal. Required. - # @option opts [Datetime] :date_submitted The date of the appeal's submission. ISO8601 format. Required. - # @option opts [String] :guid The related appeal's ID. Required. - # @option opts [String] :claimant_email The non-Veteran claimant's email address. - # @option opts [String] :claimant_first_name The non-Veteran claimant's first name. - - def perform(opts) - @opts = opts - + # rubocop:disable Metrics/MethodLength + # Sends an email to a veteran or claimant stating that their appeal has been submitted + # @param [String] appeal_id The id of the appeal record + # @param [String] appeal_class_str The classname of the appeal as a string + # @param [String] date_submitted_str The date the appeal was submitted in ISO8601 string format + def perform(appeal_id, appeal_class_str, date_submitted_str) return unless FeatureFlipper.send_email? - return Rails.logger.error 'AppealReceived: Missing required keys' unless required_keys? - - send(opts['receipt_event'].to_sym) - end - - def hlr_received - return unless Flipper.enabled?(:decision_review_hlr_email) - - return log_error(guid, 'HLR') unless valid_email_identifier? - - template_type = 'higher_level_review_received' - template_name, template_id = template_id(template_type) - - return Rails.logger.error "AppealReceived: could not find template id for #{template_name}" if template_id.blank? - - vanotify_service.send_email(params({ template_id: })) - StatsD.increment(STATSD_CLAIMANT_EMAIL_SENT, tags: { appeal_type: 'hlr', claimant_type: }) - end - - def nod_received - return unless Flipper.enabled?(:decision_review_nod_email) - - return log_error(guid, 'NOD') unless valid_email_identifier? - - template_type = 'notice_of_disagreement_received' - template_name, template_id = template_id(template_type) - return Rails.logger.error "AppealReceived: could not find template id for #{template_name}" if template_id.blank? - - vanotify_service.send_email(params({ template_id: })) - StatsD.increment(STATSD_CLAIMANT_EMAIL_SENT, tags: { appeal_type: 'nod', claimant_type: }) - end - - def sc_received - return unless Flipper.enabled?(:decision_review_sc_email) - - return log_error(guid, 'SC') unless valid_email_identifier? - - template_type = 'supplemental_claim_received' - template_name, template_id = template_id(template_type) - - return Rails.logger.error "AppealReceived: could not find template id for #{template_name}" if template_id.blank? - - vanotify_service.send_email(params({ template_id: })) - StatsD.increment(STATSD_CLAIMANT_EMAIL_SENT, tags: { appeal_type: 'sc', claimant_type: }) - end - - private - - attr_accessor :opts - - def vanotify_service - @vanotify_service ||= VaNotify::Service.new(Settings.vanotify.services.lighthouse.api_key) - end - - def params(template_opts) - [ - lookup, - template_opts, - personalisation - ].reduce(&:merge) - end - - def lookup - return { email_address: opts['claimant_email'] } if opts['claimant_email'].present? - - if opts['email_identifier']['id_type'] == 'email' - { email_address: opts['email_identifier']['id_value'] } - else - { recipient_identifier: { id_value: opts['email_identifier']['id_value'], - id_type: opts['email_identifier']['id_type'] } } + if appeal_id.blank? || appeal_class_str.blank? || date_submitted_str.blank? + argument_list = [appeal_id, appeal_class_str, date_submitted_str] + Rails.logger.error("#{self.class.name}: Missing arguments: Received #{argument_list.join(', ')}") + return end - end - def template_id(template) - t = claimant? ? "#{template}_claimant" : template - template_id = Settings.vanotify.services.lighthouse.template_id.public_send(t) + appeal = appeal_class_str.constantize.find(appeal_id) - [t, template_id] - end - - def personalisation - p = { 'date_submitted' => date_submitted } - if claimant? - p['first_name'] = opts['claimant_first_name'] - p['veterans_name'] = opts['first_name'] - else - p['first_name'] = opts['first_name'] + unless appeal.form_data.present? && appeal.auth_headers.present? + Rails.logger.error("#{self.class.name}: Missing PII for #{appeal_class_str} #{appeal_id}") + return end - { personalisation: p } - end - def log_error(guid, type) - Rails.logger.error "No lookup value present for AppealsApi::AppealReceived notification #{type} - GUID: #{guid}" - end + appeal_type_name = appeal.class.name.demodulize.snakecase + template_name = "#{appeal_type_name}_received#{appeal.non_veteran_claimant? ? '_claimant' : ''}" + template_id = Settings.vanotify.services.lighthouse.template_id[template_name] - def guid - opts['guid'] - end - - def date_submitted - @date_submitted ||= DateTime.iso8601(opts['date_submitted']).strftime('%B %d, %Y') - end + if template_id.blank? + Rails.logger.error("#{self.class.name}: could not find VANotify template id for '#{template_name}'") + return + end - def valid_email_identifier? - if claimant? - opts['claimant_email'].present? + date_submitted = DateTime.iso8601(date_submitted_str).strftime('%B %d, %Y') + + if appeal.non_veteran_claimant? + vanotify_service.send_email( + { + email_address: appeal.claimant.email, + personalisation: { + date_submitted:, + first_name: appeal.claimant.first_name, + veterans_name: appeal.veteran.first_name + }, + template_id: + } + ) else - required_email_identifier_keys.all? { |k| opts.dig('email_identifier', k).present? } + identifier = if appeal.email_identifier[:id_type] == 'email' + { email_address: appeal.email_identifier[:id_value] } + else + { recipient_identifier: appeal.email_identifier } + end + + vanotify_service.send_email( + { + **identifier, + personalisation: { + date_submitted:, + first_name: appeal.veteran.first_name + }, + template_id: + } + ) end - end - - def claimant? - opts['claimant_first_name'].present? || opts['claimant_email'].present? - end - def claimant_type - claimant? ? 'non-veteran' : 'veteran' + StatsD.increment(STATSD_CLAIMANT_EMAIL_SENT, tags: { + appeal_type: appeal.class.name.demodulize.scan(/\p{Upper}/).map(&:downcase).join, + claimant_type: appeal.non_veteran_claimant? ? 'non-veteran' : 'veteran' + }) + rescue ActiveRecord::RecordNotFound + Rails.logger.error("#{self.class.name}: Unable to find #{appeal_class_str} with id '#{appeal_id}'") + rescue Date::Error + Rails.logger.error("#{self.class.name}: Invalid date format: '#{date_submitted_str}' must be in iso8601 format") end + # rubocop:enable Metrics/MethodLength - def required_email_identifier_keys - %w[id_type id_value] - end - - def required_keys? - required_keys.all? { |k| opts.key?(k) } - end - - def required_keys - %w[receipt_event guid email_identifier date_submitted first_name] + def vanotify_service + @vanotify_service ||= VaNotify::Service.new(Settings.vanotify.services.lighthouse.api_key) end end end diff --git a/modules/appeals_api/app/sidekiq/appeals_api/appeal_submitted_job.rb b/modules/appeals_api/app/sidekiq/appeals_api/appeal_submitted_job.rb deleted file mode 100644 index d1a109e0c9d..00000000000 --- a/modules/appeals_api/app/sidekiq/appeals_api/appeal_submitted_job.rb +++ /dev/null @@ -1,91 +0,0 @@ -# frozen_string_literal: true - -require 'sidekiq' -require 'feature_flipper' - -module AppealsApi - class AppealSubmittedJob - include Sidekiq::Job - # These constants are set to match the previous version of this job, which was AppealsApi::AppealReceivedJob - STATSD_KEY_PREFIX = 'api.appeals.received' - STATSD_CLAIMANT_EMAIL_SENT = "#{STATSD_KEY_PREFIX}.claimant.email.sent".freeze - - # rubocop:disable Metrics/MethodLength - # Sends an email to a veteran or claimant stating that their appeal has been submitted - # @param [String] appeal_id The id of the appeal record - # @param [String] appeal_class_str The classname of the appeal as a string - # @param [String] date_submitted_str The date the appeal was submitted in ISO8601 string format - def perform(appeal_id, appeal_class_str, date_submitted_str) - return unless FeatureFlipper.send_email? - - if appeal_id.blank? || appeal_class_str.blank? || date_submitted_str.blank? - argument_list = [appeal_id, appeal_class_str, date_submitted_str] - Rails.logger.error("#{self.class.name}: Missing arguments: Received #{argument_list.join(', ')}") - return - end - - appeal = appeal_class_str.constantize.find(appeal_id) - - unless appeal.form_data.present? && appeal.auth_headers.present? - Rails.logger.error("#{self.class.name}: Missing PII for #{appeal_class_str} #{appeal_id}") - return - end - - appeal_type_name = appeal.class.name.demodulize.snakecase - template_name = "#{appeal_type_name}_received#{appeal.non_veteran_claimant? ? '_claimant' : ''}" - template_id = Settings.vanotify.services.lighthouse.template_id[template_name] - - if template_id.blank? - Rails.logger.error("#{self.class.name}: could not find VANotify template id for '#{template_name}'") - return - end - - date_submitted = DateTime.iso8601(date_submitted_str).strftime('%B %d, %Y') - - if appeal.non_veteran_claimant? - vanotify_service.send_email( - { - email_address: appeal.claimant.email, - personalisation: { - date_submitted:, - first_name: appeal.claimant.first_name, - veterans_name: appeal.veteran.first_name - }, - template_id: - } - ) - else - identifier = if appeal.email_identifier[:id_type] == 'email' - { email_address: appeal.email_identifier[:id_value] } - else - { recipient_identifier: appeal.email_identifier } - end - - vanotify_service.send_email( - { - **identifier, - personalisation: { - date_submitted:, - first_name: appeal.veteran.first_name - }, - template_id: - } - ) - end - - StatsD.increment(STATSD_CLAIMANT_EMAIL_SENT, tags: { - appeal_type: appeal.class.name.demodulize.scan(/\p{Upper}/).map(&:downcase).join, - claimant_type: appeal.non_veteran_claimant? ? 'non-veteran' : 'veteran' - }) - rescue ActiveRecord::RecordNotFound - Rails.logger.error("#{self.class.name}: Unable to find #{appeal_class_str} with id '#{appeal_id}'") - rescue Date::Error - Rails.logger.error("#{self.class.name}: Invalid date format: '#{date_submitted_str}' must be in iso8601 format") - end - # rubocop:enable Metrics/MethodLength - - def vanotify_service - @vanotify_service ||= VaNotify::Service.new(Settings.vanotify.services.lighthouse.api_key) - end - end -end diff --git a/modules/appeals_api/app/swagger/appealable_issues/v0/api_description.md b/modules/appeals_api/app/swagger/appealable_issues/v0/api_description.md index 07211f176f4..9297adb073a 100644 --- a/modules/appeals_api/app/swagger/appealable_issues/v0/api_description.md +++ b/modules/appeals_api/app/swagger/appealable_issues/v0/api_description.md @@ -16,3 +16,7 @@ The authentication model for the Appealable Issues API uses OAuth 2.0/OpenID Con * [Client Credentials Grant (CCG)](https://developer.va.gov/explore/api/appealable-issues/client-credentials) **Important:** To get production access using client credentials grant, 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). + +### Test data + +Our sandbox environment is populated with [Veteran test data](https://github.com/department-of-veterans-affairs/vets-api-clients/blob/master/test_accounts/appealable_issues_test_accounts.md) that can be used to test various response scenarios. This sandbox data contains no PII or PHI, but mimics real Veteran account information. diff --git a/modules/appeals_api/app/swagger/appealable_issues/v0/api_description_dev.md b/modules/appeals_api/app/swagger/appealable_issues/v0/api_description_dev.md index 7d2df06f56f..af2f6e03b8b 100644 --- a/modules/appeals_api/app/swagger/appealable_issues/v0/api_description_dev.md +++ b/modules/appeals_api/app/swagger/appealable_issues/v0/api_description_dev.md @@ -16,3 +16,7 @@ The authentication model for the Appealable Issues API uses OAuth 2.0/OpenID Con * [Client Credentials Grant (CCG)](/explore/api/appealable-issues/client-credentials) **Important:** To get production access using client credentials grant, you must either work for VA or have specific VA agreements in place. If you have questions, [contact us](/support/contact-us). + +### Test data + +Our sandbox environment is populated with [Veteran test data](https://github.com/department-of-veterans-affairs/vets-api-clients/blob/master/test_accounts/appealable_issues_test_accounts.md) that can be used to test various response scenarios. This sandbox data contains no PII or PHI, but mimics real Veteran account information. diff --git a/modules/appeals_api/app/swagger/appealable_issues/v0/swagger.json b/modules/appeals_api/app/swagger/appealable_issues/v0/swagger.json index 362d01beaed..9f21a8aa689 100644 --- a/modules/appeals_api/app/swagger/appealable_issues/v0/swagger.json +++ b/modules/appeals_api/app/swagger/appealable_issues/v0/swagger.json @@ -6,7 +6,7 @@ "contact": { "name": "developer.va.gov" }, - "description": "The Appealable Issues API lets you retrieve a list of a claimant’s appealable issues and any chains of preceding issues. Appealable issues are issues from claims about which VA has made a decision that may be eligible for appeal. Not all appealable issues are guaranteed to be eligible for appeal; for example, claimants may have another appeal in progress for an issue.\n\nTo check the status of all decision reviews and appeals for a specified individual, use the [Appeals Status API](https://developer.va.gov/explore/api/appeals-status/docs).\n\nTo file an appeal or decision review, use one of these APIs: \n* [Higher-Level Reviews API](https://developer.va.gov/explore/api/higher-level-reviews/docs)\n* [Notice of Disagreements API](https://developer.va.gov/explore/api/notice-of-disagreements/docs)\n* [Supplemental Claims API](https://developer.va.gov/explore/api/supplemental-claims/docs)\n\n## Technical overview\nThe Appealable Issues API pulls data from Caseflow, a case management system. It provides decision review and appeal data that can be used for submitting a Higher Level Review, Notice of Disagreement, or Supplemental Claim.\n\n### Authorization and Access\nThe authentication model for the Appealable Issues API uses OAuth 2.0/OpenID Connect. The following authorization models are supported:\n* [Authorization code flow](https://developer.va.gov/explore/api/appealable-issues/authorization-code)\n* [Client Credentials Grant (CCG)](https://developer.va.gov/explore/api/appealable-issues/client-credentials)\n\n**Important:** To get production access using client credentials grant, 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" + "description": "The Appealable Issues API lets you retrieve a list of a claimant’s appealable issues and any chains of preceding issues. Appealable issues are issues from claims about which VA has made a decision that may be eligible for appeal. Not all appealable issues are guaranteed to be eligible for appeal; for example, claimants may have another appeal in progress for an issue.\n\nTo check the status of all decision reviews and appeals for a specified individual, use the [Appeals Status API](https://developer.va.gov/explore/api/appeals-status/docs).\n\nTo file an appeal or decision review, use one of these APIs: \n* [Higher-Level Reviews API](https://developer.va.gov/explore/api/higher-level-reviews/docs)\n* [Notice of Disagreements API](https://developer.va.gov/explore/api/notice-of-disagreements/docs)\n* [Supplemental Claims API](https://developer.va.gov/explore/api/supplemental-claims/docs)\n\n## Technical overview\nThe Appealable Issues API pulls data from Caseflow, a case management system. It provides decision review and appeal data that can be used for submitting a Higher Level Review, Notice of Disagreement, or Supplemental Claim.\n\n### Authorization and Access\nThe authentication model for the Appealable Issues API uses OAuth 2.0/OpenID Connect. The following authorization models are supported:\n* [Authorization code flow](https://developer.va.gov/explore/api/appealable-issues/authorization-code)\n* [Client Credentials Grant (CCG)](https://developer.va.gov/explore/api/appealable-issues/client-credentials)\n\n**Important:** To get production access using client credentials grant, 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\n\nOur sandbox environment is populated with [Veteran test data](https://github.com/department-of-veterans-affairs/vets-api-clients/blob/master/test_accounts/appealable_issues_test_accounts.md) that can be used to test various response scenarios. This sandbox data contains no PII or PHI, but mimics real Veteran account information.\n" }, "tags": [ { diff --git a/modules/appeals_api/app/swagger/appealable_issues/v0/swagger_dev.json b/modules/appeals_api/app/swagger/appealable_issues/v0/swagger_dev.json index bc1ce722439..5cd3089975b 100644 --- a/modules/appeals_api/app/swagger/appealable_issues/v0/swagger_dev.json +++ b/modules/appeals_api/app/swagger/appealable_issues/v0/swagger_dev.json @@ -6,7 +6,7 @@ "contact": { "name": "developer.va.gov" }, - "description": "The Appealable Issues API lets you retrieve a list of a claimant’s appealable issues and any chains of preceding issues. Appealable issues are issues from claims about which VA has made a decision that may be eligible for appeal. Not all appealable issues are guaranteed to be eligible for appeal; for example, claimants may have another appeal in progress for an issue.\n\nTo check the status of all decision reviews and appeals for a specified individual, use the [Appeals Status API](/explore/api/appeals-status/docs).\n\nTo file an appeal or decision review, use one of these APIs:\n* [Higher-Level Reviews API](/explore/api/higher-level-reviews/docs)\n* [Notice of Disagreements API](/explore/api/notice-of-disagreements/docs)\n* [Supplemental Claims API](/explore/api/supplemental-claims/docs)\n\n## Technical overview\nThe Appealable Issues API pulls data from Caseflow, a case management system. It provides decision review and appeal data that can be used for submitting a Higher Level Review, Notice of Disagreement, or Supplemental Claim.\n\n### Authorization and Access\nThe authentication model for the Appealable Issues API uses OAuth 2.0/OpenID Connect. The following authorization models are supported:\n* [Authorization code flow](/explore/api/appealable-issues/authorization-code)\n* [Client Credentials Grant (CCG)](/explore/api/appealable-issues/client-credentials)\n\n**Important:** To get production access using client credentials grant, you must either work for VA or have specific VA agreements in place. If you have questions, [contact us](/support/contact-us).\n" + "description": "The Appealable Issues API lets you retrieve a list of a claimant’s appealable issues and any chains of preceding issues. Appealable issues are issues from claims about which VA has made a decision that may be eligible for appeal. Not all appealable issues are guaranteed to be eligible for appeal; for example, claimants may have another appeal in progress for an issue.\n\nTo check the status of all decision reviews and appeals for a specified individual, use the [Appeals Status API](/explore/api/appeals-status/docs).\n\nTo file an appeal or decision review, use one of these APIs:\n* [Higher-Level Reviews API](/explore/api/higher-level-reviews/docs)\n* [Notice of Disagreements API](/explore/api/notice-of-disagreements/docs)\n* [Supplemental Claims API](/explore/api/supplemental-claims/docs)\n\n## Technical overview\nThe Appealable Issues API pulls data from Caseflow, a case management system. It provides decision review and appeal data that can be used for submitting a Higher Level Review, Notice of Disagreement, or Supplemental Claim.\n\n### Authorization and Access\nThe authentication model for the Appealable Issues API uses OAuth 2.0/OpenID Connect. The following authorization models are supported:\n* [Authorization code flow](/explore/api/appealable-issues/authorization-code)\n* [Client Credentials Grant (CCG)](/explore/api/appealable-issues/client-credentials)\n\n**Important:** To get production access using client credentials grant, you must either work for VA or have specific VA agreements in place. If you have questions, [contact us](/support/contact-us).\n\n### Test data\n\nOur sandbox environment is populated with [Veteran test data](https://github.com/department-of-veterans-affairs/vets-api-clients/blob/master/test_accounts/appealable_issues_test_accounts.md) that can be used to test various response scenarios. This sandbox data contains no PII or PHI, but mimics real Veteran account information.\n" }, "tags": [ { diff --git a/modules/appeals_api/app/swagger/appeals_status/v1/api_description.md b/modules/appeals_api/app/swagger/appeals_status/v1/api_description.md index 919b42ed084..3c81af4ccc7 100644 --- a/modules/appeals_api/app/swagger/appeals_status/v1/api_description.md +++ b/modules/appeals_api/app/swagger/appeals_status/v1/api_description.md @@ -27,4 +27,4 @@ The authentication model for the Appeals Status API uses OAuth 2.0/OpenID Connec ### Test data -The database powering our sandbox environment is populated with [Veteran test data](https://github.com/department-of-veterans-affairs/vets-api-clients/blob/master/test_accounts/benefits_test_accounts.md). This sandbox data contains no PII or PHI, but mimics real Veteran account information. +Our sandbox environment is populated with [Veteran test data](https://github.com/department-of-veterans-affairs/vets-api-clients/blob/master/test_accounts/appeals_status_test_accounts.md) that can be used to test various response scenarios. This sandbox data contains no PII or PHI, but mimics real Veteran account information. diff --git a/modules/appeals_api/app/swagger/appeals_status/v1/api_description_dev.md b/modules/appeals_api/app/swagger/appeals_status/v1/api_description_dev.md index 88a94d6d197..ca74d811fe2 100644 --- a/modules/appeals_api/app/swagger/appeals_status/v1/api_description_dev.md +++ b/modules/appeals_api/app/swagger/appeals_status/v1/api_description_dev.md @@ -27,4 +27,4 @@ The authentication model for the Appeals Status API uses OAuth 2.0/OpenID Connec ### Test data -The database powering our sandbox environment is populated with [Veteran test data](https://github.com/department-of-veterans-affairs/vets-api-clients/blob/master/test_accounts/benefits_test_accounts.md). This sandbox data contains no PII or PHI, but mimics real Veteran account information. +Our sandbox environment is populated with [Veteran test data](https://github.com/department-of-veterans-affairs/vets-api-clients/blob/master/test_accounts/appeals_status_test_accounts.md) that can be used to test various response scenarios. This sandbox data contains no PII or PHI, but mimics real Veteran account information. diff --git a/modules/appeals_api/app/swagger/appeals_status/v1/swagger.json b/modules/appeals_api/app/swagger/appeals_status/v1/swagger.json index c46ba154a7a..d0db510e5bf 100644 --- a/modules/appeals_api/app/swagger/appeals_status/v1/swagger.json +++ b/modules/appeals_api/app/swagger/appeals_status/v1/swagger.json @@ -6,7 +6,7 @@ "contact": { "name": "developer.va.gov" }, - "description": "The Appeals Status API allows you to request the statuses of all decision reviews for a Veteran, including decision reviews following the AMA process and legacy benefit appeals. The statuses are returned as read only.\n\nTo retrieve a list of a claimant’s active contestable issues or legacy appeals, use one of these APIs:\n* [Appealable Issues API](https://developer.va.gov/explore/api/appealable-issues/docs)\n* [Legacy Appeals API](https://developer.va.gov/explore/api/legacy-appeals/docs)\n\nTo file an appeal or decision review, use one of these APIs:\n* [Higher-Level Reviews API](https://developer.va.gov/explore/api/higher-level-reviews/docs)\n* [Notice of Disagreements API](https://developer.va.gov/explore/api/notice-of-disagreements/docs)\n* [Supplemental Claims API](https://developer.va.gov/explore/api/supplemental-claims/docs)\n\n## Background\n\nThe Appeals API passes data through to Caseflow, a case management system. Caseflow returns the current status of the Veteran’s decision reviews and/or benefits appeals.\n\nBecause this application is designed to allow third-parties to request information on behalf of a Veteran, we are not using VA Authentication Federation Infrastructure (VAAFI) headers or Single Sign On External (SSOe).\n\n## Technical overview\n\n### Authentication and Authorization\n\nThe authentication model for the Appeals Status API uses OAuth 2.0/OpenID Connect. The following authorization models are supported:\n* [Authorization code flow](https://developer.va.gov/explore/api/appeals-status/authorization-code)\n* [Client Credentials Grant (CCG)](https://developer.va.gov/explore/api/appeals-status/client-credentials)\n\n**Important:** To get production access using client credentials grant, 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\n\nThe database powering our sandbox environment is populated with [Veteran test data](https://github.com/department-of-veterans-affairs/vets-api-clients/blob/master/test_accounts/benefits_test_accounts.md). This sandbox data contains no PII or PHI, but mimics real Veteran account information.\n" + "description": "The Appeals Status API allows you to request the statuses of all decision reviews for a Veteran, including decision reviews following the AMA process and legacy benefit appeals. The statuses are returned as read only.\n\nTo retrieve a list of a claimant’s active contestable issues or legacy appeals, use one of these APIs:\n* [Appealable Issues API](https://developer.va.gov/explore/api/appealable-issues/docs)\n* [Legacy Appeals API](https://developer.va.gov/explore/api/legacy-appeals/docs)\n\nTo file an appeal or decision review, use one of these APIs:\n* [Higher-Level Reviews API](https://developer.va.gov/explore/api/higher-level-reviews/docs)\n* [Notice of Disagreements API](https://developer.va.gov/explore/api/notice-of-disagreements/docs)\n* [Supplemental Claims API](https://developer.va.gov/explore/api/supplemental-claims/docs)\n\n## Background\n\nThe Appeals API passes data through to Caseflow, a case management system. Caseflow returns the current status of the Veteran’s decision reviews and/or benefits appeals.\n\nBecause this application is designed to allow third-parties to request information on behalf of a Veteran, we are not using VA Authentication Federation Infrastructure (VAAFI) headers or Single Sign On External (SSOe).\n\n## Technical overview\n\n### Authentication and Authorization\n\nThe authentication model for the Appeals Status API uses OAuth 2.0/OpenID Connect. The following authorization models are supported:\n* [Authorization code flow](https://developer.va.gov/explore/api/appeals-status/authorization-code)\n* [Client Credentials Grant (CCG)](https://developer.va.gov/explore/api/appeals-status/client-credentials)\n\n**Important:** To get production access using client credentials grant, 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\n\nOur sandbox environment is populated with [Veteran test data](https://github.com/department-of-veterans-affairs/vets-api-clients/blob/master/test_accounts/appeals_status_test_accounts.md) that can be used to test various response scenarios. This sandbox data contains no PII or PHI, but mimics real Veteran account information.\n" }, "tags": [ { diff --git a/modules/appeals_api/app/swagger/appeals_status/v1/swagger_dev.json b/modules/appeals_api/app/swagger/appeals_status/v1/swagger_dev.json index 7b848faf002..021a9114d67 100644 --- a/modules/appeals_api/app/swagger/appeals_status/v1/swagger_dev.json +++ b/modules/appeals_api/app/swagger/appeals_status/v1/swagger_dev.json @@ -6,7 +6,7 @@ "contact": { "name": "developer.va.gov" }, - "description": "The Appeals Status API allows you to request the statuses of all decision reviews for a Veteran, including decision reviews following the AMA process and legacy benefit appeals. The statuses are returned as read only.\n\nTo retrieve a list of a claimant’s active contestable issues or legacy appeals, use one of these APIs:\n* [Appealable Issues API](/explore/api/appealable-issues/docs)\n* [Legacy Appeals API](/explore/api/legacy-appeals/docs)\n\nTo file an appeal or decision review, use one of these APIs:\n* [Higher-Level Reviews API](/explore/api/higher-level-reviews/docs)\n* [Notice of Disagreements API](/explore/api/notice-of-disagreements/docs)\n* [Supplemental Claims API](/explore/api/supplemental-claims/docs)\n\n## Background\n\nThe Appeals API passes data through to Caseflow, a case management system. Caseflow returns the current status of the Veteran’s decision reviews and/or benefits appeals.\n\nBecause this application is designed to allow third-parties to request information on behalf of a Veteran, we are not using VA Authentication Federation Infrastructure (VAAFI) headers or Single Sign On External (SSOe).\n\n## Technical overview\n\n### Authentication and Authorization\n\nThe authentication model for the Appeals Status API uses OAuth 2.0/OpenID Connect. The following authorization models are supported:\n* [Authorization code flow](/explore/api/appeals-status/authorization-code)\n* [Client Credentials Grant (CCG)](/explore/api/appeals-status/client-credentials)\n\n**Important:** To get production access using client credentials grant, you must either work for VA or have specific VA agreements in place. If you have questions, [contact us](https://dev-developer.va.gov/support/contact-us).\n\n### Test data\n\nThe database powering our sandbox environment is populated with [Veteran test data](https://github.com/department-of-veterans-affairs/vets-api-clients/blob/master/test_accounts/benefits_test_accounts.md). This sandbox data contains no PII or PHI, but mimics real Veteran account information.\n" + "description": "The Appeals Status API allows you to request the statuses of all decision reviews for a Veteran, including decision reviews following the AMA process and legacy benefit appeals. The statuses are returned as read only.\n\nTo retrieve a list of a claimant’s active contestable issues or legacy appeals, use one of these APIs:\n* [Appealable Issues API](/explore/api/appealable-issues/docs)\n* [Legacy Appeals API](/explore/api/legacy-appeals/docs)\n\nTo file an appeal or decision review, use one of these APIs:\n* [Higher-Level Reviews API](/explore/api/higher-level-reviews/docs)\n* [Notice of Disagreements API](/explore/api/notice-of-disagreements/docs)\n* [Supplemental Claims API](/explore/api/supplemental-claims/docs)\n\n## Background\n\nThe Appeals API passes data through to Caseflow, a case management system. Caseflow returns the current status of the Veteran’s decision reviews and/or benefits appeals.\n\nBecause this application is designed to allow third-parties to request information on behalf of a Veteran, we are not using VA Authentication Federation Infrastructure (VAAFI) headers or Single Sign On External (SSOe).\n\n## Technical overview\n\n### Authentication and Authorization\n\nThe authentication model for the Appeals Status API uses OAuth 2.0/OpenID Connect. The following authorization models are supported:\n* [Authorization code flow](/explore/api/appeals-status/authorization-code)\n* [Client Credentials Grant (CCG)](/explore/api/appeals-status/client-credentials)\n\n**Important:** To get production access using client credentials grant, you must either work for VA or have specific VA agreements in place. If you have questions, [contact us](https://dev-developer.va.gov/support/contact-us).\n\n### Test data\n\nOur sandbox environment is populated with [Veteran test data](https://github.com/department-of-veterans-affairs/vets-api-clients/blob/master/test_accounts/appeals_status_test_accounts.md) that can be used to test various response scenarios. This sandbox data contains no PII or PHI, but mimics real Veteran account information.\n" }, "tags": [ { diff --git a/modules/appeals_api/app/swagger/legacy_appeals/v0/api_description.md b/modules/appeals_api/app/swagger/legacy_appeals/v0/api_description.md index 4343714f4f2..b678bc0adb3 100644 --- a/modules/appeals_api/app/swagger/legacy_appeals/v0/api_description.md +++ b/modules/appeals_api/app/swagger/legacy_appeals/v0/api_description.md @@ -16,3 +16,7 @@ The authentication model for the Legacy Appeals API uses OAuth 2.0/OpenID Connec * [Client Credentials Grant (CCG)](https://developer.va.gov/explore/api/legacy-appeals/client-credentials) **Important:** To get production access using client credentials grant, 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). + +### Test data + +Our sandbox environment is populated with [Veteran test data](https://github.com/department-of-veterans-affairs/vets-api-clients/blob/master/test_accounts/legacy_appeals_test_accounts.md) that can be used to test various response scenarios. This sandbox data contains no PII or PHI, but mimics real Veteran account information. diff --git a/modules/appeals_api/app/swagger/legacy_appeals/v0/api_description_dev.md b/modules/appeals_api/app/swagger/legacy_appeals/v0/api_description_dev.md index edb18f4155b..3e1ca6ce1f5 100644 --- a/modules/appeals_api/app/swagger/legacy_appeals/v0/api_description_dev.md +++ b/modules/appeals_api/app/swagger/legacy_appeals/v0/api_description_dev.md @@ -16,3 +16,7 @@ The authentication model for the Legacy Appeals API uses OAuth 2.0/OpenID Connec * [Client Credentials Grant (CCG)](/explore/api/legacy-appeals/client-credentials) **Important:** To get production access using client credentials grant, you must either work for VA or have specific VA agreements in place. If you have questions, [contact us](/support/contact-us). + +### Test data + +Our sandbox environment is populated with [Veteran test data](https://github.com/department-of-veterans-affairs/vets-api-clients/blob/master/test_accounts/legacy_appeals_test_accounts.md) that can be used to test various response scenarios. This sandbox data contains no PII or PHI, but mimics real Veteran account information. diff --git a/modules/appeals_api/app/swagger/legacy_appeals/v0/swagger.json b/modules/appeals_api/app/swagger/legacy_appeals/v0/swagger.json index a7437f2b0eb..3686648cc95 100644 --- a/modules/appeals_api/app/swagger/legacy_appeals/v0/swagger.json +++ b/modules/appeals_api/app/swagger/legacy_appeals/v0/swagger.json @@ -6,7 +6,7 @@ "contact": { "name": "developer.va.gov" }, - "description": "The Legacy Appeals API returns a list of a claimant's active legacy appeals, which are not part of the Appeals Modernization Act (AMA) process. This list can be used to determine whether to opt in to the new decision review process. [Learn more about managing a legacy appeal](https://www.va.gov/decision-reviews/legacy-appeals/).\n\nTo check the status of all decision reviews and appeals for a specified individual, use the [Appeals Status API](https://developer.va.gov/explore/api/appeals-status/docs).\n\nTo file an appeal or decision review, use one of these APIs: \n* [Higher-Level Reviews API](https://developer.va.gov/explore/api/higher-level-reviews/docs)\n* [Notice of Disagreements API](https://developer.va.gov/explore/api/notice-of-disagreements/docs)\n* [Supplemental Claims API](https://developer.va.gov/explore/api/supplemental-claims/docs)\n\n## Technical overview\nThe Legacy Appeals API pulls data from Caseflow, a case management system. It provides decision review and appeal data that can be used for submitting a Higher Level Review, Notice of Disagreement, or Supplemental Claim.\n\n### Authorization and Access\nThe authentication model for the Legacy Appeals API uses OAuth 2.0/OpenID Connect. The following authorization models are supported:\n* [Authorization code flow](https://developer.va.gov/explore/api/legacy-appeals/authorization-code)\n* [Client Credentials Grant (CCG)](https://developer.va.gov/explore/api/legacy-appeals/client-credentials)\n\n**Important:** To get production access using client credentials grant, 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" + "description": "The Legacy Appeals API returns a list of a claimant's active legacy appeals, which are not part of the Appeals Modernization Act (AMA) process. This list can be used to determine whether to opt in to the new decision review process. [Learn more about managing a legacy appeal](https://www.va.gov/decision-reviews/legacy-appeals/).\n\nTo check the status of all decision reviews and appeals for a specified individual, use the [Appeals Status API](https://developer.va.gov/explore/api/appeals-status/docs).\n\nTo file an appeal or decision review, use one of these APIs: \n* [Higher-Level Reviews API](https://developer.va.gov/explore/api/higher-level-reviews/docs)\n* [Notice of Disagreements API](https://developer.va.gov/explore/api/notice-of-disagreements/docs)\n* [Supplemental Claims API](https://developer.va.gov/explore/api/supplemental-claims/docs)\n\n## Technical overview\nThe Legacy Appeals API pulls data from Caseflow, a case management system. It provides decision review and appeal data that can be used for submitting a Higher Level Review, Notice of Disagreement, or Supplemental Claim.\n\n### Authorization and Access\nThe authentication model for the Legacy Appeals API uses OAuth 2.0/OpenID Connect. The following authorization models are supported:\n* [Authorization code flow](https://developer.va.gov/explore/api/legacy-appeals/authorization-code)\n* [Client Credentials Grant (CCG)](https://developer.va.gov/explore/api/legacy-appeals/client-credentials)\n\n**Important:** To get production access using client credentials grant, 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\n\nOur sandbox environment is populated with [Veteran test data](https://github.com/department-of-veterans-affairs/vets-api-clients/blob/master/test_accounts/legacy_appeals_test_accounts.md) that can be used to test various response scenarios. This sandbox data contains no PII or PHI, but mimics real Veteran account information.\n" }, "tags": [ { diff --git a/modules/appeals_api/app/swagger/legacy_appeals/v0/swagger_dev.json b/modules/appeals_api/app/swagger/legacy_appeals/v0/swagger_dev.json index 8bf863d6aaa..29e39855eac 100644 --- a/modules/appeals_api/app/swagger/legacy_appeals/v0/swagger_dev.json +++ b/modules/appeals_api/app/swagger/legacy_appeals/v0/swagger_dev.json @@ -6,7 +6,7 @@ "contact": { "name": "developer.va.gov" }, - "description": "The Legacy Appeals API returns a list of a claimant's active legacy appeals, which are not part of the Appeals Modernization Act (AMA) process. This list can be used to determine whether to opt in to the new decision review process. [Learn more about managing a legacy appeal](https://www.va.gov/decision-reviews/legacy-appeals/).\n\nTo check the status of all decision reviews and appeals for a specified individual, use the [Appeals Status API](/explore/api/appeals-status/docs).\n\nTo file an appeal or decision review, use one of these APIs:\n* [Higher-Level Reviews API](/explore/api/higher-level-reviews/docs)\n* [Notice of Disagreements API](/explore/api/notice-of-disagreements/docs)\n* [Supplemental Claims API](/explore/api/supplemental-claims/docs)\n\n## Technical overview\nThe Legacy Appeals API pulls data from Caseflow, a case management system. It provides decision review and appeal data that can be used for submitting a Higher Level Review, Notice of Disagreement, or Supplemental Claim.\n\n### Authorization and Access\nThe authentication model for the Legacy Appeals API uses OAuth 2.0/OpenID Connect. The following authorization models are supported:\n* [Authorization code flow](/explore/api/legacy-appeals/authorization-code)\n* [Client Credentials Grant (CCG)](/explore/api/legacy-appeals/client-credentials)\n\n**Important:** To get production access using client credentials grant, you must either work for VA or have specific VA agreements in place. If you have questions, [contact us](/support/contact-us).\n" + "description": "The Legacy Appeals API returns a list of a claimant's active legacy appeals, which are not part of the Appeals Modernization Act (AMA) process. This list can be used to determine whether to opt in to the new decision review process. [Learn more about managing a legacy appeal](https://www.va.gov/decision-reviews/legacy-appeals/).\n\nTo check the status of all decision reviews and appeals for a specified individual, use the [Appeals Status API](/explore/api/appeals-status/docs).\n\nTo file an appeal or decision review, use one of these APIs:\n* [Higher-Level Reviews API](/explore/api/higher-level-reviews/docs)\n* [Notice of Disagreements API](/explore/api/notice-of-disagreements/docs)\n* [Supplemental Claims API](/explore/api/supplemental-claims/docs)\n\n## Technical overview\nThe Legacy Appeals API pulls data from Caseflow, a case management system. It provides decision review and appeal data that can be used for submitting a Higher Level Review, Notice of Disagreement, or Supplemental Claim.\n\n### Authorization and Access\nThe authentication model for the Legacy Appeals API uses OAuth 2.0/OpenID Connect. The following authorization models are supported:\n* [Authorization code flow](/explore/api/legacy-appeals/authorization-code)\n* [Client Credentials Grant (CCG)](/explore/api/legacy-appeals/client-credentials)\n\n**Important:** To get production access using client credentials grant, you must either work for VA or have specific VA agreements in place. If you have questions, [contact us](/support/contact-us).\n\n### Test data\n\nOur sandbox environment is populated with [Veteran test data](https://github.com/department-of-veterans-affairs/vets-api-clients/blob/master/test_accounts/legacy_appeals_test_accounts.md) that can be used to test various response scenarios. This sandbox data contains no PII or PHI, but mimics real Veteran account information.\n" }, "tags": [ { diff --git a/modules/appeals_api/config/mailinglists/report_daily.yml b/modules/appeals_api/config/mailinglists/report_daily.yml index f1be8464b4a..a406f7074c4 100644 --- a/modules/appeals_api/config/mailinglists/report_daily.yml +++ b/modules/appeals_api/config/mailinglists/report_daily.yml @@ -21,5 +21,6 @@ production: - robin.garrison@adhocteam.us - rocio.de-santiago@coforma.io - sade@coforma.io + - shaun.burdick@coforma.io - steve.albers@va.gov - zachary.goldfine@va.gov diff --git a/modules/appeals_api/config/routes.rb b/modules/appeals_api/config/routes.rb index ede3daf48cf..8f6178d281e 100644 --- a/modules/appeals_api/config/routes.rb +++ b/modules/appeals_api/config/routes.rb @@ -3,16 +3,13 @@ AppealsApi::Engine.routes.draw do get '/appeals_status/metadata', to: 'metadata#appeals_status' get '/decision_reviews/metadata', to: 'metadata#decision_reviews' - get '/v0/healthcheck', to: 'metadata#healthcheck' - get '/v1/healthcheck', to: 'metadata#healthcheck' - get '/v2/healthcheck', to: 'metadata#healthcheck' - get '/v1/appeals_healthcheck', to: 'metadata#healthcheck' - get '/v1/appeals_upstream_healthcheck', to: 'metadata#appeals_status_upstream_healthcheck' - get '/v0/upstream_healthcheck', to: 'metadata#appeals_status_upstream_healthcheck' - get '/v1/upstream_healthcheck', to: 'metadata#decision_reviews_upstream_healthcheck' - get '/v2/upstream_healthcheck', to: 'metadata#decision_reviews_upstream_healthcheck' - get '/v0/appeals', to: 'v0/appeals#index' - get '/v1/appeals', to: 'v1/appeals#index' + get '/v0/healthcheck', to: 'metadata#healthcheck' # Appeals Status v0 + get '/v1/healthcheck', to: 'metadata#healthcheck_s3' # Decision Reviews v1 + get '/v2/healthcheck', to: 'metadata#healthcheck_s3' # Decision Reviews v2 + get '/v0/upstream_healthcheck', to: 'metadata#appeals_status_upstream_healthcheck' # Appeals Status v0 + get '/v1/upstream_healthcheck', to: 'metadata#decision_reviews_upstream_healthcheck' # Decision Reviews v1 + get '/v2/upstream_healthcheck', to: 'metadata#decision_reviews_upstream_healthcheck' # Decision Reviews v2 + get '/v0/appeals', to: 'v0/appeals#index' # Appeals Status v0 namespace :v1, defaults: { format: 'json' } do namespace :decision_reviews do @@ -76,20 +73,10 @@ namespace :docs do namespace :v0, defaults: { format: 'json' } do resources :api, only: [:index] - - # Routes below are deprecated - they can be removed once they are no longer used: - docs_controller = '/appeals_api/docs/v2/docs' - get 'hlr', to: "#{docs_controller}#hlr" - get 'nod', to: "#{docs_controller}#nod" - get 'sc', to: "#{docs_controller}#sc" - get 'ci', to: "#{docs_controller}#ci" - get 'la', to: "#{docs_controller}#la" - # ...end of deprecated routes end namespace :v1, defaults: { format: 'json' } do get 'decision_reviews', to: 'docs#decision_reviews' - get 'appeals', to: 'docs#appeals_status' end namespace :v2, defaults: { format: 'json' } do @@ -112,7 +99,7 @@ namespace :v0 do controller_path = '/appeals_api/notice_of_disagreements/v0/notice_of_disagreements' - get :healthcheck, to: '/appeals_api/metadata#healthcheck' + get :healthcheck, to: '/appeals_api/metadata#healthcheck_s3' get :upstream_healthcheck, to: '/appeals_api/metadata#mail_status_upstream_healthcheck', path: 'upstream-healthcheck' @@ -172,7 +159,7 @@ namespace :v0 do controller_path = '/appeals_api/supplemental_claims/v0/supplemental_claims' - get :healthcheck, to: '/appeals_api/metadata#healthcheck' + get :healthcheck, to: '/appeals_api/metadata#healthcheck_s3' get :upstream_healthcheck, to: '/appeals_api/metadata#mail_status_upstream_healthcheck', path: 'upstream-healthcheck' diff --git a/modules/appeals_api/spec/requests/metadata_request_spec.rb b/modules/appeals_api/spec/requests/metadata_request_spec.rb index 28647d8b88f..b01cd39e5e1 100644 --- a/modules/appeals_api/spec/requests/metadata_request_spec.rb +++ b/modules/appeals_api/spec/requests/metadata_request_spec.rb @@ -6,12 +6,46 @@ describe 'metadata request api', type: :request do RSpec.shared_examples 'a healthcheck' do |path| it 'returns a successful healthcheck' do + # stub successful s3 up call + s3_client = instance_double(Aws::S3::Client) + allow(s3_client).to receive(:head_bucket).with(anything).and_return(true) + s3_resource = instance_double(Aws::S3::Resource) + allow(s3_resource).to receive(:client).and_return(s3_client) + allow(Aws::S3::Resource).to receive(:new).with(anything).and_return(s3_resource) + get path parsed_response = JSON.parse(response.body) expect(response).to have_http_status(:ok) expect(parsed_response['description']).to eq('Appeals API health check') - expect(parsed_response['status']).to eq('UP') + expect(parsed_response['status']).to eq('pass') + expect(parsed_response['time']).not_to be_nil + end + end + + RSpec.shared_examples 'a failed healthcheck' do |path| + it 'returns a failed healthcheck due to s3' do + # Slack notification expected + messenger_instance = instance_double(AppealsApi::Slack::Messager) + expected_notify = { class: 'AppealsApi::MetadataController', + warning: ':warning: ' \ + 'Appeals API healthcheck failed: unable to connect to AWS S3 bucket.' } + expect(AppealsApi::Slack::Messager).to receive(:new).with(expected_notify).and_return(messenger_instance) + expect(messenger_instance).to receive(:notify!).once + + # stub failed s3 up call + s3_client = instance_double(Aws::S3::Client) + expect(s3_client).to receive(:head_bucket).with(anything).and_raise(StandardError) + s3_resource = instance_double(Aws::S3::Resource) + allow(s3_resource).to receive(:client).and_return(s3_client) + allow(Aws::S3::Resource).to receive(:new).with(anything).and_return(s3_resource) + + get path + + parsed_response = JSON.parse(response.body) + expect(response).to have_http_status(:service_unavailable) + expect(parsed_response['description']).to eq('Appeals API health check') + expect(parsed_response['status']).to eq('fail') expect(parsed_response['time']).not_to be_nil end end @@ -142,7 +176,6 @@ context 'v1' do it_behaves_like 'a healthcheck', '/services/appeals/v1/healthcheck' - it_behaves_like 'a healthcheck', '/services/appeals/v1/appeals_healthcheck' end context 'segmented APIs' do @@ -154,6 +187,15 @@ end end + describe '#failed_healthcheck' do + context 'v1' do + it_behaves_like 'a failed healthcheck', '/services/appeals/v1/healthcheck' + it_behaves_like 'a failed healthcheck', '/services/appeals/v2/healthcheck' + it_behaves_like 'a failed healthcheck', '/services/appeals/notice-of-disagreements/v0/healthcheck' + it_behaves_like 'a failed healthcheck', '/services/appeals/supplemental-claims/v0/healthcheck' + end + end + describe '#upstream_healthcheck' do before do time = Time.utc(2020, 9, 21, 0, 0, 0) @@ -167,8 +209,6 @@ end context 'v1' do - it_behaves_like 'an upstream healthcheck (caseflow)', '/services/appeals/v1/appeals_upstream_healthcheck' - it 'checks the status of both services individually' do VCR.use_cassette('caseflow/health-check') do allow(CentralMail::Service).to receive(:current_breaker_outage?).and_return(true) diff --git a/modules/appeals_api/spec/requests/v1/appeals_controller_spec.rb b/modules/appeals_api/spec/requests/v1/appeals_controller_spec.rb index d08a6bf353f..78a28226a2b 100644 --- a/modules/appeals_api/spec/requests/v1/appeals_controller_spec.rb +++ b/modules/appeals_api/spec/requests/v1/appeals_controller_spec.rb @@ -7,7 +7,7 @@ include SchemaMatchers describe '#index' do - let(:path) { '/services/appeals/v1/appeals' } + let(:path) { '/services/appeals/appeals-status/v1/appeals' } let(:caseflow_cassette_name) { 'caseflow/appeals' } let(:mpi_cassette_name) { 'mpi/find_candidate/valid' } let(:va_user) { 'test.user@example.com' } @@ -20,7 +20,7 @@ 'GET endpoint with optional Veteran ICN parameter', { cassette: 'caseflow/appeals', - path: '/services/appeals/v1/appeals', + path: '/services/appeals/appeals-status/v1/appeals', scope_base: 'AppealsStatus', headers: { 'X-VA-User' => 'test.user@example.com' } } diff --git a/modules/appeals_api/spec/sidekiq/appeal_received_job_spec.rb b/modules/appeals_api/spec/sidekiq/appeal_received_job_spec.rb index 3ff2278b110..d9443f499f4 100644 --- a/modules/appeals_api/spec/sidekiq/appeal_received_job_spec.rb +++ b/modules/appeals_api/spec/sidekiq/appeal_received_job_spec.rb @@ -4,295 +4,225 @@ describe AppealsApi::AppealReceivedJob, type: :job do let(:job) { described_class.new } - let(:client) { instance_double(VaNotify::Service) } - - before do - allow(VaNotify::Service).to receive(:new).and_return(client) - allow(client).to receive(:send_email) + let(:appeal) { create(:higher_level_review_v2) } + let(:hlr_template_name) { 'higher_level_review_received' } + let(:nod_template_name) { 'notice_of_disagreement_received' } + let(:sc_template_name) { 'supplemental_claim_received' } + let(:hlr_template_id) { SecureRandom.uuid } + let(:nod_template_id) { SecureRandom.uuid } + let(:sc_template_id) { SecureRandom.uuid } + let(:claimant_hlr_template_id) { SecureRandom.uuid } + let(:claimant_nod_template_id) { SecureRandom.uuid } + let(:claimant_sc_template_id) { SecureRandom.uuid } + let(:settings_args) do + [Settings.vanotify.services.lighthouse.template_id, { + hlr_template_name.to_sym => hlr_template_id, + nod_template_name.to_sym => nod_template_id, + sc_template_name.to_sym => sc_template_id, + :"#{hlr_template_name}_claimant" => claimant_hlr_template_id, + :"#{nod_template_name}_claimant" => claimant_nod_template_id, + :"#{sc_template_name}_claimant" => claimant_sc_template_id + }] end - describe 'va notify vet email templates' do - let(:opts) do - { - 'receipt_event' => '', - 'email_identifier' => { 'id_value' => 'fake_email@email.com', 'id_type' => 'email' }, - 'first_name' => 'first name', - 'date_submitted' => DateTime.new(2021, 1, 2, 3, 4, 5).iso8601, - 'guid' => '1234556' - } - end - - it 'uses hlr email template' do - with_settings(Settings.vanotify.services.lighthouse.template_id, - higher_level_review_received: 'hlr_veteran_template') do - opts.merge!('receipt_event' => 'hlr_received') - expect { job.perform(opts) } - .to trigger_statsd_increment(AppealsApi::AppealReceivedJob::STATSD_CLAIMANT_EMAIL_SENT, - tags: ['appeal_type:hlr', 'claimant_type:veteran'], times: 1) - expect(client).to have_received(:send_email).with(hash_including(template_id: 'hlr_veteran_template')) + describe 'perform' do + let(:vanotify_client) { instance_double(VaNotify::Service) } + let(:appeal_id) { appeal.id } + let(:appeal_class_str) { appeal.class.name } + let(:date_submitted_str) { DateTime.new(2024, 1, 2, 3, 4, 5).iso8601 } + + describe 'successes' do + let(:expected_tags) { { appeal_type: 'hlr', claimant_type: 'veteran' } } + let(:expected_date_submitted) { 'January 02, 2024' } + + before do + allow(FeatureFlipper).to receive(:send_email?).and_return(true) + allow(VaNotify::Service).to receive(:new).and_return(vanotify_client) + allow(vanotify_client).to receive(:send_email) + allow(StatsD).to receive(:increment).with(described_class::STATSD_CLAIMANT_EMAIL_SENT, tags: expected_tags) + with_settings(*settings_args) { job.perform(appeal_id, appeal_class_str, date_submitted_str) } end - end - it 'uses nod email template' do - with_settings(Settings.vanotify.services.lighthouse.template_id, - notice_of_disagreement_received: 'nod_veteran_template') do - opts.merge!('receipt_event' => 'nod_received') - expect { job.perform(opts) } - .to trigger_statsd_increment('api.appeals.received.claimant.email.sent', - tags: ['appeal_type:nod', 'claimant_type:veteran'], times: 1) - expect(client).to have_received(:send_email).with(hash_including(template_id: 'nod_veteran_template')) + context 'with HLR' do + it 'sends HLR email to veteran' do + expect(vanotify_client).to have_received(:send_email).with( + { + email_address: appeal.veteran.email, + personalisation: { + date_submitted: expected_date_submitted, + first_name: appeal.veteran.first_name + }, + template_id: hlr_template_id + } + ) + end + + context 'with non-veteran claimant' do + let(:appeal) { create(:extra_higher_level_review_v2) } + let(:expected_tags) { { appeal_type: 'hlr', claimant_type: 'non-veteran' } } + + it 'sends HLR email to non-veteran claimant' do + expect(vanotify_client).to have_received(:send_email).with( + { + email_address: appeal.claimant.email, + personalisation: { + date_submitted: expected_date_submitted, + first_name: appeal.claimant.first_name, + veterans_name: appeal.veteran.first_name + }, + template_id: claimant_hlr_template_id + } + ) + end + end + + context 'if for some hypothetical reason there is no email' do + let(:form_data) do + data = fixture_as_json('decision_reviews/v2/valid_200996.json') + data['data']['attributes']['veteran'].delete('email') + data + end + let(:appeal) { create(:higher_level_review_v2, form_data:) } + + it 'invokes VANotify with ICN instead' do + expect(vanotify_client).to have_received(:send_email).with( + { + recipient_identifier: { id_type: 'ICN', id_value: appeal.veteran_icn }, + personalisation: { + date_submitted: expected_date_submitted, + first_name: appeal.veteran.first_name + }, + template_id: hlr_template_id + } + ) + end + end end - end - it 'uses sc email template' do - with_settings(Settings.vanotify.services.lighthouse.template_id, - supplemental_claim_received: 'sc_veteran_template') do - opts.merge!('receipt_event' => 'sc_received') - expect { job.perform(opts) } - .to trigger_statsd_increment('api.appeals.received.claimant.email.sent', - tags: ['appeal_type:sc', 'claimant_type:veteran'], times: 1) - expect(client).to have_received(:send_email).with(hash_including(template_id: 'sc_veteran_template')) + context 'with NOD' do + let(:appeal) { create(:notice_of_disagreement_v2) } + let(:expected_tags) { { appeal_type: 'nod', claimant_type: 'veteran' } } + + it 'sends NOD email to veteran' do + expect(vanotify_client).to have_received(:send_email).with( + { + email_address: appeal.veteran.email, + personalisation: { + date_submitted: expected_date_submitted, + first_name: appeal.veteran.first_name + }, + template_id: nod_template_id + } + ) + end + + context 'with non-veteran claimant' do + let(:appeal) { create(:extra_notice_of_disagreement_v2) } + let(:expected_tags) { { appeal_type: 'nod', claimant_type: 'non-veteran' } } + + it 'sends NOD email to non-veteran claimant' do + expect(vanotify_client).to have_received(:send_email).with( + { + email_address: appeal.claimant.email, + personalisation: { + date_submitted: expected_date_submitted, + first_name: appeal.claimant.first_name, + veterans_name: appeal.veteran.first_name + }, + template_id: claimant_nod_template_id + } + ) + end + end end - end - end - - describe 'va notify claimant email templates' do - let(:opts) do - { - 'receipt_event' => '', - 'email_identifier' => { 'id_value' => 'fake_email@email.com', 'id_type' => 'email' }, - 'first_name' => 'first name', - 'date_submitted' => DateTime.new(2021, 1, 2, 3, 4, 5).iso8601, - 'guid' => '1234556', - 'claimant_email' => 'fc@email.com', - 'claimant_first_name' => 'AshJoeSue' - } - end - it 'uses hlr email template' do - with_settings(Settings.vanotify.services.lighthouse.template_id, - higher_level_review_received_claimant: 'hlr_claimant_template') do - opts.merge!('receipt_event' => 'hlr_received') - expect { job.perform(opts) } - .to trigger_statsd_increment(AppealsApi::AppealReceivedJob::STATSD_CLAIMANT_EMAIL_SENT, - tags: ['appeal_type:hlr', 'claimant_type:non-veteran'], times: 1) - expect(client).to have_received(:send_email).with(hash_including(template_id: 'hlr_claimant_template')) + context 'with SC' do + let(:appeal) { create(:supplemental_claim) } + let(:expected_tags) { { appeal_type: 'sc', claimant_type: 'veteran' } } + + it 'sends SC email to veteran' do + expect(vanotify_client).to have_received(:send_email).with( + { + email_address: appeal.veteran.email, + personalisation: { + date_submitted: expected_date_submitted, + first_name: appeal.veteran.first_name + }, + template_id: sc_template_id + } + ) + end + + context 'with non-veteran claimant' do + let(:appeal) { create(:extra_supplemental_claim) } + let(:expected_tags) { { appeal_type: 'sc', claimant_type: 'non-veteran' } } + + it 'sends SC email to non-veteran claimant' do + expect(vanotify_client).to have_received(:send_email).with( + { + email_address: appeal.claimant.email, + personalisation: { + date_submitted: expected_date_submitted, + first_name: appeal.claimant.first_name, + veterans_name: appeal.veteran.first_name + }, + template_id: claimant_sc_template_id + } + ) + end + end end end - it 'uses nod email template' do - with_settings(Settings.vanotify.services.lighthouse.template_id, - notice_of_disagreement_received_claimant: 'nod_claimant_template') do - opts.merge!('receipt_event' => 'nod_received') - expect { job.perform(opts) } - .to trigger_statsd_increment(AppealsApi::AppealReceivedJob::STATSD_CLAIMANT_EMAIL_SENT, - tags: ['appeal_type:nod', 'claimant_type:non-veteran'], times: 1) - expect(client).to have_received(:send_email).with(hash_including(template_id: 'nod_claimant_template')) - end - end + describe 'errors' do + let(:expected_log) { nil } - it 'uses sc email template' do - with_settings(Settings.vanotify.services.lighthouse.template_id, - supplemental_claim_received_claimant: 'sc_claimant_template') do - opts.merge!('receipt_event' => 'sc_received') - expect { job.perform(opts) } - .to trigger_statsd_increment(AppealsApi::AppealReceivedJob::STATSD_CLAIMANT_EMAIL_SENT, - tags: ['appeal_type:sc', 'claimant_type:non-veteran'], times: 1) - expect(client).to have_received(:send_email).with(hash_including(template_id: 'sc_claimant_template')) + before do + allow(FeatureFlipper).to receive(:send_email?).and_return(true) + allow(VaNotify::Service).to receive(:new).and_return(vanotify_client) + expect(Rails.logger).to receive(:error).once.with(expected_log) + with_settings(*settings_args) { job.perform(appeal_id, appeal_class_str, date_submitted_str) } end - end - end - - describe 'higher_level_review' do - it 'errors if the keys needed are missing' do - opts = { - 'receipt_event' => 'hlr_received' - } - expect(Rails.logger).to receive(:error).with 'AppealReceived: Missing required keys' - expect(client).not_to have_received(:send_email) - - job.perform(opts) - end - - it 'logs error if email identifier cannot be used' do - opts = { - 'receipt_event' => 'hlr_received', - 'email_identifier' => { 'id_value' => 'fake_email@email.com' }, # missing id_type - 'first_name' => 'first name', - 'date_submitted' => DateTime.new(2021, 11, 11, 1, 2, 3).iso8601, - 'guid' => '1234556' - } - - expect(Rails.logger).to receive(:error) - expect(client).not_to have_received(:send_email) - - job.perform(opts) - end - - it 'errors if the template id cannot be found' do - error_prefix = 'AppealReceived: could not find template id for' - - opts = { - 'receipt_event' => 'hlr_received', - 'email_identifier' => { 'id_value' => 'fake_email@email.com', 'id_type' => 'email' }, - 'first_name' => 'first name', - 'date_submitted' => DateTime.new(2021, 1, 2, 3, 4, 5).iso8601, - 'guid' => '1234556', - 'claimant_email' => '', - 'claimant_first_name' => '' - } - expect(Rails.logger).to receive(:error).with "#{error_prefix} higher_level_review_received" - expect(client).not_to have_received(:send_email) - - job.perform(opts) - - opts['claimant_email'] = 'fake_claimant_email@email.com' - opts['claimant_first_name'] = 'Betty' - - expect(Rails.logger).to receive(:error).with "#{error_prefix} higher_level_review_received_claimant" - expect(client).not_to have_received(:send_email) - - job.perform(opts) - end - - it 'errors if claimant info is missing email' do - opts = { - 'receipt_event' => 'hlr_received', - 'email_identifier' => { 'id_type' => 'email', 'id_value' => 'fake_email@email.com' }, # key order changed - 'first_name' => 'first name', - 'date_submitted' => DateTime.new(2021, 1, 2, 3, 4, 5).iso8601, - 'guid' => '1234556', - 'claimant_email' => ' ', # Blank email - 'claimant_first_name' => 'Betty' - } - - guid = opts['guid'] - error_message = "No lookup value present for AppealsApi::AppealReceived notification HLR - GUID: #{guid}" - - expect(Rails.logger).to receive(:error).with error_message - expect(client).not_to have_received(:send_email) - - job.perform(opts) - end - - it 'sends an email' do - with_settings(Settings.vanotify.services.lighthouse.template_id, - higher_level_review_received: 'veteran_template', - higher_level_review_received_claimant: 'claimant_template') do - opts = { - 'receipt_event' => 'hlr_received', - 'email_identifier' => { 'id_value' => 'fake_email@email.com', 'id_type' => 'email' }, - 'first_name' => 'first name', - 'date_submitted' => DateTime.new(2021, 1, 2, 3, 4, 5).iso8601, - 'guid' => '1234556', - 'claimant_email' => '', - 'claimant_first_name' => '' - } - - job.perform(opts) - - expect(client).to have_received(:send_email).with( - { - email_address: 'fake_email@email.com', - template_id: 'veteran_template', - personalisation: { - 'first_name' => 'first name', - 'date_submitted' => 'January 02, 2021' - } - } - ) + context 'appeal PII not available' do + let(:expected_log) { /#{appeal.class.name}.*#{appeal_id}/ } + let(:appeal) do + hlr = create(:higher_level_review_v2) + hlr.update!(form_data: nil, auth_headers: nil) + hlr + end + + it 'does not send email' do + expect(vanotify_client).not_to receive(:send_email) + end end - end - it 'does not care about the order of email identifier hash' do - with_settings(Settings.vanotify.services.lighthouse.template_id, - higher_level_review_received: 'veteran_template', - higher_level_review_received_claimant: 'claimant_template') do - opts = { - 'receipt_event' => 'hlr_received', - 'email_identifier' => { 'id_type' => 'email', 'id_value' => 'fake_email@email.com' }, # key order changed - 'first_name' => 'first name', - 'date_submitted' => DateTime.new(2021, 1, 2, 3, 4, 5).iso8601, - 'guid' => '1234556' - } + context 'appeal with given appeal_id not found' do + let(:expected_log) { /#{appeal_id}/ } + let(:appeal_id) { SecureRandom.uuid } - job.perform(opts) - - expect(client).to have_received(:send_email).with( - { - email_address: 'fake_email@email.com', - template_id: 'veteran_template', - personalisation: { - 'first_name' => 'first name', - 'date_submitted' => 'January 02, 2021' - } - } - ) + it 'does not send email' do + expect(vanotify_client).not_to receive(:send_email) + end end - end - it 'sends email to claimant using the claimant template' do - with_settings( - Settings.vanotify.services.lighthouse.template_id, - higher_level_review_received: 'veteran_template', - higher_level_review_received_claimant: 'claimant_template' - ) do - opts = { - 'receipt_event' => 'hlr_received', - 'email_identifier' => { 'id_type' => 'email', 'id_value' => 'fake_email@email.com' }, # key order changed - 'first_name' => 'veteran first name', - 'date_submitted' => DateTime.new(2021, 1, 2, 3, 4, 5).iso8601, - 'guid' => '1234556', - 'claimant_email' => 'fake_claimant_email@email.com', - 'claimant_first_name' => 'Betty' - } + context 'submitted_date_str with incorrect format' do + let(:expected_log) { /iso8601 format/ } + let(:date_submitted_str) { 'not-a-date' } - job.perform(opts) - - expect(client).to have_received(:send_email).with( - { - email_address: 'fake_claimant_email@email.com', - template_id: 'claimant_template', - personalisation: { - 'first_name' => 'Betty', - 'date_submitted' => 'January 02, 2021', - 'veterans_name' => 'veteran first name' - } - } - ) + it 'does not send email' do + expect(vanotify_client).not_to receive(:send_email) + end end - end - end - - it 'uses icn if email isn\'t present' do - with_settings( - Settings.vanotify.services.lighthouse.template_id, - higher_level_review_received: 'fake_template_id' - ) do - opts = { - 'receipt_event' => 'hlr_received', - 'email_identifier' => { 'id_value' => '1233445353', 'id_type' => 'ICN' }, - 'first_name' => 'first name', - 'date_submitted' => DateTime.new(1900, 1, 2, 3, 4, 5).iso8601, - 'guid' => '1234556' - } - job.perform(opts) + context 'missing settings for VANotify templates' do + let(:expected_log) { /template.*#{hlr_template_name}/ } + let(:settings_args) { [Settings.vanotify.services.lighthouse.template_id, {}] } - expect(client).to have_received(:send_email).with( - { - recipient_identifier: { - id_value: '1233445353', - id_type: 'ICN' - }, - template_id: 'fake_template_id', - personalisation: { - 'first_name' => 'first name', - 'date_submitted' => 'January 02, 1900' - } - } - ) + it 'does not send email' do + expect(vanotify_client).not_to receive(:send_email) + end + end end end end diff --git a/modules/appeals_api/spec/sidekiq/appeal_submitted_job_spec.rb b/modules/appeals_api/spec/sidekiq/appeal_submitted_job_spec.rb deleted file mode 100644 index 8d53dea4d7c..00000000000 --- a/modules/appeals_api/spec/sidekiq/appeal_submitted_job_spec.rb +++ /dev/null @@ -1,228 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe AppealsApi::AppealSubmittedJob, type: :job do - let(:job) { described_class.new } - let(:appeal) { create(:higher_level_review_v2) } - let(:hlr_template_name) { 'higher_level_review_received' } - let(:nod_template_name) { 'notice_of_disagreement_received' } - let(:sc_template_name) { 'supplemental_claim_received' } - let(:hlr_template_id) { SecureRandom.uuid } - let(:nod_template_id) { SecureRandom.uuid } - let(:sc_template_id) { SecureRandom.uuid } - let(:claimant_hlr_template_id) { SecureRandom.uuid } - let(:claimant_nod_template_id) { SecureRandom.uuid } - let(:claimant_sc_template_id) { SecureRandom.uuid } - let(:settings_args) do - [Settings.vanotify.services.lighthouse.template_id, { - hlr_template_name.to_sym => hlr_template_id, - nod_template_name.to_sym => nod_template_id, - sc_template_name.to_sym => sc_template_id, - :"#{hlr_template_name}_claimant" => claimant_hlr_template_id, - :"#{nod_template_name}_claimant" => claimant_nod_template_id, - :"#{sc_template_name}_claimant" => claimant_sc_template_id - }] - end - - describe 'perform' do - let(:vanotify_client) { instance_double(VaNotify::Service) } - let(:appeal_id) { appeal.id } - let(:appeal_class_str) { appeal.class.name } - let(:date_submitted_str) { DateTime.new(2024, 1, 2, 3, 4, 5).iso8601 } - - describe 'successes' do - let(:expected_tags) { { appeal_type: 'hlr', claimant_type: 'veteran' } } - let(:expected_date_submitted) { 'January 02, 2024' } - - before do - allow(FeatureFlipper).to receive(:send_email?).and_return(true) - allow(VaNotify::Service).to receive(:new).and_return(vanotify_client) - allow(vanotify_client).to receive(:send_email) - allow(StatsD).to receive(:increment).with(described_class::STATSD_CLAIMANT_EMAIL_SENT, tags: expected_tags) - with_settings(*settings_args) { job.perform(appeal_id, appeal_class_str, date_submitted_str) } - end - - context 'with HLR' do - it 'sends HLR email to veteran' do - expect(vanotify_client).to have_received(:send_email).with( - { - email_address: appeal.veteran.email, - personalisation: { - date_submitted: expected_date_submitted, - first_name: appeal.veteran.first_name - }, - template_id: hlr_template_id - } - ) - end - - context 'with non-veteran claimant' do - let(:appeal) { create(:extra_higher_level_review_v2) } - let(:expected_tags) { { appeal_type: 'hlr', claimant_type: 'non-veteran' } } - - it 'sends HLR email to non-veteran claimant' do - expect(vanotify_client).to have_received(:send_email).with( - { - email_address: appeal.claimant.email, - personalisation: { - date_submitted: expected_date_submitted, - first_name: appeal.claimant.first_name, - veterans_name: appeal.veteran.first_name - }, - template_id: claimant_hlr_template_id - } - ) - end - end - - context 'if for some hypothetical reason there is no email' do - let(:form_data) do - data = fixture_as_json('decision_reviews/v2/valid_200996.json') - data['data']['attributes']['veteran'].delete('email') - data - end - let(:appeal) { create(:higher_level_review_v2, form_data:) } - - it 'invokes VANotify with ICN instead' do - expect(vanotify_client).to have_received(:send_email).with( - { - recipient_identifier: { id_type: 'ICN', id_value: appeal.veteran_icn }, - personalisation: { - date_submitted: expected_date_submitted, - first_name: appeal.veteran.first_name - }, - template_id: hlr_template_id - } - ) - end - end - end - - context 'with NOD' do - let(:appeal) { create(:notice_of_disagreement_v2) } - let(:expected_tags) { { appeal_type: 'nod', claimant_type: 'veteran' } } - - it 'sends NOD email to veteran' do - expect(vanotify_client).to have_received(:send_email).with( - { - email_address: appeal.veteran.email, - personalisation: { - date_submitted: expected_date_submitted, - first_name: appeal.veteran.first_name - }, - template_id: nod_template_id - } - ) - end - - context 'with non-veteran claimant' do - let(:appeal) { create(:extra_notice_of_disagreement_v2) } - let(:expected_tags) { { appeal_type: 'nod', claimant_type: 'non-veteran' } } - - it 'sends NOD email to non-veteran claimant' do - expect(vanotify_client).to have_received(:send_email).with( - { - email_address: appeal.claimant.email, - personalisation: { - date_submitted: expected_date_submitted, - first_name: appeal.claimant.first_name, - veterans_name: appeal.veteran.first_name - }, - template_id: claimant_nod_template_id - } - ) - end - end - end - - context 'with SC' do - let(:appeal) { create(:supplemental_claim) } - let(:expected_tags) { { appeal_type: 'sc', claimant_type: 'veteran' } } - - it 'sends SC email to veteran' do - expect(vanotify_client).to have_received(:send_email).with( - { - email_address: appeal.veteran.email, - personalisation: { - date_submitted: expected_date_submitted, - first_name: appeal.veteran.first_name - }, - template_id: sc_template_id - } - ) - end - - context 'with non-veteran claimant' do - let(:appeal) { create(:extra_supplemental_claim) } - let(:expected_tags) { { appeal_type: 'sc', claimant_type: 'non-veteran' } } - - it 'sends SC email to non-veteran claimant' do - expect(vanotify_client).to have_received(:send_email).with( - { - email_address: appeal.claimant.email, - personalisation: { - date_submitted: expected_date_submitted, - first_name: appeal.claimant.first_name, - veterans_name: appeal.veteran.first_name - }, - template_id: claimant_sc_template_id - } - ) - end - end - end - end - - describe 'errors' do - let(:expected_log) { nil } - - before do - allow(FeatureFlipper).to receive(:send_email?).and_return(true) - allow(VaNotify::Service).to receive(:new).and_return(vanotify_client) - expect(Rails.logger).to receive(:error).once.with(expected_log) - with_settings(*settings_args) { job.perform(appeal_id, appeal_class_str, date_submitted_str) } - end - - context 'appeal PII not available' do - let(:expected_log) { /#{appeal.class.name}.*#{appeal_id}/ } - let(:appeal) do - hlr = create(:higher_level_review_v2) - hlr.update!(form_data: nil, auth_headers: nil) - hlr - end - - it 'does not send email' do - expect(vanotify_client).not_to receive(:send_email) - end - end - - context 'appeal with given appeal_id not found' do - let(:expected_log) { /#{appeal_id}/ } - let(:appeal_id) { SecureRandom.uuid } - - it 'does not send email' do - expect(vanotify_client).not_to receive(:send_email) - end - end - - context 'submitted_date_str with incorrect format' do - let(:expected_log) { /iso8601 format/ } - let(:date_submitted_str) { 'not-a-date' } - - it 'does not send email' do - expect(vanotify_client).not_to receive(:send_email) - end - end - - context 'missing settings for VANotify templates' do - let(:expected_log) { /template.*#{hlr_template_name}/ } - let(:settings_args) { [Settings.vanotify.services.lighthouse.template_id, {}] } - - it 'does not send email' do - expect(vanotify_client).not_to receive(:send_email) - end - end - end - end -end diff --git a/modules/appeals_api/spec/support/shared_examples_for_appeal_status_updates.rb b/modules/appeals_api/spec/support/shared_examples_for_appeal_status_updates.rb index 2fff8eec427..5ee0c1be981 100644 --- a/modules/appeals_api/spec/support/shared_examples_for_appeal_status_updates.rb +++ b/modules/appeals_api/spec/support/shared_examples_for_appeal_status_updates.rb @@ -71,9 +71,9 @@ context "when status has updated to 'submitted' and claimant or veteran email data present" do it 'enqueues the appeal received job' do - expect(AppealsApi::AppealSubmittedJob.jobs.size).to eq 0 + expect(AppealsApi::AppealReceivedJob.jobs.size).to eq 0 example_instance.update_status(status: 'submitted') - expect(AppealsApi::AppealSubmittedJob.jobs.size).to eq 1 + expect(AppealsApi::AppealReceivedJob.jobs.size).to eq 1 end end @@ -81,25 +81,25 @@ before { example_instance.update(status: 'submitted') } it 'does not enqueue the appeal received job' do - expect(AppealsApi::AppealSubmittedJob.jobs.size).to eq 0 + expect(AppealsApi::AppealReceivedJob.jobs.size).to eq 0 example_instance.update_status(status: 'submitted') - expect(AppealsApi::AppealSubmittedJob.jobs.size).to eq 0 + expect(AppealsApi::AppealReceivedJob.jobs.size).to eq 0 end end context "when incoming status is not 'submitted' and claimant or veteran email data present" do it 'does not enqueue the appeal received job' do - expect(AppealsApi::AppealSubmittedJob.jobs.size).to eq 0 + expect(AppealsApi::AppealReceivedJob.jobs.size).to eq 0 example_instance.update_status(status: 'pending') - expect(AppealsApi::AppealSubmittedJob.jobs.size).to eq 0 + expect(AppealsApi::AppealReceivedJob.jobs.size).to eq 0 end end context 'when veteran appellant without email provided' do it 'gets the ICN and enqueues the appeal received job' do - expect(AppealsApi::AppealSubmittedJob.jobs.size).to eq 0 + expect(AppealsApi::AppealReceivedJob.jobs.size).to eq 0 instance_without_email.update_status(status: 'submitted') - expect(AppealsApi::AppealSubmittedJob.jobs.size).to eq 1 + expect(AppealsApi::AppealReceivedJob.jobs.size).to eq 1 end end @@ -111,9 +111,9 @@ end it 'does not enqueue the appeal received job' do - expect(AppealsApi::AppealSubmittedJob.jobs.size).to eq 0 + expect(AppealsApi::AppealReceivedJob.jobs.size).to eq 0 example_instance.update_status(status: 'submitted') - expect(AppealsApi::AppealSubmittedJob.jobs.size).to eq 0 + expect(AppealsApi::AppealReceivedJob.jobs.size).to eq 0 end end end diff --git a/modules/ask_va_api/app/controllers/ask_va_api/v0/inquiries_controller.rb b/modules/ask_va_api/app/controllers/ask_va_api/v0/inquiries_controller.rb index 590612f34f3..829268f6e51 100644 --- a/modules/ask_va_api/app/controllers/ask_va_api/v0/inquiries_controller.rb +++ b/modules/ask_va_api/app/controllers/ask_va_api/v0/inquiries_controller.rb @@ -46,6 +46,11 @@ def status render json: serializer.serializable_hash, status: :ok end + def create_reply + response = Correspondences::Creator.new(message: params[:reply], inquiry_id: params[:id], service: nil).call + render json: response.to_json, status: :ok + end + private def inquiry_params diff --git a/modules/ask_va_api/app/lib/ask_va_api/correspondences/creator.rb b/modules/ask_va_api/app/lib/ask_va_api/correspondences/creator.rb new file mode 100644 index 00000000000..09d91f58cda --- /dev/null +++ b/modules/ask_va_api/app/lib/ask_va_api/correspondences/creator.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module AskVAApi + module Correspondences + class CorrespondencesCreatorError < StandardError; end + + class Creator + attr_reader :message, :inquiry_id, :service + + def initialize(message:, inquiry_id:, service:) + @message = message + @inquiry_id = inquiry_id + @service = service || default_service + end + + def call + payload = { Reply: message } + post_data(payload:) + rescue => e + ErrorHandler.handle_service_error(e) + end + + private + + def default_service + Crm::Service.new(icn: nil) + end + + def post_data(payload: {}) + endpoint = "inquiries/#{inquiry_id}/reply/new" + + response = service.call(endpoint:, payload:) + handle_response_data(response) + end + + def handle_response_data(response) + if response[:Data].nil? + raise CorrespondencesCreatorError, response[:Message] + else + response[:Data] + end + end + end + end +end diff --git a/modules/ask_va_api/app/services/ask_va_api/redis_client.rb b/modules/ask_va_api/app/services/ask_va_api/redis_client.rb new file mode 100644 index 00000000000..5c25c9f5c40 --- /dev/null +++ b/modules/ask_va_api/app/services/ask_va_api/redis_client.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module AskVAApi + class RedisClient + def fetch(key) + Rails.cache.read( + key, + namespace: 'crm-api-cache' + ) + end + + def store_data(key:, data:, ttl:) + Rails.cache.write( + key, + data, + namespace: 'crm-api-cache', + expires_in: ttl + ) + end + end +end diff --git a/modules/ask_va_api/app/services/crm/cache_data.rb b/modules/ask_va_api/app/services/crm/cache_data.rb index a3d286b9f33..9b53c16c82e 100644 --- a/modules/ask_va_api/app/services/crm/cache_data.rb +++ b/modules/ask_va_api/app/services/crm/cache_data.rb @@ -4,7 +4,7 @@ module Crm class CacheData attr_reader :cache_client, :service - def initialize(service: Service.new(icn: nil), cache_client: RedisClient.new) + def initialize(service: Service.new(icn: nil), cache_client: AskVAApi::RedisClient.new) @cache_client = cache_client @service = service end diff --git a/modules/ask_va_api/app/services/crm/crm_token.rb b/modules/ask_va_api/app/services/crm/crm_token.rb index b945e181ed7..3b023fa2ced 100644 --- a/modules/ask_va_api/app/services/crm/crm_token.rb +++ b/modules/ask_va_api/app/services/crm/crm_token.rb @@ -16,7 +16,7 @@ class CrmToken def initialize @settings = Settings.ask_va_api.crm_api - @cache_client = RedisClient.new + @cache_client = AskVAApi::RedisClient.new @logger = LogService.new end diff --git a/modules/ask_va_api/app/services/crm/service.rb b/modules/ask_va_api/app/services/crm/service.rb index d8858d11fc7..aa69ec2eb56 100644 --- a/modules/ask_va_api/app/services/crm/service.rb +++ b/modules/ask_va_api/app/services/crm/service.rb @@ -7,7 +7,7 @@ class Service attr_reader :icn, :logger, :settings, :base_uri, :token BASE_URI = 'https://dev.integration.d365.va.gov' - VEIS_API_PATH = 'veis/vagov.lob.ava/api' + VEIS_API_PATH = 'eis/vagov.lob.ava/api' def_delegators :settings, :base_url, diff --git a/modules/ask_va_api/app/services/redis_client.rb b/modules/ask_va_api/app/services/redis_client.rb deleted file mode 100644 index ae809c2eed2..00000000000 --- a/modules/ask_va_api/app/services/redis_client.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -class RedisClient - def fetch(key) - Rails.cache.read( - key, - namespace: 'crm-api-cache' - ) - end - - def store_data(key:, data:, ttl:) - Rails.cache.write( - key, - data, - namespace: 'crm-api-cache', - expires_in: ttl - ) - end -end diff --git a/modules/ask_va_api/config/routes.rb b/modules/ask_va_api/config/routes.rb index 2f077168af6..0c43f79eb11 100644 --- a/modules/ask_va_api/config/routes.rb +++ b/modules/ask_va_api/config/routes.rb @@ -10,10 +10,11 @@ get '/inquiries/:id', to: 'inquiries#show' get '/inquiries/:id/status', to: 'inquiries#status' get '/download_attachment', to: 'inquiries#download_attachment' + get '/profile', to: 'inquiries#profile' post '/inquiries/auth', to: 'inquiries#create' post '/inquiries', to: 'inquiries#unauth_create' post '/upload_attachment', to: 'inquiries#upload_attachment' - get '/profile', to: 'inquiries#profile' + post '/inquiries/:id/reply/new', to: 'inquiries#create_reply' # static_data get '/categories', to: 'static_data#categories' diff --git a/modules/ask_va_api/spec/app/lib/ask_va_api/correspondences/creator_spec.rb b/modules/ask_va_api/spec/app/lib/ask_va_api/correspondences/creator_spec.rb new file mode 100644 index 00000000000..6136b5c1609 --- /dev/null +++ b/modules/ask_va_api/spec/app/lib/ask_va_api/correspondences/creator_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require 'rails_helper' + +module AskVAApi + module Correspondences + RSpec.describe Creator do + subject(:creator) { described_class.new(message:, inquiry_id: '123', service: nil) } + + let(:message) { 'this is a corespondence message' } + + describe '#call' do + context 'when successful' do + before do + allow_any_instance_of(Crm::CrmToken).to receive(:call).and_return('Token') + allow_any_instance_of(Crm::Service).to receive(:call).and_return({ Data: { Id: '456' } }) + end + + it 'response with a correspondence ID' do + expect(creator.call).to eq({ Id: '456' }) + end + end + + context 'when not successful' do + before do + allow_any_instance_of(Crm::CrmToken).to receive(:call).and_return('Token') + allow_any_instance_of(Crm::Service).to receive(:call).and_return({ Data: nil, Message: 'Error has occur' }) + end + + it 'raise CorrespondenceCreatorError' do + expect { creator.call }.to raise_error(ErrorHandler::ServiceError) + end + end + end + end + end +end diff --git a/modules/ask_va_api/spec/requests/v0/inquiries_spec.rb b/modules/ask_va_api/spec/requests/v0/inquiries_spec.rb index 481e29ff34f..3f64c8facf1 100644 --- a/modules/ask_va_api/spec/requests/v0/inquiries_spec.rb +++ b/modules/ask_va_api/spec/requests/v0/inquiries_spec.rb @@ -337,4 +337,18 @@ def json_response 'attributes' => { 'status' => 'Reopened' } }) end end + + describe 'POST #create_reply' do + let(:payload) { { 'reply' => 'this is my reply' } } + + before do + allow_any_instance_of(Crm::Service).to receive(:call).and_return({ Data: { Id: '123' } }) + sign_in(authorized_user) + post '/ask_va_api/v0/inquiries/123/reply/new', params: payload + end + + it 'returns status 200' do + expect(response).to have_http_status(:ok) + end + end end diff --git a/modules/ask_va_api/spec/requests/v0/static_data_auth_spec.rb b/modules/ask_va_api/spec/requests/v0/static_data_auth_spec.rb index de75501f417..299f0116930 100644 --- a/modules/ask_va_api/spec/requests/v0/static_data_auth_spec.rb +++ b/modules/ask_va_api/spec/requests/v0/static_data_auth_spec.rb @@ -36,7 +36,7 @@ end it 'response with status :unauthorized' do - expect(response).to have_http_status('403') # rubocop:disable RSpec/Rails/HttpStatus + expect(response).to have_http_status('403') # rubocop:disable RSpecRails/HttpStatus expect(response.body).to include('You do not have access to this resource.') end end diff --git a/modules/ask_va_api/spec/services/crm/cache_data_spec.rb b/modules/ask_va_api/spec/services/crm/cache_data_spec.rb index b1b247a2b5c..1e341f6469d 100644 --- a/modules/ask_va_api/spec/services/crm/cache_data_spec.rb +++ b/modules/ask_va_api/spec/services/crm/cache_data_spec.rb @@ -4,7 +4,7 @@ RSpec.describe Crm::CacheData do let(:service) { double('Crm::Service') } - let(:cache_client) { double('RedisClient') } + let(:cache_client) { double('AskVAApi::RedisClient') } let(:cache_data_instance) { Crm::CacheData.new(service:, cache_client:) } let(:cache_data) { { topics: [{ id: 1, name: 'Topic 1' }] } } diff --git a/modules/ask_va_api/spec/services/crm/service_spec.rb b/modules/ask_va_api/spec/services/crm/service_spec.rb index 6d7391c85c3..ba7f2e4dd8b 100644 --- a/modules/ask_va_api/spec/services/crm/service_spec.rb +++ b/modules/ask_va_api/spec/services/crm/service_spec.rb @@ -51,7 +51,7 @@ def mock_response(status:, body:) before do allow_any_instance_of(Crm::CrmToken).to receive(:call).and_return('token') - allow_any_instance_of(Faraday::Connection).to receive(:get).with('veis/vagov.lob.ava/api/inquiries', + allow_any_instance_of(Faraday::Connection).to receive(:get).with('eis/vagov.lob.ava/api/inquiries', icn: '123', organizationName: 'iris-dev') .and_return(response) @@ -99,7 +99,7 @@ def mock_response(status:, body:) before do allow_any_instance_of(Crm::CrmToken).to receive(:call).and_return('token') - allow_any_instance_of(Faraday::Connection).to receive(:get).with('veis/vagov.lob.ava/api/inquiries', + allow_any_instance_of(Faraday::Connection).to receive(:get).with('eis/vagov.lob.ava/api/inquiries', { icn: '123', organizationName: 'iris-dev' }) .and_raise(exception) diff --git a/modules/ask_va_api/spec/services/redis_client_spec.rb b/modules/ask_va_api/spec/services/redis_client_spec.rb index fd58fdc3795..b8d2b7e74c0 100644 --- a/modules/ask_va_api/spec/services/redis_client_spec.rb +++ b/modules/ask_va_api/spec/services/redis_client_spec.rb @@ -2,8 +2,8 @@ require 'rails_helper' -RSpec.describe RedisClient do - let(:redis_client) { RedisClient.new } +RSpec.describe AskVAApi::RedisClient do + let(:redis_client) { AskVAApi::RedisClient.new } let(:token) { 'some-access-token' } describe '#fetch' do diff --git a/modules/check_in/app/controllers/check_in/v0/travel_claims_controller.rb b/modules/check_in/app/controllers/check_in/v0/travel_claims_controller.rb index 18e4a57b119..3fea4fbdacc 100644 --- a/modules/check_in/app/controllers/check_in/v0/travel_claims_controller.rb +++ b/modules/check_in/app/controllers/check_in/v0/travel_claims_controller.rb @@ -3,6 +3,9 @@ module CheckIn module V0 class TravelClaimsController < CheckIn::ApplicationController + before_action :before_logger, only: %i[show create] + after_action :after_logger, only: %i[show create] + def create check_in_session = CheckIn::V2::Session.build(data: { uuid: permitted_params[:uuid] }, jwt: low_auth_token) @@ -12,11 +15,13 @@ def create TravelClaimSubmissionWorker.perform_async(permitted_params[:uuid], permitted_params[:appointment_date]) + logger.info({ message: 'Submitted travel claim to background worker' }.merge(permitted_params)) + render nothing: true, status: :accepted end def permitted_params - params.require(:travel_claims).permit(:uuid, :appointment_date, :facility_type) + params.require(:travel_claims).permit(:uuid, :appointment_date, :facility_type, :time_to_complete) end def authorize 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..a665cd50f30 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? @@ -16,13 +17,11 @@ def show check_in_data = ::V2::Lorota::Service.build(check_in: check_in_session).check_in_data - if Flipper.enabled?('check_in_experience_45_minute_reminder') - if call_set_echeckin_started?(check_in_data) - ::V2::Chip::Service.build(check_in: check_in_session).set_echeckin_started - params[:set_e_checkin_started_called] = false - else - params[:set_e_checkin_started_called] = true - end + if call_set_echeckin_started?(check_in_data) + ::V2::Chip::Service.build(check_in: check_in_session).set_echeckin_started + params[:set_e_checkin_started_called] = false + else + params[:set_e_checkin_started_called] = true end render json: check_in_data 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/serializers/check_in/v2/appointment_data_serializer.rb b/modules/check_in/app/serializers/check_in/v2/appointment_data_serializer.rb index 582b22388f4..1beede172d5 100644 --- a/modules/check_in/app/serializers/check_in/v2/appointment_data_serializer.rb +++ b/modules/check_in/app/serializers/check_in/v2/appointment_data_serializer.rb @@ -11,7 +11,7 @@ class AppointmentDataSerializer attribute :payload do |object| appointments = object.payload[:appointments].map do |appt| - appt.except!(:patientDFN) + appt.except!(:patientDFN, :icn, :edipi) end demographics = prepare_demographics(object.payload[:demographics]) @@ -70,6 +70,8 @@ def self.prepare_contact(raw_contact) end def self.address_helper(address) + return {} if address.nil? + { street1: address[:street1], street2: address[:street2], diff --git a/modules/check_in/app/serializers/check_in/v2/appointment_identifiers_serializer.rb b/modules/check_in/app/serializers/check_in/v2/appointment_identifiers_serializer.rb index 2ae27a343dd..d440ac80bb5 100644 --- a/modules/check_in/app/serializers/check_in/v2/appointment_identifiers_serializer.rb +++ b/modules/check_in/app/serializers/check_in/v2/appointment_identifiers_serializer.rb @@ -21,7 +21,7 @@ class AppointmentIdentifiersSerializer end attribute :icn do |object| - object.payload.dig(:demographics, :icn) + object.payload.dig(:demographics, :icn) || object.payload[:appointments].first[:icn] end attribute :mobilePhone do |object| diff --git a/modules/check_in/app/services/travel_claim/client.rb b/modules/check_in/app/services/travel_claim/client.rb index 67dba0ab81b..5729f155529 100644 --- a/modules/check_in/app/services/travel_claim/client.rb +++ b/modules/check_in/app/services/travel_claim/client.rb @@ -58,7 +58,7 @@ def token # def submit_claim(token:, patient_icn:, appointment_date:) connection(server_url: claims_url).post("/#{claims_base_path}/api/ClaimIngest/submitclaim") do |req| - req.options.timeout = 120 if Flipper.enabled?(:check_in_experience_travel_claim_increase_timeout) + req.options.timeout = 120 req.headers = claims_default_header.merge('Authorization' => "Bearer #{token}") req.body = claims_data.merge({ ClaimantID: patient_icn, Appointment: { AppointmentDateTime: appointment_date } }).to_json 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/lib/check_in/utils/logger.rb b/modules/check_in/lib/check_in/utils/logger.rb index 6cd8648f308..bf3bf5723e5 100644 --- a/modules/check_in/lib/check_in/utils/logger.rb +++ b/modules/check_in/lib/check_in/utils/logger.rb @@ -29,7 +29,8 @@ def common uuid:, controller: ctrl_name, action: ctrl_action, - initiated_by: + initiated_by:, + facility_type: } end @@ -73,6 +74,11 @@ def initiated_by '' end end + + def facility_type + ctrl.params[:facility_type] || ctrl.params.dig(:session, :facility_type) || + ctrl.params.dig(:travel_claims, :facility_type) + end end end end diff --git a/modules/check_in/spec/lib/utils/logger_spec.rb b/modules/check_in/spec/lib/utils/logger_spec.rb index c61aa1c5af5..328a9f9f4da 100644 --- a/modules/check_in/spec/lib/utils/logger_spec.rb +++ b/modules/check_in/spec/lib/utils/logger_spec.rb @@ -12,27 +12,56 @@ end describe '#before' do - let(:controller) do - double('FooController', - controller_name: 'sessions', - action_name: 'show', - response: { body: '' }, - params: { id: '123' }, - permitted_params: {}) - end - let(:resp) do - { - workflow: 'Min-Auth', - uuid: '123', - controller: 'sessions', - action: 'show', - initiated_by: '', - filter: :before_action - } + context 'when endpoint called without facility_type' do + let(:controller) do + double('FooController', + controller_name: 'sessions', + action_name: 'show', + response: { body: '' }, + params: { id: '123' }, + permitted_params: {}) + end + let(:resp) do + { + workflow: 'Min-Auth', + uuid: '123', + controller: 'sessions', + action: 'show', + initiated_by: '', + facility_type: nil, + filter: :before_action + } + end + + it 'returns the before info hash with nil facility_type' do + expect(described_class.build(controller).before).to eq(resp) + end end - it 'returns the before info hash' do - expect(described_class.build(controller).before).to eq(resp) + context 'when endpoint called with facility_type' do + let(:controller) do + double('FooController', + controller_name: 'sessions', + action_name: 'show', + response: { body: '' }, + params: { id: '123', facility_type: 'oh' }, + permitted_params: {}) + end + let(:resp) do + { + workflow: 'Min-Auth', + uuid: '123', + controller: 'sessions', + action: 'show', + initiated_by: '', + facility_type: 'oh', + filter: :before_action + } + end + + it 'returns the before info hash with nil facility_type' do + expect(described_class.build(controller).before).to eq(resp) + end end end @@ -49,7 +78,7 @@ } end - context 'when set_e_checkin_started_called = false' do + context 'when set_e_checkin_started_called = false without facility_type' do let(:controller) do double('FooController', controller_name: 'patient_check_ins', @@ -59,7 +88,7 @@ permitted_params: { uuid: '345' }) end let(:resp_with_initiated_by_vetext) do - resp.merge(initiated_by: 'vetext') + resp.merge(initiated_by: 'vetext', facility_type: nil) end it 'returns the after info hash with initiated_by set with vetext' do @@ -67,7 +96,7 @@ end end - context 'when set_e_checkin_started_called = true' do + context 'when set_e_checkin_started_called = true without facility_type' do let(:controller) do double('FooController', controller_name: 'patient_check_ins', @@ -77,7 +106,25 @@ permitted_params: { uuid: '123' }) end let(:resp_with_initiated_by_veteran) do - resp.merge(initiated_by: 'veteran') + resp.merge(initiated_by: 'veteran', facility_type: nil) + end + + it 'returns the after info hash with initiated_by set with vetext' do + expect(described_class.build(controller).after).to eq(resp_with_initiated_by_veteran) + end + end + + context 'when set_e_checkin_started_called = true with oh facility_type' do + let(:controller) do + double('FooController', + controller_name: 'patient_check_ins', + action_name: 'show', + response: double('ResponseBody', body: '{"a":"b", "status":"success 200", "c":"d"}'), + params: { id: '123', set_e_checkin_started_called: true, facility_type: 'oh' }, + permitted_params: { uuid: '123' }) + end + let(:resp_with_initiated_by_veteran) do + resp.merge(initiated_by: 'veteran', facility_type: 'oh') end it 'returns the after info hash with initiated_by set with vetext' do @@ -94,6 +141,7 @@ controller: 'patient_check_ins', action: 'create', api_status: 'success 200', + facility_type: nil, filter: :after_action } end 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/request/v2/demographics_request_spec.rb b/modules/check_in/spec/request/v2/demographics_request_spec.rb index e3351b3e724..c1723f503c7 100644 --- a/modules/check_in/spec/request/v2/demographics_request_spec.rb +++ b/modules/check_in/spec/request/v2/demographics_request_spec.rb @@ -11,7 +11,6 @@ allow(Flipper).to receive(:enabled?).with('check_in_experience_enabled').and_return(true) allow(Flipper).to receive(:enabled?).with('check_in_experience_enabled', anything).and_return(true) allow(Flipper).to receive(:enabled?).with('check_in_experience_mock_enabled').and_return(false) - allow(Flipper).to receive(:enabled?).with('check_in_experience_45_minute_reminder').and_return(false) Rails.cache.clear end @@ -63,8 +62,12 @@ end VCR.use_cassette('check_in/lorota/data/data_200', match_requests_on: [:host]) do - get "/check_in/v2/patient_check_ins/#{id}" - expect(response.status).to eq(200) + VCR.use_cassette 'check_in/chip/set_echeckin_started/set_echeckin_started_200' do + VCR.use_cassette 'check_in/chip/token/token_200' do + get "/check_in/v2/patient_check_ins/#{id}" + expect(response.status).to eq(200) + end + end end VCR.use_cassette('check_in/chip/token/token_200') do @@ -104,8 +107,12 @@ end VCR.use_cassette('check_in/lorota/data/data_200', match_requests_on: [:host]) do - get "/check_in/v2/patient_check_ins/#{id}" - expect(response.status).to eq(200) + VCR.use_cassette 'check_in/chip/set_echeckin_started/set_echeckin_started_200' do + VCR.use_cassette 'check_in/chip/token/token_200' do + get "/check_in/v2/patient_check_ins/#{id}" + expect(response.status).to eq(200) + end + end end VCR.use_cassette('check_in/chip/confirm_demographics/confirm_demographics_504', match_requests_on: [:host]) do @@ -163,8 +170,12 @@ end VCR.use_cassette('check_in/lorota/data/data_200', match_requests_on: [:host]) do - get "/check_in/v2/patient_check_ins/#{id}" - expect(response.status).to eq(200) + VCR.use_cassette 'check_in/chip/set_echeckin_started/set_echeckin_started_200' do + VCR.use_cassette 'check_in/chip/token/token_200' do + get "/check_in/v2/patient_check_ins/#{id}" + expect(response.status).to eq(200) + end + end end VCR.use_cassette('check_in/chip/confirm_demographics/confirm_demographics_200', match_requests_on: [:host]) do diff --git a/modules/check_in/spec/request/v2/patient_check_ins_request_spec.rb b/modules/check_in/spec/request/v2/patient_check_ins_request_spec.rb index d8d85051703..e7c309505de 100644 --- a/modules/check_in/spec/request/v2/patient_check_ins_request_spec.rb +++ b/modules/check_in/spec/request/v2/patient_check_ins_request_spec.rb @@ -10,7 +10,6 @@ allow(Rails).to receive(:cache).and_return(memory_store) allow(Flipper).to receive(:enabled?).with('check_in_experience_enabled').and_return(true) allow(Flipper).to receive(:enabled?).with('check_in_experience_mock_enabled').and_return(false) - allow(Flipper).to receive(:enabled?).with('check_in_experience_45_minute_reminder').and_return(false) Rails.cache.clear end @@ -183,146 +182,142 @@ end VCR.use_cassette('check_in/lorota/data/data_200', match_requests_on: [:host]) do - get "/check_in/v2/patient_check_ins/#{id}" + VCR.use_cassette 'check_in/chip/set_echeckin_started/set_echeckin_started_200' do + VCR.use_cassette 'check_in/chip/token/token_200' do + get "/check_in/v2/patient_check_ins/#{id}" + end + end end expect(response.status).to eq(200) expect(JSON.parse(response.body)).to eq(resp) end - context 'when check_in_experience_45_minute_reminder feature flag is on' do - before do - allow(Flipper).to receive(:enabled?).with('check_in_experience_45_minute_reminder').and_return(true) + context 'for OH sites' do + let(:appointment) do + { + 'appointmentIEN' => '4822366', + 'clinicCreditStopCodeName' => '', + 'clinicFriendlyName' => 'Endoscopy', + 'clinicIen' => '32216049', + 'clinicLocation' => '', + 'clinicName' => 'Endoscopy', + 'clinicPhoneNumber' => '909-825-7084', + 'clinicStopCodeName' => 'Mental Health, Primary Care', + 'doctorName' => 'Dr. Jones', + 'facility' => 'Jerry L. Pettis Memorial Veterans Hospital', + 'facilityAddress' => { + 'city' => 'Loma Linda', + 'state' => 'CA', + 'street1' => '', + 'street2' => '', + 'street3' => '', + 'zip' => '92357-1000' + }, + 'kind' => 'clinic', + 'startTime' => '2024-02-14T22:10:00.000+00:00', + 'stationNo' => '530', + 'status' => 'Confirmed', + 'timezone' => 'America/Los_Angeles' + } end - - context 'for OH sites' do - let(:appointment) do - { - 'appointmentIEN' => '4822366', - 'clinicCreditStopCodeName' => '', - 'clinicFriendlyName' => 'Endoscopy', - 'clinicIen' => '32216049', - 'clinicLocation' => '', - 'clinicName' => 'Endoscopy', - 'clinicPhoneNumber' => '909-825-7084', - 'clinicStopCodeName' => 'Mental Health, Primary Care', - 'doctorName' => 'Dr. Jones', - 'edipi' => '1000000105', - 'facility' => 'Jerry L. Pettis Memorial Veterans Hospital', - 'facilityAddress' => { - 'city' => 'Loma Linda', - 'state' => 'CA', - 'street1' => '', - 'street2' => '', - 'street3' => '', - 'zip' => '92357-1000' - }, - 'icn' => '1013220078V743173', - 'kind' => 'clinic', - 'startTime' => '2024-02-14T22:10:00.000+00:00', - 'stationNo' => '530', - 'status' => 'Confirmed', - 'timezone' => 'America/Los_Angeles' - } - end - let(:resp) do - { - 'id' => id, - 'payload' => { - 'address' => '1166 6th Avenue 22, New York, NY 23423 US', - 'demographics' => {}, - 'appointments' => [appointment], - 'patientDemographicsStatus' => {}, - 'setECheckinStartedCalled' => nil - } + let(:resp) do + { + 'id' => id, + 'payload' => { + 'address' => '1166 6th Avenue 22, New York, NY 23423 US', + 'demographics' => {}, + 'appointments' => [appointment], + 'patientDemographicsStatus' => {}, + 'setECheckinStartedCalled' => nil } + } + end + + it 'does not call set_echeckin_started' do + VCR.use_cassette 'check_in/lorota/token/token_200' do + post '/check_in/v2/sessions', **session_params + expect(response.status).to eq(200) end - it 'does not call set_echeckin_started' do - VCR.use_cassette 'check_in/lorota/token/token_200' do - post '/check_in/v2/sessions', **session_params - expect(response.status).to eq(200) + VCR.use_cassette('check_in/lorota/data/data_oracle_health_200', match_requests_on: [:host]) do + VCR.use_cassette 'check_in/chip/token/token_200' do + get "/check_in/v2/patient_check_ins/#{id}?facilityType=oh" end + end + expect(response.status).to eq(200) + expect(JSON.parse(response.body)).to eq(resp) + end + end + + context 'when set_echeckin_started call succeeds' do + it 'calls set_echeckin_started and returns valid response' do + VCR.use_cassette 'check_in/lorota/token/token_200' do + post '/check_in/v2/sessions', **session_params + expect(response.status).to eq(200) + end - VCR.use_cassette('check_in/lorota/data/data_oracle_health_200', match_requests_on: [:host]) do + VCR.use_cassette('check_in/lorota/data/data_200', match_requests_on: [:host]) do + VCR.use_cassette 'check_in/chip/set_echeckin_started/set_echeckin_started_200' do VCR.use_cassette 'check_in/chip/token/token_200' do - get "/check_in/v2/patient_check_ins/#{id}?facilityType=oh" + get "/check_in/v2/patient_check_ins/#{id}" end end - expect(response.status).to eq(200) - expect(JSON.parse(response.body)).to eq(resp) end + expect(response.status).to eq(200) + expect(JSON.parse(response.body)).to eq(resp) end + end - context 'when set_echeckin_started call succeeds' do - it 'calls set_echeckin_started and returns valid response' do - VCR.use_cassette 'check_in/lorota/token/token_200' do - post '/check_in/v2/sessions', **session_params - expect(response.status).to eq(200) - end + context 'when setECheckinStartedCalled set to true' do + let(:resp_with_true_set_e_check_in) do + resp['payload']['setECheckinStartedCalled'] = true + resp + end - VCR.use_cassette('check_in/lorota/data/data_200', match_requests_on: [:host]) do - VCR.use_cassette 'check_in/chip/set_echeckin_started/set_echeckin_started_200' do - VCR.use_cassette 'check_in/chip/token/token_200' do - get "/check_in/v2/patient_check_ins/#{id}" - end - end - end + it 'returns valid response without calling set_echeckin_started' do + VCR.use_cassette 'check_in/lorota/token/token_200' do + post '/check_in/v2/sessions', **session_params expect(response.status).to eq(200) - expect(JSON.parse(response.body)).to eq(resp) end - end - context 'when setECheckinStartedCalled set to true' do - let(:resp_with_true_set_e_check_in) do - resp['payload']['setECheckinStartedCalled'] = true - resp + VCR.use_cassette('check_in/lorota/data/data_with_echeckin_started_200', match_requests_on: [:host]) do + get "/check_in/v2/patient_check_ins/#{id}" end + expect(response.status).to eq(200) + expect(JSON.parse(response.body)).to eq(resp_with_true_set_e_check_in) + end + end - it 'returns valid response without calling set_echeckin_started' do - VCR.use_cassette 'check_in/lorota/token/token_200' do - post '/check_in/v2/sessions', **session_params - expect(response.status).to eq(200) - end - - VCR.use_cassette('check_in/lorota/data/data_with_echeckin_started_200', match_requests_on: [:host]) do - get "/check_in/v2/patient_check_ins/#{id}" - end - expect(response.status).to eq(200) - expect(JSON.parse(response.body)).to eq(resp_with_true_set_e_check_in) - end + context 'when set_echeckin_started call fails' do + let(:error_body) do + { + 'errors' => [ + { + 'title' => 'Internal Server Error', + 'detail' => 'Internal Server Error', + 'code' => 'CHIP-API_500', + 'status' => '500' + } + ] + } end + let(:error_resp) { Faraday::Response.new(response_body: error_body, status: 500) } - context 'when set_echeckin_started call fails' do - let(:error_body) do - { - 'errors' => [ - { - 'title' => 'Internal Server Error', - 'detail' => 'Internal Server Error', - 'code' => 'CHIP-API_500', - 'status' => '500' - } - ] - } + it 'returns error response' do + VCR.use_cassette 'check_in/lorota/token/token_200' do + post '/check_in/v2/sessions', **session_params + expect(response.status).to eq(200) end - let(:error_resp) { Faraday::Response.new(response_body: error_body, status: 500) } - - it 'returns error response' do - VCR.use_cassette 'check_in/lorota/token/token_200' do - post '/check_in/v2/sessions', **session_params - expect(response.status).to eq(200) - end - VCR.use_cassette('check_in/lorota/data/data_200', match_requests_on: [:host]) do - VCR.use_cassette 'check_in/chip/set_echeckin_started/set_echeckin_started_500' do - VCR.use_cassette 'check_in/chip/token/token_200' do - get "/check_in/v2/patient_check_ins/#{id}" - end + VCR.use_cassette('check_in/lorota/data/data_200', match_requests_on: [:host]) do + VCR.use_cassette 'check_in/chip/set_echeckin_started/set_echeckin_started_500' do + VCR.use_cassette 'check_in/chip/token/token_200' do + get "/check_in/v2/patient_check_ins/#{id}" end end - expect(response.status).to eq(error_resp.status) - expect(response.body).to eq(error_resp.body.to_json) end + expect(response.status).to eq(error_resp.status) + expect(response.body).to eq(error_resp.body.to_json) end end end @@ -353,8 +348,12 @@ end VCR.use_cassette('check_in/lorota/data/data_200', match_requests_on: [:host]) do - get "/check_in/v2/patient_check_ins/#{id}" - expect(response.status).to eq(200) + VCR.use_cassette 'check_in/chip/set_echeckin_started/set_echeckin_started_200' do + VCR.use_cassette 'check_in/chip/token/token_200' do + get "/check_in/v2/patient_check_ins/#{id}" + expect(response.status).to eq(200) + end + end end VCR.use_cassette('check_in/chip/check_in/check_in_200', match_requests_on: [:host]) do diff --git a/modules/check_in/spec/request/v2/pre_check_ins_request_spec.rb b/modules/check_in/spec/request/v2/pre_check_ins_request_spec.rb index 3e2351f6869..aa68cf3c512 100644 --- a/modules/check_in/spec/request/v2/pre_check_ins_request_spec.rb +++ b/modules/check_in/spec/request/v2/pre_check_ins_request_spec.rb @@ -11,7 +11,6 @@ allow(Flipper).to receive(:enabled?).with('check_in_experience_enabled').and_return(true) allow(Flipper).to receive(:enabled?).with('check_in_experience_pre_check_in_enabled').and_return(true) allow(Flipper).to receive(:enabled?).with('check_in_experience_mock_enabled').and_return(false) - allow(Flipper).to receive(:enabled?).with('check_in_experience_45_minute_reminder').and_return(false) Rails.cache.clear end diff --git a/modules/check_in/spec/serializers/check_in/v2/appointment_data_serializer_spec.rb b/modules/check_in/spec/serializers/check_in/v2/appointment_data_serializer_spec.rb index 0f80337540d..5b8c33847de 100644 --- a/modules/check_in/spec/serializers/check_in/v2/appointment_data_serializer_spec.rb +++ b/modules/check_in/spec/serializers/check_in/v2/appointment_data_serializer_spec.rb @@ -324,5 +324,98 @@ expect(appt_serializer.serializable_hash).to eq(serialized_hash_response) end end + + context 'for OH data' do + let(:appointment_data_oh) do + { + id: 'd602d9eb-9a31-484f-9637-13ab0b507e0d', + scope: 'read.full', + payload: { + address: '1166 6th Avenue 22, New York, NY 23423 US', + appointments: [ + { + appointmentIEN: '4822366', + clinicCreditStopCodeName: '', + clinicFriendlyName: 'Endoscopy', + clinicIen: '32216049', + clinicLocation: '', + clinicName: 'Endoscopy', + clinicPhoneNumber: '909-825-7084', + clinicStopCodeName: 'Mental Health, Primary Care', + doctorName: 'Dr. Jones', + edipi: '1000000105', + facility: 'Jerry L. Pettis Memorial Veterans Hospital', + facilityAddress: { + city: 'Loma Linda', + state: 'CA', + street1: '', + street2: '', + street3: '', + zip: '92357-1000' + }, + icn: '1013220078V743173', + kind: 'clinic', + startTime: '2024-02-14T22:10:00.000+00:00', + stationNo: '530', + status: 'Confirmed', + timezone: 'America/Los_Angeles' + } + ], + patientCellPhone: '4445556666', + facilityType: 'OH' + } + } + end + let(:serialized_hash_response) do + { + data: { + id: 'd602d9eb-9a31-484f-9637-13ab0b507e0d', + type: :appointment_data, + attributes: { + payload: { + address: '1166 6th Avenue 22, New York, NY 23423 US', + demographics: {}, + appointments: [ + { + appointmentIEN: '4822366', + clinicCreditStopCodeName: '', + clinicFriendlyName: 'Endoscopy', + clinicIen: '32216049', + clinicLocation: '', + clinicName: 'Endoscopy', + clinicPhoneNumber: '909-825-7084', + clinicStopCodeName: 'Mental Health, Primary Care', + doctorName: 'Dr. Jones', + facility: 'Jerry L. Pettis Memorial Veterans Hospital', + facilityAddress: { + city: 'Loma Linda', + state: 'CA', + street1: '', + street2: '', + street3: '', + zip: '92357-1000' + }, + kind: 'clinic', + startTime: '2024-02-14T22:10:00.000+00:00', + stationNo: '530', + status: 'Confirmed', + timezone: 'America/Los_Angeles' + } + ], + patientDemographicsStatus: {}, + setECheckinStartedCalled: nil + } + } + } + } + end + + it 'returns a serialized hash' do + appt_struct = OpenStruct.new(appointment_data_oh) + appt_serializer = CheckIn::V2::AppointmentDataSerializer.new(appt_struct) + + expect(appt_serializer.serializable_hash).to eq(serialized_hash_response) + end + end end end diff --git a/modules/check_in/spec/serializers/check_in/v2/appointment_identifiers_serializer_spec.rb b/modules/check_in/spec/serializers/check_in/v2/appointment_identifiers_serializer_spec.rb index ff37fff9875..6f5997cc171 100644 --- a/modules/check_in/spec/serializers/check_in/v2/appointment_identifiers_serializer_spec.rb +++ b/modules/check_in/spec/serializers/check_in/v2/appointment_identifiers_serializer_spec.rb @@ -75,6 +75,47 @@ } end + let(:appointment_data_oh) do + { + id: 'd602d9eb-9a31-484f-9637-13ab0b507e0d', + scope: 'read.full', + payload: { + address: '1166 6th Avenue 22, New York, NY 23423 US', + appointments: [ + { + appointmentIEN: '4822366', + clinicCreditStopCodeName: '', + clinicFriendlyName: 'Endoscopy', + clinicIen: '32216049', + clinicLocation: '', + clinicName: 'Endoscopy', + clinicPhoneNumber: '909-825-7084', + clinicStopCodeName: 'Mental Health, Primary Care', + doctorName: 'Dr. Jones', + edipi: '1000000105', + facility: 'Jerry L. Pettis Memorial Veterans Hospital', + facilityAddress: { + city: 'Loma Linda', + state: 'CA', + street1: '', + street2: '', + street3: '', + zip: '92357-1000' + }, + icn: '1013220078V743173', + kind: 'clinic', + startTime: '2024-02-14T22:10:00.000+00:00', + stationNo: '530', + status: 'Confirmed', + timezone: 'America/Los_Angeles' + } + ], + patientCellPhone: '4445556666', + facilityType: 'OH' + } + } + end + describe '#serializable_hash' do context 'when icn does not exist' do let(:serialized_hash_response) do @@ -351,5 +392,33 @@ expect(appt_serializer.serializable_hash).to eq(serialized_hash_response) end end + + context 'for OH data' do + let(:serialized_hash_response) do + { + data: { + id: 'd602d9eb-9a31-484f-9637-13ab0b507e0d', + type: :appointment_identifier, + attributes: { + patientDFN: nil, + stationNo: '530', + appointmentIEN: '4822366', + icn: '1013220078V743173', + mobilePhone: nil, + patientCellPhone: '4445556666', + facilityType: 'OH', + edipi: '1000000105' + } + } + } + end + + it 'returns serialized identifier data' do + appt_struct = OpenStruct.new(appointment_data_oh) + appt_serializer = CheckIn::V2::AppointmentIdentifiersSerializer.new(appt_struct) + + expect(appt_serializer.serializable_hash).to eq(serialized_hash_response) + end + end end end diff --git a/modules/check_in/spec/services/check_in/vaos/base_service_spec.rb b/modules/check_in/spec/services/check_in/vaos/base_service_spec.rb new file mode 100644 index 00000000000..f523aab9fc6 --- /dev/null +++ b/modules/check_in/spec/services/check_in/vaos/base_service_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe CheckIn::VAOS::BaseService do + subject { described_class.new(patient_icn:) } + + let(:patient_icn) { '123' } + let(:token) { 'test_token' } + let(:request_id) { SecureRandom.uuid } + + describe '#config' do + it 'returns an instance of Configuration' do + expect(subject.config).to be_an_instance_of(CheckIn::VAOS::Configuration) + end + end + + describe '#headers' do + before do + allow_any_instance_of(CheckIn::Map::TokenService).to receive(:token).and_return(token) + RequestStore.store['request_id'] = request_id + end + + it 'returns correct headers' do + expect(subject.headers).to eq({ 'Referer' => 'https://review-instance.va.gov', + 'X-VAMF-JWT' => token, + 'X-Request-ID' => request_id }) + end + end + + describe '#referrer' do + context 'when ends in .gov' do + it 'returns the hostname with "vets" replaced with "va"' do + allow(Settings).to receive(:hostname).and_return('veteran.apps.vets.gov') + expect(subject.referrer).to eq('https://veteran.apps.va.gov') + end + end + + context 'when does not end in .gov' do + it 'returns https://review-instance.va.gov' do + expect(subject.referrer).to eq('https://review-instance.va.gov') + end + end + end +end diff --git a/modules/check_in/spec/services/travel_claim/client_spec.rb b/modules/check_in/spec/services/travel_claim/client_spec.rb index da48e72537a..0c812a71f96 100644 --- a/modules/check_in/spec/services/travel_claim/client_spec.rb +++ b/modules/check_in/spec/services/travel_claim/client_spec.rb @@ -10,7 +10,6 @@ before do allow(Flipper).to receive(:enabled?).with('check_in_experience_mock_enabled').and_return(false) - allow(Flipper).to receive(:enabled?).with(:check_in_experience_travel_claim_increase_timeout).and_return(true) end describe '.build' 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/check_in/spec/sidekiq/travel_claim_submission_worker_spec.rb b/modules/check_in/spec/sidekiq/travel_claim_submission_worker_spec.rb index cce04cc405e..6627802d752 100644 --- a/modules/check_in/spec/sidekiq/travel_claim_submission_worker_spec.rb +++ b/modules/check_in/spec/sidekiq/travel_claim_submission_worker_spec.rb @@ -179,7 +179,6 @@ before do allow(TravelClaim::RedisClient).to receive(:build).and_return(redis_client) allow(Flipper).to receive(:enabled?).with('check_in_experience_mock_enabled').and_return(false) - allow(Flipper).to receive(:enabled?).with(:check_in_experience_travel_claim_increase_timeout).and_return(true) allow(redis_client).to receive(:patient_cell_phone).and_return(patient_cell_phone) allow(redis_client).to receive(:token).and_return(redis_token) diff --git a/modules/claims_api/README.md b/modules/claims_api/README.md index 8bf0ffe4fdc..53cd34fdb30 100644 --- a/modules/claims_api/README.md +++ b/modules/claims_api/README.md @@ -9,6 +9,11 @@ ssh -L 4447:localhost:4447 {{aws-url}} ##### EVSS ssh -L 4431:localhost:4431 {{aws-url}} +## Testing +### Unit testing BGS service operation wrappers +If using cassettes, make sure to only make or use ones under [spec/support/vcr_cassettes/claims_api](spec/support/vcr_cassettes/claims_api) +Check out documentation in comments for the spec helper `BGSClientHelpers#use_bgs_cassette` + ## OpenApi/Swagger Doc Generation This api uses [rswag](https://github.com/rswag/rswag) to build the OpenApi/Swagger docs that are displayed in the [VA|Lighthouse APIs Documentation](https://developer.va.gov/explore/benefits/docs/claims?version=current). To generate/update the docs for this api, navigate to the root directory of `vets-api` and run the following command :: - `rake rswag:claims_api:build` diff --git a/modules/claims_api/app/controllers/claims_api/v2/veterans/claims_controller.rb b/modules/claims_api/app/controllers/claims_api/v2/veterans/claims_controller.rb index c8c2209d192..6051a9da91f 100644 --- a/modules/claims_api/app/controllers/claims_api/v2/veterans/claims_controller.rb +++ b/modules/claims_api/app/controllers/claims_api/v2/veterans/claims_controller.rb @@ -323,12 +323,12 @@ def detect_current_status(data) return 'NO_STATUS_PROVIDED' end - phase_data = if data[:phase_type].present? + phase_data = if data[:claim_status] == 'CAN' + data[:claim_status] + elsif data[:phase_type].present? data[:phase_type] - elsif data[:bnft_claim_lc_status].present? - data[:bnft_claim_lc_status] else - data[:claim_status] + data[:bnft_claim_lc_status] end return bgs_phase_status_mapper.name(phase_data) if phase_data.is_a?(String) diff --git a/modules/claims_api/app/controllers/claims_api/v2/veterans/power_of_attorney/base_controller.rb b/modules/claims_api/app/controllers/claims_api/v2/veterans/power_of_attorney/base_controller.rb index 784253396d7..3e38c77e71c 100644 --- a/modules/claims_api/app/controllers/claims_api/v2/veterans/power_of_attorney/base_controller.rb +++ b/modules/claims_api/app/controllers/claims_api/v2/veterans/power_of_attorney/base_controller.rb @@ -46,9 +46,12 @@ def request_representative def shared_form_validation(form_number) target_veteran # Custom validations for POA submission, we must check this first - @claims_api_forms_validation_errors = validate_form_2122_and_2122a_submission_values + @claims_api_forms_validation_errors = validate_form_2122_and_2122a_submission_values(user_profile) # JSON validations for POA submission, will combine with previously captured errors and raise validate_json_schema(form_number.upcase) + @rep_id = validate_registration_number!(form_number) + + add_claimant_data_to_form if user_profile # if we get here there were only validations file errors if @claims_api_forms_validation_errors raise ::ClaimsApi::Common::Exceptions::Lighthouse::JsonDisabilityCompensationValidationError, @@ -56,6 +59,22 @@ def shared_form_validation(form_number) end end + def validate_registration_number!(form_number) + base = form_number == '2122' ? 'serviceOrganization' : 'representative' + rn = form_attributes.dig(base, 'registrationNumber') + poa_code = form_attributes.dig(base, 'poaCode') + rep = ::Veteran::Service::Representative.where('? = ANY(poa_codes) AND representative_id = ?', + poa_code, + rn).order(created_at: :desc).first + if rep.nil? + raise ::Common::Exceptions::ResourceNotFound.new( + detail: "Could not find an Accredited Representative with registration number: #{rn} " \ + "and poa code: #{poa_code}" + ) + end + rep.id + end + def submit_power_of_attorney(poa_code, form_number) attributes = { status: ClaimsApi::PowerOfAttorney::PENDING, @@ -69,7 +88,7 @@ def submit_power_of_attorney(poa_code, form_number) power_of_attorney = ClaimsApi::PowerOfAttorney.create!(attributes) unless Settings.claims_api&.poa_v2&.disable_jobs - ClaimsApi::V2::PoaFormBuilderJob.perform_async(power_of_attorney.id, form_number) + ClaimsApi::V2::PoaFormBuilderJob.perform_async(power_of_attorney.id, form_number, @rep_id) end render json: ClaimsApi::V2::Blueprints::PowerOfAttorneyBlueprint.render( @@ -167,6 +186,30 @@ def nullable_icn nil end + + def user_profile + return @user_profile if defined? @user_profile + + @user_profile ||= fetch_claimant + end + + def fetch_claimant + claimant_icn = form_attributes.dig('claimant', 'claimantId') + if claimant_icn.present? + mpi_profile = mpi_service.find_profile_by_identifier(identifier: claimant_icn, + identifier_type: MPI::Constants::ICN) + end + rescue ArgumentError + mpi_profile + end + + def add_claimant_data_to_form + if user_profile&.status == :ok + first_name = user_profile.profile.given_names.first + last_name = user_profile.profile.family_name + form_attributes['claimant'].merge!(firstName: first_name, lastName: last_name) + end + end end end end diff --git a/modules/claims_api/app/controllers/concerns/claims_api/poa_verification.rb b/modules/claims_api/app/controllers/concerns/claims_api/poa_verification.rb index b7c17966f07..12fc5e63c0e 100644 --- a/modules/claims_api/app/controllers/concerns/claims_api/poa_verification.rb +++ b/modules/claims_api/app/controllers/concerns/claims_api/poa_verification.rb @@ -67,9 +67,10 @@ def target_veteran_is_current_user? # @param poa_code [String] poa code to match to @current_user # # @return [Boolean] True if valid poa code, False if not - def valid_poa_code_for_current_user?(poa_code) + def valid_poa_code_for_current_user?(poa_code) # rubocop:disable Metrics/MethodLength reps = ::Veteran::Service::Representative.all_for_user(first_name: @current_user.first_name, last_name: @current_user.last_name) + return false if reps.blank? if reps.count > 1 @@ -81,6 +82,12 @@ def valid_poa_code_for_current_user?(poa_code) last_name: @current_user.last_name, middle_initial:) + if reps.blank? || reps.count > 1 + reps = ::Veteran::Service::Representative.all_for_user(first_name: @current_user.first_name, + last_name: @current_user.last_name, + poa_code:) + end + raise ::Common::Exceptions::Unauthorized, detail: 'VSO Representative Not Found' if reps.blank? raise ::Common::Exceptions::Unauthorized, detail: 'Ambiguous VSO Representative Results' if reps.count > 1 end diff --git a/modules/claims_api/app/controllers/concerns/claims_api/v2/power_of_attorney_validation.rb b/modules/claims_api/app/controllers/concerns/claims_api/v2/power_of_attorney_validation.rb index b535ff11709..c1001eb50b0 100644 --- a/modules/claims_api/app/controllers/concerns/claims_api/v2/power_of_attorney_validation.rb +++ b/modules/claims_api/app/controllers/concerns/claims_api/v2/power_of_attorney_validation.rb @@ -1,19 +1,22 @@ # frozen_string_literal: false +# rubocop:disable Metrics/ModuleLength + module ClaimsApi module V2 module PowerOfAttorneyValidation - def validate_form_2122_and_2122a_submission_values - validate_claimant + def validate_form_2122_and_2122a_submission_values(user_profile) + validate_claimant(user_profile) # collect errors and pass back to the controller raise_error_collection if @errors end private - def validate_claimant + def validate_claimant(user_profile) return if form_attributes['claimant'].blank? + validate_claimant_id_included(user_profile) validate_address validate_relationship end @@ -92,6 +95,26 @@ def validate_relationship end end + def validate_claimant_id_included(user_profile) + claimant_icn = form_attributes.dig('claimant', 'claimantId') + if (user_profile.blank? || user_profile&.status == :not_found) && claimant_icn + collect_error_messages( + source: 'claimant/claimantId', + detail: "The 'claimantId' must be valid" + ) + else + address = form_attributes.dig('claimant', 'address') + phone = form_attributes.dig('claimant', 'phone') + relationship = form_attributes.dig('claimant', 'relationship') + return if claimant_icn.present? && (address.present? || phone.present? || relationship.present?) + + collect_error_messages( + source: '/claimant/claimantId/', + detail: "If claimant is present 'claimantId' must be filled in" + ) + end + end + def errors_array @errors ||= [] end @@ -109,3 +132,4 @@ def raise_error_collection end end end +# rubocop:enable Metrics/ModuleLength diff --git a/modules/claims_api/app/sidekiq/claims_api/poa_updater.rb b/modules/claims_api/app/sidekiq/claims_api/poa_updater.rb index 700b084fdb1..b7f24070b3f 100644 --- a/modules/claims_api/app/sidekiq/claims_api/poa_updater.rb +++ b/modules/claims_api/app/sidekiq/claims_api/poa_updater.rb @@ -40,14 +40,6 @@ def perform(power_of_attorney_id) # rubocop:disable Metrics/MethodLength private - def extract_poa_code(poa_form_data) - if poa_form_data.key?('serviceOrganization') - poa_form_data['serviceOrganization']['poaCode'] - elsif poa_form_data.key?('representative') # V2 2122a - poa_form_data['representative']['poaCode'] - end - end - def enable_vbms_access?(poa_form:) poa_form.form_data['recordConsent'] && poa_form.form_data['consentLimits'].blank? end diff --git a/modules/claims_api/app/sidekiq/claims_api/poa_vbms_updater.rb b/modules/claims_api/app/sidekiq/claims_api/poa_vbms_updater.rb index 0b090232ecb..1b64ef487c0 100644 --- a/modules/claims_api/app/sidekiq/claims_api/poa_vbms_updater.rb +++ b/modules/claims_api/app/sidekiq/claims_api/poa_vbms_updater.rb @@ -10,18 +10,18 @@ def perform(power_of_attorney_id) # rubocop:disable Metrics/MethodLength external_uid: poa_form.external_uid, external_key: poa_form.external_key ) + poa_code = extract_poa_code(poa_form.form_data) ClaimsApi::Logger.log( 'poa_vbms_updater', poa_id: power_of_attorney_id, detail: 'Updating Access', - poa_code: poa_form.form_data.dig('serviceOrganization', 'poaCode') + poa_code: ) - # allow_poa_c_add reports 'No Data' if sent lowercase response = service.corporate_update.update_poa_access( participant_id: poa_form.auth_headers['va_eauth_pid'], - poa_code: poa_form.form_data.dig('serviceOrganization', 'poaCode'), + poa_code:, allow_poa_access: 'y', allow_poa_c_add: allow_address_change?(poa_form, power_of_attorney_id) ? 'Y' : 'N' ) diff --git a/modules/claims_api/app/sidekiq/claims_api/service_base.rb b/modules/claims_api/app/sidekiq/claims_api/service_base.rb index 9a7259d5a90..18effe692f6 100644 --- a/modules/claims_api/app/sidekiq/claims_api/service_base.rb +++ b/modules/claims_api/app/sidekiq/claims_api/service_base.rb @@ -123,5 +123,13 @@ def log_job_progress(claim_id, detail) claim_id:, detail:) end + + def extract_poa_code(poa_form_data) + if poa_form_data.key?('serviceOrganization') + poa_form_data['serviceOrganization']['poaCode'] + elsif poa_form_data.key?('representative') # V2 2122a + poa_form_data['representative']['poaCode'] + end + end end end diff --git a/modules/claims_api/app/sidekiq/claims_api/v2/poa_form_builder_job.rb b/modules/claims_api/app/sidekiq/claims_api/v2/poa_form_builder_job.rb index 0517d611694..2c0c25e9aa0 100644 --- a/modules/claims_api/app/sidekiq/claims_api/v2/poa_form_builder_job.rb +++ b/modules/claims_api/app/sidekiq/claims_api/v2/poa_form_builder_job.rb @@ -15,10 +15,11 @@ class PoaFormBuilderJob < ClaimsApi::ServiceBase # it queues a job to update the POA code in BGS, as well. # # @param power_of_attorney_id [String] Unique identifier of the submitted POA - def perform(power_of_attorney_id, form_number) + def perform(power_of_attorney_id, form_number, rep_id) power_of_attorney = ClaimsApi::PowerOfAttorney.find(power_of_attorney_id) + rep = ::Veteran::Service::Representative.where(representative_id: rep_id).order(created_at: :desc).first - output_path = pdf_constructor(form_number).construct(data(power_of_attorney, form_number), + output_path = pdf_constructor(form_number).construct(data(power_of_attorney, form_number, rep), id: power_of_attorney.id) upload_to_vbms(power_of_attorney, output_path) ClaimsApi::PoaUpdater.perform_async(power_of_attorney.id) @@ -43,40 +44,50 @@ def pdf_constructor(form_number) # @param form_number [String] Either 2122 or 2122A # # @return [Hash] All data to be inserted into pdf - def data(power_of_attorney, form_number) - res = power_of_attorney - .form_data.deep_merge({ - 'veteran' => { - 'firstName' => power_of_attorney.auth_headers['va_eauth_firstName'], - 'lastName' => power_of_attorney.auth_headers['va_eauth_lastName'], - 'ssn' => power_of_attorney.auth_headers['va_eauth_pnid'], - 'birthdate' => power_of_attorney.auth_headers['va_eauth_birthdate'] - } - }) + def data(power_of_attorney, form_number, rep) + res = power_of_attorney.form_data + res.deep_merge!(veteran_attributes(power_of_attorney)) signatures = if form_number == '2122A' - individual_signatures(power_of_attorney) + individual_signatures(power_of_attorney, rep) else - organization_signatures(power_of_attorney) + organization_signatures(power_of_attorney, rep) end + res.deep_merge!({ (form_number == '2122A' ? 'representative' : 'serviceOrganization') => { + 'firstName' => rep.first_name, + 'lastName' => rep.last_name + } }) + + res.deep_merge!(organization_name(power_of_attorney)) if form_number == '2122' + res.merge!({ 'text_signatures' => signatures }) res end - def organization_signatures(power_of_attorney) - first_name = power_of_attorney.form_data['serviceOrganization']['firstName'] - last_name = power_of_attorney.form_data['serviceOrganization']['lastName'] + def veteran_attributes(power_of_attorney) + { + 'veteran' => { + 'firstName' => power_of_attorney.auth_headers['va_eauth_firstName'], + 'lastName' => power_of_attorney.auth_headers['va_eauth_lastName'], + 'ssn' => power_of_attorney.auth_headers['va_eauth_pnid'], + 'birthdate' => power_of_attorney.auth_headers['va_eauth_birthdate'] + } + } + end + + def organization_signatures(power_of_attorney, rep) + first_name, last_name = veteran_or_claimant_signature(power_of_attorney) { 'page2' => [ { - 'signature' => "#{power_of_attorney.auth_headers['va_eauth_firstName']} " \ - "#{power_of_attorney.auth_headers['va_eauth_lastName']} - signed via api.va.gov", + 'signature' => "#{first_name} " \ + "#{last_name} - signed via api.va.gov", 'x' => 35, 'y' => 240 }, { - 'signature' => "#{first_name} #{last_name} - signed via api.va.gov", + 'signature' => "#{rep.first_name} #{rep.last_name} - signed via api.va.gov", 'x' => 35, 'y' => 200 } @@ -84,45 +95,46 @@ def organization_signatures(power_of_attorney) } end - def individual_signatures(power_of_attorney) - first_name = power_of_attorney.form_data['representative']['firstName'] - last_name = power_of_attorney.form_data['representative']['lastName'] + def individual_signatures(power_of_attorney, rep) + first_name, last_name = veteran_or_claimant_signature(power_of_attorney) { - 'page1' => individual_page1_signatures(power_of_attorney, first_name, last_name), - 'page2' => individual_page2_signatures(power_of_attorney, first_name, last_name) + 'page2' => [ + { + 'signature' => "#{first_name} #{last_name} - signed via api.va.gov", + 'x' => 35, + 'y' => 306 + }, + { + 'signature' => "#{rep.first_name} #{rep.last_name} - signed via api.va.gov", + 'x' => 35, + 'y' => 200 + } + ] } end - def individual_page1_signatures(power_of_attorney, first_name, last_name) - [ - { - 'signature' => "#{power_of_attorney.auth_headers['va_eauth_firstName']} " \ - "#{power_of_attorney.auth_headers['va_eauth_lastName']} - signed via api.va.gov", - 'x' => 35, - 'y' => 73 - }, - { - 'signature' => "#{first_name} #{last_name} - signed via api.va.gov", - 'x' => 35, - 'y' => 100 - } - ] + def veteran_or_claimant_signature(power_of_attorney) + claimant = power_of_attorney.form_data['claimant'].present? + if claimant + first_name = power_of_attorney.form_data['claimant']['firstName'] + last_name = power_of_attorney.form_data['claimant']['lastName'] + else + first_name = power_of_attorney.auth_headers['va_eauth_firstName'] + last_name = power_of_attorney.auth_headers['va_eauth_lastName'] + end + [first_name, last_name] end - def individual_page2_signatures(power_of_attorney, first_name, last_name) - [ - { - 'signature' => "#{power_of_attorney.auth_headers['va_eauth_firstName']} " \ - "#{power_of_attorney.auth_headers['va_eauth_lastName']} - signed via api.va.gov", - 'x' => 35, - 'y' => 306 - }, - { - 'signature' => "#{first_name} #{last_name} - signed via api.va.gov", - 'x' => 35, - 'y' => 200 + def organization_name(power_of_attorney) + poa_code = power_of_attorney.form_data.dig('serviceOrganization', 'poaCode') + + name = ::Veteran::Service::Organization.find_by(poa: poa_code).name + + { + 'serviceOrganization' => { + 'organizationName' => name } - ] + } 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 b8b42e0fc36..9664e172d84 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", @@ -8751,6 +8751,14 @@ "pointer": "data/attributes/representative" } }, + { + "title": "Unprocessable entity", + "detail": "The property /representative did not contain the required key registrationNumber", + "status": "422", + "source": { + "pointer": "data/attributes/representative" + } + }, { "title": "Unprocessable entity", "detail": "The property / did not contain the required key veteran", @@ -8815,7 +8823,7 @@ { "title": "Resource not found", "status": "404", - "detail": "Could not find an Accredited Representative with code: 083", + "detail": "Could not find an Accredited Representative with registration number: 67890 and poa 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!'" } @@ -9022,23 +9030,10 @@ "type": "object", "additionalProperties": false, "properties": { - "firstName": { - "description": "First name of Claimant.", - "type": "string", - "example": "John", - "maxLength": 12 - }, - "middleInitial": { - "description": "Middle initial of Claimant.", - "type": "string", - "example": "M", - "maxLength": 1 - }, - "lastName": { - "description": "Last name of Claimant.", + "claimantId": { "type": "string", - "example": "Dow", - "maxLength": 18 + "example": "123456789", + "description": "Id of the claimant." }, "address": { "type": "object", @@ -9136,8 +9131,7 @@ "additionalProperties": false, "required": [ "poaCode", - "firstName", - "lastName", + "registrationNumber", "type" ], "properties": { @@ -9146,15 +9140,10 @@ "type": "string", "example": "A1Q" }, - "firstName": { - "description": "First Name of the representative.", + "registrationNumber": { + "description": "Registration Number of representative.", "type": "string", - "example": "John" - }, - "lastName": { - "description": "Last Name of the representative", - "type": "string", - "example": "Doe" + "example": "12345" }, "type": { "description": "Type of individual representative", @@ -9209,11 +9198,6 @@ "example": "6789" } } - }, - "organizationName": { - "description": "Name of the service organization.", - "type": "string", - "example": "I help vets LLC." } } }, @@ -9267,8 +9251,7 @@ }, "representative": { "poaCode": "083", - "firstName": "my", - "lastName": "name", + "registrationNumber": "67890", "type": "ATTORNEY", "address": { "addressLine1": "123", @@ -9336,7 +9319,7 @@ "application/json": { "example": { "data": { - "id": "45bf00c5-36b3-4157-9620-6a89d5bd37ae", + "id": "a7114d11-8ffd-4545-ad99-d70e74991e11", "type": "organization", "attributes": { "code": "083", @@ -9464,6 +9447,14 @@ "pointer": "data/attributes/serviceOrganization" } }, + { + "title": "Unprocessable entity", + "detail": "The property /serviceOrganization did not contain the required key registrationNumber", + "status": "422", + "source": { + "pointer": "data/attributes/serviceOrganization" + } + }, { "title": "Unprocessable entity", "detail": "The property / did not contain the required key veteran", @@ -9471,6 +9462,14 @@ "source": { "pointer": "data/attributes/" } + }, + { + "title": "Unprocessable entity", + "detail": "The property /serviceOrganization/withoutPoaCode is not defined on the schema. Additional properties are not allowed", + "status": "422", + "source": { + "pointer": "data/attributes/serviceOrganization/withoutPoaCode" + } } ] }, @@ -9528,7 +9527,7 @@ { "title": "Resource not found", "status": "404", - "detail": "Could not find an Organization with code: 083", + "detail": "Could not find an Accredited Representative with registration number: 67890 and poa 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!'" } @@ -9709,6 +9708,11 @@ "type": "string", "pattern": "^\\d{9}?$", "example": "123456789" + }, + "insuranceNumber": { + "type": "string", + "maxLength": 60, + "description": "Veteran's insurance number, if applicable. Include letter prefix." } } }, @@ -9716,20 +9720,10 @@ "type": "object", "additionalProperties": false, "properties": { - "firstName": { - "description": "First name of Claimant.", + "claimantId": { "type": "string", - "example": "John" - }, - "middleInitial": { - "description": "Middle initial of Claimant.", - "type": "string", - "example": "M" - }, - "lastName": { - "description": "Last name of Claimant.", - "type": "string", - "example": "Dow" + "example": "123456789", + "description": "Id of the claimant." }, "address": { "type": "object", @@ -9829,7 +9823,8 @@ "type": "object", "additionalProperties": false, "required": [ - "poaCode" + "poaCode", + "registrationNumber" ], "properties": { "poaCode": { @@ -9837,20 +9832,10 @@ "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", + "registrationNumber": { + "description": "Registration Number of representative.", "type": "string", - "example": "Doe" + "example": "12345" }, "jobTitle": { "description": "Job title of the representative.", @@ -9911,7 +9896,8 @@ } }, "serviceOrganization": { - "poaCode": "083" + "poaCode": "083", + "registrationNumber": "67890" } } } @@ -10088,6 +10074,14 @@ "pointer": "data/attributes/representative" } }, + { + "title": "Unprocessable entity", + "detail": "The property /representative did not contain the required key registrationNumber", + "status": "422", + "source": { + "pointer": "data/attributes/representative" + } + }, { "title": "Unprocessable entity", "detail": "The property / did not contain the required key veteran", @@ -10152,7 +10146,7 @@ { "title": "Resource not found", "status": "404", - "detail": "Could not find an Accredited Representative with code: 083", + "detail": "Could not find an Accredited Representative with registration number: 67890 and poa 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!'" } @@ -10359,23 +10353,10 @@ "type": "object", "additionalProperties": false, "properties": { - "firstName": { - "description": "First name of Claimant.", - "type": "string", - "example": "John", - "maxLength": 12 - }, - "middleInitial": { - "description": "Middle initial of Claimant.", - "type": "string", - "example": "M", - "maxLength": 1 - }, - "lastName": { - "description": "Last name of Claimant.", + "claimantId": { "type": "string", - "example": "Dow", - "maxLength": 18 + "example": "123456789", + "description": "Id of the claimant." }, "address": { "type": "object", @@ -10473,8 +10454,7 @@ "additionalProperties": false, "required": [ "poaCode", - "firstName", - "lastName", + "registrationNumber", "type" ], "properties": { @@ -10483,15 +10463,10 @@ "type": "string", "example": "A1Q" }, - "firstName": { - "description": "First Name of the representative.", + "registrationNumber": { + "description": "Registration Number of representative.", "type": "string", - "example": "John" - }, - "lastName": { - "description": "Last Name of the representative", - "type": "string", - "example": "Doe" + "example": "12345" }, "type": { "description": "Type of individual representative", @@ -10546,11 +10521,6 @@ "example": "6789" } } - }, - "organizationName": { - "description": "Name of the service organization.", - "type": "string", - "example": "I help vets LLC." } } }, @@ -10604,8 +10574,7 @@ }, "representative": { "poaCode": "083", - "firstName": "my", - "lastName": "name", + "registrationNumber": "67890", "type": "ATTORNEY", "address": { "addressLine1": "123", @@ -10792,6 +10761,14 @@ "pointer": "data/attributes/serviceOrganization" } }, + { + "title": "Unprocessable entity", + "detail": "The property /serviceOrganization did not contain the required key registrationNumber", + "status": "422", + "source": { + "pointer": "data/attributes/serviceOrganization" + } + }, { "title": "Unprocessable entity", "detail": "The property / did not contain the required key veteran", @@ -10799,6 +10776,14 @@ "source": { "pointer": "data/attributes/" } + }, + { + "title": "Unprocessable entity", + "detail": "The property /serviceOrganization/withoutPoaCode is not defined on the schema. Additional properties are not allowed", + "status": "422", + "source": { + "pointer": "data/attributes/serviceOrganization/withoutPoaCode" + } } ] }, @@ -10856,7 +10841,7 @@ { "title": "Resource not found", "status": "404", - "detail": "Could not find an Organization with code: 083", + "detail": "Could not find an Accredited Representative with registration number: 67890 and poa 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!'" } @@ -11037,6 +11022,11 @@ "type": "string", "pattern": "^\\d{9}?$", "example": "123456789" + }, + "insuranceNumber": { + "type": "string", + "maxLength": 60, + "description": "Veteran's insurance number, if applicable. Include letter prefix." } } }, @@ -11044,20 +11034,10 @@ "type": "object", "additionalProperties": false, "properties": { - "firstName": { - "description": "First name of Claimant.", + "claimantId": { "type": "string", - "example": "John" - }, - "middleInitial": { - "description": "Middle initial of Claimant.", - "type": "string", - "example": "M" - }, - "lastName": { - "description": "Last name of Claimant.", - "type": "string", - "example": "Dow" + "example": "123456789", + "description": "Id of the claimant." }, "address": { "type": "object", @@ -11157,7 +11137,8 @@ "type": "object", "additionalProperties": false, "required": [ - "poaCode" + "poaCode", + "registrationNumber" ], "properties": { "poaCode": { @@ -11165,20 +11146,10 @@ "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", + "registrationNumber": { + "description": "Registration Number of representative.", "type": "string", - "example": "Doe" + "example": "12345" }, "jobTitle": { "description": "Job title of the representative.", @@ -11239,7 +11210,8 @@ } }, "serviceOrganization": { - "poaCode": "083" + "poaCode": "083", + "registrationNumber": "67890" } } } @@ -11307,11 +11279,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" @@ -11547,4 +11519,4 @@ } } ] -} \ No newline at end of file +} 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..64add52afd5 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,2928 @@ "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 /representative did not contain the required key registrationNumber", + "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 registration number: 67890 and poa 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", + "registrationNumber", + "type" + ], + "properties": { + "poaCode": { + "description": "The POA code of the representative.", + "type": "string", + "example": "A1Q" + }, + "registrationNumber": { + "description": "Registration Number of representative.", + "type": "string", + "example": "12345" + }, + "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" + } + } + } + } + }, + "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", + "registrationNumber": "67890", + "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 /serviceOrganization did not contain the required key registrationNumber", + "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/" + } + }, + { + "title": "Unprocessable entity", + "detail": "The property /serviceOrganization/withoutPoaCode is not defined on the schema. Additional properties are not allowed", + "status": "422", + "source": { + "pointer": "data/attributes/serviceOrganization/withoutPoaCode" + } + } + ] + }, + "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 registration number: 67890 and poa 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" + }, + "insuranceNumber": { + "type": "string", + "maxLength": 60, + "description": "Veteran's insurance number, if applicable. Include letter prefix." + } + } + }, + "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", + "registrationNumber" + ], + "properties": { + "poaCode": { + "description": "The POA code of the organization.", + "type": "string", + "example": "A1Q" + }, + "registrationNumber": { + "description": "Registration Number of representative.", + "type": "string", + "example": "12345" + }, + "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", + "registrationNumber": "67890" + } + } + } + } + } + } + }, + "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 /representative did not contain the required key registrationNumber", + "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 registration number: 67890 and poa 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", + "registrationNumber", + "type" + ], + "properties": { + "poaCode": { + "description": "The POA code of the representative.", + "type": "string", + "example": "A1Q" + }, + "registrationNumber": { + "description": "Registration Number of representative.", + "type": "string", + "example": "12345" + }, + "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" + } + } + } + } + }, + "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", + "registrationNumber": "67890", + "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 /serviceOrganization did not contain the required key registrationNumber", + "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/" + } + }, + { + "title": "Unprocessable entity", + "detail": "The property /serviceOrganization/withoutPoaCode is not defined on the schema. Additional properties are not allowed", + "status": "422", + "source": { + "pointer": "data/attributes/serviceOrganization/withoutPoaCode" + } + } + ] + }, + "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 registration number: 67890 and poa 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" + }, + "insuranceNumber": { + "type": "string", + "maxLength": 60, + "description": "Veteran's insurance number, if applicable. Include letter prefix." + } + } + }, + "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", + "registrationNumber" + ], + "properties": { + "poaCode": { + "description": "The POA code of the organization.", + "type": "string", + "example": "A1Q" + }, + "registrationNumber": { + "description": "Registration Number of representative.", + "type": "string", + "example": "12345" + }, + "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", + "registrationNumber": "67890" + } + } + } + } + } + } + }, + "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'" } } ] @@ -8598,4 +11519,4 @@ } } ] -} \ No newline at end of file +} diff --git a/modules/claims_api/config/schemas/v2/2122.json b/modules/claims_api/config/schemas/v2/2122.json index 79f8511c99b..9ecceb725a2 100644 --- a/modules/claims_api/config/schemas/v2/2122.json +++ b/modules/claims_api/config/schemas/v2/2122.json @@ -25,7 +25,7 @@ "stateCode", "zipCode" ], - "properties" : { + "properties": { "addressLine1": { "description": "Street address with number and name.", "type": "string", @@ -90,7 +90,7 @@ "type": "string", "pattern": "^[0-9]{1,14}$", "example": "555-5555" }, - "phoneNumberExt": { "type": "string", "pattern": "^[a-zA-Z0-9]{1,10}$" } + "phoneNumberExt": { "type": "string", "pattern": "^[a-zA-Z0-9]{1,10}$" } } }, "email": { @@ -105,6 +105,11 @@ "type": "string", "pattern": "^\\d{9}?$", "example": "123456789" + }, + "insuranceNumber": { + "type": "string", + "maxLength": 60, + "description": "Veteran's insurance number, if applicable. Include letter prefix." } } }, @@ -112,25 +117,15 @@ "type": "object", "additionalProperties": false, "properties": { - "firstName": { - "description": "First name of Claimant.", - "type": "string", - "example": "John" - }, - "middleInitial": { - "description": "Middle initial of Claimant.", - "type": "string", - "example": "M" - }, - "lastName": { - "description": "Last name of Claimant.", + "claimantId": { "type": "string", - "example": "Dow" + "example": "123456789", + "description": "Id of the claimant." }, "address": { "type": "object", "additionalProperties": false, - "properties" : { + "properties": { "addressLine1": { "description": "Street address with number and name. Required if claimant information provided.", "type": "string", @@ -182,7 +177,7 @@ "phoneNumber" ], "properties": { - "countryCode": { "type": "string", "pattern": "^[0-9]+$" }, + "countryCode": { "type": "string", "pattern": "^[0-9]+$" }, "areaCode": { "description": "Area code of the phone number.", "type": "string", "pattern": "^[2-9][0-9]{2}$", @@ -193,7 +188,7 @@ "type": "string", "pattern": "^[0-9]{1,14}$", "example": "555-5555" }, - "phoneNumberExt": { "type": "string", "pattern": "^[a-zA-Z0-9]{1,10}$" } + "phoneNumberExt": { "type": "string", "pattern": "^[a-zA-Z0-9]{1,10}$" } } }, "email": { @@ -215,7 +210,8 @@ "type": "object", "additionalProperties": false, "required": [ - "poaCode" + "poaCode", + "registrationNumber" ], "properties": { "poaCode": { @@ -223,20 +219,10 @@ "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", + "registrationNumber": { + "description": "Registration Number of representative.", "type": "string", - "example": "Doe" + "example": "12345" }, "jobTitle": { "description": "Job title of the representative.", diff --git a/modules/claims_api/config/schemas/v2/2122a.json b/modules/claims_api/config/schemas/v2/2122a.json index 6a90f806fd4..aae8b1469a4 100644 --- a/modules/claims_api/config/schemas/v2/2122a.json +++ b/modules/claims_api/config/schemas/v2/2122a.json @@ -128,23 +128,10 @@ "type": "object", "additionalProperties": false, "properties": { - "firstName": { - "description": "First name of Claimant.", + "claimantId": { "type": "string", - "example": "John", - "maxLength": 12 - }, - "middleInitial": { - "description": "Middle initial of Claimant.", - "type": "string", - "example": "M", - "maxLength": 1 - }, - "lastName": { - "description": "Last name of Claimant.", - "type": "string", - "example": "Dow", - "maxLength": 18 + "example": "123456789", + "description": "Id of the claimant." }, "address": { "type": "object", @@ -234,8 +221,7 @@ "additionalProperties": false, "required": [ "poaCode", - "firstName", - "lastName", + "registrationNumber", "type" ], "properties": { @@ -244,15 +230,10 @@ "type": "string", "example": "A1Q" }, - "firstName": { - "description": "First Name of the representative.", + "registrationNumber": { + "description": "Registration Number of representative.", "type": "string", - "example": "John" - }, - "lastName": { - "description": "Last Name of the representative", - "type": "string", - "example": "Doe" + "example": "12345" }, "type": { "description": "Type of individual representative", @@ -307,11 +288,6 @@ "example": "6789" } } - }, - "organizationName": { - "description": "Name of the service organization.", - "type": "string", - "example": "I help vets LLC." } } }, diff --git a/modules/claims_api/lib/bgs_service/local_bgs.rb b/modules/claims_api/lib/bgs_service/local_bgs.rb index 9e99d25c9cb..e260e9f9712 100644 --- a/modules/claims_api/lib/bgs_service/local_bgs.rb +++ b/modules/claims_api/lib/bgs_service/local_bgs.rb @@ -14,9 +14,30 @@ module ClaimsApi class LocalBGS attr_accessor :external_uid, :external_key + # rubocop:disable Metrics/MethodLength def initialize(external_uid:, external_key:) + @client_ip = + if Rails.env.test? + # For all intents and purposes, BGS behaves identically no matter what + # IP we provide it. So in a test environment, let's just give it a + # fake so that cassette matching isn't defeated on CI and everyone's + # computer. + '127.0.0.1' + else + Socket + .ip_address_list + .detect(&:ipv4_private?) + .ip_address + end + + @ssl_verify_mode = + if Settings.bgs.ssl_verify_mode == 'none' + OpenSSL::SSL::VERIFY_NONE + else + OpenSSL::SSL::VERIFY_PEER + end + @application = Settings.bgs.application - @client_ip = Socket.ip_address_list.detect(&:ipv4_private?).ip_address @client_station_id = Settings.bgs.client_station_id @client_username = Settings.bgs.client_username @env = Settings.bgs.env @@ -25,16 +46,19 @@ def initialize(external_uid:, external_key:) @external_uid = external_uid || Settings.bgs.external_uid @external_key = external_key || Settings.bgs.external_key @forward_proxy_url = Settings.bgs.url - @ssl_verify_mode = Settings.bgs.ssl_verify_mode == 'none' ? OpenSSL::SSL::VERIFY_NONE : OpenSSL::SSL::VERIFY_PEER @timeout = Settings.bgs.timeout || 120 end + # rubocop:enable Metrics/MethodLength 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( @@ -211,15 +235,26 @@ def header # rubocop:disable Metrics/MethodLength header.to_s end - def full_body(action:, body:, namespace:) + def full_body(action:, body:, namespace:, namespaces:) + namespaces = + namespaces.map do |aliaz, path| + uri = URI(namespace) + uri.path = path + %(xmlns:#{aliaz}="#{uri}") + end + body = Nokogiri::XML::DocumentFragment.parse <<~EOXML - + #{header} - - #{body} - + #{body} EOXML @@ -250,6 +285,10 @@ def parsed_response(res, action, key = nil) end end + def namespaces + {} + end + def make_request(endpoint:, action:, body:, key: nil) # rubocop:disable Metrics/MethodLength connection = log_duration event: 'establish_ssl_connection' do Faraday::Connection.new(ssl: { verify_mode: @ssl_verify_mode }) do |f| @@ -263,16 +302,18 @@ def make_request(endpoint:, action:, body:, key: nil) # rubocop:disable Metrics/ wsdl = log_duration(event: 'connection_wsdl_get', endpoint:) do connection.get("#{Settings.bgs.url}/#{endpoint}?WSDL") end - target_namespace = Hash.from_xml(wsdl.body).dig('definitions', 'targetNamespace') + + url = "#{Settings.bgs.url}/#{endpoint}" + namespace = Hash.from_xml(wsdl.body).dig('definitions', 'targetNamespace').to_s + body = full_body(action:, body:, namespace:, namespaces:) + headers = { + 'Content-Type' => 'text/xml;charset=UTF-8', + 'Host' => "#{@env}.vba.va.gov", + 'Soapaction' => %("#{action}") + } + response = log_duration(event: 'connection_post', endpoint:, action:) do - connection.post("#{Settings.bgs.url}/#{endpoint}", full_body(action:, - body:, - namespace: target_namespace), - { - 'Content-Type' => 'text/xml;charset=UTF-8', - 'Host' => "#{@env}.vba.va.gov", - 'Soapaction' => "\"#{action}\"" - }) + connection.post(url, body, headers) end rescue Faraday::TimeoutError, Faraday::ConnectionFailed => e ClaimsApi::Logger.log('local_bgs', @@ -336,5 +377,31 @@ def transform_bgs_claims_to_evss(claims) def to_camelcase(claim:) claim.deep_transform_keys { |k| k.to_s.camelize(:lower) } end + + def convert_nil_values(options) + arg_strg = '' + options.each do |option| + arg = option[0].to_s.camelize(:lower) + arg_strg += (option[1].nil? ? "<#{arg} xsi:nil='true'/>" : "<#{arg}>#{option[1]}") + end + arg_strg + end + + def validate_opts!(opts, required_keys) + keys = opts.keys.map(&:to_s) + required_keys = required_keys.map(&:to_s) + missing_keys = required_keys - keys + raise ArgumentError, "Missing required keys: #{missing_keys.join(', ')}" if missing_keys.present? + end + + def jrn + { + jrn_dt: Time.current.iso8601, + jrn_lctn_id: Settings.bgs.client_station_id, + jrn_status_type_cd: 'U', + jrn_user_id: Settings.bgs.client_username, + jrn_obj_id: Settings.bgs.application + } + end end end diff --git a/modules/claims_api/lib/bgs_service/manage_representative_service.rb b/modules/claims_api/lib/bgs_service/manage_representative_service.rb new file mode 100644 index 00000000000..490c6143c89 --- /dev/null +++ b/modules/claims_api/lib/bgs_service/manage_representative_service.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require_relative 'manage_representative_service/read_poa_request' +require_relative 'manage_representative_service/update_poa_request' + +module ClaimsApi + class ManageRepresentativeService < ClaimsApi::LocalBGS + def endpoint + 'VDC/ManageRepresentativeService' + end + + def namespaces + { + 'data' => '/data' + } + end + end +end diff --git a/modules/claims_api/lib/bgs_service/manage_representative_service/read_poa_request.rb b/modules/claims_api/lib/bgs_service/manage_representative_service/read_poa_request.rb new file mode 100644 index 00000000000..5c668e78869 --- /dev/null +++ b/modules/claims_api/lib/bgs_service/manage_representative_service/read_poa_request.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module ClaimsApi + class ManageRepresentativeService < ClaimsApi::LocalBGS + # rubocop:disable Metrics/MethodLength + def read_poa_request(poa_codes: nil, statuses: nil) + builder = + Nokogiri::XML::Builder.new(namespace_inheritance: false) do |xml| + # Need to declare an arbitrary root element with placeholder + # namespace in order to leverage namespaced tag building. The root + # element itself is later ignored and only used for its contents. + # https://nokogiri.org/rdoc/Nokogiri/XML/Builder.html#method-i-5B-5D + xml.root('xmlns:data' => 'placeholder') do + if statuses + xml['data'].SecondaryStatusList do + statuses.each do |status| + xml.SecondaryStatus(status) + end + end + end + + if poa_codes + xml['data'].POACodeList do + poa_codes.each do |poa_code| + xml.POACode(poa_code) + end + end + end + end + end + + make_request( + endpoint:, + action: 'readPOARequest', + body: builder.doc.at('root').children.to_xml, + key: 'POARequestRespondReturnVO' + ) + end + # rubocop:enable Metrics/MethodLength + end +end diff --git a/modules/claims_api/lib/bgs_service/manage_representative_service/update_poa_request.rb b/modules/claims_api/lib/bgs_service/manage_representative_service/update_poa_request.rb new file mode 100644 index 00000000000..3710688cde8 --- /dev/null +++ b/modules/claims_api/lib/bgs_service/manage_representative_service/update_poa_request.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module ClaimsApi + class ManageRepresentativeService < ClaimsApi::LocalBGS + def update_poa_request(representative:, proc_id:) + body = + Nokogiri::XML::DocumentFragment.parse <<~EOXML + + #{representative.first_name} + #{representative.last_name} + #{Time.current.iso8601} + #{proc_id} + + obsolete + + EOXML + + make_request( + endpoint:, + action: 'updatePOARequest', + body: body.to_s, + key: 'POARequestUpdate' + ) + end + end +end diff --git a/modules/claims_api/lib/bgs_service/vnp_atchms_service.rb b/modules/claims_api/lib/bgs_service/vnp_atchms_service.rb new file mode 100644 index 00000000000..548350ef459 --- /dev/null +++ b/modules/claims_api/lib/bgs_service/vnp_atchms_service.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module ClaimsApi + class VnpAtchmsService < ClaimsApi::LocalBGS + # Takes an object with a minimum of (other fields are camelized and passed to BGS): + # vnp_proc_id: BGS procID + # atchms_file_nm: File name + # atchms_descp: File description + # atchms_txt: Base64 encoded file or file path + def vnp_atchms_create(opts) + validate_opts! opts, %w[vnp_proc_id atchms_file_nm atchms_descp atchms_txt] + + convert_file! opts + opts = jrn.merge(opts) + arg_strg = convert_nil_values(opts) + body = Nokogiri::XML::DocumentFragment.parse "#{arg_strg}" + make_request(endpoint: 'VnpAtchmsWebServiceBean/VnpAtchmsService', action: 'vnpAtchmsCreate', body:, + key: 'return') + end + + private + + def convert_file!(opts) + opts.deep_symbolize_keys! + txt = opts[:atchms_txt] + raise ArgumentError, 'File must be a string' unless txt.is_a? String + + if File.exist?(txt) + file = File.read(txt) + opts[:atchms_txt] = Base64.encode64 file + end + end + end +end diff --git a/modules/claims_api/lib/bgs_service/vnp_person_service.rb b/modules/claims_api/lib/bgs_service/vnp_person_service.rb new file mode 100644 index 00000000000..3c47d73facb --- /dev/null +++ b/modules/claims_api/lib/bgs_service/vnp_person_service.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module ClaimsApi + class VnpPersonService < ClaimsApi::LocalBGS + # Takes an object with a minimum of (other fields are camelized and passed to BGS): + # vnp_proc_id: BGS procID + # vnp_ptcpnt_id: Veteran's participant id + # first_nm: Veteran's first name + # last_nm: Veteran's last name + def vnp_person_create(opts) + validate_opts! opts, %w[vnp_proc_id vnp_ptcpnt_id first_nm last_nm] + + opts = jrn.merge(opts) + arg_strg = convert_nil_values(opts) + body = Nokogiri::XML::DocumentFragment.parse <<~EOXML + #{arg_strg} + EOXML + + make_request(endpoint: 'VnpPersonWebServiceBean/VnpPersonService', action: 'vnpPersonCreate', body:, + key: 'return') + end + end +end diff --git a/modules/claims_api/lib/bgs_service/vnp_proc_form_service.rb b/modules/claims_api/lib/bgs_service/vnp_proc_form_service.rb new file mode 100644 index 00000000000..807af6c3047 --- /dev/null +++ b/modules/claims_api/lib/bgs_service/vnp_proc_form_service.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module ClaimsApi + class VnpProcFormService < ClaimsApi::LocalBGS + FORM_TYPE_CD = '21-22' + + def vnp_proc_form_create(options) + vnp_proc_id = options[:vnp_proc_id] + options.delete(:vnp_proc_id) + arg_strg = convert_nil_values(options) + body = Nokogiri::XML::DocumentFragment.parse <<~EOXML + + + #{vnp_proc_id} + #{FORM_TYPE_CD} + + #{arg_strg} + + EOXML + + make_request(endpoint: 'VnpProcFormWebServiceBean/VnpProcFormService', + action: 'vnpProcFormCreate', body:, key: 'return') + end + end +end diff --git a/modules/claims_api/lib/bgs_service/vnp_proc_service_v2.rb b/modules/claims_api/lib/bgs_service/vnp_proc_service_v2.rb new file mode 100644 index 00000000000..e4d9eb48b36 --- /dev/null +++ b/modules/claims_api/lib/bgs_service/vnp_proc_service_v2.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module ClaimsApi + class VnpProcServiceV2 < ClaimsApi::LocalBGS + PROC_TYPE_CD = 'POAAUTHZ' + + def vnp_proc_create + body = Nokogiri::XML::DocumentFragment.parse <<~EOXML + + + #{PROC_TYPE_CD} + + + EOXML + + make_request(endpoint: 'VnpProcWebServiceBeanV2/VnpProcServiceV2', action: 'vnpProcCreate', body:, key: 'return') + end + end +end diff --git a/modules/claims_api/lib/bgs_service/vnp_ptcpnt_phone_service.rb b/modules/claims_api/lib/bgs_service/vnp_ptcpnt_phone_service.rb new file mode 100644 index 00000000000..3621b59aa44 --- /dev/null +++ b/modules/claims_api/lib/bgs_service/vnp_ptcpnt_phone_service.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module ClaimsApi + class VnpPtcpntPhoneService < ClaimsApi::LocalBGS + # vnpPtcpntPhoneCreate - This service is used to create VONAPP participant phone information + DEFAULT_TYPE = 'Daytime' # Daytime and Nighttime are the allowed values + + def vnp_ptcpnt_phone_create(options) + request_body = construct_body(options) + body = Nokogiri::XML::DocumentFragment.parse <<~EOXML + + + EOXML + + request_body.each do |k, z| + node = Nokogiri::XML::Node.new k.to_s, body + node.content = z.to_s + opt = body.at('arg0') + node.parent = opt + end + + make_request(endpoint: 'VnpPtcpntPhoneWebServiceBean/VnpPtcpntPhoneService', action: 'vnpPtcpntPhoneCreate', + body:, key: 'return') + end + + private + + def construct_body(options) + { + vnpProcId: options[:vnp_proc_id], + vnpPtcpntId: options[:vnp_ptcpnt_id], + phoneTypeNm: options[:phone_type_nm] || DEFAULT_TYPE, + phoneNbr: options[:phone_nbr], + efctvDt: options[:efctv_dt] || Time.current.iso8601 + } + end + end +end diff --git a/modules/claims_api/lib/bgs_service/vnp_ptcpnt_service.rb b/modules/claims_api/lib/bgs_service/vnp_ptcpnt_service.rb new file mode 100644 index 00000000000..e87a72269a6 --- /dev/null +++ b/modules/claims_api/lib/bgs_service/vnp_ptcpnt_service.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module ClaimsApi + class VnpPtcpntService < ClaimsApi::LocalBGS + # vnpPtcpntCreate - This service is used to create VONAPP participant information + def vnp_ptcpnt_create(options) + arg_strg = convert_nil_values(options) + body = Nokogiri::XML::DocumentFragment.parse <<~EOXML + + #{arg_strg} + + EOXML + + make_request(endpoint: 'VnpPtcpntWebServiceBean/VnpPtcpntService', action: 'vnpPtcpntCreate', body:, + key: 'return') + end + end +end diff --git a/modules/claims_api/lib/claims_api/error/soap_error_handler.rb b/modules/claims_api/lib/claims_api/error/soap_error_handler.rb index 2840ead9d23..33fef287e84 100644 --- a/modules/claims_api/lib/claims_api/error/soap_error_handler.rb +++ b/modules/claims_api/lib/claims_api/error/soap_error_handler.rb @@ -15,9 +15,10 @@ def handle_errors(response) end def get_fault_info - @fault_code = @hash&.dig('Envelope', 'Body', 'Fault', 'faultcode')&.split(':')&.dig(1) - @fault_string = @hash&.dig('Envelope', 'Body', 'Fault', 'faultstring') - @fault_message = @hash&.dig('Envelope', 'Body', 'Fault', 'detail', 'MessageException') + fault = @hash&.dig('Envelope', 'Body', 'Fault') + @fault_code = fault&.dig('faultcode')&.split(':')&.dig(1) + @fault_string = fault&.dig('faultstring') + @fault_message = fault&.dig('detail', 'MessageException') || fault&.dig('detail', 'MessageFaultException') return {} if @fault_string.include?('IntentToFileWebService') && @fault_string.include?('not found') get_exception diff --git a/modules/claims_api/lib/claims_api/v2/poa_pdf_constructor/organization.rb b/modules/claims_api/lib/claims_api/v2/poa_pdf_constructor/organization.rb index 66fe7ed47e7..961312c8823 100644 --- a/modules/claims_api/lib/claims_api/v2/poa_pdf_constructor/organization.rb +++ b/modules/claims_api/lib/claims_api/v2/poa_pdf_constructor/organization.rb @@ -76,6 +76,8 @@ def page1_options(data) "#{base_form}.DOBmonth[0]": data.dig('veteran', 'birthdate').split('-').second, "#{base_form}.DOBday[0]": data.dig('veteran', 'birthdate').split('-').last.first(2), "#{base_form}.DOByear[0]": data.dig('veteran', 'birthdate').split('-').first, + # Item 5 + "#{base_form}.InsuranceNumber_s[0]": data.dig('veteran', 'insuranceNumber'), # Item 7 "#{base_form}.Veterans_MailingAddress_NumberAndStreet[0]": data.dig('veteran', 'address', 'addressLine1'), "#{base_form}.Claimants_MailingAddress_ApartmentOrUnitNumber[1]": data.dig('veteran', 'address', 'addressLine2'), diff --git a/modules/claims_api/spec/concerns/claims_api/poa_verification_spec.rb b/modules/claims_api/spec/concerns/claims_api/poa_verification_spec.rb new file mode 100644 index 00000000000..5e35dc9405a --- /dev/null +++ b/modules/claims_api/spec/concerns/claims_api/poa_verification_spec.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true + +require 'rails_helper' + +class FakeController < ApplicationController + include ClaimsApi::PoaVerification + + def initialize + super + @current_user = ClaimsApi::ClaimsUser.new('test') + @current_user.first_name_last_name('John', 'Doe') + @current_user.middle_name = 'Alexander' + end +end + +describe FakeController do + context 'validating poa_code for current_user' do + let(:poa_code) { '091' } + let(:first_name) { 'John' } + let(:last_name) { 'Doe' } + let(:phone) { '123-456-7890' } + + context 'when no rep is found' do + it 'returns false' do + ret = subject.valid_poa_code_for_current_user?(poa_code) + expect(ret).to eq(false) + end + end + + context 'when a single match is found by first/last name' do + context 'when the poa_code matches' do + before do + create(:representative, representative_id: '12345', first_name:, last_name:, + poa_codes: [poa_code], phone:) + end + + it 'returns true' do + ret = subject.valid_poa_code_for_current_user?(poa_code) + expect(ret).to eq(true) + end + end + + context 'when the poa_code does not match' do + before do + create(:representative, representative_id: '12345', first_name:, last_name:, + poa_codes: ['ABC'], phone:) + end + + it 'returns false' do + ret = subject.valid_poa_code_for_current_user?(poa_code) + expect(ret).to eq(false) + end + end + end + + context 'when multiple matches are found by first/last name' do + before do + create(:representative, representative_id: '12345', first_name:, last_name:, + middle_initial: 'A', poa_codes: ['091'], phone:) + create(:representative, representative_id: '123456', first_name:, last_name:, + middle_initial: 'B', poa_codes: ['091'], phone:) + end + + it 'searches with middle name' do + res = subject.valid_poa_code_for_current_user?(poa_code) + expect(res).to eq(true) + end + end + + context 'when multiple matches are found by first/last/middle name' do + context 'when a single rep is found' do + before do + create(:representative, representative_id: '12345', first_name:, last_name:, + middle_initial: 'A', poa_codes: ['ABC'], phone:) + create(:representative, representative_id: '123456', first_name:, last_name:, + middle_initial: 'B', poa_codes: ['DEF'], phone:) + create(:representative, representative_id: '1234567', first_name:, last_name:, + middle_initial: 'A', poa_codes: ['091'], phone:) + end + + it 'returns true' do + res = subject.valid_poa_code_for_current_user?(poa_code) + expect(res).to eq(true) + end + end + + context 'when multiple reps are found' do + before do + create(:representative, representative_id: '12345', first_name:, last_name:, + middle_initial: 'A', poa_codes: ['091'], phone:) + create(:representative, representative_id: '123456', first_name:, last_name:, + middle_initial: 'B', poa_codes: ['091'], phone:) + create(:representative, representative_id: '1234567', first_name:, last_name:, + middle_initial: 'A', poa_codes: ['091'], phone:) + end + + it 'raises "Ambiguous VSO Representative Results"' do + expect { subject.valid_poa_code_for_current_user?(poa_code) }.to raise_error(Common::Exceptions::Unauthorized) + end + end + end + end +end diff --git a/modules/claims_api/spec/fixtures/21-22/v2/signed_filled_final.pdf b/modules/claims_api/spec/fixtures/21-22/v2/signed_filled_final.pdf index 1a02b93180e..ee31a770503 100644 Binary files a/modules/claims_api/spec/fixtures/21-22/v2/signed_filled_final.pdf and b/modules/claims_api/spec/fixtures/21-22/v2/signed_filled_final.pdf differ diff --git a/modules/claims_api/spec/fixtures/21-22A/v2/signed_filled_final.pdf b/modules/claims_api/spec/fixtures/21-22A/v2/signed_filled_final.pdf index 859dd3cc56f..0f6615ce398 100644 Binary files a/modules/claims_api/spec/fixtures/21-22A/v2/signed_filled_final.pdf and b/modules/claims_api/spec/fixtures/21-22A/v2/signed_filled_final.pdf differ diff --git a/modules/claims_api/spec/fixtures/21-22A/v2/signed_filled_final_other_service_branch.pdf b/modules/claims_api/spec/fixtures/21-22A/v2/signed_filled_final_other_service_branch.pdf new file mode 100644 index 00000000000..166c29d891f Binary files /dev/null and b/modules/claims_api/spec/fixtures/21-22A/v2/signed_filled_final_other_service_branch.pdf differ 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/fixtures/v2/veterans/power_of_attorney/2122/invalid_poa.json b/modules/claims_api/spec/fixtures/v2/veterans/power_of_attorney/2122/invalid_poa.json index 0c0b79de3b7..c8e51c69b95 100644 --- a/modules/claims_api/spec/fixtures/v2/veterans/power_of_attorney/2122/invalid_poa.json +++ b/modules/claims_api/spec/fixtures/v2/veterans/power_of_attorney/2122/invalid_poa.json @@ -11,7 +11,8 @@ } }, "serviceOrganization": { - "poaCode": "aaa" + "poaCode": "aaa", + "registrationNumber": "67890" } } } diff --git a/modules/claims_api/spec/fixtures/v2/veterans/power_of_attorney/2122/invalid_schema.json b/modules/claims_api/spec/fixtures/v2/veterans/power_of_attorney/2122/invalid_schema.json index c2f15214cf2..27579b43c8e 100644 --- a/modules/claims_api/spec/fixtures/v2/veterans/power_of_attorney/2122/invalid_schema.json +++ b/modules/claims_api/spec/fixtures/v2/veterans/power_of_attorney/2122/invalid_schema.json @@ -2,6 +2,7 @@ "data": { "attributes": { "serviceOrganization": { + "withoutPoaCode": true } } } diff --git a/modules/claims_api/spec/fixtures/v2/veterans/power_of_attorney/2122/valid.json b/modules/claims_api/spec/fixtures/v2/veterans/power_of_attorney/2122/valid.json index 78d1dc5d17e..82a23c3051b 100644 --- a/modules/claims_api/spec/fixtures/v2/veterans/power_of_attorney/2122/valid.json +++ b/modules/claims_api/spec/fixtures/v2/veterans/power_of_attorney/2122/valid.json @@ -11,7 +11,8 @@ } }, "serviceOrganization": { - "poaCode": "083" + "poaCode": "083", + "registrationNumber": "67890" } } } diff --git a/modules/claims_api/spec/fixtures/v2/veterans/power_of_attorney/2122a/invalid_poa.json b/modules/claims_api/spec/fixtures/v2/veterans/power_of_attorney/2122a/invalid_poa.json index f36c0e5e464..a93b72d9101 100644 --- a/modules/claims_api/spec/fixtures/v2/veterans/power_of_attorney/2122a/invalid_poa.json +++ b/modules/claims_api/spec/fixtures/v2/veterans/power_of_attorney/2122a/invalid_poa.json @@ -12,8 +12,7 @@ }, "representative": { "poaCode": "aaa", - "firstName": "my", - "lastName": "name", + "registrationNumber": "67890", "type": "ATTORNEY", "address": { "addressLine1": "123", diff --git a/modules/claims_api/spec/fixtures/v2/veterans/power_of_attorney/2122a/invalid_schema.json b/modules/claims_api/spec/fixtures/v2/veterans/power_of_attorney/2122a/invalid_schema.json index 6cad91eaee1..f9767e6e69c 100644 --- a/modules/claims_api/spec/fixtures/v2/veterans/power_of_attorney/2122a/invalid_schema.json +++ b/modules/claims_api/spec/fixtures/v2/veterans/power_of_attorney/2122a/invalid_schema.json @@ -2,11 +2,9 @@ "data": { "attributes": { "representative": { - "firstName": "my", - "lastName": "name", "type": "ATTORNEY", "address": { - "addressLine1": "123", + "addressLine1": "123", "city": "city", "country": "US", "zipCode": "12345" diff --git a/modules/claims_api/spec/fixtures/v2/veterans/power_of_attorney/2122a/valid.json b/modules/claims_api/spec/fixtures/v2/veterans/power_of_attorney/2122a/valid.json index c73ed3acc6e..ce086b08903 100644 --- a/modules/claims_api/spec/fixtures/v2/veterans/power_of_attorney/2122a/valid.json +++ b/modules/claims_api/spec/fixtures/v2/veterans/power_of_attorney/2122a/valid.json @@ -14,8 +14,7 @@ }, "representative": { "poaCode": "083", - "firstName": "my", - "lastName": "name", + "registrationNumber": "67890", "type": "ATTORNEY", "address": { "addressLine1": "123", diff --git a/modules/claims_api/spec/lib/claims_api/bd_spec.rb b/modules/claims_api/spec/lib/claims_api/bd_spec.rb index 4ed688e0b09..67b2cb70d8c 100644 --- a/modules/claims_api/spec/lib/claims_api/bd_spec.rb +++ b/modules/claims_api/spec/lib/claims_api/bd_spec.rb @@ -16,7 +16,7 @@ let(:pdf_path) { 'modules/claims_api/spec/fixtures/21-526EZ.pdf' } it 'uploads a document to BD' do - VCR.use_cassette('bd/upload') do + VCR.use_cassette('claims_api/bd/upload') do result = subject.upload(claim:, pdf_path:) expect(result).to be_a Hash expect(result[:data][:success]).to be true diff --git a/modules/claims_api/spec/lib/claims_api/local_bgs_spec.rb b/modules/claims_api/spec/lib/claims_api/local_bgs_spec.rb index bfe30ab011d..482c897a6d8 100644 --- a/modules/claims_api/spec/lib/claims_api/local_bgs_spec.rb +++ b/modules/claims_api/spec/lib/claims_api/local_bgs_spec.rb @@ -11,7 +11,7 @@ describe '#find_poa_by_participant_id' do it 'responds as expected, with extra ClaimsApi::Logger logging' do - VCR.use_cassette('bgs/claimant_web_service/find_poa_by_participant_id') do + VCR.use_cassette('claims_api/bgs/claimant_web_service/find_poa_by_participant_id') do allow_any_instance_of(BGS::OrgWebService).to receive(:find_poa_history_by_ptcpnt_id).and_return({}) # Events logged: @@ -42,7 +42,8 @@ end it 'triggers StatsD measurements' do - VCR.use_cassette('bgs/claimant_web_service/find_poa_by_participant_id', allow_playback_repeats: true) do + VCR.use_cassette('claims_api/bgs/claimant_web_service/find_poa_by_participant_id', + allow_playback_repeats: true) do allow_any_instance_of(BGS::OrgWebService).to receive(:find_poa_history_by_ptcpnt_id).and_return({}) %w[establish_ssl_connection connection_wsdl_get connection_post parsed_response].each do |event| @@ -72,7 +73,7 @@ context 'when claims come back as a hash instead of an array' do it 'casts the hash as an array' do - VCR.use_cassette('bgs/claims/claims_trimmed_down') do + VCR.use_cassette('claims_api/bgs/claims/claims_trimmed_down') do claims = subject_instance.find_benefit_claims_status_by_ptcpnt_id('600061742') claims[:benefit_claims_dto][:benefit_claim] = claims[:benefit_claims_dto][:benefit_claim][0] allow(subject_instance).to receive(:find_benefit_claims_status_by_ptcpnt_id).with(id).and_return(claims) diff --git a/modules/claims_api/spec/lib/claims_api/manage_representative_service/read_poa_request_spec.rb b/modules/claims_api/spec/lib/claims_api/manage_representative_service/read_poa_request_spec.rb new file mode 100644 index 00000000000..9bcd9d6f55b --- /dev/null +++ b/modules/claims_api/spec/lib/claims_api/manage_representative_service/read_poa_request_spec.rb @@ -0,0 +1,191 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'bgs_service/manage_representative_service' +require Rails.root.join('modules', 'claims_api', 'spec', 'support', 'bgs_client_helpers.rb') + +metadata = { + bgs: { + service: 'manage_representative_service', + operation: 'read_poa_request' + } +} + +describe ClaimsApi::ManageRepresentativeService, metadata do + describe '#read_poa_request' do + subject do + service = described_class.new(**header_params) + service.read_poa_request(**params) + end + + describe 'with invalid external uid and key' do + let(:header_params) do + { + external_uid: 'invalidUid', + external_key: 'invalidKey' + } + end + + let(:params) do + { + poa_codes: ['091'], + statuses: ['new'] + } + end + + it 'does not seem to care' do + use_bgs_cassette('invalid_external_uid_and_key') do + expect(subject).to be_a(Hash) + end + end + end + + describe 'with valid external uid and key' do + let(:header_params) do + { + external_uid: 'xUid', + external_key: 'xKey' + } + end + + describe 'with no params' do + let(:params) do + {} + end + + it 'raises Common::Exceptions::ServiceError' do + use_bgs_cassette('no_params') do + expect { subject }.to raise_error( + Common::Exceptions::ServiceError + ) + end + end + end + + describe 'with no statuses param' do + let(:params) do + { + poa_codes: ['1'] + } + end + + it 'raises Common::Exceptions::ServiceError' do + use_bgs_cassette('no_statuses') do + expect { subject }.to raise_error( + Common::Exceptions::ServiceError + ) + end + end + end + + describe 'with invalid status in statuses param' do + let(:params) do + { + poa_codes: ['1'], + statuses: %w[invalid new] + } + end + + it 'raises Common::Exceptions::ServiceError' do + use_bgs_cassette('invalid_status') do + expect { subject }.to raise_error( + Common::Exceptions::ServiceError + ) + end + end + end + + describe 'with no poa_codes param' do + let(:params) do + { + statuses: ['new'] + } + end + + it 'raises Common::Exceptions::ServiceError' do + use_bgs_cassette('no_poa_codes') do + expect { subject }.to raise_error( + Common::Exceptions::ServiceError + ) + end + end + end + + describe 'with nonexistent poa_code param' do + let(:params) do + { + poa_codes: ['1'], + statuses: ['new'] + } + end + + it 'raises Common::Exceptions::ServiceError' do + use_bgs_cassette('nonexistent_poa_code') do + expect { subject }.to raise_error( + Common::Exceptions::ServiceError + ) + end + end + end + + describe 'with existent poa_code param' do + let(:params) do + { + poa_codes: ['091'], + statuses: ['new'] + } + end + + let(:expected) do + { + poa_request_respond_return_vo_list: { + vso_user_email: nil, + vso_user_first_name: 'VDC USER', + vso_user_last_name: nil, + change_address_auth: 'Y', + claimant_city: 'SEASIDE', + claimant_country: 'USA', + claimant_military_po: nil, + claimant_military_postal_code: nil, + claimant_state: 'MT', + claimant_zip: '95102', + date_request_actioned: '2015-08-05T11:33:20-05:00', + date_request_received: '2015-08-05T11:33:20-05:00', + declined_reason: nil, + health_info_auth: 'N', + poa_code: '091', + proc_id: '52095', + secondary_status: 'New', + vet_first_name: 'Wallace', + vet_last_name: 'Webb', + vet_middle_name: 'R', + vet_ptcpnt_id: '600043200' + }, + total_nbr_of_records: '1' + } + end + + it 'returns poa requests' do + use_bgs_cassette('existent_poa_code') do + expect(subject).to eq(expected) + end + end + + describe 'and nonexistent poa_code param' do + let(:params) do + { + poa_codes: %w[091 1], + statuses: ['new'] + } + end + + it 'returns the existent poa requests' do + use_bgs_cassette('existent_and_nonexistent_poa_code') do + expect(subject).to eq(expected) + end + end + end + end + end + end +end diff --git a/modules/claims_api/spec/lib/claims_api/manage_representative_service/update_poa_request_spec.rb b/modules/claims_api/spec/lib/claims_api/manage_representative_service/update_poa_request_spec.rb new file mode 100644 index 00000000000..2e0b72304f0 --- /dev/null +++ b/modules/claims_api/spec/lib/claims_api/manage_representative_service/update_poa_request_spec.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'bgs_service/manage_representative_service' +require Rails.root.join('modules', 'claims_api', 'spec', 'support', 'bgs_client_helpers.rb') + +metadata = { + bgs: { + service: 'manage_representative_service', + operation: 'update_poa_request' + } +} + +describe ClaimsApi::ManageRepresentativeService, metadata do + describe '#update_poa_request' do + subject do + service = described_class.new(**header_params) + service.update_poa_request(**params) + end + + describe 'on the happy path' do + let(:header_params) do + { + external_uid: 'abcdefg', + external_key: 'abcdefg' + } + end + + let(:params) do + representative = + FactoryBot.create( + :representative, + { + poa_codes: ['A1Q'], + first_name: 'abraham', + last_name: 'lincoln' + } + ) + + { + proc_id: '8675309', + representative: + } + end + + it 'responds with attributes', run_at: '2024-03-27T13:05:01Z' do + use_bgs_cassette('happy_path') do + expect(subject).to eq( + { + vso_user_email: nil, + vso_user_first_name: params[:representative].first_name, + vso_user_last_name: params[:representative].last_name, + declined_reason: nil, + proc_id: params[:proc_id], + secondary_status: 'OBS', + date_request_actioned: + # Formatting this to show the difference between the date returned + # in response and the date sent in request. + Time.current.in_time_zone('America/Chicago').iso8601 + } + ) + end + end + end + end +end diff --git a/modules/claims_api/spec/lib/claims_api/v2/poa_pdf_constructor/individual_spec.rb b/modules/claims_api/spec/lib/claims_api/v2/poa_pdf_constructor/individual_spec.rb index 0c906d3defa..aafa7a5aae3 100644 --- a/modules/claims_api/spec/lib/claims_api/v2/poa_pdf_constructor/individual_spec.rb +++ b/modules/claims_api/spec/lib/claims_api/v2/poa_pdf_constructor/individual_spec.rb @@ -47,9 +47,8 @@ }, representative: { poaCode: 'A1Q', + registrationNumber: '1234', type: 'ATTORNEY', - firstName: 'Bob', - lastName: 'Law', address: { addressLine1: '2719 Hyperion Ave', city: 'Los Angeles', @@ -68,7 +67,8 @@ other_service_branch_temp.form_data = { veteran: { serviceNumber: '987654321', - otherServiceBranch: 'Air National Guard', + serviceBranch: 'OTHER', + serviceBranchOther: 'Air National Guard', address: { addressLine1: '2719 Hyperion Ave', city: 'Los Angeles', @@ -103,8 +103,7 @@ representative: { poaCode: 'A1Q', type: 'ATTORNEY', - firstName: 'Bob', - lastName: 'Law', + registrationNumber: '1234', address: { addressLine1: '2719 Hyperion Ave', city: 'Los Angeles', @@ -138,8 +137,7 @@ 'text_signatures' => { 'page2' => [ { - 'signature' => "#{power_of_attorney.auth_headers['va_eauth_firstName']} " \ - "#{power_of_attorney.auth_headers['va_eauth_lastName']} - signed via api.va.gov", + 'signature' => 'Lillian Disney - signed via api.va.gov', 'x' => 35, 'y' => 306 }, @@ -149,6 +147,10 @@ 'y' => 200 } ] + }, + 'representative' => { + 'firstName' => 'Bob', + 'lastName' => 'Law' } } ) @@ -173,8 +175,7 @@ 'text_signatures' => { 'page2' => [ { - 'signature' => "#{power_of_attorney.auth_headers['va_eauth_firstName']} " \ - "#{power_of_attorney.auth_headers['va_eauth_lastName']} - signed via api.va.gov", + 'signature' => 'Lillian Disney - signed via api.va.gov', 'x' => 35, 'y' => 306 }, @@ -184,13 +185,17 @@ 'y' => 200 } ] + }, + 'representative' => { + 'firstName' => 'Bob', + 'lastName' => 'Law' } } ) constructor = ClaimsApi::V2::PoaPdfConstructor::Individual.new expected_pdf = Rails.root.join('modules', 'claims_api', 'spec', 'fixtures', '21-22A', 'v2', - 'signed_filled_final.pdf') + 'signed_filled_final_other_service_branch.pdf') generated_pdf = constructor.construct(data, id: power_of_attorney.id) expect(generated_pdf).to match_pdf_content_of(expected_pdf) end diff --git a/modules/claims_api/spec/lib/claims_api/v2/poa_pdf_constructor/organization_spec.rb b/modules/claims_api/spec/lib/claims_api/v2/poa_pdf_constructor/organization_spec.rb index 8f75f0a5045..1a4b7f60b7d 100644 --- a/modules/claims_api/spec/lib/claims_api/v2/poa_pdf_constructor/organization_spec.rb +++ b/modules/claims_api/spec/lib/claims_api/v2/poa_pdf_constructor/organization_spec.rb @@ -23,7 +23,8 @@ areaCode: '555', phoneNumber: '5551337' }, - email: 'test@example.com' + email: 'test@example.com', + insuranceNumber: 'Ar67346578674' }, claimant: { firstName: 'Lillian', @@ -45,9 +46,7 @@ }, serviceOrganization: { poaCode: '456', - firstName: 'Bob', - lastName: 'Representative', - organizationName: 'I Help Vets LLC', + registrationNumber: '1234', address: { addressLine1: '2719 Hyperion Ave', city: 'Los Angeles', @@ -82,8 +81,7 @@ 'text_signatures' => { 'page2' => [ { - 'signature' => "#{power_of_attorney.auth_headers['va_eauth_firstName']} " \ - "#{power_of_attorney.auth_headers['va_eauth_lastName']} - signed via api.va.gov", + 'signature' => 'Lillian Disney - signed via api.va.gov', 'x' => 35, 'y' => 240 }, @@ -93,7 +91,13 @@ 'y' => 200 } ] - } + }, + 'serviceOrganization' => + { + 'firstName' => 'Bob', + 'lastName' => 'Representative', + 'organizationName' => 'I Help Vets LLC' + } } ) diff --git a/modules/claims_api/spec/lib/claims_api/vnp_atchms_service_spec.rb b/modules/claims_api/spec/lib/claims_api/vnp_atchms_service_spec.rb new file mode 100644 index 00000000000..6260c7993a6 --- /dev/null +++ b/modules/claims_api/spec/lib/claims_api/vnp_atchms_service_spec.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'bgs_service/vnp_atchms_service' +require Rails.root.join('modules', 'claims_api', 'spec', 'support', 'bgs_client_helpers.rb') + +metadata = { + bgs: { + service: 'vnp_atchms_service', + operation: 'vnp_atchms_create' + } +} + +describe ClaimsApi::VnpAtchmsService, metadata do + describe '#vnp_atchms_create', run_at: '2024-04-01T18:48:27Z' do + subject { described_class.new external_uid: 'xUid', external_key: 'xKey' } + + describe 'validation' do + # get a proc_id from vnp_proc_create + let(:vnp_proc_id) { '3854593' } + let(:expected_response) do + { vnp_proc_id:, + atchms_file_nm: 'test.pdf', + atchms_descp: 'test' } + end + + context 'when missing required params' do + it 'raises an error' do + data = { asdf: 'qwerty' } + expect { subject.vnp_atchms_create(data) }.to(raise_error do |error| + expect(error).to be_a(ArgumentError) + expect(error.message).to eq('Missing required keys: vnp_proc_id, atchms_file_nm, atchms_descp, atchms_txt') + end) + end + end + + describe 'when submitting valid data' do + context 'with a base64 string' do + it 'creates a attachment from data' do + data = { + vnp_proc_id:, + atchms_file_nm: 'test.pdf', + atchms_descp: 'test', + atchms_txt: 'base64here' + } + use_bgs_cassette('happy_path_base64') do + result = subject.vnp_atchms_create(data) + expect((expected_response.to_a & result.to_a).to_h).to eq expected_response + end + end + end + + context 'with a file path' do + it 'creates a attachment from data' do + data = { + vnp_proc_id:, + atchms_file_nm: 'test.pdf', + atchms_descp: 'test', + atchms_txt: Rails.root.join('modules', 'claims_api', 'spec', 'fixtures', 'extras.pdf').to_s + } + use_bgs_cassette('happy_path_file') do + result = subject.vnp_atchms_create(data) + expect((expected_response.to_a & result.to_a).to_h).to eq expected_response + end + end + end + end + + context 'when providing an invalid procId' do + it 'raises an error' do + data = { + vnp_proc_id: '1234abc', + atchms_file_nm: 'test.pdf', + atchms_descp: 'test', + atchms_txt: 'base64here' + } + + use_bgs_cassette('invalid_procId') do + expect { subject.vnp_atchms_create(data) }.to raise_error(Common::Exceptions::ServiceError) + end + end + end + end + end +end diff --git a/modules/claims_api/spec/lib/claims_api/vnp_person_service_spec.rb b/modules/claims_api/spec/lib/claims_api/vnp_person_service_spec.rb new file mode 100644 index 00000000000..47acbb2681c --- /dev/null +++ b/modules/claims_api/spec/lib/claims_api/vnp_person_service_spec.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'bgs_service/vnp_person_service' +require Rails.root.join('modules', 'claims_api', 'spec', 'support', 'bgs_client_helpers.rb') + +metadata = { + bgs: { + service: 'vnp_person_service', + operation: 'vnp_person_create' + } +} + +describe ClaimsApi::VnpPersonService, metadata do + describe '#vnp_person_create' do + subject { described_class.new external_uid: 'xUid', external_key: 'xKey' } + + # get a proc_id from vnp_proc_create + # get a ptcpnt_id from vnp_ptcpnt_create (using the proc_id from the previous step) + let(:vnp_proc_id) { '3854545' } + let(:vnp_ptcpnt_id) { '182008' } + let(:expected_response) do + { vnp_proc_id:, vnp_ptcpnt_id:, + first_nm: 'Tamara', last_nm: 'Ellis' } + end + + it 'validates data' do + data = { asdf: 'qwerty' } + e = an_instance_of(ArgumentError).and having_attributes( + message: 'Missing required keys: vnp_proc_id, vnp_ptcpnt_id, first_nm, last_nm' + ) + expect { subject.vnp_person_create(data) }.to raise_error(e) + end + + describe 'valid data' do + it 'creates a new person from data', run_at: '2024-04-01T18:48:27Z' do + data = { + vnp_proc_id:, + vnp_ptcpnt_id:, + first_nm: 'Tamara', + last_nm: 'Ellis' + } + + use_bgs_cassette('happy_path') do + result = subject.vnp_person_create(data) + expect((expected_response.to_a & result.to_a).to_h).to eq expected_response + end + end + end + + describe 'invalid procId' do + it 'raises an error', run_at: '2024-04-01T18:48:27Z' do + data = { + vnp_proc_id: '1234', + vnp_ptcpnt_id:, + first_nm: 'Tamara', + last_nm: 'Ellis' + } + + use_bgs_cassette('invalid_procId') do + expect { subject.vnp_person_create(data) }.to raise_error(Common::Exceptions::ServiceError) + end + end + end + end +end diff --git a/modules/claims_api/spec/lib/claims_api/vnp_proc_form_service_spec.rb b/modules/claims_api/spec/lib/claims_api/vnp_proc_form_service_spec.rb new file mode 100644 index 00000000000..5cc9f286278 --- /dev/null +++ b/modules/claims_api/spec/lib/claims_api/vnp_proc_form_service_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'bgs_service/vnp_proc_form_service' + +describe ClaimsApi::VnpProcFormService do + subject { described_class.new external_uid: 'xUid', external_key: 'xKey' } + + describe 'vnp_proc_form_create' do + let(:options) { {} } + + it 'responds with a vnc_proc_id' do + options[:vnp_proc_id] = '3831394' + options[:vnp_ptcpnt_id] = nil + options[:jrn_dt] = nil + options[:jrn_obj_id] = 'VAgovAPI' + options[:jrn_status_type_cd] = 'U' + options[:jrn_user_id] = 'VAgovAPI' + VCR.use_cassette('claims_api/bgs/vnp_proc_form_service/vnp_proc_form_create') do + response = subject.vnp_proc_form_create(options) + expect(response[:comp_id][:vnp_proc_id]).to eq '3831394' + end + end + end +end diff --git a/modules/claims_api/spec/lib/claims_api/vnp_proc_service_v2_spec.rb b/modules/claims_api/spec/lib/claims_api/vnp_proc_service_v2_spec.rb new file mode 100644 index 00000000000..95236633e37 --- /dev/null +++ b/modules/claims_api/spec/lib/claims_api/vnp_proc_service_v2_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'bgs_service/vnp_proc_service_v2' + +describe ClaimsApi::VnpProcServiceV2 do + subject { described_class.new external_uid: 'xUid', external_key: 'xKey' } + + describe 'vnp_proc_create' do + it 'responds with a vnp_proc_id' do + VCR.use_cassette('claims_api/bgs/vnp_proc_service_v2/vnp_proc_create') do + result = subject.vnp_proc_create + expect(result[:vnp_proc_id]).to eq '29637' + end + end + end +end diff --git a/modules/claims_api/spec/lib/claims_api/vnp_ptcpnt_phone_service_spec.rb b/modules/claims_api/spec/lib/claims_api/vnp_ptcpnt_phone_service_spec.rb new file mode 100644 index 00000000000..e7903eb65ce --- /dev/null +++ b/modules/claims_api/spec/lib/claims_api/vnp_ptcpnt_phone_service_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'bgs_service/vnp_ptcpnt_phone_service' + +describe ClaimsApi::VnpPtcpntPhoneService do + subject { described_class.new external_uid: 'xUid', external_key: 'xKey' } + + describe 'vnp_ptcpnt_phone_create' do + let(:options) { {} } + + it 'responds with attributes' do + options[:vnp_proc_id] = '29798' + options[:vnp_ptcpnt_id] = '44693' + options[:phone_nbr] = '2225552252' + options[:efctv_dt] = '2020-07-16T18:20:17Z' + VCR.use_cassette('claims_api/bgs/vnp_ptcpnt_phone_service/vnp_ptcpnt_phone_create') do + response = subject.vnp_ptcpnt_phone_create(options) + expect(response[:vnp_proc_id]).to eq '29798' + expect(response[:vnp_ptcpnt_id]).to eq '44693' + expect(response[:phone_type_nm]).to eq 'Daytime' + expect(response[:phone_nbr]).to eq '2225552252' + expect(response[:efctv_dt]).to eq '2020-07-16T18:20:17Z' + expect(response[:vnp_ptcpnt_phone_id]).to eq '30888' + end + end + end +end diff --git a/modules/claims_api/spec/lib/claims_api/vnp_ptcpnt_service_spec.rb b/modules/claims_api/spec/lib/claims_api/vnp_ptcpnt_service_spec.rb new file mode 100644 index 00000000000..e84e40b5dc6 --- /dev/null +++ b/modules/claims_api/spec/lib/claims_api/vnp_ptcpnt_service_spec.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'bgs_service/vnp_ptcpnt_service' + +describe ClaimsApi::VnpPtcpntService do + subject { described_class.new external_uid: 'xUid', external_key: 'xKey' } + + describe 'vnp_ptcpnt_create' do + let(:options) { {} } + + it 'responds with attributes' do + options[:vnp_proc_id] = '3854437' + options[:vnp_ptcpnt_id] = nil + options[:fraud_ind] = nil + options[:jrn_dt] = '2020-07-16T18:20:17Z' + options[:jrn_lctn_id] = 281 + options[:jrn_obj_id] = 'VAgovAPI' + options[:jrn_status_type_cd] = 'U' + options[:jrn_user_id] = 'VAgovAPI' + options[:legacy_poa_cd] = nil + options[:misc_vendor_ind] = nil + options[:ptcpnt_short_nm] = nil + options[:ptcpnt_type_nm] = 'Person' + options[:tax_idfctn_nbr] = nil + options[:tin_waiver_reason_type_cd] = nil + options[:ptcpnt_fk_ptcpnt_id] = nil + options[:corp_ptcpnt_id] = nil + VCR.use_cassette('claims_api/bgs/vnp_ptcpnt_service/vnp_ptcpnt_create') do + response = subject.vnp_ptcpnt_create(options) + expect(response).to include( + { vnp_ptcpnt_id: '181913', + vnp_proc_id: '3854437', + jrn_dt: '2020-07-16T18:20:17Z', + jrn_lctn_id: '281', + jrn_obj_id: 'VAgovAPI', + jrn_status_type_cd: 'U', + jrn_user_id: 'VAgovAPI', + ptcpnt_type_nm: 'Person' } + ) + end + end + end +end diff --git a/modules/claims_api/spec/models/veteran/service/user_spec.rb b/modules/claims_api/spec/models/veteran/service/user_spec.rb index d6b6263bdde..d6965951db2 100644 --- a/modules/claims_api/spec/models/veteran/service/user_spec.rb +++ b/modules/claims_api/spec/models/veteran/service/user_spec.rb @@ -18,7 +18,7 @@ let(:ows) { ClaimsApi::LocalBGS } it 'initializes from a user' do - VCR.use_cassette('bgs/claimant_web_service/find_poa_by_participant_id') do + VCR.use_cassette('claims_api/bgs/claimant_web_service/find_poa_by_participant_id') do allow_any_instance_of(ows).to receive(:find_poa_history_by_ptcpnt_id) .and_return({ person_poa_history: { person_poa: [{ begin_dt: Time.zone.now, legacy_poa_cd: '033' }] } }) veteran = Veteran::User.new(user) @@ -28,7 +28,7 @@ end it 'does not bomb out if poa is missing' do - VCR.use_cassette('bgs/claimant_web_service/not_find_poa_by_participant_id') do + VCR.use_cassette('claims_api/bgs/claimant_web_service/not_find_poa_by_participant_id') do allow_any_instance_of(ows).to receive(:find_poa_history_by_ptcpnt_id) .and_return({ person_poa_history: nil }) veteran = Veteran::User.new(user) @@ -38,7 +38,7 @@ end it 'provides most recent previous poa' do - VCR.use_cassette('bgs/claimant_web_service/find_poa_by_participant_id') do + VCR.use_cassette('claims_api/bgs/claimant_web_service/find_poa_by_participant_id') do allow_any_instance_of(ows).to receive(:find_poa_history_by_ptcpnt_id) .and_return({ person_poa_history: { @@ -55,7 +55,7 @@ end it 'does not bomb out if poa history contains a single record' do - VCR.use_cassette('bgs/claimant_web_service/find_poa_by_participant_id') do + VCR.use_cassette('claims_api/bgs/claimant_web_service/find_poa_by_participant_id') do allow_any_instance_of(ows).to receive(:find_poa_history_by_ptcpnt_id) .and_return({ person_poa_history: { person_poa: { begin_dt: Time.zone.now, legacy_poa_cd: '033' } } }) veteran = Veteran::User.new(user) diff --git a/modules/claims_api/spec/requests/v1/claims_request_spec.rb b/modules/claims_api/spec/requests/v1/claims_request_spec.rb index 7f6df854843..cd0395aee41 100644 --- a/modules/claims_api/spec/requests/v1/claims_request_spec.rb +++ b/modules/claims_api/spec/requests/v1/claims_request_spec.rb @@ -48,7 +48,7 @@ context 'index' do it 'lists all Claims', run_at: 'Tue, 12 Dec 2017 03:09:06 GMT' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do + VCR.use_cassette('claims_api/bgs/claims/claims') do allow_any_instance_of(ClaimsApi::V1::ApplicationController) .to receive(:target_veteran).and_return(target_veteran) get '/services/claims/v1/claims', params: nil, headers: request_headers.merge(auth_header) @@ -59,7 +59,7 @@ it 'lists all Claims when camel-inflection', run_at: 'Tue, 12 Dec 2017 03:09:06 GMT' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do + VCR.use_cassette('claims_api/bgs/claims/claims') do allow_any_instance_of(ClaimsApi::V1::ApplicationController) .to receive(:target_veteran).and_return(target_veteran) get '/services/claims/v1/claims', params: nil, headers: request_headers_camel.merge(auth_header) @@ -71,7 +71,7 @@ context 'with errors' do it 'shows a errored Claims not found error message' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims_with_errors') do + VCR.use_cassette('claims_api/bgs/claims/claims_with_errors') do get '/services/claims/v1/claims', params: nil, headers: request_headers.merge(auth_header) expect(response.status).to eq(404) end @@ -83,7 +83,7 @@ context 'for a single claim' do it 'shows a single Claim', run_at: 'Wed, 13 Dec 2017 03:28:23 GMT' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claim') do + VCR.use_cassette('claims_api/bgs/claims/claim') do get "/services/claims/v1/claims/#{bgs_claim_id}", params: nil, headers: request_headers.merge(auth_header) expect(response).to match_response_schema('claims_api/claim') end @@ -92,7 +92,7 @@ it 'shows a single Claim when camel-inflected', run_at: 'Wed, 13 Dec 2017 03:28:23 GMT' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claim') do + VCR.use_cassette('claims_api/bgs/claims/claim') do get "/services/claims/v1/claims/#{bgs_claim_id}", params: nil, headers: request_headers_camel.merge(auth_header) expect(response).to match_camelized_response_schema('claims_api/claim') @@ -109,7 +109,7 @@ auth_headers: { some: 'data' }, evss_id: 600_118_851, id: 'd5536c5c-0465-4038-a368-1a9d9daf65c9') - VCR.use_cassette('bgs/claims/claim') do + VCR.use_cassette('claims_api/bgs/claims/claim') do get( "/services/claims/v1/claims/#{bgs_claim_id}", params: nil, headers: request_headers.merge(auth_header) @@ -128,7 +128,7 @@ auth_headers: { some: 'data' }, evss_id: 600_118_851, id: 'd5536c5c-0465-4038-a368-1a9d9daf65c9') - VCR.use_cassette('bgs/claims/claim') do + VCR.use_cassette('claims_api/bgs/claims/claim') do get( "/services/claims/v1/claims/#{bgs_claim_id}", params: nil, headers: request_headers_camel.merge(auth_header) @@ -148,7 +148,7 @@ auth_headers: { some: 'data' }, evss_id: 600_118_851, id: 'd5536c5c-0465-4038-a368-1a9d9daf65c9') - VCR.use_cassette('bgs/claims/claim') do + VCR.use_cassette('claims_api/bgs/claims/claim') do get( '/services/claims/v1/claims/d5536c5c-0465-4038-a368-1a9d9daf65c9', params: nil, headers: request_headers.merge(auth_header) @@ -171,7 +171,7 @@ id: 'd5536c5c-0465-4038-a368-1a9d9daf65c9') expect_any_instance_of(claims_service).to receive(:update_from_remote) .and_raise(StandardError.new('no claim found')) - VCR.use_cassette('bgs/claims/claim') do + VCR.use_cassette('claims_api/bgs/claims/claim') do get( "/services/claims/v1/claims/#{bgs_claim_id}", params: nil, headers: request_headers.merge(auth_header) @@ -185,7 +185,7 @@ context 'with errors' do it '404s' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claim_with_errors') do + VCR.use_cassette('claims_api/bgs/claims/claim_with_errors') do get '/services/claims/v1/claims/123123131', params: nil, headers: request_headers.merge(auth_header) expect(response.status).to eq(404) end @@ -194,7 +194,7 @@ it 'missing MPI Record' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claim_with_errors') do + VCR.use_cassette('claims_api/bgs/claims/claim_with_errors') do vet = ClaimsApi::Veteran.new( uuid: request_headers['X-VA-SSN']&.gsub(/[^0-9]/, ''), ssn: request_headers['X-VA-SSN']&.gsub(/[^0-9]/, ''), @@ -223,7 +223,7 @@ it 'missing an ICN' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claim_with_errors') do + VCR.use_cassette('claims_api/bgs/claims/claim_with_errors') do vet = ClaimsApi::Veteran.new( uuid: request_headers['X-VA-SSN']&.gsub(/[^0-9]/, ''), ssn: request_headers['X-VA-SSN']&.gsub(/[^0-9]/, ''), @@ -256,7 +256,7 @@ id: 'd5536c5c-0465-4038-a368-1a9d9daf65c9', status: 'errored', evss_response: [{ 'key' => 'Error', 'severity' => 'FATAL', 'text' => 'Failed' }]) - VCR.use_cassette('bgs/claims/claim') do + VCR.use_cassette('claims_api/bgs/claims/claim') do headers = request_headers.merge(auth_header) get('/services/claims/v1/claims/d5536c5c-0465-4038-a368-1a9d9daf65c9', params: nil, headers:) expect(response.status).to eq(422) @@ -273,7 +273,7 @@ id: 'd5536c5c-0465-4038-a368-1a9d9daf65c9', status: 'errored', evss_response: nil) - VCR.use_cassette('bgs/claims/claim') do + VCR.use_cassette('claims_api/bgs/claims/claim') do headers = request_headers.merge(auth_header) get('/services/claims/v1/claims/d5536c5c-0465-4038-a368-1a9d9daf65c9', params: nil, headers:) expect(response.status).to eq(422) @@ -286,7 +286,7 @@ context 'POA verifier' do it 'users the poa verifier when the header is present' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claim') do + VCR.use_cassette('claims_api/bgs/claims/claim') do verifier_stub = instance_double('BGS::PowerOfAttorneyVerifier') allow(BGS::PowerOfAttorneyVerifier).to receive(:new) { verifier_stub } allow(verifier_stub).to receive(:verify) @@ -304,7 +304,7 @@ verifier_stub = instance_double('BGS::PowerOfAttorneyVerifier') allow(BGS::PowerOfAttorneyVerifier).to receive(:new) { verifier_stub } allow(verifier_stub).to receive(:verify) - VCR.use_cassette('bgs/claims/claims') do + VCR.use_cassette('claims_api/bgs/claims/claims') do allow_any_instance_of(ClaimsApi::V1::ApplicationController) .to receive(:target_veteran).and_return(target_veteran) get '/services/claims/v1/claims', params: nil, headers: auth_header @@ -318,7 +318,7 @@ verifier_stub = instance_double('BGS::PowerOfAttorneyVerifier') allow(BGS::PowerOfAttorneyVerifier).to receive(:new) { verifier_stub } allow(verifier_stub).to receive(:verify) - VCR.use_cassette('bgs/claims/claims') do + VCR.use_cassette('claims_api/bgs/claims/claims') do get '/services/claims/v1/claims', params: nil, headers: auth_header.merge(camel_inflection_header) expect(response).to match_camelized_response_schema('claims_api/claims') end @@ -329,7 +329,7 @@ context "when a 'Token Validation Error' is received" do it "raises a 'Common::Exceptions::Unauthorized' exception", run_at: 'Tue, 12 Dec 2017 03:09:06 GMT' do auth = { Authorization: 'Bearer The-quick-brown-fox-jumped-over-the-lazy-dog' } - VCR.use_cassette('bgs/claims/claims') do + VCR.use_cassette('claims_api/bgs/claims/claims') do get '/services/claims/v1/claims', params: nil, headers: request_headers.merge(auth) parsed_response = JSON.parse(response.body) @@ -343,7 +343,7 @@ context 'events timeline' do it 'maps BGS data to match previous logic with EVSS data' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claim') do + VCR.use_cassette('claims_api/bgs/claims/claim') do get "/services/claims/v1/claims/#{bgs_claim_id}", params: nil, headers: request_headers.merge(auth_header) body = JSON.parse(response.body) events_timeline = body['data']['attributes']['events_timeline'] diff --git a/modules/claims_api/spec/requests/v1/disability_compensation_request_spec.rb b/modules/claims_api/spec/requests/v1/disability_compensation_request_spec.rb index 4cc5e1a6332..eb2a0818cb0 100644 --- a/modules/claims_api/spec/requests/v1/disability_compensation_request_spec.rb +++ b/modules/claims_api/spec/requests/v1/disability_compensation_request_spec.rb @@ -74,7 +74,7 @@ it 'returns a bad request' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data params['data']['attributes']['treatments'] = treatments @@ -90,8 +90,8 @@ it 'returns a 200' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data params['data']['attributes']['treatments'] = treatments @@ -108,7 +108,7 @@ it 'returns a 422' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data params['data']['attributes']['treatments'] = treatments @@ -136,8 +136,8 @@ it 'returns a 200' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data params['data']['attributes']['treatments'] = treatments @@ -172,7 +172,7 @@ it 'returns a bad request' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data params['data']['attributes']['treatments'] = treatments @@ -188,8 +188,8 @@ it 'returns a 200' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data params['data']['attributes']['treatments'] = treatments @@ -219,8 +219,8 @@ it 'returns a 200' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data params['data']['attributes']['treatments'] = treatments @@ -252,7 +252,7 @@ it 'returns a bad request' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data params['data']['attributes']['treatments'] = treatments @@ -270,7 +270,7 @@ it 'returns a bad request' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data params['data']['attributes']['treatments'] = treatments @@ -289,7 +289,7 @@ it 'returns a bad request' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data params['data']['attributes']['treatments'] = treatments @@ -305,8 +305,8 @@ it 'returns a 200' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data params['data']['attributes']['treatments'] = treatments @@ -324,8 +324,8 @@ it 'returns a 200' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data params['data']['attributes']['treatments'] = treatments @@ -342,8 +342,8 @@ it 'returns a 200' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data params['data']['attributes']['treatments'] = treatments @@ -360,8 +360,8 @@ it 'returns a 200' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data params['data']['attributes']['treatments'] = treatments @@ -387,8 +387,8 @@ it 'returns a successful response with all the data' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do post path, params: data, headers: headers.merge(auth_header) parsed = JSON.parse(response.body) expect(parsed['data']['type']).to eq('claims_api_claim') @@ -403,8 +403,8 @@ it 'creates the sidekick job' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do expect(ClaimsApi::ClaimEstablisher).to receive(:perform_async) post path, params: data, headers: headers.merge(auth_header) end @@ -418,8 +418,8 @@ it 'creates the sidekick job' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do post path, params: data, headers: headers.merge(auth_header) end end @@ -429,8 +429,8 @@ it 'assigns a source' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do post path, params: data, headers: headers.merge(auth_header) token = JSON.parse(response.body)['data']['attributes']['token'] aec = ClaimsApi::AutoEstablishedClaim.find(token) @@ -442,8 +442,8 @@ it "assigns a 'cid' (OKTA client_id)" do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do jwt_payload = { 'ver' => 1, 'jti' => 'AT.04f_GBSkMkWYbLgG5joGNlApqUthsZnYXhiyPc_5KZ0', @@ -471,8 +471,8 @@ it 'sets the flashes' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do post path, params: data, headers: headers.merge(auth_header) token = JSON.parse(response.body)['data']['attributes']['token'] aec = ClaimsApi::AutoEstablishedClaim.find(token) @@ -484,8 +484,8 @@ it 'sets the special issues' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do post path, params: data, headers: headers.merge(auth_header) token = JSON.parse(response.body)['data']['attributes']['token'] aec = ClaimsApi::AutoEstablishedClaim.find(token) @@ -499,8 +499,8 @@ it 'builds the auth headers' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do auth_header_stub = instance_double('EVSS::DisabilityCompensationAuthHeaders') expect(EVSS::DisabilityCompensationAuthHeaders).to(receive(:new).once { auth_header_stub }) expect(auth_header_stub).to receive(:add_headers).once @@ -534,8 +534,8 @@ it 'raises an exception that beginningDate is not valid' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('brd/intake_sites') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/brd/intake_sites') do + VCR.use_cassette('claims_api/brd/countries') do par = json_data par['data']['attributes']['veteran']['changeOfAddress'] = change_of_address @@ -565,8 +565,8 @@ it 'raises an exception that country is invalid' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('brd/intake_sites') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/brd/intake_sites') do + VCR.use_cassette('claims_api/brd/countries') do par = json_data par['data']['attributes']['veteran']['changeOfAddress'] = change_of_address @@ -606,8 +606,8 @@ it 'raises an exception that title10ActivationDate is invalid' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do par = json_data par['data']['attributes']['serviceInformation']['reservesNationalGuardService'] = reserves_national_guard_service @@ -625,7 +625,7 @@ it 'raises an exception that title10ActivationDate is invalid' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/brd/countries') do par = json_data par['data']['attributes']['serviceInformation']['reservesNationalGuardService'] = reserves_national_guard_service @@ -642,8 +642,8 @@ it 'returns a successful response' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do par = json_data par['data']['attributes']['serviceInformation']['reservesNationalGuardService'] = reserves_national_guard_service @@ -661,8 +661,8 @@ it 'returns a successful response' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do par = json_data par['data']['attributes']['serviceInformation']['reservesNationalGuardService'] = reserves_national_guard_service @@ -680,7 +680,7 @@ it 'raises an exception that title10ActivationDate is invalid' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/brd/countries') do par = json_data par['data']['attributes']['serviceInformation']['reservesNationalGuardService'] = reserves_national_guard_service @@ -699,8 +699,8 @@ it "raises an exception that 'anticipatedSeparationDate' is invalid" do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do par = json_data par['data']['attributes']['serviceInformation']['reservesNationalGuardService'] = reserves_national_guard_service @@ -718,7 +718,7 @@ it "raises an exception that 'anticipatedSeparationDate' is invalid" do mock_acg(scopes) do |auth_header| - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/brd/countries') do par = json_data par['data']['attributes']['serviceInformation']['reservesNationalGuardService'] = reserves_national_guard_service @@ -735,8 +735,8 @@ it 'returns a successful response' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do par = json_data par['data']['attributes']['serviceInformation']['reservesNationalGuardService'] = reserves_national_guard_service @@ -808,7 +808,7 @@ end it 'requires homelessness homelessnessRisk subfields' do - VCR.use_cassette('bgs/claims/claims') do + VCR.use_cassette('claims_api/bgs/claims/claims') do mock_acg(scopes) do |auth_header| par = json_data par['data']['attributes']['veteran']['homelessness'] = { @@ -856,8 +856,8 @@ context 'when correct types are passed for specialIssues' do it 'returns a successful status' do - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do mock_acg(scopes) do |auth_header| params = json_data params['data']['attributes']['disabilities'][0]['specialIssues'] = %w[ALS PTSD/1] @@ -885,8 +885,8 @@ context 'when correct types are passed for flashes' do it 'returns a successful status' do - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do mock_acg(scopes) do |auth_header| params = json_data params['data']['attributes']['veteran']['flashes'] = %w[Hardship POW] @@ -923,7 +923,7 @@ def obj.class receive(:body).and_return(fake_puma_null_io_object) ) mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do + VCR.use_cassette('claims_api/bgs/claims/claims') do post path, params: data, headers: headers.merge(auth_header) expect(response.status).to eq 422 expect(JSON.parse(response.body)['errors']).to be_an Array @@ -942,7 +942,7 @@ def obj.class it 'responds with a properly formed error object' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do + VCR.use_cassette('claims_api/bgs/claims/claims') do post path, params: data, headers: headers.merge(auth_header) body = JSON.parse(response.body) expect(response.status).to eq 422 @@ -958,7 +958,7 @@ def obj.class it 'responds with a properly formed error object' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do + VCR.use_cassette('claims_api/bgs/claims/claims') do post path, params: data, headers: headers.merge(auth_header) body = JSON.parse(response.body) expect(response.status).to eq 422 @@ -976,8 +976,8 @@ def obj.class it 'returns a successful response when valid' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('brd/countries') do - VCR.use_cassette('bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do VCR.use_cassette('claims_api/v1/disability_comp/validate') do post path, params: data, headers: headers.merge(auth_header) parsed = JSON.parse(response.body) @@ -992,8 +992,8 @@ def obj.class # temp disable until LH Dash can fix xit 'returns a list of errors when invalid hitting EVSS' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('brd/countries') do - VCR.use_cassette('bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do VCR.use_cassette('claims_api/v1/disability_comp/invalid') do post path, params: data, headers: headers.merge(auth_header) parsed = JSON.parse(response.body) @@ -1006,7 +1006,7 @@ def obj.class it 'increment counters for statsd' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('evss/disability_compensation_form/form_526_invalid_validation') do + VCR.use_cassette('claims_api/evss/disability_compensation_form/form_526_invalid_validation') do expect(StatsD).to receive(:increment).at_least(:once) post path, params: data, headers: headers.merge(auth_header) end @@ -1030,8 +1030,8 @@ def obj.class context error_klass.to_s do it 'is logged to PersonalInformationLog' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('brd/countries') do - VCR.use_cassette('bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do allow_any_instance_of(ClaimsApi::DisabilityCompensation::MockOverrideService) .to receive(:validate_form526).and_raise(error_klass) allow_any_instance_of(EVSS::DisabilityCompensationForm::Service) @@ -1057,7 +1057,7 @@ def obj.class context 'when consumer is representative' do it 'returns an unprocessible entity status' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/brd/countries') do post path, params: data, headers: headers.merge(auth_header) expect(response.status).to eq(422) end @@ -1083,10 +1083,10 @@ def obj.class it 'returns a 422 without an edipi' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do - VCR.use_cassette('mpi/add_person/add_person_success') do - VCR.use_cassette('mpi/find_candidate/orch_search_with_attributes') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do + VCR.use_cassette('claims_api/mpi/add_person/add_person_success') do + VCR.use_cassette('claims_api/mpi/find_candidate/orch_search_with_attributes') do allow_any_instance_of(MPIData) .to receive(:mvi_response).and_return(multi_profile) allow_any_instance_of(MPI::Service).to receive(:find_profile_by_identifier) @@ -1106,10 +1106,10 @@ def obj.class it 'adds person to MPI and checks for edipi' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do - VCR.use_cassette('mpi/add_person/add_person_success') do - VCR.use_cassette('mpi/find_candidate/orch_search_with_attributes') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do + VCR.use_cassette('claims_api/mpi/add_person/add_person_success') do + VCR.use_cassette('claims_api/mpi/find_candidate/orch_search_with_attributes') do allow_any_instance_of(ClaimsApi::Veteran).to receive(:mpi_record?).and_return(true) allow_any_instance_of(MPIData).to receive(:mvi_response) .and_return(profile_with_edipi) @@ -1130,8 +1130,8 @@ def obj.class it 'raises a 422, with message' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do mpi_profile_response.profile.participant_ids = [] mpi_profile_response.profile.participant_id = '' allow_any_instance_of(MPIData).to receive(:add_person_proxy) @@ -1162,7 +1162,7 @@ def obj.class it 'returns an unprocessible entity status' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/brd/countries') do post path, params: data, headers: headers.merge(auth_header) expect(response.status).to eq(422) end @@ -1178,8 +1178,8 @@ def obj.class it 'returns an unprocessible entity status' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('brd/countries') do - VCR.use_cassette('bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do allow_any_instance_of(ClaimsApi::Veteran) .to receive(:mpi_record?).and_return(true) allow_any_instance_of(MPIData) @@ -1216,8 +1216,8 @@ def obj.class it 'responds with a 200' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do post path, params: data, headers: headers.merge(auth_header) expect(response.status).to eq(200) end @@ -1231,8 +1231,8 @@ def obj.class it 'responds with a 200' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do post path, params: data, headers: headers.merge(auth_header) expect(response.status).to eq(200) end @@ -1246,7 +1246,7 @@ def obj.class it 'responds with a bad request' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do + VCR.use_cassette('claims_api/bgs/claims/claims') do post path, params: data, headers: headers.merge(auth_header) expect(response.status).to eq(400) end @@ -1269,8 +1269,8 @@ def obj.class it 'responds with a 200' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do post path, params: data, headers: headers.merge(auth_header) expect(response.status).to eq(200) end @@ -1284,8 +1284,8 @@ def obj.class it 'responds with a 200' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do post path, params: data, headers: headers.merge(auth_header) expect(response.status).to eq(200) end @@ -1299,7 +1299,7 @@ def obj.class it 'responds with bad request' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do + VCR.use_cassette('claims_api/bgs/claims/claims') do post path, params: data, headers: headers.merge(auth_header) expect(response.status).to eq(400) end @@ -1312,8 +1312,8 @@ def obj.class it 'responds with a 200' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do post path, params: data, headers: headers.merge(auth_header) expect(response.status).to eq(200) end @@ -1327,8 +1327,8 @@ def obj.class it 'responds with a 200' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do post path, params: data, headers: headers.merge(auth_header) expect(response.status).to eq(200) end @@ -1342,7 +1342,7 @@ def obj.class it 'responds with a bad request' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do + VCR.use_cassette('claims_api/bgs/claims/claims') do post path, params: data, headers: headers.merge(auth_header) expect(response.status).to eq(422) end @@ -1355,7 +1355,7 @@ def obj.class it 'responds with a 422' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do + VCR.use_cassette('claims_api/bgs/claims/claims') do post path, params: data, headers: headers.merge(auth_header) expect(response.status).to eq(422) end @@ -1368,7 +1368,7 @@ def obj.class it 'responds with a 422' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do + VCR.use_cassette('claims_api/bgs/claims/claims') do post path, params: data, headers: headers.merge(auth_header) expect(response.status).to eq(422) end @@ -1381,7 +1381,7 @@ def obj.class it 'responds with bad request' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do + VCR.use_cassette('claims_api/bgs/claims/claims') do post path, params: data, headers: headers.merge(auth_header) expect(response.status).to eq(422) end @@ -1394,7 +1394,7 @@ def obj.class it 'responds with bad request' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do + VCR.use_cassette('claims_api/bgs/claims/claims') do post path, params: data, headers: headers.merge(auth_header) expect(response.status).to eq(422) end @@ -1407,7 +1407,7 @@ def obj.class context 'when submitted application_expiration_date is in the past' do it 'responds with bad request' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do + VCR.use_cassette('claims_api/bgs/claims/claims') do json_data = JSON.parse data params = json_data params['data']['attributes']['applicationExpirationDate'] = (Time.zone.today - 1.day).to_s @@ -1421,7 +1421,7 @@ def obj.class context 'when submitted application_expiration_date is today' do it 'responds with bad request' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data params['data']['attributes']['applicationExpirationDate'] = Time.zone.today.to_s @@ -1435,8 +1435,8 @@ def obj.class context 'when submitted application_expiration_date is in the future' do it 'responds with a 200' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data params['data']['attributes']['applicationExpirationDate'] = (Time.zone.today + 1.day).to_s @@ -1451,7 +1451,7 @@ def obj.class context 'when submitted claimant_certification is false' do it 'responds with bad request' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do + VCR.use_cassette('claims_api/bgs/claims/claims') do json_data = JSON.parse data params = json_data params['data']['attributes']['claimantCertification'] = false @@ -1465,8 +1465,8 @@ def obj.class context 'when submitted separationLocationCode is missing for a future activeDutyEndDate' do it 'responds with bad request' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('brd/intake_sites') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/brd/intake_sites') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data params['data']['attributes']['serviceInformation']['servicePeriods'].first['activeDutyEndDate'] = @@ -1484,7 +1484,7 @@ def obj.class context 'when submitted separationLocationCode is invalid' do it 'responds with bad request' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('brd/intake_sites') do + VCR.use_cassette('claims_api/brd/intake_sites') do json_data = JSON.parse data params = json_data params['data']['attributes']['serviceInformation']['servicePeriods'].first['activeDutyEndDate'] = @@ -1501,8 +1501,8 @@ def obj.class context 'when confinements don\'t fall within service periods' do it 'responds with a bad request' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data params['data']['attributes']['serviceInformation']['confinements'] = [{ @@ -1522,7 +1522,7 @@ def obj.class context 'when confinements are overlapping' do it 'responds with a bad request' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data params['data']['attributes']['serviceInformation']['confinements'] = [{ @@ -1572,8 +1572,8 @@ def obj.class context "when 'pointOfContact' is provided" do it 'responds with a 422' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data params['data']['attributes']['veteran']['homelessness'] = {} @@ -1604,7 +1604,7 @@ def obj.class context "when 'pointOfContact' is not provided" do it 'responds with a 422' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data params['data']['attributes']['veteran']['homelessness']['currentlyHomeless'] = { @@ -1650,8 +1650,8 @@ def obj.class it 'responds with a bad request' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data params['data']['attributes']['servicePay'] = service_pay_attribute @@ -1669,7 +1669,7 @@ def obj.class it 'responds with a bad request' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data params['data']['attributes']['servicePay'] = service_pay_attribute @@ -1688,8 +1688,8 @@ def obj.class it 'responds with a 200' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data params['data']['attributes']['servicePay'] = service_pay_attribute @@ -1707,8 +1707,8 @@ def obj.class it 'responds with a 200' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data params['data']['attributes']['servicePay'] = service_pay_attribute @@ -1741,7 +1741,7 @@ def obj.class it 'responds with an unprocessible entity' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data params['data']['attributes']['servicePay'] = service_pay_attribute @@ -1757,8 +1757,8 @@ def obj.class it 'responds with an unprocessible entity' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data params['data']['attributes']['servicePay'] = service_pay_attribute @@ -1775,8 +1775,8 @@ def obj.class it 'responds with a 200' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data params['data']['attributes']['servicePay'] = service_pay_attribute @@ -1808,7 +1808,7 @@ def obj.class it 'responds with an unprocessible entity' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data params['data']['attributes']['servicePay'] = service_pay_attribute @@ -1835,8 +1835,8 @@ def obj.class it 'responds with a 200' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data params['data']['attributes']['servicePay'] = service_pay_attribute @@ -1871,7 +1871,7 @@ def obj.class it 'responds with an unprocessible entity' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data params['data']['attributes']['servicePay'] = service_pay_attribute @@ -1887,8 +1887,8 @@ def obj.class it 'responds with an unprocessible entity' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data params['data']['attributes']['servicePay'] = service_pay_attribute @@ -1905,8 +1905,8 @@ def obj.class it 'responds with a 200' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data params['data']['attributes']['servicePay'] = service_pay_attribute @@ -1938,7 +1938,7 @@ def obj.class it 'responds with a bad request' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data params['data']['attributes']['servicePay'] = service_pay_attribute @@ -1954,8 +1954,8 @@ def obj.class it 'responds with a 200' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data params['data']['attributes']['servicePay'] = service_pay_attribute @@ -1974,7 +1974,7 @@ def obj.class context 'when disabilityActionType is NONE without secondaryDisabilities' do it 'raises an exception' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data disabilities = [ @@ -1994,7 +1994,7 @@ def obj.class context 'when secondaryDisability disabilityActionType is something other than SECONDARY' do it 'raises an exception' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data disabilities = [ @@ -2029,7 +2029,7 @@ def obj.class it 'raises an exception' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data disabilities = [ @@ -2065,7 +2065,7 @@ def obj.class it 'raises an exception' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data disabilities = [ @@ -2094,7 +2094,7 @@ def obj.class context "when 'disabilites.secondaryDisabilities.approximateBeginDate' is present" do it 'raises an exception if date is invalid' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data disabilities = [ @@ -2121,7 +2121,7 @@ def obj.class it 'raises an exception if date is not in the past' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data disabilities = [ @@ -2150,7 +2150,7 @@ def obj.class context "when 'disabilites.secondaryDisabilities.classificationCode' is not present" do it 'raises an exception if name is not valid structure' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data disabilities = [ @@ -2176,8 +2176,8 @@ def obj.class it 'raises an exception if name is longer than 255 characters' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data disabilities = [ @@ -2216,8 +2216,8 @@ def obj.class context "when 'disabilites.classificationCode' is valid" do it 'returns a successful response' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data disabilities = [ @@ -2239,7 +2239,7 @@ def obj.class context "when 'disabilites.classificationCode' is invalid" do it 'responds with a bad request' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data disabilities = [ @@ -2263,8 +2263,8 @@ def obj.class context "and 'disabilities.ratedDisabilityId' is not provided" do it 'returns an unprocessible entity status' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data disabilities = [ @@ -2286,8 +2286,8 @@ def obj.class context "and 'disabilities.ratedDisabilityId' is provided" do it 'responds with a 200' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data disabilities = [ @@ -2310,7 +2310,7 @@ def obj.class context "and 'disabilities.diagnosticCode' is not provided" do it 'returns an unprocessible entity status' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data disabilities = [ @@ -2334,8 +2334,8 @@ def obj.class context "and 'disabilites.diagnosticCode is not provided" do it 'returns an unprocessible entity status' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data disabilities = [ @@ -2367,8 +2367,8 @@ def obj.class context "and 'disabilities.ratedDisabilityId' is not provided" do it 'responds with a 200' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data disabilities = [ @@ -2405,7 +2405,7 @@ def obj.class it 'responds with a bad request' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data params['data']['attributes']['disabilities'] = disabilities @@ -2421,8 +2421,8 @@ def obj.class it 'responds with a 200' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data disabilities = [ @@ -2461,7 +2461,7 @@ def obj.class it 'responds with a bad request' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data params['data']['attributes']['disabilities'] = disabilities @@ -2473,8 +2473,8 @@ def obj.class it 'responds with a useful error message ' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data params['data']['attributes']['disabilities'] = disabilities @@ -2493,8 +2493,8 @@ def obj.class it 'responds with a 200' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data params['data']['attributes']['disabilities'] = disabilities @@ -2514,7 +2514,7 @@ def obj.class context "when a valid 'confinements' is not included" do it 'responds with a bad request' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data params['data']['attributes']['disabilities'] = disabilities @@ -2527,7 +2527,7 @@ def obj.class it 'responds with a useful error message ' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data params['data']['attributes']['disabilities'] = disabilities @@ -2553,8 +2553,8 @@ def obj.class it 'responds with a 200' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data params['data']['attributes']['disabilities'] = disabilities @@ -2585,8 +2585,8 @@ def obj.class ) mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data params['data']['attributes']['disabilities'] = disabilities @@ -2621,8 +2621,8 @@ def obj.class ) mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data params['data']['attributes']['disabilities'] = disabilities @@ -2639,8 +2639,8 @@ def obj.class describe "'addressLine3'" do it "accepts 'addressLine3' and returns a 200" do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data params['data']['attributes']['veteran']['currentMailingAddress']['addressLine3'] = 'Box 123' @@ -2655,8 +2655,8 @@ def obj.class describe "'currentMailingAddress.country'" do it "accepts 'USA'" do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data params['data']['attributes']['veteran']['currentMailingAddress']['country'] = 'USA' @@ -2669,7 +2669,7 @@ def obj.class it "does not accept 'US'" do mock_acg(scopes) do |auth_header| - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data params['data']['attributes']['veteran']['currentMailingAddress']['country'] = 'US' @@ -2686,8 +2686,8 @@ def obj.class context 'when not provided' do it 'responds with a 200' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do json_data = JSON.parse data params = json_data params['data']['attributes'].delete('applicationExpirationDate') @@ -2705,8 +2705,8 @@ def obj.class describe 'is case insensitive' do it 'is properly transformed to uppercase before submission to EVSS' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do direct_deposit_info = File.read(Rails.root.join('modules', 'claims_api', 'spec', 'fixtures', 'form_526_direct_deposit.json')) json_data = JSON.parse data @@ -2747,8 +2747,8 @@ def obj.class it 'sets the flashes and special_issues' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do post path, params: data_no_flashes, headers: headers.merge(auth_header) token = JSON.parse(response.body)['data']['attributes']['token'] aec = ClaimsApi::AutoEstablishedClaim.find(token) @@ -2907,8 +2907,8 @@ def obj.class it 'returns existing claim if duplicate submit occurs by using the md5 lookup' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do json = JSON.parse(data) post path, params: json.to_json, headers: headers.merge(auth_header) expect(response.status).to eq(200) @@ -2918,8 +2918,8 @@ def obj.class end end mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do json = JSON.parse(data) post path, params: json.to_json, headers: headers.merge(auth_header) expect(response.status).to eq(200) diff --git a/modules/claims_api/spec/requests/v1/intent_to_file_request_spec.rb b/modules/claims_api/spec/requests/v1/intent_to_file_request_spec.rb index 059c1c7df9a..3a1c3590cd4 100644 --- a/modules/claims_api/spec/requests/v1/intent_to_file_request_spec.rb +++ b/modules/claims_api/spec/requests/v1/intent_to_file_request_spec.rb @@ -39,7 +39,7 @@ it 'posts a minimum payload and returns a payload with an expiration date' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/intent_to_file_web_service/insert_intent_to_file') do + VCR.use_cassette('claims_api/bgs/intent_to_file_web_service/insert_intent_to_file') do post path, params: data.to_json, headers: headers.merge(auth_header) expect(response.status).to eq(200) expect(JSON.parse(response.body)['data']['attributes']['status']).to eq('duplicate') @@ -49,7 +49,7 @@ it 'posts a maximum payload and returns a payload with an expiration date' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/intent_to_file_web_service/insert_intent_to_file') do + VCR.use_cassette('claims_api/bgs/intent_to_file_web_service/insert_intent_to_file') do data[:data][:attributes] = extra post path, params: data.to_json, headers: headers.merge(auth_header) expect(response.status).to eq(200) @@ -60,7 +60,7 @@ it 'posts a 404 error with detail when BGS returns a 500 response' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/intent_to_file_web_service/insert_intent_to_file_500') do + VCR.use_cassette('claims_api/bgs/intent_to_file_web_service/insert_intent_to_file_500') do data[:data][:attributes] = { type: 'pension' } post path, params: data.to_json, headers: headers.merge(auth_header) expect(response.status).to eq(404) @@ -71,7 +71,7 @@ describe "'burial' submission" do it "returns a 403 when veteran is submitting for 'burial'" do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/intent_to_file_web_service/insert_intent_to_file') do + VCR.use_cassette('claims_api/bgs/intent_to_file_web_service/insert_intent_to_file') do data[:data][:attributes] = { type: 'burial' } post path, params: data.to_json, headers: auth_header expect(response.status).to eq(403) @@ -81,7 +81,7 @@ it "returns a 403 when neither 'participant_claimant_id' nor 'claimant_ssn' are provided" do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/intent_to_file_web_service/insert_intent_to_file') do + VCR.use_cassette('claims_api/bgs/intent_to_file_web_service/insert_intent_to_file') do data[:data][:attributes] = { type: 'burial' } post path, params: data.to_json, headers: headers.merge(auth_header) expect(response.status).to eq(403) @@ -91,7 +91,7 @@ it "returns a 200 if the veteran is not the submitter and 'participant_claimant_id' is provided" do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/intent_to_file_web_service/insert_intent_to_file') do + VCR.use_cassette('claims_api/bgs/intent_to_file_web_service/insert_intent_to_file') do data[:attributes] = extra data[:attributes][:type] = 'burial' post path, params: data.to_json, headers: headers.merge(auth_header) @@ -102,7 +102,7 @@ it "returns a 200 if the veteran is not the submitter and 'claimant_ssn' is provided" do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/intent_to_file_web_service/insert_intent_to_file') do + VCR.use_cassette('claims_api/bgs/intent_to_file_web_service/insert_intent_to_file') do data[:data][:attributes][:type] = 'burial' data[:data][:attributes][:claimant_ssn] = '123_456_789' post path, params: data.to_json, headers: headers.merge(auth_header) @@ -210,7 +210,7 @@ context 'when submitting the ITF to BGS is successful' do it "adds a 'ClaimsApi::IntentToFile' record" do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/intent_to_file_web_service/insert_intent_to_file') do + VCR.use_cassette('claims_api/bgs/intent_to_file_web_service/insert_intent_to_file') do expect do post path, params: data.to_json, headers: headers.merge(auth_header) end.to change(ClaimsApi::IntentToFile, :count).by(1) @@ -224,7 +224,7 @@ context 'when submitting the ITF to BGS is NOT successful' do it "adds a 'ClaimsApi::IntentToFile' record" do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/intent_to_file_web_service/insert_intent_to_file_500') do + VCR.use_cassette('claims_api/bgs/intent_to_file_web_service/insert_intent_to_file_500') do data[:data][:attributes] = { type: 'pension' } expect do post path, params: data.to_json, headers: headers.merge(auth_header) @@ -280,7 +280,7 @@ it 'returns the latest itf of a compensation type' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/intent_to_file_web_service/get_intent_to_file') do + VCR.use_cassette('claims_api/bgs/intent_to_file_web_service/get_intent_to_file') do get "#{path}/active", params: { type: 'compensation' }, headers: headers.merge(auth_header) expect(response.status).to eq(200) expect(JSON.parse(response.body)['data']['attributes']['status']).to eq('active') @@ -290,7 +290,7 @@ it 'returns the latest itf of a pension type' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/intent_to_file_web_service/get_intent_to_file') do + VCR.use_cassette('claims_api/bgs/intent_to_file_web_service/get_intent_to_file') do get "#{path}/active", params: { type: 'pension' }, headers: headers.merge(auth_header) expect(response.status).to eq(200) expect(JSON.parse(response.body)['data']['attributes']['status']).to eq('active') @@ -300,7 +300,7 @@ it 'returns the latest itf of a burial type' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/intent_to_file_web_service/get_intent_to_file') do + VCR.use_cassette('claims_api/bgs/intent_to_file_web_service/get_intent_to_file') do get "#{path}/active", params: { type: 'burial' }, headers: headers.merge(auth_header) expect(response.status).to eq(200) expect(JSON.parse(response.body)['data']['attributes']['status']).to eq('active') diff --git a/modules/claims_api/spec/requests/v1/power_of_attorney_request_spec.rb b/modules/claims_api/spec/requests/v1/power_of_attorney_request_spec.rb index e02c7e4b305..7267e85502d 100644 --- a/modules/claims_api/spec/requests/v1/power_of_attorney_request_spec.rb +++ b/modules/claims_api/spec/requests/v1/power_of_attorney_request_spec.rb @@ -113,7 +113,7 @@ context 'when consumer is Veteran and missing EDIPI' do it 'catches a raised 422' do mock_acg(scopes) do |auth_header| - VCR.use_cassette('bgs/intent_to_file_web_service/insert_intent_to_file') do + VCR.use_cassette('claims_api/bgs/intent_to_file_web_service/insert_intent_to_file') do allow_any_instance_of(MPIData) .to receive(:mvi_response).and_return(multi_profile) post path, params: data, headers: auth_header diff --git a/modules/claims_api/spec/requests/v1/rswag_claims_request_spec.rb b/modules/claims_api/spec/requests/v1/rswag_claims_request_spec.rb index 391860a23b4..7d83b317786 100644 --- a/modules/claims_api/spec/requests/v1/rswag_claims_request_spec.rb +++ b/modules/claims_api/spec/requests/v1/rswag_claims_request_spec.rb @@ -62,7 +62,7 @@ stub_poa_verification mock_acg(scopes) do - VCR.use_cassette('bgs/claims/claims_trimmed_down') do + VCR.use_cassette('claims_api/bgs/claims/claims_trimmed_down') do allow_any_instance_of(ClaimsApi::V1::ApplicationController) .to receive(:target_veteran).and_return(target_veteran) submit_request(example.metadata) @@ -96,7 +96,7 @@ stub_poa_verification mock_acg(scopes) do - VCR.use_cassette('bgs/claims/claims') do + VCR.use_cassette('claims_api/bgs/claims/claims') do allow(ClaimsApi::ValidatedToken).to receive(:new).and_return(nil) submit_request(example.metadata) end @@ -131,7 +131,7 @@ Common::Exceptions::ResourceNotFound.new(detail: 'The Resource was not found.') ) mock_acg(scopes) do - VCR.use_cassette('bgs/claims/claims') do + VCR.use_cassette('claims_api/bgs/claims/claims') do submit_request(example.metadata) end end @@ -201,7 +201,7 @@ stub_poa_verification mock_acg(scopes) do - VCR.use_cassette('bgs/claims/claim') do + VCR.use_cassette('claims_api/bgs/claims/claim') do submit_request(example.metadata) end end @@ -234,7 +234,7 @@ stub_poa_verification mock_acg(scopes) do - VCR.use_cassette('bgs/claims/claim') do + VCR.use_cassette('claims_api/bgs/claims/claim') do allow(ClaimsApi::ValidatedToken).to receive(:new).and_return(nil) submit_request(example.metadata) end diff --git a/modules/claims_api/spec/requests/v1/rswag_disability_compensation_request_spec.rb b/modules/claims_api/spec/requests/v1/rswag_disability_compensation_request_spec.rb index 3d38036b403..1dd9d4aa6a5 100644 --- a/modules/claims_api/spec/requests/v1/rswag_disability_compensation_request_spec.rb +++ b/modules/claims_api/spec/requests/v1/rswag_disability_compensation_request_spec.rb @@ -105,8 +105,8 @@ stub_poa_verification mock_acg(scopes) do - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do submit_request(example.metadata) end end @@ -147,8 +147,8 @@ stub_poa_verification mock_acg(scopes) do - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do allow(ClaimsApi::ValidatedToken).to receive(:new).and_return(nil) submit_request(example.metadata) end @@ -182,7 +182,7 @@ def make_stubbed_request(example) stub_poa_verification mock_acg(scopes) do - VCR.use_cassette('bgs/claims/claims') do + VCR.use_cassette('claims_api/bgs/claims/claims') do submit_request(example.metadata) end end @@ -520,10 +520,10 @@ def append_example_metadata(example, response) stub_poa_verification stub_claims_api_auth_token - VCR.use_cassette('evss/disability_compensation_form/form_526_valid_validation') do + VCR.use_cassette('claims_api/evss/disability_compensation_form/form_526_valid_validation') do mock_acg(scopes) do - VCR.use_cassette('bgs/claims/claims') do - VCR.use_cassette('brd/countries') do + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do VCR.use_cassette('claims_api/v1/disability_comp/bd_token') do VCR.use_cassette('claims_api/v1/disability_comp/validate') do submit_request(example.metadata) @@ -568,9 +568,9 @@ def append_example_metadata(example, response) before do |example| stub_poa_verification - VCR.use_cassette('evss/disability_compensation_form/form_526_valid_validation') do + VCR.use_cassette('claims_api/evss/disability_compensation_form/form_526_valid_validation') do mock_acg(scopes) do - VCR.use_cassette('bgs/claims/claims') do + VCR.use_cassette('claims_api/bgs/claims/claims') do allow(ClaimsApi::ValidatedToken).to receive(:new).and_return(nil) submit_request(example.metadata) end @@ -605,8 +605,8 @@ def append_example_metadata(example, response) stub_poa_verification mock_acg(scopes) do - VCR.use_cassette('evss/disability_compensation_form/form_526_invalid_validation') do - VCR.use_cassette('bgs/claims/claims') do + VCR.use_cassette('claims_api/evss/disability_compensation_form/form_526_invalid_validation') do + VCR.use_cassette('claims_api/bgs/claims/claims') do submit_request(example.metadata) end end diff --git a/modules/claims_api/spec/requests/v1/rswag_intent_to_file_request_spec.rb b/modules/claims_api/spec/requests/v1/rswag_intent_to_file_request_spec.rb index 04dfe1c6163..0703424421d 100644 --- a/modules/claims_api/spec/requests/v1/rswag_intent_to_file_request_spec.rb +++ b/modules/claims_api/spec/requests/v1/rswag_intent_to_file_request_spec.rb @@ -79,7 +79,7 @@ stub_poa_verification mock_acg(scopes) do - VCR.use_cassette('bgs/intent_to_file_web_service/insert_intent_to_file') do + VCR.use_cassette('claims_api/bgs/intent_to_file_web_service/insert_intent_to_file') do submit_request(example.metadata) end end @@ -112,7 +112,7 @@ stub_poa_verification mock_acg(scopes) do - VCR.use_cassette('bgs/intent_to_file_web_service/insert_intent_to_file') do + VCR.use_cassette('claims_api/bgs/intent_to_file_web_service/insert_intent_to_file') do allow(ClaimsApi::ValidatedToken).to receive(:new).and_return(nil) submit_request(example.metadata) end @@ -148,7 +148,7 @@ expect_any_instance_of( ClaimsApi::V1::Forms::IntentToFileController ).to receive(:veteran_submitting_burial_itf?).and_return(true) - VCR.use_cassette('bgs/intent_to_file_web_service/insert_intent_to_file') do + VCR.use_cassette('claims_api/bgs/intent_to_file_web_service/insert_intent_to_file') do submit_request(example.metadata) end end @@ -180,7 +180,7 @@ stub_poa_verification mock_acg(scopes) do - VCR.use_cassette('bgs/intent_to_file_web_service/insert_intent_to_file') do + VCR.use_cassette('claims_api/bgs/intent_to_file_web_service/insert_intent_to_file') do submit_request(example.metadata) end end @@ -247,7 +247,7 @@ Timecop.freeze(Time.zone.parse('2020-01-01T08:00:00Z')) mock_acg(scopes) do - VCR.use_cassette('bgs/intent_to_file_web_service/get_intent_to_file') do + VCR.use_cassette('claims_api/bgs/intent_to_file_web_service/get_intent_to_file') do submit_request(example.metadata) end end @@ -281,7 +281,7 @@ stub_poa_verification mock_acg(scopes) do - VCR.use_cassette('bgs/intent_to_file_web_service/get_intent_to_file') do + VCR.use_cassette('claims_api/bgs/intent_to_file_web_service/get_intent_to_file') do allow(ClaimsApi::ValidatedToken).to receive(:new).and_return(nil) submit_request(example.metadata) end @@ -316,7 +316,7 @@ stub_poa_verification mock_acg(scopes) do - VCR.use_cassette('bgs/intent_to_file_web_service/get_intent_to_file') do + VCR.use_cassette('claims_api/bgs/intent_to_file_web_service/get_intent_to_file') do submit_request(example.metadata) end end @@ -348,7 +348,7 @@ stub_poa_verification mock_acg(scopes) do - VCR.use_cassette('bgs/intent_to_file_web_service/get_intent_to_file') do + VCR.use_cassette('claims_api/bgs/intent_to_file_web_service/get_intent_to_file') do submit_request(example.metadata) end end @@ -415,7 +415,7 @@ stub_poa_verification mock_acg(scopes) do - VCR.use_cassette('bgs/intent_to_file_web_service/insert_intent_to_file') do + VCR.use_cassette('claims_api/bgs/intent_to_file_web_service/insert_intent_to_file') do submit_request(example.metadata) end end @@ -448,7 +448,7 @@ stub_poa_verification mock_acg(scopes) do - VCR.use_cassette('bgs/intent_to_file_web_service/insert_intent_to_file') do + VCR.use_cassette('claims_api/bgs/intent_to_file_web_service/insert_intent_to_file') do allow(ClaimsApi::ValidatedToken).to receive(:new).and_return(nil) submit_request(example.metadata) end @@ -481,7 +481,7 @@ stub_poa_verification mock_acg(scopes) do - VCR.use_cassette('bgs/intent_to_file_web_service/insert_intent_to_file') do + VCR.use_cassette('claims_api/bgs/intent_to_file_web_service/insert_intent_to_file') do submit_request(example.metadata) end end diff --git a/modules/claims_api/spec/requests/v2/rswag_veteran_identifier_request_spec.rb b/modules/claims_api/spec/requests/v2/rswag_veteran_identifier_request_spec.rb index 2054230c0af..ff93624926f 100644 --- a/modules/claims_api/spec/requests/v2/rswag_veteran_identifier_request_spec.rb +++ b/modules/claims_api/spec/requests/v2/rswag_veteran_identifier_request_spec.rb @@ -55,8 +55,7 @@ ) before do |example| - mock_ccg(scopes) do |auth_header| - Authorization = auth_header # rubocop:disable Naming/ConstantName + mock_ccg(scopes) do submit_request(example.metadata) end end @@ -78,8 +77,7 @@ describe 'Getting a 400 response' do context 'when parameters are missing' do before do |example| - mock_ccg(scopes) do |auth_header| - Authorization = auth_header # rubocop:disable Naming/ConstantName + mock_ccg(scopes) do data[:ssn] = nil submit_request(example.metadata) end @@ -140,8 +138,7 @@ expect(ClaimsApi::Veteran).to receive(:new).and_return(veteran) allow(veteran).to receive(:mpi).and_return(veteran_mpi_data) allow(veteran_mpi_data).to receive(:icn).and_return(nil) - mock_ccg(scopes) do |auth_header| - Authorization = auth_header # rubocop:disable Naming/ConstantName + mock_ccg(scopes) do submit_request(example.metadata) end end diff --git a/modules/claims_api/spec/requests/v2/veterans/claims_request_spec.rb b/modules/claims_api/spec/requests/v2/veterans/claims_request_spec.rb index 5dcaec5b877..e67b76cfee5 100644 --- a/modules/claims_api/spec/requests/v2/veterans/claims_request_spec.rb +++ b/modules/claims_api/spec/requests/v2/veterans/claims_request_spec.rb @@ -169,15 +169,15 @@ end it 'are listed' do - lighthouse_claim = create(:auto_established_claim, status: 'PEND', veteran_icn: veteran_id, + lighthouse_claim = create(:auto_established_claim, status: 'established', veteran_icn: veteran_id, evss_id: '600098193') - lighthouse_claim_two = create(:auto_established_claim, status: 'CAN', veteran_icn: veteran_id, - evss_id: '600098194') + lighthouse_claim_two = create(:auto_established_claim, status: 'pending', veteran_icn: veteran_id, + evss_id: nil) lh_claims = ClaimsApi::AutoEstablishedClaim.where(id: [lighthouse_claim.id, lighthouse_claim_two.id]) mock_ccg(scopes) do |auth_header| - VCR.use_cassette('bgs/tracked_items/find_tracked_items') do + VCR.use_cassette('claims_api/bgs/tracked_items/find_tracked_items') do expect_any_instance_of(bcs) .to receive(:find_benefit_claims_status_by_ptcpnt_id).and_return(bgs_claims) expect(ClaimsApi::AutoEstablishedClaim) @@ -190,7 +190,7 @@ claim = json_response['data'].first claim_two = json_response['data'][1] expect(claim['attributes']['status']).to eq('COMPLETE') - expect(claim_two['attributes']['status']).to eq('CANCELED') + expect(claim_two['attributes']['status']).to eq('PENDING') expect(claim['attributes']['claimPhaseDates']['phaseChangeDate']).to eq('2017-10-18') end end @@ -248,7 +248,7 @@ lh_claims = ClaimsApi::AutoEstablishedClaim.where(id: [lighthouse_claim.id, lighthouse_claim_two.id]) mock_ccg(scopes) do |auth_header| - VCR.use_cassette('bgs/tracked_items/find_tracked_items') do + VCR.use_cassette('claims_api/bgs/tracked_items/find_tracked_items') do expect_any_instance_of(bcs) .to receive(:find_benefit_claims_status_by_ptcpnt_id).and_return(bgs_claims) expect(ClaimsApi::AutoEstablishedClaim) @@ -284,7 +284,7 @@ ) mock_ccg(scopes) do |auth_header| - VCR.use_cassette('bgs/tracked_items/find_tracked_items') do + VCR.use_cassette('claims_api/bgs/tracked_items/find_tracked_items') do expect_any_instance_of(bcs) .to receive(:find_benefit_claims_status_by_ptcpnt_id).and_return(bgs_claims) expect(ClaimsApi::AutoEstablishedClaim) @@ -316,7 +316,8 @@ { base_end_prdct_type_cd: '400', benefit_claim_id: '111111111', - claim_status: 'Preparation for notification' + claim_status: 'PEND', + phase_type: 'Gathering of Evidence' } ] } @@ -327,7 +328,7 @@ lighthouse_claim = create( :auto_established_claim, id: '0958d973-36fb-43ef-8801-2718bd33c825', - status: 'Preparation for notification', + status: 'pending', evss_id: '111111111' ) @@ -336,7 +337,7 @@ ) mock_ccg(scopes) do |auth_header| - VCR.use_cassette('bgs/tracked_items/find_tracked_items') do + VCR.use_cassette('claims_api/bgs/tracked_items/find_tracked_items') do expect_any_instance_of(bcs) .to receive(:find_benefit_claims_status_by_ptcpnt_id).and_return(bgs_claims) expect(ClaimsApi::AutoEstablishedClaim) @@ -351,7 +352,7 @@ expect(json_response.count).to eq(1) claim = json_response['data'].first expect(claim['attributes']['baseEndProductCode']).to eq('400') - expect(claim['attributes']['status']).to eq('PREPARATION_FOR_NOTIFICATION') + expect(claim['attributes']['status']).to eq('EVIDENCE_GATHERING_REVIEW_DECISION') expect(claim['id']).to eq('111111111') expect(claim['attributes']['lighthouseId']).to eq('0958d973-36fb-43ef-8801-2718bd33c825') end @@ -377,7 +378,7 @@ it "provides a value for 'claimId', but 'lighthouseId' will be 'nil' " do mock_ccg(scopes) do |auth_header| - VCR.use_cassette('bgs/tracked_items/find_tracked_items') do + VCR.use_cassette('claims_api/bgs/tracked_items/find_tracked_items') do expect_any_instance_of(bcs) .to receive(:find_benefit_claims_status_by_ptcpnt_id).and_return(bgs_claims) expect(ClaimsApi::AutoEstablishedClaim) @@ -419,7 +420,7 @@ it "provides a value for 'lighthouseId', but 'claimId' will be 'nil' " do mock_ccg(scopes) do |auth_header| - VCR.use_cassette('bgs/tracked_items/find_tracked_items') do + VCR.use_cassette('claims_api/bgs/tracked_items/find_tracked_items') do expect_any_instance_of(bcs) .to receive(:find_benefit_claims_status_by_ptcpnt_id).and_return(bgs_claims) expect(ClaimsApi::AutoEstablishedClaim) @@ -442,7 +443,7 @@ it "provides a value for 'lighthouseId', but 'claimId' will be 'nil' when bgs returns nil" do mock_ccg(scopes) do |auth_header| - VCR.use_cassette('bgs/tracked_items/find_tracked_items') do + VCR.use_cassette('claims_api/bgs/tracked_items/find_tracked_items') do expect_any_instance_of(bcs) .to receive(:find_benefit_claims_status_by_ptcpnt_id).and_return(nil) expect(ClaimsApi::AutoEstablishedClaim) @@ -523,8 +524,8 @@ lh_claim = create(:auto_established_claim, status: 'PENDING', veteran_icn: veteran_id, evss_id: '111111111') mock_ccg(scopes) do |auth_header| - VCR.use_cassette('bgs/tracked_items/find_tracked_items') do - VCR.use_cassette('evss/documents/get_claim_documents') do + VCR.use_cassette('claims_api/bgs/tracked_items/find_tracked_items') do + VCR.use_cassette('claims_api/evss/documents/get_claim_documents') do bgs_claim_response[:benefit_claim_details_dto][:ptcpnt_vet_id] = '600061742' expect_any_instance_of(bcs) .to receive(:find_benefit_claim_details_by_benefit_claim_id).and_return(bgs_claim_response) @@ -553,8 +554,8 @@ lh_claim = create(:auto_established_claim, status: 'PENDING', veteran_icn: '2023062086V8675309', evss_id: '111111111') mock_ccg(scopes) do |auth_header| - VCR.use_cassette('bgs/tracked_items/find_tracked_items') do - VCR.use_cassette('evss/documents/get_claim_documents') do + VCR.use_cassette('claims_api/bgs/tracked_items/find_tracked_items') do + VCR.use_cassette('claims_api/evss/documents/get_claim_documents') do expect_any_instance_of(bcs) .to receive(:find_benefit_claim_details_by_benefit_claim_id).and_return(bgs_claim_response) @@ -589,8 +590,8 @@ lh_claim = create(:auto_established_claim, status: 'PENDING', veteran_icn: veteran_id, evss_id: '111111111') mock_ccg(scopes) do |auth_header| - VCR.use_cassette('bgs/tracked_items/find_tracked_items') do - VCR.use_cassette('evss/documents/get_claim_documents') do + VCR.use_cassette('claims_api/bgs/tracked_items/find_tracked_items') do + VCR.use_cassette('claims_api/evss/documents/get_claim_documents') do expect_any_instance_of(bcs) .to receive(:find_benefit_claim_details_by_benefit_claim_id).and_return(bgs_claim_response) expect(ClaimsApi::AutoEstablishedClaim) @@ -683,7 +684,7 @@ it 'returns a 200' do mock_ccg(scopes) do |auth_header| - VCR.use_cassette('bgs/tracked_items/find_tracked_items') do + VCR.use_cassette('claims_api/bgs/tracked_items/find_tracked_items') do expect_any_instance_of(bcs) .to receive(:find_benefit_claim_details_by_benefit_claim_id).and_return(bgs_claim) @@ -701,7 +702,7 @@ describe "handling 'lighthouseId' and 'claimId'" do it "provides a value for 'lighthouseId', but 'claimId' will be 'nil' " do mock_ccg(scopes) do |auth_header| - VCR.use_cassette('bgs/tracked_items/find_tracked_items') do + VCR.use_cassette('claims_api/bgs/tracked_items/find_tracked_items') do expect_any_instance_of(bcs) .to receive(:find_benefit_claim_details_by_benefit_claim_id).and_return(bgs_claim) @@ -724,8 +725,8 @@ describe "handling 'lighthouseId' and 'claimId'" do it "provides a value for 'lighthouseId' and 'claimId'" do mock_ccg(scopes) do |auth_header| - VCR.use_cassette('bgs/tracked_items/find_tracked_items') do - VCR.use_cassette('evss/documents/get_claim_documents') do + VCR.use_cassette('claims_api/bgs/tracked_items/find_tracked_items') do + VCR.use_cassette('claims_api/evss/documents/get_claim_documents') do expect(ClaimsApi::AutoEstablishedClaim) .to receive(:get_by_id_and_icn).and_return(lighthouse_claim) expect_any_instance_of(bcs) @@ -778,8 +779,8 @@ describe "handling 'lighthouseId' and 'claimId'" do it "provides a value for 'lighthouseId' and 'claimId'" do mock_ccg(scopes) do |auth_header| - VCR.use_cassette('bgs/tracked_items/find_tracked_items') do - VCR.use_cassette('evss/documents/get_claim_documents') do + VCR.use_cassette('claims_api/bgs/tracked_items/find_tracked_items') do + VCR.use_cassette('claims_api/evss/documents/get_claim_documents') do expect_any_instance_of(bcs) .to receive(:find_benefit_claim_details_by_benefit_claim_id).and_return(bgs_claim) expect(ClaimsApi::AutoEstablishedClaim) @@ -803,8 +804,8 @@ context 'and a Lighthouse claim does not exit' do it "provides a value for 'claimId', but 'lighthouseId' will be 'nil' " do mock_ccg(scopes) do |auth_header| - VCR.use_cassette('bgs/tracked_items/find_tracked_items') do - VCR.use_cassette('evss/documents/get_claim_documents') do + VCR.use_cassette('claims_api/bgs/tracked_items/find_tracked_items') do + VCR.use_cassette('claims_api/evss/documents/get_claim_documents') do expect_any_instance_of(bcs) .to receive(:find_benefit_claim_details_by_benefit_claim_id).and_return(bgs_claim) expect(ClaimsApi::AutoEstablishedClaim) @@ -827,8 +828,8 @@ context 'when the file_number is nil' do it 'returns an empty array and not a 404' do mock_ccg(scopes) do |auth_header| - VCR.use_cassette('bgs/tracked_items/find_tracked_items') do - VCR.use_cassette('evss/documents/get_claim_documents') do + VCR.use_cassette('claims_api/bgs/tracked_items/find_tracked_items') do + VCR.use_cassette('claims_api/evss/documents/get_claim_documents') do allow_any_instance_of(ClaimsApi::V2::Veterans::ClaimsController) .to receive(:benefits_documents_enabled?).and_return(true) @@ -868,8 +869,8 @@ context 'when there is 1 status' do it "sets the 'status'" do mock_ccg(scopes) do |auth_header| - VCR.use_cassette('bgs/tracked_items/find_tracked_items') do - VCR.use_cassette('evss/documents/get_claim_documents') do + VCR.use_cassette('claims_api/bgs/tracked_items/find_tracked_items') do + VCR.use_cassette('claims_api/evss/documents/get_claim_documents') do expect_any_instance_of(bcs) .to receive(:find_benefit_claim_details_by_benefit_claim_id).and_return(bgs_claim_response) expect(ClaimsApi::AutoEstablishedClaim) @@ -892,8 +893,8 @@ it 'shows a closed date' do mock_ccg(scopes) do |auth_header| - VCR.use_cassette('bgs/tracked_items/find_tracked_items') do - VCR.use_cassette('evss/documents/get_claim_documents') do + VCR.use_cassette('claims_api/bgs/tracked_items/find_tracked_items') do + VCR.use_cassette('claims_api/evss/documents/get_claim_documents') do expect_any_instance_of(bcs) .to receive(:find_benefit_claim_details_by_benefit_claim_id).and_return(bgs_claim) expect(ClaimsApi::AutoEstablishedClaim) @@ -916,8 +917,8 @@ context 'when a typical status is received' do it "the v2 mapper sets the correct 'status'" do mock_ccg(scopes) do |auth_header| - VCR.use_cassette('bgs/tracked_items/find_tracked_items') do - VCR.use_cassette('evss/documents/get_claim_documents') do + VCR.use_cassette('claims_api/bgs/tracked_items/find_tracked_items') do + VCR.use_cassette('claims_api/evss/documents/get_claim_documents') do expect_any_instance_of(bcs) .to receive(:find_benefit_claim_details_by_benefit_claim_id).and_return(bgs_claim_response) expect(ClaimsApi::AutoEstablishedClaim) @@ -950,8 +951,8 @@ it "the v2 mapper sets the 'status' correctly" do mock_ccg(scopes) do |auth_header| - VCR.use_cassette('bgs/tracked_items/find_tracked_items') do - VCR.use_cassette('evss/documents/get_claim_documents') do + VCR.use_cassette('claims_api/bgs/tracked_items/find_tracked_items') do + VCR.use_cassette('claims_api/evss/documents/get_claim_documents') do expect_any_instance_of(bcs) .to receive(:find_benefit_claim_details_by_benefit_claim_id).and_return(bgs_claim_response) expect(ClaimsApi::AutoEstablishedClaim) @@ -974,8 +975,8 @@ it "the v2 mapper sets the 'status' correctly" do mock_ccg(scopes) do |auth_header| - VCR.use_cassette('bgs/tracked_items/find_tracked_items') do - VCR.use_cassette('evss/documents/get_claim_documents') do + VCR.use_cassette('claims_api/bgs/tracked_items/find_tracked_items') do + VCR.use_cassette('claims_api/evss/documents/get_claim_documents') do expect_any_instance_of(bcs) .to receive(:find_benefit_claim_details_by_benefit_claim_id).and_return(bgs_claim) expect(ClaimsApi::AutoEstablishedClaim) @@ -998,8 +999,8 @@ it "the v2 mapper sets the 'status' correctly" do mock_ccg(scopes) do |auth_header| - VCR.use_cassette('bgs/tracked_items/find_tracked_items') do - VCR.use_cassette('evss/documents/get_claim_documents') do + VCR.use_cassette('claims_api/bgs/tracked_items/find_tracked_items') do + VCR.use_cassette('claims_api/evss/documents/get_claim_documents') do expect_any_instance_of(bcs) .to receive(:find_benefit_claim_details_by_benefit_claim_id).and_return(bgs_claim) expect(ClaimsApi::AutoEstablishedClaim) @@ -1023,8 +1024,8 @@ context 'it picks the newest status' do it "returns a claim with the 'claimId' and 'lighthouseId' set" do mock_ccg(scopes) do |auth_header| - VCR.use_cassette('bgs/tracked_items/find_tracked_items') do - VCR.use_cassette('evss/documents/get_claim_documents') do + VCR.use_cassette('claims_api/bgs/tracked_items/find_tracked_items') do + VCR.use_cassette('claims_api/evss/documents/get_claim_documents') do expect_any_instance_of(bcs) .to receive(:find_benefit_claim_details_by_benefit_claim_id).and_return(bgs_claim_response) expect(ClaimsApi::AutoEstablishedClaim) @@ -1052,8 +1053,8 @@ claim_contentions = bgs_claim_response claim_contentions[:benefit_claim_details_dto][:contentions] = ' c1 (New), c2 (Old), c3 (Unknown)' mock_ccg(scopes) do |auth_header| - VCR.use_cassette('bgs/tracked_items/find_tracked_items') do - VCR.use_cassette('evss/documents/get_claim_documents') do + VCR.use_cassette('claims_api/bgs/tracked_items/find_tracked_items') do + VCR.use_cassette('claims_api/evss/documents/get_claim_documents') do expect_any_instance_of(bcs) .to receive(:find_benefit_claim_details_by_benefit_claim_id).and_return(claim_contentions) expect(ClaimsApi::AutoEstablishedClaim) @@ -1078,8 +1079,8 @@ claim_contentions[:benefit_claim_details_dto][:contentions] = 'Low back strain (New), Knee, internal derangement (New)' mock_ccg(scopes) do |auth_header| - VCR.use_cassette('bgs/tracked_items/find_tracked_items') do - VCR.use_cassette('evss/documents/get_claim_documents') do + VCR.use_cassette('claims_api/bgs/tracked_items/find_tracked_items') do + VCR.use_cassette('claims_api/evss/documents/get_claim_documents') do expect_any_instance_of(bcs) .to receive(:find_benefit_claim_details_by_benefit_claim_id).and_return(claim_contentions) expect(ClaimsApi::AutoEstablishedClaim) @@ -1103,8 +1104,8 @@ context 'it has documents' do it "returns a claim with 'supporting_documents'" do mock_ccg(scopes) do |auth_header| - VCR.use_cassette('bgs/tracked_items/find_tracked_items') do - VCR.use_cassette('evss/documents/get_claim_documents') do + VCR.use_cassette('claims_api/bgs/tracked_items/find_tracked_items') do + VCR.use_cassette('claims_api/evss/documents/get_claim_documents') do expect_any_instance_of(bcs) .to receive(:find_benefit_claim_details_by_benefit_claim_id).and_return(bgs_claim_response) expect(ClaimsApi::AutoEstablishedClaim) @@ -1131,8 +1132,8 @@ bgs_claim[:benefit_claim_details_dto][:benefit_claim_id] = '222222222' mock_ccg(scopes) do |auth_header| - VCR.use_cassette('bgs/tracked_items/find_tracked_items') do - VCR.use_cassette('evss/documents/get_claim_documents') do + VCR.use_cassette('claims_api/bgs/tracked_items/find_tracked_items') do + VCR.use_cassette('claims_api/evss/documents/get_claim_documents') do expect_any_instance_of(bcs) .to receive(:find_benefit_claim_details_by_benefit_claim_id).and_return(bgs_claim) expect(ClaimsApi::AutoEstablishedClaim) @@ -1163,7 +1164,7 @@ it "returns a claim with 'suporting_documents' as an empty array" do mock_ccg(scopes) do |auth_header| - VCR.use_cassette('bgs/tracked_items/find_tracked_items') do + VCR.use_cassette('claims_api/bgs/tracked_items/find_tracked_items') do expect_any_instance_of(bcs) .to receive(:find_benefit_claim_details_by_benefit_claim_id).and_return(bgs_claim) @@ -1198,7 +1199,7 @@ it "returns a claim with the 'errors' attribute populated" do mock_ccg(scopes) do |auth_header| - VCR.use_cassette('evss/claims/claims') do + VCR.use_cassette('claims_api/evss/claims/claims') do get claim_by_id_path, headers: auth_header json_response = JSON.parse(response.body) @@ -1244,8 +1245,8 @@ it "returns a claim with 'tracked_items'" do mock_ccg(scopes) do |auth_header| - VCR.use_cassette('evss/documents/get_claim_documents') do - VCR.use_cassette('bgs/tracked_item_service/claims_v2_show_tracked_items') do + VCR.use_cassette('claims_api/evss/documents/get_claim_documents') do + VCR.use_cassette('claims_api/bgs/tracked_item_service/claims_v2_show_tracked_items') do allow(ClaimsApi::AutoEstablishedClaim).to receive(:get_by_id_and_icn) get claim_by_id_with_items_path, headers: auth_header @@ -1283,8 +1284,8 @@ it "returns a claim with 'tracked_items'" do mock_ccg(scopes) do |auth_header| - VCR.use_cassette('bgs/tracked_item_service/get_claim_documents_with_tracked_item_ids') do - VCR.use_cassette('bgs/tracked_item_service/claims_v2_show_tracked_items') do + VCR.use_cassette('claims_api/bgs/tracked_item_service/get_claim_documents_with_tracked_item_ids') do + VCR.use_cassette('claims_api/bgs/tracked_item_service/claims_v2_show_tracked_items') do allow(ClaimsApi::AutoEstablishedClaim).to receive(:get_by_id_and_icn) get claim_by_id_with_items_path, headers: auth_header @@ -1315,7 +1316,7 @@ it "returns a claim with 'tracked_items' as an empty array" do mock_ccg(scopes) do |auth_header| - VCR.use_cassette('bgs/tracked_items/find_tracked_items') do + VCR.use_cassette('claims_api/bgs/tracked_items/find_tracked_items') do expect_any_instance_of(bcs) .to receive(:find_benefit_claim_details_by_benefit_claim_id).and_return(bgs_claim) @@ -1345,7 +1346,7 @@ context 'when provided' do context 'when valid' do it 'returns a 200' do - VCR.use_cassette('bgs/tracked_items/find_tracked_items') do + VCR.use_cassette('claims_api/bgs/tracked_items/find_tracked_items') do mock_ccg(scopes) do |auth_header| expect(ClaimsApi::AutoEstablishedClaim) .to receive(:get_by_id_and_icn).and_return(lighthouse_claim) @@ -1377,7 +1378,7 @@ context 'claims show' do it 'returns a 200 response when successful' do - VCR.use_cassette('bgs/tracked_items/find_tracked_items') do + VCR.use_cassette('claims_api/bgs/tracked_items/find_tracked_items') do mock_ccg_for_fine_grained_scope(claims_show_scopes) do |auth_header| expect(ClaimsApi::AutoEstablishedClaim) .to receive(:get_by_id_and_icn).and_return(lighthouse_claim) diff --git a/modules/claims_api/spec/requests/v2/veterans/evidence_waiver_request_spec.rb b/modules/claims_api/spec/requests/v2/veterans/evidence_waiver_request_spec.rb index 93ff2e2cba3..1cf9d712b60 100644 --- a/modules/claims_api/spec/requests/v2/veterans/evidence_waiver_request_spec.rb +++ b/modules/claims_api/spec/requests/v2/veterans/evidence_waiver_request_spec.rb @@ -27,7 +27,7 @@ context 'when success' do it 'returns a 200' do mock_ccg(scopes) do |auth_header| - VCR.use_cassette('bgs/benefit_claim/update_5103_200') do + VCR.use_cassette('claims_api/bgs/benefit_claim/update_5103_200') do allow_any_instance_of(ClaimsApi::LocalBGS) .to receive(:find_by_ssn).and_return({ file_nbr: '123456780' }) @@ -51,7 +51,7 @@ context 'when claim id is not found' do it 'returns a 404' do mock_ccg(scopes) do |auth_header| - VCR.use_cassette('bgs/benefit_claim/find_bnft_claim_400') do + VCR.use_cassette('claims_api/bgs/benefit_claim/find_bnft_claim_400') do allow_any_instance_of(ClaimsApi::LocalBGS) .to receive(:find_by_ssn).and_return({ file_nbr: '123456780' }) @@ -82,7 +82,7 @@ it 'silently passes for an invalid type' do mock_ccg(scopes) do |auth_header| - VCR.use_cassette('bgs/benefit_claim/update_5103_200') do + VCR.use_cassette('claims_api/bgs/benefit_claim/update_5103_200') do allow_any_instance_of(ClaimsApi::LocalBGS) .to receive(:find_by_ssn).and_return({ file_nbr: '123456780' }) post sub_path, params: { sponsorIcn: sponsor_id }, headers: auth_header @@ -96,7 +96,7 @@ context 'when a veteran does not have a file number' do it 'returns an error message' do mock_ccg(scopes) do |auth_header| - VCR.use_cassette('bgs/benefit_claim/update_5103_200') do + VCR.use_cassette('claims_api/bgs/benefit_claim/update_5103_200') do allow_any_instance_of(ClaimsApi::V2::Veterans::EvidenceWaiverController) .to receive(:file_number_check).and_return(@file_number = nil) @@ -120,7 +120,7 @@ context 'evidence waiver' do it 'returns a 200 response when successful' do mock_ccg_for_fine_grained_scope(ews_scopes) do |auth_header| - VCR.use_cassette('bgs/benefit_claim/update_5103_200') do + VCR.use_cassette('claims_api/bgs/benefit_claim/update_5103_200') do post sub_path, headers: auth_header expect(response).to have_http_status(:ok) end diff --git a/modules/claims_api/spec/requests/v2/veterans/power_of_attorney_ind_request_spec.rb b/modules/claims_api/spec/requests/v2/veterans/power_of_attorney_ind_request_spec.rb index 95c25dcb5fa..dee3b666e60 100644 --- a/modules/claims_api/spec/requests/v2/veterans/power_of_attorney_ind_request_spec.rb +++ b/modules/claims_api/spec/requests/v2/veterans/power_of_attorney_ind_request_spec.rb @@ -39,8 +39,7 @@ }, representative: { poaCode: individual_poa_code, - firstName: 'my', - lastName: 'name', + registrationNumber: '12345', type: 'ATTORNEY', address: { addressLine1: '123', @@ -69,8 +68,7 @@ }, representative: { poaCode: individual_poa_code, - firstName: 'my', - lastName: 'name', + registrationNumber: '12345', type: 'ATTORNEY', address: { addressLine1: '123', @@ -81,8 +79,7 @@ } }, claimant: { - firstName: 'first', - lastName: 'last', + claimantId: '1013062086V794840', address: { addressLine1: '123', city: 'city', @@ -112,15 +109,16 @@ context 'when provided' do context 'when valid' do it 'returns a 202' do - mock_ccg(scopes) do |auth_header| - expect_any_instance_of(local_bgs).to receive(:find_poa_by_participant_id) - .and_return(bgs_poa) - allow_any_instance_of(local_bgs).to receive(:find_poa_history_by_ptcpnt_id) - .and_return({ person_poa_history: nil }) - - post appoint_individual_path, params: data.to_json, headers: auth_header + VCR.use_cassette('claims_api/mpi/find_candidate/valid_icn_full') do + mock_ccg(scopes) do |auth_header| + expect_any_instance_of(local_bgs).to receive(:find_poa_by_participant_id) + .and_return(bgs_poa) + allow_any_instance_of(local_bgs).to receive(:find_poa_history_by_ptcpnt_id) + .and_return({ person_poa_history: nil }) - expect(response).to have_http_status(:accepted) + post appoint_individual_path, params: data.to_json, headers: auth_header + expect(response).to have_http_status(:accepted) + end end end end @@ -147,8 +145,9 @@ it 'returns a 202 when all conditionally required data is present' do mock_ccg(scopes) do |auth_header| - post appoint_individual_path, params: claimant_data.to_json, headers: auth_header - + VCR.use_cassette('claims_api/mpi/find_candidate/valid_icn_full') do + post appoint_individual_path, params: claimant_data.to_json, headers: auth_header + end expect(response).to have_http_status(:accepted) end end @@ -156,9 +155,9 @@ it 'returns a 422 if claimant.address.addressLine1 is not provided' do mock_ccg(scopes) do |auth_header| claimant_data[:data][:attributes][:claimant][:address][:addressLine1] = nil - - post appoint_individual_path, params: claimant_data.to_json, headers: auth_header - + VCR.use_cassette('claims_api/mpi/find_candidate/valid_icn_full') do + post appoint_individual_path, params: claimant_data.to_json, headers: auth_header + end expect(response).to have_http_status(:unprocessable_entity) response_body = JSON.parse(response.body) expect(response_body['errors'][0]['detail']).to eq( @@ -170,9 +169,9 @@ it 'returns a 422 if claimant.address.city is not provided' do mock_ccg(scopes) do |auth_header| claimant_data[:data][:attributes][:claimant][:address][:city] = nil - - post appoint_individual_path, params: claimant_data.to_json, headers: auth_header - + VCR.use_cassette('claims_api/mpi/find_candidate/valid_icn_full') do + post appoint_individual_path, params: claimant_data.to_json, headers: auth_header + end expect(response).to have_http_status(:unprocessable_entity) response_body = JSON.parse(response.body) expect(response_body['errors'][0]['detail']).to eq( @@ -184,9 +183,9 @@ it 'returns a 422 if claimant.address.stateCode is not provided' do mock_ccg(scopes) do |auth_header| claimant_data[:data][:attributes][:claimant][:address][:stateCode] = nil - - post appoint_individual_path, params: claimant_data.to_json, headers: auth_header - + VCR.use_cassette('claims_api/mpi/find_candidate/valid_icn_full') do + post appoint_individual_path, params: claimant_data.to_json, headers: auth_header + end expect(response).to have_http_status(:unprocessable_entity) response_body = JSON.parse(response.body) expect(response_body['errors'][0]['detail']).to eq( @@ -198,9 +197,9 @@ it 'returns a 422 if claimant.address.country is not provided' do mock_ccg(scopes) do |auth_header| claimant_data[:data][:attributes][:claimant][:address][:country] = nil - - post appoint_individual_path, params: claimant_data.to_json, headers: auth_header - + VCR.use_cassette('claims_api/mpi/find_candidate/valid_icn_full') do + post appoint_individual_path, params: claimant_data.to_json, headers: auth_header + end expect(response).to have_http_status(:unprocessable_entity) response_body = JSON.parse(response.body) expect(response_body['errors'][0]['detail']).to eq( @@ -212,9 +211,9 @@ it 'returns a 422 if claimant.address.zipCode is not provided' do mock_ccg(scopes) do |auth_header| claimant_data[:data][:attributes][:claimant][:address][:zipCode] = nil - - post appoint_individual_path, params: claimant_data.to_json, headers: auth_header - + VCR.use_cassette('claims_api/mpi/find_candidate/valid_icn_full') do + post appoint_individual_path, params: claimant_data.to_json, headers: auth_header + end expect(response).to have_http_status(:unprocessable_entity) response_body = JSON.parse(response.body) expect(response_body['errors'][0]['detail']).to eq( @@ -224,16 +223,17 @@ end it 'returns a 422 if claimant.relationship is not provided' do - mock_ccg(scopes) do |auth_header| - claimant_data[:data][:attributes][:claimant][:relationship] = nil - - post appoint_individual_path, params: claimant_data.to_json, headers: auth_header - - expect(response).to have_http_status(:unprocessable_entity) - response_body = JSON.parse(response.body) - expect(response_body['errors'][0]['detail']).to eq( - "If claimant is present 'relationship' must be filled in" - ) + VCR.use_cassette('claims_api/mpi/find_candidate/valid_icn_full') do + mock_ccg(scopes) do |auth_header| + claimant_data[:data][:attributes][:claimant][:relationship] = nil + + post appoint_individual_path, params: claimant_data.to_json, headers: auth_header + expect(response).to have_http_status(:unprocessable_entity) + response_body = JSON.parse(response.body) + expect(response_body['errors'][0]['detail']).to eq( + "If claimant is present 'relationship' must be filled in" + ) + end end end end @@ -257,8 +257,6 @@ }, representative: { poaCode: individual_poa_code, - firstName: 'my', - lastName: 'name', type: 'ATTORNEY', address: { addressLine1: '123', @@ -348,10 +346,10 @@ it 'returns a meaningful 404' do mock_ccg(%w[claim.write claim.read]) do |auth_header| - detail = 'Could not find an Accredited Representative with code: aaa' + detail = 'Could not find an Accredited Representative with registration number: 67890 ' \ + 'and poa code: aaa' post validate2122a_path, params: request_body, headers: auth_header - response_body = JSON.parse(response.body)['errors'][0] expect(response).to have_http_status(:not_found) @@ -380,6 +378,45 @@ end end end + + context 'when no claimantId is provided and other claimant data is present' do + let(:request_body) do + Rails.root.join('modules', 'claims_api', 'spec', 'fixtures', 'v2', 'veterans', + 'power_of_attorney', '2122a', 'valid.json').read + end + + let(:claimant) do + { + email: 'lillian@disney.com', + relationship: 'Spouse', + address: { + addressLine1: '2688 S Camino Real', + city: 'Palm Springs', + stateCode: 'CA', + country: 'US', + zipCode: '92264' + }, + phone: { + areaCode: '555', + phoneNumber: '5551337' + } + } + end + let(:error_msg) { "If claimant is present 'claimantId' must be filled in" } + + it 'returns a meaningful 422' do + mock_ccg(%w[claim.write claim.read]) do |auth_header| + json = JSON.parse(request_body) + json['data']['attributes']['claimant'] = claimant + request_body = json.to_json + post validate2122a_path, params: request_body, headers: auth_header + + response_body = JSON.parse(response.body)['errors'][0] + expect(response).to have_http_status(:unprocessable_entity) + expect(response_body['detail']).to eq(error_msg) + end + end + end end end end diff --git a/modules/claims_api/spec/requests/v2/veterans/power_of_attorney_org_request_spec.rb b/modules/claims_api/spec/requests/v2/veterans/power_of_attorney_org_request_spec.rb index 4d90fd57f7d..e9016bdde23 100644 --- a/modules/claims_api/spec/requests/v2/veterans/power_of_attorney_org_request_spec.rb +++ b/modules/claims_api/spec/requests/v2/veterans/power_of_attorney_org_request_spec.rb @@ -38,7 +38,8 @@ } }, serviceOrganization: { - poaCode: organization_poa_code.to_s + poaCode: organization_poa_code.to_s, + registrationNumber: '67890' } } } @@ -77,7 +78,7 @@ let(:poa_scopes) { %w[system/claim.write] } context 'POA organization' do - it 'returns a 200 response when successful' do + it 'returns a 202 response when successful' do mock_ccg_for_fine_grained_scope(poa_scopes) do |auth_header| expect_any_instance_of(local_bgs).to receive(:find_poa_by_participant_id) .and_return(bgs_poa) @@ -99,6 +100,29 @@ end end end + + context 'multiple reps with same poa code and registration number' do + let(:rep_id) do + Veteran::Service::Representative.create!(representative_id: '67890', poa_codes: [organization_poa_code], + first_name: 'George', last_name: 'Washington-test').id + end + + it 'returns the last one with a 202 response' do + mock_ccg(scopes) do |auth_header| + expect_any_instance_of(local_bgs).to receive(:find_poa_by_participant_id) + .and_return(bgs_poa) + allow_any_instance_of(local_bgs).to receive(:find_poa_history_by_ptcpnt_id) + .and_return({ person_poa_history: nil }) + expect(ClaimsApi::V2::PoaFormBuilderJob).to receive(:perform_async) do |*args| + expect(args[2]).to eq(rep_id) + end + + post appoint_organization_path, params: data.to_json, headers: auth_header + + expect(response).to have_http_status(:accepted) + end + end + end end describe 'validate2122' do @@ -157,7 +181,8 @@ it 'returns a meaningful 404' do mock_ccg(%w[claim.write claim.read]) do |auth_header| - detail = 'Could not find an Organization with code: aaa' + detail = 'Could not find an Accredited Representative with registration number: 67890 ' \ + 'and poa code: aaa' post validate2122_path, params: request_body, headers: auth_header @@ -189,6 +214,47 @@ end end end + + context 'when no claimantId is provided and other claimant data is present' do + let(:request_body) do + Rails.root.join('modules', 'claims_api', 'spec', 'fixtures', 'v2', 'veterans', + 'power_of_attorney', '2122', 'valid.json').read + end + + let(:claimant) do + { + email: 'lillian@disney.com', + relationship: 'Spouse', + address: { + addressLine1: '2688 S Camino Real', + city: 'Palm Springs', + stateCode: 'CA', + country: 'US', + zipCode: '92264' + }, + phone: { + areaCode: '555', + phoneNumber: '5551337' + } + } + end + let(:error_msg) { "If claimant is present 'claimantId' must be filled in" } + + it 'returns a meaningful 422' do + VCR.use_cassette('claims_api/mpi/find_candidate/valid_icn_full') do + mock_ccg(%w[claim.write claim.read]) do |auth_header| + json = JSON.parse(request_body) + json['data']['attributes']['claimant'] = claimant + request_body = json.to_json + post validate2122_path, params: request_body, headers: auth_header + + response_body = JSON.parse(response.body)['errors'][0] + expect(response).to have_http_status(:unprocessable_entity) + expect(response_body['detail']).to eq(error_msg) + end + end + end + end end end end diff --git a/modules/claims_api/spec/requests/v2/veterans/rswag_claims_request_spec.rb b/modules/claims_api/spec/requests/v2/veterans/rswag_claims_request_spec.rb index c4600dee1d7..8dfb2d682cd 100644 --- a/modules/claims_api/spec/requests/v2/veterans/rswag_claims_request_spec.rb +++ b/modules/claims_api/spec/requests/v2/veterans/rswag_claims_request_spec.rb @@ -61,7 +61,7 @@ before do |example| mock_ccg(scopes) do - VCR.use_cassette('bgs/tracked_items/find_tracked_items') do + VCR.use_cassette('claims_api/bgs/tracked_items/find_tracked_items') do expect_any_instance_of(bcs) .to receive(:find_benefit_claims_status_by_ptcpnt_id).and_return(bgs_response) expect(ClaimsApi::AutoEstablishedClaim) @@ -181,8 +181,8 @@ before do |example| mock_ccg(scopes) do - VCR.use_cassette('bgs/tracked_item_service/claims_v2_show_tracked_items') do - VCR.use_cassette('evss/documents/get_claim_documents') do + VCR.use_cassette('claims_api/bgs/tracked_item_service/claims_v2_show_tracked_items') do + VCR.use_cassette('claims_api/evss/documents/get_claim_documents') do bgs_response[:benefit_claim_details_dto][:ptcpnt_vet_id] = target_veteran.participant_id expect_any_instance_of(bcs) .to receive(:find_benefit_claim_details_by_benefit_claim_id).and_return(bgs_response) diff --git a/modules/claims_api/spec/requests/v2/veterans/rswag_intent_to_file_request_spec.rb b/modules/claims_api/spec/requests/v2/veterans/rswag_intent_to_file_request_spec.rb index 190344828d7..063e858df64 100644 --- a/modules/claims_api/spec/requests/v2/veterans/rswag_intent_to_file_request_spec.rb +++ b/modules/claims_api/spec/requests/v2/veterans/rswag_intent_to_file_request_spec.rb @@ -296,7 +296,7 @@ stub_poa_verification mock_ccg(scopes) do - VCR.use_cassette('bgs/intent_to_file_web_service/insert_intent_to_file') do + VCR.use_cassette('claims_api/bgs/intent_to_file_web_service/insert_intent_to_file') do submit_request(example.metadata) end end 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..9c17a58ad68 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 @@ -57,8 +57,7 @@ first_name: 'Firstname', last_name: 'Lastname', phone: '555-555-5555').save! - mock_ccg(scopes) do |auth_header| - Authorization = auth_header # rubocop:disable Naming/ConstantName + mock_ccg(scopes) do submit_request(example.metadata) end end @@ -121,8 +120,7 @@ first_name: 'Another', last_name: 'Name', phone: '222-222-2222').save! - mock_ccg(scopes) do |auth_header| - Authorization = auth_header # rubocop:disable Naming/ConstantName + mock_ccg(scopes) do submit_request(example.metadata) end end @@ -143,7 +141,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' @@ -191,8 +189,7 @@ first_name: 'Firstname', last_name: 'Lastname', phone: '555-555-5555').save! - mock_ccg(scopes) do |auth_header| - Authorization = auth_header # rubocop:disable Naming/ConstantName + mock_ccg(scopes) do submit_request(example.metadata) end end @@ -312,7 +309,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' @@ -355,9 +352,11 @@ Veteran::Service::Organization.create!(poa: organization_poa_code, name: "#{organization_poa_code} - DISABLED AMERICAN VETERANS", phone: '555-555-5555') + Veteran::Service::Representative.create!(representative_id: '67890', poa_codes: [organization_poa_code], + first_name: 'Firstname', last_name: 'Lastname', + phone: '555-555-5555') - mock_ccg(scopes) do |auth_header| - Authorization = auth_header # rubocop:disable Naming/ConstantName + mock_ccg(scopes) do submit_request(example.metadata) end end @@ -413,10 +412,9 @@ end before do |example| - mock_ccg(scopes) do |auth_header| + mock_ccg(scopes) do allow_any_instance_of(local_bgs).to receive(:find_poa_history_by_ptcpnt_id) .and_return({ person_poa_history: nil }) - Authorization = auth_header # rubocop:disable Naming/ConstantName submit_request(example.metadata) end end @@ -468,7 +466,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' @@ -512,7 +510,7 @@ end before do |example| - Veteran::Service::Representative.new(representative_id: '12345', + Veteran::Service::Representative.new(representative_id: '67890', poa_codes: [poa_code], first_name: 'Firstname', last_name: 'Lastname', @@ -638,7 +636,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' @@ -681,6 +679,9 @@ before do |example| Veteran::Service::Organization.create!(poa: poa_code) + Veteran::Service::Representative.create!(representative_id: '67890', poa_codes: [poa_code], + first_name: 'Firstname', last_name: 'Lastname', + phone: '555-555-5555') mock_ccg(scopes) do submit_request(example.metadata) @@ -796,7 +797,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' @@ -832,8 +833,7 @@ 'veterans', 'power_of_attorney', 'status.json'))) before do |example| - mock_ccg(scopes) do |auth_header| - Authorization = auth_header # rubocop:disable Naming/ConstantName + mock_ccg(scopes) do submit_request(example.metadata) end end diff --git a/modules/claims_api/spec/sidekiq/ews_updater_spec.rb b/modules/claims_api/spec/sidekiq/ews_updater_spec.rb index 942ff238ad1..64cbc63c84d 100644 --- a/modules/claims_api/spec/sidekiq/ews_updater_spec.rb +++ b/modules/claims_api/spec/sidekiq/ews_updater_spec.rb @@ -16,7 +16,7 @@ context 'when waiver consent is present and allowed' do it 'updates evidence waiver record for a qualifying ews submittal' do - VCR.use_cassette('bgs/benefit_claim/update_5103_claim') do + VCR.use_cassette('claims_api/bgs/benefit_claim/update_5103_claim') do subject.new.perform(ews.id) ews.reload diff --git a/modules/claims_api/spec/sidekiq/v2/disability_compensation_benefits_documents_uploader_spec.rb b/modules/claims_api/spec/sidekiq/v2/disability_compensation_benefits_documents_uploader_spec.rb index 6eca37d6bdd..1b473800722 100644 --- a/modules/claims_api/spec/sidekiq/v2/disability_compensation_benefits_documents_uploader_spec.rb +++ b/modules/claims_api/spec/sidekiq/v2/disability_compensation_benefits_documents_uploader_spec.rb @@ -57,7 +57,7 @@ end it 'the claim should still be established on a successful BD submission' do - VCR.use_cassette('bd/upload') do + VCR.use_cassette('claims_api/bd/upload') do expect(claim.status).to eq('pending') # where we start service.perform(claim.id) diff --git a/modules/claims_api/spec/sidekiq/v2/poa_form_builder_job_spec.rb b/modules/claims_api/spec/sidekiq/v2/poa_form_builder_job_spec.rb index af45a521059..a1e7ba21d7f 100644 --- a/modules/claims_api/spec/sidekiq/v2/poa_form_builder_job_spec.rb +++ b/modules/claims_api/spec/sidekiq/v2/poa_form_builder_job_spec.rb @@ -8,13 +8,17 @@ let(:power_of_attorney) { create(:power_of_attorney, :with_full_headers) } let(:poa_code) { 'ABC' } + let(:rep) do + create(:representative, representative_id: '1234', poa_codes: [poa_code], first_name: 'Bob', + last_name: 'Representative') + end before do Sidekiq::Job.clear_all end describe 'generating and uploading the signed pdf' do - context '2122a' do + context '2122a veteran claimant' do before do power_of_attorney.form_data = { recordConsent: true, @@ -22,11 +26,113 @@ consentLimits: %w[DRUG_ABUSE SICKLE_CELL], veteran: { address: { - numberAndStreet: '2719 Hyperion Ave', + addressLine1: '2719 Hyperion Ave', city: 'Los Angeles', - state: 'CA', + stateCode: 'CA', country: 'US', - zipFirstFive: '92264' + zipCode: '92264' + }, + phone: { + areaCode: '555', + phoneNumber: '5551337' + } + }, + representative: { + poaCode: poa_code.to_s, + registrationNumber: '1234', + type: 'ATTORNEY', + address: { + addressLine1: '2719 Hyperion Ave', + city: 'Los Angeles', + stateCode: 'CA', + country: 'US', + zipCode: '92264' + } + } + } + power_of_attorney.save + end + + it 'generates e-signatures correctly for a veteran claimant' do + VCR.use_cassette('claims_api/mpi/find_candidate/valid_icn_full') do + data = power_of_attorney + .form_data + .deep_merge( + { + 'veteran' => { + 'firstName' => power_of_attorney.auth_headers['va_eauth_firstName'], + 'lastName' => power_of_attorney.auth_headers['va_eauth_lastName'], + 'ssn' => power_of_attorney.auth_headers['va_eauth_pnid'], + 'birthdate' => power_of_attorney.auth_headers['va_eauth_birthdate'] + } + } + ) + final_data = data.deep_merge( + { + 'text_signatures' => { + 'page2' => [ + { + 'signature' => 'JESSE GRAY - signed via api.va.gov', + 'x' => 35, + 'y' => 306 + }, + { + 'signature' => 'Bob Representative - signed via api.va.gov', + 'x' => 35, + 'y' => 200 + } + ] + }, + 'representative' => { + 'firstName' => 'Bob', + 'lastName' => 'Representative' + } + } + ) + + allow_any_instance_of(BGS::PersonWebService).to receive(:find_by_ssn).and_return({ file_nbr: '123456789' }) + expect_any_instance_of(ClaimsApi::V2::PoaPdfConstructor::Individual) + .to receive(:construct) + .with(final_data, id: power_of_attorney.id) + .and_call_original + + subject.new.perform(power_of_attorney.id, '2122A', rep.id) + end + end + + it 'Calls the POA updater job upon successful upload to VBMS' do + VCR.use_cassette('claims_api/mpi/find_candidate/valid_icn_full') do + token_response = OpenStruct.new(upload_token: '<{573F054F-E9F7-4BF2-8C66-D43ADA5C62E7}') + document_response = OpenStruct.new(upload_document_response: { + '@new_document_version_ref_id' => '{52300B69-1D6E-43B2-8BEB-67A7C55346A2}', + '@document_series_ref_id' => '{A57EF6CC-2236-467A-BA4F-1FA1EFD4B374}' + }.with_indifferent_access) + + allow_any_instance_of(ClaimsApi::VBMSUploader).to receive(:fetch_upload_token).and_return(token_response) + allow_any_instance_of(ClaimsApi::VBMSUploader).to receive(:upload_document).and_return(document_response) + allow_any_instance_of(BGS::PersonWebService).to receive(:find_by_ssn).and_return({ file_nbr: '123456789' }) + + expect(ClaimsApi::PoaUpdater).to receive(:perform_async) + + subject.new.perform(power_of_attorney.id, '2122A', rep.id) + end + end + end + + context '2122a non-veteran claimant' do + before do + power_of_attorney.form_data = { + recordConsent: true, + consentAddressChange: true, + consentLimits: %w[DRUG_ABUSE SICKLE_CELL], + veteran: { + serviceBranch: 'ARMY', + address: { + addressLine1: '2719 Hyperion Ave', + city: 'Los Angeles', + stateCode: 'CA', + country: 'US', + zipCode: '92264' }, phone: { areaCode: '555', @@ -34,35 +140,119 @@ } }, claimant: { - firstName: 'Lillian', - middleInitial: 'A', - lastName: 'Disney', + claimantId: '1012830872V584140', email: 'lillian@disney.com', relationship: 'Spouse', address: { - numberAndStreet: '2688 S Camino Real', + addressLine1: '2688 S Camino Real', city: 'Palm Springs', - state: 'CA', + stateCode: 'CA', country: 'US', - zipFirstFive: '92264' + zipCode: '92264' }, phone: { areaCode: '555', phoneNumber: '5551337' - } + }, + firstName: 'Mitchell', + lastName: 'Jenkins' }, representative: { poaCode: poa_code.to_s, - type: 'ATTORNEY', + registrationNumber: '1234', + type: 'SERVICE ORGANIZATION REPRESENTATIVE', firstName: 'Bob', lastName: 'Representative', - organizationName: 'I Help Vets LLC', address: { - numberAndStreet: '2719 Hyperion Ave', + addressLine1: '2719 Hyperion Ave', + city: 'Los Angeles', + stateCode: 'CA', + country: 'US', + zipCode: '92264' + } + } + } + power_of_attorney.save + end + + it 'generates e-signatures correctly for a non-veteran claimant' do + VCR.use_cassette('claims_api/mpi/find_candidate/valid_icn_full') do + data = power_of_attorney + .form_data + .deep_merge( + { + 'veteran' => { + 'firstName' => power_of_attorney.auth_headers['va_eauth_firstName'], + 'lastName' => power_of_attorney.auth_headers['va_eauth_lastName'], + 'ssn' => power_of_attorney.auth_headers['va_eauth_pnid'], + 'birthdate' => power_of_attorney.auth_headers['va_eauth_birthdate'] + } + } + ) + final_data = data.deep_merge( + { + 'text_signatures' => { + 'page2' => [ + { + 'signature' => 'Mitchell Jenkins - signed via api.va.gov', + 'x' => 35, + 'y' => 306 + }, + { + 'signature' => 'Bob Representative - signed via api.va.gov', + 'x' => 35, + 'y' => 200 + } + ] + }, + 'representative' => { + 'firstName' => 'Bob', + 'lastName' => 'Representative' + } + } + ) + + allow_any_instance_of(BGS::PersonWebService).to receive(:find_by_ssn).and_return({ file_nbr: '123456789' }) + expect_any_instance_of(ClaimsApi::V2::PoaPdfConstructor::Individual) + .to receive(:construct) + .with(final_data, id: power_of_attorney.id) + .and_call_original + + subject.new.perform(power_of_attorney.id, '2122A', rep.id) + end + end + end + + context '2122 veteran claimant' do + let!(:org) { create(:organization, name: 'I Help Vets LLC', poa: poa_code) } + + before do + power_of_attorney.form_data = { + recordConsent: true, + consentAddressChange: true, + consentLimits: %w[DRUG_ABUSE SICKLE_CELL], + veteran: { + address: { + addressLine1: '2719 Hyperion Ave', + city: 'Los Angeles', + stateCode: 'CA', + country: 'US', + zipCode: '92264' + }, + phone: { + areaCode: '555', + phoneNumber: '5551337' + } + }, + serviceOrganization: { + poaCode: poa_code.to_s, + registrationNumber: '1234', + address: { + addressLine1: '2719 Hyperion Ave', city: 'Los Angeles', - state: 'CA', + stateCode: 'CA', country: 'US', - zipFirstFive: '92264' + zipCode: '92264' } } } @@ -82,26 +272,14 @@ } } ) - final_data = data.merge( + final_data = data.deep_merge( { 'text_signatures' => { - 'page1' => [ - { - 'signature' => 'JESSE GRAY - signed via api.va.gov', - 'x' => 35, - 'y' => 73 - }, - { - 'signature' => 'Bob Representative - signed via api.va.gov', - 'x' => 35, - 'y' => 100 - } - ], 'page2' => [ { 'signature' => 'JESSE GRAY - signed via api.va.gov', 'x' => 35, - 'y' => 306 + 'y' => 240 }, { 'signature' => 'Bob Representative - signed via api.va.gov', @@ -109,17 +287,24 @@ 'y' => 200 } ] + }, + 'serviceOrganization' => { + 'firstName' => 'Bob', + 'lastName' => 'Representative', + 'organizationName' => 'I Help Vets LLC' } } ) allow_any_instance_of(BGS::PersonWebService).to receive(:find_by_ssn).and_return({ file_nbr: '123456789' }) - expect_any_instance_of(ClaimsApi::V2::PoaPdfConstructor::Individual) - .to receive(:construct) - .with(final_data, id: power_of_attorney.id) - .and_call_original + VCR.use_cassette('claims_api/mpi/find_candidate/valid_icn_full') do + expect_any_instance_of(ClaimsApi::V2::PoaPdfConstructor::Organization) + .to receive(:construct) + .with(final_data, id: power_of_attorney.id) + .and_call_original - subject.new.perform(power_of_attorney.id, '2122A') + subject.new.perform(power_of_attorney.id, '2122', rep.id) + end end it 'Calls the POA updater job upon successful upload to VBMS' do @@ -132,14 +317,17 @@ allow_any_instance_of(ClaimsApi::VBMSUploader).to receive(:fetch_upload_token).and_return(token_response) allow_any_instance_of(ClaimsApi::VBMSUploader).to receive(:upload_document).and_return(document_response) allow_any_instance_of(BGS::PersonWebService).to receive(:find_by_ssn).and_return({ file_nbr: '123456789' }) + VCR.use_cassette('claims_api/mpi/find_candidate/valid_icn_full') do + expect(ClaimsApi::PoaUpdater).to receive(:perform_async) - expect(ClaimsApi::PoaUpdater).to receive(:perform_async) - - subject.new.perform(power_of_attorney.id, '2122A') + subject.new.perform(power_of_attorney.id, '2122', rep.id) + end end end - context '2122' do + context '2122 non-veteran claimant' do + let!(:org) { create(:organization, name: 'I Help Vets LLC', poa: poa_code) } + before do power_of_attorney.form_data = { recordConsent: true, @@ -147,11 +335,11 @@ consentLimits: %w[DRUG_ABUSE SICKLE_CELL], veteran: { address: { - numberAndStreet: '2719 Hyperion Ave', + addressLine1: '2719 Hyperion Ave', city: 'Los Angeles', - state: 'CA', + stateCode: 'CA', country: 'US', - zipFirstFive: '92264' + zipCode: '92264' }, phone: { areaCode: '555', @@ -159,34 +347,32 @@ } }, claimant: { - firstName: 'Lillian', - middleInitial: 'A', - lastName: 'Disney', + claimantId: '1012830872V584140', email: 'lillian@disney.com', relationship: 'Spouse', address: { - numberAndStreet: '2688 S Camino Real', + addressLine1: '2688 S Camino Real', city: 'Palm Springs', - state: 'CA', + stateCode: 'CA', country: 'US', - zipFirstFive: '92264' + zipCode: '92264' }, phone: { areaCode: '555', phoneNumber: '5551337' - } + }, + firstName: 'Mitchell', + lastName: 'Jenkins' }, serviceOrganization: { poaCode: poa_code.to_s, - firstName: 'Bob', - lastName: 'Representative', - organizationName: 'I Help Vets LLC', + registrationNumber: '1234', address: { - numberAndStreet: '2719 Hyperion Ave', + addressLine1: '2719 Hyperion Ave', city: 'Los Angeles', - state: 'CA', + stateCode: 'CA', country: 'US', - zipFirstFive: '92264' + zipCode: '92264' } } } @@ -206,12 +392,12 @@ } } ) - final_data = data.merge( + final_data = data.deep_merge( { 'text_signatures' => { 'page2' => [ { - 'signature' => 'JESSE GRAY - signed via api.va.gov', + 'signature' => 'Mitchell Jenkins - signed via api.va.gov', 'x' => 35, 'y' => 240 }, @@ -221,33 +407,24 @@ 'y' => 200 } ] + }, + 'serviceOrganization' => { + 'firstName' => 'Bob', + 'lastName' => 'Representative', + 'organizationName' => 'I Help Vets LLC' } } ) allow_any_instance_of(BGS::PersonWebService).to receive(:find_by_ssn).and_return({ file_nbr: '123456789' }) - expect_any_instance_of(ClaimsApi::V2::PoaPdfConstructor::Organization) - .to receive(:construct) - .with(final_data, id: power_of_attorney.id) - .and_call_original - - subject.new.perform(power_of_attorney.id, '2122') - end - - it 'Calls the POA updater job upon successful upload to VBMS' do - token_response = OpenStruct.new(upload_token: '<{573F054F-E9F7-4BF2-8C66-D43ADA5C62E7}') - document_response = OpenStruct.new(upload_document_response: { - '@new_document_version_ref_id' => '{52300B69-1D6E-43B2-8BEB-67A7C55346A2}', - '@document_series_ref_id' => '{A57EF6CC-2236-467A-BA4F-1FA1EFD4B374}' - }.with_indifferent_access) - - allow_any_instance_of(ClaimsApi::VBMSUploader).to receive(:fetch_upload_token).and_return(token_response) - allow_any_instance_of(ClaimsApi::VBMSUploader).to receive(:upload_document).and_return(document_response) - allow_any_instance_of(BGS::PersonWebService).to receive(:find_by_ssn).and_return({ file_nbr: '123456789' }) - - expect(ClaimsApi::PoaUpdater).to receive(:perform_async) + VCR.use_cassette('claims_api/mpi/find_candidate/valid_icn_full') do + expect_any_instance_of(ClaimsApi::V2::PoaPdfConstructor::Organization) + .to receive(:construct) + .with(final_data, id: power_of_attorney.id) + .and_call_original - subject.new.perform(power_of_attorney.id, '2122') + subject.new.perform(power_of_attorney.id, '2122', rep.id) + end end end end diff --git a/modules/claims_api/spec/sidekiq/vbms_upload_job_spec.rb b/modules/claims_api/spec/sidekiq/vbms_upload_job_spec.rb index 17d01eb5b5c..432118e1fc9 100644 --- a/modules/claims_api/spec/sidekiq/vbms_upload_job_spec.rb +++ b/modules/claims_api/spec/sidekiq/vbms_upload_job_spec.rb @@ -24,7 +24,7 @@ let(:power_of_attorney) { create(:power_of_attorney) } it 'responds properly when there is a 500 error' do - VCR.use_cassette('vbms/document_upload_500') do + VCR.use_cassette('claims_api/vbms/document_upload_500') do allow_any_instance_of(BGS::PersonWebService) .to receive(:find_by_ssn).and_return({ file_nbr: '123456789' }) @@ -35,7 +35,7 @@ end it 'creates a second job if there is a failure' do - VCR.use_cassette('vbms/document_upload_500') do + VCR.use_cassette('claims_api/vbms/document_upload_500') do allow_any_instance_of(BGS::PersonWebService) .to receive(:find_by_ssn).and_return({ file_nbr: '123456789' }) expect(ClaimsApi::PoaUpdater).not_to receive(:perform_async) @@ -46,7 +46,7 @@ end it 'does not create an new job if had 5 failures' do - VCR.use_cassette('vbms/document_upload_500') do + VCR.use_cassette('claims_api/vbms/document_upload_500') do allow_any_instance_of(BGS::PersonWebService) .to receive(:find_by_ssn).and_return({ file_nbr: '123456789' }) expect(ClaimsApi::PoaUpdater).not_to receive(:perform_async) @@ -70,7 +70,7 @@ allow_any_instance_of(ClaimsApi::VBMSUploader).to receive(:fetch_upload_token).and_return(token_response) allow_any_instance_of(ClaimsApi::VBMSUploader).to receive(:upload_document).and_return(document_response) - VCR.use_cassette('vbms/document_upload_success') do + VCR.use_cassette('claims_api/vbms/document_upload_success') do expect(ClaimsApi::PoaUpdater).to receive(:perform_async) subject.new.perform(power_of_attorney.id) @@ -83,7 +83,7 @@ end it 'rescues file not found from S3, updates POA record, and re-raises to allow Sidekiq retries' do - VCR.use_cassette('vbms/document_upload_success') do + VCR.use_cassette('claims_api/vbms/document_upload_success') do token_response = OpenStruct.new(upload_token: '<{573F054F-E9F7-4BF2-8C66-D43ADA5C62E7}') OpenStruct.new(upload_document_response: { '@new_document_version_ref_id' => '{52300B69-1D6E-43B2-8BEB-67A7C55346A2}', @@ -101,7 +101,7 @@ end it "rescues 'VBMS::FilenumberDoesNotExist' error, updates record, and re-raises exception" do - VCR.use_cassette('vbms/document_upload_success') do + VCR.use_cassette('claims_api/vbms/document_upload_success') do allow_any_instance_of(BGS::PersonWebService) .to receive(:find_by_ssn).and_return({ file_nbr: '123456789' }) allow_any_instance_of(ClaimsApi::VBMSUploader).to receive(:fetch_upload_token) @@ -118,7 +118,7 @@ end it 'uploads to VBMS' do - VCR.use_cassette('vbms/document_upload_success') do + VCR.use_cassette('claims_api/vbms/document_upload_success') do token_response = OpenStruct.new(upload_token: '<{573F054F-E9F7-4BF2-8C66-D43ADA5C62E7}') response = OpenStruct.new(upload_document_response: { '@new_document_version_ref_id' => '{52300B69-1D6E-43B2-8BEB-67A7C55346A2}', diff --git a/modules/claims_api/spec/support/auth_helper.rb b/modules/claims_api/spec/support/auth_helper.rb index 4dd2e5db343..a3f0fa86f22 100644 --- a/modules/claims_api/spec/support/auth_helper.rb +++ b/modules/claims_api/spec/support/auth_helper.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true def mock_acg(_scopes) - VCR.use_cassette('token_validation/v3/indicates_token_is_valid_sandbox') do - VCR.use_cassette('token_validation/v3/userinfo_sandbox') do + VCR.use_cassette('claims_api/token_validation/v3/indicates_token_is_valid_sandbox') do + VCR.use_cassette('claims_api/token_validation/v3/userinfo_sandbox') do profile = build(:mpi_profile, given_names: %w[abraham], family_name: 'lincoln', ssn: '796111863') profile_response = build(:find_profile_response, profile:) allow_any_instance_of(MPI::Service).to receive(:find_profile_by_identifier).and_return(profile_response) @@ -14,14 +14,15 @@ def mock_acg(_scopes) end def mock_ccg(_scopes) - VCR.use_cassette('token_validation/v3/shows_token_is_valid') do + VCR.use_cassette('claims_api/token_validation/v3/shows_token_is_valid') do auth_header = { authorization: 'Bearer token' } yield(auth_header) end end def mock_ccg_for_fine_grained_scope(scope_names) - VCR.use_cassette('token_validation/v3/shows_token_is_valid_with_fine_grained_scope', erb: { scopes: scope_names }) do + VCR.use_cassette('claims_api/token_validation/v3/shows_token_is_valid_with_fine_grained_scope', + erb: { scopes: scope_names }) do auth_header = { authorization: 'Bearer token' } yield(auth_header) end diff --git a/modules/claims_api/spec/support/bgs_client_helpers.rb b/modules/claims_api/spec/support/bgs_client_helpers.rb new file mode 100644 index 00000000000..8cd2c5f8ebc --- /dev/null +++ b/modules/claims_api/spec/support/bgs_client_helpers.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +module BGSClientHelpers + # If one finds this request matcher useful elsewhere in the future, + # Rather than using a callable custom request matcher: + # https://benoittgt.github.io/vcr/#/request_matching/custom_matcher?id=use-a-callable-as-a-custom-request-matcher + # This could instead be registered as a named custom request matcher: + # https://benoittgt.github.io/vcr/#/request_matching/custom_matcher?id=register-a-named-custom-matcher + # Called `:body_as_xml` as inspired by `:body_as_json`: + # https://benoittgt.github.io/vcr/#/request_matching/body_as_json?id=matching-on-body + body_as_xml_matcher = + lambda do |req_a, req_b| + # I suspect that this is not a fully correct implementation of XML + # equality but that there is a fully correct implementation of it + # somewhere out there. + xml_a = Nokogiri::XML(req_a.body, &:noblanks).canonicalize + xml_b = Nokogiri::XML(req_b.body, &:noblanks).canonicalize + xml_a == xml_b + end + + VCR_OPTIONS = { + # Consider matching on `:headers` too? + match_requests_on: [ + :method, :uri, + body_as_xml_matcher.freeze + ].freeze + }.freeze + + # This convenience method affords a handful of quality of life improvements + # for developing BGS service operation wrappers. It makes development a less + # manual process. It also turns VCR cassettes into a human readable resource + # that documents the behavior of BGS. + # + # In order to take advantage of this method, you will need to have supplied, + # to your example or example group, metadata of this form: + # `{ bgs: { service: "service", operation: "operation" } }`. + # + # Then, HTTP interactions that occur within the block supplied to this method + # will be captured by VCR cassettes that have the following convenient + # properties: + # - They will be nicely organized at `bgs/:service/:operation/:name` + # - Cassette matching will be done on canonicalized XML bodies, so + # reformatting cassettes for human readability won't defeat matching + def use_bgs_cassette(name, &) + metadata = RSpec.current_example.metadata[:bgs].to_h + service, operation = metadata.values_at(:service, :operation) + + if service.blank? || operation.blank? + raise ArgumentError, <<~HEREDOC + Must provide spec metadata of the form: + `{ bgs: { service: "service", operation: "operation" } }' + HEREDOC + end + + name = File.join('claims_api/bgs', service, operation, name) + VCR.use_cassette(name, VCR_OPTIONS, &) + end +end + +RSpec.configure do |config| + config.include BGSClientHelpers, :bgs +end 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/debts_api/app/models/debts_api/v0/form5655_submission.rb b/modules/debts_api/app/models/debts_api/v0/form5655_submission.rb index 64ab94ca8ff..9451214c3a3 100644 --- a/modules/debts_api/app/models/debts_api/v0/form5655_submission.rb +++ b/modules/debts_api/app/models/debts_api/v0/form5655_submission.rb @@ -16,7 +16,7 @@ class StaleUserError < StandardError; end def kms_encryption_context { - model_name: Form5655Submission.model_name.to_s, + model_name: 'Form5655Submission', model_id: id } end diff --git a/modules/debts_api/lib/debts_api/v0/financial_status_report_service.rb b/modules/debts_api/lib/debts_api/v0/financial_status_report_service.rb index 377d5a74fbd..fb95b382d42 100644 --- a/modules/debts_api/lib/debts_api/v0/financial_status_report_service.rb +++ b/modules/debts_api/lib/debts_api/v0/financial_status_report_service.rb @@ -73,6 +73,7 @@ def submit_combined_fsr(fsr_builder) Rails.logger.info('Submitting Combined FSR') create_vba_fsr(fsr_builder) create_vha_fsr(fsr_builder) + fsr_builder.destroy_related_form user_form = fsr_builder.user_form.form_data { diff --git a/modules/debts_api/lib/debts_api/v0/fsr_form_builder.rb b/modules/debts_api/lib/debts_api/v0/fsr_form_builder.rb index 0b527cb7e71..b80a3f13a84 100644 --- a/modules/debts_api/lib/debts_api/v0/fsr_form_builder.rb +++ b/modules/debts_api/lib/debts_api/v0/fsr_form_builder.rb @@ -104,5 +104,9 @@ def build_vba_form form = DebtsApi::V0::VbaFsrForm.new(params) form.form_data.nil? ? nil : form end + + def destroy_related_form + InProgressForm.form_for_user('5655', @user)&.destroy! + end end end diff --git a/modules/debts_api/spec/lib/debt_api/v0/financial_status_report_service_spec.rb b/modules/debts_api/spec/lib/debt_api/v0/financial_status_report_service_spec.rb index fcfabe88a37..e743694e6e3 100644 --- a/modules/debts_api/spec/lib/debt_api/v0/financial_status_report_service_spec.rb +++ b/modules/debts_api/spec/lib/debt_api/v0/financial_status_report_service_spec.rb @@ -267,7 +267,9 @@ def mock_pdf_fill service = described_class.new(user) builder = DebtsApi::V0::FsrFormBuilder.new(vha_form_data, '', user) copay_count = builder.vha_forms.length - expect { service.submit_combined_fsr(builder) }.to change(Form5655Submission, :count).by(copay_count) + expect { service.submit_combined_fsr(builder) }.to change( + DebtsApi::V0::Form5655Submission, :count + ).by(copay_count) expect(DebtsApi::V0::Form5655Submission.last.in_progress?).to eq(true) form = service.send(:add_vha_specific_data, DebtsApi::V0::Form5655Submission.last) expect(form.class).to be(Hash) @@ -280,7 +282,9 @@ def mock_pdf_fill copay_count = builder.vha_forms.length debt_count = builder.vba_form.present? ? 1 : 0 needed_count = copay_count + debt_count - expect { service.submit_combined_fsr(builder) }.to change(Form5655Submission, :count).by(needed_count) + expect do + service.submit_combined_fsr(builder) + end.to change(DebtsApi::V0::Form5655Submission, :count).by(needed_count) expect(DebtsApi::V0::Form5655Submission.last.public_metadata['combined']).to eq(true) debt_amounts = DebtsApi::V0::Form5655Submission.with_debt_type('DEBT').last.public_metadata['debt_amounts'] expect(debt_amounts).to eq(['541.67', '1134.22']) @@ -300,13 +304,13 @@ def mock_pdf_fill it 'persists vba FSRs' do service = described_class.new(user) builder = DebtsApi::V0::FsrFormBuilder.new(valid_vba_form_data, '', user) - expect { service.create_vba_fsr(builder) }.to change(Form5655Submission, :count).by(1) + expect { service.create_vba_fsr(builder) }.to change(DebtsApi::V0::Form5655Submission, :count).by(1) end it 'gracefully handles a lack of vba FSRs' do service = described_class.new(user) builder = DebtsApi::V0::FsrFormBuilder.new(valid_vha_form_data, '', user) - expect { service.create_vba_fsr(builder) }.not_to change(Form5655Submission, :count) + expect { service.create_vba_fsr(builder) }.not_to change(DebtsApi::V0::Form5655Submission, :count) end end diff --git a/modules/debts_api/spec/lib/debt_api/v0/fsr_form_builder_spec.rb b/modules/debts_api/spec/lib/debt_api/v0/fsr_form_builder_spec.rb index 67a8c016d9c..1ac1a7d0315 100644 --- a/modules/debts_api/spec/lib/debt_api/v0/fsr_form_builder_spec.rb +++ b/modules/debts_api/spec/lib/debt_api/v0/fsr_form_builder_spec.rb @@ -214,4 +214,28 @@ end end end + + describe '#destroy_related_form' do + let(:combined_form_data) { get_fixture_absolute('modules/debts_api/spec/fixtures/fsr_forms/combined_fsr_form') } + let(:user) { build(:user, :loa3) } + let(:user_data) { build(:user_profile_attributes) } + let(:in_progress_form) { create(:in_progress_5655_form, user_uuid: user.uuid) } + + context 'when IPF has already been deleted' do + it 'does not throw an error' do + expect(InProgressForm.all.length).to eq(0) + described_class.new(combined_form_data, '123', user).destroy_related_form + expect(InProgressForm.all.length).to eq(0) + end + end + + context 'when IPF is present' do + it 'deletes related In Progress Form' do + in_progress_form + expect(InProgressForm.all.length).to eq(1) + described_class.new(combined_form_data, '123', user).destroy_related_form + expect(InProgressForm.all.length).to eq(0) + end + end + end end diff --git a/modules/debts_api/spec/models/debt_api/v0/form5655_submission_spec.rb b/modules/debts_api/spec/models/debt_api/v0/form5655_submission_spec.rb index 56de9a15da8..c9ef1655198 100644 --- a/modules/debts_api/spec/models/debt_api/v0/form5655_submission_spec.rb +++ b/modules/debts_api/spec/models/debt_api/v0/form5655_submission_spec.rb @@ -3,41 +3,39 @@ require 'rails_helper' RSpec.describe DebtsApi::V0::Form5655Submission do - describe 'namespace portability' do - let!(:some_record) do - create(:form5655_submission, public_metadata: { 'streamlined' => { 'type' => 'short', 'value' => true } }) - end - - it 'shares data with the old model scope' do - expect(described_class.last.form).to eq(some_record.form) - expect(Form5655Submission.last.form).to eq(some_record.form) - end - end - describe 'scopes' do let!(:first_record) do - create(:form5655_submission, public_metadata: { 'streamlined' => { 'type' => 'short', 'value' => true } }) + create( + :debts_api_form5655_submission, + public_metadata: { 'streamlined' => { 'type' => 'short', 'value' => true } } + ) end let!(:second_record) do - create(:form5655_submission, public_metadata: { 'streamlined' => { 'type' => 'short', 'value' => false } }) + create( + :debts_api_form5655_submission, + public_metadata: { 'streamlined' => { 'type' => 'short', 'value' => false } } + ) end - let!(:third_record) { create(:form5655_submission, public_metadata: {}) } + let!(:third_record) { create(:debts_api_form5655_submission, public_metadata: {}) } let!(:fourth_record) do - create(:form5655_submission, public_metadata: { 'streamlined' => { 'type' => 'short', 'value' => nil } }) + create( + :debts_api_form5655_submission, + public_metadata: { 'streamlined' => { 'type' => 'short', 'value' => nil } } + ) end it 'includes records within scope' do - expect(Form5655Submission.streamlined).to include(first_record) - expect(Form5655Submission.streamlined.length).to eq(1) + expect(DebtsApi::V0::Form5655Submission.streamlined).to include(first_record) + expect(DebtsApi::V0::Form5655Submission.streamlined.length).to eq(1) - expect(Form5655Submission.not_streamlined).to include(second_record) - expect(Form5655Submission.not_streamlined.length).to eq(1) + expect(DebtsApi::V0::Form5655Submission.not_streamlined).to include(second_record) + expect(DebtsApi::V0::Form5655Submission.not_streamlined.length).to eq(1) - expect(Form5655Submission.streamlined_unclear).to include(third_record) - expect(Form5655Submission.streamlined_unclear.length).to eq(1) + expect(DebtsApi::V0::Form5655Submission.streamlined_unclear).to include(third_record) + expect(DebtsApi::V0::Form5655Submission.streamlined_unclear.length).to eq(1) - expect(Form5655Submission.streamlined_nil).to include(fourth_record) - expect(Form5655Submission.streamlined_nil.length).to eq(1) + expect(DebtsApi::V0::Form5655Submission.streamlined_nil).to include(fourth_record) + expect(DebtsApi::V0::Form5655Submission.streamlined_nil.length).to eq(1) end end @@ -65,7 +63,7 @@ end describe '.user_cache_id' do - let(:form5655_submission) { create(:form5655_submission) } + let(:form5655_submission) { create(:debts_api_form5655_submission) } let(:user) { build(:user, :loa3) } it 'creates a new User profile attribute' do @@ -81,7 +79,7 @@ end it 'returns an error' do - expect { form5655_submission.user_cache_id }.to raise_error(Form5655Submission::StaleUserError) + expect { form5655_submission.user_cache_id }.to raise_error(DebtsApi::V0::Form5655Submission::StaleUserError) end end end diff --git a/modules/ivc_champva/Gemfile b/modules/ivc_champva/Gemfile new file mode 100644 index 00000000000..d9266b3d7af --- /dev/null +++ b/modules/ivc_champva/Gemfile @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +source 'https://rubygems.org' + +# Declare your gem's dependencies in ivcchampva.gemspec. +# Bundler will treat runtime dependencies like base dependencies, and +# development dependencies will be added by default to the :development group. +gemspec + +# Declare any dependencies that are still in development here instead of in +# your gemspec. These might include edge Rails or gems from your path or +# Git. Remember to move these dependencies to your gemspec before releasing +# your gem to rubygems.org. + +# To use a debugger +# gem 'byebug', group: [:development, :test] diff --git a/modules/ivc_champva/README.rdoc b/modules/ivc_champva/README.rdoc new file mode 100644 index 00000000000..189a7a8e2e4 --- /dev/null +++ b/modules/ivc_champva/README.rdoc @@ -0,0 +1,19 @@ += IvcChampva +This module allows you to generate form_mappings based on a PDF file. +With this in place, you can submit a form payload from the vets-website +and have this module map that payload to the associated PDF and submit it +to PEGA via S3. + +To generate files: +rails ivc_champva:generate\['path to PDF file'\] + +Submission endpoint: +/ivc_champva/v1/forms + +== Installation +Ensure the following line is in the root project's Gemfile: + + gem 'ivcchampva', path: 'modules/ivcchampva' + +== License +This module is open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). \ No newline at end of file diff --git a/modules/ivc_champva/Rakefile b/modules/ivc_champva/Rakefile new file mode 100644 index 00000000000..887d143b182 --- /dev/null +++ b/modules/ivc_champva/Rakefile @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +begin + require 'bundler/setup' +rescue LoadError + puts 'You must `gem install bundler` and `bundle install` to run rake tasks' +end + +require 'rdoc/task' + +RDoc::Task.new(:rdoc) do |rdoc| + rdoc.rdoc_dir = 'rdoc' + rdoc.title = 'IvcChampva' + rdoc.options << '--line-numbers' + rdoc.rdoc_files.include('README.md') + rdoc.rdoc_files.include('lib/**/*.rb') +end + +load 'rails/tasks/statistics.rake' + +require 'bundler/gem_tasks' + +require 'rake/testtask' + +Rake::TestTask.new(:test) do |t| + t.libs << 'test' + t.pattern = 'test/**/*_test.rb' + t.verbose = false +end + +task default: :test diff --git a/modules/ivc_champva/app/controllers/ivc_champva/v1/application_controller.rb b/modules/ivc_champva/app/controllers/ivc_champva/v1/application_controller.rb new file mode 100644 index 00000000000..d7c1c0563b8 --- /dev/null +++ b/modules/ivc_champva/app/controllers/ivc_champva/v1/application_controller.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module IvcChampva + module V1 + class ApplicationController < ::ApplicationController + service_tag 'veteran-ivc-champva-forms' + end + end +end diff --git a/modules/ivc_champva/app/controllers/ivc_champva/v1/uploads_controller.rb b/modules/ivc_champva/app/controllers/ivc_champva/v1/uploads_controller.rb new file mode 100644 index 00000000000..35ea5688dcd --- /dev/null +++ b/modules/ivc_champva/app/controllers/ivc_champva/v1/uploads_controller.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true + +require 'ddtrace' + +module IvcChampva + module V1 + class UploadsController < ApplicationController + skip_after_action :set_csrf_header + + FORM_NUMBER_MAP = { + '10-10D' => 'vha_10_10d', + '10-7959F-1' => 'vha_10_7959f_1', + '10-7959F-2' => 'vha_10_7959f_2', + '10-7959C' => 'vha_10_7959c' + }.freeze + + def submit + Datadog::Tracing.trace('Start IVC File Submission') do + Datadog::Tracing.active_trace&.set_tag('form_id', params[:form_number]) + form_id = get_form_id + parsed_form_data = JSON.parse(params.to_json) + file_paths, metadata = get_file_paths_and_metadata(parsed_form_data) + status, error_message = FileUploader.new(form_id, metadata, file_paths).handle_uploads + + render json: build_json(Array(status), error_message) + rescue + puts 'An unknown error occurred while uploading document(s).' + end + end + + def submit_supporting_documents + if %w[10-10D 10-7959F-2].include?(params[:form_id]) + attachment = PersistentAttachments::MilitaryRecords.new(form_id: params[:form_id]) + attachment.file = params['file'] + raise Common::Exceptions::ValidationErrors, attachment unless attachment.valid? + + attachment.save + render json: attachment + end + end + + private + + def get_file_paths_and_metadata(parsed_form_data) + form_id = get_form_id + form = "IvcChampva::#{form_id.titleize.gsub(' ', '')}".constantize.new(parsed_form_data) + filler = IvcChampva::PdfFiller.new(form_number: form_id, form:) + + file_path = if @current_user + filler.generate(@current_user.loa[:current]) + else + filler.generate + end + + metadata = IvcChampva::MetadataValidator.validate(form.metadata) + file_paths = form.handle_attachments(file_path) + + [file_paths, metadata] + end + + def get_form_id + form_number = params[:form_number] + raise 'missing form_number in params' unless form_number + + FORM_NUMBER_MAP[form_number] + end + + def build_json(status, error_message) + if status.all? { |s| s == 200 } + { + status: 200 + } + elsif status.all? { |s| s == 400 } + { + error_message:, + status: 400 + } + else + { + error_message: 'Partial upload failure', + status: 206 + } + end + end + + def authenticate + super + rescue Common::Exceptions::Unauthorized + Rails.logger.info( + 'IVC Champva - unauthenticated user submitting form', + { form_number: params[:form_number] } + ) + end + + def should_authenticate + true + end + end + end +end diff --git a/modules/ivc_champva/app/form_mappings/vha_10_10d.json.erb b/modules/ivc_champva/app/form_mappings/vha_10_10d.json.erb new file mode 100644 index 00000000000..9f3ecb0a88f --- /dev/null +++ b/modules/ivc_champva/app/form_mappings/vha_10_10d.json.erb @@ -0,0 +1,74 @@ +{ + "form1[0].#subform[0].VeteransLastName[0]": "<%= form.data.dig('veteran', 'full_name', 'last') %>", + "form1[0].#subform[0].VeteransFirstName[0]": "<%= form.data.dig('veteran', 'full_name', 'first') %>", + "form1[0].#subform[0].VeteransMI[0]": "<%= form.data.dig('veteran', 'full_name', 'middle') %>", + "form1[0].#subform[0].VeteransSSN[0]": "<%= form.data.dig('veteran', 'ssn_or_tin') %>", + "form1[0].#subform[0].VAFileNumber[0]": "<%= form.data.dig('veteran', 'va_claim_number') %>", + "form1[0].#subform[0].VeteransStreetAddress[0]": "<%= form.data.dig('veteran', 'address', 'street') %>", + "form1[0].#subform[0].VeteransState[0]": "<%= form.data.dig('veteran', 'address', 'state') %>", + "form1[0].#subform[0].VeteransCity[0]": "<%= form.data.dig('veteran', 'address', 'city') %>", + "form1[0].#subform[0].VeteransZipCode[0]": "<%= form.data.dig('veteran', 'address', 'postal_code') %>", + "form1[0].#subform[0].VeteransPhoneNumber[0]": "<%= form.data.dig('veteran', 'phone_number') %>", + "form1[0].#subform[0].VeteransDateOfBirth[0]": "<%= form.data.dig('veteran', 'date_of_birth') %>", + "form1[0].#subform[0].VeteransDateOfMarriage[0]": "<%= form.data.dig('veteran', 'date_of_marriage') %>", + "form1[0].#subform[0].IsTheVeteranDeceased[0]": "<%= form.data.dig('veteran', 'is_deceased') ? 1 : 0 %>", + "form1[0].#subform[0].VeteransDateOfDeath[0]": "<%= form.data.dig('veteran', 'date_of_death') %>", + "form1[0].#subform[0].DieOnActiveMilitaryService[0]": "<%= form.data.dig('veteran', 'is_active_service_death') ? 1 : 0 %>", + "form1[0].#subform[0].LastName1[0]": "<%= form.data['applicants'][0]&.dig('full_name', 'last') %>", + "form1[0].#subform[0].FirstName1[0]": "<%= form.data['applicants'][0]&.dig('full_name', 'first') %>", + "form1[0].#subform[0].MI1[0]": "<%= form.data['applicants'][0]&.dig('full_name', 'middle') %>", + "form1[0].#subform[0].SSN1[0]": "<%= form.data['applicants'][0]&.dig('ssn_or_tin') %>", + "form1[0].#subform[0].StreetAddress1[0]": "<%= form.data['applicants'][0]&.dig('address', 'street') %>", + "form1[0].#subform[0].City1[0]": "<%= form.data['applicants'][0]&.dig('address', 'city') %>", + "form1[0].#subform[0].ZipCode1[0]": "<%= form.data['applicants'][0]&.dig('address', 'postal_code') %>", + "form1[0].#subform[0].DateOfBirth1[0]": "<%= form.data['applicants'][0]&.dig('date_of_birth') %>", + "form1[0].#subform[0].EmailAddress1[0]": "<%= form.data['applicants'][0]&.dig('email') %>", + "form1[0].#subform[0].PhoneNumber1[0]": "<%= form.data['applicants'][0]&.dig('phone_number') %>", + "form1[0].#subform[0].Gender1[0]": "<%= form.data['applicants'][0]&.dig('gender') ? 1 : 0 %>", + "form1[0].#subform[0].EnrolledMedicare[0]": "<%= form.data['applicants'][0]&.dig('is_enrolled_in_medicare') ? 1 : 0 %>", + "form1[0].#subform[0].HasOtherInsurance[0]": "<%= form.data['applicants'][0]&.dig('has_other_health_insurance') ? 1 : 0 %>", + "form1[0].#subform[0].RelationshipToVeteran1[0]": "<%= form.data['applicants'][0]&.dig('vet_relationship') %>", + "form1[0].#subform[0].State1[0]": "<%= form.data['applicants'][0]&.dig('address', 'state') %>", + "form1[0].#subform[0].LastName2[0]": "<%= form.data['applicants'][1]&.dig('full_name', 'last') %>", + "form1[0].#subform[0].FirstName2[0]": "<%= form.data['applicants'][1]&.dig('full_name', 'first') %>", + "form1[0].#subform[0].MI2[0]": "<%= form.data['applicants'][1]&.dig('full_name', 'middle') %>", + "form1[0].#subform[0].SSN2[0]": "<%= form.data['applicants'][1]&.dig('ssn_or_tin') %>", + "form1[0].#subform[0].StreetAddress2[0]": "<%= form.data['applicants'][1]&.dig('address', 'street') %>", + "form1[0].#subform[0].City2[0]": "<%= form.data['applicants'][1]&.dig('address', 'city') %>", + "form1[0].#subform[0].ZipCode2[0]": "<%= form.data['applicants'][1]&.dig('address', 'postal_code') %>", + "form1[0].#subform[0].DateOfBirth2[0]": "<%= form.data['applicants'][1]&.dig('date_of_birth') %>", + "form1[0].#subform[0].EmailAddress2[0]": "<%= form.data['applicants'][1]&.dig('email') %>", + "form1[0].#subform[0].PhoneNumber2[0]": "<%= form.data['applicants'][1]&.dig('phone_number') %>", + "form1[0].#subform[0].Gender2[0]": "<%= form.data['applicants'][1]&.dig('gender') ? 1 : 0 %>", + "form1[0].#subform[0].EnrolledMedicare[1]": "<%= form.data['applicants'][1]&.dig('is_enrolled_in_medicare') ? 1 : 0 %>", + "form1[0].#subform[0].HasOtherInsurance[1]": "<%= form.data['applicants'][1]&.dig('has_other_health_insurance') ? 1 : 0 %>", + "form1[0].#subform[0].RelationshipToVeteran2[0]": "<%= form.data['applicants'][1]&.dig('vet_relationship') %>", + "form1[0].#subform[0].State2[0]": "<%= form.data['applicants'][1]&.dig('address', 'state') %>", + "form1[0].#subform[0].LastName3[0]": "<%= form.data['applicants'][2]&.dig('full_name', 'last') %>", + "form1[0].#subform[0].FirstName3[0]": "<%= form.data['applicants'][2]&.dig('full_name', 'first') %>", + "form1[0].#subform[0].MI3[0]": "<%= form.data['applicants'][2]&.dig('full_name', 'middle') %>", + "form1[0].#subform[0].SSN3[0]": "<%= form.data['applicants'][2]&.dig('ssn_or_tin') %>", + "form1[0].#subform[0].StreetAddress3[0]": "<%= form.data['applicants'][2]&.dig('address', 'street') %>", + "form1[0].#subform[0].City3[0]": "<%= form.data['applicants'][2]&.dig('address', 'city') %>", + "form1[0].#subform[0].ZipCode3[0]": "<%= form.data['applicants'][2]&.dig('address', 'postal_code') %>", + "form1[0].#subform[0].DateOfBirth3[0]": "<%= form.data['applicants'][2]&.dig('date_of_birth') %>", + "form1[0].#subform[0].EmailAddress3[0]": "<%= form.data['applicants'][2]&.dig('email') %>", + "form1[0].#subform[0].PhoneNumber3[0]": "<%= form.data['applicants'][2]&.dig('phone_number') %>", + "form1[0].#subform[0].Gender3[0]": "<%= form.data['applicants'][2]&.dig('gender') ? 1 : 0 %>", + "form1[0].#subform[0].EnrolledMedicare[2]": "<%= form.data['applicants'][2]&.dig('is_enrolled_in_medicare') ? 1 : 0 %>", + "form1[0].#subform[0].HasOtherInsurance[2]": "<%= form.data['applicants'][2]&.dig('has_other_health_insurance') ? 1 : 0 %>", + "form1[0].#subform[0].RelationshipToVeteran3[0]": "<%= form.data['applicants'][2]&.dig('vet_relationship') %>", + "form1[0].#subform[0].State3[0]": "<%= form.data['applicants'][2]&.dig('address', 'state') %>", + "form1[0].#subform[0].DateSigned[0]": "<%= form.data.dig('certification', 'date') %>", + "form1[0].#subform[0].SignatureField11[0]": "<%= form.data['statement_of_truth_signature'] %>", + "form1[0].#subform[0].LastName4[0]": "<%= form.data.dig('certification', 'lastName') %>", + "form1[0].#subform[0].FirstName4[0]": "<%= form.data.dig('certification', 'firstName') %>", + "form1[0].#subform[0].MI4[0]": "<%= form.data.dig('certification', 'middleInitial') %>", + "form1[0].#subform[0].StreetAddress4[0]": "<%= form.data.dig('certification', 'streetAddress') %>", + "form1[0].#subform[0].City4[0]": "<%= form.data.dig('certification', 'city') %>", + "form1[0].#subform[0].ZipCode4[0]": "<%= form.data.dig('certification', 'postal_code') %>", + "form1[0].#subform[0].State4[0]": "<%= form.data.dig('certification', 'state') %>", + "form1[0].#subform[0].RelationshipToApplicants[0]": "<%= form.data.dig('certification', 'relationship') %>", + "form1[0].#subform[0].PhoneNumber4[0]": "<%= form.data.dig('certification', 'phone_number') %>", + "form1[0]": "<%= form.data.dig('form1') %>" +} diff --git a/modules/ivc_champva/app/form_mappings/vha_10_7959c.json.erb b/modules/ivc_champva/app/form_mappings/vha_10_7959c.json.erb new file mode 100644 index 00000000000..da2e998ad6a --- /dev/null +++ b/modules/ivc_champva/app/form_mappings/vha_10_7959c.json.erb @@ -0,0 +1,63 @@ +{ + "form1[0].#subform[0].#area[0].Doyouhaveotherinsyesno[0]": "<%= form.data.dig('has_other_health_insurance') == 'T' ? 1 : 0 %>", + "form1[0].#subform[0].#area[0]": "<%= form.data.dig('') %>", + "form1[0].#subform[0].NewAddressBox-1[0]": "<%= form.data.dig('is_new_address') == 'T' ? 1 : 0 %>", + "form1[0].#subform[0].PhoneNumb-1[0]": "<%= form.data.dig('applicants', 'home_phone') %>", + "form1[0].#subform[0].PartABDBkgrnd[0].PartARadioButtonList[0]": "<%= form.data.dig('part_a', 'has_part_a') == 'F' ? 1 : 0 %>", + "form1[0].#subform[0].PartABDBkgrnd[0].PartBRadioButtonList[0]": "<%= form.data.dig('part_b','has_part_b') == 'F' ? 1 : 0 %>", + "form1[0].#subform[0].PartABDBkgrnd[0].PartDRadioButtonList[0]": "<%= form.data.dig('part_d','has_part_d') == 'F' ? 1 : 0 %>", + "form1[0].#subform[0].PartABDBkgrnd[0]": "<%= form.data.dig('') %>", + "form1[0].#subform[0].PartA_CarrierName[0]": "<%= form.data.dig('part_a','part_a_carrier') %>", + "form1[0].#subform[0].PartB_CarrierName[0]": "<%= form.data.dig('part_b','part_b_carrier') %>", + "form1[0].#subform[0].PartD_CarrierName[0]": "<%= form.data.dig('part_d','part_d_carrier') %>", + "form1[0].#subform[0].PharmacyBenefitsRadioButtonList[0]": "<%= form.data.dig('has_pharmacy_benefits') == 'T' ? 1 : 0 %>", + "form1[0].#subform[0].MedicareAdvantageRadioButtonList[0]": "<%= form.data.dig('has_medicare_advantage') == 'T' ? 1 : 0 %>", + "form1[0].#subform[0].#area[2].CheckBox-HMO-1[0]": "<%= form.data.dig('other_health_insurance1' , 'is_hmo') ? 1 : 0 %>", + "form1[0].#subform[0].#area[2].CheckBox-PPO-1[0]": "<%= form.data.dig('other_health_insurance1' , 'is_ppo') ? 1 : 0 %>", + "form1[0].#subform[0].#area[2].CheckBox1-MedicaidSA-1[0]": "<%= form.data.dig('other_health_insurance1' , 'is_medicaid') ? 1 : 0 %>", + "form1[0].#subform[0].#area[2].CheckBox-RxDiscount-1[0]": "<%= form.data.dig('other_health_insurance1' , 'is_rx_discount') ? 1 : 0 %>", + "form1[0].#subform[0].#area[2].#area[3].CheckBox-Medigap-1[0]": "<%= form.data.dig('other_health_insurance1' , 'is_medigap') ? 1 : 0 %>", + "form1[0].#subform[0].#area[2].#area[3].DropDownList-Medigap-1[0]": "<%= form.data.dig('other_health_insurance1' , 'is_medigap_type') ? 1 : 0 %>", + "form1[0].#subform[0].#area[2].CheckBox-Other-1[0]": "<%= form.data.dig('other_health_insurance1' , 'is_other_type') ? 1 : 0 %>", + "form1[0].#subform[0].Comments-Ins-1[0]": "<%= form.data.dig('other_health_insurance1', 'comments' ) %>", + "form1[0].#subform[0].DateTimeField1[0]": "<%= form.data.dig('DateTimeField1') %>", + "form1[0].#subform[0].NameofInsurance-1[0]": "<%= form.data.dig('other_health_insurance1' , 'name_of_health_insurance') %>", + "form1[0].#subform[0].Signature[0]": "<%= form.data['statement_of_truth_signature'] %>", + "form1[0].#subform[0].Date-PartA[0]": "<%= form.data.dig('part_a','part_a_effective_date') %>", + "form1[0].#subform[0].Date-PartB[0]": "<%= form.data.dig('part_b','part_b_effective_date') %>", + "form1[0].#subform[0].Date-PartD[0]": "<%= form.data.dig('part_d','part_d_effective_date') %>", + "form1[0].#subform[0].Date-NameInsurance-1[0]": "<%= form.data.dig('other_health_insurance1' , 'date_health_insurance') %>", + "form1[0].#subform[0].Date-TermnNameInsurance-1[0]": "<%= form.data.dig('other_health_insurance1' , 'terminate_date_health_insurance') %>", + "form1[0].#subform[0].#area[4].applicantSexRadioButtons2[0]": "<%= form.data.dig('male_or_female') == 'M' ? 1 : 0 %>", + "form1[0].#subform[0].#area[4]": "<%= form.data.dig('') %>", + "form1[0].#subform[0].applicantZipCode2[0]": "<%= form.data.dig('applicants', 'address', 'postal_code') %>", + "form1[0].#subform[0].applicantState2[0]": "<%= form.data.dig('applicants', 'address', 'state') %>", + "form1[0].#subform[0].applicantCity2[0]": "<%= form.data.dig('applicants', 'address', 'city') %>", + "form1[0].#subform[0].applicantStreetAddress2[0]": "<%= form.data.dig('applicants', 'address', 'street') %>", + "form1[0].#subform[0].applicantMiddleInitial2[0]": "<%= form.data.dig('applicants', 'full_name', 'middle') %>", + "form1[0].#subform[0].applicantFirstName2[0]": "<%= form.data.dig('applicants', 'full_name', 'first') %>", + "form1[0].#subform[0].applicantLastName2[0]": "<%= form.data.dig('applicants', 'full_name', 'last') %>", + "form1[0].#subform[0].applicantSocialSecurityNumber2[0]": "<%= form.data.dig('applicants', 'ssn_or_tin') %>", + "form1[0].#subform[0].RadioButtonList[0]": "<%= form.data.dig('other_health_insurance1' , 'does_insurance') == 'T' ? 1 : 0 %>", + "form1[0].#subform[0].#area[6].#area[7].RadioButtonList[1]": "<%= form.data.dig('other_health_insurance1' , 'does_explain') == 'T' ? 1 : 0 %>", + "form1[0].#subform[0].#area[6].#area[7]": "<%= form.data.dig('#area') %>", + "form1[0].#subform[0].#area[6]": "<%= form.data.dig('#area') %>", + "form1[0].#subform[0].RadioButtonList[2]": "<%= form.data.dig('other_health_insurance1' , 'does_prescription') == 'T' ? 1 : 0 %>", + "form1[0].#subform[0].#area[8].CheckBox-HMO-2[0]": "<%= form.data.dig('other_health_insurance2' , 'is_hmo') ? 1 : 0 %>", + "form1[0].#subform[0].#area[8].CheckBox-PPO-2[0]": "<%= form.data.dig('other_health_insurance2' , 'is_ppo') ? 1 : 0 %>", + "form1[0].#subform[0].#area[8].CheckBox1-MedicaidSA-2[0]": "<%= form.data.dig('other_health_insurance2' , 'is_medicaid') ? 1 : 0 %>", + "form1[0].#subform[0].#area[8].CheckBox-RxDiscount-2[0]": "<%= form.data.dig('other_health_insurance2' , 'is_rx_discount') ? 1 : 0 %>", + "form1[0].#subform[0].#area[8].#area[9].CheckBox-Medigap-2[0]": "<%= form.data.dig('other_health_insurance2' , 'is_medigap') ? 1 : 0 %>", + "form1[0].#subform[0].#area[8].#area[9].DropDownList-Medigap-2[0]": "<%= form.data.dig('other_health_insurance2' , 'is_medigap_type') ? 1 : 0 %>", + "form1[0].#subform[0].#area[8].CheckBox-Other-2[0]": "<%= form.data.dig('other_health_insurance2' , 'is_other_type') ? 1 : 0 %>", + "form1[0].#subform[0].Comments-Ins-2[0]": "<%= form.data.dig('other_health_insurance2', 'comments') %>", + "form1[0].#subform[0].NameofInsurance-2[0]": "<%= form.data.dig('other_health_insurance2', 'name_of_health_insurance') %>", + "form1[0].#subform[0].Date-NameInsurance-2[0]": "<%= form.data.dig('other_health_insurance2', 'date_health_insurance') %>", + "form1[0].#subform[0].Date-TermnNameInsurance-2[0]": "<%= form.data.dig('other_health_insurance2', 'terminate_date_health_insurance') %>", + "form1[0].#subform[0].RadioButtonList[3]": "<%= form.data.dig('other_health_insurance2' , 'does_insurance') == 'T' ? 1 : 0 %>", + "form1[0].#subform[0].RadioButtonList[4]": "<%= form.data.dig('other_health_insurance2' , 'does_explain') == 'T' ? 1 : 0 %>", + "form1[0].#subform[0].RadioButtonList[5]": "<%= form.data.dig('other_health_insurance2' , 'does_prescription') == 'T' ? 1 : 0 %>", + "form1[0]": "<%= form.data.dig('form1') %>" +} + + diff --git a/modules/ivc_champva/app/form_mappings/vha_10_7959f_1.json.erb b/modules/ivc_champva/app/form_mappings/vha_10_7959f_1.json.erb new file mode 100644 index 00000000000..ed90786035f --- /dev/null +++ b/modules/ivc_champva/app/form_mappings/vha_10_7959f_1.json.erb @@ -0,0 +1,16 @@ +{ + "form1[0].#subform[0].VetLastName[0]": "<%= form.data.dig('veteran', 'full_name', 'last') %>", + "form1[0].#subform[0].VetFirstName[0]": "<%= form.data.dig('veteran', 'full_name', 'first') %>", + "form1[0].#subform[0].MiddleInitials[0]": "<%= form.data.dig('veteran', 'full_name', 'middle') %>", + "form1[0].#subform[0].SocialSecurityNumber[0]": "<%= form.data.dig('veteran', 'ssn') %>", + "form1[0].#subform[0].VAClaimFileNumber[0]": "<%= form.data.dig('veteran', 'va_claim_number') %>", + "form1[0].#subform[0].DateofBirth[0]": "<%= form.data.dig('veteran', 'date_of_birth') %>", + "form1[0].#subform[0].PhysicalAddress[0]": "<%= form.data.dig('veteran', 'physical_address', 'street') + '\n' + form.data.dig('veteran', 'physical_address', 'city') + ', ' + form.data.dig('veteran', 'physical_address', 'state') + '\n' + form.data.dig('veteran', 'physical_address', 'postal_code') %>", + "form1[0].#subform[0].Country[0]": "<%= form.data.dig('veteran', 'physical_address', 'country') %>", + "form1[0].#subform[0].MailingAddress[0]": "<%= form.data.dig('veteran', 'mailing_address', 'street') + '\n' + form.data.dig('veteran', 'mailing_address', 'city') + ', ' + form.data.dig('veteran', 'mailing_address', 'state') + '\n' + form.data.dig('veteran', 'mailing_address', 'postal_code') %>", + "form1[0].#subform[0].Country[1]": "<%= form.data.dig('veteran', 'mailing_address', 'country') %>", + "form1[0].#subform[0].TelephoneNumber[0]": "<%= form.data.dig('veteran', 'phone_number') %>", + "form1[0].#subform[0].EmailAddress[0]": "<%= form.data.dig('veteran', 'email_address') %>", + "form1[0].#subform[0].VeteranSignature[0]": "<%= form.data['statement_of_truth_signature'] %>", + "form1[0].#subform[0].Date[0]": "<%= form.data['current_date'] %>" +} diff --git a/modules/ivc_champva/app/form_mappings/vha_10_7959f_2.json.erb b/modules/ivc_champva/app/form_mappings/vha_10_7959f_2.json.erb new file mode 100644 index 00000000000..6d5a307c362 --- /dev/null +++ b/modules/ivc_champva/app/form_mappings/vha_10_7959f_2.json.erb @@ -0,0 +1,17 @@ +{ + "vha107959fform[0].#subform[0].RadioButtonList[0]": "<%= form.data['payment_to_be_sent_type'] == 'Veteran' ? 0 : 1 %>", + "vha107959fform[0].#subform[0].LastName-1[0]": "<%= form.data.dig('veteran', 'full_name', 'last') %>", + "vha107959fform[0].#subform[0].FirstName-1[0]": "<%= form.data.dig('veteran', 'full_name', 'first') %>", + "vha107959fform[0].#subform[0].MiddleInitial-1[0]": "<%= form.data.dig('veteran', 'full_name', 'middle') %>", + "vha107959fform[0].#subform[0].SSN-1[0]": "<%= form.data.dig('veteran', 'ssn') %>", + "vha107959fform[0].#subform[0].VAClaimNumber-1[0]": "<%= form.data.dig('veteran', 'va_claim_number') %>", + "vha107959fform[0].#subform[0].DateofBirth-1[0]": "<%= form.data.dig('veteran', 'va_claim_number') %>", + "vha107959fform[0].#subform[0].PhysicalAddress1-1[0]": "<%= form.data.dig('veteran', 'physical_address', 'street') + '\n' + form.data.dig('veteran', 'physical_address', 'city') + ', ' + form.data.dig('veteran', 'physical_address', 'state') + '\n' + form.data.dig('veteran', 'physical_address', 'postal_code') %>", + "vha107959fform[0].#subform[0].PhysicalAddressCountry-1[0]": "<%= form.data.dig('veteran', 'physical_address', 'country') %>", + "vha107959fform[0].#subform[0].MailingAddress1-2[0]": "<%= form.data.dig('veteran', 'mailing_address', 'street') + '\n' + form.data.dig('veteran', 'mailing_address', 'city') + ', ' + form.data.dig('veteran', 'mailing_address', 'state') + '\n' + form.data.dig('veteran', 'mailing_address', 'postal_code') %>", + "vha107959fform[0].#subform[0].MailingAddressCountry[0]": "<%= form.data.dig('veteran', 'mailing_address', 'country') %>", + "vha107959fform[0].#subform[0].Telephone-1[0]": "<%= form.data.dig('veteran', 'phone_number') %>", + "vha107959fform[0].#subform[0].EmailAddress[0]": "<%= form.data.dig('veteran', 'email_address') %>", + "vha107959fform[0].#subform[0].SignatureDate-1[0]": "<%= form.data['current_date'] %>", + "vha107959fform[0].#subform[0].VeteranFiduciarySignature-1[0]": "<%= form.data['statement_of_truth_signature'] %>" +} diff --git a/modules/ivc_champva/app/json/cemeteries.json b/modules/ivc_champva/app/json/cemeteries.json new file mode 100644 index 00000000000..455dc70d7c8 --- /dev/null +++ b/modules/ivc_champva/app/json/cemeteries.json @@ -0,0 +1,3304 @@ +{ + "data": [ + { + "id": "915", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "915", + "name": "ABRAHAM LINCOLN NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "915" + } + }, + { + "id": "944", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "944", + "name": "ACADIA NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "944" + } + }, + { + "id": "927", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "927", + "name": "ALABAMA NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "927" + } + }, + { + "id": "400", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "400", + "name": "ALABAMA STATE VETERANS MEMORIAL CEMETERY", + "cemetery_type": "S", + "num": "400" + } + }, + { + "id": "946", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "946", + "name": "ALAMEDA NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "946" + } + }, + { + "id": "088", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "088", + "name": "ALBANY RURAL CEMETERY", + "cemetery_type": "N", + "num": "088" + } + }, + { + "id": "109", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "109", + "name": "ALBERT G. HORTON, JR. MEMORIAL VETERANS CEMETERY", + "cemetery_type": "S", + "num": "109" + } + }, + { + "id": "825", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "825", + "name": "ALEXANDRIA NATIONAL CEMETERY, LA", + "cemetery_type": "N", + "num": "825" + } + }, + { + "id": "826", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "826", + "name": "ALEXANDRIA NATIONAL CEMETERY, VA", + "cemetery_type": "N", + "num": "826" + } + }, + { + "id": "417", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "417", + "name": "ALL NATIONS VETERANS CEMETERY", + "cemetery_type": "S", + "num": "417" + } + }, + { + "id": "120", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "120", + "name": "ALLEGHENY CEMETERY", + "cemetery_type": "N", + "num": "120" + } + }, + { + "id": "800", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "800", + "name": "ALTON NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "800" + } + }, + { + "id": "409", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "409", + "name": "ANDERSONVILLE NATIONAL CEMETERY", + "cemetery_type": "I", + "num": "409" + } + }, + { + "id": "410", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "410", + "name": "ANDREW JOHNSON NATIONAL CEMETERY", + "cemetery_type": "I", + "num": "410" + } + }, + { + "id": "136", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "136", + "name": "ANGEL FIRE NM STATE VETERANS CEMETERY", + "cemetery_type": "S", + "num": "136" + } + }, + { + "id": "801", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "801", + "name": "ANNAPOLIS NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "801" + } + }, + { + "id": "152", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "152", + "name": "APSAALOOKE VETERANS CEMETERY", + "cemetery_type": "S", + "num": "152" + } + }, + { + "id": "412", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "412", + "name": "ARIZONA VETERANS MEMORIAL CEMETERY AT CAMP NAVAJO", + "cemetery_type": "S", + "num": "412" + } + }, + { + "id": "413", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "413", + "name": "ARIZONA VETERANS MEMORIAL CEMETERY AT MARANA", + "cemetery_type": "S", + "num": "413" + } + }, + { + "id": "091", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "091", + "name": "ARKANSAS STATE VETERANS CEMETERY", + "cemetery_type": "S", + "num": "091" + } + }, + { + "id": "396", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "396", + "name": "ARKANSAS STATE VETERANS CEMETERY-BIRDEYE", + "cemetery_type": "S", + "num": "396" + } + }, + { + "id": "411", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "411", + "name": "ARLINGTON NATIONAL CEMETERY", + "cemetery_type": "A", + "num": "411" + } + }, + { + "id": "121", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "121", + "name": "ASHLAND CEMETERY", + "cemetery_type": "N", + "num": "121" + } + }, + { + "id": "406", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "406", + "name": "ATLANTIC GARDEN VETERANS CEMETERY", + "cemetery_type": "S", + "num": "406" + } + }, + { + "id": "929", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "929", + "name": "BAKERSFIELD NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "929" + } + }, + { + "id": "827", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "827", + "name": "BALLS BLUFF NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "827" + } + }, + { + "id": "802", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "802", + "name": "BALTIMORE NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "802" + } + }, + { + "id": "828", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "828", + "name": "BARRANCAS NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "828" + } + }, + { + "id": "803", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "803", + "name": "BATH NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "803" + } + }, + { + "id": "829", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "829", + "name": "BATON ROUGE NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "829" + } + }, + { + "id": "042", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "042", + "name": "BAXTER SPRINGS", + "cemetery_type": "N", + "num": "042" + } + }, + { + "id": "830", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "830", + "name": "BAY PINES NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "830" + } + }, + { + "id": "831", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "831", + "name": "BEAUFORT NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "831" + } + }, + { + "id": "961", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "961", + "name": "BENICIA ARSENAL POST CEMETERY", + "cemetery_type": "N", + "num": "961" + } + }, + { + "id": "804", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "804", + "name": "BEVERLY NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "804" + } + }, + { + "id": "025", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "025", + "name": "BG WILLIAM C DOYLE VET'S MEM CEM", + "cemetery_type": "S", + "num": "025" + } + }, + { + "id": "117", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "117", + "name": "BIG SANDY RANCHERIA VETERANS CEMETERY", + "cemetery_type": "S", + "num": "117" + } + }, + { + "id": "832", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "832", + "name": "BILOXI NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "832" + } + }, + { + "id": "884", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "884", + "name": "BLACK HILLS NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "884" + } + }, + { + "id": "414", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "414", + "name": "CALIFORNIA CENTRAL COAST VETERANS CEMETERY", + "cemetery_type": "S", + "num": "414" + } + }, + { + "id": "805", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "805", + "name": "CALVERTON NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "805" + } + }, + { + "id": "806", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "806", + "name": "CAMP BUTLER NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "806" + } + }, + { + "id": "075", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "075", + "name": "CAMP CHASE CONFEDERATE CEMETERY", + "cemetery_type": "N", + "num": "075" + } + }, + { + "id": "833", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "833", + "name": "CAMP NELSON NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "833" + } + }, + { + "id": "934", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "934", + "name": "CAPE CANAVERAL NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "934" + } + }, + { + "id": "834", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "834", + "name": "CAVE HILL NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "834" + } + }, + { + "id": "942", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "942", + "name": "CEDAR CITY NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "942" + } + }, + { + "id": "397", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "397", + "name": "CENTRAL LOUISIANA VETERANS CEMETERY", + "cemetery_type": "S", + "num": "397" + } + }, + { + "id": "127", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "127", + "name": "CENTRAL TEXAS STATE VETERANS CEMETERY", + "cemetery_type": "S", + "num": "127" + } + }, + { + "id": "015", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "015", + "name": "CENTRAL WISCONSIN VETERANS MEMORIAL CEMETERY", + "cemetery_type": "S", + "num": "015" + } + }, + { + "id": "835", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "835", + "name": "CHATTANOOGA NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "835" + } + }, + { + "id": "001", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "001", + "name": "CHELTENHAM VETERANS CEMETERY", + "cemetery_type": "S", + "num": "001" + } + }, + { + "id": "945", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "945", + "name": "CHEYENNE NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "945" + } + }, + { + "id": "950", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "950", + "name": "CHICAGO NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "950" + } + }, + { + "id": "836", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "836", + "name": "CITY POINT NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "836" + } + }, + { + "id": "132", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "132", + "name": "CNMI VETERANS CEMETERY", + "cemetery_type": "S", + "num": "132" + } + }, + { + "id": "395", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "395", + "name": "COASTAL BEND VETERANS CEMETERY", + "cemetery_type": "S", + "num": "395" + } + }, + { + "id": "052", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "052", + "name": "COASTAL CAROLINA STATE VETERANS CEMETERY", + "cemetery_type": "S", + "num": "052" + } + }, + { + "id": "837", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "837", + "name": "COLD HARBOR NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "837" + } + }, + { + "id": "010", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "010", + "name": "COLONEL RAYMOND F. GATES CEMETERY", + "cemetery_type": "S", + "num": "010" + } + }, + { + "id": "073", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "073", + "name": "CONFEDERATE MOUND", + "cemetery_type": "N", + "num": "073" + } + }, + { + "id": "076", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "076", + "name": "CONFEDERATE STOCKADE CEMETERY", + "cemetery_type": "N", + "num": "076" + } + }, + { + "id": "054", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "054", + "name": "CONGRESSIONAL CEMETERY", + "cemetery_type": "N", + "num": "054" + } + }, + { + "id": "030", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "030", + "name": "CONNECTICUT STATE VETERANS CEMETERY", + "cemetery_type": "S", + "num": "030" + } + }, + { + "id": "838", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "838", + "name": "CORINTH NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "838" + } + }, + { + "id": "074", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "074", + "name": "CROWN HILL CONFEDERATE PLOT", + "cemetery_type": "N", + "num": "074" + } + }, + { + "id": "807", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "807", + "name": "CROWN HILL NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "807" + } + }, + { + "id": "002", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "002", + "name": "CROWNSVILLE VETERANS CEMETERY", + "cemetery_type": "S", + "num": "002" + } + }, + { + "id": "839", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "839", + "name": "CULPEPER NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "839" + } + }, + { + "id": "808", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "808", + "name": "CYPRESS HILLS NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "808" + } + }, + { + "id": "916", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "916", + "name": "DALLAS - FT. WORTH NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "916" + } + }, + { + "id": "809", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "809", + "name": "DANVILLE NATIONAL CEMETERY, IL", + "cemetery_type": "N", + "num": "809" + } + }, + { + "id": "840", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "840", + "name": "DANVILLE NATIONAL CEMETERY, KY", + "cemetery_type": "N", + "num": "840" + } + }, + { + "id": "841", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "841", + "name": "DANVILLE NATIONAL CEMETERY, VA", + "cemetery_type": "N", + "num": "841" + } + }, + { + "id": "810", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "810", + "name": "DAYTON NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "810" + } + }, + { + "id": "036", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "036", + "name": "DELAWARE VETERANS MEMORIAL CEMETERY", + "cemetery_type": "S", + "num": "036" + } + }, + { + "id": "093", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "093", + "name": "DELAWARE VETERANS MEMORIAL CEMETERY-SUSSEX CO.", + "cemetery_type": "S", + "num": "093" + } + }, + { + "id": "398", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "398", + "name": "DONEL KINNARD MEMORIAL STATE VETERANS CEMETERY", + "cemetery_type": "S", + "num": "398" + } + }, + { + "id": "906", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "906", + "name": "EAGLE POINT NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "906" + } + }, + { + "id": "394", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "394", + "name": "EAST TENNESSEE STATE VETERANS CEMETERY", + "cemetery_type": "S", + "num": "394" + } + }, + { + "id": "416", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "416", + "name": "EASTERN CAROLINA STATE VETERANS CEMETERY", + "cemetery_type": "S", + "num": "416" + } + }, + { + "id": "090", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "090", + "name": "EASTERN MONTANA STATE VETERANS CEMETERY", + "cemetery_type": "S", + "num": "090" + } + }, + { + "id": "004", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "004", + "name": "EASTERN SHORE VETERANS CEMETERY", + "cemetery_type": "S", + "num": "004" + } + }, + { + "id": "943", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "943", + "name": "ELKO NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "943" + } + }, + { + "id": "055", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "055", + "name": "EVERGREEN CEMETERY", + "cemetery_type": "N", + "num": "055" + } + }, + { + "id": "940", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "940", + "name": "FARGO NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "940" + } + }, + { + "id": "842", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "842", + "name": "FAYETTEVILLE NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "842" + } + }, + { + "id": "811", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "811", + "name": "FINN'S POINT NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "811" + } + }, + { + "id": "843", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "843", + "name": "FLORENCE NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "843" + } + }, + { + "id": "911", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "911", + "name": "FLORIDA NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "911" + } + }, + { + "id": "123", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "123", + "name": "FOREST HILL CEMETERY", + "cemetery_type": "N", + "num": "123" + } + }, + { + "id": "122", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "122", + "name": "FOREST HOME CEMETERY", + "cemetery_type": "N", + "num": "122" + } + }, + { + "id": "058", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "058", + "name": "FOREST LAWN CEMETERY", + "cemetery_type": "N", + "num": "058" + } + }, + { + "id": "140", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "140", + "name": "FORT CAMPBELL POW POST CEMETERY", + "cemetery_type": "M", + "num": "140" + } + }, + { + "id": "960", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "960", + "name": "FORT DEVENS POST CEMETERY", + "cemetery_type": "N", + "num": "960" + } + }, + { + "id": "957", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "957", + "name": "FORT DOUGLAS POST CEMETERY", + "cemetery_type": "N", + "num": "957" + } + }, + { + "id": "141", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "141", + "name": "FORT DRUM POW POST CEMETERY", + "cemetery_type": "M", + "num": "141" + } + }, + { + "id": "142", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "142", + "name": "FORT GORDON GERMAN AND ITALIAN POW CEMETERY", + "cemetery_type": "M", + "num": "142" + } + }, + { + "id": "930", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "930", + "name": "FORT JACKSON VA NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "930" + } + }, + { + "id": "143", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "143", + "name": "FORT LEAVENWORTH POST CEMETERY", + "cemetery_type": "M", + "num": "143" + } + }, + { + "id": "082", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "082", + "name": "FORT MACKINAC POST CEMETERY", + "cemetery_type": "N", + "num": "082" + } + }, + { + "id": "089", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "089", + "name": "FORT MCCLELLAN EPW CEMETERY", + "cemetery_type": "N", + "num": "089" + } + }, + { + "id": "955", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "955", + "name": "FORT MCCLELLAN POST CEMETERY", + "cemetery_type": "N", + "num": "955" + } + }, + { + "id": "144", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "144", + "name": "FORT MEADE POST CEMETERY", + "cemetery_type": "M", + "num": "144" + } + }, + { + "id": "952", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "952", + "name": "FORT MISSOULA POST CEMETERY", + "cemetery_type": "N", + "num": "952" + } + }, + { + "id": "953", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "953", + "name": "FORT SHERIDAN NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "953" + } + }, + { + "id": "113", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "113", + "name": "FORT STANTON STATE VETERANS CEMETERY", + "cemetery_type": "S", + "num": "113" + } + }, + { + "id": "959", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "959", + "name": "FORT STEVENS NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "959" + } + }, + { + "id": "958", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "958", + "name": "FORT WORDEN POST CEMETERY", + "cemetery_type": "N", + "num": "958" + } + }, + { + "id": "391", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "391", + "name": "FT LEONARD WOOD STATE VETERANS CEMETERY", + "cemetery_type": "S", + "num": "391" + } + }, + { + "id": "885", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "885", + "name": "FT. BAYARD NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "885" + } + }, + { + "id": "886", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "886", + "name": "FT. BLISS NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "886" + } + }, + { + "id": "059", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "059", + "name": "FT. CRAWFORD CEMETERY", + "cemetery_type": "N", + "num": "059" + } + }, + { + "id": "909", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "909", + "name": "FT. CUSTER NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "909" + } + }, + { + "id": "844", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "844", + "name": "FT. GIBSON NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "844" + } + }, + { + "id": "845", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "845", + "name": "FT. HARRISON NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "845" + } + }, + { + "id": "951", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "951", + "name": "FT. LAWTON POST CEMETERY", + "cemetery_type": "N", + "num": "951" + } + }, + { + "id": "887", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "887", + "name": "FT. LEAVENWORTH NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "887" + } + }, + { + "id": "888", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "888", + "name": "FT. LOGAN NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "888" + } + }, + { + "id": "889", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "889", + "name": "FT. LYON NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "889" + } + }, + { + "id": "890", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "890", + "name": "FT. MCPHERSON NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "890" + } + }, + { + "id": "891", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "891", + "name": "FT. MEADE NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "891" + } + }, + { + "id": "908", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "908", + "name": "FT. MITCHELL NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "908" + } + }, + { + "id": "910", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "910", + "name": "FT. RICHARDSON NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "910" + } + }, + { + "id": "407", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "407", + "name": "FT. RILEY POST CEMETERY", + "cemetery_type": "M", + "num": "407" + } + }, + { + "id": "892", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "892", + "name": "FT. ROSECRANS NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "892" + } + }, + { + "id": "846", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "846", + "name": "FT. SAM HOUSTON NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "846" + } + }, + { + "id": "893", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "893", + "name": "FT. SCOTT NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "893" + } + }, + { + "id": "920", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "920", + "name": "FT. SILL NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "920" + } + }, + { + "id": "847", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "847", + "name": "FT. SMITH NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "847" + } + }, + { + "id": "894", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "894", + "name": "FT. SNELLING NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "894" + } + }, + { + "id": "083", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "083", + "name": "FT. WINNEBAGO CEMETERY", + "cemetery_type": "N", + "num": "083" + } + }, + { + "id": "118", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "118", + "name": "GALLUP STATE VETERANS CEMETERY", + "cemetery_type": "S", + "num": "118" + } + }, + { + "id": "003", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "003", + "name": "GARRISON FOREST VETERANS CEMETERY", + "cemetery_type": "S", + "num": "003" + } + }, + { + "id": "922", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "922", + "name": "GEORGIA NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "922" + } + }, + { + "id": "096", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "096", + "name": "GEORGIA VETERANS MEMORIAL CEMETERY", + "cemetery_type": "S", + "num": "096" + } + }, + { + "id": "303", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "303", + "name": "GEORGIA VETERANS MEMORIAL CEMETERY - GLENNVILLE", + "cemetery_type": "S", + "num": "303" + } + }, + { + "id": "917", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "917", + "name": "GERALD B.H. SOLOMON SARATOGA NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "917" + } + }, + { + "id": "848", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "848", + "name": "GLENDALE NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "848" + } + }, + { + "id": "895", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "895", + "name": "GOLDEN GATE NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "895" + } + }, + { + "id": "812", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "812", + "name": "GRAFTON NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "812" + } + }, + { + "id": "923", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "923", + "name": "GREAT LAKES NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "923" + } + }, + { + "id": "061", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "061", + "name": "GREEN MOUNT CEMETERY", + "cemetery_type": "N", + "num": "061" + } + }, + { + "id": "151", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "151", + "name": "GUAM VETERANS CEMETERY", + "cemetery_type": "S", + "num": "151" + } + }, + { + "id": "849", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "849", + "name": "HAMPTON NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "849" + } + }, + { + "id": "850", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "850", + "name": "HAMPTON VA NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "850" + } + }, + { + "id": "043", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "043", + "name": "HAWAII STATE VETERANS CEMETERY", + "cemetery_type": "S", + "num": "043" + } + }, + { + "id": "896", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "896", + "name": "HOT SPRINGS NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "896" + } + }, + { + "id": "124", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "124", + "name": "HOULTON BAND OF MALISEET INDIANS VETERANS CEMETERY", + "cemetery_type": "S", + "num": "124" + } + }, + { + "id": "851", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "851", + "name": "HOUSTON NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "851" + } + }, + { + "id": "107", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "107", + "name": "IDAHO STATE VETERANS CEMETERY", + "cemetery_type": "S", + "num": "107" + } + }, + { + "id": "139", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "139", + "name": "IDAHO STATE VETERANS CEMETERY AT BLACKFOOT", + "cemetery_type": "S", + "num": "139" + } + }, + { + "id": "006", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "006", + "name": "INDIANA VETERANS MEMORIAL CEMETERY", + "cemetery_type": "S", + "num": "006" + } + }, + { + "id": "948", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "948", + "name": "INDIANAPOLIS NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "948" + } + }, + { + "id": "813", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "813", + "name": "INDIANTOWN GAP NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "813" + } + }, + { + "id": "301", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "301", + "name": "IOWA VETERANS CEMETERY AT VAN METER", + "cemetery_type": "S", + "num": "301" + } + }, + { + "id": "928", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "928", + "name": "JACKSONVILLE NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "928" + } + }, + { + "id": "852", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "852", + "name": "JEFFERSON BARRACKS NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "852" + } + }, + { + "id": "853", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "853", + "name": "JEFFERSON CITY NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "853" + } + }, + { + "id": "094", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "094", + "name": "KANSAS VETERANS CEMETERY AT FORT DODGE", + "cemetery_type": "S", + "num": "094" + } + }, + { + "id": "277", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "277", + "name": "KANSAS VETERANS CEMETERY AT FORT RILEY", + "cemetery_type": "S", + "num": "277" + } + }, + { + "id": "110", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "110", + "name": "KANSAS VETERANS CEMETERY AT WAKEENEY", + "cemetery_type": "S", + "num": "110" + } + }, + { + "id": "128", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "128", + "name": "KANSAS VETERANS CEMETERY AT WINFIELD", + "cemetery_type": "S", + "num": "128" + } + }, + { + "id": "112", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "112", + "name": "KENTUCKY VETERAN CEMETERY SOUTHEAST", + "cemetery_type": "S", + "num": "112" + } + }, + { + "id": "114", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "114", + "name": "KENTUCKY VETERANS CEMETERY - NORTHEAST", + "cemetery_type": "S", + "num": "114" + } + }, + { + "id": "134", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "134", + "name": "KENTUCKY VETERANS CEMETERY CENTRAL", + "cemetery_type": "S", + "num": "134" + } + }, + { + "id": "135", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "135", + "name": "KENTUCKY VETERANS CEMETERY NORTH", + "cemetery_type": "S", + "num": "135" + } + }, + { + "id": "105", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "105", + "name": "KENTUCKY VETERANS CEMETERY-WEST", + "cemetery_type": "S", + "num": "105" + } + }, + { + "id": "814", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "814", + "name": "KEOKUK NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "814" + } + }, + { + "id": "854", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "854", + "name": "KERRVILLE NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "854" + } + }, + { + "id": "855", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "855", + "name": "KNOXVILLE NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "855" + } + }, + { + "id": "062", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "062", + "name": "LAKESIDE CEMETERY", + "cemetery_type": "N", + "num": "062" + } + }, + { + "id": "403", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "403", + "name": "LAKOTA FREEDOM VETERANS CEMETERY", + "cemetery_type": "S", + "num": "403" + } + }, + { + "id": "897", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "897", + "name": "LEAVENWORTH NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "897" + } + }, + { + "id": "856", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "856", + "name": "LEBANON NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "856" + } + }, + { + "id": "418", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "418", + "name": "LEECH LAKE VETERANS CEMETERY", + "cemetery_type": "S", + "num": "418" + } + }, + { + "id": "857", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "857", + "name": "LEXINGTON NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "857" + } + }, + { + "id": "858", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "858", + "name": "LITTLE ROCK NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "858" + } + }, + { + "id": "815", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "815", + "name": "LONG ISLAND NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "815" + } + }, + { + "id": "898", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "898", + "name": "LOS ANGELES NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "898" + } + }, + { + "id": "816", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "816", + "name": "LOUDON PARK NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "816" + } + }, + { + "id": "970", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "970", + "name": "LOUISIANA NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "970" + } + }, + { + "id": "302", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "302", + "name": "M.J. DOLLY COOPER VETERANS CEMETERY", + "cemetery_type": "S", + "num": "302" + } + }, + { + "id": "012", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "012", + "name": "MAINE VETERANS' MEMORIAL CEMETERY", + "cemetery_type": "S", + "num": "012" + } + }, + { + "id": "095", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "095", + "name": "MAINE VETERANS' MEMORIAL CEMETERY - MT VERNON RD", + "cemetery_type": "S", + "num": "095" + } + }, + { + "id": "859", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "859", + "name": "MARIETTA NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "859" + } + }, + { + "id": "817", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "817", + "name": "MARION NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "817" + } + }, + { + "id": "818", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "818", + "name": "MASSACHUSETTS NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "818" + } + }, + { + "id": "108", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "108", + "name": "MASSACHUSETTS VETERAN MEMORIAL CEMETERY/WINCHENDON", + "cemetery_type": "S", + "num": "108" + } + }, + { + "id": "034", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "034", + "name": "MASSACHUSETTS VETERANS' MEMORIAL CEMETERY", + "cemetery_type": "S", + "num": "034" + } + }, + { + "id": "999", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "999", + "name": "MBMS TEST AUTOMATION CEM ONE", + "cemetery_type": "N", + "num": "999" + } + }, + { + "id": "997", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "997", + "name": "MBMS TEST AUTOMATION CEM THREE", + "cemetery_type": "N", + "num": "997" + } + }, + { + "id": "998", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "998", + "name": "MBMS TEST AUTOMATION CEM TWO", + "cemetery_type": "N", + "num": "998" + } + }, + { + "id": "860", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "860", + "name": "MEMPHIS NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "860" + } + }, + { + "id": "137", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "137", + "name": "METLAKATLA VETERANS MEMORIAL CEMETERY", + "cemetery_type": "S", + "num": "137" + } + }, + { + "id": "050", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "050", + "name": "MIDDLE TENNESSEE STATE VETERANS CEMETERY", + "cemetery_type": "S", + "num": "050" + } + }, + { + "id": "861", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "861", + "name": "MILL SPRINGS NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "861" + } + }, + { + "id": "116", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "116", + "name": "MINNESOTA STATE VETERANS CEMETERY - DULUTH", + "cemetery_type": "S", + "num": "116" + } + }, + { + "id": "053", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "053", + "name": "MINNESOTA STATE VETERANS CEMETERY - LITTLE FALLS", + "cemetery_type": "S", + "num": "053" + } + }, + { + "id": "415", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "415", + "name": "MINNESOTA STATE VETERANS CEMETERY - PRESTON", + "cemetery_type": "S", + "num": "415" + } + }, + { + "id": "154", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "154", + "name": "MINNESOTA STATE VETERANS CEMETERY - REDWOOD FALLS", + "cemetery_type": "S", + "num": "154" + } + }, + { + "id": "992", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "992", + "name": "MIRAMAR NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "992" + } + }, + { + "id": "278", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "278", + "name": "MISSISSIPPI STATE VETERANS MEMORIAL CEMETERY", + "cemetery_type": "S", + "num": "278" + } + }, + { + "id": "104", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "104", + "name": "MISSOURI STATE VETERANS CEMETERY/JACKSONVILLE", + "cemetery_type": "S", + "num": "104" + } + }, + { + "id": "056", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "056", + "name": "MISSOURI VETERANS CEMETERY / SPRINGFIELD", + "cemetery_type": "S", + "num": "056" + } + }, + { + "id": "103", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "103", + "name": "MISSOURI VETERANS CEMETERY AT BLOOMFIELD", + "cemetery_type": "S", + "num": "103" + } + }, + { + "id": "057", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "057", + "name": "MISSOURI VETERANS CEMETERY/HIGGINSVILLE", + "cemetery_type": "S", + "num": "057" + } + }, + { + "id": "862", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "862", + "name": "MOBILE NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "862" + } + }, + { + "id": "040", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "040", + "name": "MONTANA STATE VETERANS CEMETERY", + "cemetery_type": "S", + "num": "040" + } + }, + { + "id": "147", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "147", + "name": "MONTE CALVARIO CEMETERY", + "cemetery_type": "S", + "num": "147" + } + }, + { + "id": "971", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "971", + "name": "MOROVIS NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "971" + } + }, + { + "id": "063", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "063", + "name": "MOUND CEMETERY", + "cemetery_type": "N", + "num": "063" + } + }, + { + "id": "064", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "064", + "name": "MOUND CITY", + "cemetery_type": "N", + "num": "064" + } + }, + { + "id": "863", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "863", + "name": "MOUND CITY NATIONAL CEMETERY - IL", + "cemetery_type": "N", + "num": "863" + } + }, + { + "id": "864", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "864", + "name": "MOUNTAIN HOME NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "864" + } + }, + { + "id": "065", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "065", + "name": "MT MORIAH SOLDIERS LOT", + "cemetery_type": "N", + "num": "065" + } + }, + { + "id": "066", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "066", + "name": "MT. MORIAH NAVAL PLOT", + "cemetery_type": "N", + "num": "066" + } + }, + { + "id": "067", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "067", + "name": "MT. PLEASANT CEMETERY", + "cemetery_type": "N", + "num": "067" + } + }, + { + "id": "865", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "865", + "name": "NASHVILLE NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "865" + } + }, + { + "id": "866", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "866", + "name": "NATCHEZ NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "866" + } + }, + { + "id": "925", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "925", + "name": "NATIONAL CEMETERY OF THE ALLEGHENIES", + "cemetery_type": "N", + "num": "925" + } + }, + { + "id": "914", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "914", + "name": "NATIONAL MEMORIAL CEMETERY OF ARIZONA", + "cemetery_type": "N", + "num": "914" + } + }, + { + "id": "899", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "899", + "name": "NATIONAL MEMORIAL CEMETERY OF THE PACIFIC", + "cemetery_type": "N", + "num": "899" + } + }, + { + "id": "267", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "267", + "name": "NEBRASKA VETERANS CEMETERY AT ALLIANCE", + "cemetery_type": "S", + "num": "267" + } + }, + { + "id": "867", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "867", + "name": "NEW ALBANY NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "867" + } + }, + { + "id": "868", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "868", + "name": "NEW BERN NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "868" + } + }, + { + "id": "087", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "087", + "name": "NEW HAMPSHIRE STATE CEMETERY", + "cemetery_type": "S", + "num": "087" + } + }, + { + "id": "155", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "155", + "name": "NEW YORK STATE VETERANS CEMETERY - FINGER LAKES", + "cemetery_type": "S", + "num": "155" + } + }, + { + "id": "077", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "077", + "name": "NORTH ALTON CONFEDERATE CEMETERY", + "cemetery_type": "N", + "num": "077" + } + }, + { + "id": "049", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "049", + "name": "NORTH DAKOTA VETERANS CEMETERY", + "cemetery_type": "S", + "num": "049" + } + }, + { + "id": "035", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "035", + "name": "NORTH MISSISSIPPI VETERANS MEMORIAL CEMETERY", + "cemetery_type": "S", + "num": "035" + } + }, + { + "id": "392", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "392", + "name": "NORTHEAST LOUISIANA VETERANS CEMETERY", + "cemetery_type": "S", + "num": "392" + } + }, + { + "id": "130", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "130", + "name": "NORTHERN CALIFORNIA VETERANS CEMETERY AT REDDING", + "cemetery_type": "S", + "num": "130" + } + }, + { + "id": "099", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "099", + "name": "NORTHERN MAINE VETERANS CEMETERY", + "cemetery_type": "S", + "num": "099" + } + }, + { + "id": "046", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "046", + "name": "NORTHERN NEVADA VETERANS MEMORIAL CEMETERY", + "cemetery_type": "S", + "num": "046" + } + }, + { + "id": "102", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "102", + "name": "NORTHERN WISCONSIN VETERANS MEMORIAL CEMETERY", + "cemetery_type": "S", + "num": "102" + } + }, + { + "id": "131", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "131", + "name": "NORTHWEST LOUISIANA VETERANS CEMETERY", + "cemetery_type": "S", + "num": "131" + } + }, + { + "id": "939", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "939", + "name": "NORTHWOODS NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "939" + } + }, + { + "id": "068", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "068", + "name": "OAKDALE CEMETERY", + "cemetery_type": "N", + "num": "068" + } + }, + { + "id": "092", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "092", + "name": "OHIO VETERANS HOME CEMETERY", + "cemetery_type": "S", + "num": "092" + } + }, + { + "id": "918", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "918", + "name": "OHIO WESTERN RESERVE NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "918" + } + }, + { + "id": "935", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "935", + "name": "OMAHA NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "935" + } + }, + { + "id": "016", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "016", + "name": "OREGON TRAIL STATE VETERANS CEMETERY", + "cemetery_type": "S", + "num": "016" + } + }, + { + "id": "148", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "148", + "name": "PENNSYLVANIA SOLDIERS AND SAILORS HOME CEMETERY", + "cemetery_type": "S", + "num": "148" + } + }, + { + "id": "819", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "819", + "name": "PHILADELPHIA NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "819" + } + }, + { + "id": "933", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "933", + "name": "PIKES PEAK NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "933" + } + }, + { + "id": "078", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "078", + "name": "POINT LOOKOUT CONFEDERATE CEMETERY", + "cemetery_type": "N", + "num": "078" + } + }, + { + "id": "870", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "870", + "name": "PORT HUDSON NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "870" + } + }, + { + "id": "900", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "900", + "name": "PRESCOTT NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "900" + } + }, + { + "id": "069", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "069", + "name": "PROSPECT HILL CEMETERY, PA", + "cemetery_type": "N", + "num": "069" + } + }, + { + "id": "070", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "070", + "name": "PROSPECT HILL CEMETERY, VT", + "cemetery_type": "N", + "num": "070" + } + }, + { + "id": "871", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "871", + "name": "PUERTO RICO NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "871" + } + }, + { + "id": "872", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "872", + "name": "QUANTICO NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "872" + } + }, + { + "id": "820", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "820", + "name": "QUINCY NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "820" + } + }, + { + "id": "873", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "873", + "name": "RALEIGH NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "873" + } + }, + { + "id": "013", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "013", + "name": "RHODE ISLAND VETERAN MEMORIAL CEMETERY", + "cemetery_type": "S", + "num": "013" + } + }, + { + "id": "874", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "874", + "name": "RICHMOND NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "874" + } + }, + { + "id": "133", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "133", + "name": "RIO GRANDE VALLEY STATE VETERANS CEMETERY", + "cemetery_type": "S", + "num": "133" + } + }, + { + "id": "901", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "901", + "name": "RIVERSIDE NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "901" + } + }, + { + "id": "079", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "079", + "name": "ROCK ISLAND CONFEDERATE CEMETERY", + "cemetery_type": "N", + "num": "079" + } + }, + { + "id": "821", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "821", + "name": "ROCK ISLAND NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "821" + } + }, + { + "id": "005", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "005", + "name": "ROCKY GAP VETERANS CEMETERY", + "cemetery_type": "S", + "num": "005" + } + }, + { + "id": "902", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "902", + "name": "ROSEBURG NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "902" + } + }, + { + "id": "921", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "921", + "name": "SACRAMENTO VALLEY VA NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "921" + } + }, + { + "id": "875", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "875", + "name": "SAINT AUGUSTINE NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "875" + } + }, + { + "id": "876", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "876", + "name": "SALISBURY NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "876" + } + }, + { + "id": "877", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "877", + "name": "SAN ANTONIO NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "877" + } + }, + { + "id": "119", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "119", + "name": "SAN CARLOS APACHE TRIBE", + "cemetery_type": "S", + "num": "119" + } + }, + { + "id": "903", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "903", + "name": "SAN FRANCISCO NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "903" + } + }, + { + "id": "913", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "913", + "name": "SAN JOAQUIN VALLEY NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "913" + } + }, + { + "id": "085", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "085", + "name": "SANDHILLS STATE VETERANS CEMETERY", + "cemetery_type": "S", + "num": "085" + } + }, + { + "id": "904", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "904", + "name": "SANTA FE NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "904" + } + }, + { + "id": "931", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "931", + "name": "SARASOTA VA NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "931" + } + }, + { + "id": "153", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "153", + "name": "SEAMAN FIRST CLASS BILLY TURNER VETERANS CEMETERY", + "cemetery_type": "S", + "num": "153" + } + }, + { + "id": "149", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "149", + "name": "SEMINOLE NATION AND VETERANS MEMORIAL CEMETERY", + "cemetery_type": "S", + "num": "149" + } + }, + { + "id": "878", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "878", + "name": "SEVEN PINES NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "878" + } + }, + { + "id": "401", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "401", + "name": "SICANGU AKICITA OWICAHE VETERANS CEMETERY", + "cemetery_type": "S", + "num": "401" + } + }, + { + "id": "125", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "125", + "name": "SISSETON WAHPETON OYATE VETERANS CEMETERY", + "cemetery_type": "S", + "num": "125" + } + }, + { + "id": "905", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "905", + "name": "SITKA NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "905" + } + }, + { + "id": "941", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "941", + "name": "SNAKE RIVER CANYON NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "941" + } + }, + { + "id": "138", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "138", + "name": "SOUTH DAKOTA VETERANS CEMETERY", + "cemetery_type": "S", + "num": "138" + } + }, + { + "id": "924", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "924", + "name": "SOUTH FLORIDA VA NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "924" + } + }, + { + "id": "404", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "404", + "name": "SOUTHEAST LOUISIANA VETERANS CEMETERY", + "cemetery_type": "S", + "num": "404" + } + }, + { + "id": "098", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "098", + "name": "SOUTHERN ARIZONA VETERANS MEMORIAL CEMETERY", + "cemetery_type": "S", + "num": "098" + } + }, + { + "id": "389", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "389", + "name": "SOUTHERN MAINE VETERANS CEMETERY", + "cemetery_type": "S", + "num": "389" + } + }, + { + "id": "045", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "045", + "name": "SOUTHERN NEVADA VETERANS MEMORIAL CEMETERY", + "cemetery_type": "S", + "num": "045" + } + }, + { + "id": "060", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "060", + "name": "SOUTHERN WISCONSIN VETERANS MEMORIAL CEMETERY", + "cemetery_type": "S", + "num": "060" + } + }, + { + "id": "126", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "126", + "name": "SOUTHWEST LOUISIANA VETERANS CEMETERY", + "cemetery_type": "S", + "num": "126" + } + }, + { + "id": "186", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "186", + "name": "SOUTHWEST VIRGINIA VETERANS CEMETERY", + "cemetery_type": "S", + "num": "186" + } + }, + { + "id": "009", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "009", + "name": "SPRING GROVE VETERANS CEMETERY", + "cemetery_type": "S", + "num": "009" + } + }, + { + "id": "879", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "879", + "name": "SPRINGFIELD NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "879" + } + }, + { + "id": "947", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "947", + "name": "ST. ALBANS NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "947" + } + }, + { + "id": "880", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "880", + "name": "STAUNTON NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "880" + } + }, + { + "id": "150", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "150", + "name": "SUNSET ILLINOIS VETERAN HOME CEMETERY", + "cemetery_type": "S", + "num": "150" + } + }, + { + "id": "919", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "919", + "name": "TAHOMA NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "919" + } + }, + { + "id": "937", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "937", + "name": "TALLAHASSEE NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "937" + } + }, + { + "id": "044", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "044", + "name": "TENNESSEE STATE VETERANS CEMETERY", + "cemetery_type": "S", + "num": "044" + } + }, + { + "id": "273", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "273", + "name": "TEXAS STATE VETERANS CEMETERY AT ABILENE", + "cemetery_type": "S", + "num": "273" + } + }, + { + "id": "115", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "115", + "name": "TN STATE VETERANS CEMETERY AT PARKERS CROSSROADS", + "cemetery_type": "S", + "num": "115" + } + }, + { + "id": "822", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "822", + "name": "TOGUS NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "822" + } + }, + { + "id": "080", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "080", + "name": "UNION CONFEDERATE MONUMENT SITE", + "cemetery_type": "N", + "num": "080" + } + }, + { + "id": "408", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "408", + "name": "US MILITARY ACADEMY CEMETERY", + "cemetery_type": "M", + "num": "408" + } + }, + { + "id": "170", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "170", + "name": "US SOLDIERS' & AIRMEN'S HOME NATIONAL CEMETERY", + "cemetery_type": "A", + "num": "170" + } + }, + { + "id": "038", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "038", + "name": "UTAH STATE VETERANS CEMETERY", + "cemetery_type": "S", + "num": "038" + } + }, + { + "id": "954", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "954", + "name": "VANCOUVER BARRACKS NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "954" + } + }, + { + "id": "399", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "399", + "name": "VERMONT VETERANS MEMORIAL CEMETERY", + "cemetery_type": "S", + "num": "399" + } + }, + { + "id": "097", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "097", + "name": "VETERANS MEMORIAL CEMETERY OF WESTERN COLORADO", + "cemetery_type": "S", + "num": "097" + } + }, + { + "id": "084", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "084", + "name": "VIRGINIA VETERANS CEMETERY AT AMELIA", + "cemetery_type": "S", + "num": "084" + } + }, + { + "id": "926", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "926", + "name": "WASHINGTON CROSSING NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "926" + } + }, + { + "id": "390", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "390", + "name": "WASHINGTON STATE VETERANS CEMETERY", + "cemetery_type": "S", + "num": "390" + } + }, + { + "id": "145", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "145", + "name": "WATERVLIET ARSENAL POST CEMETERY", + "cemetery_type": "M", + "num": "145" + } + }, + { + "id": "048", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "048", + "name": "WEST TENNESSEE VETERANS CEMETERY", + "cemetery_type": "S", + "num": "048" + } + }, + { + "id": "171", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "171", + "name": "WEST TEXAS STATE VETERANS CEMETERY", + "cemetery_type": "S", + "num": "171" + } + }, + { + "id": "912", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "912", + "name": "WEST VIRGINIA NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "912" + } + }, + { + "id": "051", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "051", + "name": "WESTERN CAROLINA STATE VETERAN CEMETERY", + "cemetery_type": "S", + "num": "051" + } + }, + { + "id": "388", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "388", + "name": "WESTERN MONTANA STATE VETERANS CEMETERY", + "cemetery_type": "S", + "num": "388" + } + }, + { + "id": "936", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "936", + "name": "WESTERN NEW YORK NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "936" + } + }, + { + "id": "146", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "146", + "name": "WHITE EAGLE CEMETERY", + "cemetery_type": "S", + "num": "146" + } + }, + { + "id": "907", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "907", + "name": "WILLAMETTE NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "907" + } + }, + { + "id": "881", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "881", + "name": "WILMINGTON NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "881" + } + }, + { + "id": "882", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "882", + "name": "WINCHESTER NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "882" + } + }, + { + "id": "823", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "823", + "name": "WOOD NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "823" + } + }, + { + "id": "071", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "071", + "name": "WOODLAND CEMETERY", + "cemetery_type": "N", + "num": "071" + } + }, + { + "id": "072", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "072", + "name": "WOODLAWN CEMETERY", + "cemetery_type": "N", + "num": "072" + } + }, + { + "id": "081", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "081", + "name": "WOODLAWN MONUMENT SITE", + "cemetery_type": "N", + "num": "081" + } + }, + { + "id": "824", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "824", + "name": "WOODLAWN NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "824" + } + }, + { + "id": "305", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "305", + "name": "YELLOWSTONE COUNTY VETERAN'S CEMETERY", + "cemetery_type": "S", + "num": "305" + } + }, + { + "id": "938", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "938", + "name": "YELLOWSTONE NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "938" + } + }, + { + "id": "405", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "405", + "name": "YUROK VETERANS CEMETERY", + "cemetery_type": "S", + "num": "405" + } + }, + { + "id": "883", + "type": "preneeds_cemeteries", + "attributes": { + "cemetery_id": "883", + "name": "ZACHARY TAYLOR NATIONAL CEMETERY", + "cemetery_type": "N", + "num": "883" + } + } + ] +} diff --git a/modules/ivc_champva/app/models/ivc_champva/vha_10_10d.rb b/modules/ivc_champva/app/models/ivc_champva/vha_10_10d.rb new file mode 100644 index 00000000000..c18734f0b46 --- /dev/null +++ b/modules/ivc_champva/app/models/ivc_champva/vha_10_10d.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module IvcChampva + class VHA1010d + include Virtus.model(nullify_blank: true) + include Attachments + + attribute :data + + def initialize(data) + @data = data + @uuid = SecureRandom.uuid + @form_id = 'vha_10_10d' + end + + def metadata + { + 'veteranFirstName' => @data.dig('veteran', 'full_name', 'first'), + 'veteranMiddleName' => @data.dig('veteran', 'full_name', 'middle'), + 'veteranLastName' => @data.dig('veteran', 'full_name', 'last'), + 'sponsorFirstName' => @data.fetch('applicants', [])&.first&.dig('full_name', 'first'), + 'sponsorMiddleName' => @data.fetch('applicants', [])&.first&.dig('full_name', 'middle'), + 'sponsorLastName' => @data.fetch('applicants', [])&.first&.dig('full_name', 'last'), + 'fileNumber' => @data.dig('veteran', 'va_claim_number').presence || @data.dig('veteran', 'ssn_or_tin'), + 'zipCode' => @data.dig('veteran', 'address', 'postal_code') || '00000', + 'source' => 'VA Platform Digital Forms', + 'docType' => @data['form_number'], + 'businessLine' => 'CMP', + 'ssn_or_tin' => @data.dig('veteran', 'ssn_or_tin'), + 'uuid' => @uuid + } + end + + def submission_date_config + { should_stamp_date?: false } + end + + def method_missing(_, *args) + args&.first + end + + def respond_to_missing?(_) + true + end + end +end diff --git a/modules/ivc_champva/app/models/ivc_champva/vha_10_7959c.rb b/modules/ivc_champva/app/models/ivc_champva/vha_10_7959c.rb new file mode 100644 index 00000000000..8cc2011d05f --- /dev/null +++ b/modules/ivc_champva/app/models/ivc_champva/vha_10_7959c.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module IvcChampva + class VHA107959c + include Virtus.model(nullify_blank: true) + + attribute :data + + def initialize(data) + @data = data + end + + def metadata + { + 'veteranFirstName' => @data.dig('applicants', 'full_name', 'first'), + 'veteranMiddleName' => @data.dig('applicants', 'full_name', 'middle'), + 'veteranLastName' => @data.dig('applicants', 'full_name', 'last'), + 'fileNumber' => @data.dig('applicants', 'ssn_or_tin'), + 'zipCode' => @data.dig('applicants', 'address', 'postal_code') || '00000', + 'source' => 'VA Platform Digital Forms', + 'docType' => @data['form_number'], + 'businessLine' => 'CMP' + } + end + + def submission_date_config + { + should_stamp_date?: false, + page_number: 1, + title_coords: [440, 690], + text_coords: [440, 670] + } + end + + def method_missing(_, *args) + args&.first + end + + def respond_to_missing?(_) + true + end + end +end diff --git a/modules/ivc_champva/app/models/ivc_champva/vha_10_7959f_1.rb b/modules/ivc_champva/app/models/ivc_champva/vha_10_7959f_1.rb new file mode 100644 index 00000000000..ba80a283df4 --- /dev/null +++ b/modules/ivc_champva/app/models/ivc_champva/vha_10_7959f_1.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module IvcChampva + class VHA107959f1 + include Virtus.model(nullify_blank: true) + + attribute :data + + def initialize(data) + @data = data + end + + def metadata + { + 'veteranFirstName' => @data.dig('veteran', 'full_name', 'first'), + 'veteranMiddleName' => @data.dig('veteran', 'full_name', 'middle'), + 'veteranLastName' => @data.dig('veteran', 'full_name', 'last'), + 'fileNumber' => @data.dig('veteran', 'va_claim_number').presence || @data.dig('veteran', 'ssn'), + 'zipCode' => @data.dig('veteran', 'mailing_address', 'postal_code') || '00000', + 'source' => 'VA Platform Digital Forms', + 'docType' => @data['form_number'], + 'businessLine' => 'CMP' + } + end + + def submission_date_config + { should_stamp_date?: false } + end + + def method_missing(_, *args) + args&.first + end + + def respond_to_missing?(_) + true + end + end +end diff --git a/modules/ivc_champva/app/models/ivc_champva/vha_10_7959f_2.rb b/modules/ivc_champva/app/models/ivc_champva/vha_10_7959f_2.rb new file mode 100644 index 00000000000..11006fb01f1 --- /dev/null +++ b/modules/ivc_champva/app/models/ivc_champva/vha_10_7959f_2.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module IvcChampva + class VHA107959f2 + include Virtus.model(nullify_blank: true) + include Attachments + + attribute :data + + def initialize(data) + @data = data + @uuid = SecureRandom.uuid + @form_id = 'vha_10_7959f_2' + end + + def metadata + { + 'veteranFirstName' => @data.dig('veteran', 'full_name', 'first'), + 'veteranMiddleName' => @data.dig('veteran', 'full_name', 'middle'), + 'veteranLastName' => @data.dig('veteran', 'full_name', 'last'), + 'fileNumber' => @data.dig('veteran', 'va_claim_number').presence || @data.dig('veteran', 'ssn'), + 'zipCode' => @data.dig('veteran', 'mailing_address', 'postal_code') || '00000', + 'source' => 'VA Platform Digital Forms', + 'docType' => @data['form_number'], + 'businessLine' => 'CMP', + 'uuid' => @uuid + } + end + + def submission_date_config + { should_stamp_date?: false } + end + + def method_missing(_, *args) + args&.first + end + + def respond_to_missing?(_) + true + end + end +end diff --git a/modules/ivc_champva/app/services/ivc_champva/attachments.rb b/modules/ivc_champva/app/services/ivc_champva/attachments.rb new file mode 100644 index 00000000000..05623a05f51 --- /dev/null +++ b/modules/ivc_champva/app/services/ivc_champva/attachments.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module IvcChampva + module Attachments + attr_accessor :form_id, :uuid, :data + + def handle_attachments(file_path) + file_path_uuid = file_path.gsub("#{form_id}-tmp", "#{uuid}_#{form_id}-tmp") + File.rename(file_path, file_path_uuid) + attachments = get_attachments + file_paths = [file_path_uuid] + + if attachments.count.positive? + attachments.each_with_index do |attachment, index| + new_file_name = "#{uuid}_#{form_id}-tmp#{index + 1}.pdf" + new_file_path = File.join(File.dirname(attachment), new_file_name) + File.rename(attachment, new_file_path) + file_paths << new_file_path + end + end + + file_paths + end + + private + + def get_attachments + attachments = [] + + supporting_documents = @data['supporting_docs'] + if supporting_documents + confirmation_codes = [] + supporting_documents&.map { |doc| confirmation_codes << doc['confirmation_code'] } + PersistentAttachment.where(guid: confirmation_codes).map { |attachment| attachments << attachment.to_pdf } + end + + attachments + end + end +end diff --git a/modules/ivc_champva/app/services/ivc_champva/file_uploader.rb b/modules/ivc_champva/app/services/ivc_champva/file_uploader.rb new file mode 100644 index 00000000000..ff633083d85 --- /dev/null +++ b/modules/ivc_champva/app/services/ivc_champva/file_uploader.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +module IvcChampva + class FileUploader + def initialize(form_id, metadata, file_paths) + @form_id = form_id + @metadata = metadata || {} + @file_paths = Array(file_paths) + end + + def handle_uploads + pdf_results = @file_paths.map do |pdf_file_path| + upload_pdf(pdf_file_path) + end + + all_pdf_success = pdf_results.all? { |(status, _)| status == 200 } + + if all_pdf_success + generate_and_upload_meta_json + else + pdf_results + end + end + + private + + def upload_pdf(file_path) + file_name = file_path.gsub('tmp/', '').gsub('-tmp', '') + upload(file_name, file_path) + end + + def generate_and_upload_meta_json + meta_file_name = "#{@form_id}_metadata.json" + meta_file_path = "tmp/#{meta_file_name}" + + File.write(meta_file_path, @metadata.to_json) + meta_upload_status, meta_upload_error_message = upload(meta_file_name, meta_file_path) + + if meta_upload_status == 200 + FileUtils.rm_f(meta_file_path) + [meta_upload_status, nil] + else + [meta_upload_status, meta_upload_error_message] + end + end + + def upload(file_name, file_path) + case client.put_object(file_name, file_path, @metadata) + in { success: true } + [200] + in { success: false, error_message: error_message } + [400, error_message] + else + [500, 'Unexpected response from S3 upload'] + end + end + + def client + @client ||= IvcChampva::S3.new( + region: Settings.ivc_forms.s3.region, + access_key_id: Settings.ivc_forms.s3.aws_access_key_id, + secret_access_key: Settings.ivc_forms.s3.aws_secret_access_key, + bucket: Settings.ivc_forms.s3.bucket + ) + end + end +end diff --git a/modules/ivc_champva/app/services/ivc_champva/metadata_validator.rb b/modules/ivc_champva/app/services/ivc_champva/metadata_validator.rb new file mode 100644 index 00000000000..01fba1c9fbf --- /dev/null +++ b/modules/ivc_champva/app/services/ivc_champva/metadata_validator.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +module IvcChampva + class MetadataValidator + def self.validate(metadata, zip_code_is_us_based: true) + validate_first_name(metadata) + .then { |m| validate_last_name(m) } + .then { |m| validate_file_number(m) } + .then { |m| validate_zip_code(m, zip_code_is_us_based) } + .then { |m| validate_source(m) } + .then { |m| validate_doc_type(m) } + end + + def self.validate_first_name(metadata) + validate_presence_and_stringiness(metadata['veteranFirstName'], 'veteran first name') + metadata['veteranFirstName'] = + I18n.transliterate(metadata['veteranFirstName']).gsub(%r{[^a-zA-Z\-\/\s]}, '').strip.first(50) + + metadata + end + + def self.validate_last_name(metadata) + validate_presence_and_stringiness(metadata['veteranLastName'], 'veteran last name') + metadata['veteranLastName'] = + I18n.transliterate(metadata['veteranLastName']).gsub(%r{[^a-zA-Z\-\/\s]}, '').strip.first(50) + + metadata + end + + def self.validate_file_number(metadata) + validate_presence_and_stringiness(metadata['fileNumber'], 'file number') + unless metadata['fileNumber'].match?(/^\d{8,9}$/) + raise ArgumentError, 'file number is invalid. It must be 8 or 9 digits' + end + + metadata + end + + def self.validate_zip_code(metadata, zip_code_is_us_based) + zip_code = metadata['zipCode'] + if zip_code_is_us_based + validate_presence_and_stringiness(zip_code, 'zip code') + zip_code = zip_code.dup.gsub(/[^0-9]/, '') + zip_code.insert(5, '-') if zip_code.match?(/\A[0-9]{9}\z/) + zip_code = '00000' unless zip_code.match?(/\A[0-9]{5}(-[0-9]{4})?\z/) + else + zip_code = '00000' + end + + metadata['zipCode'] = zip_code + + metadata + end + + def self.validate_source(metadata) + validate_presence_and_stringiness(metadata['source'], 'source') + + metadata + end + + def self.validate_doc_type(metadata) + validate_presence_and_stringiness(metadata['docType'], 'doc type') + + metadata + end + + def self.validate_presence_and_stringiness(value, error_label) + raise ArgumentError, "#{error_label} is missing" unless value + raise ArgumentError, "#{error_label} is not a string" if value.class != String + end + end +end diff --git a/modules/ivc_champva/app/services/ivc_champva/pdf_filler.rb b/modules/ivc_champva/app/services/ivc_champva/pdf_filler.rb new file mode 100644 index 00000000000..db0ad19ac2b --- /dev/null +++ b/modules/ivc_champva/app/services/ivc_champva/pdf_filler.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require 'common/file_helpers' + +module IvcChampva + class PdfFiller + attr_accessor :form, :form_number, :name + + TEMPLATE_BASE = Rails.root.join('modules', 'simple_forms_api', 'templates') + + def initialize(form_number:, form:, name: nil) + @form = form + @form_number = form_number + @name = name || form_number + end + + def generate(current_loa = nil) + template_form_path = "#{TEMPLATE_BASE}/#{form_number}.pdf" + generated_form_path = "tmp/#{name}-tmp.pdf" + stamped_template_path = "tmp/#{name}-stamped.pdf" + pdftk = PdfForms.new(Settings.binaries.pdftk) + FileUtils.copy(template_form_path, stamped_template_path) + PdfStamper.stamp_pdf(stamped_template_path, form, current_loa) + if File.exist? stamped_template_path + pdftk.fill_form(stamped_template_path, generated_form_path, mapped_data, flatten: true) + generated_form_path + else + raise "stamped template file does not exist: #{stamped_template_path}" + end + ensure + Common::FileHelpers.delete_file_if_exists(stamped_template_path) if defined?(stamped_template_path) + end + + def mapped_data + template = Rails.root.join('modules', 'simple_forms_api', 'app', 'form_mappings', "#{form_number}.json.erb").read + b = binding + b.local_variable_set(:data, form) + result = ERB.new(template).result(b) + JSON.parse(escape_json_string(result)) + end + + def escape_json_string(str) + # remove characters that will break the json parser + # \u0000-\u001f: control characters in the ASCII table, + # characters such as null, tab, line feed, and carriage return + # \u0080-\u009f: control characters in the Latin-1 Supplement block of Unicode + # \u2000-\u201f: various punctuation and other non-printable characters in Unicode, + # including various types of spaces, dashes, and quotation marks. + str.gsub(/[\u0000-\u001f\u0080-\u009f\u2000-\u201f]/, ' ') + end + end +end diff --git a/modules/ivc_champva/app/services/ivc_champva/pdf_stamper.rb b/modules/ivc_champva/app/services/ivc_champva/pdf_stamper.rb new file mode 100644 index 00000000000..5fd0d297619 --- /dev/null +++ b/modules/ivc_champva/app/services/ivc_champva/pdf_stamper.rb @@ -0,0 +1,123 @@ +# frozen_string_literal: true + +require 'central_mail/datestamp_pdf' + +module IvcChampva + class PdfStamper + FORM_REQUIRES_STAMP = %w[10-7959F-1].freeze + SUBMISSION_TEXT = 'Signed electronically and submitted via VA.gov at ' + SUBMISSION_DATE_TITLE = 'Application Submitted:' + + def self.stamp_pdf(stamped_template_path, form, current_loa) + form_number = form.data['form_number'] + if FORM_REQUIRES_STAMP.include? form_number + stamp_method = "stamp#{form_number.gsub('-', '')}".downcase + send(stamp_method, stamped_template_path, form) + end + + current_time = "#{Time.current.in_time_zone('America/Chicago').strftime('%H:%M:%S')} " + auth_text = case current_loa + when 3 + 'Signee signed with an identity-verified account.' + when 2 + 'Signee signed in but hasn’t verified their identity.' + else + 'Signee not signed in.' + end + stamp_text = SUBMISSION_TEXT + current_time + desired_stamps = [[10, 10, stamp_text]] + 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 + + def self.stamp107959f1(stamped_template_path, form) + desired_stamps = [[26, 82.5, form.data['statement_of_truth_signature']]] + append_to_stamp = false + verify(stamped_template_path) { stamp(desired_stamps, stamped_template_path, append_to_stamp) } + end + + def self.multistamp(stamped_template_path, signature_text, page_configuration, font_size = 16) + stamp_path = Common::FileHelpers.random_file_path + Prawn::Document.generate(stamp_path, margin: [0, 0]) do |pdf| + page_configuration.each do |config| + case config[:type] + when :text + pdf.draw_text signature_text, at: config[:position], size: font_size + when :new_page + pdf.start_new_page + end + end + end + + perform_multistamp(stamped_template_path, stamp_path) + rescue => e + Rails.logger.error 'Simple forms api - Failed to generate stamped file', message: e.message + raise + ensure + Common::FileHelpers.delete_file_if_exists(stamp_path) if defined?(stamp_path) + end + + 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| + 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 + + def self.perform_multistamp(stamped_template_path, stamp_path) + out_path = "#{Common::FileHelpers.random_file_path}.pdf" + pdftk = PdfFill::Filler::PDF_FORMS + pdftk.multistamp(stamped_template_path, stamp_path, out_path) + File.delete(stamped_template_path) + File.rename(out_path, stamped_template_path) + rescue + Common::FileHelpers.delete_file_if_exists(out_path) + raise + end + + def self.stamp_submission_date(stamped_template_path, config) + if config[:should_stamp_date?] + date_title_stamp_position = config[:title_coords] + date_text_stamp_position = config[:text_coords] + page_configuration = default_page_configuration + page_configuration[config[:page_number]] = { type: :text, position: date_title_stamp_position } + + 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 } + + 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 }, + { type: :new_page }, + { type: :new_page }, + { type: :new_page } + ] + end + end +end diff --git a/modules/ivc_champva/app/services/ivc_champva/s3.rb b/modules/ivc_champva/app/services/ivc_champva/s3.rb new file mode 100644 index 00000000000..7e9a19a0a3f --- /dev/null +++ b/modules/ivc_champva/app/services/ivc_champva/s3.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +# S3 Module for ivc form submission +# Return +# { success: Boolean, [error_message: String] } +module IvcChampva + class S3 + attr_reader :region, :access_key_id, :secret_access_key, :bucket + + def initialize(region:, access_key_id:, secret_access_key:, bucket:) + @region = region + @access_key_id = access_key_id + @secret_access_key = secret_access_key + @bucket = bucket + end + + def put_object(key, file, metadata = {}) + Datadog::Tracing.trace('S3 Put File(s)') do + # Convert nil values to empty strings in the metadata + metadata&.transform_values! { |value| value || '' } + + client.put_object({ + bucket:, + key:, + body: File.read(file), + metadata: + }) + { success: true } + rescue => e + { success: false, error_message: "S3 PutObject failure for #{file}: #{e.message}" } + end + end + + def upload_file(key, file) + Datadog::Tracing.trace('S3 Upload File(s)') do + obj = resource.bucket(bucket).object(key) + obj.upload_file(file) + + { success: true } + rescue => e + { success: false, error_message: "S3 UploadFile failure for #{file}: #{e.message}" } + end + end + + private + + def client + @client ||= Aws::S3::Client.new( + region:, + access_key_id:, + secret_access_key: + ) + end + + def resource + @resource ||= Aws::S3::Resource.new(client:) + end + end +end diff --git a/modules/ivc_champva/bin/rails b/modules/ivc_champva/bin/rails new file mode 100755 index 00000000000..9b6185dcb07 --- /dev/null +++ b/modules/ivc_champva/bin/rails @@ -0,0 +1,12 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +ENGINE_ROOT = File.expand_path('../..', __dir__) +ENGINE_PATH = File.expand_path('../../lib/ivcchampva/engine', __dir__) + +# Set up gems listed in the Gemfile. +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __dir__) +require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) + +require 'rails/all' +require 'rails/engine/commands' diff --git a/modules/ivc_champva/config/routes.rb b/modules/ivc_champva/config/routes.rb new file mode 100644 index 00000000000..6bdddf4ae08 --- /dev/null +++ b/modules/ivc_champva/config/routes.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +IvcChampva::Engine.routes.draw do + namespace :v1, defaults: { format: 'json' } do + post '/forms', to: 'uploads#submit' + post '/forms/submit_supporting_documents', to: 'uploads#submit_supporting_documents' + end +end diff --git a/modules/ivc_champva/ivc_champva.gemspec b/modules/ivc_champva/ivc_champva.gemspec new file mode 100644 index 00000000000..29e05d6cb81 --- /dev/null +++ b/modules/ivc_champva/ivc_champva.gemspec @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +$LOAD_PATH.push File.expand_path('lib', __dir__) + +# Maintain your gem's version: +require 'ivc_champva/version' + +# Describe your gem and declare its dependencies: +Gem::Specification.new do |spec| + spec.name = 'ivc_champva' + spec.version = IvcChampva::VERSION + spec.authors = ['Bryan Alexander', 'Don Shin'] + spec.email = ['bryan.alexander@adhocteam.us', 'donald.shin@agile6.com'] + spec.homepage = 'https://api.va.gov' + spec.summary = 'An api.va.gov module' + spec.description = 'This module is responsible for parsing and filling IVC CHAMPVA forms' + spec.license = 'CC0-1.0' + + spec.files = Dir['{app,config,db,lib}/**/*', 'Rakefile', 'README.md'] +end diff --git a/modules/ivc_champva/lib/ivc_champva.rb b/modules/ivc_champva/lib/ivc_champva.rb new file mode 100644 index 00000000000..a358580f85b --- /dev/null +++ b/modules/ivc_champva/lib/ivc_champva.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +require 'ivc_champva/engine' +require 'securerandom' + +module IvcChampva + # Your code goes here... +end diff --git a/modules/ivc_champva/lib/ivc_champva/engine.rb b/modules/ivc_champva/lib/ivc_champva/engine.rb new file mode 100644 index 00000000000..e5e34440026 --- /dev/null +++ b/modules/ivc_champva/lib/ivc_champva/engine.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module IvcChampva + class Engine < ::Rails::Engine + isolate_namespace IvcChampva + config.generators.api_only = true + + initializer 'model_core.factories', after: 'factory_bot.set_factory_paths' do + FactoryBot.definition_file_paths << File.expand_path('../../spec/factories', __dir__) if defined?(FactoryBot) + end + end +end diff --git a/modules/ivc_champva/lib/ivc_champva/version.rb b/modules/ivc_champva/lib/ivc_champva/version.rb new file mode 100644 index 00000000000..d5ff6d86809 --- /dev/null +++ b/modules/ivc_champva/lib/ivc_champva/version.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module IvcChampva + VERSION = '0.1.0' +end diff --git a/modules/ivc_champva/lib/tasks/forms.rake b/modules/ivc_champva/lib/tasks/forms.rake new file mode 100644 index 00000000000..05dc99b082d --- /dev/null +++ b/modules/ivc_champva/lib/tasks/forms.rake @@ -0,0 +1,110 @@ +# frozen_string_literal: true + +PDFTK_HOMEBREW_PATH = '/opt/homebrew/bin/pdftk' +PDFTK_LOCAL_PATH = '/usr/local/bin/pdftk' +MODELS_PATH = 'modules/ivc_champva/app/models/ivc_champva' +MAPPINGS_PATH = 'modules/ivc_champva/app/form_mappings' + +# rubocop:disable Metrics/BlockLength +namespace :ivc_champva do + task :generate, [:form_path] => :environment do |_, args| + file_path = args[:form_path] + + reader = if File.exist?(PDFTK_HOMEBREW_PATH) + PdfForms.new(PDFTK_HOMEBREW_PATH) + else + PdfForms.new(PDFTK_LOCAL_PATH) + end + + form_name = file_path.split('/').last.split('.').first + + new_model_file = Rails.root.join(MODELS_PATH, "#{form_name}.rb") + + meta_data = reader.get_field_names(file_path).map do |field| + { pdf_field: field, data_type: 'String', attribute: field.split('.').last.split('[').first } + end + + metadata_method = <<-METADATA + def metadata + { + 'veteranFirstName' => @data.dig('veteran', 'full_name', 'first'), + 'veteranLastName' => @data.dig('veteran', 'full_name', 'last'), + 'fileNumber' => @data.dig('veteran', 'va_file_number').presence || @data.dig('veteran', 'ssn'), + 'zipCode' => @data.dig('veteran', 'address', 'postal_code'), + 'source' => 'VA Platform Digital Forms', + 'docType' => @data['form_number'], + 'businessLine' => 'CMP' + } + end + METADATA + + submission_date_config_method = <<-SUB_DATE_CONFIG + def submission_date_config + { should_stamp_date?: false } + end + SUB_DATE_CONFIG + + method_missing_method = <<-METHOD_MISSING + def method_missing(_, *args) + args&.first + end + METHOD_MISSING + + respond_to_missing_method = <<-RESPOND_METHOD_MISSING + def respond_to_missing?(_) + true + end + RESPOND_METHOD_MISSING + + File.open(new_model_file, 'w') do |f| + f.puts '# frozen_string_literal: true' + f.puts '' + f.puts 'module IvcChampva' + f.puts " class #{form_name.upcase.gsub('_', '')}" + f.puts ' include Virtus.model(nullify_blank: true)' + f.puts '' + f.puts ' attribute :data' + + # Attributes are not yet needed. Their advantage is that they provide datatypes that can manipulated such as + # formatting dates. This is also a bit overkill for only central mail + # meta_data.each do |field| + # f.puts " attribute :#{field[:attribute].underscore}" + # end + + f.puts '' + f.puts ' def initialize(data)' + f.puts ' @data = data' + f.puts ' end' + + f.puts '' + + f.puts metadata_method + + f.puts submission_date_config_method + + f.puts method_missing_method + + f.puts respond_to_missing_method + + f.puts ' end' + f.puts 'end' + end + + puts "Created #{new_model_file}" + + # create the form mapping file + mapping_file = Rails.root.join(MAPPINGS_PATH, "#{form_name}.json.erb") + File.open(mapping_file, 'w') do |f| + f.puts '{' + meta_data.each_with_index do |field, index| + puts field.inspect + f.print " \"#{field[:pdf_field]}\": \"<%= data.dig('#{field[:attribute]}') %>" + f.puts "\"#{index + 1 == meta_data.size ? '' : ','}" + end + f.puts '}' + end + + puts "Created #{mapping_file}" + end +end +# rubocop:enable Metrics/BlockLength diff --git a/modules/ivc_champva/spec/fixtures/form_json/vha_10_10d.json b/modules/ivc_champva/spec/fixtures/form_json/vha_10_10d.json new file mode 100644 index 00000000000..5a81f0a6e50 --- /dev/null +++ b/modules/ivc_champva/spec/fixtures/form_json/vha_10_10d.json @@ -0,0 +1,106 @@ +{ + "form_number": "10-10D", + "veteran": { + "date_of_birth": "1987-02-02", + "date_of_marriage": "2005-04-06", + "is_deceased": true, + "date_of_death": "2021-01-08", + "is_active_service_death": true, + "address": { + "country": "USA", + "street": "1 First Ln", + "city": "Place", + "state": "AL", + "postal_code": "12345" + }, + "full_name": { + "first": "Veteran", + "middle": "B", + "last": "Surname" + }, + "ssn_or_tin": "222554444", + "va_claim_number": "123456789", + "phone_number": "9876543213" + }, + "applicants": [ + { + "address": { + "country": "USA", + "street": "2 Second St", + "city": "Town", + "state": "LA", + "postal_code": "16542" + }, + "full_name": { + "first": "Applicant", + "middle": "C", + "last": "Onceler" + }, + "ssn_or_tin": "123456644", + "gender": "F", + "email": "email@address.com", + "phone_number": "6543219877", + "date_of_birth": "1978-03-04", + "is_enrolled_in_medicare": true, + "has_other_health_insurance": true, + "vet_relationship": "Relative - Other" + }, + { + "address": { + "country": "USA", + "street": "3 Third Ave", + "city": "Ville", + "state": "AR", + "postal_code": "65478" + }, + "full_name": { + "first": "Appy", + "middle": "D", + "last": "Twos" + }, + "ssn_or_tin": "123664444", + "gender": "M", + "email": "mailme@domain.com", + "phone_number": "2345698777", + "date_of_birth": "1985-03-10", + "is_enrolled_in_medicare": true, + "has_other_health_insurance": true, + "vet_relationship": "Relative - Other" + }, + { + "address": { + "country": "USA", + "street": "4 Third Ave", + "city": "Mark", + "state": "AR", + "postal_code": "65478" + }, + "full_name": { + "first": "Homer", + "middle": "D", + "last": "Simpson" + }, + "ssn_or_tin": "123664444", + "gender": "M", + "email": "mailme@homer.com", + "phone_number": "2345698777", + "date_of_birth": "1985-03-10", + "is_enrolled_in_medicare": true, + "has_other_health_insurance": true, + "vet_relationship": "Relative - Other" + } + ], + "certification": { + "lastName": "Joe", + "firstName": "GI", + "middleInitial": "Canceled", + "streetAddress": "Hasbro", + "city": "Burbank", + "postal_code": "90041", + "state": "CA", + "relationship": "Agent", + "date": "2021-01-08", + "phone_number": "2345698777" + }, + "statement_of_truth_signature": "GI Joe" +} diff --git a/modules/ivc_champva/spec/fixtures/form_json/vha_10_7959c.json b/modules/ivc_champva/spec/fixtures/form_json/vha_10_7959c.json new file mode 100644 index 00000000000..8798bc1c2ca --- /dev/null +++ b/modules/ivc_champva/spec/fixtures/form_json/vha_10_7959c.json @@ -0,0 +1,76 @@ +{ + "form_number": "10-7959C", + "applicants": { + "address": { + "country": "USA", + "street": "2 Second St", + "city": "Town", + "state": "LA", + "postal_code": "16542" + }, + "full_name": { + "first": "Applicant", + "middle": "C", + "last": "Onceler" + }, + "ssn_or_tin": "123456644", + "home_phone": "6543219877" + }, + + "is_new_address": "T", + "male_or_female": "M", + + "part_a": { + "has_part_a": "T", + "part_a_effective_date": "2010-05-05", + "part_a_carrier": "United Health" + }, + "part_b": { + "has_part_b": "F", + "part_b_effective_date": "2010-05-05", + "part_d_carrier": "United Health" + }, + "part_d": { + "has_part_d": "F", + "part_b_effective_date": "2010-05-05", + "part_d_carrier": "United Health" + }, + "has_pharmacy_benefits": "F", + "has_medicare_advantage": "F", + "has_other_health_insurance": "F", + + "other_health_insurance1": { + "name_of_health_insurance": "Blue Cross", + "date_health_insurance": "2010-02-02", + "terminate_date_health_insurance": "2012-09-09", + "does_insurance": "F", + "does_prescription": "F", + "does_explain": "F", + "is_hmo": true, + "is_ppo": false, + "is_medicaid": false, + "is_rx_discount": false, + "is_medigap": false, + "is_medigap_type": "A", + "is_other_type": false, + "comments": "many stuff to say. Mary had a little lamb. Gingerbread man ate my muffins" + }, + + "other_health_insurance2": { + "name_of_health_insurance": "Aetna", + "date_health_insurance": "2010-02-02", + "terminate_date_health_insurance": "2012-09-09", + "does_insurance": "F", + "does_prescription": "F", + "does_explain": "F", + "is_hmo": true, + "is_ppo": false, + "is_medicaid": false, + "is_rx_discount": false, + "is_medigap": false, + "is_medigap_type": "A", + "is_other_type": false, + "comments": "Row row row your boat gently down the stream. Oh McDonald had a farm eee yaaa" + }, + "statement_of_truth_signature": "Veteran B Surname" +} diff --git a/modules/ivc_champva/spec/fixtures/form_json/vha_10_7959f_1.json b/modules/ivc_champva/spec/fixtures/form_json/vha_10_7959f_1.json new file mode 100644 index 00000000000..b7e43026ac2 --- /dev/null +++ b/modules/ivc_champva/spec/fixtures/form_json/vha_10_7959f_1.json @@ -0,0 +1,31 @@ +{ + "form_number": "10-7959F-1", + "veteran": { + "date_of_birth": "02/02/1987", + "full_name": { + "first": "Veteran", + "middle": "B", + "last": "Surname" + }, + "physical_address": { + "country": "USA", + "street": "1 Physical Ln", + "city": "Place", + "state": "AL", + "postal_code": "12345" + }, + "mailing_address": { + "country": "USA", + "street": "1 Mail Ln", + "city": "Place", + "state": "PA", + "postal_code": "12345" + }, + "ssn": "222554444", + "va_claim_number": "123456789", + "phone_number": "9876543213", + "email_address": "veteran@mail.com" + }, + "statement_of_truth_signature": "Veteran B Surname", + "current_date": "01/01/2024" +} \ No newline at end of file diff --git a/modules/ivc_champva/spec/fixtures/form_json/vha_10_7959f_2.json b/modules/ivc_champva/spec/fixtures/form_json/vha_10_7959f_2.json new file mode 100644 index 00000000000..308f182b3cc --- /dev/null +++ b/modules/ivc_champva/spec/fixtures/form_json/vha_10_7959f_2.json @@ -0,0 +1,32 @@ +{ + "form_number": "10-7959F-2", + "veteran": { + "date_of_birth": "02/02/1987", + "full_name": { + "first": "Veteran", + "middle": "B", + "last": "Surname" + }, + "physical_address": { + "country": "USA", + "street": "1 Physical Ln", + "city": "Place", + "state": "AL", + "postal_code": "12345" + }, + "mailing_address": { + "country": "USA", + "street": "1 Mail Ln", + "city": "Place", + "state": "PA", + "postal_code": "12345" + }, + "ssn": "222554444", + "va_claim_number": "123456789", + "phone_number": "9876543213", + "email_address": "veteran@mail.com" + }, + "statement_of_truth_signature": "Veteran B Surname", + "payment_to_be_sent_type": "Veteran", + "current_date": "01/01/2024" +} \ No newline at end of file diff --git a/modules/ivc_champva/spec/fixtures/test_file/test_file.pdf b/modules/ivc_champva/spec/fixtures/test_file/test_file.pdf new file mode 100644 index 00000000000..c72e23f5a84 Binary files /dev/null and b/modules/ivc_champva/spec/fixtures/test_file/test_file.pdf differ diff --git a/modules/ivc_champva/spec/models/vha_10_10d_spec.rb b/modules/ivc_champva/spec/models/vha_10_10d_spec.rb new file mode 100644 index 00000000000..ecf0f7a2ef9 --- /dev/null +++ b/modules/ivc_champva/spec/models/vha_10_10d_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe IvcChampva::VHA1010d do + let(:data) do + { + 'veteran' => { + 'full_name' => { 'first' => 'John', 'middle' => 'P', 'last' => 'Doe' }, + 'va_claim_number' => '123456789', + 'address' => { 'postal_code' => '12345' } + }, + 'form_number' => 'VHA1010d', + 'veteran_supporting_documents' => [ + { 'confirmation_code' => 'abc123' }, + { 'confirmation_code' => 'def456' } + ] + } + end + let(:vha1010d) { described_class.new(data) } + + describe '#metadata' do + it 'returns metadata for the form' do + metadata = vha1010d.metadata + + expect(metadata).to include( + 'veteranFirstName' => 'John', + 'veteranLastName' => 'Doe', + 'fileNumber' => '123456789', + 'zipCode' => '12345', + 'source' => 'VA Platform Digital Forms', + 'docType' => 'VHA1010d', + 'businessLine' => 'CMP' + ) + end + end +end diff --git a/modules/ivc_champva/spec/models/vha_10_7959c_spec.rb b/modules/ivc_champva/spec/models/vha_10_7959c_spec.rb new file mode 100644 index 00000000000..0cd2139b0f0 --- /dev/null +++ b/modules/ivc_champva/spec/models/vha_10_7959c_spec.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe IvcChampva::VHA107959c do + let(:data) do + { + 'applicants' => { + 'full_name' => { 'first' => 'John', 'middle' => 'P', 'last' => 'Doe' }, + 'ssn_or_tin' => '123456789', + 'address' => { 'postal_code' => '12345' } + }, + 'form_number' => '10-7959C', + 'veteran_supporting_documents' => [ + { 'confirmation_code' => 'abc123' }, + { 'confirmation_code' => 'def456' } + ] + } + end + let(:vha107959c) { described_class.new(data) } + + describe '#metadata' do + it 'returns metadata for the form' do + metadata = vha107959c.metadata + + expect(metadata).to include( + 'veteranFirstName' => 'John', + 'veteranMiddleName' => 'P', + 'veteranLastName' => 'Doe', + 'fileNumber' => '123456789', + 'zipCode' => '12345', + 'source' => 'VA Platform Digital Forms', + 'docType' => '10-7959C', + 'businessLine' => 'CMP' + ) + end + end + + describe '#method_missing' do + context 'when method is missing' do + it 'returns the arguments passed to it' do + args = %w[arg1 arg2] + expect(IvcChampva::VHA107959c.new('data').handle_attachments(args)).to eq(args) + end + end + end +end diff --git a/modules/ivc_champva/spec/models/vha_10_7959f_1_spec.rb b/modules/ivc_champva/spec/models/vha_10_7959f_1_spec.rb new file mode 100644 index 00000000000..5c767a361bb --- /dev/null +++ b/modules/ivc_champva/spec/models/vha_10_7959f_1_spec.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe IvcChampva::VHA107959f1 do + let(:data) do + { + 'veteran' => { + 'full_name' => { 'first' => 'John', 'middle' => 'P', 'last' => 'Doe' }, + 'va_claim_number' => '123456789', + 'mailing_address' => { 'postal_code' => '12345' } + }, + 'form_number' => '10-7959F-1', + 'veteran_supporting_documents' => [ + { 'confirmation_code' => 'abc123' }, + { 'confirmation_code' => 'def456' } + ] + } + end + let(:vha107959f1) { described_class.new(data) } + + describe '#metadata' do + it 'returns metadata for the form' do + metadata = vha107959f1.metadata + + expect(metadata).to include( + 'veteranFirstName' => 'John', + 'veteranMiddleName' => 'P', + 'veteranLastName' => 'Doe', + 'fileNumber' => '123456789', + 'zipCode' => '12345', + 'source' => 'VA Platform Digital Forms', + 'docType' => '10-7959F-1', + 'businessLine' => 'CMP' + ) + end + end + + describe '#method_missing' do + context 'when method is missing' do + it 'returns the arguments passed to it' do + args = %w[arg1 arg2] + expect(IvcChampva::VHA107959f1.new('data').handle_attachments(args)).to eq(args) + end + end + end +end diff --git a/modules/ivc_champva/spec/models/vha_10_7959f_2_spec.rb b/modules/ivc_champva/spec/models/vha_10_7959f_2_spec.rb new file mode 100644 index 00000000000..3b275d916f9 --- /dev/null +++ b/modules/ivc_champva/spec/models/vha_10_7959f_2_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe IvcChampva::VHA107959f2 do + let(:data) do + { + 'veteran' => { + 'full_name' => { 'first' => 'John', 'middle' => 'P', 'last' => 'Doe' }, + 'va_claim_number' => '123456789', + 'mailing_address' => { 'postal_code' => '12345' } + }, + 'form_number' => '10-7959F-2', + 'veteran_supporting_documents' => [ + { 'confirmation_code' => 'abc123' }, + { 'confirmation_code' => 'def456' } + ] + } + end + let(:vha107959f2) { described_class.new(data) } + + describe '#metadata' do + it 'returns metadata for the form' do + metadata = vha107959f2.metadata + + expect(metadata).to include( + 'veteranFirstName' => 'John', + 'veteranMiddleName' => 'P', + 'veteranLastName' => 'Doe', + 'fileNumber' => '123456789', + 'zipCode' => '12345', + 'source' => 'VA Platform Digital Forms', + 'docType' => '10-7959F-2', + 'businessLine' => 'CMP' + ) + end + end +end diff --git a/modules/ivc_champva/spec/requests/v1/uploads_spec.rb b/modules/ivc_champva/spec/requests/v1/uploads_spec.rb new file mode 100644 index 00000000000..5872f8b048b --- /dev/null +++ b/modules/ivc_champva/spec/requests/v1/uploads_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Forms uploader', type: :request do + forms = [ + 'vha_10_10d.json', + 'vha_10_7959f_1.json', + 'vha_10_7959f_2.json', + 'vha_10_7959c.json' + ] + + describe '#submit' do + forms.each do |form| + fixture_path = Rails.root.join('modules', 'ivc_champva', 'spec', 'fixtures', 'form_json', form) + data = JSON.parse(fixture_path.read) + + it 'uploads a PDF file to S3' do + allow(IvcChampva::MetadataValidator).to receive(:validate) + allow_any_instance_of(Aws::S3::Client).to receive(:put_object).and_return(true) + + post '/ivc_champva/v1/forms', params: data + + expect(response).to have_http_status(:ok) + end + end + end + + describe '#submit_supporting_documents' do + it 'renders the attachment as json' do + clamscan = double(safe?: true) + allow(Common::VirusScan).to receive(:scan).and_return(clamscan) + file = fixture_file_upload('doctors-note.gif') + + data_sets = [ + { form_id: '10-10D', file: } + ] + + data_sets.each do |data| + expect do + post '/ivc_champva/v1/forms/submit_supporting_documents', params: data + end.to change(PersistentAttachment, :count).by(1) + + expect(response).to have_http_status(:ok) + resp = JSON.parse(response.body) + expect(resp['data']['attributes'].keys.sort).to eq(%w[confirmation_code name size]) + expect(PersistentAttachment.last).to be_a(PersistentAttachments::MilitaryRecords) + end + end + end +end diff --git a/modules/ivc_champva/spec/services/attachments_spec.rb b/modules/ivc_champva/spec/services/attachments_spec.rb new file mode 100644 index 00000000000..1bef12c5589 --- /dev/null +++ b/modules/ivc_champva/spec/services/attachments_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require 'rails_helper' + +class TestClass + include IvcChampva::Attachments + attr_accessor :form_id, :uuid, :data + + def initialize(form_id, uuid, data) + @form_id = form_id + @uuid = uuid + @data = data + end +end + +RSpec.describe IvcChampva::Attachments do + # Mocking a class to include the Attachments module + let(:form_id) { '123' } + let(:uuid) { 'abc123' } + let(:data) { { 'supporting_docs' => [{ 'confirmation_codes' => 'doc1' }, { 'confirmation_codes' => 'doc2' }] } } + let(:test_instance) { TestClass.new(form_id, uuid, data) } + + describe '#handle_attachments' do + context 'when there are supporting documents' do + let(:file_path) { 'tmp/123-tmp.pdf' } + + it 'renames and processes attachments' do + expect(File).to receive(:rename).with(file_path, "tmp/#{uuid}_#{form_id}-tmp.pdf") + expect(test_instance).to receive(:get_attachments).and_return(['attachment1.pdf', 'attachment2.pdf']) + expect(File).to receive(:rename).with('attachment1.pdf', "./#{uuid}_#{form_id}-tmp1.pdf") + expect(File).to receive(:rename).with('attachment2.pdf', "./#{uuid}_#{form_id}-tmp2.pdf") + + result = test_instance.handle_attachments(file_path) + expect(result).to match_array( + ["tmp/#{uuid}_#{form_id}-tmp.pdf", "./#{uuid}_#{form_id}-tmp1.pdf", "./#{uuid}_#{form_id}-tmp2.pdf"] + ) + end + end + + context 'when there are no supporting documents' do + let(:file_path) { 'tmp/123-tmp.pdf' } + + before do + allow(test_instance).to receive(:get_attachments).and_return([]) + end + + it 'renames the file without processing attachments' do + expect(File).to receive(:rename).with(file_path, "tmp/#{uuid}_#{form_id}-tmp.pdf") + + result = test_instance.handle_attachments(file_path) + expect(result).to eq(["tmp/#{uuid}_#{form_id}-tmp.pdf"]) + end + end + end +end diff --git a/modules/ivc_champva/spec/services/file_uploader_spec.rb b/modules/ivc_champva/spec/services/file_uploader_spec.rb new file mode 100644 index 00000000000..4b147fcedea --- /dev/null +++ b/modules/ivc_champva/spec/services/file_uploader_spec.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe IvcChampva::FileUploader do + let(:form_id) { '123' } + let(:metadata) { { key: 'value' } } + let(:file_paths) { ['tmp/file1.pdf', 'tmp/file2.pdf'] } + let(:uploader) { IvcChampva::FileUploader.new(form_id, metadata, file_paths) } + + describe '#handle_uploads' do + context 'when all PDF uploads succeed' do + before do + allow(uploader).to receive(:upload).and_return([200]) + end + + it 'generates and uploads meta JSON' do + expect(uploader).to receive(:generate_and_upload_meta_json).and_return([200, nil]) + uploader.handle_uploads + end + end + + context 'when at least one PDF upload fails' do + before do + allow(uploader).to receive(:upload).and_return([400, 'Upload failed']) + end + + it 'returns an array of upload results' do + expect(uploader.handle_uploads).to eq([[400, 'Upload failed'], [400, 'Upload failed']]) + end + end + end + + describe '#generate_and_upload_meta_json' do + let(:meta_file_path) { "tmp/#{form_id}_metadata.json" } + + before do + allow(File).to receive(:write) + allow(uploader).to receive(:upload).and_return([200, nil]) + allow(FileUtils).to receive(:rm_f) + end + + it 'writes metadata to a JSON file and uploads it' do + expect(File).to receive(:write).with(meta_file_path, metadata.to_json) + expect(uploader).to receive(:upload).with("#{form_id}_metadata.json", meta_file_path).and_return([200, nil]) + uploader.send(:generate_and_upload_meta_json) + end + + context 'when meta upload succeeds' do + it 'deletes the meta file and returns success' do + expect(FileUtils).to receive(:rm_f).with(meta_file_path) + expect(uploader.send(:generate_and_upload_meta_json)).to eq([200, nil]) + end + end + + context 'when meta upload fails' do + before do + allow(uploader).to receive(:upload).and_return([400, 'Upload failed']) + end + + it 'returns the upload error' do + expect(uploader.send(:generate_and_upload_meta_json)).to eq([400, 'Upload failed']) + end + end + end + + describe '#upload' do + let(:s3_client) { double('S3Client') } + + before do + allow(uploader).to receive(:client).and_return(s3_client) + end + + it 'uploads the file to S3 and returns the upload status' do + expect(s3_client).to receive(:put_object).and_return({ success: true }) + expect(uploader.send(:upload, 'file_name', 'file_path')).to eq([200]) + end + + context 'when upload fails' do + it 'returns the error message' do + expect(s3_client).to receive(:put_object).and_return({ success: false, error_message: 'Upload failed' }) + expect(uploader.send(:upload, 'file_name', 'file_path')).to eq([400, 'Upload failed']) + end + end + + context 'when unexpected response from S3' do + it 'returns an unexpected response error' do + expect(s3_client).to receive(:put_object).and_return(nil) + expect(uploader.send(:upload, 'file_name', 'file_path')).to eq([500, 'Unexpected response from S3 upload']) + end + end + end +end diff --git a/modules/ivc_champva/spec/services/metadata_validator_spec.rb b/modules/ivc_champva/spec/services/metadata_validator_spec.rb new file mode 100644 index 00000000000..d7fa5098017 --- /dev/null +++ b/modules/ivc_champva/spec/services/metadata_validator_spec.rb @@ -0,0 +1,288 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe IvcChampva::MetadataValidator do + describe 'metadata is valid' do + it 'returns unmodified metadata' do + metadata = { + 'veteranFirstName' => 'John', + 'veteranLastName' => 'Doe', + 'fileNumber' => '444444444', + 'zipCode' => '12345', + 'source' => 'VA Platform Digital Forms', + 'docType' => '21-0845', + 'businessLine' => 'CMP' + } + + validated_metadata = IvcChampva::MetadataValidator.validate(metadata) + + expect(validated_metadata).to eq(metadata) + end + end + + describe 'metadata key has a missing value' do + it 'raises a missing exception' do + expect do + IvcChampva::MetadataValidator.validate_presence_and_stringiness(nil, 'veteran first name') + end.to raise_error(ArgumentError, 'veteran first name is missing') + end + end + + describe 'metadata key has a non-string value' do + it 'raises a non-string exception' do + expect do + IvcChampva::MetadataValidator.validate_presence_and_stringiness(12, 'veteran first name') + end.to raise_error(ArgumentError, 'veteran first name is not a string') + end + end + + describe 'veteran first name is malformed' do + describe 'too long' do + it 'returns metadata with first 50 characters of veteran first name' do + metadata = { + 'veteranFirstName' => 'Wolfeschlegelsteinhausenbergerdorffwelchevoralternwarengewissenhaftschaferswessenschafe + warenwohlgepflegeundsorgfaltigkeitbeschutzenvonangreifendurchihrraubgierigfeindewelchevoralternzwolftausend + jahresvorandieerscheinenvanderersteerdemenschderraumschiffgebrauchlichtalsseinursprungvonkraftgestartsein + langefahrthinzwischensternartigraumaufdersuchenachdiesternwelchegehabtbewohnbarplanetenkreisedrehensichund + wohinderneurassevonverstandigmenschlichkeitkonntefortpflanzenundsicherfreuenanlebenslanglichfreudeundruhemit + nichteinfurchtvorangreifenvonandererintelligentgeschopfsvonhinzwischensternartigraum', + 'veteranLastName' => 'Doe', + 'fileNumber' => '444444444', + 'zipCode' => '12345', + 'source' => 'VA Platform Digital Forms', + 'docType' => '21-0845', + 'businessLine' => 'CMP' + } + expected_metadata = { + 'veteranFirstName' => 'Wolfeschlegelsteinhausenbergerdorffwelchevoraltern', + 'veteranLastName' => 'Doe', + 'fileNumber' => '444444444', + 'zipCode' => '12345', + 'source' => 'VA Platform Digital Forms', + 'docType' => '21-0845', + 'businessLine' => 'CMP' + } + + validated_metadata = IvcChampva::MetadataValidator.validate(metadata) + + expect(validated_metadata).to eq expected_metadata + end + end + + describe 'contains disallowed characters' do + it 'returns metadata with disallowed characters of veteran first name stripped or corrected' do + metadata = { + 'veteranFirstName' => '2Jöhn~! - Jo/hn?\\', + 'veteranLastName' => 'Doe', + 'fileNumber' => '444444444', + 'zipCode' => '12345', + 'source' => 'VA Platform Digital Forms', + 'docType' => '21-0845', + 'businessLine' => 'CMP' + } + expected_metadata = { + 'veteranFirstName' => 'John - Jo/hn', + 'veteranLastName' => 'Doe', + 'fileNumber' => '444444444', + 'zipCode' => '12345', + 'source' => 'VA Platform Digital Forms', + 'docType' => '21-0845', + 'businessLine' => 'CMP' + } + + validated_metadata = IvcChampva::MetadataValidator.validate(metadata) + + expect(validated_metadata).to eq expected_metadata + end + end + end + + describe 'veteran last name is malformed' do + describe 'too long' do + it 'returns metadata with first 50 characters of veteran last name' do + metadata = { + 'veteranFirstName' => 'John', + 'veteranLastName' => 'Wolfeschlegelsteinhausenbergerdorffwelchevoralternwarengewissenhaftschaferswessenschafe + warenwohlgepflegeundsorgfaltigkeitbeschutzenvonangreifendurchihrraubgierigfeindewelchevoralternzwolftausend + jahresvorandieerscheinenvanderersteerdemenschderraumschiffgebrauchlichtalsseinursprungvonkraftgestartsein + langefahrthinzwischensternartigraumaufdersuchenachdiesternwelchegehabtbewohnbarplanetenkreisedrehensichund + wohinderneurassevonverstandigmenschlichkeitkonntefortpflanzenundsicherfreuenanlebenslanglichfreudeundruhemit + nichteinfurchtvorangreifenvonandererintelligentgeschopfsvonhinzwischensternartigraum', + 'fileNumber' => '444444444', + 'zipCode' => '12345', + 'source' => 'VA Platform Digital Forms', + 'docType' => '21-0845', + 'businessLine' => 'CMP' + } + expected_metadata = { + 'veteranFirstName' => 'John', + 'veteranLastName' => 'Wolfeschlegelsteinhausenbergerdorffwelchevoraltern', + 'fileNumber' => '444444444', + 'zipCode' => '12345', + 'source' => 'VA Platform Digital Forms', + 'docType' => '21-0845', + 'businessLine' => 'CMP' + } + + validated_metadata = IvcChampva::MetadataValidator.validate(metadata) + + expect(validated_metadata).to eq expected_metadata + end + end + + describe 'contains disallowed characters' do + it 'returns metadata with disallowed characters of veteran last name stripped or corrected' do + metadata = { + 'veteranFirstName' => 'John', + 'veteranLastName' => '2Jöh’n~! - J\'o/hn?\\', + 'fileNumber' => '444444444', + 'zipCode' => '12345', + 'source' => 'VA Platform Digital Forms', + 'docType' => '21-0845', + 'businessLine' => 'CMP' + } + expected_metadata = { + 'veteranFirstName' => 'John', + 'veteranLastName' => 'John - Jo/hn', + 'fileNumber' => '444444444', + 'zipCode' => '12345', + 'source' => 'VA Platform Digital Forms', + 'docType' => '21-0845', + 'businessLine' => 'CMP' + } + + validated_metadata = IvcChampva::MetadataValidator.validate(metadata) + + expect(validated_metadata).to eq expected_metadata + end + end + end + + describe 'file number is malformed' do + describe 'too long' do + it 'raises an exception' do + metadata = { + 'veteranFirstName' => 'John', + 'veteranLastName' => 'Doe', + 'fileNumber' => '4444444442789', + 'zipCode' => '12345', + 'source' => 'VA Platform Digital Forms', + 'docType' => '21-0845', + 'businessLine' => 'CMP' + } + + expect do + IvcChampva::MetadataValidator.validate(metadata) + end.to raise_error(ArgumentError, 'file number is invalid. It must be 8 or 9 digits') + end + end + end + + describe 'zip code is malformed' do + it 'defaults to 00000' do + metadata = { + 'veteranFirstName' => 'John', + 'veteranLastName' => 'Doe', + 'fileNumber' => '444444444', + 'zipCode' => '1234567890', + 'source' => 'VA Platform Digital Forms', + 'docType' => '21-0845', + 'businessLine' => 'CMP' + } + expected_metadata = { + 'veteranFirstName' => 'John', + 'veteranLastName' => 'Doe', + 'fileNumber' => '444444444', + 'zipCode' => '00000', + 'source' => 'VA Platform Digital Forms', + 'docType' => '21-0845', + 'businessLine' => 'CMP' + } + + validated_metadata = IvcChampva::MetadataValidator.validate(metadata) + + expect(validated_metadata).to eq expected_metadata + end + end + + describe 'zip code is 9 digits long' do + it 'is transformed to a 5+4 format US zip code' do + metadata = { + 'veteranFirstName' => 'John', + 'veteranLastName' => 'Doe', + 'fileNumber' => '444444444', + 'zipCode' => '123456789', + 'source' => 'VA Platform Digital Forms', + 'docType' => '21-0845', + 'businessLine' => 'CMP' + } + expected_metadata = { + 'veteranFirstName' => 'John', + 'veteranLastName' => 'Doe', + 'fileNumber' => '444444444', + 'zipCode' => '12345-6789', + 'source' => 'VA Platform Digital Forms', + 'docType' => '21-0845', + 'businessLine' => 'CMP' + } + + validated_metadata = IvcChampva::MetadataValidator.validate(metadata) + + expect(validated_metadata).to eq expected_metadata + end + end + + describe 'zip code is not US based' do + it 'is set to 00000' do + metadata = { + 'veteranFirstName' => 'John', + 'veteranLastName' => 'Doe', + 'fileNumber' => '444444444', + 'zipCode' => '12345', + 'source' => 'VA Platform Digital Forms', + 'docType' => '21-0845', + 'businessLine' => 'CMP' + } + expected_metadata = { + 'veteranFirstName' => 'John', + 'veteranLastName' => 'Doe', + 'fileNumber' => '444444444', + 'zipCode' => '00000', + 'source' => 'VA Platform Digital Forms', + 'docType' => '21-0845', + 'businessLine' => 'CMP' + } + + validated_metadata = IvcChampva::MetadataValidator.validate(metadata, zip_code_is_us_based: false) + + expect(validated_metadata).to eq expected_metadata + end + + describe 'zip code is nil' do + it 'is set to 00000' do + metadata = { + 'veteranFirstName' => 'John', + 'veteranLastName' => 'Doe', + 'fileNumber' => '444444444', + 'source' => 'VA Platform Digital Forms', + 'docType' => '21-0845', + 'businessLine' => 'CMP' + } + expected_metadata = { + 'veteranFirstName' => 'John', + 'veteranLastName' => 'Doe', + 'fileNumber' => '444444444', + 'zipCode' => '00000', + 'source' => 'VA Platform Digital Forms', + 'docType' => '21-0845', + 'businessLine' => 'CMP' + } + + validated_metadata = IvcChampva::MetadataValidator.validate(metadata, zip_code_is_us_based: false) + + expect(validated_metadata).to eq expected_metadata + end + end + end +end diff --git a/modules/ivc_champva/spec/services/pdf_filler_spec.rb b/modules/ivc_champva/spec/services/pdf_filler_spec.rb new file mode 100644 index 00000000000..180cea7f956 --- /dev/null +++ b/modules/ivc_champva/spec/services/pdf_filler_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'rails_helper' +require IvcChampva::Engine.root.join('spec', 'spec_helper.rb') + +describe IvcChampva::PdfFiller do + def self.test_pdf_fill(form_number, test_payload = form_number) + 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) + + # fill the PDF + data = JSON.parse(File.read("modules/ivc_champva/spec/fixtures/form_json/#{test_payload}.json")) + form = "IvcChampva::#{form_number.titleize.gsub(' ', '')}".constantize.new(data) + filler = IvcChampva::PdfFiller.new(form_number:, form:) + filler.generate + expect(File.exist?(expected_pdf_path)).to eq(true) + end + end + end + + test_pdf_fill 'vha_10_10d' + test_pdf_fill 'vha_10_7959f_1' + test_pdf_fill 'vha_10_7959f_2' + + def self.test_json_valid(mapping_file) + it 'validates json is parseable' do + expect do + JSON.parse(File.read("modules/ivc_champva/app/form_mappings/#{mapping_file}")) + end.not_to raise_error + end + end + + test_json_valid 'vha_10_10d.json.erb' + test_json_valid 'vha_10_7959f_1.json.erb' + test_json_valid 'vha_10_7959f_2.json.erb' +end diff --git a/modules/ivc_champva/spec/services/pdf_stamper_spec.rb b/modules/ivc_champva/spec/services/pdf_stamper_spec.rb new file mode 100644 index 00000000000..10745e5a04f --- /dev/null +++ b/modules/ivc_champva/spec/services/pdf_stamper_spec.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +require 'rails_helper' +require IvcChampva::Engine.root.join('spec', 'spec_helper.rb') + +describe IvcChampva::PdfStamper do + let(:data) { JSON.parse(File.read("modules/ivc_champva/spec/fixtures/form_json/#{test_payload}.json")) } + let(:form) { "IvcChampva::#{test_payload.titleize.gsub(' ', '')}".constantize.new(data) } + let(:path) { 'tmp/stuff.json' } + + 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 '.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/ivc_champva/spec/services/s3_spec.rb b/modules/ivc_champva/spec/services/s3_spec.rb new file mode 100644 index 00000000000..e7ec408fbea --- /dev/null +++ b/modules/ivc_champva/spec/services/s3_spec.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'common/file_helpers' + +describe IvcChampva::S3 do + let(:region) { 'test-region' } + let(:access_key_id) { 'test-access-key' } + let(:secret_access_key) { 'test-secret-key' } + let(:bucket_name) { 'test-bucket' } + let(:bucket) { instance_double(Aws::S3::Bucket) } + let(:object) { instance_double(Aws::S3::Object) } + + # rubocop:disable Style/HashSyntax + let(:s3_instance) do + IvcChampva::S3.new( + region: region, + access_key_id: access_key_id, + secret_access_key: secret_access_key, + bucket: bucket_name + ) + end + # rubocop:enable Style/HashSyntax + + describe '#put_object' do + let(:key) { 'test_file.pdf' } + let(:file_path) { 'spec/fixtures/files/doctors-note.pdf' } + + context 'when upload is successful' do + before do + allow_any_instance_of(Aws::S3::Client).to receive(:put_object).and_return(true) + end + + it 'returns success response' do + expect(s3_instance.put_object(key, file_path)).to eq({ success: true }) + end + end + + context 'when upload fails' do + before do + allow_any_instance_of(Aws::S3::Client).to receive(:put_object) + .and_raise(Aws::S3::Errors::ServiceError.new(nil, 'upload failed')) + end + + it 'returns error response' do + expect(s3_instance.put_object(key, file_path)) + .to eq({ success: false, error_message: "S3 PutObject failure for #{file_path}: upload failed" }) + end + end + end + + describe '#upload_file' do + let(:key) { 'test_form.pdf' } + let(:file_path) { 'test_form.pdf' } + + context 'when upload is successful' do + before do + allow_any_instance_of(Aws::S3::Resource).to receive(:bucket).and_return(bucket) + allow(bucket).to receive(:object).with(key).and_return(object) + allow(object).to receive(:upload_file).and_return(true) + end + + it 'returns success response' do + expect(s3_instance.upload_file(key, file_path)).to eq({ success: true }) + end + end + + context 'when upload fails' do + before do + allow_any_instance_of(Aws::S3::Resource).to receive(:bucket).and_return(bucket) + allow(bucket).to receive(:object).with(key).and_return(object) + allow(object).to receive(:upload_file).and_raise(Aws::S3::Errors::ServiceError.new(nil, 'upload failed')) + end + + it 'returns error response' do + expect(s3_instance.upload_file(key, file_path)) + .to eq({ success: false, error_message: "S3 UploadFile failure for #{file_path}: upload failed" }) + end + end + end +end diff --git a/modules/ivc_champva/spec/spec_helper.rb b/modules/ivc_champva/spec/spec_helper.rb new file mode 100644 index 00000000000..9b94ee05d3c --- /dev/null +++ b/modules/ivc_champva/spec/spec_helper.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +# Configure Rails Envinronment +ENV['RAILS_ENV'] = 'test' + +require 'rspec/rails' + +RSpec.configure { |config| config.use_transactional_fixtures = true } diff --git a/modules/ivc_champva/templates/vha_10_10d.pdf b/modules/ivc_champva/templates/vha_10_10d.pdf new file mode 100644 index 00000000000..6ff50ae7030 Binary files /dev/null and b/modules/ivc_champva/templates/vha_10_10d.pdf differ diff --git a/modules/ivc_champva/templates/vha_10_7959c.pdf b/modules/ivc_champva/templates/vha_10_7959c.pdf new file mode 100644 index 00000000000..84248cdc042 Binary files /dev/null and b/modules/ivc_champva/templates/vha_10_7959c.pdf differ diff --git a/modules/ivc_champva/templates/vha_10_7959f_1.pdf b/modules/ivc_champva/templates/vha_10_7959f_1.pdf new file mode 100644 index 00000000000..d41a9d53eab Binary files /dev/null and b/modules/ivc_champva/templates/vha_10_7959f_1.pdf differ diff --git a/modules/ivc_champva/templates/vha_10_7959f_2.pdf b/modules/ivc_champva/templates/vha_10_7959f_2.pdf new file mode 100644 index 00000000000..26f6b7ab601 Binary files /dev/null and b/modules/ivc_champva/templates/vha_10_7959f_2.pdf differ diff --git a/modules/meb_api/app/controllers/meb_api/v0/education_benefits_controller.rb b/modules/meb_api/app/controllers/meb_api/v0/education_benefits_controller.rb index a378b4f5bb5..ef37f0a34db 100644 --- a/modules/meb_api/app/controllers/meb_api/v0/education_benefits_controller.rb +++ b/modules/meb_api/app/controllers/meb_api/v0/education_benefits_controller.rb @@ -58,9 +58,10 @@ def claim_letter def submit_claim response_data = nil + if Flipper.enabled?(:show_dgi_direct_deposit_1990EZ, @current_user) && !Rails.env.development? begin - response_data = payment_service.get_ch33_dd_eft_info + response_data = DirectDeposit::Client.new(@current_user&.icn).get_payment_info rescue => e Rails.logger.error("BGS service error: #{e}") head :internal_server_error diff --git a/modules/meb_api/app/controllers/meb_api/v0/forms_controller.rb b/modules/meb_api/app/controllers/meb_api/v0/forms_controller.rb index dc9b7ed081a..568ff34ef3b 100644 --- a/modules/meb_api/app/controllers/meb_api/v0/forms_controller.rb +++ b/modules/meb_api/app/controllers/meb_api/v0/forms_controller.rb @@ -54,7 +54,7 @@ def submit_claim dd_response = nil if Flipper.enabled?(:toe_short_circuit_bgs_failure, @current_user) begin - dd_response = payment_service.get_ch33_dd_eft_info + dd_response = direct_deposit_response rescue => e Rails.logger.error('BDN service error: ', e) head :internal_server_error @@ -96,6 +96,14 @@ def submission_service def payment_service BGS::Service.new(@current_user) end + + def direct_deposit_response + if Flipper.enabled?(:toe_light_house_dgi_direct_deposit, @current_user) + DirectDeposit::Client.new(@current_user&.icn).get_payment_info + else + payment_service.get_ch33_dd_eft_info + end + end end end end diff --git a/modules/meb_api/lib/dgi/forms/service/submission_service.rb b/modules/meb_api/lib/dgi/forms/service/submission_service.rb index 3c546e01cd3..8adae906f53 100644 --- a/modules/meb_api/lib/dgi/forms/service/submission_service.rb +++ b/modules/meb_api/lib/dgi/forms/service/submission_service.rb @@ -49,10 +49,14 @@ def format_params(params) def update_dd_params(params, dd_params) check_masking = params.dig(:form, :direct_deposit, :direct_deposit_account_number).include?('*') - - if check_masking + if check_masking && !Flipper.enabled?(:toe_light_house_dgi_direct_deposit, @current_user) params[:form][:direct_deposit][:direct_deposit_account_number] = dd_params[:dposit_acnt_nbr] params[:form][:direct_deposit][:direct_deposit_routing_number] = dd_params[:routng_trnsit_nbr] + elsif check_masking && Flipper.enabled?(:toe_light_house_dgi_direct_deposit, @current_user) + params[:form][:direct_deposit][:direct_deposit_account_number] = + dd_params&.payment_account ? dd_params.payment_account[:account_number] : nil + params[:form][:direct_deposit][:direct_deposit_routing_number] = + dd_params&.payment_account ? dd_params.payment_account[:routing_number] : nil end params end diff --git a/modules/meb_api/lib/dgi/submission/service.rb b/modules/meb_api/lib/dgi/submission/service.rb index 96969f5503a..85b91d8482c 100644 --- a/modules/meb_api/lib/dgi/submission/service.rb +++ b/modules/meb_api/lib/dgi/submission/service.rb @@ -52,10 +52,14 @@ def format_params(params) def update_dd_params(params, dd_params) account_number = params.dig(:direct_deposit, :account_number) check_masking = account_number&.include?('*') - if check_masking - params[:direct_deposit][:account_number] = dd_params[:dposit_acnt_nbr] - params[:direct_deposit][:routing_number] = dd_params[:routng_trnsit_nbr] + + if check_masking && Flipper.enabled?(:show_dgi_direct_deposit_1990EZ, @current_user) + params[:direct_deposit][:direct_deposit_account_number] = + dd_params&.payment_account ? dd_params.payment_account[:account_number] : nil + params[:direct_deposit][:direct_deposit_routing_number] = + dd_params&.payment_account ? dd_params.payment_account[:routing_number] : nil end + params end diff --git a/modules/meb_api/spec/dgi/forms/service/toe_submission_service_spec.rb b/modules/meb_api/spec/dgi/forms/service/toe_submission_service_spec.rb index 77845deb0c3..bd07082f310 100644 --- a/modules/meb_api/spec/dgi/forms/service/toe_submission_service_spec.rb +++ b/modules/meb_api/spec/dgi/forms/service/toe_submission_service_spec.rb @@ -68,6 +68,31 @@ } end + let(:dd_params_lighthouse) do + { + paymentAccount: { + accountType: 'CHECKING', + accountNumber: '1234567890', + financialInstitutionRoutingNumber: '031000503', + financialInstitutionName: 'WELLSFARGO BANK' + }, + controlInformation: { + canUpdateDirectDeposit: true, + isCorpAvailable: true, + isCorpRecFound: true, + hasNoBdnPayments: true, + hasIndentity: true, + hasIndex: true, + isCompetent: true, + hasMailingAddress: true, + hasNoFiduciaryAssigned: true, + isNotDeceased: true, + hasPaymentAddress: true, + isEduClaimAvailable: true + } + } + end + describe '#submit_toe_claim' do let(:faraday_response) { double('faraday_connection') } @@ -75,12 +100,33 @@ allow(faraday_response).to receive(:env) end - context 'when successful' do - it 'returns a status of 200' do + context 'Feature toe_light_house_dgi_direct_deposit=true' do + before do + Flipper.enable(:toe_light_house_dgi_direct_deposit) + end + + it 'Lighthouse returns a status of 200' do + VCR.use_cassette('dgi/forms/submit_toe_claim') do + response = service.submit_claim(ActionController::Parameters.new(claimant_params), + ActionController::Parameters.new(dd_params_lighthouse), + 'toe') + + expect(response.status).to eq(200) + end + end + end + + context 'Feature toe_light_house_dgi_direct_deposit=false' do + before do + Flipper.disable(:toe_light_house_dgi_direct_deposit) + end + + it 'EVSS returns a status of 200' do VCR.use_cassette('dgi/forms/submit_toe_claim') do response = service.submit_claim(ActionController::Parameters.new(claimant_params), ActionController::Parameters.new(dd_params), 'toe') + expect(response.status).to eq(200) end end diff --git a/modules/meb_api/spec/dgi/submission/service_spec.rb b/modules/meb_api/spec/dgi/submission/service_spec.rb index d61fe28c914..ccb65fec380 100644 --- a/modules/meb_api/spec/dgi/submission/service_spec.rb +++ b/modules/meb_api/spec/dgi/submission/service_spec.rb @@ -66,6 +66,32 @@ routng_trnsit_nbr: '042102115' } end + + let(:dd_params_lighthouse) do + { + 'payment_account' => { + 'account_type' => 'CHECKING', + 'account_number' => '1234567890', + 'financial_institution_routing_number' => '031000503', + 'financial_institution_name' => 'WELLSFARGO BANK' + }, + 'controlInformation' => { + 'canUpdateDirectDeposit' => 'true', + 'isCorpAvailable' => 'true', + 'isCorpRecFound' => 'true', + 'hasNoBdnPayments' => 'true', + 'hasIndentity' => 'true', + 'hasIndex' => 'true', + 'isCompetent' => 'true', + 'hasMailingAddress' => 'true', + 'hasNoFiduciaryAssigned' => 'true', + 'isNotDeceased' => 'true', + 'hasPaymentAddress' => 'true', + 'isEduClaimAvailable' => 'true' + } + } + end + let(:claimant_params_with_asterisks) do duplicated_params = claimant_params.deep_dup # Explicitly creating the nested structure if it doesn't exist @@ -84,25 +110,15 @@ end describe '#submit_claim' do - context 'when successful' do + context 'Lighthouse direct deposit' do it 'returns a status of 200' do - VCR.use_cassette('dgi/submit_claim') do - response = service.submit_claim( - ActionController::Parameters.new(claimant_params[:education_benefit]), - ActionController::Parameters.new(dd_params) - ) - expect(response.status).to eq(200) - end - end - end - - context 'with leading asterisks in account number' do - it 'replaces asterisked account and routing numbers with real values' do - VCR.use_cassette('dgi/submit_claim') do + VCR.use_cassette('dgi/submit_claim_lighthouse') do + lighthouse_dd_response = OpenStruct.new(body: dd_params_lighthouse) response = service.submit_claim( ActionController::Parameters.new(claimant_params_with_asterisks[:education_benefit]), - ActionController::Parameters.new(dd_params) + Lighthouse::DirectDeposit::PaymentInfoParser.parse(lighthouse_dd_response) ) + expect(response.status).to eq(200) end end diff --git a/modules/meb_api/spec/requests/meb_api/v0/education_benefits_spec.rb b/modules/meb_api/spec/requests/meb_api/v0/education_benefits_spec.rb index 3a003a423d2..583333620fa 100644 --- a/modules/meb_api/spec/requests/meb_api/v0/education_benefits_spec.rb +++ b/modules/meb_api/spec/requests/meb_api/v0/education_benefits_spec.rb @@ -112,11 +112,11 @@ params: { "education_benefit": { enrollment_verifications: { enrollment_certify_requests: [{ - "certified_period_begin_date": '2022-08-01', - "certified_period_end_date": '2022-08-31', - "certified_through_date": '2022-08-31', - "certification_method": 'MEB', - "app_communication": { "response_type": 'Y' } + 'certified_period_begin_date': '2022-08-01', + 'certified_period_end_date': '2022-08-31', + 'certified_through_date': '2022-08-31', + 'certification_method': 'MEB', + 'app_communication': { 'response_type': 'Y' } }] } } } expect(response).to have_http_status(:ok) @@ -148,37 +148,61 @@ end end - describe 'POST /meb_api/v0/send_confirmation_email' do - context 'delegates to submit_0994_form_confirmation job' do - it 'with name and email params' do - allow(MebApi::V0::Submit1990mebFormConfirmation).to receive(:perform_async) - - post '/meb_api/v0/send_confirmation_email', params: { - claim_status: 'ELIGIBLE', email: 'test@test.com', first_name: 'test' + describe 'POST /meb_api/v0/submit_claim' do + let(:claimant_params) do + { + form_id: 1, + education_benefit: { + claimant: { + first_name: 'Herbert', + middle_name: 'Hoover', + last_name: 'Hoover', + date_of_birth: '1980-03-11', + contact_info: { + address_line1: '503 upper park', + address_line2: '', + city: 'falls church', + zipcode: '22046', + email_address: 'hhover@test.com', + address_type: 'DOMESTIC', + mobile_phone_number: '4409938894', + country_code: 'US', + state_code: 'VA' + }, + notification_method: 'EMAIL' + } + }, + relinquished_benefit: { + eff_relinquish_date: '2021-10-15', + relinquished_benefit: 'Chapter30' + }, + additional_considerations: { + active_duty_kicker: 'N/A', + academy_rotc_scholarship: 'YES', + reserve_kicker: 'N/A', + senior_rotc_scholarship: 'YES', + active_duty_dod_repay_loan: 'YES' + }, + comments: { + disagree_with_service_period: false + }, + direct_deposit: { + account_number: '********3123', + account_type: 'savings', + routing_number: '*******3123' } + } + end - expect(MebApi::V0::Submit1990mebFormConfirmation).to have_received(:perform_async) - .with('ELIGIBLE', 'test@test.com', 'TEST') - end - - it 'without name and email params uses current user' do - allow(MebApi::V0::Submit1990mebFormConfirmation).to receive(:perform_async) - - post '/meb_api/v0/send_confirmation_email', params: { claim_status: 'DENIED' } - - expect(MebApi::V0::Submit1990mebFormConfirmation).to have_received(:perform_async) - .with('DENIED', 'abraham.lincoln@vets.gov', 'HERBERT') - end - - it 'does not delegate when feature is disabled' do - allow(MebApi::V0::Submit1990mebFormConfirmation).to receive(:perform_async) - Flipper.disable(:form1990meb_confirmation_email) - - post '/meb_api/v0/send_confirmation_email', params: {} - - expect(MebApi::V0::Submit1990mebFormConfirmation).not_to have_received(:perform_async) + context 'direct deposit' do + it 'successfully submits with new lighthouse api' do + VCR.use_cassette('dgi/submit_claim_lighthouse') do + Settings.mobile_lighthouse.rsa_key = OpenSSL::PKey::RSA.generate(2048).to_s + Settings.lighthouse.direct_deposit.use_mocks = true + post '/meb_api/v0/submit_claim', params: claimant_params - Flipper.enable(:form1990meb_confirmation_email) + expect(response).to have_http_status(:ok) + end end end end diff --git a/modules/mobile/app/controllers/mobile/v0/awards_controller.rb b/modules/mobile/app/controllers/mobile/v0/awards_controller.rb index cc66fdc463a..dc9db5ddbf9 100644 --- a/modules/mobile/app/controllers/mobile/v0/awards_controller.rb +++ b/modules/mobile/app/controllers/mobile/v0/awards_controller.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require 'bgs/awards_service' -require 'bid/awards/service' module Mobile module V0 @@ -10,7 +9,6 @@ def index award_data = regular_award_service.get_awards raise Common::Exceptions::BackendServiceException, 'MOBL_502_upstream_error' unless award_data - award_data.merge!(pension_award_service.get_awards_pension.body['awards_pension']&.transform_keys(&:to_sym)) award_data[:id] = current_user.uuid awards = Mobile::V0::Award.new(award_data) render json: AwardSerializer.new(awards) @@ -21,10 +19,6 @@ def index def regular_award_service @regular_award_service ||= BGS::AwardsService.new(current_user) end - - def pension_award_service - @pension_award_service ||= BID::Awards::Service.new(current_user) - end end end end diff --git a/modules/mobile/app/controllers/mobile/v0/clinics_controller.rb b/modules/mobile/app/controllers/mobile/v0/clinics_controller.rb index 6bc38101367..db6dad43eb6 100644 --- a/modules/mobile/app/controllers/mobile/v0/clinics_controller.rb +++ b/modules/mobile/app/controllers/mobile/v0/clinics_controller.rb @@ -16,12 +16,37 @@ def slots response = systems_service.get_available_slots(location_id: facility_id, clinic_id:, + clinical_service: nil, start_dt: start_date, end_dt: end_date) render json: Mobile::V0::ClinicSlotsSerializer.new(response) end + def facility_slots + if params[:clinic_id] || params[:clinical_service] + start_date = params[:start_date] || now.iso8601 + end_date = params[:end_date] || two_months_from_now.iso8601 + + response = systems_service.get_available_slots(location_id: facility_id, + clinic_id: params[:clinic_id], + clinical_service: params[:clinical_service], + start_dt: start_date, + end_dt: end_date) + + render json: Mobile::V0::ClinicSlotsSerializer.new(response) + else + render status: :bad_request, json: { + errors: [ + { + status: 400, + detail: 'clinic_id or clinical_service is required.' + } + ] + } + end + end + private def systems_service diff --git a/modules/mobile/app/controllers/mobile/v0/community_care_providers_controller.rb b/modules/mobile/app/controllers/mobile/v0/community_care_providers_controller.rb index 472238e5c2c..607fd79e093 100644 --- a/modules/mobile/app/controllers/mobile/v0/community_care_providers_controller.rb +++ b/modules/mobile/app/controllers/mobile/v0/community_care_providers_controller.rb @@ -52,7 +52,8 @@ def coordinates end def facility_coordinates - facility = Mobile::FacilitiesHelper.get_facilities(Array(params[:facilityId])).first + v1_facilities_flag = Flipper.enabled?(:mobile_v1_lighthouse_facilities, @current_user) + facility = Mobile::FacilitiesHelper.get_facilities(Array(params[:facilityId]), v1_facilities_flag).first raise Common::Exceptions::RecordNotFound, params[:facilityId] unless facility [facility.lat, facility.long] diff --git a/modules/mobile/app/controllers/mobile/v0/facilities_info_controller.rb b/modules/mobile/app/controllers/mobile/v0/facilities_info_controller.rb index a6d965f96d1..19d49e8cc24 100644 --- a/modules/mobile/app/controllers/mobile/v0/facilities_info_controller.rb +++ b/modules/mobile/app/controllers/mobile/v0/facilities_info_controller.rb @@ -52,7 +52,6 @@ def sort_by_name(facilities) def sort_by_recent_appointment(facilities) appointments = Mobile::V0::Appointment.get_cached(@current_user)&.sort_by(&:start_date_utc) - log_nil_cache if appointments.nil? return facilities if appointments.blank? appointment_facility_ids = appointments.map(&:facility_id).uniq @@ -68,10 +67,6 @@ def sort_by_recent_appointment(facilities) facilities.sort_by { |facility| appointment_facilities_hash[facility.id] || appointment_facility_ids.size } end - def log_nil_cache - Rails.logger.info('mobile facilities info appointments cache nil', user_uuid: @current_user.uuid) - end - def validate_sort_method_inclusion! unless SORT_METHODS.include?(params[:sort]) raise Common::Exceptions::InvalidFieldValue.new('sort', params[:sort]) diff --git a/modules/mobile/app/controllers/mobile/v0/locations_controller.rb b/modules/mobile/app/controllers/mobile/v0/locations_controller.rb index 29a911a1cbe..96e30098a25 100644 --- a/modules/mobile/app/controllers/mobile/v0/locations_controller.rb +++ b/modules/mobile/app/controllers/mobile/v0/locations_controller.rb @@ -9,8 +9,9 @@ def show raise Common::Exceptions::BackendServiceException, 'validation_errors_bad_request' end - id = lh_location[:identifier].first[:value][4..6] - facility = Mobile::FacilitiesHelper.get_facilities([id]) + id = lh_location[:identifier].first[:value].split('_').second + v1_facilities_flag = Flipper.enabled?(:mobile_v1_lighthouse_facilities, @current_user) + facility = Mobile::FacilitiesHelper.get_facilities([id], v1_facilities_flag) raise Common::Exceptions::BackendServiceException, 'LIGHTHOUSE_FACILITIES404' if facility.first.nil? parsed_result = locations_adapter.parse(facility.first, params[:id]) diff --git a/modules/mobile/app/controllers/mobile/v0/pensions_controller.rb b/modules/mobile/app/controllers/mobile/v0/pensions_controller.rb new file mode 100644 index 00000000000..85c87e089aa --- /dev/null +++ b/modules/mobile/app/controllers/mobile/v0/pensions_controller.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require 'bid/awards/service' + +module Mobile + module V0 + class PensionsController < ApplicationController + def index + pension_data = pension_award_service.get_awards_pension + extracted_data = pension_data.try(:body)&.dig('awards_pension')&.transform_keys(&:to_sym) + raise Common::Exceptions::BackendServiceException, 'MOBL_502_upstream_error' unless extracted_data + + pensions = Mobile::V0::Pension.new(extracted_data) + render json: PensionSerializer.new(pensions) + end + + private + + def pension_award_service + @pension_award_service ||= BID::Awards::Service.new(current_user) + end + end + end +end diff --git a/modules/mobile/app/controllers/mobile/v1/messages_controller.rb b/modules/mobile/app/controllers/mobile/v1/messages_controller.rb index 19260fbc43d..61ce4d50dc7 100644 --- a/modules/mobile/app/controllers/mobile/v1/messages_controller.rb +++ b/modules/mobile/app/controllers/mobile/v1/messages_controller.rb @@ -8,7 +8,9 @@ def thread resource = client.get_messages_for_thread(message_id) raise Common::Exceptions::RecordNotFound, message_id if resource.blank? - resource.data = resource.data.filter { |m| m.message_id.to_s != params[:id] } if params[:excludeProvidedMessage] + if ActiveModel::Type::Boolean.new.cast(params[:excludeProvidedMessage]) + resource.data = resource.data.filter { |m| m.message_id.to_s != params[:id] } + end resource.metadata.merge!(message_counts(resource)) render json: resource.data, diff --git a/modules/mobile/app/helpers/mobile/facilities_helper.rb b/modules/mobile/app/helpers/mobile/facilities_helper.rb index ab53e75133d..8fbffb1bf62 100644 --- a/modules/mobile/app/helpers/mobile/facilities_helper.rb +++ b/modules/mobile/app/helpers/mobile/facilities_helper.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require 'lighthouse/facilities/v1/client' require 'lighthouse/facilities/client' module Mobile @@ -16,17 +17,21 @@ def fetch_facilities_from_ids(user, facility_ids, include_children:, schedulable vaos_facilities[:data] end - def get_facilities(facility_ids) - facilities_service.get_facilities(ids: facility_ids.to_a.map { |id| "vha_#{id}" }.join(',')) + def get_facilities(facility_ids, v1_facilties_flag) + facilities_service(v1_facilties_flag).get_facilities(ids: facility_ids.to_a.map { |id| "vha_#{id}" }.join(',')) end - def get_facility_names(facility_ids) - facilities = get_facilities(facility_ids) + def get_facility_names(facility_ids, v1_facilties_flag) + facilities = get_facilities(facility_ids, v1_facilties_flag) facilities.map(&:name) end - def facilities_service - Lighthouse::Facilities::Client.new + def facilities_service(v1_facilties_flag) + if v1_facilties_flag + Lighthouse::Facilities::V1::Client.new + else + Lighthouse::Facilities::Client.new + end end def address_from_facility(facility) @@ -36,7 +41,8 @@ def address_from_facility(facility) zip_code = address[:postal_code] else address = facility.address['physical'] - street = address.slice('address_1', 'address_2', 'address_3').values.compact.join(', ') + street = address.slice('address1', 'address2', 'address3', 'address_1', 'address_2', + 'address_3').values.compact.join(', ') zip_code = address['zip'] end Mobile::V0::Address.new( @@ -74,16 +80,7 @@ def phone_from_facility(facility) # captures area code (\d{3}) number (\d{3}-\d{4}) # and optional extension (until the end of the string) (?:\sx(\d*))?$ - phone_captures = phone.match(/^(\d{3})-(\d{3}-\d{4})(?:\sx(\d*))?$/) - - if phone_captures.nil? - Rails.logger.warn( - 'mobile appointments failed to parse facility phone number', - facility_id: facility.id, - facility_phone: facility.phone - ) - return nil - end + phone_captures = phone.match(/^(\d{3})-?(\d{3}-?\d{4})(?:\sx(\d*))?$/) Mobile::V0::AppointmentPhone.new( area_code: phone_captures[1].presence, @@ -104,7 +101,6 @@ def phone_from_facility(facility) # Returns the distance between these two # points in either miles or kilometers def haversine_distance(geo_a, geo_b, miles: true) - Rails.logger.info('haversine_distance coords', geo_a, geo_b) # Get latitude and longitude lat1, lon1 = geo_a lat2, lon2 = geo_b diff --git a/modules/mobile/app/models/mobile/v0/adapters/military_information.rb b/modules/mobile/app/models/mobile/v0/adapters/military_information.rb index 296d1fab65b..b53c23e10ee 100644 --- a/modules/mobile/app/models/mobile/v0/adapters/military_information.rb +++ b/modules/mobile/app/models/mobile/v0/adapters/military_information.rb @@ -92,9 +92,6 @@ def format_service_period(service_period) def discharge_code_section(service_period) discharge_code = service_period[:character_of_discharge_code] - if discharge_code.present? && DISCHARGE_CODE_MAP.keys.exclude?(discharge_code) - Rails.logger.error('Invalid discharge code', code: discharge_code) - end DISCHARGE_CODE_MAP[discharge_code] end end diff --git a/modules/mobile/app/models/mobile/v0/adapters/vaos_v2_appointment.rb b/modules/mobile/app/models/mobile/v0/adapters/vaos_v2_appointment.rb index ae79c722e3a..bbaa6d8fe53 100644 --- a/modules/mobile/app/models/mobile/v0/adapters/vaos_v2_appointment.rb +++ b/modules/mobile/app/models/mobile/v0/adapters/vaos_v2_appointment.rb @@ -122,6 +122,7 @@ def build_appointment_model Mobile::V0::Appointment.new(adapted_appointment) end + # rubocop:enable Metrics/MethodLength private @@ -203,15 +204,8 @@ def type_of_care(service_type) def cancellation_reason(cancellation_reason) if cancellation_reason.nil? - if status == STATUSES[:cancelled] - Rails.logger.info('cancelled appt missing cancellation reason with debug info', - type: appointment_type, - kind: appointment[:kind], - vista_status: appointment.dig(:extension, :vista_status), - facility_id:, - clinic: appointment[:clinic]) - return CANCELLATION_REASON[:prov] - end + return CANCELLATION_REASON[:prov] if status == STATUSES[:cancelled] + return nil end @@ -264,7 +258,9 @@ def start_date_utc @start_date_utc ||= begin start = appointment[:start] if start.nil? - sorted_dates = requested_periods.map { |period| time_to_datetime(period[:start]) }.sort + sorted_dates = requested_periods.map do |period| + time_to_datetime(period[:start]) + end.sort future_dates = sorted_dates.select { |period| period > DateTime.now } future_dates.any? ? future_dates.first : sorted_dates.first else @@ -394,6 +390,7 @@ def location location end end + # rubocop:enable Metrics/MethodLength def parse_phone(phone) diff --git a/modules/mobile/app/models/mobile/v0/award.rb b/modules/mobile/app/models/mobile/v0/award.rb index 05bff9029b5..8bfb9ed31a7 100644 --- a/modules/mobile/app/models/mobile/v0/award.rb +++ b/modules/mobile/app/models/mobile/v0/award.rb @@ -41,10 +41,6 @@ class Award < Common::Resource attribute :ptcpnt_vet_id, Types::String attribute :reason_one_txt, Types::String attribute :spouse_txt, Types::String - attribute :veteran_id, Types::String - attribute :is_eligible_for_pension, Types::Bool - attribute :is_in_receipt_of_pension, Types::Bool - attribute :net_worth_limit, Types::Decimal end end end diff --git a/modules/mobile/app/models/mobile/v0/pension.rb b/modules/mobile/app/models/mobile/v0/pension.rb new file mode 100644 index 00000000000..a9448e90923 --- /dev/null +++ b/modules/mobile/app/models/mobile/v0/pension.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require 'common/models/resource' + +module Mobile + module V0 + class Pension < Common::Resource + attribute :veteran_id, Types::String + attribute :is_eligible_for_pension, Types::Bool + attribute :is_in_receipt_of_pension, Types::Bool + attribute :net_worth_limit, Types::Decimal + end + end +end diff --git a/modules/mobile/app/serializers/mobile/v0/award_serializer.rb b/modules/mobile/app/serializers/mobile/v0/award_serializer.rb index 7c1a7f39f24..d1e3ab73124 100644 --- a/modules/mobile/app/serializers/mobile/v0/award_serializer.rb +++ b/modules/mobile/app/serializers/mobile/v0/award_serializer.rb @@ -41,11 +41,7 @@ class AwardSerializer :ptcpnt_bene_id, :ptcpnt_vet_id, :reason_one_txt, - :spouse_txt, - :veteran_id, - :is_eligible_for_pension, - :is_in_receipt_of_pension, - :net_worth_limit + :spouse_txt end end end diff --git a/modules/mobile/app/serializers/mobile/v0/pension_serializer.rb b/modules/mobile/app/serializers/mobile/v0/pension_serializer.rb new file mode 100644 index 00000000000..17d3c759f52 --- /dev/null +++ b/modules/mobile/app/serializers/mobile/v0/pension_serializer.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Mobile + module V0 + class PensionSerializer + include JSONAPI::Serializer + + set_id :veteran_id + set_type :pensions + attributes :is_eligible_for_pension, + :is_in_receipt_of_pension, + :net_worth_limit + end + end +end diff --git a/modules/mobile/app/serializers/mobile/v0/user_serializer.rb b/modules/mobile/app/serializers/mobile/v0/user_serializer.rb index bf4cec21dfa..fb4377012f6 100644 --- a/modules/mobile/app/serializers/mobile/v0/user_serializer.rb +++ b/modules/mobile/app/serializers/mobile/v0/user_serializer.rb @@ -106,7 +106,8 @@ def fetch_additional_resources # fetches MPI, either from external source or cache, then makes an external call for facility data def fetch_locations lambda { - Mobile::FacilitiesHelper.get_facility_names(user.va_treatment_facility_ids) + v1_facilities_flag = Flipper.enabled?(:mobile_v1_lighthouse_facilities, user) + Mobile::FacilitiesHelper.get_facility_names(user.va_treatment_facility_ids, v1_facilities_flag) } end diff --git a/modules/mobile/app/services/mobile/v0/legacy_disability_rating/proxy.rb b/modules/mobile/app/services/mobile/v0/legacy_disability_rating/proxy.rb index 06a11ee8070..d0fddb6db36 100644 --- a/modules/mobile/app/services/mobile/v0/legacy_disability_rating/proxy.rb +++ b/modules/mobile/app/services/mobile/v0/legacy_disability_rating/proxy.rb @@ -29,7 +29,6 @@ def get_disability_ratings # rubocop:disable Metrics/MethodLength raise Common::Exceptions::BackendServiceException, 'MOBL_502_upstream_error' rescue => e if e.respond_to?('response') - Rails.logger.info('LEGACY DR ERRORS WITH RESPONSE', error: e) case e.response[:status] when 400 raise Common::Exceptions::BackendServiceException, 'MOBL_404_rating_not_found' @@ -41,7 +40,6 @@ def get_disability_ratings # rubocop:disable Metrics/MethodLength raise e end else - Rails.logger.info('LEGACY DR ERRORS WITHOUT RESPONSE', error: e) raise e end end diff --git a/modules/mobile/app/services/mobile/v0/profile/sync_update_service.rb b/modules/mobile/app/services/mobile/v0/profile/sync_update_service.rb index 535cb046dff..b75e8e6af9d 100644 --- a/modules/mobile/app/services/mobile/v0/profile/sync_update_service.rb +++ b/modules/mobile/app/services/mobile/v0/profile/sync_update_service.rb @@ -88,7 +88,6 @@ def poll_with_backoff # next_try_seconds = Float(2**try) / 10 elapsed = seconds_elapsed_since(start) - log_incomplete(elapsed, next_try_seconds, try) # raise gateway timeout if we're at try number 10 or if the next retry would fall outside the timeout window raise_timeout_error(elapsed, try) if tries_or_time_exhausted?(next_try_seconds, elapsed, try) @@ -119,11 +118,6 @@ def check_transaction_status!(transaction_id) raise Common::Exceptions::RecordNotFound, transaction unless transaction raise IncompleteTransaction unless transaction.finished? - Rails.logger.info( - 'mobile syncronous profile update complete', - transaction_id: @transaction_id - ) - transaction end @@ -131,20 +125,9 @@ def contact_information_service VAProfile::ContactInformation::Service.new @user end - def raise_timeout_error(elapsed, try) - Rails.logger.error( - 'mobile syncronous profile update timeout', - transaction_id: @transaction_id, try:, elapsed: - ) + def raise_timeout_error(_elapsed, _try) raise Common::Exceptions::GatewayTimeout end - - def log_incomplete(elapsed, next_try_seconds, try) - Rails.logger.info( - 'mobile syncronous profile update not yet complete', - transaction_id: @transaction_id, try:, seconds_until_retry: next_try_seconds, elapsed: - ) - end end end end diff --git a/modules/mobile/config/routes.rb b/modules/mobile/config/routes.rb index f5bc4504761..f87dcb7ad89 100644 --- a/modules/mobile/config/routes.rb +++ b/modules/mobile/config/routes.rb @@ -12,6 +12,7 @@ get '/appointments/facility/eligibility', to: 'facility_eligibility#index' get '/appointments/facilities/:facility_id/clinics', to: 'clinics#index' get '/appointments/facilities/:facility_id/clinics/:clinic_id/slots', to: 'clinics#slots' + get '/appointments/facilities/:facility_id/slots', to: 'clinics#facility_slots' get '/appointments/preferences', to: 'appointment_preferences#show' put '/appointments/preferences', to: 'appointment_preferences#update' post '/appointments/check-in', to: 'check_in#create' @@ -19,6 +20,7 @@ post '/appointment', to: 'appointments#create' get '/appointments/check-in/demographics', to: 'check_in_demographics#show' get '/awards', to: 'awards#index' + get '/pensions', to: 'pensions#index' get '/claims-and-appeals-overview', to: 'claims_and_appeals#index' get '/claims/decision-letters', to: 'decision_letters#index' get '/claims/decision-letters/:document_id/download', to: 'decision_letters#download' diff --git a/modules/mobile/docs/index.html b/modules/mobile/docs/index.html index 0026b6a7e0a..e32bfc7b86f 100755 --- a/modules/mobile/docs/index.html +++ b/modules/mobile/docs/index.html @@ -2218,7 +2218,7 @@ -
502

An upstream service the API depends on returned an error.

Response samples

Content type
application/json
{
  • "data": {
    }
}

/v0/claim/{id}

Returns info on all user's claims and appeals for mobile overview page

+

Response samples

Content type
application/json
{
  • "data": {
    }
}

/v0/claim/{id}

Returns info on all user's claims and appeals for mobile overview page

Authorizations:
Bearer
path Parameters
id
required
string

Claim Id

header Parameters
X-Key-Inflection
string
Default: snake
Enum: "camel" "snake"

Allows the API to return camelCase keys rather than snake_case

Responses

Request samples

Content type
application/json
{
  • "accountNumber": "12345678901",
  • "accountType": "Savings",
  • "financialInstitutionName": "PACIFIC PREMIER BANK",
  • "financialInstitutionRoutingNumber": "021000021"
}

Response samples

Content type
application/json
{
  • "type": "paymentInformation",
  • "id": "abe3f152-90b0-45cb-8776-4958bad0e0ef",
  • "attributes": {
    }
}

/v0/push/prefs/{endpointSid}

Get the user's push notification preferences

+

Request samples

Content type
application/json
{
  • "accountNumber": "12345678901",
  • "accountType": "Savings",
  • "financialInstitutionName": "PACIFIC PREMIER BANK",
  • "financialInstitutionRoutingNumber": "021000021"
}

Response samples

Content type
application/json
{
  • "type": "paymentInformation",
  • "id": "abe3f152-90b0-45cb-8776-4958bad0e0ef",
  • "attributes": {
    }
}

/v0/pensions

Get current pensions overview

+
Authorizations:
Bearer

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

/v0/push/prefs/{endpointSid}

Get the user's push notification preferences

Authorizations:
Bearer
path Parameters
endpointSid
required
string

device endpointSid provided by the register endpoint

header Parameters
X-Key-Inflection
string
Default: snake
Enum: "camel" "snake"

Allows the API to return camelCase keys rather than snake_case

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

/v0/push/prefs/{endpointSid}

Set the user's push notification preferences

+

Response samples

Content type
application/json
{
  • "data": {
    }
}

/v0/push/prefs/{endpointSid}

Set the user's push notification preferences

Authorizations:
Bearer
path Parameters
endpointSid
required
string

device endpointSid provided by the register endpoint

header Parameters
X-Key-Inflection
string
Default: snake
Enum: "camel" "snake"

Allows the API to return camelCase keys rather than snake_case

Request Body schema: application/json

Push notification preferences

@@ -3267,7 +3278,7 @@

Request samples

Content type
application/json
{
  • "preference": "appointment_reminders",
  • "enabled": true
}

Response samples

Content type
application/json
{
  • "errors": [
    ]
}

/v0/push/register

Allows a new app install to register to receive push notifications

+

Request samples

Content type
application/json
{
  • "preference": "appointment_reminders",
  • "enabled": true
}

Response samples

Content type
application/json
{
  • "errors": [
    ]
}

/v0/push/register

Allows a new app install to register to receive push notifications

Authorizations:
Bearer
header Parameters
X-Key-Inflection
string
Default: snake
Enum: "camel" "snake"

Allows the API to return camelCase keys rather than snake_case

Request Body schema: application/json

Device information

deviceToken
required
string
osName
required
string
Enum: "ios" "android"
deviceName
string
appName
required
string
debug
boolean

Flag to switch between sandbox and non-sandbox app sid. Lower envs only

@@ -3281,7 +3292,7 @@

Request samples

Content type
application/json
{
  • "deviceToken": "740f4707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bb78ad",
  • "osName": "ios",
  • "deviceName": "Galaxy 8",
  • "appName": "va_mobile_app",
  • "debug": true
}

Response samples

Content type
application/json
{
  • "data": {
    }
}

/v0/push/send

Allows client to trigger specified push notification to be sent to specified endpoint

+

Request samples

Content type
application/json
{
  • "deviceToken": "740f4707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bb78ad",
  • "osName": "ios",
  • "deviceName": "Galaxy 8",
  • "appName": "va_mobile_app",
  • "debug": true
}

Response samples

Content type
application/json
{
  • "data": {
    }
}

/v0/push/send

Allows client to trigger specified push notification to be sent to specified endpoint

Authorizations:
Bearer
header Parameters
X-Key-Inflection
string
Default: snake
Enum: "camel" "snake"

Allows the API to return camelCase keys rather than snake_case

Request Body schema: application/json

Template id, endpoint sid, and personalization for template

appName
required
string
templateId
required
string
required
object
debug
boolean

Flag to switch between sandbox and non-sandbox app sid. Lower envs only

@@ -3295,7 +3306,7 @@

Request samples

Content type
application/json
{
  • "appName": "va_mobile_app",
  • "templateId": "0EF7C8C9390847D7B3B521426EFF5814",
  • "personalization": {
    },
  • "debug": true
}

Response samples

Content type
application/json
{
  • "errors": [
    ]
}

/v0/user

Returns the user profile, including the user's addresses and the services the user has the requisite ids to access. Meta data for this endpoint returns all the services available in the API.

+

Request samples

Content type
application/json
{
  • "appName": "va_mobile_app",
  • "templateId": "0EF7C8C9390847D7B3B521426EFF5814",
  • "personalization": {
    },
  • "debug": true
}

Response samples

Content type
application/json
{
  • "errors": [
    ]
}

/v0/user

Returns the user profile, including the user's addresses and the services the user has the requisite ids to access. Meta data for this endpoint returns all the services available in the API.

Authorizations:
Bearer
header Parameters
X-Key-Inflection
string
Default: snake
Enum: "camel" "snake"

Allows the API to return camelCase keys rather than snake_case

Responses

Response samples

Content type
application/json
{
  • "data": {
    },
  • "meta": {
    }
}

/v0/user/authorized-services

Returns a hash of all available services, and a boolean value of whether the user has access to that service.

+

Response samples

Content type
application/json
{
  • "data": {
    },
  • "meta": {
    }
}

/v0/user/authorized-services

Returns a hash of all available services, and a boolean value of whether the user has access to that service.

Authorizations:
Bearer
header Parameters
X-Key-Inflection
string
Default: snake
Enum: "camel" "snake"

Allows the API to return camelCase keys rather than snake_case

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

/v0/user/contact-info

Returns the user contact info. If the user does not have a vet360 id, the contact info will be null.

+

Response samples

Content type
application/json
{
  • "data": {
    }
}

/v0/user/contact-info

Returns the user contact info. If the user does not have a vet360 id, the contact info will be null.

Authorizations:
Bearer
header Parameters
X-Key-Inflection
string
Default: snake
Enum: "camel" "snake"

Allows the API to return camelCase keys rather than snake_case

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

/v1/user

Returns the user profile, including the user's addresses and the services the user has the requisite ids to access. Meta data for this endpoint returns all the services available in the API. v1 includes LOGINGOV as login type.

+

Response samples

Content type
application/json
{
  • "data": {
    }
}

/v1/user

Returns the user profile, including the user's addresses and the services the user has the requisite ids to access. Meta data for this endpoint returns all the services available in the API. v1 includes LOGINGOV as login type.

Authorizations:
Bearer
header Parameters
X-Key-Inflection
string
Default: snake
Enum: "camel" "snake"

Allows the API to return camelCase keys rather than snake_case

Responses

Response samples

Content type
application/json
{
  • "data": {
    },
  • "meta": {
    }
}

/v2/user

Returns basic user information

+

Response samples

Content type
application/json
{
  • "data": {
    },
  • "meta": {
    }
}

/v2/user

Returns basic user information

Authorizations:
Bearer
header Parameters
X-Key-Inflection
string
Default: snake
Enum: "camel" "snake"

Allows the API to return camelCase keys rather than snake_case

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

/v0/user/addresses

Deletes a user's address

+

Response samples

Content type
application/json
{
  • "data": {
    }
}

/v0/user/addresses

Deletes a user's address

Authorizations:
Bearer
header Parameters
X-Key-Inflection
string
Default: snake
Enum: "camel" "snake"

Allows the API to return camelCase keys rather than snake_case

Request Body schema: application/json

A domestic, internation, or military address

id
required
integer
addressLine1
required
string
addressLine2
required
string, null
addressLine3
required
string, null
addressPou
required
string
Enum: "RESIDENCE/CHOICE" "CORRESPONDENCE"
addressType
required
string
Enum: "DOMESTIC" "INTERNATIONAL" "MILITARY"
city
required
string
countryCode
required
string
internationalPostalCode
required
string, null
province
required
string, null
stateCode
required
string
zipCode
required
string
zipCodeSuffix
required
string, null

Responses

Request samples

Content type
application/json
{
  • "id": 157032,
  • "addressLine1": "1493 Martin Luther King Rd",
  • "addressLine2": null,
  • "addressLine3": null,
  • "addressPou": "RESIDENCE/CHOICE",
  • "addressType": "DOMESTIC",
  • "city": "Fulton",
  • "countryCode": "US",
  • "internationalPostalCode": null,
  • "province": null,
  • "stateCode": "NY",
  • "zipCode": "97062",
  • "zipCodeSuffix": "1234"
}

Response samples

Content type
application/json
{
  • "data": {
    }
}

/v0/user/addresses

Creates a new residential or mailing address for a user. Calling this endpoint is the second step in adding a new address for a user. The first step is to call the address validation endpoint to check if an address is valid. If it is valid you'll receive a 'addressMetaData' object back with the addresses confidence score. This object should then be included along with the new address in the request body. If the user wishes to continue with an 'invalid' address then the 'validationKey' should be passed along with the new address in the request body. This lets the underlying service know that an invalid address be passed through.

+

Request samples

Content type
application/json
{
  • "id": 157032,
  • "addressLine1": "1493 Martin Luther King Rd",
  • "addressLine2": null,
  • "addressLine3": null,
  • "addressPou": "RESIDENCE/CHOICE",
  • "addressType": "DOMESTIC",
  • "city": "Fulton",
  • "countryCode": "US",
  • "internationalPostalCode": null,
  • "province": null,
  • "stateCode": "NY",
  • "zipCode": "97062",
  • "zipCodeSuffix": "1234"
}

Response samples

Content type
application/json
{
  • "data": {
    }
}

/v0/user/addresses

Creates a new residential or mailing address for a user. Calling this endpoint is the second step in adding a new address for a user. The first step is to call the address validation endpoint to check if an address is valid. If it is valid you'll receive a 'addressMetaData' object back with the addresses confidence score. This object should then be included along with the new address in the request body. If the user wishes to continue with an 'invalid' address then the 'validationKey' should be passed along with the new address in the request body. This lets the underlying service know that an invalid address be passed through.

Authorizations:
Bearer
header Parameters
X-Key-Inflection
string
Default: snake
Enum: "camel" "snake"

Allows the API to return camelCase keys rather than snake_case

Request Body schema: application/json

A domestic, internation, or military address

required
object

Responses

Request samples

Content type
application/json
Example
{
  • "addressLine1": "1493 Martin Luther King Rd",
  • "addressPou": "RESIDENCE/CHOICE",
  • "addressType": "DOMESTIC",
  • "city": "Fulton",
  • "countryCodeIso3": "USA",
  • "countryName": "United States",
  • "stateCode": "MS",
  • "validationKey": -1206619807,
  • "zipCode": "38843"
}

Response samples

Content type
application/json
{
  • "data": {
    }
}

/v0/user/addresses

Updates a user's residential or mailing address. Calling this endpoint is the second step in adding a new address for a user. The first step is to call the address validation endpoint to check if an address is valid. If it is valid you'll receive a 'addressMetaData' object back with the addresses confidence score. This object should then be included along with the new address in the request body. If the user wishes to continue with an 'invalid' address then the 'validationKey' should be passed along with the new address in the request body. This lets the underlying service know that an invalid address be passed through.

+

Request samples

Content type
application/json
Example
{
  • "addressLine1": "1493 Martin Luther King Rd",
  • "addressPou": "RESIDENCE/CHOICE",
  • "addressType": "DOMESTIC",
  • "city": "Fulton",
  • "countryCodeIso3": "USA",
  • "countryName": "United States",
  • "stateCode": "MS",
  • "validationKey": -1206619807,
  • "zipCode": "38843"
}

Response samples

Content type
application/json
{
  • "data": {
    }
}

/v0/user/addresses

Updates a user's residential or mailing address. Calling this endpoint is the second step in adding a new address for a user. The first step is to call the address validation endpoint to check if an address is valid. If it is valid you'll receive a 'addressMetaData' object back with the addresses confidence score. This object should then be included along with the new address in the request body. If the user wishes to continue with an 'invalid' address then the 'validationKey' should be passed along with the new address in the request body. This lets the underlying service know that an invalid address be passed through.

Authorizations:
Bearer
header Parameters
X-Key-Inflection
string
Default: snake
Enum: "camel" "snake"

Allows the API to return camelCase keys rather than snake_case

Request Body schema: application/json

A domestic, internation, or military address

id
required
string
object
validationKey
integer
addressLine1
required
string
addressLine2
string, null
addressLine3
string, null
addressPou
required
string
Enum: "RESIDENCE/CHOICE" "CORRESPONDENCE"
addressType
required
string
Enum: "DOMESTIC" "INTERNATIONAL" "MILITARY"
city
required
string
countryCode
required
string
internationalPostalCode
string, null
province
string, null
stateCode
string
zipCode
string
zipCodeSuffix
string, null

Responses

Request samples

Content type
application/json
Example
{
  • "addressLine1": "1493 Martin Luther King Rd",
  • "addressPou": "RESIDENCE/CHOICE",
  • "addressType": "DOMESTIC",
  • "city": "Fulton",
  • "countryCodeIso3": "USA",
  • "countryName": "United States",
  • "id": 181513,
  • "stateCode": "MS",
  • "validationKey": -1206619807,
  • "zipCode": "38843"
}

Response samples

Content type
application/json
{
  • "data": {
    }
}

/v0/user/addresses/validate

Validates a residential or mailing address for a user. Calling this endpoint is the first step in adding a new address for a user. If the address is valid you'll receive a 'addressMetaData' object back with the addresses confidence score. This object should then be included along with the new address in the request body. If the user wishes to continue with an 'invalid' address then the 'validationKey' should be passed along with the new address in the request body. This lets the underlying service know that an invalid address be passed through.

+

Request samples

Content type
application/json
Example
{
  • "addressLine1": "1493 Martin Luther King Rd",
  • "addressPou": "RESIDENCE/CHOICE",
  • "addressType": "DOMESTIC",
  • "city": "Fulton",
  • "countryCodeIso3": "USA",
  • "countryName": "United States",
  • "id": 181513,
  • "stateCode": "MS",
  • "validationKey": -1206619807,
  • "zipCode": "38843"
}

Response samples

Content type
application/json
{
  • "data": {
    }
}

/v0/user/addresses/validate

Validates a residential or mailing address for a user. Calling this endpoint is the first step in adding a new address for a user. If the address is valid you'll receive a 'addressMetaData' object back with the addresses confidence score. This object should then be included along with the new address in the request body. If the user wishes to continue with an 'invalid' address then the 'validationKey' should be passed along with the new address in the request body. This lets the underlying service know that an invalid address be passed through.

Authorizations:
Bearer
header Parameters
X-Key-Inflection
string
Default: snake
Enum: "camel" "snake"

Allows the API to return camelCase keys rather than snake_case

Request Body schema: application/json

A domestic, internation, or military address

id
required
integer
addressLine1
required
string
addressLine2
required
string, null
addressLine3
required
string, null
addressPou
required
string
Enum: "RESIDENCE/CHOICE" "CORRESPONDENCE"
addressType
required
string
Enum: "DOMESTIC" "INTERNATIONAL" "MILITARY"
city
required
string
countryCode
required
string
internationalPostalCode
required
string, null
province
required
string, null
stateCode
required
string
zipCode
required
string
zipCodeSuffix
required
string, null

Responses

Request samples

Content type
application/json
{
  • "addressLine1": "51 W Weber Rd",
  • "addressPou": "CORRESPONDENCE",
  • "addressType": "DOMESTIC",
  • "city": "Columbus",
  • "countryCodeIso3": "USA",
  • "countryName": "United States",
  • "stateCode": "OH",
  • "type": "DOMESTIC",
  • "zipCode": "43202"
}

Response samples

Content type
application/json
Example
{
  • "data": [
    ],
  • "meta": {
    }
}

/v0/user/demographics

Returns the users demographics info

+

Request samples

Content type
application/json
{
  • "addressLine1": "51 W Weber Rd",
  • "addressPou": "CORRESPONDENCE",
  • "addressType": "DOMESTIC",
  • "city": "Columbus",
  • "countryCodeIso3": "USA",
  • "countryName": "United States",
  • "stateCode": "OH",
  • "type": "DOMESTIC",
  • "zipCode": "43202"
}

Response samples

Content type
application/json
Example
{
  • "data": [
    ],
  • "meta": {
    }
}

/v0/user/demographics

Returns the users demographics info

Authorizations:
Bearer
header Parameters
X-Key-Inflection
string
Default: snake
Enum: "camel" "snake"

Allows the API to return camelCase keys rather than snake_case

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

/v0/user/emails

Deletes a user's email address

+

Response samples

Content type
application/json
{
  • "data": {
    }
}

/v0/user/emails

Deletes a user's email address

Authorizations:
Bearer
header Parameters
X-Key-Inflection
string
Default: snake
Enum: "camel" "snake"

Allows the API to return camelCase keys rather than snake_case

Request Body schema: application/json

The email address to delete

id
required
integer
emailAddress
required
string

Responses

Request samples

Content type
application/json
{
  • "emailAddress": "person42@example.com",
  • "id": "42,"
}

Response samples

Content type
application/json
{
  • "data": {
    }
}

/v0/user/emails

Creates a new email address

+

Request samples

Content type
application/json
{
  • "emailAddress": "person42@example.com",
  • "id": "42,"
}

Response samples

Content type
application/json
{
  • "data": {
    }
}

/v0/user/emails

Creates a new email address

Authorizations:
Bearer
header Parameters
X-Key-Inflection
string
Default: snake
Enum: "camel" "snake"

Allows the API to return camelCase keys rather than snake_case

Request Body schema: application/json

The new email address

emailAddress
required
string

Responses

Request samples

Content type
application/json
{
  • "emailAddress": "person42@example.com"
}

Response samples

Content type
application/json
{
  • "data": {
    }
}

/v0/user/emails

Updates a user's email address

+

Request samples

Content type
application/json
{
  • "emailAddress": "person42@example.com"
}

Response samples

Content type
application/json
{
  • "data": {
    }
}

/v0/user/emails

Updates a user's email address

Authorizations:
Bearer
header Parameters
X-Key-Inflection
string
Default: snake
Enum: "camel" "snake"

Allows the API to return camelCase keys rather than snake_case

Request Body schema: application/json

The new email address

id
required
integer
emailAddress
required
string

Responses

Request samples

Content type
application/json
{
  • "emailAddress": "person42@example.com",
  • "id": "42,"
}

Response samples

Content type
application/json
{
  • "data": {
    }
}

/v0/user/gender_identity

Updates a user's gender identity. Only users with id.me or login.gov accounts may use this

+

Request samples

Content type
application/json
{
  • "emailAddress": "person42@example.com",
  • "id": "42,"
}

Response samples

Content type
application/json
{
  • "data": {
    }
}

/v0/user/gender_identity

Updates a user's gender identity. Only users with id.me or login.gov accounts may use this

Authorizations:
Bearer
header Parameters
X-Key-Inflection
string
Default: snake
Enum: "camel" "snake"

Allows the API to return camelCase keys rather than snake_case

Request Body schema: application/json

The new gender identity key

code
required
string

Responses

Request samples

Content type
application/json
{
  • "code": "B"
}

Response samples

Content type
application/json
{
  • "errors": [
    ]
}

/v0/user/gender_identity/edit

Retrieves a list of valid gender identity keys. Note that this endpoint does not use the camel case key inflection header like most other mobile endpoints to keep the keys upcase.

+

Request samples

Content type
application/json
{
  • "code": "B"
}

Response samples

Content type
application/json
{
  • "errors": [
    ]
}

/v0/user/gender_identity/edit

Retrieves a list of valid gender identity keys. Note that this endpoint does not use the camel case key inflection header like most other mobile endpoints to keep the keys upcase.

Authorizations:
Bearer

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

/v0/user/logout

Logs the user out by revoking their access token from the IAM SSOe OAuth service and destroying the IAM user, user identity, and session objects from Redis.

+

Response samples

Content type
application/json
{
  • "data": {
    }
}

/v0/user/logout

Logs the user out by revoking their access token from the IAM SSOe OAuth service and destroying the IAM user, user identity, and session objects from Redis.

Authorizations:
Bearer
header Parameters
X-Key-Inflection
string
Default: snake
Enum: "camel" "snake"

Allows the API to return camelCase keys rather than snake_case

Responses

Response samples

Content type
application/json
{
  • "errors": [
    ]
}

/v0/user/logged-in

Called by the mobile app after successful login to perform any actions needed to start a session.

+

Response samples

Content type
application/json
{
  • "errors": [
    ]
}

/v0/user/logged-in

Called by the mobile app after successful login to perform any actions needed to start a session.

Authorizations:
Bearer
header Parameters
X-Key-Inflection
string
Default: snake
Enum: "camel" "snake"

Allows the API to return camelCase keys rather than snake_case

Responses

Response samples

Content type
application/json
{
  • "errors": [
    ]
}

/v0/user/preferred_name

Updates a user's preferred name. Only users with id.me or login.gov accounts may use this

+

Response samples

Content type
application/json
{
  • "errors": [
    ]
}

/v0/user/preferred_name

Updates a user's preferred name. Only users with id.me or login.gov accounts may use this

Authorizations:
Bearer
header Parameters
X-Key-Inflection
string
Default: snake
Enum: "camel" "snake"

Allows the API to return camelCase keys rather than snake_case

Request Body schema: application/json

The new preferred name

text
required
string

Responses

Request samples

Content type
application/json
{
  • "text": "New Preferred Name"
}

Response samples

Content type
application/json
{
  • "errors": [
    ]
}

/v0/user/phones

Deletes one of a user's phone numbers

+

Request samples

Content type
application/json
{
  • "text": "New Preferred Name"
}

Response samples

Content type
application/json
{
  • "errors": [
    ]
}

/v0/user/phones

Deletes one of a user's phone numbers

Authorizations:
Bearer
header Parameters
X-Key-Inflection
string
Default: snake
Enum: "camel" "snake"

Allows the API to return camelCase keys rather than snake_case

Request Body schema: application/json

The phone number to delete

id
required
integer
areaCode
required
string
countryCode
required
string
phoneNumber
required
string
phoneType
required
string
Enum: "HOME" "FAX" "MOBILE" "WORK"
extension
required
string

Responses

Request samples

Content type
application/json
{
  • "id": 157032,
  • "areaCode": "704",
  • "countryCode": "1",
  • "phoneNumber": "7749069",
  • "phoneType": "HOME",
  • "extension": "4567"
}

Response samples

Content type
application/json
{
  • "data": {
    }
}

/v0/user/phones

Creates a phone number for a user

+

Request samples

Content type
application/json
{
  • "id": 157032,
  • "areaCode": "704",
  • "countryCode": "1",
  • "phoneNumber": "7749069",
  • "phoneType": "HOME",
  • "extension": "4567"
}

Response samples

Content type
application/json
{
  • "data": {
    }
}

/v0/user/phones

Creates a phone number for a user

Authorizations:
Bearer
header Parameters
X-Key-Inflection
string
Default: snake
Enum: "camel" "snake"

Allows the API to return camelCase keys rather than snake_case

Request Body schema: application/json

The new phone number

id
integer
areaCode
required
string
countryCode
required
string
phoneNumber
required
string
phoneType
required
string
Enum: "HOME" "FAX" "MOBILE" "WORK"
extension
required
string

Responses

Request samples

Content type
application/json
{
  • "id": 157032,
  • "areaCode": "704",
  • "countryCode": "1",
  • "phoneNumber": "7749069",
  • "phoneType": "HOME",
  • "extension": "4567"
}

Response samples

Content type
application/json
{
  • "data": {
    }
}

/v0/user/phones

Updates a user's phone number

+

Request samples

Content type
application/json
{
  • "id": 157032,
  • "areaCode": "704",
  • "countryCode": "1",
  • "phoneNumber": "7749069",
  • "phoneType": "HOME",
  • "extension": "4567"
}

Response samples

Content type
application/json
{
  • "data": {
    }
}

/v0/user/phones

Updates a user's phone number

Authorizations:
Bearer
header Parameters
X-Key-Inflection
string
Default: snake
Enum: "camel" "snake"

Allows the API to return camelCase keys rather than snake_case

Request Body schema: application/json

The new phone number

id
required
integer
areaCode
required
string
countryCode
required
string
phoneNumber
required
string
phoneType
required
string
Enum: "HOME" "FAX" "MOBILE" "WORK"
extension
required
string

Responses

Request samples

Content type
application/json
{
  • "id": 157032,
  • "areaCode": "704",
  • "countryCode": "1",
  • "phoneNumber": "7749069",
  • "phoneType": "HOME",
  • "extension": "4567"
}

Response samples

Content type
application/json
{
  • "data": {
    }
}

/v1/health/immunizations

Returns a paginated list of immunization records for given user

+

Request samples

Content type
application/json
{
  • "id": 157032,
  • "areaCode": "704",
  • "countryCode": "1",
  • "phoneNumber": "7749069",
  • "phoneType": "HOME",
  • "extension": "4567"
}

Response samples

Content type
application/json
{
  • "data": {
    }
}

/v1/health/immunizations

Returns a paginated list of immunization records for given user

Authorizations:
Bearer
header Parameters
X-Key-Inflection
string
Default: snake
Enum: "camel" "snake"

Allows the API to return camelCase keys rather than snake_case

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "meta": {
    }
}
+

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "meta": {
    }
}