From 19ccb2fc82dc9b2e4d71c25b0ad8fba6e0466f91 Mon Sep 17 00:00:00 2001 From: Trevor Bosaw Date: Thu, 3 Oct 2024 15:42:38 -0700 Subject: [PATCH 01/30] Adding nbf field to SiS STS access token which describes the start time the token should be considered valid (#18740) --- .../sign_in/service_account_access_token_jwt_encoder.rb | 1 + .../sign_in/service_account_access_token_jwt_encoder_spec.rb | 2 ++ 2 files changed, 3 insertions(+) diff --git a/app/services/sign_in/service_account_access_token_jwt_encoder.rb b/app/services/sign_in/service_account_access_token_jwt_encoder.rb index a360ce0beba..9164e1e2b9b 100644 --- a/app/services/sign_in/service_account_access_token_jwt_encoder.rb +++ b/app/services/sign_in/service_account_access_token_jwt_encoder.rb @@ -26,6 +26,7 @@ def payload sub: service_account_access_token.user_identifier, exp: service_account_access_token.expiration_time.to_i, iat: service_account_access_token.created_time.to_i, + nbf: service_account_access_token.created_time.to_i, version: service_account_access_token.version, scopes: service_account_access_token.scopes, service_account_id: service_account_access_token.service_account_id, diff --git a/spec/services/sign_in/service_account_access_token_jwt_encoder_spec.rb b/spec/services/sign_in/service_account_access_token_jwt_encoder_spec.rb index 3f7687cec5d..d6c119770b2 100644 --- a/spec/services/sign_in/service_account_access_token_jwt_encoder_spec.rb +++ b/spec/services/sign_in/service_account_access_token_jwt_encoder_spec.rb @@ -15,6 +15,7 @@ let(:expected_sub) { service_account_access_token.user_identifier } let(:expected_exp) { service_account_access_token.expiration_time.to_i } let(:expected_iat) { service_account_access_token.created_time.to_i } + let(:expected_nbf) { service_account_access_token.created_time.to_i } let(:expected_version) { service_account_access_token.version } let(:expected_scopes) { service_account_access_token.scopes } let(:expected_service_account_id) { service_account_access_token.service_account_id } @@ -32,6 +33,7 @@ expect(decoded_jwt.sub).to eq expected_sub expect(decoded_jwt.exp).to eq expected_exp expect(decoded_jwt.iat).to eq expected_iat + expect(decoded_jwt.nbf).to eq expected_nbf expect(decoded_jwt.version).to eq expected_version expect(decoded_jwt.scopes).to eq expected_scopes expect(decoded_jwt.service_account_id).to eq expected_service_account_id From c1fae2e57d52a6f528469b9919a10dbf178b67b2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 4 Oct 2024 09:27:26 +0000 Subject: [PATCH 02/30] Bump aws-sdk-sns from 1.85.0 to 1.88.0 Bumps [aws-sdk-sns](https://github.com/aws/aws-sdk-ruby) from 1.85.0 to 1.88.0. - [Release notes](https://github.com/aws/aws-sdk-ruby/releases) - [Changelog](https://github.com/aws/aws-sdk-ruby/blob/version-3/gems/aws-sdk-sns/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-ruby/commits) --- updated-dependencies: - dependency-name: aws-sdk-sns dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index a6ff131f430..cdb408acb60 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -243,7 +243,7 @@ GEM attr_extras (7.1.0) awesome_print (1.9.2) aws-eventstream (1.3.0) - aws-partitions (1.983.0) + aws-partitions (1.984.0) aws-sdk-core (3.209.1) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.651.0) @@ -256,8 +256,8 @@ GEM aws-sdk-core (~> 3, >= 3.207.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) - aws-sdk-sns (1.85.0) - aws-sdk-core (~> 3, >= 3.205.0) + aws-sdk-sns (1.88.0) + aws-sdk-core (~> 3, >= 3.207.0) aws-sigv4 (~> 1.5) aws-sigv4 (1.10.0) aws-eventstream (~> 1, >= 1.0.2) From d61bf2f263baa3d059ac076509831e19a5c315a4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 4 Oct 2024 09:26:25 +0000 Subject: [PATCH 03/30] Bump super_diff from 0.12.1 to 0.13.0 Bumps [super_diff](https://github.com/splitwise/super_diff) from 0.12.1 to 0.13.0. - [Release notes](https://github.com/splitwise/super_diff/releases) - [Changelog](https://github.com/splitwise/super_diff/blob/main/CHANGELOG.md) - [Commits](https://github.com/splitwise/super_diff/compare/v0.12.1...v0.13.0) --- updated-dependencies: - dependency-name: super_diff dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index cdb408acb60..236ed4414ed 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1022,7 +1022,7 @@ GEM stringio (3.1.1) strong_migrations (2.0.0) activerecord (>= 6.1) - super_diff (0.12.1) + super_diff (0.13.0) attr_extras (>= 6.2.4) diff-lcs patience_diff From a01d921e0ae42087dd9070bc7d59e761e73383e2 Mon Sep 17 00:00:00 2001 From: Eric Tillberg Date: Fri, 4 Oct 2024 10:38:08 -0400 Subject: [PATCH 04/30] Simple Forms NotificationEmail bugfix on getting first_name (#18734) * Simple Forms NotificationEmail bugfix on getting first_name * changes per feedback * tests * catch if first_name is missing --- .../simple_forms_api/notification_email.rb | 26 ++++++++++++++----- .../spec/services/notification_email_spec.rb | 2 +- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/modules/simple_forms_api/app/services/simple_forms_api/notification_email.rb b/modules/simple_forms_api/app/services/simple_forms_api/notification_email.rb index ef9f90f4ff5..2f140cd1d16 100644 --- a/modules/simple_forms_api/app/services/simple_forms_api/notification_email.rb +++ b/modules/simple_forms_api/app/services/simple_forms_api/notification_email.rb @@ -98,9 +98,7 @@ def check_missing_keys(config) def enqueue_email(at, template_id, data) # async job and we have a UserAccount if user_account - mpi_profile = MPI::Service.new.find_profile_by_identifier(identifier_type: 'ICN', identifier: user_account.icn) - first_name = mpi_profile.first_name - data[:personalization]['first_name'] = first_name + data[:personalization]['first_name'] = get_first_name VANotify::UserAccountJob.perform_at( at, user_account.id, @@ -140,6 +138,22 @@ def send_email_now(template_id, data) end end + def get_first_name + if user_account + mpi_profile = MPI::Service.new.find_profile_by_identifier(identifier_type: 'ICN', identifier: user_account.icn) + if mpi_profile + raise mpi_profile.error if mpi_profile.error + raise 'First name not found in MPI profile' unless mpi_profile.first_name + + mpi_profile.first_name + end + elsif user + raise 'First name not found in user profile' unless user.first_name + + user.first_name + end + end + # rubocop:disable Metrics/MethodLength # email and personalization hash def form_specific_data @@ -162,7 +176,7 @@ def form_specific_data { email: @user&.va_profile_email, - personalization: default_personalization(@user.first_name) + personalization: default_personalization(get_first_name) .merge(form21_0966_personalization) } when 'vba_21_0972' @@ -226,7 +240,7 @@ def default_personalization(first_name) def form20_10206_contact_info # email address not required and omitted if @form_data['email_address'].blank? && @user - [@user&.va_profile_email, @form_data.dig('full_name', 'first')] + [@user.va_profile_email, @form_data.dig('full_name', 'first')] # email address not required and optionally entered else @@ -261,7 +275,7 @@ def form20_10207_contact_info def form21_0845_contact_info # (vet && signed in) if @form_data['authorizer_type'] == 'veteran' && @user - [@user&.va_profile_email, @form_data.dig('veteran_full_name', 'first')] + [@user.va_profile_email, @form_data.dig('veteran_full_name', 'first')] # (non-vet && signed in) || (non-vet && anon) elsif @form_data['authorizer_type'] == 'nonVeteran' diff --git a/modules/simple_forms_api/spec/services/notification_email_spec.rb b/modules/simple_forms_api/spec/services/notification_email_spec.rb index d392c6986a4..2658e2a2a6c 100644 --- a/modules/simple_forms_api/spec/services/notification_email_spec.rb +++ b/modules/simple_forms_api/spec/services/notification_email_spec.rb @@ -112,7 +112,7 @@ it 'sends the email at the specified time' do time = double - mpi_profile = double(first_name: double) + mpi_profile = double(first_name: double, error: nil) allow(VANotify::UserAccountJob).to receive(:perform_at) allow_any_instance_of(MPI::Service).to receive(:find_profile_by_identifier).and_return(mpi_profile) subject = described_class.new(config, notification_type:, user_account:) From 007fa4a3d96385b3765d03d09a4dcc8e36e1c0e6 Mon Sep 17 00:00:00 2001 From: Jeff Marks <106996298+jefftmarks@users.noreply.github.com> Date: Fri, 4 Oct 2024 09:30:40 -0600 Subject: [PATCH 05/30] EDM-190/enable unlimited sob access behind feature toggle (#18657) * Put 24/7 access to post-911 SOB behind flipper toggle * Fix linting * Revert hard-coded icn * Update tests and leave comments of TO-DOs to remove logic when 24/7 released * Linting * FIx failing test in swagger spec * Fix failing tests and move location of request spec for post 911 GI bill * Resolve merge conflict with backend status controller and related models * Update codeowners for post911 gi bill (SOB) * Fix code owners to govcio * Fix failing test * Update codeowners to govcio vfep * Fix codeowners * Revert codeowners * Change v0 to v1 --- app/controllers/v0/backend_statuses_controller.rb | 7 +++---- .../v1/post911_gi_bill_statuses_controller.rb | 5 ++++- app/models/backend_status.rb | 10 +++++++--- app/serializers/backend_status_serializer.rb | 2 ++ .../benefits_education/outside_working_hours.rb | 2 ++ lib/lighthouse/benefits_education/service.rb | 7 +++++++ .../v1/post911_gi_bill_statuses_controller_spec.rb | 9 ++++++++- spec/lib/lighthouse/benefits_education/service_spec.rb | 1 + spec/requests/swagger_spec.rb | 2 ++ spec/requests/v0/backend_status_spec.rb | 4 ++++ .../requests/{v0 => v1}/post911_gi_bill_status_spec.rb | 9 +++++++-- 11 files changed, 47 insertions(+), 11 deletions(-) rename spec/requests/{v0 => v1}/post911_gi_bill_status_spec.rb (84%) diff --git a/app/controllers/v0/backend_statuses_controller.rb b/app/controllers/v0/backend_statuses_controller.rb index 70568051fba..cd2fc52ab07 100644 --- a/app/controllers/v0/backend_statuses_controller.rb +++ b/app/controllers/v0/backend_statuses_controller.rb @@ -14,6 +14,9 @@ def index render json: BackendStatusesSerializer.new(backend_statuses, options) end + # TO-DO: After transition of Post-911 GI Bill to 24/7 availability, confirm show action + # and related logic can be completely removed + # # GET /v0/backend_statuses/:service def show render json: BackendStatusSerializer.new(backend_status) @@ -46,9 +49,5 @@ def validate_service def recognized_service? BackendServices.all.include?(backend_service) end - - def backend_status_is_available - backend_service == BackendServices::GI_BILL_STATUS - end end end diff --git a/app/controllers/v1/post911_gi_bill_statuses_controller.rb b/app/controllers/v1/post911_gi_bill_statuses_controller.rb index 18ed654b730..4836234f414 100644 --- a/app/controllers/v1/post911_gi_bill_statuses_controller.rb +++ b/app/controllers/v1/post911_gi_bill_statuses_controller.rb @@ -10,6 +10,7 @@ class Post911GIBillStatusesController < ApplicationController include SentryLogging service_tag 'gibill-statement' + # TO-DO: Remove this action after transition of LTS to 24/7 availability before_action :service_available?, only: :show STATSD_GI_BILL_TOTAL_KEY = 'api.lighthouse.gi_bill_status.total' @@ -35,8 +36,9 @@ def handle_error(e) render json: { errors: e.errors }, status: status || :internal_server_error end + # TO-DO: Remove this method after transition of LTS to 24/7 availability def service_available? - unless BenefitsEducation::Service.within_scheduled_uptime? + unless Flipper.enabled?(:sob_updated_design) || BenefitsEducation::Service.within_scheduled_uptime? StatsD.increment(STATSD_GI_BILL_FAIL_KEY, tags: ['error:scheduled_downtime']) headers['Retry-After'] = BenefitsEducation::Service.retry_after_time # 503 response @@ -67,6 +69,7 @@ def user_json(user) }.to_json end + # TO-DO: Remove this method after transition of LTS to 24/7 availability def skip_sentry_exception_types super + [BenefitsEducation::OutsideWorkingHours] end diff --git a/app/models/backend_status.rb b/app/models/backend_status.rb index cfca2b7c2b9..8f1bc39999b 100644 --- a/app/models/backend_status.rb +++ b/app/models/backend_status.rb @@ -2,6 +2,8 @@ require 'backend_services' +# TO-DO: After transition of Post-911 GI Bill to 24/7 availability, confirm +# BackendStatus (singular) model and related logic can be removed class BackendStatus include ActiveModel::Serialization include ActiveModel::Validations @@ -18,16 +20,18 @@ def initialize(name:, service_id: nil) end def available? - gibs_service? ? BenefitsEducation::Service.within_scheduled_uptime? : true + service_subject_to_downtime? ? BenefitsEducation::Service.within_scheduled_uptime? : true end def uptime_remaining - gibs_service? ? BenefitsEducation::Service.seconds_until_downtime.to_i : 0 + service_subject_to_downtime? ? BenefitsEducation::Service.seconds_until_downtime.to_i : 0 end private - def gibs_service? + def service_subject_to_downtime? + return false if Flipper.enabled?(:sob_updated_design) + @name == BackendServices::GI_BILL_STATUS end end diff --git a/app/serializers/backend_status_serializer.rb b/app/serializers/backend_status_serializer.rb index 6f714574d29..83bc9c1c197 100644 --- a/app/serializers/backend_status_serializer.rb +++ b/app/serializers/backend_status_serializer.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +# TO-DO: After transition of Post-911 GI Bill to 24/7 availability, confirm +# serializer and related logic can be completely removed class BackendStatusSerializer include JSONAPI::Serializer diff --git a/lib/lighthouse/benefits_education/outside_working_hours.rb b/lib/lighthouse/benefits_education/outside_working_hours.rb index d9825f5210a..104f77c9a12 100644 --- a/lib/lighthouse/benefits_education/outside_working_hours.rb +++ b/lib/lighthouse/benefits_education/outside_working_hours.rb @@ -2,6 +2,8 @@ require 'common/exceptions/base_error' +# TO-DO: Remove this error after transition of LTS to 24/7 availability and associated lines +# in exceptions.en.yml module BenefitsEducation ## # Custom error for when the user is attempting to access the service diff --git a/lib/lighthouse/benefits_education/service.rb b/lib/lighthouse/benefits_education/service.rb index c4a7d16addd..b1c8418dc63 100644 --- a/lib/lighthouse/benefits_education/service.rb +++ b/lib/lighthouse/benefits_education/service.rb @@ -15,6 +15,7 @@ class Service < Common::Client::Base STATSD_KEY_PREFIX = 'api.benefits_education' + # TO-DO: Remove these constants after transition of LTS to 24/7 availability OPERATING_ZONE = 'Eastern Time (US & Canada)' OPERATING_HOURS = { start: 6, @@ -67,6 +68,8 @@ def handle_error(error, lighthouse_client_id, endpoint) end ## + # TO-DO: Remove this method after transition of LTS to 24/7 availability + # # @return [Boolean] Is the current time within the system's scheduled uptime # def self.within_scheduled_uptime? @@ -79,6 +82,8 @@ def self.within_scheduled_uptime? end ## + # TO-DO: Remove this method after transition of LTS to 24/7 availability + # # @return [Integer] The number of seconds until scheduled system downtime begins # def self.seconds_until_downtime @@ -94,6 +99,8 @@ def self.seconds_until_downtime end ## + # TO-DO: Remove this method after transition of LTS to 24/7 availability + # # @return [String] Next earliest date and time that the service will be available # def self.retry_after_time diff --git a/spec/controllers/v1/post911_gi_bill_statuses_controller_spec.rb b/spec/controllers/v1/post911_gi_bill_statuses_controller_spec.rb index 8e342648e35..301939a0a7b 100644 --- a/spec/controllers/v1/post911_gi_bill_statuses_controller_spec.rb +++ b/spec/controllers/v1/post911_gi_bill_statuses_controller_spec.rb @@ -14,6 +14,8 @@ context 'inside working hours' do before do + # TO-DO: Remove this flipper toggle after transition of LTS to 24/7 availability + Flipper.enable(:sob_updated_design) allow(BenefitsEducation::Service).to receive(:within_scheduled_uptime?).and_return(true) end @@ -63,9 +65,14 @@ end end + # TO-DO: Remove this suite of tests after transition of LTS to 24/7 availability context 'outside working hours' do # midnight - before { Timecop.freeze(tz.parse('2nd Feb 1993 00:00:00')) } + before do + Flipper.disable(:sob_updated_design) + Timecop.freeze(tz.parse('2nd Feb 1993 00:00:00')) + end + after { Timecop.return } it 'returns 503' do diff --git a/spec/lib/lighthouse/benefits_education/service_spec.rb b/spec/lib/lighthouse/benefits_education/service_spec.rb index a0da7cde0f5..842d3010622 100644 --- a/spec/lib/lighthouse/benefits_education/service_spec.rb +++ b/spec/lib/lighthouse/benefits_education/service_spec.rb @@ -44,6 +44,7 @@ end end + # TO-DO: Remove this context after transition of LTS to 24/7 availability describe 'uptime/downtime tests' do let(:tz) { ActiveSupport::TimeZone.new(described_class::OPERATING_ZONE) } let(:late_time) { tz.parse('1st Feb 2018 23:00:00') } diff --git a/spec/requests/swagger_spec.rb b/spec/requests/swagger_spec.rb index 142d37c59fa..f1766fd3ba5 100644 --- a/spec/requests/swagger_spec.rb +++ b/spec/requests/swagger_spec.rb @@ -3798,6 +3798,7 @@ context 'GI Bill Status' do it 'supports getting Gi Bill Status' do + Flipper.enable(:sob_updated_design) Timecop.freeze(ActiveSupport::TimeZone.new('Eastern Time (US & Canada)').parse('1st Feb 2018 12:15:06')) expect(subject).to validate(:get, '/v1/post911_gi_bill_status', 401) VCR.use_cassette('lighthouse/benefits_education/200_response') do @@ -3807,6 +3808,7 @@ end it 'supports Gi Bill Status 503 condition' do + Flipper.disable(:sob_updated_design) Timecop.freeze(ActiveSupport::TimeZone.new('Eastern Time (US & Canada)').parse('1st Feb 2018 00:15:06')) expect(subject).to validate(:get, '/v1/post911_gi_bill_status', 503, headers) Timecop.return diff --git a/spec/requests/v0/backend_status_spec.rb b/spec/requests/v0/backend_status_spec.rb index ab40ebeb748..9d44b7f5663 100644 --- a/spec/requests/v0/backend_status_spec.rb +++ b/spec/requests/v0/backend_status_spec.rb @@ -8,6 +8,8 @@ let(:user) { build(:user, :loa3) } + # TO-DO: After transition of Post-911 GI Bill to 24/7 availability, confirm show action + # and related logic can be completely removed describe '#show' do let(:token) { 'fa0f28d6-224a-4015-a3b0-81e77de269f2' } let(:auth_header) { { 'Authorization' => "Token token=#{token}" } } @@ -26,6 +28,8 @@ end context 'for the gibs service' do + before { Flipper.disable(:sob_updated_design) } + context 'during offline hours on saturday' do before { Timecop.freeze(offline_saturday) } diff --git a/spec/requests/v0/post911_gi_bill_status_spec.rb b/spec/requests/v1/post911_gi_bill_status_spec.rb similarity index 84% rename from spec/requests/v0/post911_gi_bill_status_spec.rb rename to spec/requests/v1/post911_gi_bill_status_spec.rb index 73e90c941b7..0bacd87bf1b 100644 --- a/spec/requests/v0/post911_gi_bill_status_spec.rb +++ b/spec/requests/v1/post911_gi_bill_status_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe 'V0::Post911GIBillStatus', type: :request do +RSpec.describe 'V1::Post911GIBillStatus', type: :request do include SchemaMatchers let(:tz) { ActiveSupport::TimeZone.new(BenefitsEducation::Service::OPERATING_ZONE) } @@ -15,6 +15,7 @@ allow(Settings.evss).to receive(:mock_gi_bill_status).and_return(false) end + # TO-DO: Rename context after transition of LTS to 24/7 availability context 'inside working hours' do before { Timecop.freeze(noon) } @@ -31,8 +32,12 @@ end end + # TO-DO: Remove context after transition of LTS to 24/7 availability context 'outside working hours' do - before { Timecop.freeze(midnight) } + before do + Flipper.disable(:sob_updated_design) + Timecop.freeze(midnight) + end after { Timecop.return } From 34abc5bac4dde1a1065cc60783d9973d98cd7803 Mon Sep 17 00:00:00 2001 From: Nathan Burgess Date: Fri, 4 Oct 2024 12:04:47 -0400 Subject: [PATCH 06/30] Disability benefits nb refactor document upload provider initialization 1 (#18745) * Refactor ApiProviderFactory supplemental upload provider Initializer Updates the ApiProviderFactory to pass more metadata to the SupplementalDocumentUploadProvider initializers so we can encapsulate more behavior (logging/polling) in the provider instead of the upload jobs that are using them * Update Lighthouse upload provider for new behavior Updates LighthouseSupplementalDocumentUploadProvider to use the new initializing signature that allows us to move logging and polling record creation out of the job * Update EVSS upload provider for new behavior Updates EVSSSupplementalDocumentUploadProvider to use the new initializing signature that allows us to move logging out of the job * Revert accidental BDD instruction cherry pick * Update EVSS upload provider for new behavior Updates EVSSSupplementalDocumentUploadProvider to use the new initializing signature that allows us to move logging out of the job * Update shared example for new document upload provider initialize sig * Revert BDD Instruction-specific provider implementaiton; will follow in another PR * Trim docstrings to reduce line count in PR * Fix linting error --- .../factories/api_provider_factory.rb | 10 ++- ...s_supplemental_document_upload_provider.rb | 24 +++-- ...e_supplemental_document_upload_provider.rb | 66 ++++++++++---- .../factories/api_provider_factory_spec.rb | 15 ++-- ...plemental_document_upload_provider_spec.rb | 53 +++++++---- ...plemental_document_upload_provider_spec.rb | 87 ++++++++++++++----- .../supplemental_document_upload_provider.rb | 2 +- 7 files changed, 173 insertions(+), 84 deletions(-) diff --git a/lib/disability_compensation/factories/api_provider_factory.rb b/lib/disability_compensation/factories/api_provider_factory.rb index 95f2e087b8e..5a0e06f0f79 100644 --- a/lib/disability_compensation/factories/api_provider_factory.rb +++ b/lib/disability_compensation/factories/api_provider_factory.rb @@ -187,11 +187,17 @@ def generate_pdf_service_provider end def supplemental_document_upload_service_provider + provider_options = [ + @options[:form526_submission], + @options[:document_type], + @options[:statsd_metric_prefix] + ] + case api_provider when API_PROVIDER[:evss] - EVSSSupplementalDocumentUploadProvider.new(@options[:form526_submission]) + EVSSSupplementalDocumentUploadProvider.new(*provider_options) when API_PROVIDER[:lighthouse] - LighthouseSupplementalDocumentUploadProvider.new(@options[:form526_submission]) + LighthouseSupplementalDocumentUploadProvider.new(*provider_options) else raise NotImplementedError, 'No known Supplemental Document Upload Api Provider type provided' end diff --git a/lib/disability_compensation/providers/document_upload/evss_supplemental_document_upload_provider.rb b/lib/disability_compensation/providers/document_upload/evss_supplemental_document_upload_provider.rb index e5f55a8534c..3d2a29486bf 100644 --- a/lib/disability_compensation/providers/document_upload/evss_supplemental_document_upload_provider.rb +++ b/lib/disability_compensation/providers/document_upload/evss_supplemental_document_upload_provider.rb @@ -8,8 +8,12 @@ class EVSSSupplementalDocumentUploadProvider STATSD_PROVIDER_METRIC = 'evss_supplemental_document_upload_provider' # @param form526_submission [Form526Submission] - def initialize(form526_submission) + # @param va_document_type [String] VA document code; see LighthouseDocument::DOCUMENT_TYPES + # @param statsd_metric_prefix [String] prefix, e.g. 'worker.evss.submit_form526_bdd_instructions' from including job + def initialize(form526_submission, va_document_type, statsd_metric_prefix) @form526_submission = form526_submission + @va_document_type = va_document_type + @statsd_metric_prefix = statsd_metric_prefix end # Uploads to EVSS via the EVSS::DocumentsService require both the file body and an instance @@ -18,16 +22,12 @@ def initialize(form526_submission) # an assembly of file-related EVSS metadata, not the actual uploaded file itself # # @param file_name [String] The name of the file we want to appear in EVSS - # @param document_type [String] The VA document code, which corresponds to - # the type of document being uploaded ('Buddy/Lay Statement', 'Disability Benefits Questionnaire (DBQ)' etc.) - # These types are mapped in EVSSClaimDocument[DOCUMENT_TYPES] - # # @return [EVSSClaimDocument] - def generate_upload_document(file_name, document_type) + def generate_upload_document(file_name) EVSSClaimDocument.new( evss_claim_id: @form526_submission.submitted_claim_id, - file_name:, - document_type: + document_type: @va_document_type, + file_name: ) end @@ -49,14 +49,12 @@ def validate_upload_document(evss_claim_document) def submit_upload_document(evss_claim_document, file_body) client = EVSS::DocumentsService.new(@form526_submission.auth_headers) client.upload(file_body, evss_claim_document) - end - def log_upload_success(uploading_class_prefix) - StatsD.increment("#{uploading_class_prefix}.#{STATSD_PROVIDER_METRIC}.#{STATSD_SUCCESS_METRIC}") + StatsD.increment("#{@statsd_metric_prefix}.#{STATSD_PROVIDER_METRIC}.#{STATSD_SUCCESS_METRIC}") end - def log_upload_failure(uploading_class_prefix, error_class, error_message) - StatsD.increment("#{uploading_class_prefix}.#{STATSD_PROVIDER_METRIC}.#{STATSD_FAILED_METRIC}") + def log_upload_failure(error_class, error_message) + StatsD.increment("#{@statsd_metric_prefix}.#{STATSD_PROVIDER_METRIC}.#{STATSD_FAILED_METRIC}") Rails.logger.error( 'EVSSSupplementalDocumentUploadProvider upload failure', diff --git a/lib/disability_compensation/providers/document_upload/lighthouse_supplemental_document_upload_provider.rb b/lib/disability_compensation/providers/document_upload/lighthouse_supplemental_document_upload_provider.rb index 6623f443b5a..3dc54d34d34 100644 --- a/lib/disability_compensation/providers/document_upload/lighthouse_supplemental_document_upload_provider.rb +++ b/lib/disability_compensation/providers/document_upload/lighthouse_supplemental_document_upload_provider.rb @@ -8,9 +8,20 @@ class LighthouseSupplementalDocumentUploadProvider STATSD_PROVIDER_METRIC = 'lighthouse_supplemental_document_upload_provider' + # Maps VA's internal Document Types to the correct document_type attribute for a Lighthouse526DocumentUpload polling + # record. We need this to create a valid polling record + POLLING_DOCUMENT_TYPES = { + 'L023' => Lighthouse526DocumentUpload::BDD_INSTRUCTIONS_DOCUMENT_TYPE + }.freeze + # @param form526_submission [Form526Submission] - def initialize(form526_submission) + # + # @param va_document_type [String] VA document code, see LighthouseDocument::DOCUMENT_TYPES + # @param statsd_metric_prefix [String] prefix, e.g. 'worker.evss.submit_form526_bdd_instructions' from including job + def initialize(form526_submission, va_document_type, statsd_metric_prefix) @form526_submission = form526_submission + @va_document_type = va_document_type + @statsd_metric_prefix = statsd_metric_prefix end # Uploads to Lighthouse require both the file body and an instance @@ -19,16 +30,16 @@ def initialize(form526_submission) # an assembly of file-related Lighthouse metadata, not the actual uploaded file itself # # @param file_name [String] The name of the file we want to appear in Lighthouse - # @param document_type [String] The VA document code, which corresponds to - # the type of document being uploaded ('Buddy/Lay Statement', 'Disability Benefits Questionnaire (DBQ)' etc.) - # These types are mapped in LighthouseDocument::DOCUMENT_TYPES # # @return [LighthouseDocument] - def generate_upload_document(file_name, document_type) + def generate_upload_document(file_name) + user = User.find(@form526_submission.user_uuid) + LighthouseDocument.new( - evss_claim_id: @form526_submission.submitted_claim_id, - file_name:, - document_type: + claim_id: @form526_submission.submitted_claim_id, + participant_id: user.participant_id, + document_type: @va_document_type, + file_name: ) end @@ -45,19 +56,13 @@ def validate_upload_document(lighthouse_document) # # @param lighthouse_document [LighthouseDocument] # @param file_body [String] - # - # return [Faraday::Response] BenefitsDocuments::WorkerService makes http - # calls with the Faraday gem under the hood def submit_upload_document(lighthouse_document, file_body) - BenefitsDocuments::Form526::UploadSupplementalDocumentService.call(file_body, lighthouse_document) + api_response = BenefitsDocuments::Form526::UploadSupplementalDocumentService.call(file_body, lighthouse_document) + handle_lighthouse_response(api_response) end - def log_upload_success(uploading_class_prefix) - StatsD.increment("#{uploading_class_prefix}.#{STATSD_PROVIDER_METRIC}.#{STATSD_SUCCESS_METRIC}") - end - - def log_upload_failure(uploading_class_prefix, error_class, error_message) - StatsD.increment("#{uploading_class_prefix}.#{STATSD_PROVIDER_METRIC}.#{STATSD_FAILED_METRIC}") + def log_upload_failure(error_class, error_message) + StatsD.increment("#{@statsd_metric_prefix}.#{STATSD_PROVIDER_METRIC}.#{STATSD_FAILED_METRIC}") Rails.logger.error( 'LighthouseSupplementalDocumentUploadProvider upload failure', @@ -68,4 +73,29 @@ def log_upload_failure(uploading_class_prefix, error_class, error_message) } ) end + + private + + # @param api_response [Faraday::Response] Lighthouse API response returned from the UploadSupplementalDocumentService + def handle_lighthouse_response(api_response) + response_body = api_response.body['data'] + + if response_body['success'] == true && response_body['requestId'] + create_lighthouse_polling_record(response_body['requestId']) + StatsD.increment("#{@statsd_metric_prefix}.#{STATSD_PROVIDER_METRIC}.#{STATSD_SUCCESS_METRIC}") + end + end + + # Creates a Lighthouse526DocumentUpload polling record + # + # @param lighthouse_request_id [String] unique ID Lighthouse provides us in the API response after we + # upload a document. We use this ID in the Form526DocumentUploadPollingJob chron job to check the status + # of the document after Lighthouse has received it. + def create_lighthouse_polling_record(lighthouse_request_id) + Lighthouse526DocumentUpload.create!( + form526_submission: @form526_submission, + document_type: POLLING_DOCUMENT_TYPES[@va_document_type], + lighthouse_document_request_id: lighthouse_request_id + ) + end end diff --git a/spec/lib/disability_compensation/factories/api_provider_factory_spec.rb b/spec/lib/disability_compensation/factories/api_provider_factory_spec.rb index 302b6523172..c507a82d1e3 100644 --- a/spec/lib/disability_compensation/factories/api_provider_factory_spec.rb +++ b/spec/lib/disability_compensation/factories/api_provider_factory_spec.rb @@ -247,6 +247,8 @@ def provider(api_provider = nil) context 'upload supplemental document' do let(:submission) { create(:form526_submission) } + # BDD Document Type + let(:va_document_type) { 'L023' } def provider(api_provider = nil) ApiProviderFactory.call( @@ -254,10 +256,11 @@ def provider(api_provider = nil) provider: api_provider, options: { form526_submission: submission, - file_body: '' + document_type: va_document_type, + statsd_metric_prefix: 'my_stats_metric_prefix' }, current_user:, - feature_toggle: ApiProviderFactory::FEATURE_TOGGLE_UPLOAD_SUPPLEMENTAL_DOCUMENT + feature_toggle: nil ) end @@ -269,14 +272,6 @@ def provider(api_provider = nil) expect(provider(:lighthouse).class).to equal(LighthouseSupplementalDocumentUploadProvider) end - it 'provides upload_supplemental_document provider based on Flipper' do - Flipper.enable(ApiProviderFactory::FEATURE_TOGGLE_UPLOAD_SUPPLEMENTAL_DOCUMENT) - expect(provider.class).to equal(LighthouseSupplementalDocumentUploadProvider) - - Flipper.disable(ApiProviderFactory::FEATURE_TOGGLE_UPLOAD_SUPPLEMENTAL_DOCUMENT) - expect(provider.class).to equal(EVSSSupplementalDocumentUploadProvider) - end - it 'throw error if provider unknown' do expect do provider(:random) diff --git a/spec/lib/disability_compensation/providers/document_upload/evss_supplemental_document_upload_provider_spec.rb b/spec/lib/disability_compensation/providers/document_upload/evss_supplemental_document_upload_provider_spec.rb index 8b7a0abedd7..499a7faabe6 100644 --- a/spec/lib/disability_compensation/providers/document_upload/evss_supplemental_document_upload_provider_spec.rb +++ b/spec/lib/disability_compensation/providers/document_upload/evss_supplemental_document_upload_provider_spec.rb @@ -8,7 +8,16 @@ let(:submission) { create(:form526_submission) } let(:file_body) { File.read(fixture_file_upload('doctors-note.pdf', 'application/pdf')) } let(:file_name) { Faker::File.file_name } - let(:provider) { EVSSSupplementalDocumentUploadProvider.new(submission) } + + let(:va_document_type) { 'L023' } + + let(:provider) do + EVSSSupplementalDocumentUploadProvider.new( + submission, + va_document_type, + 'my_upload_job_prefix' + ) + end let(:evss_claim_document) do EVSSClaimDocument.new( @@ -23,16 +32,14 @@ describe '#generate_upload_document' do it 'generates an EVSSClaimDocument' do file_name = Faker::File.file_name - document_type = 'L023' - - upload_document = provider.generate_upload_document(file_name, document_type) + upload_document = provider.generate_upload_document(file_name) expect(upload_document).to be_an_instance_of(EVSSClaimDocument) expect(upload_document).to have_attributes( { evss_claim_id: submission.submitted_claim_id, file_name:, - document_type: + document_type: va_document_type } ) end @@ -55,14 +62,25 @@ describe '#submit_upload_document' do context 'for a valid payload' do - let(:faraday_response) { instance_double(Faraday::Response) } + it 'submits the document via the EVSSDocumentService' do + expect_any_instance_of(EVSS::DocumentsService).to receive(:upload) + .with(file_body, evss_claim_document) + + provider.submit_upload_document(evss_claim_document, file_body) + end + + it 'increments a StatsD success metric' do + faraday_response = instance_double(Faraday::Response) - it 'submits the document via the EVSSDocumentService and returns the API response' do allow_any_instance_of(EVSS::DocumentsService).to receive(:upload) .with(file_body, evss_claim_document) .and_return(faraday_response) - expect(provider.submit_upload_document(evss_claim_document, file_body)).to eq(faraday_response) + expect(StatsD).to receive(:increment).with( + 'my_upload_job_prefix.evss_supplemental_document_upload_provider.success' + ) + + provider.submit_upload_document(evss_claim_document, file_body) end end end @@ -72,15 +90,12 @@ # since submissions have callbacks that log to StatsD and we need to test # only the metrics in this class let(:submission) { instance_double(Form526Submission) } - let(:provider) { EVSSSupplementalDocumentUploadProvider.new(submission) } - - describe 'log_upload_success' do - it 'increments a StatsD success metric' do - expect(StatsD).to receive(:increment).with( - 'my_upload_job_prefix.evss_supplemental_document_upload_provider.success' - ) - provider.log_upload_success('my_upload_job_prefix') - end + let(:provider) do + EVSSSupplementalDocumentUploadProvider.new( + submission, + va_document_type, + 'my_upload_job_prefix' + ) end describe 'log_upload_failure' do @@ -91,7 +106,7 @@ expect(StatsD).to receive(:increment).with( 'my_upload_job_prefix.evss_supplemental_document_upload_provider.failed' ) - provider.log_upload_failure('my_upload_job_prefix', error_class, error_message) + provider.log_upload_failure(error_class, error_message) end it 'logs to the Rails logger' do @@ -104,7 +119,7 @@ } ) - provider.log_upload_failure('my_upload_job_prefix', error_class, error_message) + provider.log_upload_failure(error_class, error_message) end end end diff --git a/spec/lib/disability_compensation/providers/document_upload/lighthouse_supplemental_document_upload_provider_spec.rb b/spec/lib/disability_compensation/providers/document_upload/lighthouse_supplemental_document_upload_provider_spec.rb index b2c741117db..095f365a501 100644 --- a/spec/lib/disability_compensation/providers/document_upload/lighthouse_supplemental_document_upload_provider_spec.rb +++ b/spec/lib/disability_compensation/providers/document_upload/lighthouse_supplemental_document_upload_provider_spec.rb @@ -6,16 +6,27 @@ require 'support/disability_compensation_form/shared_examples/supplemental_document_upload_provider' RSpec.describe LighthouseSupplementalDocumentUploadProvider do - let(:submission) { create(:form526_submission) } + let(:submission) { create(:form526_submission, :with_submitted_claim_id) } + let(:submission_user) { User.find(submission.user_uuid) } let(:file_body) { File.read(fixture_file_upload('doctors-note.pdf', 'application/pdf')) } let(:file_name) { Faker::File.file_name } - let(:provider) { LighthouseSupplementalDocumentUploadProvider.new(submission) } + # BDD Document Type + let(:va_document_type) { 'L023' } + + let(:provider) do + LighthouseSupplementalDocumentUploadProvider.new( + submission, + va_document_type, + 'my_stats_metric_prefix' + ) + end let(:lighthouse_document) do LighthouseDocument.new( claim_id: submission.submitted_claim_id, - document_type: 'L023', + participant_id: submission_user.participant_id, + document_type: va_document_type, file_name: ) end @@ -25,16 +36,16 @@ describe 'generate_upload_document' do it 'generates a LighthouseDocument' do file_name = Faker::File.file_name - document_type = 'L023' - upload_document = provider.generate_upload_document(file_name, document_type) + upload_document = provider.generate_upload_document(file_name) expect(upload_document).to be_an_instance_of(LighthouseDocument) expect(upload_document).to have_attributes( { claim_id: submission.submitted_claim_id, - file_name:, - document_type: + participant_id: submission_user.participant_id, + document_type: va_document_type, + file_name: } ) end @@ -58,13 +69,50 @@ describe 'submit_upload_document' do let(:faraday_response) { instance_double(Faraday::Response) } + let(:lighthouse_request_id) { Faker::Number.number(digits: 8) } - it 'submits the document via the UploadSupplementalDocumentService and returns the response' do + before do allow(BenefitsDocuments::Form526::UploadSupplementalDocumentService).to receive(:call) .with(file_body, lighthouse_document) .and_return(faraday_response) - expect(provider.submit_upload_document(lighthouse_document, file_body)).to eq(faraday_response) + allow(faraday_response).to receive(:body).and_return( + { + 'data' => { + 'success' => true, + 'requestId' => lighthouse_request_id + } + } + ) + end + + it 'uploads the document via the UploadSupplementalDocumentService' do + expect(BenefitsDocuments::Form526::UploadSupplementalDocumentService).to receive(:call) + .with(file_body, lighthouse_document) + + provider.submit_upload_document(lighthouse_document, file_body) + end + + it 'increments a StatsD success metric' do + expect(StatsD).to receive(:increment).with( + 'my_stats_metric_prefix.lighthouse_supplemental_document_upload_provider.success' + ) + + provider.submit_upload_document(lighthouse_document, file_body) + end + + it 'creates a pending Lighthouse526DocumentUpload record for the submission so we can poll Lighthouse later' do + upload_attributes = { + aasm_state: 'pending', + form526_submission_id: submission.id, + # Polling record type mapped to L023 used in tests + document_type: Lighthouse526DocumentUpload::BDD_INSTRUCTIONS_DOCUMENT_TYPE, + lighthouse_document_request_id: lighthouse_request_id + } + + expect do + provider.submit_upload_document(lighthouse_document, file_body) + end.to change { Lighthouse526DocumentUpload.where(**upload_attributes).count }.by(1) end end @@ -74,15 +122,12 @@ # only the metrics in this class let(:submission) { instance_double(Form526Submission) } - let(:provider) { LighthouseSupplementalDocumentUploadProvider.new(submission) } - - describe 'log_upload_success' do - it 'increments a StatsD success metric' do - expect(StatsD).to receive(:increment).with( - 'my_upload_job_prefix.lighthouse_supplemental_document_upload_provider.success' - ) - provider.log_upload_success('my_upload_job_prefix') - end + let(:provider) do + LighthouseSupplementalDocumentUploadProvider.new( + submission, + 'MyUploadingClass', + 'my_stats_metric_prefix' + ) end describe 'log_upload_failure' do @@ -91,9 +136,9 @@ it 'increments a StatsD failure metric' do expect(StatsD).to receive(:increment).with( - 'my_upload_job_prefix.lighthouse_supplemental_document_upload_provider.failed' + 'my_stats_metric_prefix.lighthouse_supplemental_document_upload_provider.failed' ) - provider.log_upload_failure('my_upload_job_prefix', error_class, error_message) + provider.log_upload_failure(error_class, error_message) end it 'logs to the Rails logger' do @@ -106,7 +151,7 @@ } ) - provider.log_upload_failure('my_upload_job_prefix', error_class, error_message) + provider.log_upload_failure(error_class, error_message) end end end diff --git a/spec/support/disability_compensation_form/shared_examples/supplemental_document_upload_provider.rb b/spec/support/disability_compensation_form/shared_examples/supplemental_document_upload_provider.rb index d84e24b2f2e..4c24ac416cc 100644 --- a/spec/support/disability_compensation_form/shared_examples/supplemental_document_upload_provider.rb +++ b/spec/support/disability_compensation_form/shared_examples/supplemental_document_upload_provider.rb @@ -3,7 +3,7 @@ require 'rails_helper' shared_examples 'supplemental document upload provider' do - subject { described_class.new(submission) } + subject { described_class.new(submission, 'My Document Type', 'my_metrics_prefix') } let(:submission) { create(:form526_submission) } From bf2d3d7b0eac3928aceeac750de708a3265f993e Mon Sep 17 00:00:00 2001 From: John Bramley Date: Fri, 4 Oct 2024 10:11:13 -0600 Subject: [PATCH 07/30] updates mhv account creation betamocks path (#18748) --- config/betamocks/services_config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/betamocks/services_config.yml b/config/betamocks/services_config.yml index 04493f2e3cd..24a9b3fc06e 100644 --- a/config/betamocks/services_config.yml +++ b/config/betamocks/services_config.yml @@ -126,7 +126,7 @@ :endpoints: - :method: :post :path: '/v1/usermgmt/account-service/account' - :file_path: 'mhv/account_creation/create_account' + :file_path: 'mhv/account_creation/create_account/default' - :name: 'MHV_Rx' :base_uri: <%= "#{URI(Settings.mhv.rx.host).host}:#{URI(Settings.mhv.rx.host).port}" %> From 6635baa206055c9ea4bacdd588f2e4d3191ce1ac Mon Sep 17 00:00:00 2001 From: kanchanasuriya <89944361+kanchanasuriya@users.noreply.github.com> Date: Fri, 4 Oct 2024 10:16:56 -0700 Subject: [PATCH 08/30] 91993 Modify CIE vaos appointment serializer for upcoming appointments (#18434) * 91993 Modify CIE vaos appointment serializer for upcoming appointments * Fixing rubocop failures * Update modules/check_in/app/serializers/check_in/vaos/appointment_serializer.rb Co-authored-by: Gaurav Gupta * Update modules/check_in/app/serializers/check_in/vaos/appointment_serializer.rb Co-authored-by: Gaurav Gupta * Addressing review comments --------- Co-authored-by: kanchanasuriya Co-authored-by: Gaurav Gupta --- .../check_in/vaos/appointment_serializer.rb | 19 + .../check_in/v2/sessions/appointments_spec.rb | 72 ++ .../vaos/appointment_serializer_spec.rb | 735 ++++++++++++++++-- 3 files changed, 743 insertions(+), 83 deletions(-) diff --git a/modules/check_in/app/serializers/check_in/vaos/appointment_serializer.rb b/modules/check_in/app/serializers/check_in/vaos/appointment_serializer.rb index a1f737806f9..e6217901ff3 100644 --- a/modules/check_in/app/serializers/check_in/vaos/appointment_serializer.rb +++ b/modules/check_in/app/serializers/check_in/vaos/appointment_serializer.rb @@ -10,6 +10,25 @@ class AppointmentSerializer attributes :kind, :status, :serviceType, :locationId, :clinic, :start, :end, :minutesDuration + attribute :telehealth do |object| + { + vvsKind: object.dig('telehealth', 'vvsKind'), + atlas: object.dig('telehealth', 'atlas') + } + end + + attribute :extension do |object| + { + preCheckinAllowed: object.dig('extension', 'preCheckinAllowed'), + eCheckinAllowed: object.dig('extension', 'eCheckinAllowed'), + patientHasMobileGfe: object.dig('extension', 'patientHasMobileGfe') + } + end + + attribute :serviceCategory do |object| + object.serviceCategory&.map { |category| { text: category['text'] } } + end + attribute :facilityName do |object| object.dig(:facility, :name) end diff --git a/modules/check_in/spec/requests/check_in/v2/sessions/appointments_spec.rb b/modules/check_in/spec/requests/check_in/v2/sessions/appointments_spec.rb index 48c24f8d4bf..0578be530f0 100644 --- a/modules/check_in/spec/requests/check_in/v2/sessions/appointments_spec.rb +++ b/modules/check_in/spec/requests/check_in/v2/sessions/appointments_spec.rb @@ -150,6 +150,18 @@ start: '2023-11-13T16:00:00Z', end: '2023-11-13T16:30:00Z', minutesDuration: 30, + telehealth: { + vvsKind: nil, + atlas: nil + }, + extension: { + preCheckinAllowed: true, + eCheckinAllowed: true, + patientHasMobileGfe: nil + }, + serviceCategory: [{ + text: 'REGULAR' + }], facilityName: 'Ralph H. Johnson Department of Veterans Affairs Medical Center', facilityVistaSite: '534', facilityTimezone: 'America/New_York', @@ -171,6 +183,18 @@ start: '2023-12-11T16:00:00Z', end: '2023-12-11T16:30:00Z', minutesDuration: 30, + telehealth: { + vvsKind: nil, + atlas: nil + }, + extension: { + preCheckinAllowed: true, + eCheckinAllowed: true, + patientHasMobileGfe: nil + }, + serviceCategory: [{ + text: 'REGULAR' + }], facilityName: 'Ralph H. Johnson Department of Veterans Affairs Medical Center', facilityVistaSite: '534', facilityTimezone: 'America/New_York', @@ -216,6 +240,18 @@ start: '2023-11-13T16:00:00Z', end: '2023-11-13T16:30:00Z', minutesDuration: 30, + telehealth: { + vvsKind: nil, + atlas: nil + }, + extension: { + preCheckinAllowed: true, + eCheckinAllowed: true, + patientHasMobileGfe: nil + }, + serviceCategory: [{ + text: 'REGULAR' + }], facilityName: nil, facilityVistaSite: nil, facilityTimezone: nil, @@ -237,6 +273,18 @@ start: '2023-12-11T16:00:00Z', end: '2023-12-11T16:30:00Z', minutesDuration: 30, + telehealth: { + vvsKind: nil, + atlas: nil + }, + extension: { + preCheckinAllowed: true, + eCheckinAllowed: true, + patientHasMobileGfe: nil + }, + serviceCategory: [{ + text: 'REGULAR' + }], facilityName: 'Ralph H. Johnson Department of Veterans Affairs Medical Center', facilityVistaSite: '534', facilityTimezone: 'America/New_York', @@ -281,6 +329,18 @@ start: '2023-11-13T16:00:00Z', end: '2023-11-13T16:30:00Z', minutesDuration: 30, + telehealth: { + vvsKind: nil, + atlas: nil + }, + extension: { + preCheckinAllowed: true, + eCheckinAllowed: true, + patientHasMobileGfe: nil + }, + serviceCategory: [{ + text: 'REGULAR' + }], facilityName: 'Ralph H. Johnson Department of Veterans Affairs Medical Center', facilityVistaSite: '534', facilityTimezone: 'America/New_York', @@ -302,6 +362,18 @@ start: '2023-12-11T16:00:00Z', end: '2023-12-11T16:30:00Z', minutesDuration: 30, + telehealth: { + vvsKind: nil, + atlas: nil + }, + extension: { + preCheckinAllowed: true, + eCheckinAllowed: true, + patientHasMobileGfe: nil + }, + serviceCategory: [{ + text: 'REGULAR' + }], facilityName: 'Ralph H. Johnson Department of Veterans Affairs Medical Center', facilityVistaSite: '534', facilityTimezone: 'America/New_York', diff --git a/modules/check_in/spec/serializers/vaos/appointment_serializer_spec.rb b/modules/check_in/spec/serializers/vaos/appointment_serializer_spec.rb index 1aa4a0200ca..5eb4717f71b 100644 --- a/modules/check_in/spec/serializers/vaos/appointment_serializer_spec.rb +++ b/modules/check_in/spec/serializers/vaos/appointment_serializer_spec.rb @@ -5,40 +5,390 @@ RSpec.describe CheckIn::VAOS::AppointmentSerializer do subject { described_class } - let(:vaos_appointment_data) do - '{ - "data": [ - { - "id": "180766", - "identifier": [ - { - "system": "Appointment/", - "value": "413938333130383736" + context 'for valid vaos clinic appointment data' do + let(:vaos_clinic_appointment_data) do + '{ + "data": [ + { + "id": "180766", + "identifier": [ + { + "system": "Appointment/", + "value": "413938333130383736" + } + ], + "kind": "clinic", + "status": "booked", + "serviceType": "amputation", + "patientIcn": "1013125218V696863", + "locationId": "983GC", + "clinic": "1081", + "start": "2023-11-13T16:00:00Z", + "end": "2023-11-13T16:30:00Z", + "minutesDuration": 30, + "facility": { + "name": "abc facility", + "vistaSite": "534", + "timezone": { "timeZoneId": "America/New York" }, + "phone": { "main": "843-577-5011" } + }, + "clinicInfo":{ + "data": { + "serviceName": "CHS NEUROSURGERY VARMA", + "physicalLocation": "1ST FL SPECIALTY MODULE 2", + "friendlyName": "CHS NEUROSURGERY VARMA" + } } - ], - "kind": "clinic", - "status": "booked", - "serviceType": "amputation", - "patientIcn": "1013125218V696863", - "locationId": "983GC", - "clinic": "1081", - "start": "2023-11-13T16:00:00Z", - "end": "2023-11-13T16:30:00Z", - "minutesDuration": 30, - "facility": { - "name": "abc facility", - "vistaSite": "534", - "timezone": { "timeZoneId": "America/New York" }, - "phone": { "main": "843-577-5011" } }, - "clinicInfo":{ - "data": { - "serviceName": "CHS NEUROSURGERY VARMA", - "physicalLocation": "1ST FL SPECIALTY MODULE 2", - "friendlyName": "CHS NEUROSURGERY VARMA" + { + "id": "180770", + "identifier": [ + { + "system": "Appointment/", + "value": "413938333130383736" + } + ], + "kind": "clinic", + "status": "booked", + "serviceType": "amputation", + "patientIcn": "1013125218V696863", + "locationId": "983GC", + "clinic": "1081", + "start": "2023-11-13T16:00:00Z", + "end": "2023-11-13T16:30:00Z", + "minutesDuration": 30, + "facility": { + "name": "def facility", + "vistaSite": "909", + "timezone": { "timeZoneId": "America/New York" }, + "phone": { "main": "843-577-5011" } + }, + "clinicInfo":{ + "data": { + "serviceName": "CaregiverSupport", + "physicalLocation": "2360 East Pershing Boulevard", + "friendlyName": "CaregiverSupport" + } + } + } + ] + }' + end + let(:appt_struct_data) do + struct = JSON.parse(vaos_clinic_appointment_data, object_class: OpenStruct) + struct.data + end + let(:appointment1) do + { + id: '180766', + type: :appointments, + attributes: { + kind: 'clinic', + status: 'booked', + serviceType: 'amputation', + locationId: '983GC', + clinic: '1081', + start: '2023-11-13T16:00:00Z', + end: '2023-11-13T16:30:00Z', + minutesDuration: 30, + telehealth: { + vvsKind: nil, + atlas: nil + }, + extension: { + preCheckinAllowed: nil, + eCheckinAllowed: nil, + patientHasMobileGfe: nil + }, + serviceCategory: nil, + facilityName: 'abc facility', + facilityVistaSite: '534', + facilityTimezone: 'America/New York', + facilityPhoneMain: '843-577-5011', + clinicServiceName: 'CHS NEUROSURGERY VARMA', + clinicPhysicalLocation: '1ST FL SPECIALTY MODULE 2', + clinicFriendlyName: 'CHS NEUROSURGERY VARMA' + } + } + end + let(:appointment2) do + { + id: '180770', + type: :appointments, + attributes: { + kind: 'clinic', + status: 'booked', + serviceType: 'amputation', + locationId: '983GC', + clinic: '1081', + start: '2023-11-13T16:00:00Z', + end: '2023-11-13T16:30:00Z', + minutesDuration: 30, + telehealth: { + vvsKind: nil, + atlas: nil + }, + extension: { + preCheckinAllowed: nil, + eCheckinAllowed: nil, + patientHasMobileGfe: nil + }, + serviceCategory: nil, + facilityName: 'def facility', + facilityVistaSite: '909', + facilityTimezone: 'America/New York', + facilityPhoneMain: '843-577-5011', + clinicServiceName: 'CaregiverSupport', + clinicPhysicalLocation: '2360 East Pershing Boulevard', + clinicFriendlyName: 'CaregiverSupport' + } + } + end + + let(:serialized_hash_response) do + { + data: [appointment1, appointment2] + } + end + + it 'returns a serialized hash' do + serializer = subject.new(appt_struct_data) + expect(serializer.serializable_hash).to eq(serialized_hash_response) + end + end + + context 'for valid vaos video appointment data at atlas location' do + let(:vaos_video_appointment_data) do + '{ + "data": [ + { + "id": "180766", + "identifier": [ + { + "system": "Appointment/", + "value": "413938333130383736" + } + ], + "kind": "telehealth", + "status": "booked", + "serviceType": "amputation", + "patientIcn": "1013125218V696863", + "locationId": "983GC", + "clinic": "1081", + "start": "2023-11-13T16:00:00Z", + "end": "2023-11-13T16:30:00Z", + "minutesDuration": 30, + "telehealth": { + "url": "https://dev.care2.va.gov/vvc-app/?join=1&media=1&escalate=1&userType=guest&conference=VAC000003896@dev.care2.va.gov&pin=104039&aid=5c21ee08-7bc9-4cc3-b557-0fc543c40148#", + "atlas": { + "siteCode": "VFW-DC-20011-02", + "confirmationCode": "075041", + "address": { + "streetAddress": "5929 Georgia Ave NW", + "city": "Washington", + "state": "DC", + "zipCode": "20011", + "country": "USA", + "latitutde": 38.961979, + "longitude": -77.027908, + "additionalDetails": "" + } + }, + "group": false, + "vvsKind": "ADHOC" + }, + "extension": { + "ccLocation": { + "address": {} + }, + "vistaStatus": [ + "NO ACTION TAKEN" + ], + "preCheckinAllowed": true, + "eCheckinAllowed": true, + "clinic": {} + }, + "facility": { + "name": "abc facility", + "vistaSite": "534", + "timezone": { "timeZoneId": "America/New York" }, + "phone": { "main": "843-577-5011" } + }, + "clinicInfo":{ + "data": { + "serviceName": "CHS NEUROSURGERY VARMA", + "physicalLocation": "1ST FL SPECIALTY MODULE 2", + "friendlyName": "CHS NEUROSURGERY VARMA" + } } } - }, + ] + }' + end + let(:video_appt_struct_data) do + struct = JSON.parse(vaos_video_appointment_data, object_class: OpenStruct) + struct.data + end + let(:atlas_struct_data) do + address = OpenStruct.new(streetAddress: '5929 Georgia Ave NW', city: 'Washington', state: 'DC', zipCode: '20011', + country: 'USA', latitutde: 38.961979, longitude: -77.027908, additionalDetails: '') + OpenStruct.new(siteCode: 'VFW-DC-20011-02', confirmationCode: '075041', address:) + end + let(:appointment1) do + { + id: '180766', + type: :appointments, + attributes: { + kind: 'telehealth', + status: 'booked', + serviceType: 'amputation', + locationId: '983GC', + clinic: '1081', + start: '2023-11-13T16:00:00Z', + end: '2023-11-13T16:30:00Z', + minutesDuration: 30, + telehealth: { + vvsKind: 'ADHOC', + atlas: atlas_struct_data + }, + extension: { + preCheckinAllowed: true, + eCheckinAllowed: true, + patientHasMobileGfe: nil + }, + serviceCategory: nil, + facilityName: 'abc facility', + facilityVistaSite: '534', + facilityTimezone: 'America/New York', + facilityPhoneMain: '843-577-5011', + clinicServiceName: 'CHS NEUROSURGERY VARMA', + clinicPhysicalLocation: '1ST FL SPECIALTY MODULE 2', + clinicFriendlyName: 'CHS NEUROSURGERY VARMA' + } + } + end + + let(:serialized_hash_response) do + { + data: [appointment1] + } + end + + it 'returns a serialized hash' do + serializer = subject.new(video_appt_struct_data) + expect(serializer.serializable_hash).to eq(serialized_hash_response) + end + end + + context 'for valid vaos video appointment data at home' do + let(:vaos_video_appointment_data) do + '{ + "data": [ + { + "id": "180770", + "identifier": [ + { + "system": "Appointment/", + "value": "413938333130383736" + } + ], + "kind": "telehealth", + "status": "booked", + "serviceType": "amputation", + "patientIcn": "1013125218V696863", + "locationId": "983GC", + "clinic": "1081", + "start": "2023-11-13T16:00:00Z", + "end": "2023-11-13T16:30:00Z", + "minutesDuration": 30, + "telehealth": { + "url": "https://pexip.mapsandbox.net/vvc-app/?join=1&media=1&escalate=1&userType=guest&conference=VAC000056916@pexip.mapsandbox.net&pin=351792&aid=0703f6b2-a033-489a-bf54-1162e6f3019d#", + "group": true, + "vvsKind": "ADHOC" + }, + "extension": { + "ccLocation": { + "address": {} + }, + "vistaStatus": [], + "patientHasMobileGfe": false + }, + "facility": { + "name": "def facility", + "vistaSite": "909", + "timezone": { "timeZoneId": "America/New York" }, + "phone": { "main": "843-577-5011" } + }, + "clinicInfo":{ + "data": { + "serviceName": "CaregiverSupport", + "physicalLocation": "2360 East Pershing Boulevard", + "friendlyName": "CaregiverSupport" + } + } + } + ] + }' + end + let(:video_appt_struct_data) do + struct = JSON.parse(vaos_video_appointment_data, object_class: OpenStruct) + struct.data + end + let(:atlas_struct_data) do + address = OpenStruct.new(streetAddress: '5929 Georgia Ave NW', city: 'Washington', state: 'DC', zipCode: '20011', + country: 'USA', latitutde: 38.961979, longitude: -77.027908, additionalDetails: '') + OpenStruct.new(siteCode: 'VFW-DC-20011-02', confirmationCode: '075041', address:) + end + let(:appointment1) do + { + id: '180770', + type: :appointments, + attributes: { + kind: 'telehealth', + status: 'booked', + serviceType: 'amputation', + locationId: '983GC', + clinic: '1081', + start: '2023-11-13T16:00:00Z', + end: '2023-11-13T16:30:00Z', + minutesDuration: 30, + telehealth: { + vvsKind: 'ADHOC', + atlas: nil + }, + extension: { + preCheckinAllowed: nil, + eCheckinAllowed: nil, + patientHasMobileGfe: false + }, + serviceCategory: nil, + facilityName: 'def facility', + facilityVistaSite: '909', + facilityTimezone: 'America/New York', + facilityPhoneMain: '843-577-5011', + clinicServiceName: 'CaregiverSupport', + clinicPhysicalLocation: '2360 East Pershing Boulevard', + clinicFriendlyName: 'CaregiverSupport' + } + } + end + + let(:serialized_hash_response) do + { + data: [appointment1] + } + end + + it 'returns a serialized hash' do + serializer = subject.new(video_appt_struct_data) + expect(serializer.serializable_hash).to eq(serialized_hash_response) + end + end + + context 'for valid vaos video appointment on GFE' do + let(:vaos_video_appointment_data) do + '{ + "data": [ { "id": "180770", "identifier": [ @@ -47,7 +397,7 @@ "value": "413938333130383736" } ], - "kind": "clinic", + "kind": "telehealth", "status": "booked", "serviceType": "amputation", "patientIcn": "1013125218V696863", @@ -56,6 +406,18 @@ "start": "2023-11-13T16:00:00Z", "end": "2023-11-13T16:30:00Z", "minutesDuration": 30, + "telehealth": { + "url": "https://pexip.mapsandbox.net/vvc-app/?join=1&media=1&escalate=1&userType=guest&conference=VAC000056916@pexip.mapsandbox.net&pin=351792&aid=0703f6b2-a033-489a-bf54-1162e6f3019d#", + "group": true, + "vvsKind": "ADHOC" + }, + "extension": { + "ccLocation": { + "address": {} + }, + "vistaStatus": [], + "patientHasMobileGfe": true + }, "facility": { "name": "def facility", "vistaSite": "909", @@ -67,72 +429,279 @@ "serviceName": "CaregiverSupport", "physicalLocation": "2360 East Pershing Boulevard", "friendlyName": "CaregiverSupport" - } } - } + } + } ] }' - end - let(:appt_struct_data) do - struct = JSON.parse(vaos_appointment_data, object_class: OpenStruct) - struct.data - end - let(:appointment1) do - { - id: '180766', - type: :appointments, - attributes: { - kind: 'clinic', - status: 'booked', - serviceType: 'amputation', - locationId: '983GC', - clinic: '1081', - start: '2023-11-13T16:00:00Z', - end: '2023-11-13T16:30:00Z', - minutesDuration: 30, - facilityName: 'abc facility', - facilityVistaSite: '534', - facilityTimezone: 'America/New York', - facilityPhoneMain: '843-577-5011', - clinicServiceName: 'CHS NEUROSURGERY VARMA', - clinicPhysicalLocation: '1ST FL SPECIALTY MODULE 2', - clinicFriendlyName: 'CHS NEUROSURGERY VARMA' + end + let(:video_appt_struct_data) do + struct = JSON.parse(vaos_video_appointment_data, object_class: OpenStruct) + struct.data + end + let(:atlas_struct_data) do + address = OpenStruct.new(streetAddress: '5929 Georgia Ave NW', city: 'Washington', state: 'DC', zipCode: '20011', + country: 'USA', latitutde: 38.961979, longitude: -77.027908, additionalDetails: '') + OpenStruct.new(siteCode: 'VFW-DC-20011-02', confirmationCode: '075041', address:) + end + + let(:appointment1) do + { + id: '180770', + type: :appointments, + attributes: { + kind: 'telehealth', + status: 'booked', + serviceType: 'amputation', + locationId: '983GC', + clinic: '1081', + start: '2023-11-13T16:00:00Z', + end: '2023-11-13T16:30:00Z', + minutesDuration: 30, + telehealth: { + vvsKind: 'ADHOC', + atlas: nil + }, + extension: { + preCheckinAllowed: nil, + eCheckinAllowed: nil, + patientHasMobileGfe: true + }, + serviceCategory: nil, + facilityName: 'def facility', + facilityVistaSite: '909', + facilityTimezone: 'America/New York', + facilityPhoneMain: '843-577-5011', + clinicServiceName: 'CaregiverSupport', + clinicPhysicalLocation: '2360 East Pershing Boulevard', + clinicFriendlyName: 'CaregiverSupport' + } + } + end + + let(:serialized_hash_response) do + { + data: [appointment1] } - } + end + + it 'returns a serialized hash' do + serializer = subject.new(video_appt_struct_data) + expect(serializer.serializable_hash).to eq(serialized_hash_response) + end end - let(:appointment2) do - { - id: '180770', - type: :appointments, - attributes: { - kind: 'clinic', - status: 'booked', - serviceType: 'amputation', - locationId: '983GC', - clinic: '1081', - start: '2023-11-13T16:00:00Z', - end: '2023-11-13T16:30:00Z', - minutesDuration: 30, - facilityName: 'def facility', - facilityVistaSite: '909', - facilityTimezone: 'America/New York', - facilityPhoneMain: '843-577-5011', - clinicServiceName: 'CaregiverSupport', - clinicPhysicalLocation: '2360 East Pershing Boulevard', - clinicFriendlyName: 'CaregiverSupport' + + context 'for valid vaos video appointment at VA location' do + let(:vaos_video_appointment_data) do + '{ + "data": [ + { + "id": "180770", + "identifier": [ + { + "system": "Appointment/", + "value": "413938333130383736" + } + ], + "kind": "telehealth", + "status": "booked", + "serviceType": "amputation", + "patientIcn": "1013125218V696863", + "locationId": "983GC", + "clinic": "1081", + "start": "2023-11-13T16:00:00Z", + "end": "2023-11-13T16:30:00Z", + "minutesDuration": 30, + "telehealth": { + "url": "https://pexip.mapsandbox.net/vvc-app/?join=1&media=1&escalate=1&userType=guest&conference=VAC000056916@pexip.mapsandbox.net&pin=351792&aid=0703f6b2-a033-489a-bf54-1162e6f3019d#", + "group": true, + "vvsKind": "CLINIC_BASED" + }, + "extension": { + "ccLocation": { + "address": {} + }, + "vistaStatus": [], + "patientHasMobileGfe": false + }, + "facility": { + "name": "def facility", + "vistaSite": "909", + "timezone": { "timeZoneId": "America/New York" }, + "phone": { "main": "843-577-5011" } + }, + "clinicInfo":{ + "data": { + "serviceName": "CaregiverSupport", + "physicalLocation": "2360 East Pershing Boulevard", + "friendlyName": "CaregiverSupport" + } + } + } + ] + }' + end + let(:video_appt_struct_data) do + struct = JSON.parse(vaos_video_appointment_data, object_class: OpenStruct) + struct.data + end + let(:atlas_struct_data) do + address = OpenStruct.new(streetAddress: '5929 Georgia Ave NW', city: 'Washington', state: 'DC', zipCode: '20011', + country: 'USA', latitutde: 38.961979, longitude: -77.027908, additionalDetails: '') + OpenStruct.new(siteCode: 'VFW-DC-20011-02', confirmationCode: '075041', address:) + end + + let(:appointment1) do + { + id: '180770', + type: :appointments, + attributes: { + kind: 'telehealth', + status: 'booked', + serviceType: 'amputation', + locationId: '983GC', + clinic: '1081', + start: '2023-11-13T16:00:00Z', + end: '2023-11-13T16:30:00Z', + minutesDuration: 30, + telehealth: { + vvsKind: 'CLINIC_BASED', + atlas: nil + }, + extension: { + preCheckinAllowed: nil, + eCheckinAllowed: nil, + patientHasMobileGfe: false + }, + serviceCategory: nil, + facilityName: 'def facility', + facilityVistaSite: '909', + facilityTimezone: 'America/New York', + facilityPhoneMain: '843-577-5011', + clinicServiceName: 'CaregiverSupport', + clinicPhysicalLocation: '2360 East Pershing Boulevard', + clinicFriendlyName: 'CaregiverSupport' + } } - } + end + + let(:serialized_hash_response) do + { + data: [appointment1] + } + end + + it 'returns a serialized hash' do + serializer = subject.new(video_appt_struct_data) + expect(serializer.serializable_hash).to eq(serialized_hash_response) + end end - context 'for valid vaos appointment data' do + context 'for valid vaos claim appointment data' do + let(:vaos_claim_appointment_data) do + '{ + "data": [ + { + "id": "180766", + "identifier": [ + { + "system": "Appointment/", + "value": "413938333130383736" + } + ], + "kind": "telehealth", + "status": "booked", + "serviceType": "amputation", + "patientIcn": "1013125218V696863", + "locationId": "983GC", + "clinic": "1081", + "start": "2023-11-13T16:00:00Z", + "end": "2023-11-13T16:30:00Z", + "minutesDuration": 30, + "serviceCategory": [ + { + "coding": [ + { + "system": "http://www.va.gov/Terminology/VistADefinedTerms/409_1", + "code": "REGULAR", + "display": "REGULAR" + } + ], + "text": "COMPENSATION & PENSION" + } + ], + "facility": { + "name": "abc facility", + "vistaSite": "534", + "timezone": { "timeZoneId": "America/New York" }, + "phone": { "main": "843-577-5011" } + }, + "clinicInfo":{ + "data": { + "serviceName": "CHS NEUROSURGERY VARMA", + "physicalLocation": "1ST FL SPECIALTY MODULE 2", + "friendlyName": "CHS NEUROSURGERY VARMA" + } + } + } + ] + }' + end + let(:claim_appt_struct_data) do + struct = JSON.parse(vaos_claim_appointment_data, object_class: OpenStruct) + struct.data + end + let(:atlas_struct_data) do + address = OpenStruct.new(streetAddress: '5929 Georgia Ave NW', city: 'Washington', state: 'DC', zipCode: '20011', + country: 'USA', latitutde: 38.961979, longitude: -77.027908, additionalDetails: '') + OpenStruct.new(siteCode: 'VFW-DC-20011-02', confirmationCode: '075041', address:) + end + let(:appointment1) do + { + id: '180766', + type: :appointments, + attributes: { + kind: 'telehealth', + status: 'booked', + serviceType: 'amputation', + locationId: '983GC', + clinic: '1081', + start: '2023-11-13T16:00:00Z', + end: '2023-11-13T16:30:00Z', + minutesDuration: 30, + telehealth: { + vvsKind: nil, + atlas: nil + }, + extension: { + preCheckinAllowed: nil, + eCheckinAllowed: nil, + patientHasMobileGfe: nil + }, + serviceCategory: [ + { + text: 'COMPENSATION & PENSION' + } + ], + facilityName: 'abc facility', + facilityVistaSite: '534', + facilityTimezone: 'America/New York', + facilityPhoneMain: '843-577-5011', + clinicServiceName: 'CHS NEUROSURGERY VARMA', + clinicPhysicalLocation: '1ST FL SPECIALTY MODULE 2', + clinicFriendlyName: 'CHS NEUROSURGERY VARMA' + } + } + end + let(:serialized_hash_response) do { - data: [appointment1, appointment2] + data: [appointment1] } end it 'returns a serialized hash' do - serializer = subject.new(appt_struct_data) + serializer = subject.new(claim_appt_struct_data) expect(serializer.serializable_hash).to eq(serialized_hash_response) end end From 592b550a6489e6af3c3da424f2b28f2c0cae52dd Mon Sep 17 00:00:00 2001 From: Jerek Shoemaker Date: Fri, 4 Oct 2024 13:00:22 -0500 Subject: [PATCH 09/30] Adding new feature toggle for evidence failure emails (#18749) --- config/features.yml | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/config/features.yml b/config/features.yml index 961c0f1157f..0e4739664d6 100644 --- a/config/features.yml +++ b/config/features.yml @@ -215,6 +215,18 @@ features: actor_type: user description: When enabled, claims status tool uses the new claim phase designs enable_in_development: true + cst_include_ddl_5103_letters: + actor_type: user + description: When enabled, the Download Decision Letters feature includes 5103 letters + enable_in_development: true + cst_include_ddl_boa_letters: + actor_type: user + description: When enabled, the Download Decision Letters feature includes Board of Appeals decision letters + enable_in_development: true + cst_include_ddl_sqd_letters: + actor_type: user + description: When enabled, the Download Decision Letters feature includes Subsequent Development Letters + 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 @@ -227,26 +239,18 @@ features: actor_type: user description: When enabled, claims status tool uses the Lighthouse API for the show endpoint enable_in_development: true - cst_include_ddl_boa_letters: - actor_type: user - description: When enabled, the Download Decision Letters feature includes Board of Appeals decision letters - enable_in_development: true - cst_include_ddl_5103_letters: + cst_send_evidence_failure_emails: actor_type: user - description: When enabled, the Download Decision Letters feature includes 5103 letters + description: When enabled, emails will be sent when evidence uploads from the CST fail enable_in_development: true - cst_include_ddl_sqd_letters: + cst_synchronous_evidence_uploads: actor_type: user - description: When enabled, the Download Decision Letters feature includes Subsequent Development Letters + description: When enabled, claims status tool uses synchronous evidence uploads 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 - cst_synchronous_evidence_uploads: - actor_type: user - description: When enabled, claims status tool uses synchronous evidence uploads - enable_in_development: true coe_access: actor_type: user description: Feature gates the certificate of eligibility application From a7a88a03cf6b78ebe418ddda01088d0907d9dd3a Mon Sep 17 00:00:00 2001 From: Rebecca Tolmach <10993987+rmtolmach@users.noreply.github.com> Date: Fri, 4 Oct 2024 15:35:28 -0400 Subject: [PATCH 10/30] Add redis sidekiq health checker to initializer (#18750) --- config/initializers/sidekiq.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index d312e79341d..62d2d3a8f91 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -8,6 +8,7 @@ require 'sidekiq/set_request_id' require 'sidekiq/set_request_attributes' require 'datadog/statsd' # gem 'dogstatsd-ruby' +require 'admin/redis_health_checker' Rails.application.reloader.to_prepare do Sidekiq::Enterprise.unique! if Rails.env.production? @@ -64,4 +65,5 @@ end Sidekiq.strict_args!(false) + RedisHealthChecker.sidekiq_redis_up if Settings.vsp_environment != 'production' end From 8641a965f9dd766c191cc6bb9089ae9a809157ca Mon Sep 17 00:00:00 2001 From: Tom Harrison Date: Fri, 4 Oct 2024 15:57:01 -0400 Subject: [PATCH 11/30] Remove authorization policy to allow access for LOA1 users (#18690) --- app/controllers/v0/my_va/submission_statuses_controller.rb | 1 - spec/requests/v0/my_va/submission_statuses_spec.rb | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/controllers/v0/my_va/submission_statuses_controller.rb b/app/controllers/v0/my_va/submission_statuses_controller.rb index 27391415c2d..538a413d0fd 100644 --- a/app/controllers/v0/my_va/submission_statuses_controller.rb +++ b/app/controllers/v0/my_va/submission_statuses_controller.rb @@ -7,7 +7,6 @@ module MyVA class SubmissionStatusesController < ApplicationController service_tag 'form-submission-statuses' before_action :controller_enabled? - before_action { authorize :lighthouse, :access? } def show report = Forms::SubmissionStatuses::Report.new( diff --git a/spec/requests/v0/my_va/submission_statuses_spec.rb b/spec/requests/v0/my_va/submission_statuses_spec.rb index 7930affc43d..fd90680fd07 100644 --- a/spec/requests/v0/my_va/submission_statuses_spec.rb +++ b/spec/requests/v0/my_va/submission_statuses_spec.rb @@ -4,7 +4,7 @@ require 'forms/submission_statuses/gateway' RSpec.describe 'V0::MyVA::SubmissionStatuses', type: :request do - let(:user) { build(:user, :loa3, :with_terms_of_use_agreement) } + let(:user) { build(:user, :loa1) } before do sign_in_as(user) From 5d366190c023b2e15be5799c81e2e92d67357688 Mon Sep 17 00:00:00 2001 From: Nathan Burgess Date: Fri, 4 Oct 2024 15:57:48 -0400 Subject: [PATCH 12/30] Overhaul Lighthouse API Upload Provider logging (#18751) * Overhaul Lighthouse API Upload Provider logging Adds an extensive logging paradigm for documenting uploads via the LighthouseSupplementalDocumentUploadProvider. This will track all of the important events during an upload and allow us to easily query and make dashboards in DataDog * Abbreviate docstrings to reduce line count in PR * Fix linting error * Update EVSS spec for changes in shared metrics constants * Update spec/lib/disability_compensation/providers/document_upload/lighthouse_supplemental_document_upload_provider_spec.rb Co-authored-by: Alison Jones --------- Co-authored-by: Alison Jones --- ...e_supplemental_document_upload_provider.rb | 83 ++++++-- .../supplemental_document_upload_provider.rb | 7 +- ...plemental_document_upload_provider_spec.rb | 4 +- ...plemental_document_upload_provider_spec.rb | 190 +++++++++++++----- 4 files changed, 221 insertions(+), 63 deletions(-) diff --git a/lib/disability_compensation/providers/document_upload/lighthouse_supplemental_document_upload_provider.rb b/lib/disability_compensation/providers/document_upload/lighthouse_supplemental_document_upload_provider.rb index 3dc54d34d34..ea79186bd1e 100644 --- a/lib/disability_compensation/providers/document_upload/lighthouse_supplemental_document_upload_provider.rb +++ b/lib/disability_compensation/providers/document_upload/lighthouse_supplemental_document_upload_provider.rb @@ -57,40 +57,99 @@ def validate_upload_document(lighthouse_document) # @param lighthouse_document [LighthouseDocument] # @param file_body [String] def submit_upload_document(lighthouse_document, file_body) + log_upload_attempt api_response = BenefitsDocuments::Form526::UploadSupplementalDocumentService.call(file_body, lighthouse_document) handle_lighthouse_response(api_response) end - def log_upload_failure(error_class, error_message) - StatsD.increment("#{@statsd_metric_prefix}.#{STATSD_PROVIDER_METRIC}.#{STATSD_FAILED_METRIC}") - + # To call in the sidekiq_retries_exhausted block of the including job for DataDog monitoring + # + # @param uploading_job_class [String] the job uploading the document (e.g. UploadBDDInstructions) + # @param error_class [String] the Error class of the exception that exhausted the upload job + # @param error_message [String] the message in the exception that exhausted the upload job + def log_uploading_job_failure(uploading_job_class, error_class, error_message) Rails.logger.error( - 'LighthouseSupplementalDocumentUploadProvider upload failure', + "#{uploading_job_class} LighthouseSupplementalDocumentUploadProvider Failure", { - class: 'LighthouseSupplementalDocumentUploadProvider', + **base_logging_info, + uploading_job_class:, error_class:, error_message: } ) + + StatsD.increment("#{@statsd_metric_prefix}.#{STATSD_PROVIDER_METRIC}.#{STASTD_UPLOAD_JOB_FAILED_METRIC}") end private + def base_logging_info + { + class: 'LighthouseSupplementalDocumentUploadProvider', + submission_id: @form526_submission.submitted_claim_id, + user_uuid: @form526_submission.user_uuid, + va_document_type_code: @va_document_type, + primary_form: 'Form526' + } + end + + def log_upload_attempt + Rails.logger.info('LighthouseSupplementalDocumentUploadProvider upload attempted', base_logging_info) + StatsD.increment("#{@statsd_metric_prefix}.#{STATSD_PROVIDER_METRIC}.#{STATSD_ATTEMPT_METRIC}") + end + + def log_upload_success(lighthouse_request_id) + Rails.logger.info( + 'LighthouseSupplementalDocumentUploadProvider upload successful', + { + **base_logging_info, + lighthouse_request_id: + } + ) + + StatsD.increment("#{@statsd_metric_prefix}.#{STATSD_PROVIDER_METRIC}.#{STATSD_SUCCESS_METRIC}") + end + + # For logging an error response from the Lighthouse Benefits Document API + # + # @param lighthouse_error_response [Hash] parsed JSON response from the Lighthouse API + # this will be an array of errors + def log_upload_failure(lighthouse_error_response) + Rails.logger.error( + 'LighthouseSupplementalDocumentUploadProvider upload failed', + { + **base_logging_info, + lighthouse_error_response: + } + ) + + StatsD.increment("#{@statsd_metric_prefix}.#{STATSD_PROVIDER_METRIC}.#{STATSD_FAILED_METRIC}") + end + + # Processes the response from Lighthouse and logs accordingly. If the upload is successful, creates + # a polling record so we can check on the status of the document after Lighthouse has receieved it + # # @param api_response [Faraday::Response] Lighthouse API response returned from the UploadSupplementalDocumentService def handle_lighthouse_response(api_response) - response_body = api_response.body['data'] + response_body = api_response.body - if response_body['success'] == true && response_body['requestId'] - create_lighthouse_polling_record(response_body['requestId']) - StatsD.increment("#{@statsd_metric_prefix}.#{STATSD_PROVIDER_METRIC}.#{STATSD_SUCCESS_METRIC}") + if lighthouse_success_response?(response_body) + lighthouse_request_id = response_body.dig('data', 'requestId') + create_lighthouse_polling_record(lighthouse_request_id) + log_upload_success(lighthouse_request_id) + else + log_upload_failure(response_body) end end + # @param response_body [JSON] Lighthouse API response returned from the UploadSupplementalDocumentService + def lighthouse_success_response?(response_body) + !response_body['errors'] && response_body.dig('data', 'success') && response_body.dig('data', 'requestId') + end + # Creates a Lighthouse526DocumentUpload polling record # - # @param lighthouse_request_id [String] unique ID Lighthouse provides us in the API response after we - # upload a document. We use this ID in the Form526DocumentUploadPollingJob chron job to check the status - # of the document after Lighthouse has received it. + # @param lighthouse_request_id [String] unique ID Lighthouse provides us in the API response for polling later def create_lighthouse_polling_record(lighthouse_request_id) Lighthouse526DocumentUpload.create!( form526_submission: @form526_submission, diff --git a/lib/disability_compensation/providers/document_upload/supplemental_document_upload_provider.rb b/lib/disability_compensation/providers/document_upload/supplemental_document_upload_provider.rb index baff54efbe5..f1b48ca29c0 100644 --- a/lib/disability_compensation/providers/document_upload/supplemental_document_upload_provider.rb +++ b/lib/disability_compensation/providers/document_upload/supplemental_document_upload_provider.rb @@ -1,9 +1,10 @@ # frozen_string_literal: true module SupplementalDocumentUploadProvider - STATSD_SUCCESS_METRIC = 'success' - STATSD_RETRIED_METRIC = 'retried' - STATSD_FAILED_METRIC = 'failed' + STATSD_ATTEMPT_METRIC = 'upload_attempt' + STATSD_SUCCESS_METRIC = 'upload_success' + STATSD_FAILED_METRIC = 'upload_failure' + STASTD_UPLOAD_JOB_FAILED_METRIC = 'upload_job_failed' def self.raise_not_implemented_error raise NotImplementedError, 'Do not use base module methods. Override this method in implementation class.' diff --git a/spec/lib/disability_compensation/providers/document_upload/evss_supplemental_document_upload_provider_spec.rb b/spec/lib/disability_compensation/providers/document_upload/evss_supplemental_document_upload_provider_spec.rb index 499a7faabe6..8f5be81b986 100644 --- a/spec/lib/disability_compensation/providers/document_upload/evss_supplemental_document_upload_provider_spec.rb +++ b/spec/lib/disability_compensation/providers/document_upload/evss_supplemental_document_upload_provider_spec.rb @@ -77,7 +77,7 @@ .and_return(faraday_response) expect(StatsD).to receive(:increment).with( - 'my_upload_job_prefix.evss_supplemental_document_upload_provider.success' + 'my_upload_job_prefix.evss_supplemental_document_upload_provider.upload_success' ) provider.submit_upload_document(evss_claim_document, file_body) @@ -104,7 +104,7 @@ it 'increments a StatsD failure metric' do expect(StatsD).to receive(:increment).with( - 'my_upload_job_prefix.evss_supplemental_document_upload_provider.failed' + 'my_upload_job_prefix.evss_supplemental_document_upload_provider.upload_failure' ) provider.log_upload_failure(error_class, error_message) end diff --git a/spec/lib/disability_compensation/providers/document_upload/lighthouse_supplemental_document_upload_provider_spec.rb b/spec/lib/disability_compensation/providers/document_upload/lighthouse_supplemental_document_upload_provider_spec.rb index 095f365a501..bacf96e4c10 100644 --- a/spec/lib/disability_compensation/providers/document_upload/lighthouse_supplemental_document_upload_provider_spec.rb +++ b/spec/lib/disability_compensation/providers/document_upload/lighthouse_supplemental_document_upload_provider_spec.rb @@ -31,6 +31,25 @@ ) end + let(:faraday_response) { instance_double(Faraday::Response) } + let(:lighthouse_request_id) { Faker::Number.number(digits: 8) } + + # Mock Lighthouse API response + before do + allow(BenefitsDocuments::Form526::UploadSupplementalDocumentService).to receive(:call) + .with(file_body, lighthouse_document) + .and_return(faraday_response) + + allow(faraday_response).to receive(:body).and_return( + { + 'data' => { + 'success' => true, + 'requestId' => lighthouse_request_id + } + } + ) + end + it_behaves_like 'supplemental document upload provider' describe 'generate_upload_document' do @@ -68,24 +87,6 @@ end describe 'submit_upload_document' do - let(:faraday_response) { instance_double(Faraday::Response) } - let(:lighthouse_request_id) { Faker::Number.number(digits: 8) } - - before do - allow(BenefitsDocuments::Form526::UploadSupplementalDocumentService).to receive(:call) - .with(file_body, lighthouse_document) - .and_return(faraday_response) - - allow(faraday_response).to receive(:body).and_return( - { - 'data' => { - 'success' => true, - 'requestId' => lighthouse_request_id - } - } - ) - end - it 'uploads the document via the UploadSupplementalDocumentService' do expect(BenefitsDocuments::Form526::UploadSupplementalDocumentService).to receive(:call) .with(file_body, lighthouse_document) @@ -93,14 +94,6 @@ provider.submit_upload_document(lighthouse_document, file_body) end - it 'increments a StatsD success metric' do - expect(StatsD).to receive(:increment).with( - 'my_stats_metric_prefix.lighthouse_supplemental_document_upload_provider.success' - ) - - provider.submit_upload_document(lighthouse_document, file_body) - end - it 'creates a pending Lighthouse526DocumentUpload record for the submission so we can poll Lighthouse later' do upload_attributes = { aasm_state: 'pending', @@ -116,42 +109,147 @@ end end - describe 'logging methods' do - # We don't want to generate an actual submission for these tests, - # since submissions have callbacks that log to StatsD and we need to test - # only the metrics in this class - let(:submission) { instance_double(Form526Submission) } - - let(:provider) do - LighthouseSupplementalDocumentUploadProvider.new( - submission, - 'MyUploadingClass', - 'my_stats_metric_prefix' - ) + describe 'events logging' do + context 'when attempting to upload a document' do + before do + allow(BenefitsDocuments::Form526::UploadSupplementalDocumentService).to receive(:call) + allow(provider).to receive(:handle_lighthouse_response) + end + + it 'logs to the Rails logger' do + expect(Rails.logger).to receive(:info).with( + 'LighthouseSupplementalDocumentUploadProvider upload attempted', + { + class: 'LighthouseSupplementalDocumentUploadProvider', + submission_id: submission.submitted_claim_id, + user_uuid: submission.user_uuid, + va_document_type_code: va_document_type, + primary_form: 'Form526' + } + ) + + provider.submit_upload_document(lighthouse_document, file_body) + end + + it 'increments a StatsD attempt metric' do + expect(StatsD).to receive(:increment).with( + 'my_stats_metric_prefix.lighthouse_supplemental_document_upload_provider.upload_attempt' + ) + + provider.submit_upload_document(lighthouse_document, file_body) + end end - describe 'log_upload_failure' do - let(:error_class) { 'StandardError' } - let(:error_message) { 'Something broke' } + context 'when an upload is successful' do + before do + # Skip upload attempt logging + allow(provider).to receive(:log_upload_attempt) + end - it 'increments a StatsD failure metric' do + it 'logs to the Rails logger' do + expect(Rails.logger).to receive(:info).with( + 'LighthouseSupplementalDocumentUploadProvider upload successful', + { + class: 'LighthouseSupplementalDocumentUploadProvider', + submission_id: submission.submitted_claim_id, + user_uuid: submission.user_uuid, + va_document_type_code: va_document_type, + primary_form: 'Form526', + lighthouse_request_id: + } + ) + + provider.submit_upload_document(lighthouse_document, file_body) + end + + it 'increments a StatsD success metric' do expect(StatsD).to receive(:increment).with( - 'my_stats_metric_prefix.lighthouse_supplemental_document_upload_provider.failed' + 'my_stats_metric_prefix.lighthouse_supplemental_document_upload_provider.upload_success' ) - provider.log_upload_failure(error_class, error_message) + + provider.submit_upload_document(lighthouse_document, file_body) + end + end + + context 'when we get a non-200 response from Lighthouse' do + let(:error_response_body) do + # From vcr_cassettes/lighthouse/benefits_claims/documents/lighthouse_form_526_document_upload_400.yml + { + 'errors' => [ + { + 'detail' => 'Something broke', + 'status' => 400, + 'title' => 'Bad Request', + 'instance' => Faker::Internet.uuid + } + ] + } + end + + before do + # Skip upload attempt logging + allow(provider).to receive(:log_upload_attempt) + + allow(BenefitsDocuments::Form526::UploadSupplementalDocumentService).to receive(:call) + .with(file_body, lighthouse_document) + .and_return(faraday_response) + + allow(faraday_response).to receive(:body).and_return(error_response_body) end it 'logs to the Rails logger' do expect(Rails.logger).to receive(:error).with( - 'LighthouseSupplementalDocumentUploadProvider upload failure', + 'LighthouseSupplementalDocumentUploadProvider upload failed', { class: 'LighthouseSupplementalDocumentUploadProvider', + submission_id: submission.submitted_claim_id, + user_uuid: submission.user_uuid, + va_document_type_code: va_document_type, + primary_form: 'Form526', + lighthouse_error_response: error_response_body + } + ) + + provider.submit_upload_document(lighthouse_document, file_body) + end + + it 'increments a StatsD metric' do + expect(StatsD).to receive(:increment).with( + 'my_stats_metric_prefix.lighthouse_supplemental_document_upload_provider.upload_failure' + ) + + provider.submit_upload_document(lighthouse_document, file_body) + end + end + + context 'uploading job failure' do + let(:uploading_job_class) { 'MyUploadJob' } + let(:error_class) { 'StandardError' } + let(:error_message) { 'Something broke' } + + it 'logs to the Rails logger' do + expect(Rails.logger).to receive(:error).with( + "#{uploading_job_class} LighthouseSupplementalDocumentUploadProvider Failure", + { + class: 'LighthouseSupplementalDocumentUploadProvider', + submission_id: submission.submitted_claim_id, + user_uuid: submission.user_uuid, + va_document_type_code: va_document_type, + primary_form: 'Form526', + uploading_job_class:, error_class:, error_message: } ) - provider.log_upload_failure(error_class, error_message) + provider.log_uploading_job_failure(uploading_job_class, error_class, error_message) + end + + it 'increments a StatsD failure metric' do + expect(StatsD).to receive(:increment).with( + 'my_stats_metric_prefix.lighthouse_supplemental_document_upload_provider.upload_job_failed' + ) + provider.log_uploading_job_failure(uploading_job_class, error_class, error_message) end end end From 86985fb74c4b5ccbd66778cd7ebe2e5f88f9c5e4 Mon Sep 17 00:00:00 2001 From: Liz Townsend <72234279+liztownd@users.noreply.github.com> Date: Fri, 4 Oct 2024 14:59:50 -0500 Subject: [PATCH 13/30] Travel Pay/Update query path builder for appointments client (#18747) * dont' use URI gem anymore * remove leading slash --- .../app/services/travel_pay/appointments_client.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/modules/travel_pay/app/services/travel_pay/appointments_client.rb b/modules/travel_pay/app/services/travel_pay/appointments_client.rb index 51d22a8fd50..d2e32a9d039 100644 --- a/modules/travel_pay/app/services/travel_pay/appointments_client.rb +++ b/modules/travel_pay/app/services/travel_pay/appointments_client.rb @@ -23,7 +23,11 @@ def get_all_appointments(veis_token, btsss_token, params = {}) correlation_id = SecureRandom.uuid Rails.logger.debug(message: 'Correlation ID', correlation_id:) - query_path = URI::HTTPS.build(path: '/api/v1.1/appointments', query: params.to_query) + query_path = if params.empty? + 'api/v1.1/appointments' + else + "api/v1.1/appointments?#{params.to_query}" + end connection(server_url: btsss_url).get(query_path) do |req| req.headers['Authorization'] = "Bearer #{veis_token}" From bfd5aadd8bd3e240790a9a1af9b6bce5156f5ad6 Mon Sep 17 00:00:00 2001 From: Liz Townsend <72234279+liztownd@users.noreply.github.com> Date: Fri, 4 Oct 2024 15:13:40 -0500 Subject: [PATCH 14/30] Travel Pay/Create new claim client and service (#18678) * create new claim client and service + tests * add params comment, remove unneeded comment * provide default for required claim name * fix tests --- .../app/services/travel_pay/claims_client.rb | 30 +++++++++++++ .../app/services/travel_pay/claims_service.rb | 19 ++++++++ .../spec/services/claims_client_spec.rb | 26 +++++++++++ .../spec/services/claims_service_spec.rb | 45 +++++++++++++++++++ 4 files changed, 120 insertions(+) diff --git a/modules/travel_pay/app/services/travel_pay/claims_client.rb b/modules/travel_pay/app/services/travel_pay/claims_client.rb index c5b29f49646..e509fb6f78f 100644 --- a/modules/travel_pay/app/services/travel_pay/claims_client.rb +++ b/modules/travel_pay/app/services/travel_pay/claims_client.rb @@ -23,5 +23,35 @@ def get_claims(veis_token, btsss_token) req.headers.merge!(claim_headers) end end + + ## + # HTTP POST call to the BTSSS 'claims' endpoint + # API responds with a new travel pay claim ID + # + # @params { + # "appointmentId": "string", (BTSSS internal appointment ID - uuid) + # "claimName": "string", + # "claimantType": "Veteran" (currently, "Veteran" is the only claimant type supported) + # } + # + # @return claimID => string + # + def create_claim(veis_token, btsss_token, params = {}) + btsss_url = Settings.travel_pay.base_url + correlation_id = SecureRandom.uuid + Rails.logger.debug(message: 'Correlation ID', correlation_id:) + + connection(server_url: btsss_url).post('api/v1.1/claims') do |req| + req.headers['Authorization'] = "Bearer #{veis_token}" + req.headers['BTSSS-Access-Token'] = btsss_token + req.headers['X-Correlation-ID'] = correlation_id + req.headers.merge!(claim_headers) + req.body = { + 'appointmentId' => params['btsss_appt_id'], + 'claimName' => params['claim_name'] || 'Travel reimbursement', + 'claimantType' => params['claimant_type'] || 'Veteran' + }.to_json + end + end end end diff --git a/modules/travel_pay/app/services/travel_pay/claims_service.rb b/modules/travel_pay/app/services/travel_pay/claims_service.rb index 876144748b9..6434482c755 100644 --- a/modules/travel_pay/app/services/travel_pay/claims_service.rb +++ b/modules/travel_pay/app/services/travel_pay/claims_service.rb @@ -36,6 +36,25 @@ def get_claim_by_id(veis_token, btsss_token, claim_id) end end + def create_new_claim(veis_token, btsss_token, params = {}) + # ensure appt ID is the right format, allowing any version + uuid_all_version_format = /^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[89ABCD][0-9A-F]{3}-[0-9A-F]{12}$/i + + unless params['btsss_appt_id'] + raise ArgumentError, + message: 'You must provide a BTSSS appointment ID to create a claim.' + end + + unless uuid_all_version_format.match?(params['btsss_appt_id']) + raise ArgumentError, + message: "Expected BTSSS appointment id to be a valid UUID, got #{params['btsss_appt_id']}." + end + + new_claim_response = client.create_claim(veis_token, btsss_token, params) + + new_claim_response.body + end + private def filter_by_date(date_string, claims) diff --git a/modules/travel_pay/spec/services/claims_client_spec.rb b/modules/travel_pay/spec/services/claims_client_spec.rb index f0195a4dea8..9e0a40563d2 100644 --- a/modules/travel_pay/spec/services/claims_client_spec.rb +++ b/modules/travel_pay/spec/services/claims_client_spec.rb @@ -42,6 +42,7 @@ .and_return('veis_token', 'btsss_token') end + # GET it 'returns response from claims endpoint' do @stubs.get('/api/v1/claims') do [ @@ -89,5 +90,30 @@ expect(actual_claim_ids).to eq(expected_ids) end + + # POST create_claim + it 'returns a claim ID from the claims endpoint' do + claim_id = '3fa85f64-5717-4562-b3fc-2c963f66afa6' + body = { 'appointmentId' => 'fake_btsss_appt_id', 'claimName' => 'SMOC claim', + 'claimantType' => 'Veteran' }.to_json + @stubs.post('/api/v1.1/claims') do + [ + 200, + {}, + { + 'data' => + { + 'claimId' => claim_id + } + } + ] + end + + client = TravelPay::ClaimsClient.new + new_claim_response = client.create_claim('veis_token', 'btsss_token', body) + actual_claim_id = new_claim_response.body['data']['claimId'] + + expect(actual_claim_id).to eq(claim_id) + end end end diff --git a/modules/travel_pay/spec/services/claims_service_spec.rb b/modules/travel_pay/spec/services/claims_service_spec.rb index b5aad5e93f5..82605be1148 100644 --- a/modules/travel_pay/spec/services/claims_service_spec.rb +++ b/modules/travel_pay/spec/services/claims_service_spec.rb @@ -135,4 +135,49 @@ end end end + + context 'create_new_claim' do + let(:user) { build(:user) } + let(:new_claim_data) do + { + 'data' => + { + 'claimId' => '3fa85f64-5717-4562-b3fc-2c963f66afa6' + } + } + end + let(:new_claim_response) do + Faraday::Response.new( + body: new_claim_data + ) + end + + let(:tokens) { %w[veis_token btsss_token] } + + it 'returns a claim ID when passed a valid btsss appt id' do + btsss_appt_id = '73611905-71bf-46ed-b1ec-e790593b8565' + allow_any_instance_of(TravelPay::ClaimsClient) + .to receive(:create_claim) + .with(*tokens, { 'btsss_appt_id' => btsss_appt_id, 'claim_name' => 'SMOC claim' }) + .and_return(new_claim_response) + + service = TravelPay::ClaimsService.new + actual_claim_response = service.create_new_claim(*tokens, + { 'btsss_appt_id' => btsss_appt_id, + 'claim_name' => 'SMOC claim' }) + + expect(actual_claim_response['data']).to equal(new_claim_data['data']) + end + + it 'throws an ArgumentException if btsss_appt_id is invalid format' do + btsss_appt_id = 'this-is-definitely-a-uuid-right' + service = TravelPay::ClaimsService.new + + expect { service.create_new_claim(*tokens, { 'btsss_appt_id' => btsss_appt_id }) } + .to raise_error(ArgumentError, /valid UUID/i) + + expect { service.create_new_claim(*tokens, { 'btsss_appt_id' => nil }) } + .to raise_error(ArgumentError, /must provide/i) + end + end end From ae6c51219a1c4ea27a3671b7655c144ebef87dbb Mon Sep 17 00:00:00 2001 From: Eric Tillberg Date: Fri, 4 Oct 2024 16:27:11 -0400 Subject: [PATCH 15/30] Fix how we query MPI Profile for first name (#18752) --- .../simple_forms_api/notification_email.rb | 26 ++++++++++++------- .../spec/services/notification_email_spec.rb | 3 ++- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/modules/simple_forms_api/app/services/simple_forms_api/notification_email.rb b/modules/simple_forms_api/app/services/simple_forms_api/notification_email.rb index 2f140cd1d16..d75fe2b98c9 100644 --- a/modules/simple_forms_api/app/services/simple_forms_api/notification_email.rb +++ b/modules/simple_forms_api/app/services/simple_forms_api/notification_email.rb @@ -99,6 +99,8 @@ def enqueue_email(at, template_id, data) # async job and we have a UserAccount if user_account data[:personalization]['first_name'] = get_first_name + return if data[:personalization]['first_name'].blank? + VANotify::UserAccountJob.perform_at( at, user_account.id, @@ -107,7 +109,7 @@ def enqueue_email(at, template_id, data) ) # async job and we don't have a UserAccount but form data should include email else - return if data[:email].blank? + return if data[:email].blank? || data[:personalization]['first_name'].blank? VANotify::EmailJob.perform_at( at, @@ -121,6 +123,8 @@ def enqueue_email(at, template_id, data) def send_email_now(template_id, data) # sync job and we have a User if user + return if data[:personalization]['first_name'].blank? + VANotify::EmailJob.perform_async( user.va_profile_email, template_id, @@ -128,7 +132,7 @@ def send_email_now(template_id, data) ) # sync job and form data should include email else - return if data[:email].blank? + return if data[:email].blank? || data[:personalization]['first_name'].blank? VANotify::EmailJob.perform_async( data[:email], @@ -140,17 +144,21 @@ def send_email_now(template_id, data) def get_first_name if user_account - mpi_profile = MPI::Service.new.find_profile_by_identifier(identifier_type: 'ICN', identifier: user_account.icn) - if mpi_profile - raise mpi_profile.error if mpi_profile.error - raise 'First name not found in MPI profile' unless mpi_profile.first_name + mpi_response = MPI::Service.new.find_profile_by_identifier(identifier_type: 'ICN', identifier: user_account.icn) + if mpi_response + error = mpi_response.error + Rails.logger.error('MPI response error', { error: }) if error + + first_name = mpi_response.profile&.given_names&.first + Rails.logger.error('MPI profile missing first_name') unless first_name - mpi_profile.first_name + first_name end elsif user - raise 'First name not found in user profile' unless user.first_name + first_name = user.first_name + Rails.logger.error('First name not found in user profile') unless first_name - user.first_name + first_name end end diff --git a/modules/simple_forms_api/spec/services/notification_email_spec.rb b/modules/simple_forms_api/spec/services/notification_email_spec.rb index 2658e2a2a6c..5e85dafbadb 100644 --- a/modules/simple_forms_api/spec/services/notification_email_spec.rb +++ b/modules/simple_forms_api/spec/services/notification_email_spec.rb @@ -112,7 +112,8 @@ it 'sends the email at the specified time' do time = double - mpi_profile = double(first_name: double, error: nil) + profile = double(given_names: [double]) + mpi_profile = double(profile:, error: nil) allow(VANotify::UserAccountJob).to receive(:perform_at) allow_any_instance_of(MPI::Service).to receive(:find_profile_by_identifier).and_return(mpi_profile) subject = described_class.new(config, notification_type:, user_account:) From 56f38404198c9d2dda0575d6c44ed1620d80a62c Mon Sep 17 00:00:00 2001 From: Liz Townsend <72234279+liztownd@users.noreply.github.com> Date: Fri, 4 Oct 2024 15:43:30 -0500 Subject: [PATCH 16/30] add expense client and service + tests (#18728) --- .../services/travel_pay/expenses_client.rb | 47 +++++++++++ .../services/travel_pay/expenses_service.rb | 23 ++++++ .../spec/services/expenses_client_spec.rb | 53 +++++++++++++ .../spec/services/expenses_service_spec.rb | 77 +++++++++++++++++++ 4 files changed, 200 insertions(+) create mode 100644 modules/travel_pay/app/services/travel_pay/expenses_client.rb create mode 100644 modules/travel_pay/app/services/travel_pay/expenses_service.rb create mode 100644 modules/travel_pay/spec/services/expenses_client_spec.rb create mode 100644 modules/travel_pay/spec/services/expenses_service_spec.rb diff --git a/modules/travel_pay/app/services/travel_pay/expenses_client.rb b/modules/travel_pay/app/services/travel_pay/expenses_client.rb new file mode 100644 index 00000000000..98683cda4f6 --- /dev/null +++ b/modules/travel_pay/app/services/travel_pay/expenses_client.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require 'securerandom' +require_relative './base_client' + +module TravelPay + class ExpensesClient < TravelPay::BaseClient + ## + # HTTP POST call to the BTSSS 'expenses' endpoint to add a new mileage expense + # API responds with an expenseId + # + # @external API params { + # "claimId": "string", // uuid of the claim to attach the expense to + # "dateIncurred": "2024-10-02T14:36:38.043Z", // This is the appointment date-time + # "description": "string", // ?? Not sure what this is or if it is required + # "tripType": "string" // Enum: [ OneWay, RoundTrip, Unspecified ] + # } + # + # @params { + # 'claim_id' => 'string' + # 'appt_date' => 'string' + # 'trip_type' => 'OneWay' | 'RoundTrip' | 'Unspecified' + # 'description' => 'string' + # } + # + # @return expenseId => string + # + def add_mileage_expense(veis_token, btsss_token, params = {}) + btsss_url = Settings.travel_pay.base_url + correlation_id = SecureRandom.uuid + Rails.logger.debug(message: 'Correlation ID', correlation_id:) + + connection(server_url: btsss_url).post('api/v1.1/expenses/mileage') do |req| + req.headers['Authorization'] = "Bearer #{veis_token}" + req.headers['BTSSS-Access-Token'] = btsss_token + req.headers['X-Correlation-ID'] = correlation_id + req.headers.merge!(claim_headers) + req.body = { + 'claimId' => params['claim_id'], + 'dateIncurred' => params['appt_date'], + 'tripType' => params['trip_type'] || 'RoundTrip', # default to Round Trip if not specified + 'description' => params['description'] || 'mileage' # this is required, default to mileage + }.to_json + end + end + end +end diff --git a/modules/travel_pay/app/services/travel_pay/expenses_service.rb b/modules/travel_pay/app/services/travel_pay/expenses_service.rb new file mode 100644 index 00000000000..7573fd5748f --- /dev/null +++ b/modules/travel_pay/app/services/travel_pay/expenses_service.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module TravelPay + class ExpensesService + def add_expense(veis_token, btsss_token, params = {}) + # check for required params (that don't have a default set in the client) + unless params['claim_id'] && params['appt_date'] + raise ArgumentError, + message: 'You must provide a claim ID and appointment date to add an expense.' + end + + new_expense_response = client.add_mileage_expense(veis_token, btsss_token, params) + + new_expense_response.body + end + + private + + def client + TravelPay::ExpensesClient.new + end + end +end diff --git a/modules/travel_pay/spec/services/expenses_client_spec.rb b/modules/travel_pay/spec/services/expenses_client_spec.rb new file mode 100644 index 00000000000..4b8af550da1 --- /dev/null +++ b/modules/travel_pay/spec/services/expenses_client_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe TravelPay::ExpensesClient do + let(:user) { build(:user) } + + before do + @stubs = Faraday::Adapter::Test::Stubs.new + + conn = Faraday.new do |c| + c.adapter(:test, @stubs) + c.response :json + c.request :json + end + + allow_any_instance_of(TravelPay::ExpensesClient).to receive(:connection).and_return(conn) + end + + context '/expenses/mileage' do + before do + allow_any_instance_of(TravelPay::TokenService) + .to receive(:get_tokens) + .and_return('veis_token', 'btsss_token') + end + + # POST add_expense + it 'returns an expenseId from the /expenses/mileage endpoint' do + expense_id = '3fa85f64-5717-4562-b3fc-2c963f66afa6' + @stubs.post('/api/v1.1/expenses/mileage') do + [ + 200, + {}, + { + 'data' => + { + 'expenseId' => expense_id + } + } + ] + end + + client = TravelPay::ExpensesClient.new + new_expense_response = client.add_mileage_expense('veis_token', 'btsss_token', + { 'claimId' => 'fake_claim_id', + 'dateIncurred' => '2024-10-02T14:36:38.043Z', + 'tripType' => 'RoundTrip' }.to_json) + actual_expense_id = new_expense_response.body['data']['expenseId'] + + expect(actual_expense_id).to eq(expense_id) + end + end +end diff --git a/modules/travel_pay/spec/services/expenses_service_spec.rb b/modules/travel_pay/spec/services/expenses_service_spec.rb new file mode 100644 index 00000000000..64eded663a2 --- /dev/null +++ b/modules/travel_pay/spec/services/expenses_service_spec.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'securerandom' + +describe TravelPay::ExpensesService do + context 'add_expense' do + let(:user) { build(:user) } + let(:add_expense_data) do + { + 'data' => + { + 'expenseId' => '3fa85f64-5717-4562-b3fc-2c963f66afa6' + } + } + end + let(:add_expense_response) do + Faraday::Response.new( + body: add_expense_data + ) + end + + let(:tokens) { %w[veis_token btsss_token] } + + context 'add new expense' do + it 'returns an expense ID when passed a valid claim id and appointment date' do + params = { 'claim_id' => '73611905-71bf-46ed-b1ec-e790593b8565', + 'appt_date' => '2024-10-02T14:36:38.043Z', + 'trip_type' => 'RoundTrip', + 'description' => 'this is my description' } + + allow_any_instance_of(TravelPay::ExpensesClient) + .to receive(:add_mileage_expense) + .with(*tokens, params) + .and_return(add_expense_response) + + service = TravelPay::ExpensesService.new + actual_new_expense_response = service.add_expense(*tokens, params) + + expect(actual_new_expense_response['data']).to equal(add_expense_data['data']) + end + + it 'succeeds and returns an expense ID when trip type is not specified' do + params = { 'claim_id' => '73611905-71bf-46ed-b1ec-e790593b8565', + 'appt_date' => '2024-10-02T14:36:38.043Z' } + + allow_any_instance_of(TravelPay::ExpensesClient) + .to receive(:add_mileage_expense) + .with(*tokens, params) + .and_return(add_expense_response) + + service = TravelPay::ExpensesService.new + actual_new_expense_response = service.add_expense(*tokens, params) + + expect(actual_new_expense_response['data']).to equal(add_expense_data['data']) + end + + it 'throws an ArgumentException if not passed the right params' do + service = TravelPay::ExpensesService.new + + expect do + service.add_expense(*tokens, { 'claim_id' => nil, + 'appt_date' => '2024-10-02T14:36:38.043Z', + 'trip_type' => 'OneWay' }) + end + .to raise_error(ArgumentError, /You must provide/i) + + expect do + service.add_expense(*tokens, { 'claim_id' => '73611905-71bf-46ed-b1ec-e790593b8565', + 'appt_date' => nil, + 'trip_type' => 'RoundTrip' }) + end + .to raise_error(ArgumentError, /You must provide/i) + end + end + end +end From 0d165aa8c3b8dc8f63650e7d06c7f7b4e89f97f1 Mon Sep 17 00:00:00 2001 From: Eric Tillberg Date: Sat, 5 Oct 2024 11:35:10 -0400 Subject: [PATCH 17/30] Add logging when we're about to send a failure email (#18704) --- app/models/form_submission_attempt.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/models/form_submission_attempt.rb b/app/models/form_submission_attempt.rb index a456549b9f5..7ce20e62fa6 100644 --- a/app/models/form_submission_attempt.rb +++ b/app/models/form_submission_attempt.rb @@ -25,6 +25,12 @@ class FormSubmissionAttempt < ApplicationRecord event :fail do after do + Rails.logger.info({ + message: 'Preparing to send Form Submission Attempt error email', + form_submission_id:, + benefits_intake_uuid: form_submission&.benefits_intake_uuid, + form_type: form_submission&.form_type + }) enqueue_result_email(:error) if Flipper.enabled?(:simple_forms_email_notifications) end From bffd515094995e58e243759e27a60846eb8cb6a7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 10:00:56 +0000 Subject: [PATCH 18/30] Bump jwt from 2.8.2 to 2.9.3 Bumps [jwt](https://github.com/jwt/ruby-jwt) from 2.8.2 to 2.9.3. - [Release notes](https://github.com/jwt/ruby-jwt/releases) - [Changelog](https://github.com/jwt/ruby-jwt/blob/main/CHANGELOG.md) - [Commits](https://github.com/jwt/ruby-jwt/compare/v2.8.2...v2.9.3) --- updated-dependencies: - dependency-name: jwt dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 236ed4414ed..1c09a711155 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -600,7 +600,7 @@ GEM jsonapi-serializer (2.2.0) activesupport (>= 4.2) jwe (0.4.0) - jwt (2.8.2) + jwt (2.9.3) base64 kms_encrypted (1.6.0) activesupport (>= 6.1) From 29bc72eaeeb28373335bde6a193192e6256d72d4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 09:59:52 +0000 Subject: [PATCH 19/30] Bump flipper-active_support_cache_store from 1.3.0 to 1.3.1 Bumps [flipper-active_support_cache_store](https://github.com/flippercloud/flipper) from 1.3.0 to 1.3.1. - [Release notes](https://github.com/flippercloud/flipper/releases) - [Changelog](https://github.com/flippercloud/flipper/blob/main/Changelog.md) - [Commits](https://github.com/flippercloud/flipper/compare/v1.3.0...v1.3.1) --- updated-dependencies: - dependency-name: flipper-active_support_cache_store dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 1c09a711155..aa4f477413c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -466,9 +466,9 @@ GEM flipper-active_record (1.3.1) activerecord (>= 4.2, < 8) flipper (~> 1.3.1) - flipper-active_support_cache_store (1.3.0) + flipper-active_support_cache_store (1.3.1) activesupport (>= 4.2, < 8) - flipper (~> 1.3.0) + flipper (~> 1.3.1) flipper-ui (1.3.1) erubi (>= 1.0.0, < 2.0.0) flipper (~> 1.3.1) From f1ac3e2fdeca668a56e3a2a16178da20cfb1fb1d Mon Sep 17 00:00:00 2001 From: Bryan Alexander Date: Mon, 7 Oct 2024 07:33:20 -0400 Subject: [PATCH 20/30] =?UTF-8?q?86426:=20Add=20logic=20to=20Sidekiq=20job?= =?UTF-8?q?=20to=20avoid=20duplicating=20logic=20if=20pending=E2=80=A6=20(?= =?UTF-8?q?#18733)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 86426: Add logic to Sidekiq job to avoid duplicating logic if pending or success submission attempt * 86426: Add test for FormSubmission model * 86426: Alter guard statement to not care about intake.uuid * 86426: Update method doc --- app/models/form_submission.rb | 4 ++ .../pensions/pension_benefit_intake_job.rb | 14 +++++++ .../pensions/spec/factories/pension_claim.rb | 18 +++++++++ .../pension_benefit_intake_job_spec.rb | 39 ++++++++++++++++++- spec/models/form_submission_spec.rb | 16 ++++++++ 5 files changed, 90 insertions(+), 1 deletion(-) diff --git a/app/models/form_submission.rb b/app/models/form_submission.rb index f229d474726..c531f909713 100644 --- a/app/models/form_submission.rb +++ b/app/models/form_submission.rb @@ -54,4 +54,8 @@ def with_form_types(form_types) def latest_pending_attempt form_submission_attempts.where(aasm_state: 'pending').order(created_at: :asc).last end + + def non_failure_attempt + form_submission_attempts.where(aasm_state: %w[pending success]).first + end end diff --git a/modules/pensions/app/sidekiq/pensions/pension_benefit_intake_job.rb b/modules/pensions/app/sidekiq/pensions/pension_benefit_intake_job.rb index 9610bc7f851..b4d1dea6296 100644 --- a/modules/pensions/app/sidekiq/pensions/pension_benefit_intake_job.rb +++ b/modules/pensions/app/sidekiq/pensions/pension_benefit_intake_job.rb @@ -52,6 +52,8 @@ class PensionBenefitIntakeError < StandardError; end def perform(saved_claim_id, user_account_uuid = nil) init(saved_claim_id, user_account_uuid) + return if form_submission_pending_or_success + # generate and validate claim pdf documents @form_path = process_document(@claim.to_pdf) @attachment_paths = @claim.persistent_attachments.map { |pa| process_document(pa.to_pdf) } @@ -96,6 +98,18 @@ def init(saved_claim_id, user_account_uuid) @intake_service = BenefitsIntake::Service.new end + ## + # Check FormSubmissionAttempts for record with 'pending' or 'success' + # + # @return true if FormSubmissionAttempt has 'pending' or 'success' + # @return false if unable to find a FormSubmission or FormSubmissionAttempt not 'pending' or 'success' + # + def form_submission_pending_or_success + @claim&.form_submissions&.any? do |form_submission| + form_submission.non_failure_attempt.present? + end || false + end + ## # Create a temp stamped PDF and validate the PDF satisfies Benefits Intake specification # diff --git a/modules/pensions/spec/factories/pension_claim.rb b/modules/pensions/spec/factories/pension_claim.rb index d709d3771f7..d4d272297f6 100644 --- a/modules/pensions/spec/factories/pension_claim.rb +++ b/modules/pensions/spec/factories/pension_claim.rb @@ -23,5 +23,23 @@ statementOfTruthSignature: 'Test User' }.to_json end + + trait :pending do + after(:create) do |pension_claim| + create(:form_submission, :pending, saved_claim_id: pension_claim.id) + end + end + + trait :success do + after(:create) do |pension_claim| + create(:form_submission, :success, saved_claim_id: pension_claim.id) + end + end + + trait :failure do + after(:create) do |pension_claim| + create(:form_submission, :failure, saved_claim_id: pension_claim.id) + end + end end end diff --git a/modules/pensions/spec/sidekiq/pensions/pension_benefit_intake_job_spec.rb b/modules/pensions/spec/sidekiq/pensions/pension_benefit_intake_job_spec.rb index 2a292dfea7a..c87c811211d 100644 --- a/modules/pensions/spec/sidekiq/pensions/pension_benefit_intake_job_spec.rb +++ b/modules/pensions/spec/sidekiq/pensions/pension_benefit_intake_job_spec.rb @@ -37,7 +37,7 @@ end it 'submits the saved claim successfully' do - allow(job).to receive(:process_document).and_return(pdf_path) + allow(job).to receive_messages(process_document: pdf_path, form_submission_pending_or_success: false) expect(FormSubmission).to receive(:create) expect(FormSubmissionAttempt).to receive(:create) @@ -84,6 +84,43 @@ # perform end + describe '#form_submission_pending_or_success' do + before do + job.instance_variable_set(:@claim, claim) + allow(Pensions::SavedClaim).to receive(:find).and_return(claim) + end + + context 'with no form submissions' do + it 'returns false' do + expect(job.send(:form_submission_pending_or_success)).to eq(false).or be_nil + end + end + + context 'with pending form submission attempt' do + let(:claim) { create(:pensions_module_pension_claim, :pending) } + + it 'return true' do + expect(job.send(:form_submission_pending_or_success)).to eq(true) + end + end + + context 'with success form submission attempt' do + let(:claim) { create(:pensions_module_pension_claim, :success) } + + it 'return true' do + expect(job.send(:form_submission_pending_or_success)).to eq(true) + end + end + + context 'with failure form submission attempt' do + let(:claim) { create(:pensions_module_pension_claim, :failure) } + + it 'return false' do + expect(job.send(:form_submission_pending_or_success)).to eq(false) + end + end + end + describe '#process_document' do let(:service) { double('service') } let(:pdf_path) { 'random/path/to/pdf' } diff --git a/spec/models/form_submission_spec.rb b/spec/models/form_submission_spec.rb index 536790ca091..f5e728b8a62 100644 --- a/spec/models/form_submission_spec.rb +++ b/spec/models/form_submission_spec.rb @@ -86,5 +86,21 @@ expect(results.count).to eq(3) end end + + context 'latest_pending_attempt' do + it 'returns db record' do + form_submission = FormSubmission.with_form_types(nil).first + + expect(form_submission.latest_pending_attempt).not_to be_nil + end + end + + context 'non_failure_attempt' do + it 'returns db record' do + form_submission = FormSubmission.with_form_types(nil).first + + expect(form_submission.non_failure_attempt).not_to be_nil + end + end end end From e0aad47d763abe430d22b80f434c9c270e1d4bf9 Mon Sep 17 00:00:00 2001 From: Jennica Stiehl <25069483+stiehlrod@users.noreply.github.com> Date: Mon, 7 Oct 2024 07:15:37 -0600 Subject: [PATCH 21/30] API-40501-sync-errors-nightly-report (#18712) * WIP: Adds va_gov to the nightly report. * Solution 1: using transaction id to group report GUIDs. * Adds logic to alias transaction_ids as transaction_groups. Sets up preview. Adjusts tests to new format. Adds tests to check new format. * Fixes formatting on preview values. * Linting * Updates hash syntax to ruby 1.9 * more linting; rubocop seems to be on strike * llinting * Linting --- .../claims_api/unsuccessful_report_mailer.rb | 1 + .../report_unsuccessful_submissions.rb | 1 + .../app/sidekiq/claims_api/reporting_base.rb | 37 ++++++++++++++++++- .../_submission_grouped_table.html.erb | 17 +++++++++ .../claims_api/_submission_table.html.erb | 2 +- .../unsuccessful_report.html.erb | 20 ++++++---- .../spec/factories/auto_established_claims.rb | 21 ++++++++++- .../unsuccessful_report_mailer_spec.rb | 1 + .../report_unsuccessful_submissions_spec.rb | 9 +++-- .../sidekiq/shared_reporting_examples_spec.rb | 19 ++++++++++ ..._api_unsuccessful_report_mailer_preview.rb | 10 +++++ 11 files changed, 123 insertions(+), 15 deletions(-) create mode 100644 modules/claims_api/app/views/claims_api/_submission_grouped_table.html.erb diff --git a/modules/claims_api/app/mailers/claims_api/unsuccessful_report_mailer.rb b/modules/claims_api/app/mailers/claims_api/unsuccessful_report_mailer.rb index e18b5fc4f09..ad66831d230 100644 --- a/modules/claims_api/app/mailers/claims_api/unsuccessful_report_mailer.rb +++ b/modules/claims_api/app/mailers/claims_api/unsuccessful_report_mailer.rb @@ -23,6 +23,7 @@ def build(date_from, date_to, data) @date_to = date_to.in_time_zone('Eastern Time (US & Canada)').strftime('%a %D %I:%M %p') @consumer_claims_totals = data[:consumer_claims_totals] @unsuccessful_claims_submissions = data[:unsuccessful_claims_submissions] + @unsuccessful_va_gov_claims_submissions = data[:unsuccessful_va_gov_claims_submissions] @poa_totals = data[:poa_totals] @unsuccessful_poa_submissions = data[:unsuccessful_poa_submissions] @itf_totals = data[:itf_totals] diff --git a/modules/claims_api/app/sidekiq/claims_api/report_unsuccessful_submissions.rb b/modules/claims_api/app/sidekiq/claims_api/report_unsuccessful_submissions.rb index 4006f6897e3..7ae0321dd38 100644 --- a/modules/claims_api/app/sidekiq/claims_api/report_unsuccessful_submissions.rb +++ b/modules/claims_api/app/sidekiq/claims_api/report_unsuccessful_submissions.rb @@ -16,6 +16,7 @@ def perform @to, consumer_claims_totals: claims_totals, unsuccessful_claims_submissions:, + unsuccessful_va_gov_claims_submissions:, poa_totals:, unsuccessful_poa_submissions:, itf_totals:, diff --git a/modules/claims_api/app/sidekiq/claims_api/reporting_base.rb b/modules/claims_api/app/sidekiq/claims_api/reporting_base.rb index 86df9d9a253..eb232133ebf 100644 --- a/modules/claims_api/app/sidekiq/claims_api/reporting_base.rb +++ b/modules/claims_api/app/sidekiq/claims_api/reporting_base.rb @@ -12,11 +12,25 @@ def unsuccessful_claims_submissions def errored_claims ClaimsApi::AutoEstablishedClaim.where( - created_at: @from..@to, - status: %w[errored] + 'status = ? AND created_at BETWEEN ? AND ? AND cid <> ?', + 'errored', @from, @to, '0oagdm49ygCSJTp8X297' ).order(:cid, :status) end + def unsuccessful_va_gov_claims_submissions + arr = errored_va_gov_claims.pluck(:transaction_id, :id).map do |transaction_id, id| + { transaction_id:, id: } + end + map_transaction_ids(arr) if arr.count > 1 + end + + def errored_va_gov_claims + ClaimsApi::AutoEstablishedClaim.where(created_at: @from..@to, + status: 'errored', cid: '0oagdm49ygCSJTp8X297') + .group(:id) + .order(:transaction_id) + end + def with_special_issues(cid: nil) claims = ClaimsApi::AutoEstablishedClaim.where(created_at: @from..@to) claims = claims.where(cid:) if cid.present? @@ -121,5 +135,24 @@ def errored_ews status: %w[errored] ).order(:cid, :status) end + + def map_transaction_ids(array) + # Dynamically generate unique keys like A, B, C, etc. + transaction_mapping = {} + key_sequence = ('A'..'Z').to_a + key_index = 0 + + # Map each unique transaction_id to a new key + array.each do |item| + transaction_id = item[:transaction_id] + unless transaction_mapping.key?(transaction_id) + transaction_mapping[transaction_id] = key_sequence[key_index] + key_index += 1 + end + end + + # Group the array by the new keys + array.group_by { |item| transaction_mapping[item[:transaction_id]] } + end end end diff --git a/modules/claims_api/app/views/claims_api/_submission_grouped_table.html.erb b/modules/claims_api/app/views/claims_api/_submission_grouped_table.html.erb new file mode 100644 index 00000000000..8e9c848328e --- /dev/null +++ b/modules/claims_api/app/views/claims_api/_submission_grouped_table.html.erb @@ -0,0 +1,17 @@ + + + + + + + + + <% claims&.each_with_index do |grps, idx| %> + <% grps[1]&.each do |arr| %> + + + + <% end %> + <% end %> + +
Transaction GroupGUID
<%= grps[0] %><%= arr[:id] %>
diff --git a/modules/claims_api/app/views/claims_api/_submission_table.html.erb b/modules/claims_api/app/views/claims_api/_submission_table.html.erb index 6293e642f56..8dc4752bd9d 100644 --- a/modules/claims_api/app/views/claims_api/_submission_table.html.erb +++ b/modules/claims_api/app/views/claims_api/_submission_table.html.erb @@ -6,7 +6,7 @@ - <% claims.each do |claim| %> + <% claims&.each do |claim| %> <%= claim[:created_at] %> <%= claim[:id] %> diff --git a/modules/claims_api/app/views/claims_api/unsuccessful_report_mailer/unsuccessful_report.html.erb b/modules/claims_api/app/views/claims_api/unsuccessful_report_mailer/unsuccessful_report.html.erb index 5fd9a0a1ac2..b9a391d0b59 100644 --- a/modules/claims_api/app/views/claims_api/unsuccessful_report_mailer/unsuccessful_report.html.erb +++ b/modules/claims_api/app/views/claims_api/unsuccessful_report_mailer/unsuccessful_report.html.erb @@ -52,13 +52,19 @@

526EZ Claim Submissions

Per Consumer Status Counts

- <%= render partial: 'claims_api/claims_status_table', locals: {claims_consumers: @consumer_claims_totals } unless @consumer_claims_totals.count.zero? %> + <%= render partial: 'claims_api/claims_status_table', locals: {claims_consumers: @consumer_claims_totals } unless @consumer_claims_totals&.count&.zero? %>

- <%= @unsuccessful_claims_submissions.count %> + <%= @unsuccessful_claims_submissions&.count %> 526 Errored Submissions

- <%= render partial: 'claims_api/submission_table', locals: { claims: @unsuccessful_claims_submissions } unless @unsuccessful_claims_submissions.count.zero? %> + <%= render partial: 'claims_api/submission_table', locals: { claims: @unsuccessful_claims_submissions } unless @unsuccessful_claims_submissions&.count&.zero? %> + +

+ <%= @unsuccessful_va_gov_claims_submissions&.count%> + 526 VA GOV Errored Submissions +

+ <%= render partial: 'claims_api/submission_grouped_table', locals: { claims: @unsuccessful_va_gov_claims_submissions } unless @unsuccessful_va_gov_claims_submissions&.count&.zero? %>

Power of Attorney Submissions

@@ -67,10 +73,10 @@ <%= render partial: 'claims_api/poa_status_table', locals: { poa_consumers: @poa_totals } unless @poa_totals.blank? %>

- <%= @unsuccessful_poa_submissions.count %> + <%= @unsuccessful_poa_submissions&.count %> POA Errored Submissions

- <%= render partial: 'claims_api/poa_errors_table', locals: { poa_errors: @unsuccessful_poa_submissions } unless @unsuccessful_poa_submissions.count.zero? %> + <%= render partial: 'claims_api/poa_errors_table', locals: { poa_errors: @unsuccessful_poa_submissions } unless @unsuccessful_poa_submissions&.count&.zero? %>

Evidence Waiver Submissions

@@ -79,10 +85,10 @@ <%= render partial: 'claims_api/ews_status_table', locals: { ews_consumers: @ews_totals } unless @ews_totals.blank? %>

- <%= @unsuccessful_evidence_waiver_submissions.count %> + <%= @unsuccessful_evidence_waiver_submissions&.count %> EWS Errored Submissions

- <%= render partial: 'claims_api/ews_errors_table', locals: { ews_errors: @unsuccessful_evidence_waiver_submissions } unless @unsuccessful_evidence_waiver_submissions.count.zero? %> + <%= render partial: 'claims_api/ews_errors_table', locals: { ews_errors: @unsuccessful_evidence_waiver_submissions } unless @unsuccessful_evidence_waiver_submissions&.count&.zero? %>
diff --git a/modules/claims_api/spec/factories/auto_established_claims.rb b/modules/claims_api/spec/factories/auto_established_claims.rb index 307e0db3264..7884b3698c5 100644 --- a/modules/claims_api/spec/factories/auto_established_claims.rb +++ b/modules/claims_api/spec/factories/auto_established_claims.rb @@ -53,6 +53,26 @@ end end + factory :auto_established_claim_va_gov, class: 'ClaimsApi::AutoEstablishedClaim' do + id { SecureRandom.uuid } + evss_id { nil } + auth_headers { { test: ('a'..'z').to_a.shuffle.join } } + form_data do + # rubocop:disable Layout/LineLength + json = JSON.parse(File + .read(::Rails.root.join(*'/modules/claims_api/spec/fixtures/form_526_no_flashes_no_special_issues.json'.split('/')).to_s)) + json['data']['attributes'] + end + cid { '0oagdm49ygCSJTp8X297' } + transaction_id { Faker::Number.number(digits: 20) } + created_at { Faker::Date.between(from: 1.day.ago, to: Time.zone.now) } + status { ClaimsApi::AutoEstablishedClaim::ERRORED } + end + + trait :set_transaction_id do + transaction_id { '25' } + end + factory :auto_established_claim_without_flashes_or_special_issues, class: 'ClaimsApi::AutoEstablishedClaim' do id { SecureRandom.uuid } status { 'pending' } @@ -60,7 +80,6 @@ evss_id { nil } auth_headers { { test: ('a'..'z').to_a.shuffle.join } } form_data do - # rubocop:disable Layout/LineLength json = JSON.parse(File .read(::Rails.root.join(*'/modules/claims_api/spec/fixtures/form_526_no_flashes_no_special_issues.json'.split('/')).to_s)) json['data']['attributes'] diff --git a/modules/claims_api/spec/mailers/unsuccessful_report_mailer_spec.rb b/modules/claims_api/spec/mailers/unsuccessful_report_mailer_spec.rb index d3a5c7ef41a..e0abb7ded96 100644 --- a/modules/claims_api/spec/mailers/unsuccessful_report_mailer_spec.rb +++ b/modules/claims_api/spec/mailers/unsuccessful_report_mailer_spec.rb @@ -7,6 +7,7 @@ subject do described_class.build(1.day.ago, Time.zone.now, consumer_claims_totals: [], unsuccessful_claims_submissions: [], + unsuccessful_va_gov_claims_submissions: [], poa_totals: [], unsuccessful_poa_submissions: [], ews_totals: [], diff --git a/modules/claims_api/spec/sidekiq/report_unsuccessful_submissions_spec.rb b/modules/claims_api/spec/sidekiq/report_unsuccessful_submissions_spec.rb index 5307b99b6f0..ab0b5c6e643 100644 --- a/modules/claims_api/spec/sidekiq/report_unsuccessful_submissions_spec.rb +++ b/modules/claims_api/spec/sidekiq/report_unsuccessful_submissions_spec.rb @@ -28,10 +28,11 @@ from, to, consumer_claims_totals: [], - unsuccessful_claims_submissions: ClaimsApi::AutoEstablishedClaim.where(created_at: from..to, - status: 'errored') - .order(:cid, :status) - .pluck(:cid, :status, :id), + unsuccessful_claims_submissions: ClaimsApi::AutoEstablishedClaim.where( + 'status = ? AND created_at BETWEEN ? AND ? AND cid <> ?', + 'errored', @from, @to, '0oagdm49ygCSJTp8X297' + ).order(:cid, :status).pluck(:cid, :status, :id), + unsuccessful_va_gov_claims_submissions: nil, poa_totals: [], unsuccessful_poa_submissions: [], ews_totals: [], diff --git a/modules/claims_api/spec/sidekiq/shared_reporting_examples_spec.rb b/modules/claims_api/spec/sidekiq/shared_reporting_examples_spec.rb index 072619c55d0..9e7ea744240 100644 --- a/modules/claims_api/spec/sidekiq/shared_reporting_examples_spec.rb +++ b/modules/claims_api/spec/sidekiq/shared_reporting_examples_spec.rb @@ -57,4 +57,23 @@ expect(itf_totals[1]['VA Connect Pro'][:totals]).to eq(2) end end + + it 'includes 526EZ claims from VaGov' do + with_settings(Settings.claims_api, report_enabled: true) do + create(:auto_established_claim_va_gov, created_at: Time.zone.now).freeze + create(:auto_established_claim_va_gov, created_at: Time.zone.now).freeze + create(:auto_established_claim_va_gov, :set_transaction_id, created_at: Time.zone.now).freeze + create(:auto_established_claim_va_gov, :set_transaction_id, created_at: Time.zone.now).freeze + + job = described_class.new + job.perform + va_gov_groups = job.unsuccessful_va_gov_claims_submissions + + expect(va_gov_groups).to include('A') + expect(va_gov_groups).to include('B') + expect(va_gov_groups).to include('C') + expect(va_gov_groups['A'][0][:transaction_id]).to be_a(String) + expect(va_gov_groups['A'][0][:id]).to be_a(String) + end + end end diff --git a/spec/mailers/previews/claims_api_unsuccessful_report_mailer_preview.rb b/spec/mailers/previews/claims_api_unsuccessful_report_mailer_preview.rb index 1f97d006195..55453d60efc 100644 --- a/spec/mailers/previews/claims_api_unsuccessful_report_mailer_preview.rb +++ b/spec/mailers/previews/claims_api_unsuccessful_report_mailer_preview.rb @@ -10,6 +10,7 @@ def build to, consumer_claims_totals: claims_totals, unsuccessful_claims_submissions:, + unsuccessful_va_gov_claims_submissions:, poa_totals:, unsuccessful_poa_submissions:, ews_totals:, @@ -26,6 +27,15 @@ def unsuccessful_claims_submissions ] end + def unsuccessful_va_gov_claims_submissions + { + A: [{ transaction_id: '13259605526122682833', id: '82664de8-b3de-4e6f-aec1-8da32287f42f' }], + B: [{ transaction_id: '25', id: '30de2023-c86f-448d-a5d8-c129d9db1175' }, + { transaction_id: '25', id: 'd4acf34d-5bb8-42fc-9b1d-55d5ef4040e6' }], + C: [{ transaction_id: '33282616173397531367', id: '92a8f4c6-e1a7-435a-8134-921ed1548f45' }] + } + end + def claims_totals [ { 'consumer 1' => { pending: 2, From 943b957d1d1142bc9e3f7d01fd2efe8553ca68e0 Mon Sep 17 00:00:00 2001 From: Eric Tillberg Date: Mon, 7 Oct 2024 09:19:07 -0400 Subject: [PATCH 22/30] Handle nil form_data with a sensible default, needed for JSON parsing (#18758) --- app/models/form_submission.rb | 4 ++++ spec/models/form_submission_spec.rb | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/app/models/form_submission.rb b/app/models/form_submission.rb index c531f909713..e7e93902cb1 100644 --- a/app/models/form_submission.rb +++ b/app/models/form_submission.rb @@ -51,6 +51,10 @@ def with_form_types(form_types) end end + def form_data + super || '{}' + end + def latest_pending_attempt form_submission_attempts.where(aasm_state: 'pending').order(created_at: :asc).last end diff --git a/spec/models/form_submission_spec.rb b/spec/models/form_submission_spec.rb index f5e728b8a62..751650b9afe 100644 --- a/spec/models/form_submission_spec.rb +++ b/spec/models/form_submission_spec.rb @@ -14,6 +14,14 @@ it { is_expected.to validate_presence_of(:form_type) } end + describe '#form_data' do + it 'defaults to an empty hash in a string' do + form_submission = create(:form_submission, form_data: nil) + + expect(form_submission.form_data).to eq '{}' + end + end + describe 'user form submission statuses' do before do @fsa, @fsb, @fsc = create_list(:form_submission, 3, user_account:) From f14bf330c4045d850303f303197ed56e324965cf Mon Sep 17 00:00:00 2001 From: Rockwell Windsor Rice <129893414+rockwellwindsor-va@users.noreply.github.com> Date: Mon, 7 Oct 2024 08:27:34 -0500 Subject: [PATCH 23/30] API-40626-errored-claims-show-example (#18737) * Adds example for a claim that get saccepted but fails in a background job * Comples Swagger docs for dev and prod with new example for claims show modified: modules/claims_api/app/swagger/claims_api/v2/dev/swagger.json modified: modules/claims_api/app/swagger/claims_api/v2/production/swagger.json modified: modules/claims_api/spec/requests/v2/veterans/rswag_claims_spec.rb --- .../swagger/claims_api/v2/dev/swagger.json | 342 +++++++++++------- .../claims_api/v2/production/swagger.json | 342 +++++++++++------- .../requests/v2/veterans/rswag_claims_spec.rb | 89 ++++- 3 files changed, 516 insertions(+), 257 deletions(-) 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 6f747ed93a0..38cef54fca6 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 @@ -5077,7 +5077,7 @@ "202 without a transactionId": { "value": { "data": { - "id": "eb843fc3-8604-46ae-a08d-c60ce6bd9d9b", + "id": "842d260d-1ffe-4a29-b5ed-117b38bd3ad5", "type": "forms/526", "attributes": { "claimId": "600442191", @@ -5262,7 +5262,7 @@ }, "federalActivation": { "activationDate": "2023-10-01", - "anticipatedSeparationDate": "2024-10-02" + "anticipatedSeparationDate": "2024-10-05" }, "confinements": [ { @@ -5308,7 +5308,7 @@ "202 with a transactionId": { "value": { "data": { - "id": "f05ebeae-33e2-42d7-8a2c-b2e293ea3d3a", + "id": "9580281a-95bc-4a99-8171-10b987a1b887", "type": "forms/526", "attributes": { "claimId": "600442191", @@ -10526,7 +10526,7 @@ "application/json": { "example": { "data": { - "id": "b482d045-7bdc-44c9-9729-435c9518ba34", + "id": "854bc725-fa9e-438e-bc7f-9a484160cf30", "type": "forms/526", "attributes": { "claimProcessType": "STANDARD_CLAIM_PROCESS", @@ -13269,127 +13269,176 @@ ], "responses": { "200": { - "description": "claim response", + "description": "errored claim response", "content": { "application/json": { - "example": { - "data": { - "id": "555555555", - "type": "claim", - "attributes": { - "claimTypeCode": "400PREDSCHRG", - "claimDate": "2017-05-02", - "claimPhaseDates": { - "phaseChangeDate": "2017-10-18", - "currentPhaseBack": false, - "latestPhaseType": "COMPLETE", - "previousPhases": { - "phase7CompleteDate": "2017-10-18" - } - }, - "claimType": "Compensation", - "closeDate": "2017-10-18", - "contentions": [ - { - "name": "abnormal heart (New)" - }, - { - "name": "abscess kidney (New)" - }, - { - "name": "encephalitis lethargica residuals (New)" - }, - { - "name": "dracunculiasis (New)" - }, - { - "name": "gingivitis (New)" - }, - { - "name": "abnormal weight loss (New)" - }, - { - "name": "groin condition (New)" - }, - { - "name": "metritis (New)" - } - ], - "decisionLetterSent": false, - "developmentLetterSent": false, - "documentsNeeded": false, - "endProductCode": "404", - "evidenceWaiverSubmitted5103": false, - "errors": [ + "examples": { + "returns a 200 response for established claim": { + "value": { + "data": { + "id": "555555555", + "type": "claim", + "attributes": { + "claimTypeCode": "400PREDSCHRG", + "claimDate": "2017-05-02", + "claimPhaseDates": { + "phaseChangeDate": "2017-10-18", + "currentPhaseBack": false, + "latestPhaseType": "COMPLETE", + "previousPhases": { + "phase7CompleteDate": "2017-10-18" + } + }, + "claimType": "Compensation", + "closeDate": "2017-10-18", + "contentions": [ + { + "name": "abnormal heart (New)" + }, + { + "name": "abscess kidney (New)" + }, + { + "name": "encephalitis lethargica residuals (New)" + }, + { + "name": "dracunculiasis (New)" + }, + { + "name": "gingivitis (New)" + }, + { + "name": "abnormal weight loss (New)" + }, + { + "name": "groin condition (New)" + }, + { + "name": "metritis (New)" + } + ], + "decisionLetterSent": false, + "developmentLetterSent": false, + "documentsNeeded": false, + "endProductCode": "404", + "evidenceWaiverSubmitted5103": false, + "errors": [ - ], - "jurisdiction": "National Work Queue", - "lighthouseId": null, - "maxEstClaimDate": null, - "minEstClaimDate": null, - "status": "CANCELED", - "submitterApplicationCode": "EBN", - "submitterRoleCode": "VET", - "supportingDocuments": [ - { - "documentId": "{54EF0C16-A9E7-4C3F-B876-B2C7BEC1F834}", - "documentTypeLabel": "Medical", - "originalFileName": null, - "trackedItemId": null, - "uploadDate": null + ], + "jurisdiction": "National Work Queue", + "lighthouseId": null, + "maxEstClaimDate": null, + "minEstClaimDate": null, + "status": "CANCELED", + "submitterApplicationCode": "EBN", + "submitterRoleCode": "VET", + "supportingDocuments": [ + { + "documentId": "{54EF0C16-A9E7-4C3F-B876-B2C7BEC1F834}", + "documentTypeLabel": "Medical", + "originalFileName": null, + "trackedItemId": null, + "uploadDate": null + } + ], + "tempJurisdiction": null, + "trackedItems": [ + { + "closedDate": "2021-06-04", + "description": null, + "displayName": "21-4142a", + "overdue": false, + "receivedDate": null, + "requestedDate": "2021-05-05", + "status": "NO_LONGER_REQUIRED", + "suspenseDate": "2021-06-04", + "id": 293440, + "uploadsAllowed": false + }, + { + "closedDate": "2021-06-04", + "description": null, + "displayName": "Employment info needed", + "overdue": false, + "receivedDate": null, + "requestedDate": "2021-05-05", + "status": "NO_LONGER_REQUIRED", + "suspenseDate": "2021-06-04", + "id": 293443, + "uploadsAllowed": false + }, + { + "closedDate": "2021-06-04", + "description": null, + "displayName": "Accidental injury - 21-4176 needed", + "overdue": false, + "receivedDate": null, + "requestedDate": "2021-05-05", + "status": "NO_LONGER_REQUIRED", + "suspenseDate": "2021-06-04", + "id": 293444, + "uploadsAllowed": false + }, + { + "closedDate": "2021-06-04", + "description": null, + "displayName": "Buddy mentioned - No complete address", + "overdue": false, + "receivedDate": null, + "requestedDate": "2021-05-05", + "status": "NO_LONGER_REQUIRED", + "suspenseDate": "2021-06-04", + "id": 293446, + "uploadsAllowed": false + } + ] } - ], - "tempJurisdiction": null, - "trackedItems": [ - { - "closedDate": "2021-06-04", - "description": null, - "displayName": "21-4142a", - "overdue": false, - "receivedDate": null, - "requestedDate": "2021-05-05", - "status": "NO_LONGER_REQUIRED", - "suspenseDate": "2021-06-04", - "id": 293440, - "uploadsAllowed": false - }, - { - "closedDate": "2021-06-04", - "description": null, - "displayName": "Employment info needed", - "overdue": false, - "receivedDate": null, - "requestedDate": "2021-05-05", - "status": "NO_LONGER_REQUIRED", - "suspenseDate": "2021-06-04", - "id": 293443, - "uploadsAllowed": false - }, - { - "closedDate": "2021-06-04", - "description": null, - "displayName": "Accidental injury - 21-4176 needed", - "overdue": false, - "receivedDate": null, - "requestedDate": "2021-05-05", - "status": "NO_LONGER_REQUIRED", - "suspenseDate": "2021-06-04", - "id": 293444, - "uploadsAllowed": false - }, - { - "closedDate": "2021-06-04", - "description": null, - "displayName": "Buddy mentioned - No complete address", - "overdue": false, - "receivedDate": null, - "requestedDate": "2021-05-05", - "status": "NO_LONGER_REQUIRED", - "suspenseDate": "2021-06-04", - "id": 293446, - "uploadsAllowed": false + } + } + }, + "returns a 200 response for errored claim": { + "value": { + "data": { + "id": null, + "type": "claim", + "attributes": { + "claimTypeCode": null, + "claimDate": null, + "claimPhaseDates": null, + "claimType": null, + "closeDate": null, + "contentions": null, + "decisionLetterSent": null, + "developmentLetterSent": null, + "documentsNeeded": null, + "endProductCode": null, + "evidenceWaiverSubmitted5103": null, + "errors": [ + { + "detail": "ERROR must match d{7}", + "source": "form526/serviceInformation/reservesNationalGuardService/unitPhone/phoneNumber/Pattern" + }, + { + "detail": "ERROR must match d{7}", + "source": "form526/veteran/homelessness/pointOfContact/primaryPhone/phoneNumber/Pattern" + } + ], + "jurisdiction": null, + "lighthouseId": "d5536c5c-0465-4038-a368-1a9d9daf65c9", + "maxEstClaimDate": null, + "minEstClaimDate": null, + "status": "ERRORED", + "submitterApplicationCode": null, + "submitterRoleCode": null, + "supportingDocuments": [ + + ], + "tempJurisdiction": null, + "trackedItems": [ + + ] } - ] + } } } }, @@ -13773,7 +13822,46 @@ } } } - } + }, + "benefit_claim_details_dto": { + "attention_needed": "No", + "base_end_prdct_type_cd": "400", + "benefit_claim_id": "555555555", + "bnft_claim_lc_status": { + "phase_chngd_dt": "2017-10-18T08:23:35.000+00:00", + "phase_type": "COMPLETE", + "phase_type_change_ind": "78" + }, + "bnft_claim_type_cd": "400PREDSCHRG", + "claim_complete_dt": "2017-10-18T08:23:35.000+00:00", + "claim_dt": "2017-05-02", + "claim_status": "CAN", + "claim_status_type": "Compensation", + "contentions": "abnormal heart (New), abscess kidney (New), encephalitis lethargica residuals (New), dracunculiasis (New), gingivitis (New), abnormal weight loss (New), groin condition (New), metritis (New)", + "decision_notification_sent": "No", + "development_letter_sent": "No", + "end_prdct_type_cd": "404", + "errors": [ + + ], + "poa": "RANDOM E PERSON", + "program_type": "CPL", + "ptcpnt_clmant_id": "111111111", + "ptcpnt_vet_id": "111111111", + "regional_office_jrsdctn": "National Work Queue", + "submtr_applcn_type_cd": "EBN", + "submtr_role_type_cd": "VET", + "temp_regional_office_jrsdctn": null, + "wsyswwn": { + "address_line1": "National Work Queue", + "address_line2": "810 Vermont Avenue NW", + "address_line3": null, + "city": "Washington", + "state": "DC", + "zip": "20420" + } + }, + "@xmlns:ns0": "http://claimstatus.services.ebenefits.vba.va.gov/" } } } @@ -14186,8 +14274,8 @@ "id": "1", "type": "intent_to_file", "attributes": { - "creationDate": "2024-09-30", - "expirationDate": "2025-09-30", + "creationDate": "2024-10-03", + "expirationDate": "2025-10-03", "type": "compensation", "status": "active" } @@ -15083,7 +15171,7 @@ "application/json": { "example": { "data": { - "id": "ceb05896-10e7-47f7-aab4-5ac12570f1ab", + "id": "10295307-73bb-4825-95f5-02d6df41a4e1", "type": "individual", "attributes": { "code": "067", @@ -15776,7 +15864,7 @@ "application/json": { "example": { "data": { - "id": "aa12527e-85d9-4791-b98f-6193d204b213", + "id": "9abb338d-2390-472c-bb0a-8a1c7da2ba32", "type": "organization", "attributes": { "code": "083", @@ -17727,10 +17815,10 @@ "application/json": { "example": { "data": { - "id": "69f815eb-24fe-4190-b419-044ceae7ab77", + "id": "3b595df2-25af-4c3e-a48b-56478c80983d", "type": "claimsApiPowerOfAttorneys", "attributes": { - "dateRequestAccepted": "2024-09-30", + "dateRequestAccepted": "2024-10-03", "previousPoa": null, "representative": { "serviceOrganization": { 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 a0b83f38da4..4fa378fe322 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 @@ -3690,7 +3690,7 @@ "202 without a transactionId": { "value": { "data": { - "id": "c0d53d35-b874-4b02-ae95-0d0a6d7cb480", + "id": "53a89319-954b-4f30-96c8-c4b832d104a6", "type": "forms/526", "attributes": { "claimId": "600442191", @@ -3875,7 +3875,7 @@ }, "federalActivation": { "activationDate": "2023-10-01", - "anticipatedSeparationDate": "2024-10-02" + "anticipatedSeparationDate": "2024-10-05" }, "confinements": [ { @@ -3921,7 +3921,7 @@ "202 with a transactionId": { "value": { "data": { - "id": "9528cc9f-0346-440f-88db-a1713957486f", + "id": "a71d411d-5626-416f-9c01-366b3c2719f2", "type": "forms/526", "attributes": { "claimId": "600442191", @@ -9139,7 +9139,7 @@ "application/json": { "example": { "data": { - "id": "7a26e7c1-6a02-4d87-82be-73f80c6132cb", + "id": "de754a68-be13-4767-a299-dc7587bf23ab", "type": "forms/526", "attributes": { "claimProcessType": "STANDARD_CLAIM_PROCESS", @@ -11882,127 +11882,176 @@ ], "responses": { "200": { - "description": "claim response", + "description": "errored claim response", "content": { "application/json": { - "example": { - "data": { - "id": "555555555", - "type": "claim", - "attributes": { - "claimTypeCode": "400PREDSCHRG", - "claimDate": "2017-05-02", - "claimPhaseDates": { - "phaseChangeDate": "2017-10-18", - "currentPhaseBack": false, - "latestPhaseType": "COMPLETE", - "previousPhases": { - "phase7CompleteDate": "2017-10-18" - } - }, - "claimType": "Compensation", - "closeDate": "2017-10-18", - "contentions": [ - { - "name": "abnormal heart (New)" - }, - { - "name": "abscess kidney (New)" - }, - { - "name": "encephalitis lethargica residuals (New)" - }, - { - "name": "dracunculiasis (New)" - }, - { - "name": "gingivitis (New)" - }, - { - "name": "abnormal weight loss (New)" - }, - { - "name": "groin condition (New)" - }, - { - "name": "metritis (New)" - } - ], - "decisionLetterSent": false, - "developmentLetterSent": false, - "documentsNeeded": false, - "endProductCode": "404", - "evidenceWaiverSubmitted5103": false, - "errors": [ + "examples": { + "returns a 200 response for established claim": { + "value": { + "data": { + "id": "555555555", + "type": "claim", + "attributes": { + "claimTypeCode": "400PREDSCHRG", + "claimDate": "2017-05-02", + "claimPhaseDates": { + "phaseChangeDate": "2017-10-18", + "currentPhaseBack": false, + "latestPhaseType": "COMPLETE", + "previousPhases": { + "phase7CompleteDate": "2017-10-18" + } + }, + "claimType": "Compensation", + "closeDate": "2017-10-18", + "contentions": [ + { + "name": "abnormal heart (New)" + }, + { + "name": "abscess kidney (New)" + }, + { + "name": "encephalitis lethargica residuals (New)" + }, + { + "name": "dracunculiasis (New)" + }, + { + "name": "gingivitis (New)" + }, + { + "name": "abnormal weight loss (New)" + }, + { + "name": "groin condition (New)" + }, + { + "name": "metritis (New)" + } + ], + "decisionLetterSent": false, + "developmentLetterSent": false, + "documentsNeeded": false, + "endProductCode": "404", + "evidenceWaiverSubmitted5103": false, + "errors": [ - ], - "jurisdiction": "National Work Queue", - "lighthouseId": null, - "maxEstClaimDate": null, - "minEstClaimDate": null, - "status": "CANCELED", - "submitterApplicationCode": "EBN", - "submitterRoleCode": "VET", - "supportingDocuments": [ - { - "documentId": "{54EF0C16-A9E7-4C3F-B876-B2C7BEC1F834}", - "documentTypeLabel": "Medical", - "originalFileName": null, - "trackedItemId": null, - "uploadDate": null + ], + "jurisdiction": "National Work Queue", + "lighthouseId": null, + "maxEstClaimDate": null, + "minEstClaimDate": null, + "status": "CANCELED", + "submitterApplicationCode": "EBN", + "submitterRoleCode": "VET", + "supportingDocuments": [ + { + "documentId": "{54EF0C16-A9E7-4C3F-B876-B2C7BEC1F834}", + "documentTypeLabel": "Medical", + "originalFileName": null, + "trackedItemId": null, + "uploadDate": null + } + ], + "tempJurisdiction": null, + "trackedItems": [ + { + "closedDate": "2021-06-04", + "description": null, + "displayName": "21-4142a", + "overdue": false, + "receivedDate": null, + "requestedDate": "2021-05-05", + "status": "NO_LONGER_REQUIRED", + "suspenseDate": "2021-06-04", + "id": 293440, + "uploadsAllowed": false + }, + { + "closedDate": "2021-06-04", + "description": null, + "displayName": "Employment info needed", + "overdue": false, + "receivedDate": null, + "requestedDate": "2021-05-05", + "status": "NO_LONGER_REQUIRED", + "suspenseDate": "2021-06-04", + "id": 293443, + "uploadsAllowed": false + }, + { + "closedDate": "2021-06-04", + "description": null, + "displayName": "Accidental injury - 21-4176 needed", + "overdue": false, + "receivedDate": null, + "requestedDate": "2021-05-05", + "status": "NO_LONGER_REQUIRED", + "suspenseDate": "2021-06-04", + "id": 293444, + "uploadsAllowed": false + }, + { + "closedDate": "2021-06-04", + "description": null, + "displayName": "Buddy mentioned - No complete address", + "overdue": false, + "receivedDate": null, + "requestedDate": "2021-05-05", + "status": "NO_LONGER_REQUIRED", + "suspenseDate": "2021-06-04", + "id": 293446, + "uploadsAllowed": false + } + ] } - ], - "tempJurisdiction": null, - "trackedItems": [ - { - "closedDate": "2021-06-04", - "description": null, - "displayName": "21-4142a", - "overdue": false, - "receivedDate": null, - "requestedDate": "2021-05-05", - "status": "NO_LONGER_REQUIRED", - "suspenseDate": "2021-06-04", - "id": 293440, - "uploadsAllowed": false - }, - { - "closedDate": "2021-06-04", - "description": null, - "displayName": "Employment info needed", - "overdue": false, - "receivedDate": null, - "requestedDate": "2021-05-05", - "status": "NO_LONGER_REQUIRED", - "suspenseDate": "2021-06-04", - "id": 293443, - "uploadsAllowed": false - }, - { - "closedDate": "2021-06-04", - "description": null, - "displayName": "Accidental injury - 21-4176 needed", - "overdue": false, - "receivedDate": null, - "requestedDate": "2021-05-05", - "status": "NO_LONGER_REQUIRED", - "suspenseDate": "2021-06-04", - "id": 293444, - "uploadsAllowed": false - }, - { - "closedDate": "2021-06-04", - "description": null, - "displayName": "Buddy mentioned - No complete address", - "overdue": false, - "receivedDate": null, - "requestedDate": "2021-05-05", - "status": "NO_LONGER_REQUIRED", - "suspenseDate": "2021-06-04", - "id": 293446, - "uploadsAllowed": false + } + } + }, + "returns a 200 response for errored claim": { + "value": { + "data": { + "id": null, + "type": "claim", + "attributes": { + "claimTypeCode": null, + "claimDate": null, + "claimPhaseDates": null, + "claimType": null, + "closeDate": null, + "contentions": null, + "decisionLetterSent": null, + "developmentLetterSent": null, + "documentsNeeded": null, + "endProductCode": null, + "evidenceWaiverSubmitted5103": null, + "errors": [ + { + "detail": "ERROR must match d{7}", + "source": "form526/serviceInformation/reservesNationalGuardService/unitPhone/phoneNumber/Pattern" + }, + { + "detail": "ERROR must match d{7}", + "source": "form526/veteran/homelessness/pointOfContact/primaryPhone/phoneNumber/Pattern" + } + ], + "jurisdiction": null, + "lighthouseId": "d5536c5c-0465-4038-a368-1a9d9daf65c9", + "maxEstClaimDate": null, + "minEstClaimDate": null, + "status": "ERRORED", + "submitterApplicationCode": null, + "submitterRoleCode": null, + "supportingDocuments": [ + + ], + "tempJurisdiction": null, + "trackedItems": [ + + ] } - ] + } } } }, @@ -12386,7 +12435,46 @@ } } } - } + }, + "benefit_claim_details_dto": { + "attention_needed": "No", + "base_end_prdct_type_cd": "400", + "benefit_claim_id": "555555555", + "bnft_claim_lc_status": { + "phase_chngd_dt": "2017-10-18T08:23:35.000+00:00", + "phase_type": "COMPLETE", + "phase_type_change_ind": "78" + }, + "bnft_claim_type_cd": "400PREDSCHRG", + "claim_complete_dt": "2017-10-18T08:23:35.000+00:00", + "claim_dt": "2017-05-02", + "claim_status": "CAN", + "claim_status_type": "Compensation", + "contentions": "abnormal heart (New), abscess kidney (New), encephalitis lethargica residuals (New), dracunculiasis (New), gingivitis (New), abnormal weight loss (New), groin condition (New), metritis (New)", + "decision_notification_sent": "No", + "development_letter_sent": "No", + "end_prdct_type_cd": "404", + "errors": [ + + ], + "poa": "RANDOM E PERSON", + "program_type": "CPL", + "ptcpnt_clmant_id": "111111111", + "ptcpnt_vet_id": "111111111", + "regional_office_jrsdctn": "National Work Queue", + "submtr_applcn_type_cd": "EBN", + "submtr_role_type_cd": "VET", + "temp_regional_office_jrsdctn": null, + "wsyswwn": { + "address_line1": "National Work Queue", + "address_line2": "810 Vermont Avenue NW", + "address_line3": null, + "city": "Washington", + "state": "DC", + "zip": "20420" + } + }, + "@xmlns:ns0": "http://claimstatus.services.ebenefits.vba.va.gov/" } } } @@ -12799,8 +12887,8 @@ "id": "1", "type": "intent_to_file", "attributes": { - "creationDate": "2024-09-30", - "expirationDate": "2025-09-30", + "creationDate": "2024-10-03", + "expirationDate": "2025-10-03", "type": "compensation", "status": "active" } @@ -13696,7 +13784,7 @@ "application/json": { "example": { "data": { - "id": "c330c1b9-3fc0-47b3-bfb8-eb7317e96b48", + "id": "fc09bcf9-4334-40f9-83cd-e105223c5e77", "type": "individual", "attributes": { "code": "067", @@ -14389,7 +14477,7 @@ "application/json": { "example": { "data": { - "id": "440f704c-7689-4357-bab5-575d7df6bdf4", + "id": "da954be2-3a04-4889-adec-7ea428c56e2b", "type": "organization", "attributes": { "code": "083", @@ -16340,10 +16428,10 @@ "application/json": { "example": { "data": { - "id": "a6a3e890-9be5-48c3-b053-e909b4f29f99", + "id": "6ccb865e-ef3f-4999-ace0-963a62194085", "type": "claimsApiPowerOfAttorneys", "attributes": { - "dateRequestAccepted": "2024-09-30", + "dateRequestAccepted": "2024-10-03", "previousPoa": null, "representative": { "serviceOrganization": { diff --git a/modules/claims_api/spec/requests/v2/veterans/rswag_claims_spec.rb b/modules/claims_api/spec/requests/v2/veterans/rswag_claims_spec.rb index a46e6961262..27623fa260a 100644 --- a/modules/claims_api/spec/requests/v2/veterans/rswag_claims_spec.rb +++ b/modules/claims_api/spec/requests/v2/veterans/rswag_claims_spec.rb @@ -151,7 +151,7 @@ ) end - describe 'Getting a successful response' do + describe 'Established Claim' do response '200', 'claim response' do schema JSON.parse( Rails.root.join('spec', 'support', 'schemas', 'claims_api', 'v2', 'veterans', 'claims', @@ -187,14 +187,97 @@ end after do |example| + response_title = example.metadata[:description] example.metadata[:response][:content] = { 'application/json' => { - example: JSON.parse(response.body, symbolize_names: true) + examples: { + "#{response_title}": { + value: JSON.parse(response.body, symbolize_names: true) + } + } } } end - it 'returns a valid 200 response' do |example| + it 'returns a 200 response for established claim' do |example| + assert_response_matches_metadata(example.metadata) + end + end + end + + describe 'Errored Claim' do + response '200', 'errored claim response' do + schema JSON.parse( + Rails.root.join('modules', 'claims_api', 'spec', 'fixtures', 'v2', 'veterans', 'claims', + 'claim_by_id_response.json').read + ) + + let(:bgs_response) do + bgs_data = JSON.parse( + Rails.root.join('modules', 'claims_api', 'spec', 'fixtures', 'v2', 'veterans', 'claims', + 'claim_by_id_response.json').read, + symbolize_names: true + ) + bgs_data[:benefit_claim_details_dto][:claim_dt] = Date.parse( + bgs_data[:benefit_claim_details_dto][:claim_dt] + ) + bgs_data + end + let(:scopes) { %w[system/claim.read] } + + let(:id) do + 'd5536c5c-0465-4038-a368-1a9d9daf65c9' + end + + let(:evss_response) do + [{ 'key' => 'form526.serviceInformation.reservesNationalGuardService.unitPhone.phoneNumber.Pattern', + 'severity' => 'ERROR', + 'detail' => 'must match d{7}', + 'text' => 'must match d{7}' }, + { 'key' => 'form526.veteran.homelessness.pointOfContact.primaryPhone.phoneNumber.Pattern', + 'severity' => 'ERROR', + 'detail' => 'must match d{7}', + 'text' => 'must match d{7}' }] + end + + before do |example| + mock_acg(scopes) do |auth_header| + VCR.use_cassette('claims_api/bgs/claims/claim') do + bgs_response[:benefit_claim_details_dto][:ptcpnt_vet_id] = target_veteran.participant_id + allow_any_instance_of(ClaimsApi::V2::ApplicationController) + .to receive(:target_veteran).and_return(target_veteran) + allow_any_instance_of(ClaimsApi::V2::ApplicationController) + .to receive(:authenticate).and_return(true) + allow_any_instance_of(ClaimsApi::V2::Veterans::ClaimsController) + .to receive(:find_bgs_claim!).and_return(nil) + create(:auto_established_claim, + source: 'abraham lincoln', + auth_headers: auth_header, + evss_id: 600_118_851, + veteran_icn: '1013062086V794840', + id: 'd5536c5c-0465-4038-a368-1a9d9daf65c9', + status: 'errored', + evss_response:) + + submit_request(example.metadata) + end + end + end + + after do |example| + response_title = example.metadata[:description] + example.metadata[:response][:content] = { + 'application/json' => { + examples: { + "#{response_title}": { + value: JSON.parse(response.body, symbolize_names: true) + } + } + } + } + end + + it 'returns a 200 response for errored claim', run_at: 'Wed, 13 Dec 2017 03:28:23 GMT' do |example| assert_response_matches_metadata(example.metadata) end end From 0788b93610d7d8e8acb4eb8aadb4238cabcfea6b Mon Sep 17 00:00:00 2001 From: Eric Tillberg Date: Mon, 7 Oct 2024 10:32:35 -0400 Subject: [PATCH 24/30] Set timezone to US Eastern for Simple Forms email sends (#18761) --- app/models/form_submission_attempt.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/form_submission_attempt.rb b/app/models/form_submission_attempt.rb index 7ce20e62fa6..e6647a4c632 100644 --- a/app/models/form_submission_attempt.rb +++ b/app/models/form_submission_attempt.rb @@ -95,7 +95,7 @@ def enqueue_result_email(notification_type) end def time_to_send - now = Time.zone.now + now = Time.now.in_time_zone('Eastern Time (US & Canada)') if now.hour < HOUR_TO_SEND_NOTIFICATIONS now.change(hour: HOUR_TO_SEND_NOTIFICATIONS, min: 0) From 2ac04922aec86bb4568814d8349256a1651eb1ba Mon Sep 17 00:00:00 2001 From: Nathan Burgess Date: Mon, 7 Oct 2024 10:35:52 -0400 Subject: [PATCH 25/30] Overhaul EVSS API Upload Provider logging (#18762) Adds an extensive logging paradigm for documenting uploads via the EVSSSupplementalDocumentUploadProvider. This will track all of the important events during an upload and allow us to easily query and make dashboards in DataDog Note since the EVSS::DocumentsService has some custom error response logic that involves throwing exceptions, we don't have an easy way of capturing an actual upload response body here. But we want to log the failure event and re-raise the exception to preserve existing behavior --- ...s_supplemental_document_upload_provider.rb | 56 ++++++- ...plemental_document_upload_provider_spec.rb | 141 ++++++++++++++---- ...plemental_document_upload_provider_spec.rb | 2 +- 3 files changed, 163 insertions(+), 36 deletions(-) diff --git a/lib/disability_compensation/providers/document_upload/evss_supplemental_document_upload_provider.rb b/lib/disability_compensation/providers/document_upload/evss_supplemental_document_upload_provider.rb index 3d2a29486bf..bde08dfdeb2 100644 --- a/lib/disability_compensation/providers/document_upload/evss_supplemental_document_upload_provider.rb +++ b/lib/disability_compensation/providers/document_upload/evss_supplemental_document_upload_provider.rb @@ -47,22 +47,66 @@ def validate_upload_document(evss_claim_document) # # @return [Faraday::Response] The EVSS::DocumentsService API calls are implemented with Faraday def submit_upload_document(evss_claim_document, file_body) + log_upload_attempt + client = EVSS::DocumentsService.new(@form526_submission.auth_headers) client.upload(file_body, evss_claim_document) - StatsD.increment("#{@statsd_metric_prefix}.#{STATSD_PROVIDER_METRIC}.#{STATSD_SUCCESS_METRIC}") + # EVSS::DocumentsService uploads throw a EVSS::ErrorMiddleware::EVSSError if they fail + # If no exception is raised, log a success response + log_upload_success + rescue EVSS::ErrorMiddleware::EVSSError => e + # If exception is raised, log and re-raise the error + log_upload_failure + raise e end - def log_upload_failure(error_class, error_message) - StatsD.increment("#{@statsd_metric_prefix}.#{STATSD_PROVIDER_METRIC}.#{STATSD_FAILED_METRIC}") - + # To call in the sidekiq_retries_exhausted block of the including job + # This is meant to log an upload attempt that was retried and eventually given up on, + # so we can investigate the failure in Datadog + # + # @param uploading_job_class [String] the job where we are uploading the EVSSClaimDocument + # (e.g. UploadBDDInstructions) + # @param error_class [String] the Error class of the exception that exhausted the upload job + # @param error_message [String] the message in the exception that exhausted the upload job + def log_uploading_job_failure(uploading_job_class, error_class, error_message) Rails.logger.error( - 'EVSSSupplementalDocumentUploadProvider upload failure', + "#{uploading_job_class} EVSSSupplementalDocumentUploadProvider Failure", { - class: 'EVSSSupplementalDocumentUploadProvider', + **base_logging_info, + uploading_job_class:, error_class:, error_message: } ) + + StatsD.increment("#{@statsd_metric_prefix}.#{STATSD_PROVIDER_METRIC}.#{STASTD_UPLOAD_JOB_FAILED_METRIC}") + end + + private + + def base_logging_info + { + class: 'EVSSSupplementalDocumentUploadProvider', + submission_id: @form526_submission.submitted_claim_id, + user_uuid: @form526_submission.user_uuid, + va_document_type_code: @va_document_type, + primary_form: 'Form526' + } + end + + def log_upload_attempt + Rails.logger.info('EVSSSupplementalDocumentUploadProvider upload attempted', base_logging_info) + StatsD.increment("#{@statsd_metric_prefix}.#{STATSD_PROVIDER_METRIC}.#{STATSD_ATTEMPT_METRIC}") + end + + def log_upload_success + Rails.logger.info('EVSSSupplementalDocumentUploadProvider upload successful', base_logging_info) + StatsD.increment("#{@statsd_metric_prefix}.#{STATSD_PROVIDER_METRIC}.#{STATSD_SUCCESS_METRIC}") + end + + def log_upload_failure + Rails.logger.error('EVSSSupplementalDocumentUploadProvider upload failed', base_logging_info) + StatsD.increment("#{@statsd_metric_prefix}.#{STATSD_PROVIDER_METRIC}.#{STATSD_FAILED_METRIC}") end end diff --git a/spec/lib/disability_compensation/providers/document_upload/evss_supplemental_document_upload_provider_spec.rb b/spec/lib/disability_compensation/providers/document_upload/evss_supplemental_document_upload_provider_spec.rb index 8f5be81b986..28818cb197b 100644 --- a/spec/lib/disability_compensation/providers/document_upload/evss_supplemental_document_upload_provider_spec.rb +++ b/spec/lib/disability_compensation/providers/document_upload/evss_supplemental_document_upload_provider_spec.rb @@ -5,17 +5,17 @@ require 'support/disability_compensation_form/shared_examples/supplemental_document_upload_provider' RSpec.describe EVSSSupplementalDocumentUploadProvider do - let(:submission) { create(:form526_submission) } + let(:submission) { create(:form526_submission, :with_submitted_claim_id) } let(:file_body) { File.read(fixture_file_upload('doctors-note.pdf', 'application/pdf')) } let(:file_name) { Faker::File.file_name } let(:va_document_type) { 'L023' } - let(:provider) do + let!(:provider) do EVSSSupplementalDocumentUploadProvider.new( submission, va_document_type, - 'my_upload_job_prefix' + 'my_stats_metric_prefix' ) end @@ -27,6 +27,11 @@ ) end + before do + # Disallow actual API calls + allow_any_instance_of(EVSS::DocumentsService).to receive(:upload) + end + it_behaves_like 'supplemental document upload provider' describe '#generate_upload_document' do @@ -68,58 +73,136 @@ provider.submit_upload_document(evss_claim_document, file_body) end + end + end - it 'increments a StatsD success metric' do - faraday_response = instance_double(Faraday::Response) + describe 'events logging' do + context 'when attempting to upload a document' do + before do + # Skip success logging + allow(provider).to receive(:log_upload_success) + end - allow_any_instance_of(EVSS::DocumentsService).to receive(:upload) - .with(file_body, evss_claim_document) - .and_return(faraday_response) + it 'logs to the Rails logger' do + expect(Rails.logger).to receive(:info).with( + 'EVSSSupplementalDocumentUploadProvider upload attempted', + { + class: 'EVSSSupplementalDocumentUploadProvider', + submission_id: submission.submitted_claim_id, + user_uuid: submission.user_uuid, + va_document_type_code: va_document_type, + primary_form: 'Form526' + } + ) + + provider.submit_upload_document(evss_claim_document, file_body) + end + it 'increments a StatsD attempt metric' do expect(StatsD).to receive(:increment).with( - 'my_upload_job_prefix.evss_supplemental_document_upload_provider.upload_success' + 'my_stats_metric_prefix.evss_supplemental_document_upload_provider.upload_attempt' ) provider.submit_upload_document(evss_claim_document, file_body) end end - end - describe 'logging methods' do - # We don't want to generate an actual submission for these tests, - # since submissions have callbacks that log to StatsD and we need to test - # only the metrics in this class - let(:submission) { instance_double(Form526Submission) } - let(:provider) do - EVSSSupplementalDocumentUploadProvider.new( - submission, - va_document_type, - 'my_upload_job_prefix' - ) + context 'when an upload is successfull' do + before do + # Skip upload attempt logging + allow(provider).to receive(:log_upload_attempt) + end + + it 'logs to the Rails logger' do + expect(Rails.logger).to receive(:info).with( + 'EVSSSupplementalDocumentUploadProvider upload successful', + { + class: 'EVSSSupplementalDocumentUploadProvider', + submission_id: submission.submitted_claim_id, + user_uuid: submission.user_uuid, + va_document_type_code: va_document_type, + primary_form: 'Form526' + } + ) + + provider.submit_upload_document(evss_claim_document, file_body) + end + + it 'increments a StatsD success metric' do + expect(StatsD).to receive(:increment).with( + 'my_stats_metric_prefix.evss_supplemental_document_upload_provider.upload_success' + ) + + provider.submit_upload_document(evss_claim_document, file_body) + end end - describe 'log_upload_failure' do - let(:error_class) { 'StandardError' } - let(:error_message) { 'Something broke' } + # The EVSS::DocumentsService client we used in this API provider has custom exception logic + # for unsucessful upload responses from EVSS (which still have a 200 response code) + # We want to preserve this behavior while logging the event for tracking purposes + context 'when an upload raises an EVSS response error' do + before do + # Skip upload attempt logging + allow(provider).to receive(:log_upload_attempt) + allow_any_instance_of(EVSS::DocumentsService).to receive(:upload).and_raise(EVSS::ErrorMiddleware::EVSSError) + end + + it 'logs to the Rails logger, increments a StatsD failure metric, and re-raises the error' do + expect(Rails.logger).to receive(:error).with( + 'EVSSSupplementalDocumentUploadProvider upload failed', + { + class: 'EVSSSupplementalDocumentUploadProvider', + submission_id: submission.submitted_claim_id, + user_uuid: submission.user_uuid, + va_document_type_code: va_document_type, + primary_form: 'Form526' + } + ) + + # Ensure we don't increment the success metric + expect(StatsD).not_to receive(:increment).with( + 'my_stats_metric_prefix.evss_supplemental_document_upload_provider.upload_success' + ) - it 'increments a StatsD failure metric' do expect(StatsD).to receive(:increment).with( - 'my_upload_job_prefix.evss_supplemental_document_upload_provider.upload_failure' + 'my_stats_metric_prefix.evss_supplemental_document_upload_provider.upload_failure' + ) + + expect { provider.submit_upload_document(evss_claim_document, file_body) }.to raise_exception( + EVSS::ErrorMiddleware::EVSSError ) - provider.log_upload_failure(error_class, error_message) end + end + + # Will be called in the sidekiq_retries_exhausted block of the including job + context 'uploading job failure' do + let(:uploading_job_class) { 'MyUploadJob' } + let(:error_class) { 'StandardError' } + let(:error_message) { 'Something broke' } it 'logs to the Rails logger' do expect(Rails.logger).to receive(:error).with( - 'EVSSSupplementalDocumentUploadProvider upload failure', + "#{uploading_job_class} EVSSSupplementalDocumentUploadProvider Failure", { class: 'EVSSSupplementalDocumentUploadProvider', + submission_id: submission.submitted_claim_id, + user_uuid: submission.user_uuid, + va_document_type_code: va_document_type, + primary_form: 'Form526', + uploading_job_class:, error_class:, error_message: } ) - provider.log_upload_failure(error_class, error_message) + provider.log_uploading_job_failure(uploading_job_class, error_class, error_message) + end + + it 'increments a StatsD failure metric' do + expect(StatsD).to receive(:increment).with( + 'my_stats_metric_prefix.evss_supplemental_document_upload_provider.upload_job_failed' + ) + provider.log_uploading_job_failure(uploading_job_class, error_class, error_message) end end end diff --git a/spec/lib/disability_compensation/providers/document_upload/lighthouse_supplemental_document_upload_provider_spec.rb b/spec/lib/disability_compensation/providers/document_upload/lighthouse_supplemental_document_upload_provider_spec.rb index bacf96e4c10..956cf242643 100644 --- a/spec/lib/disability_compensation/providers/document_upload/lighthouse_supplemental_document_upload_provider_spec.rb +++ b/spec/lib/disability_compensation/providers/document_upload/lighthouse_supplemental_document_upload_provider_spec.rb @@ -14,7 +14,7 @@ # BDD Document Type let(:va_document_type) { 'L023' } - let(:provider) do + let!(:provider) do LighthouseSupplementalDocumentUploadProvider.new( submission, va_document_type, From 946cba640aec46c874dc0441eb76f0e714f8909b Mon Sep 17 00:00:00 2001 From: Nathan Wright Date: Mon, 7 Oct 2024 09:05:16 -0600 Subject: [PATCH 26/30] VANotify - Add Statsd for successful icn and user account jobs (#18702) --- modules/va_notify/app/sidekiq/va_notify/icn_job.rb | 5 +++++ .../va_notify/app/sidekiq/va_notify/user_account_job.rb | 7 +++++-- modules/va_notify/spec/sidekiq/icn_job_spec.rb | 2 ++ modules/va_notify/spec/sidekiq/user_account_job_spec.rb | 2 ++ 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/modules/va_notify/app/sidekiq/va_notify/icn_job.rb b/modules/va_notify/app/sidekiq/va_notify/icn_job.rb index 1fc98cf2a73..f09bcfa2138 100644 --- a/modules/va_notify/app/sidekiq/va_notify/icn_job.rb +++ b/modules/va_notify/app/sidekiq/va_notify/icn_job.rb @@ -26,7 +26,12 @@ def perform(icn, template_id, personalisation = nil, api_key = Settings.vanotify template_id:, personalisation: }.compact ) + StatsD.increment('api.vanotify.icn_job.success') rescue Common::Exceptions::BackendServiceException => e + handle_backend_exception(e, icn, template_id, personalisation) + end + + def handle_backend_exception(e, icn, template_id, personalisation) if e.status_code == 400 log_exception_to_sentry( e, diff --git a/modules/va_notify/app/sidekiq/va_notify/user_account_job.rb b/modules/va_notify/app/sidekiq/va_notify/user_account_job.rb index 3f30d774e41..1f0631b4b9b 100644 --- a/modules/va_notify/app/sidekiq/va_notify/user_account_job.rb +++ b/modules/va_notify/app/sidekiq/va_notify/user_account_job.rb @@ -17,7 +17,6 @@ class UserAccountJob StatsD.increment("sidekiq.jobs.#{job_class.underscore}.retries_exhausted") end - # rubocop:disable Metrics/MethodLength def perform( user_account_id, template_id, @@ -33,7 +32,12 @@ def perform( template_id:, personalisation: }.compact ) + StatsD.increment('api.vanotify.user_account_job.success') rescue Common::Exceptions::BackendServiceException => e + handle_backend_exception(e, user_account, template_id, personalisation) + end + + def handle_backend_exception(e, user_account, template_id, personalisation) if e.status_code == 400 log_exception_to_sentry( e, @@ -47,6 +51,5 @@ def perform( raise e end end - # rubocop:enable Metrics/MethodLength end end diff --git a/modules/va_notify/spec/sidekiq/icn_job_spec.rb b/modules/va_notify/spec/sidekiq/icn_job_spec.rb index 83d2fcb3354..61a67a0a614 100644 --- a/modules/va_notify/spec/sidekiq/icn_job_spec.rb +++ b/modules/va_notify/spec/sidekiq/icn_job_spec.rb @@ -31,6 +31,8 @@ } ) + expect(StatsD).to receive(:increment).with('api.vanotify.icn_job.success') + described_class.new.perform(icn, template_id) end diff --git a/modules/va_notify/spec/sidekiq/user_account_job_spec.rb b/modules/va_notify/spec/sidekiq/user_account_job_spec.rb index aa3c0feafc8..27285bad680 100644 --- a/modules/va_notify/spec/sidekiq/user_account_job_spec.rb +++ b/modules/va_notify/spec/sidekiq/user_account_job_spec.rb @@ -32,6 +32,8 @@ } ) + expect(StatsD).to receive(:increment).with('api.vanotify.user_account_job.success') + described_class.new.perform(user_account.id, template_id) end From 156055d95cc4cffd3b520d7c76b5753e953d2fe5 Mon Sep 17 00:00:00 2001 From: khenson-oddball <145150351+khenson-oddball@users.noreply.github.com> Date: Mon, 7 Oct 2024 09:22:30 -0600 Subject: [PATCH 27/30] Add status card eligibility to service history (#18701) --- .../profile/service_histories_controller.rb | 2 +- app/serializers/service_history_serializer.rb | 6 +- app/swagger/swagger/requests/profile.rb | 7 ++ config/features.yml | 4 +- .../service_history_response.rb | 17 ++++ lib/va_profile/models/service_history.rb | 30 ++++++- .../military_personnel/service_spec.rb | 55 ++++++++++++- .../va_profile/models/service_history_spec.rb | 82 ++++++++++++++++++- .../v0/profile/service_history_spec.rb | 15 +++- .../service_history_serializer_spec.rb | 9 +- .../schemas/service_history_response.json | 15 ++++ .../service_history_response.json | 15 ++++ 12 files changed, 240 insertions(+), 17 deletions(-) diff --git a/app/controllers/v0/profile/service_histories_controller.rb b/app/controllers/v0/profile/service_histories_controller.rb index f2b0297246d..3b54f357a17 100644 --- a/app/controllers/v0/profile/service_histories_controller.rb +++ b/app/controllers/v0/profile/service_histories_controller.rb @@ -43,7 +43,7 @@ def get_military_info handle_errors!(response.episodes) report_results(response.episodes) - service_history_json = JSON.parse(response.episodes.to_json, symbolize_names: true) + service_history_json = JSON.parse(response.to_json, symbolize_names: true) options = { is_collection: false } render json: ServiceHistorySerializer.new(service_history_json, options), status: response.status diff --git a/app/serializers/service_history_serializer.rb b/app/serializers/service_history_serializer.rb index 79025c32932..b040840ebc7 100644 --- a/app/serializers/service_history_serializer.rb +++ b/app/serializers/service_history_serializer.rb @@ -6,6 +6,10 @@ class ServiceHistorySerializer set_id { '' } attributes :service_history do |object| - object + object[:episodes] + end + + attributes :vet_status_eligibility do |object| + object[:vet_status_eligibility] end end diff --git a/app/swagger/swagger/requests/profile.rb b/app/swagger/swagger/requests/profile.rb index 26f3bcf148d..8beff5038bc 100644 --- a/app/swagger/swagger/requests/profile.rb +++ b/app/swagger/swagger/requests/profile.rb @@ -933,6 +933,13 @@ class Profile property :character_of_discharge_code, type: :string, example: 'DVN', description: 'The abbreviated code used to reference the status of a Servicemember upon termination of an episode' end end + property :vet_status_eligibility do + key :type, :object + items do + property :confirmed, type: :boolean + property :message, type: :array + end + end end end end diff --git a/config/features.yml b/config/features.yml index 0e4739664d6..0dcbff0dcbb 100644 --- a/config/features.yml +++ b/config/features.yml @@ -1038,9 +1038,9 @@ features: profile_show_quick_submit_notification_setting: actor_type: user description: Show/Hide the quick submit section of notification settings in profile - profile_show_proof_of_veteran_status: + profile_show_proof_of_veteran_status_eligible: actor_type: user - description: Show/Hide the proof of veteran status page and links + description: Include/exclude the proof of veteran status eligibility in service_history response profile_use_experimental: description: Use experimental features for Profile application - Do not remove enable_in_development: true diff --git a/lib/va_profile/military_personnel/service_history_response.rb b/lib/va_profile/military_personnel/service_history_response.rb index 93451a782ac..0a58bb3313e 100644 --- a/lib/va_profile/military_personnel/service_history_response.rb +++ b/lib/va_profile/military_personnel/service_history_response.rb @@ -9,6 +9,7 @@ class ServiceHistoryResponse < VAProfile::Response attribute :episodes, Array attribute :uniformed_service_initial_entry_date, String attribute :release_from_active_duty_date, String + attribute :vet_status_eligibility, Object def self.from(current_user, raw_response = nil) body = raw_response&.body @@ -17,6 +18,18 @@ def self.from(current_user, raw_response = nil) episodes += get_military_service_episodes(body) episodes += get_academy_attendance_episodes(body) if include_academy_attendance?(current_user) + if Flipper.enabled?(:profile_show_proof_of_veteran_status_eligible) + eligibility = get_eligibility(episodes) + + return new( + raw_response&.status, + episodes: episodes ? sort_by_begin_date(episodes) : episodes, + uniformed_service_initial_entry_date: get_uniformed_service_initial_entry_date(body), + release_from_active_duty_date: get_release_from_active_duty_date(body), + vet_status_eligibility: eligibility + ) + end + new( raw_response&.status, episodes: episodes ? sort_by_begin_date(episodes) : episodes, @@ -68,6 +81,10 @@ def self.sort_by_begin_date(service_episodes) def self.include_academy_attendance?(current_user) Flipper.enabled?(:profile_show_military_academy_attendance, current_user) end + + def self.get_eligibility(episodes) + VAProfile::Models::ServiceHistory.determine_eligibility(episodes) + end end end end diff --git a/lib/va_profile/models/service_history.rb b/lib/va_profile/models/service_history.rb index 27f2370c067..818fc34efa8 100644 --- a/lib/va_profile/models/service_history.rb +++ b/lib/va_profile/models/service_history.rb @@ -8,9 +8,9 @@ module Models class ServiceHistory < Base include VAProfile::Concerns::Defaultable - MILITARY_SERVICE = 'Military Service' - MILITARY_SERVICE_EPISODE = 'military_service_episodes' - ACADEMY_ATTENDANCE = 'Academy Attendance' + MILITARY_SERVICE = 'Military Service' + MILITARY_SERVICE_EPISODE = 'military_service_episodes' + ACADEMY_ATTENDANCE = 'Academy Attendance' ACADEMY_ATTENDANCE_EPISODE = 'service_academy_episodes' attribute :service_type, String @@ -80,6 +80,30 @@ def self.build_from_academy_episode(episode) end_date: episode['academy_end_date'] ) end + + def self.determine_eligibility(episodes) + problem_message = [ + 'We’re sorry. There’s a problem with your discharge status records. We can’t provide a Veteran status ' \ + 'card for you right now.', + 'To fix the problem with your records, call the Defense Manpower Data Center at 800-538-9552 (TTY: 711).' \ + ' They’re open Monday through Friday, 8:00 a.m. to 8:00 p.m. ET.' + ] + not_eligible_message = [ + 'Our records show that you’re not eligible for a Veteran status card. To get a Veteran status card, you ' \ + 'must have received an honorable discharge for at least one period of service.', + 'If you think your discharge status is incorrect, call the Defense Manpower Data Center at 800-538-9552 ' \ + '(TTY: 711). They’re open Monday through Friday, 8:00 a.m. to 8:00 p.m. ET.' + ] + + return { confirmed: false, message: problem_message } if episodes.empty? + + codes = episodes.map(&:character_of_discharge_code).uniq.compact + return { confirmed: true, message: [] } if codes.intersect?(%w[A B H J]) # Honorable discharge + # Not honorable discharge + return { confirmed: false, message: not_eligible_message } if codes.intersect?(%w[D E F K]) || codes.empty? + + { confirmed: false, message: problem_message } # No service history OR unknown (Z) discharge + end end end end diff --git a/spec/lib/va_profile/military_personnel/service_spec.rb b/spec/lib/va_profile/military_personnel/service_spec.rb index b34140fa997..3745de23421 100644 --- a/spec/lib/va_profile/military_personnel/service_spec.rb +++ b/spec/lib/va_profile/military_personnel/service_spec.rb @@ -18,7 +18,11 @@ end describe '#get_service_history' do - context 'when successful' do + context 'when successful without show_proof_of_veteran_status_eligible flipper' do + before do + Flipper.disable(:profile_show_proof_of_veteran_status_eligible) + end + it 'returns a status of 200' do VCR.use_cassette('va_profile/military_personnel/post_read_service_history_200') do response = subject.get_service_history @@ -64,6 +68,54 @@ expect(episodes[4].begin_date).to eq('2012-03-02') end end + + it 'does not contain eligibility information' do + VCR.use_cassette('va_profile/military_personnel/service_history_200_many_episodes') do + response = subject.get_service_history + + expect(response).to be_ok + expect(response.vet_status_eligibility).to be_nil + end + end + end + + context 'when successful with show_proof_of_veteran_status_eligible flipper' do + before do + Flipper.enable(:profile_show_proof_of_veteran_status_eligible) + end + + it 'contains eligibility information' do + VCR.use_cassette('va_profile/military_personnel/service_history_200_many_episodes') do + response = subject.get_service_history + + expect(response).to be_ok + expect(response.vet_status_eligibility).to be_a(Object) + end + end + + it 'eligibility information contains confirmed and message attributes' do + VCR.use_cassette('va_profile/military_personnel/service_history_200_many_episodes') do + response = subject.get_service_history + + expect(response.vet_status_eligibility[:confirmed]).to eq(true) + expect(response.vet_status_eligibility[:message]).to eq([]) + end + end + + it 'returns not eligible if character_of_discharge_codes are missing' do + VCR.use_cassette('va_profile/military_personnel/post_read_service_histories_200') do + response = subject.get_service_history + message = [ + 'Our records show that you’re not eligible for a Veteran status card. To get a Veteran status card, you ' \ + 'must have received an honorable discharge for at least one period of service.', + 'If you think your discharge status is incorrect, call the Defense Manpower Data Center at 800-538-9552 ' \ + '(TTY: 711). They’re open Monday through Friday, 8:00 a.m. to 8:00 p.m. ET.' + ] + + expect(response.vet_status_eligibility[:confirmed]).to eq(false) + expect(response.vet_status_eligibility[:message]).to eq(message) + end + end end context 'when not successful' do @@ -73,6 +125,7 @@ expect(response).not_to be_ok expect(response.episodes.count).to eq(0) + expect(response.vet_status_eligibility).to be_nil end end diff --git a/spec/lib/va_profile/models/service_history_spec.rb b/spec/lib/va_profile/models/service_history_spec.rb index 412a14c32c3..70ba48e7264 100644 --- a/spec/lib/va_profile/models/service_history_spec.rb +++ b/spec/lib/va_profile/models/service_history_spec.rb @@ -17,9 +17,7 @@ context 'when service history json is present' do it 'returns a service_history model' do - data = JSON.parse(json) - episode_type = VAProfile::Models::ServiceHistory::MILITARY_SERVICE_EPISODE - model = VAProfile::Models::ServiceHistory.build_from(data, episode_type) + model = create_model(json) expect(model).not_to be_nil expect(model.branch_of_service).to eq('National Guard') @@ -47,4 +45,82 @@ expect(model).to be_nil end end + + describe '#determing_eligibility' do + let(:not_eligible_message) do + [ + 'Our records show that you’re not eligible for a Veteran status card. To get a Veteran status card, you must ' \ + 'have received an honorable discharge for at least one period of service.', + 'If you think your discharge status is incorrect, call the Defense Manpower Data Center at 800-538-9552 ' \ + '(TTY: 711). They’re open Monday through Friday, 8:00 a.m. to 8:00 p.m. ET.' + ] + end + let(:problem_message) do + [ + 'We’re sorry. There’s a problem with your discharge status records. We can’t provide a Veteran status card ' \ + 'for you right now.', + 'To fix the problem with your records, call the Defense Manpower Data Center at 800-538-9552 (TTY: 711). ' \ + 'They’re open Monday through Friday, 8:00 a.m. to 8:00 p.m. ET.' + ] + end + + it 'returns not eligible with service history missing characterOfDischargeCode' do + eligibility = VAProfile::Models::ServiceHistory.determine_eligibility([create_model(json)]) + + expect(eligibility).to eq({ confirmed: false, message: not_eligible_message }) + end + + it 'returns not eligible with dishonorable service history' do + json = '{ + "branch_of_service_text": "National Guard", + "period_of_service_begin_date": "2010-01-01", + "period_of_service_end_date": "2015-12-31", + "period_of_service_type_code": "N", + "period_of_service_type_text": "National Guard member", + "character_of_discharge_code":"D" + }' + eligibility = VAProfile::Models::ServiceHistory.determine_eligibility([create_model(json)]) + + expect(eligibility).to eq({ confirmed: false, message: not_eligible_message }) + end + + it 'returns eligible with honorable service history' do + json = '{ + "branch_of_service_text": "National Guard", + "period_of_service_begin_date": "2010-01-01", + "period_of_service_end_date": "2015-12-31", + "period_of_service_type_code": "N", + "period_of_service_type_text": "National Guard member", + "character_of_discharge_code":"A" + }' + eligibility = VAProfile::Models::ServiceHistory.determine_eligibility([create_model(json)]) + + expect(eligibility).to eq({ confirmed: true, message: [] }) + end + + it 'returns problem message with no service history' do + eligibility = VAProfile::Models::ServiceHistory.determine_eligibility([]) + expect(eligibility).to eq({ confirmed: false, message: problem_message }) + end + + it 'returns problem message with service history containing unknown discharge code' do + json = '{ + "branch_of_service_text": "National Guard", + "period_of_service_begin_date": "2010-01-01", + "period_of_service_end_date": "2015-12-31", + "period_of_service_type_code": "N", + "period_of_service_type_text": "National Guard member", + "character_of_discharge_code":"Z" + }' + eligibility = VAProfile::Models::ServiceHistory.determine_eligibility([create_model(json)]) + + expect(eligibility).to eq({ confirmed: false, message: problem_message }) + end + end + + def create_model(json) + data = JSON.parse(json) + episode_type = VAProfile::Models::ServiceHistory::MILITARY_SERVICE_EPISODE + VAProfile::Models::ServiceHistory.build_from(data, episode_type) + end end diff --git a/spec/requests/v0/profile/service_history_spec.rb b/spec/requests/v0/profile/service_history_spec.rb index c52521058fd..3e83fa7d494 100644 --- a/spec/requests/v0/profile/service_history_spec.rb +++ b/spec/requests/v0/profile/service_history_spec.rb @@ -41,7 +41,7 @@ end end - it 'returns a single service history episode' do + it 'returns a single service history episode and vet_status_eligibility' do VCR.use_cassette('va_profile/military_personnel/post_read_service_history_200') do get '/v0/profile/service_history' @@ -55,17 +55,26 @@ expect(episode['period_of_service_type_text']).to eq('National Guard member') expect(episode['termination_reason_code']).to eq('S') expect(episode['termination_reason_text']).to eq('Separation from personnel category or organization') + expect(json.dig('attributes', 'vet_status_eligibility')).to eq({ 'confirmed' => true, 'message' => [] }) end end it 'returns no service history episodes' do VCR.use_cassette('va_profile/military_personnel/post_read_service_history_200_empty') do get '/v0/profile/service_history' + problem_message = [ + 'We’re sorry. There’s a problem with your discharge status records. We can’t provide a Veteran status ' \ + 'card for you right now.', + 'To fix the problem with your records, call the Defense Manpower Data Center at 800-538-9552 (TTY: 711).' \ + ' They’re open Monday through Friday, 8:00 a.m. to 8:00 p.m. ET.' + ] json = json_body_for(response) - expect(response).to be_ok - episodes = json.dig('attributes', 'service_history') + vet_status_eligibility = json.dig('attributes', 'vet_status_eligibility') + + expect(response).to be_ok expect(episodes.count).to eq(0) + expect(vet_status_eligibility).to eq({ 'confirmed' => false, 'message' => problem_message }) end end diff --git a/spec/serializers/service_history_serializer_spec.rb b/spec/serializers/service_history_serializer_spec.rb index 6118866d55c..afc0b737ff6 100644 --- a/spec/serializers/service_history_serializer_spec.rb +++ b/spec/serializers/service_history_serializer_spec.rb @@ -6,7 +6,10 @@ subject { serialize(service_history, { serializer_class: described_class, is_collection: false }) } let(:service_history) do - histories = [build(:service_history, :with_deployments)] + histories = { + episodes: [build(:service_history, :with_deployments)], + vet_status_eligibility: { confirmed: true, message: [] } + } JSON.parse(histories.to_json, symbolize_names: true) end let(:data) { JSON.parse(subject)['data'] } @@ -17,10 +20,10 @@ end it 'includes :service_history' do - expect(attributes['service_history'].size).to eq service_history.size + expect(attributes['service_history'].size).to eq service_history[:episodes].size end it 'includes :service_history with attributes' do - expect(attributes['service_history'].first).to eq service_history.first.deep_stringify_keys + expect(attributes['service_history'].first).to eq service_history[:episodes].first.deep_stringify_keys end end diff --git a/spec/support/schemas/service_history_response.json b/spec/support/schemas/service_history_response.json index 1f41422c0ee..5f8934324bf 100644 --- a/spec/support/schemas/service_history_response.json +++ b/spec/support/schemas/service_history_response.json @@ -38,6 +38,21 @@ } }, "type": "array" + }, + "vet_status_eligibility": { + "description": "Proof of status card eligibility confirmation and message", + "items": { + "type": "object" + }, + "properties": { + "confirmed": { + "type": "boolean" + }, + "message" : { + "type": "array" + } + }, + "type": "object" } }, "type": "object" diff --git a/spec/support/schemas_camelized/service_history_response.json b/spec/support/schemas_camelized/service_history_response.json index 75119ff8e1e..4bb0ef9e1bd 100644 --- a/spec/support/schemas_camelized/service_history_response.json +++ b/spec/support/schemas_camelized/service_history_response.json @@ -36,6 +36,21 @@ } }, "type": "array" + }, + "vetStatusEligibility": { + "description": "Proof of status card eligibility confirmation and message", + "items": { + "type": "object" + }, + "properties": { + "confirmed": { + "type": "boolean" + }, + "message" : { + "type": "array" + } + }, + "type": "object" } }, "type": "object" From b6171bb2db9ddd3dc4346012c8a1ab7b59f7c647 Mon Sep 17 00:00:00 2001 From: Tyler Date: Mon, 7 Oct 2024 08:30:39 -0700 Subject: [PATCH 28/30] [API-40614] halt v2 526 synchronous submission if pdf empty (#18724) * halt v2 526 synchronous submission if pdf empty * add test --- .../disability_compensation_controller.rb | 18 +++++++++++++---- .../pdf_generation_service.rb | 2 +- .../pdf_generation_service_spec.rb | 20 ++++++++++++++++--- 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/modules/claims_api/app/controllers/claims_api/v2/veterans/disability_compensation_controller.rb b/modules/claims_api/app/controllers/claims_api/v2/veterans/disability_compensation_controller.rb index 886066d0b9b..aa510565235 100644 --- a/modules/claims_api/app/controllers/claims_api/v2/veterans/disability_compensation_controller.rb +++ b/modules/claims_api/app/controllers/claims_api/v2/veterans/disability_compensation_controller.rb @@ -103,9 +103,9 @@ def synchronous auto_claim = shared_submit_methods unless claims_load_testing # || sandbox_request(request) - pdf_generation_service.generate(auto_claim&.id, veteran_middle_initial) unless mocking - docker_container_service.upload(auto_claim&.id) - queue_flash_updater(auto_claim.flashes, auto_claim&.id) + generate_pdf_from_service!(auto_claim.id, veteran_middle_initial) unless mocking + docker_container_service.upload(auto_claim.id) + queue_flash_updater(auto_claim.flashes, auto_claim.id) start_bd_uploader_job(auto_claim) if auto_claim.status != errored_state_value auto_claim.reload end @@ -115,6 +115,8 @@ def synchronous ), status: :accepted, location: url_for(controller: 'claims', action: 'show', id: auto_claim.id) end + private + def shared_submit_methods auto_claim = ClaimsApi::AutoEstablishedClaim.create( status: ClaimsApi::AutoEstablishedClaim::PENDING, @@ -136,7 +138,15 @@ def shared_submit_methods auto_claim end - private + def generate_pdf_from_service!(auto_claim_id, veteran_middle_initial) + claim_status = pdf_generation_service.generate(auto_claim_id, veteran_middle_initial) + + if claim_status == ClaimsApi::AutoEstablishedClaim::ERRORED + raise ::ClaimsApi::Common::Exceptions::Lighthouse::UnprocessableEntity.new( + detail: 'Failed to generate PDF' + ) + end + end def generate_pdf_mapper_service(form_data, pdf_data_wrapper, auth_headers, middle_initial, created_at) ClaimsApi::V2::DisabilityCompensationPdfMapper.new( diff --git a/modules/claims_api/app/services/claims_api/disability_compensation/pdf_generation_service.rb b/modules/claims_api/app/services/claims_api/disability_compensation/pdf_generation_service.rb index 3c050ef434c..cd7e2bbadd2 100644 --- a/modules/claims_api/app/services/claims_api/disability_compensation/pdf_generation_service.rb +++ b/modules/claims_api/app/services/claims_api/disability_compensation/pdf_generation_service.rb @@ -46,7 +46,7 @@ def generate(claim_id, middle_initial) # rubocop:disable Metrics/MethodLength log_job_progress(auto_claim.id, '526EZ PDF generator job finished', auto_claim.transaction_id) - auto_claim.id + auto_claim.status end def generate_mapped_claim(auto_claim, middle_initial) diff --git a/modules/claims_api/spec/services/disability_compensation/pdf_generation_service_spec.rb b/modules/claims_api/spec/services/disability_compensation/pdf_generation_service_spec.rb index b4250ee856c..ec030715681 100644 --- a/modules/claims_api/spec/services/disability_compensation/pdf_generation_service_spec.rb +++ b/modules/claims_api/spec/services/disability_compensation/pdf_generation_service_spec.rb @@ -5,7 +5,7 @@ require './modules/claims_api/app/services/claims_api/disability_compensation/pdf_generation_service' describe ClaimsApi::DisabilityCompensation::PdfGenerationService do - let(:pdf_generation_service) { ClaimsApi::DisabilityCompensation::PdfGenerationService.new } + let(:pdf_generation_service) { described_class.new } let(:user) { FactoryBot.create(:user, :loa3) } let(:auth_headers) do EVSS::DisabilityCompensationAuthHeaders.new(user).add_headers(EVSS::AuthHeaders.new(user).to_h) @@ -35,12 +35,12 @@ end describe '#generate' do - it 'has a generate method that returns a claim id' do + it 'returns the claim status' do VCR.use_cassette('claims_api/pdf_client') do allow(pdf_generation_service).to receive(:generate_mapped_claim).with(claim, middle_initial).and_return(mapped_claim) - expect(pdf_generation_service.send(:generate, claim.id, middle_initial)).to be_a(String) + expect(pdf_generation_service.send(:generate, claim.id, middle_initial)).to eq('pending') end end @@ -53,5 +53,19 @@ expect(Rails.logger).to have_received(:info).with(/#{claim.transaction_id}/).at_least(:once) end end + + context 'when the pdf string is empty' do + before do + allow(pdf_generation_service).to receive(:generate_mapped_claim).with(claim, + middle_initial).and_return(mapped_claim) + allow(pdf_generation_service).to receive(:generate_526_pdf).with(mapped_claim).and_return('') + end + + it 'returns the errored claim status' do + VCR.use_cassette('claims_api/pdf_client') do + expect(pdf_generation_service.send(:generate, claim.id, middle_initial)).to eq('errored') + end + end + end end end From 0a3d55204d92f8eb254ee7e8c0b251c168d07c08 Mon Sep 17 00:00:00 2001 From: Gaurav Gupta Date: Mon, 7 Oct 2024 08:51:07 -0700 Subject: [PATCH 29/30] 93074 Update travel claims client to take in client_number as initialization parameter (#18717) * add client_id as initialization parameter to travel claim client * Revert "add client_id as initialization parameter to travel claim client" This reverts commit b7425aba1383c26674d84a15cbd384020d469bae. * add client_number to travel claim client initialization * minor change in spec name --- modules/check_in/app/services/travel_claim/client.rb | 6 ++++-- .../spec/services/travel_claim/client_spec.rb | 12 +++++++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/modules/check_in/app/services/travel_claim/client.rb b/modules/check_in/app/services/travel_claim/client.rb index d388f5a6d10..71a5dec8355 100644 --- a/modules/check_in/app/services/travel_claim/client.rb +++ b/modules/check_in/app/services/travel_claim/client.rb @@ -12,16 +12,17 @@ class Client CLAIMANT_ID_TYPE = 'icn' TRIP_TYPE = 'RoundTrip' - attr_reader :settings, :check_in + attr_reader :settings, :check_in, :client_number def_delegators :settings, :auth_url, :tenant_id, :client_id, :client_secret, :scope, :claims_url, :claims_base_path, - :client_number, :subscription_key, :e_subscription_key, :s_subscription_key, :service_name + :subscription_key, :e_subscription_key, :s_subscription_key, :service_name ## # Builds a Client instance # # @param opts [Hash] options to create a Client # @option opts [CheckIn::V2::Session] :check_in the check_in session object + # @option opts [String] :client_number the client number to use for the claim # # @return [TravelClaim::Client] an instance of this class # @@ -32,6 +33,7 @@ def self.build(opts = {}) def initialize(opts) @settings = Settings.check_in.travel_reimbursement_api_v2 @check_in = opts[:check_in] + @client_number = opts[:client_number] || settings.client_number 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 2baf74ceb52..bc8f3aaddfd 100644 --- a/modules/check_in/spec/services/travel_claim/client_spec.rb +++ b/modules/check_in/spec/services/travel_claim/client_spec.rb @@ -3,10 +3,11 @@ require 'rails_helper' describe TravelClaim::Client do - subject { described_class.build(check_in:) } + subject { described_class.build(check_in:, client_number:) } let(:uuid) { 'd602d9eb-9a31-484f-9637-13ab0b507e0d' } let(:check_in) { CheckIn::V2::Session.build(data: { uuid: }) } + let(:client_number) { 'test-client-number' } before do allow(Flipper).to receive(:enabled?).with('check_in_experience_mock_enabled').and_return(false) @@ -32,6 +33,15 @@ it 'has a session' do expect(subject.check_in).to be_a(CheckIn::V2::Session) end + + it 'has the client_number it is initialized with' do + expect(subject.client_number).to eq(client_number) + end + + it 'has default client_number if not initialized with it' do + cls = described_class.build(check_in:) + expect(cls.client_number).to eq(Settings.check_in.travel_reimbursement_api_v2.client_number) + end end describe '#token' do From f703394781459573e836b5bb961ea818d464e255 Mon Sep 17 00:00:00 2001 From: Tyler Date: Mon, 7 Oct 2024 09:06:38 -0700 Subject: [PATCH 30/30] [API-40280] Appoint the POA for the dependent claimant (v1) (#18673) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * rename benefitclaimwebservice; add update_benefit… distinguish between benefitclaimwebservice and benefitclaimservice. add update_benefit_claim. * add cassette and test * add dependent claimant poa assignment service * finish benefit_claim_update flow; refactor * add exceptions and tests * add cassettes * add controller test * add allow_poa_cadd * add allow_poa_access * fix assignment service spec * fix controller spec * add nil for consistency * add attr_reader for participant_id and ssn * pass through allow_poa_access and allow_poa_cadd * return nil for consistency * fix test --- .../v1/forms/power_of_attorney_controller.rb | 58 ++++-- ...pendent_claimant_poa_assignment_service.rb | 171 ++++++++++++++++++ ...dependent_claimant_verification_service.rb | 52 ++++-- .../lib/bgs_service/person_web_service.rb | 2 +- .../lib/claims_api/person_web_service_spec.rb | 5 +- .../spec/requests/v1/forms/2122_spec.rb | 68 ++++++- ...nt_claimant_poa_assignment_service_spec.rb | 134 ++++++++++++++ 7 files changed, 456 insertions(+), 34 deletions(-) create mode 100644 modules/claims_api/app/services/claims_api/dependent_claimant_poa_assignment_service.rb create mode 100644 modules/claims_api/spec/services/dependent_claimant_poa_assignment_service_spec.rb diff --git a/modules/claims_api/app/controllers/claims_api/v1/forms/power_of_attorney_controller.rb b/modules/claims_api/app/controllers/claims_api/v1/forms/power_of_attorney_controller.rb index 2651623c496..630e2bcba32 100644 --- a/modules/claims_api/app/controllers/claims_api/v1/forms/power_of_attorney_controller.rb +++ b/modules/claims_api/app/controllers/claims_api/v1/forms/power_of_attorney_controller.rb @@ -29,7 +29,9 @@ def submit_form_2122 # rubocop:disable Metrics/MethodLength poa_code = form_attributes.dig('serviceOrganization', 'poaCode') validate_poa_code!(poa_code) validate_poa_code_for_current_user!(poa_code) if header_request? && !token.client_credentials_token? - check_file_number_exists! + file_number = check_file_number_exists! + dependent_participant_id, claimant_ssn = validate_dependent_claimant!(poa_code:) + assign_poa_to_dependent_claimant!(poa_code:, file_number:, dependent_participant_id:, claimant_ssn:) power_of_attorney = ClaimsApi::PowerOfAttorney.find_using_identifier_and_source(header_md5:, source_name:) @@ -140,23 +142,51 @@ def validate poa_code = form_attributes.dig('serviceOrganization', 'poaCode') validate_poa_code!(poa_code) validate_poa_code_for_current_user!(poa_code) if header_request? && !token.client_credentials_token? - if Flipper.enabled?(:lighthouse_claims_api_poa_dependent_claimants) && form_attributes['claimant'].present? - veteran_participant_id = target_veteran.participant_id - claimant_first_name = form_attributes.dig('claimant', 'firstName') - claimant_last_name = form_attributes.dig('claimant', 'lastName') - service = ClaimsApi::DependentClaimantVerificationService.new(veteran_participant_id:, - claimant_first_name:, - claimant_last_name:, - poa_code:) - service.validate_poa_code_exists! - service.validate_dependent_by_participant_id! - end + validate_dependent_claimant!(poa_code:) render json: validation_success end private + def feature_enabled_and_claimant_present? + Flipper.enabled?(:lighthouse_claims_api_poa_dependent_claimants) && + form_attributes['claimant'].present? + end + + def validate_dependent_claimant!(poa_code:) + return nil unless feature_enabled_and_claimant_present? + + veteran_participant_id = target_veteran.participant_id + claimant_first_name = form_attributes.dig('claimant', 'firstName') + claimant_last_name = form_attributes.dig('claimant', 'lastName') + service = ClaimsApi::DependentClaimantVerificationService.new(veteran_participant_id:, + claimant_first_name:, + claimant_last_name:, + poa_code:) + + service.validate_poa_code_exists! + service.validate_dependent_by_participant_id! + + [service.claimant_participant_id, service.claimant_ssn] + end + + def assign_poa_to_dependent_claimant!(poa_code:, file_number:, dependent_participant_id:, claimant_ssn:) + return nil unless feature_enabled_and_claimant_present? + + service = ClaimsApi::DependentClaimantPoaAssignmentService.new( + poa_code:, + veteran_participant_id: target_veteran.participant_id, + dependent_participant_id:, + veteran_file_number: file_number, + allow_poa_access: form_attributes[:recordConsent].present? ? 'Y' : nil, + allow_poa_cadd: form_attributes[:consentAddressChange].present? ? 'Y' : nil, + claimant_ssn: + ) + + service.assign_poa_to_dependent! + end + def current_poa_begin_date return nil if power_of_attorney_verifier.current_poa.try(:begin_date).blank? @@ -252,7 +282,7 @@ def check_request_ssn_matches_mpi(req_headers) end def check_file_number_exists! - ssn = target_veteran&.ssn + ssn = target_veteran.ssn begin response = find_by_ssn(ssn) @@ -262,6 +292,8 @@ def check_file_number_exists! 'or call 1-800-MyVA411 (800-698-2411) for assistance.' raise ::Common::Exceptions::UnprocessableEntity.new(detail: error_message) end + + response[:file_nbr] rescue BGS::ShareError error_message = "A BGS failure occurred while trying to retrieve Veteran 'FileNumber'" claims_v1_logging('poa_find_by_ssn', message: error_message) diff --git a/modules/claims_api/app/services/claims_api/dependent_claimant_poa_assignment_service.rb b/modules/claims_api/app/services/claims_api/dependent_claimant_poa_assignment_service.rb new file mode 100644 index 00000000000..c2bd091399d --- /dev/null +++ b/modules/claims_api/app/services/claims_api/dependent_claimant_poa_assignment_service.rb @@ -0,0 +1,171 @@ +# frozen_string_literal: true + +require 'date' +require 'bgs_service/person_web_service' +require 'bgs_service/redis/find_poas_service' +require 'bgs_service/benefit_claim_web_service' +require 'bgs_service/benefit_claim_service' + +module ClaimsApi + class DependentClaimantPoaAssignmentService + def initialize(**options) + @poa_code = options[:poa_code] + @veteran_participant_id = options[:veteran_participant_id] + @dependent_participant_id = options[:dependent_participant_id] + @veteran_file_number = options[:veteran_file_number] + @allow_poa_access = options[:allow_poa_access] + @allow_poa_cadd = options[:allow_poa_cadd] + @claimant_ssn = options[:claimant_ssn] + end + + def assign_poa_to_dependent! + return nil if assign_poa_to_dependent_via_manage_ptcpnt_rlnshp? + + return nil if assign_poa_to_dependent_via_update_benefit_claim? + + log(level: :error, detail: 'Failed to assign POA to dependent') + + raise ::Common::Exceptions::FailedDependency + end + + private + + def person_web_service + ClaimsApi::PersonWebService.new(external_uid: @dependent_participant_id, + external_key: @dependent_participant_id) + end + + def log(level: :info, **rest) + ClaimsApi::Logger.log('dependent_claimant_poa_assignment_service', + level:, dependent_participant_id: @dependent_participant_id, + poa_code: @poa_code, veteran_participant_id: @veteran_participant_id, **rest) + end + + def assign_poa_to_dependent_via_manage_ptcpnt_rlnshp? + res = person_web_service.manage_ptcpnt_rlnshp_poa(ptcpnt_id_a: @dependent_participant_id, + ptcpnt_id_b: poa_participant_id, + authzn_poa_access_ind: @allow_poa_access, + authzn_change_clmant_addrs_ind: @allow_poa_cadd) + + if manage_ptcpnt_rlnshp_poa_success?(res) + log(detail: 'POA assigned to dependent.') + + return true + end + + log(level: :warn, + detail: 'Something else went wrong with manage_ptcpnt_rlnshp. Falling back to update_benefit_claim.') + + false + rescue ::Common::Exceptions::ServiceError => e + if e.errors.first.detail == 'PtcpntIdA has open claims.' + log(detail: 'Dependent has open claims, continuing.') + else + log(level: :warn, + detail: 'Something else went wrong with manage_ptcpnt_rlnshp. Falling back to update_benefit_claim.') + end + + false + end + + def iso_to_date(iso_date) + DateTime.parse(iso_date).strftime('%m/%d/%Y') + end + + def build_benefit_claim_update_input(claim_details:) + claim_rcvd_dt = iso_to_date(claim_details[:claim_rcvd_dt]) + + { + file_number: @veteran_file_number, + payee_code: claim_details[:payee_type_cd], + date_of_claim: claim_rcvd_dt, + claimant_ssn: @claimant_ssn, + power_of_attorney: @poa_code, + benefit_claim_type: benefit_claim_type(claim_details[:pgm_type_cd]), + old_end_product_code: claim_details[:cp_claim_end_prdct_type_cd], + new_end_product_label: claim_details[:bnft_claim_type_cd], + old_date_of_claim: claim_rcvd_dt, + allow_poa_access: @allow_poa_access, + allow_poa_cadd: @allow_poa_cadd + } + end + + def assign_poa_to_dependent_via_update_benefit_claim? + first_open_claim = dependent_claims.find do |claim| + claim[:phase_type] != 'Complete' && claim[:ptcpnt_vet_id] == @veteran_participant_id + end + first_open_claim_details = claim_details(first_open_claim[:benefit_claim_id]) + + benefit_claim_update_input = build_benefit_claim_update_input(claim_details: first_open_claim_details) + + result = benefit_claim_service.update_benefit_claim(benefit_claim_update_input) + + if result[:return][:return_message] == 'Update to Corporate was successful' + log(detail: 'POA assigned to dependent.') + + return true + end + + false + end + + def dependent_claims + local_bgs = ClaimsApi::LocalBGS.new(external_uid: @dependent_participant_id, + external_key: @dependent_participant_id) + res = local_bgs.find_benefit_claims_status_by_ptcpnt_id(@dependent_participant_id) + + return res&.dig(:benefit_claims_dto, :benefit_claim) if res&.dig(:benefit_claims_dto, :benefit_claim).present? + + log(level: :error, detail: 'Dependent claims not found in BGS') + + raise ::Common::Exceptions::FailedDependency + end + + def benefit_claim_web_service + ClaimsApi::BenefitClaimWebService.new(external_uid: @dependent_participant_id, + external_key: @dependent_participant_id) + end + + def benefit_claim_service + ClaimsApi::BenefitClaimService.new(external_uid: @dependent_participant_id, + external_key: @dependent_participant_id) + end + + def claim_details(claim_id) + res = benefit_claim_web_service.find_bnft_claim(claim_id:) + + return res&.dig(:bnft_claim_dto) if res&.dig(:bnft_claim_dto).present? + + log(level: :error, detail: 'Claim details not found in BGS', claim_id:) + + raise ::Common::Exceptions::FailedDependency + end + + def poa_participant_id + poa_ptcpnt = FindPOAsService.new.response.find { |combo| combo[:legacy_poa_cd] == @poa_code } + + return poa_ptcpnt&.dig(:ptcpnt_id) if poa_ptcpnt&.dig(:ptcpnt_id).present? + + log(level: :error, detail: 'POA code/participant ID combo not found in BGS') + + raise ::Common::Exceptions::FailedDependency + end + + def manage_ptcpnt_rlnshp_poa_success?(response) + response.is_a?(Hash) && response.dig(:comp_id, :ptcpnt_rlnshp_type_nm) == 'Power of Attorney For' + end + + def benefit_claim_type(pgm_type_cd) + case pgm_type_cd + when 'CPL' + '1' + when 'CPD' + '2' + else + log(level: :error, detail: 'Program type code not recognized', pgm_type_cd:) + + raise ::Common::Exceptions::FailedDependency + end + end + end +end diff --git a/modules/claims_api/app/services/claims_api/dependent_claimant_verification_service.rb b/modules/claims_api/app/services/claims_api/dependent_claimant_verification_service.rb index db4c41612a3..9f70d11a499 100644 --- a/modules/claims_api/app/services/claims_api/dependent_claimant_verification_service.rb +++ b/modules/claims_api/app/services/claims_api/dependent_claimant_verification_service.rb @@ -9,22 +9,25 @@ class DependentClaimantVerificationService 'Please submit VA Form 21-686c to add this dependent.' POA_CODE_NOT_FOUND_ERROR_MESSAGE = 'The requested POA code could not be found.' - def initialize(options = {}) + attr_reader :claimant_participant_id, :claimant_ssn + + def initialize(**options) @veteran_participant_id = options[:veteran_participant_id] @claimant_first_name = options[:claimant_first_name] @claimant_last_name = options[:claimant_last_name] @claimant_participant_id = options[:claimant_participant_id] + @claimant_ssn = nil @poa_code = options[:poa_code] end def validate_dependent_by_participant_id! - return if valid_participant_dependent_combo? + return nil if valid_participant_dependent_combo? raise ::Common::Exceptions::UnprocessableEntity.new(detail: CLAIMANT_NOT_A_DEPENDENT_ERROR_MESSAGE) end def validate_poa_code_exists! - return if poa_code_exists? + return nil if poa_code_exists? raise ::Common::Exceptions::UnprocessableEntity.new(detail: POA_CODE_NOT_FOUND_ERROR_MESSAGE) end @@ -35,22 +38,22 @@ def normalize(item) item.to_s.strip.upcase end - def valid_participant_dependent_combo? - return false if @veteran_participant_id.blank? + def person_web_service + ClaimsApi::PersonWebService.new(external_uid: @veteran_participant_id, external_key: @veteran_participant_id) + end - person_web_service = PersonWebService.new(external_uid: 'dependent_claimant_verification_uid', - external_key: 'dependent_claimant_verification_key') - response = person_web_service.find_dependents_by_ptcpnt_id(@veteran_participant_id) + def matching_participant_id?(dependent) + return false unless normalize(@claimant_participant_id) == normalize(dependent[:ptcpnt_id]) - return false if response.nil? || response.fetch(:number_of_records, 0).to_i.zero? + @claimant_ssn = dependent[:ssn_nbr] - dependents = response[:dependent] + true + end + def any_matching_dependents?(dependents) Array.wrap(dependents).any? do |dependent| # If the claimant_participant_id is present (most v2), use it to verify the dependent - if @claimant_participant_id.present? - return normalize(@claimant_participant_id) == normalize(dependent[:ptcpnt_id]) - end + return matching_participant_id?(dependent) if @claimant_participant_id.present? # Otherwise, we need to verify the dependent by first and last name (all v1 and some v2 without participant_ids) normalized_claimant_first_name = normalize(@claimant_first_name) @@ -61,15 +64,32 @@ def valid_participant_dependent_combo? return false if [normalized_claimant_first_name, normalized_claimant_last_name, normalized_dependent_first_name, normalized_dependent_last_name].any?(&:blank?) - normalized_claimant_first_name == normalized_dependent_first_name && - normalized_claimant_last_name == normalized_dependent_last_name + if normalized_claimant_first_name == normalized_dependent_first_name && + normalized_claimant_last_name == normalized_dependent_last_name + @claimant_participant_id = dependent[:ptcpnt_id] + @claimant_ssn = dependent[:ssn_nbr] + + return true + end end end + def valid_participant_dependent_combo? + return false if @veteran_participant_id.blank? + + response = person_web_service.find_dependents_by_ptcpnt_id(@veteran_participant_id) + + return false if response.nil? || response.fetch(:number_of_records, 0).to_i.zero? + + dependents = response[:dependent] + + any_matching_dependents?(dependents) + end + def poa_code_exists? return false if @poa_code.blank? - response = FindPOAsService.new.response + response = ClaimsApi::FindPOAsService.new.response return false if response.nil? || !response.is_a?(Array) || response.empty? diff --git a/modules/claims_api/lib/bgs_service/person_web_service.rb b/modules/claims_api/lib/bgs_service/person_web_service.rb index b108d4c70c9..b8a9971f535 100644 --- a/modules/claims_api/lib/bgs_service/person_web_service.rb +++ b/modules/claims_api/lib/bgs_service/person_web_service.rb @@ -18,7 +18,7 @@ def find_dependents_by_ptcpnt_id(id) # ptcpntIdA is the veteranʼs or dependentʼs participant id # ptcpntIdB is the poaʼs participant id - def manage_ptcpnt_rlnshp_poa(options = {}) + def manage_ptcpnt_rlnshp_poa(**options) builder = Nokogiri::XML::Builder.new do PtcpntRlnshpDTO do authznChangeClmantAddrsInd 'Y' if options[:authzn_change_clmant_addrs_ind].present? diff --git a/modules/claims_api/spec/lib/claims_api/person_web_service_spec.rb b/modules/claims_api/spec/lib/claims_api/person_web_service_spec.rb index a6e2e7ea249..2a772b881b3 100644 --- a/modules/claims_api/spec/lib/claims_api/person_web_service_spec.rb +++ b/modules/claims_api/spec/lib/claims_api/person_web_service_spec.rb @@ -54,10 +54,9 @@ ptcpnt_id_a:, ptcpnt_id_b: } - result = subject.manage_ptcpnt_rlnshp_poa(options) + result = subject.manage_ptcpnt_rlnshp_poa(options:) expect(result).to be_a Hash - expect(result[:authzn_poa_access_ind]).to eq 'Y' expect(result[:comp_id][:ptcpnt_id_a]).to eq ptcpnt_id_a expect(result[:comp_id][:ptcpnt_id_b]).to eq ptcpnt_id_b expect(result[:comp_id][:ptcpnt_rlnshp_type_nm]).to eq 'Power of Attorney For' @@ -74,7 +73,7 @@ } expect do - subject.manage_ptcpnt_rlnshp_poa(options) + subject.manage_ptcpnt_rlnshp_poa(options:) end.to raise_error(Common::Exceptions::ServiceError) { |error| expect(error.errors.first.detail).to eq 'PtcpntIdA has open claims.' } diff --git a/modules/claims_api/spec/requests/v1/forms/2122_spec.rb b/modules/claims_api/spec/requests/v1/forms/2122_spec.rb index 2b78246824c..6dc4440e235 100644 --- a/modules/claims_api/spec/requests/v1/forms/2122_spec.rb +++ b/modules/claims_api/spec/requests/v1/forms/2122_spec.rb @@ -44,7 +44,7 @@ end end - describe 'submit_form_2122' do + describe '#submit_form_2122' do let(:bgs_poa_verifier) { BGS::PowerOfAttorneyVerifier.new(nil) } context 'when poa code is valid' do @@ -378,6 +378,72 @@ end end end + + shared_context 'stub validation methods' do + before do + allow_any_instance_of(ClaimsApi::V1::Forms::PowerOfAttorneyController) + .to receive(:check_request_ssn_matches_mpi).and_return(nil) + allow_any_instance_of(ClaimsApi::V1::Forms::PowerOfAttorneyController) + .to receive(:validate_json_schema).and_return(nil) + allow_any_instance_of(ClaimsApi::V1::Forms::PowerOfAttorneyController) + .to receive(:validate_poa_code!).and_return(nil) + allow_any_instance_of(ClaimsApi::V1::Forms::PowerOfAttorneyController) + .to receive(:validate_poa_code_for_current_user!).and_return(nil) + allow_any_instance_of(ClaimsApi::V1::Forms::PowerOfAttorneyController) + .to receive(:check_file_number_exists!).and_return(nil) + allow_any_instance_of(ClaimsApi::V1::Forms::PowerOfAttorneyController) + .to receive(:validate_dependent_claimant!).and_return(nil) + allow_any_instance_of(ClaimsApi::DependentClaimantPoaAssignmentService) + .to receive(:assign_poa_to_dependent!).and_return(nil) + end + end + + context 'when the lighthouse_claims_api_poa_dependent_claimants feature is enabled' do + include_context 'stub validation methods' + + before do + Flipper.enable(:lighthouse_claims_api_poa_dependent_claimants) + end + + context 'and the request includes a dependent claimant' do + it 'calls assign_poa_to_dependent!' do + mock_acg(scopes) do |auth_header| + expect_any_instance_of(ClaimsApi::DependentClaimantPoaAssignmentService) + .to receive(:assign_poa_to_dependent!) + + post path, params: data_with_claimant, headers: headers.merge(auth_header) + end + end + end + + context 'and the request does not include a dependent claimant' do + it 'does not call assign_poa_to_dependent!' do + mock_acg(scopes) do |auth_header| + expect_any_instance_of(ClaimsApi::DependentClaimantPoaAssignmentService) + .not_to receive(:assign_poa_to_dependent!) + + post path, params: data, headers: headers.merge(auth_header) + end + end + end + end + + context 'when the lighthouse_claims_api_poa_dependent_claimants feature is disabled' do + include_context 'stub validation methods' + + before do + Flipper.disable(:lighthouse_claims_api_poa_dependent_claimants) + end + + it 'does not call assign_poa_to_dependent!' do + mock_acg(scopes) do |auth_header| + expect_any_instance_of(ClaimsApi::DependentClaimantPoaAssignmentService) + .not_to receive(:assign_poa_to_dependent!) + + post path, params: data_with_claimant, headers: headers.merge(auth_header) + end + end + end end describe '#status' do diff --git a/modules/claims_api/spec/services/dependent_claimant_poa_assignment_service_spec.rb b/modules/claims_api/spec/services/dependent_claimant_poa_assignment_service_spec.rb new file mode 100644 index 00000000000..075ac58d837 --- /dev/null +++ b/modules/claims_api/spec/services/dependent_claimant_poa_assignment_service_spec.rb @@ -0,0 +1,134 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'bgs_service/local_bgs' +require 'bgs_service/benefit_claim_web_service' +require 'bgs_service/benefit_claim_service' + +Rspec.describe ClaimsApi::DependentClaimantPoaAssignmentService do + describe '#assign_poa_to_dependent!' do + let(:dependent_participant_id) { '600052700' } + let(:service) do + described_class.new(poa_code: '002', veteran_participant_id: '600052699', dependent_participant_id:, + veteran_file_number: '796163671', claimant_ssn: '796163672') + end + let(:mock_find_benefit_claims_status_by_ptcpnt_id) do + { + benefit_claims_dto: + { benefit_claim: + [{ + appeal_possible: 'No', + attention_needed: 'Yes', + base_end_prdct_type_cd: '690', + benefit_claim_id: '256009', + bnft_claim_type_cd: '690AUTRWPMC', + claim_dt: '2013-03-01', + claim_status: 'RDC', + claim_status_type: 'Authorization Review', + decision_notification_sent: 'No', + development_letter_sent: 'Yes', + ealiest_evidence_due_date: '2024-08-25', + end_prdct_type_cd: '691', + filed5103_waiver_ind: 'Y', + latest_evidence_recd_date: '2015-09-18', + max_est_claim_complete_dt: '2013-03-30', + min_est_claim_complete_dt: '2013-03-28', + phase_chngd_dt: '2013-03-26T06:24:43', + phase_type: 'Pending Decision Approval', + program_type: 'CPD', + ptcpnt_clmant_id: '600052700', + ptcpnt_vet_id: '600052699' + }] } + } + end + let(:mock_find_bnft_claim) do + { + bnft_claim_dto: + { + bnft_claim_id: '256009', + bnft_claim_type_cd: '690AUTRWPMC', + bnft_claim_type_label: 'Authorization Review', + bnft_claim_type_nm: 'PMC-Reviews - Authorization Only', + bnft_claim_user_display: 'YES', + claim_jrsdtn_lctn_id: '347', + claim_rcvd_dt: '2013-03-01T00:00:00-06:00', + claim_suspns_dt: '2024-08-20T12:09:48-05:00', + cp_claim_end_prdct_type_cd: '691', + filed5103_waiver_ind: 'Y', + jrn_dt: '2024-10-01T11:58:31-05:00', + jrn_lctn_id: '281', + jrn_obj_id: 'VAgovAPI', + jrn_status_type_cd: 'U', + jrn_user_id: 'VAgovAPI', + payee_type_cd: '10', + payee_type_nm: 'Spouse', + pgm_type_cd: 'CPD', + pgm_type_nm: 'Compensation-Pension Death', + ptcpnt_clmant_id: '600052700', + ptcpnt_clmant_nm: 'CURTIS MARGIE', + ptcpnt_mail_addrs_id: '16542930', + ptcpnt_pymt_addrs_id: '14781119', + ptcpnt_vet_id: '600052699', + station_of_jurisdiction: '317', + status_type_cd: 'RDC', + status_type_nm: 'Rating Decision Complete', + svc_type_cd: 'CP', + temp_jrsdtn_lctn_id: '123725', + temporary_station_of_jurisdiction: '499', + termnl_digit_nbr: '71' + } + } + end + let(:mock_update_benefit_claim) do + { + return: + { benefit_claim_record: { + pre_dschrg_type_cd: nil + }, + life_cycle_record: nil, + participant_record: nil, + return_code: 'GUIE05000', + return_message: 'Update to Corporate was successful', + suspence_record: nil } + } + end + + context 'when the dependent has no open claims' do + it 'assigns the POA to the dependent via manage_ptcpnt_rlnshp' do + VCR.use_cassette('claims_api/bgs/person_web_service/manage_ptcpnt_rlnshp_poa_no_open_claims') do + VCR.use_cassette('claims_api/bgs/standard_data_web_service/find_poas') do + allow(service).to receive(:assign_poa_to_dependent_via_manage_ptcpnt_rlnshp?).and_call_original + + expect do + service.assign_poa_to_dependent! + end.not_to raise_error + + expect(service).to have_received(:assign_poa_to_dependent_via_manage_ptcpnt_rlnshp?) + end + end + end + end + + context 'when the dependent has open claims' do + it 'assigns the POA to the dependent via update_benefit_claim' do + VCR.use_cassette('claims_api/bgs/person_web_service/manage_ptcpnt_rlnshp_poa_with_open_claims') do + VCR.use_cassette('claims_api/bgs/standard_data_web_service/find_poas') do + allow(service).to receive(:assign_poa_to_dependent_via_update_benefit_claim?).and_call_original + allow_any_instance_of(ClaimsApi::LocalBGS).to receive(:find_benefit_claims_status_by_ptcpnt_id) + .with(dependent_participant_id).and_return(mock_find_benefit_claims_status_by_ptcpnt_id) + allow_any_instance_of(ClaimsApi::BenefitClaimWebService).to receive(:find_bnft_claim) + .with(claim_id: '256009').and_return(mock_find_bnft_claim) + allow_any_instance_of(ClaimsApi::BenefitClaimService).to receive(:update_benefit_claim) + .and_return(mock_update_benefit_claim) + + expect do + service.assign_poa_to_dependent! + end.not_to raise_error + + expect(service).to have_received(:assign_poa_to_dependent_via_update_benefit_claim?) + end + end + end + end + end +end