diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 536bfee49a7..5f3c1446369 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -505,6 +505,7 @@ app/swagger/swagger/requests/mdot @department-of-veterans-affairs/va-cto-health- app/swagger/swagger/requests/medical_copays.rb @department-of-veterans-affairs/vsa-debt-resolution @department-of-veterans-affairs/backend-review-group app/swagger/swagger/requests/messages @department-of-veterans-affairs/vfs-mhv-secure-messaging @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group app/swagger/swagger/requests/mvi_users.rb @department-of-veterans-affairs/octo-identity +app/swagger/swagger/requests/my_va/submission_statuses.rb @department-of-veterans-affairs/vfs-authenticated-experience-backend @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group app/swagger/swagger/requests/onsite_notifications.rb @department-of-veterans-affairs/vfs-authenticated-experience-backend @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group app/swagger/swagger/v1/requests/post911_gi_bill_statuses.rb @department-of-veterans-affairs/backend-review-group @department-of-veterans-affairs/va-iir app/swagger/swagger/requests/ppiu.rb @department-of-veterans-affairs/vfs-authenticated-experience-backend @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group @@ -642,10 +643,11 @@ app/sidekiq/feature_cleaner_job.rb @department-of-veterans-affairs/va-api-engine app/sidekiq/form1010cg @department-of-veterans-affairs/vfs-10-10 @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group app/sidekiq/form1095 @department-of-veterans-affairs/vfs-1095-b @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group app/sidekiq/form526_confirmation_email_job.rb @department-of-veterans-affairs/Disability-Experience @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group +app/sidekiq/form526_failure_state_snapshot_job.rb @department-of-veterans-affairs/disability-experience @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group app/sidekiq/form526_paranoid_success_polling_job.rb @department-of-veterans-affairs/Disability-Experience @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group -app/sidekiq/form526_state_logging_job.rb @department-of-veterans-affairs/disability-experience @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group app/sidekiq/form526_status_polling_job.rb @department-of-veterans-affairs/Disability-Experience @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group app/sidekiq/form526_submission_failed_email_job.rb @department-of-veterans-affairs/Disability-Experience @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group +app/sidekiq/form526_submission_processing_report_job.rb @department-of-veterans-affairs/disability-experience @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group app/sidekiq/gi_bill_feedback_submission_job.rb @department-of-veterans-affairs/my-education-benefits @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group app/sidekiq/hca @department-of-veterans-affairs/vfs-10-10 @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group app/sidekiq/identity @department-of-veterans-affairs/octo-identity @@ -839,6 +841,7 @@ lib/benefits_intake_service @department-of-veterans-affairs/benefits-dependents- lib/bgs @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group lib/bid @department-of-veterans-affairs/Disability-Experience @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group lib/bip_claims @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group +lib/burials @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group lib/carma @department-of-veterans-affairs/vfs-10-10 @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group lib/caseflow @department-of-veterans-affairs/lighthouse-banana-peels @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group lib/central_mail @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group @@ -1343,9 +1346,10 @@ spec/sidekiq/facilities @department-of-veterans-affairs/vfs-facilities-frontend spec/sidekiq/form1010cg @department-of-veterans-affairs/vfs-10-10 @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/sidekiq/form1095 @department-of-veterans-affairs/vfs-1095-b @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/sidekiq/form526_confirmation_email_job_spec.rb @department-of-veterans-affairs/Disability-Experience @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group +spec/sidekiq/form526_failure_state_snapshot_job_spec.rb @department-of-veterans-affairs/disability-experience @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/sidekiq/form526_paranoid_success_polling_job_spec.rb @department-of-veterans-affairs/Disability-Experience @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group -spec/sidekiq/form526_state_logging_job_spec.rb @department-of-veterans-affairs/disability-experience @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/sidekiq/form526_status_polling_job_spec.rb @department-of-veterans-affairs/Disability-Experience @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group +spec/sidekiq/form526_submission_processing_report_job_spec.rb @department-of-veterans-affairs/disability-experience @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/sidekiq/form5655 @department-of-veterans-affairs/vsa-debt-resolution @department-of-veterans-affairs/backend-review-group spec/sidekiq/gi_bill_feedback_submission_job_spec.rb @department-of-veterans-affairs/my-education-benefits @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/sidekiq/hca @department-of-veterans-affairs/vfs-10-10 @department-of-veterans-affairs/backend-review-group @department-of-veterans-affairs/va-api-engineers @@ -1379,6 +1383,7 @@ spec/lib/bgs @department-of-veterans-affairs/benefits-dependents-management @dep spec/lib/bid @department-of-veterans-affairs/Disability-Experience @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/lib/bip_claims @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/lib/breakers @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group +spec/lib/burials @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/lib/carma @department-of-veterans-affairs/vfs-10-10 @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/lib/caseflow @department-of-veterans-affairs/lighthouse-banana-peels @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/lib/central_mail @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group @@ -2126,4 +2131,5 @@ README.md @department-of-veterans-affairs/backend-review-group modules/accredited_representative_portal/spec/services/accredited_representative_portal/representative_user_loader_spec.rb @department-of-veterans-affairs/octo-identity modules/accredited_representative_portal/app/services/accredited_representative_portal/representative_user_loader.rb @department-of-veterans-affairs/octo-identity config/form_profile_mappings/FORM-MOCK-AE-DESIGN-PATTERNS.yml @department-of-veterans-affairs/tmf-auth-exp-design-patterns @department-of-veterans-affairs/backend-review-group -vets-api.pm-collection.json @department-of-veterans-affairs/backend-review-group +postman/vets-api.pm-collection.json @department-of-veterans-affairs/backend-review-group +postman/Dockerfile @department-of-veterans-affairs/backend-review-group diff --git a/.github/workflows/build-and-publish.yaml b/.github/workflows/build-and-publish.yaml index 296cf3f2731..70dfa194115 100644 --- a/.github/workflows/build-and-publish.yaml +++ b/.github/workflows/build-and-publish.yaml @@ -33,7 +33,17 @@ jobs: uses: aws-actions/amazon-ecr-login@v2.0.1 with: mask-password: true - - name: Build Docker Image + - name: Build Postman Image + uses: docker/build-push-action@v6 + env: + DOCKER_BUILD_SUMMARY: false + with: + context: ./postman + file: ./postman/Dockerfile + push: true + tags: | + ${{ steps.ecr-login.outputs.registry }}/dsva/vets-api-postman:${{ steps.version.outputs.version }} + - name: Build vets-api Docker Image uses: docker/build-push-action@v6 env: DOCKER_BUILD_SUMMARY: false diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 452b541e5f0..8d2e829b591 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,8 +30,17 @@ jobs: - name: Login to ECR id: login-ecr uses: aws-actions/amazon-ecr-login@v2 - - - name: Build Docker Image + - name: Build Postman Docker Image + uses: docker/build-push-action@v6 + env: + DOCKER_BUILD_SUMMARY: false + with: + context: ./postman + file: ./postman/Dockerfile + push: true + tags: | + ${{ steps.login-ecr.outputs.registry }}/dsva/vets-api-postman:${{ github.sha }} + - name: Build vets-api Docker Image uses: docker/build-push-action@v6 env: DOCKER_BUILD_SUMMARY: false diff --git a/Gemfile.lock b/Gemfile.lock index 689c092dacf..bfdfd1c39ba 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -512,9 +512,14 @@ GEM thor (>= 0.20, < 2.a) google-cloud-env (2.1.1) faraday (>= 1.0, < 3.a) - google-protobuf (4.28.1) + google-protobuf (4.28.2) bigdecimal rake (>= 13) + google-protobuf (4.28.2-java) + bigdecimal + ffi (~> 1) + ffi-compiler (~> 1) + rake (>= 13) googleauth (1.11.0) faraday (>= 1.0, < 3.a) google-cloud-env (~> 2.1) @@ -622,7 +627,7 @@ GEM llhttp-ffi (0.5.0) ffi-compiler (~> 1.0) rake (~> 13.0) - lockbox (1.4.0) + lockbox (1.4.1) logger (1.6.1) loofah (2.22.0) crass (~> 1.0.2) @@ -647,7 +652,7 @@ GEM mini_mime (1.1.5) mini_portile2 (2.8.7) minitest (5.25.1) - mock_redis (0.44.0) + mock_redis (0.45.0) msgpack (1.7.2) msgpack (1.7.2-java) multi_json (1.15.0) @@ -764,9 +769,9 @@ GEM psych (5.1.2-java) jar-dependencies (>= 0.1.7) public_suffix (6.0.1) - puma (6.4.2) + puma (6.4.3) nio4r (~> 2.0) - puma (6.4.2-java) + puma (6.4.3-java) nio4r (~> 2.0) pundit (2.4.0) activesupport (>= 3.0.0) @@ -930,7 +935,7 @@ GEM rubocop-factory_bot (2.26.1) rubocop (~> 1.61) rubocop-junit-formatter (0.1.4) - rubocop-rails (2.26.1) + rubocop-rails (2.26.2) activesupport (>= 4.2.0) rack (>= 1.1) rubocop (>= 1.52.0, < 2.0) diff --git a/app/controllers/v0/apidocs_controller.rb b/app/controllers/v0/apidocs_controller.rb index 5989652d783..da5ce23d00b 100644 --- a/app/controllers/v0/apidocs_controller.rb +++ b/app/controllers/v0/apidocs_controller.rb @@ -164,6 +164,7 @@ class ApidocsController < ApplicationController Swagger::Requests::Messages::TriageTeams, Swagger::Requests::MviUsers, Swagger::Requests::OnsiteNotifications, + Swagger::Requests::MyVA::SubmissionStatuses, Swagger::Requests::IncomeAndAssetsClaims, Swagger::Requests::PPIU, Swagger::Requests::PreneedsClaims, diff --git a/app/controllers/v0/backend_statuses_controller.rb b/app/controllers/v0/backend_statuses_controller.rb index 0c10422fc80..70568051fba 100644 --- a/app/controllers/v0/backend_statuses_controller.rb +++ b/app/controllers/v0/backend_statuses_controller.rb @@ -7,41 +7,48 @@ module V0 class BackendStatusesController < ApplicationController service_tag 'maintenance-windows' skip_before_action :authenticate + before_action :validate_service, only: [:show] - # NOTE: this endpoint is somewhat misleading. Index gets data from PagerDuty and - # show only looks at GI bill scheduled downtime (and gets no data from PagerDuty) def index - statuses = ExternalServicesRedis::Status.new.fetch_or_cache - maintenance_windows = MaintenanceWindow.end_after(Time.zone.now) - options = { params: { maintenance_windows: } } - render json: BackendStatusesSerializer.new(statuses, options) + render json: BackendStatusesSerializer.new(backend_statuses, options) end # GET /v0/backend_statuses/:service def show - @backend_service = params[:service] - raise Common::Exceptions::RecordNotFound, @backend_service unless recognized_service? + render json: BackendStatusSerializer.new(backend_status) + end - # get status - be_status = BackendStatus.new(name: @backend_service) - case @backend_service - when BackendServices::GI_BILL_STATUS - be_status.is_available = BenefitsEducation::Service.within_scheduled_uptime? - be_status.uptime_remaining = BenefitsEducation::Service.seconds_until_downtime - else - # default service is up! - be_status.is_available = true - be_status.uptime_remaining = 0 - end + private - render json: BackendStatusSerializer.new(be_status) + # NOTE: Data is from PagerDuty + def backend_statuses + @backend_statuses ||= ExternalServicesRedis::Status.new.fetch_or_cache end - private + def maintenance_windows + @maintenance_windows ||= MaintenanceWindow.end_after(Time.zone.now) + end + + # NOTE: Data is GI bill scheduled downtime + def backend_status + @backend_status ||= BackendStatus.new(name: backend_service) + end + + def backend_service + params[:service] + end + + def validate_service + raise Common::Exceptions::RecordNotFound, backend_service unless recognized_service? + end def recognized_service? - BackendServices.all.include?(@backend_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/v0/burial_claims_controller.rb b/app/controllers/v0/burial_claims_controller.rb index 5e3b70df986..132bddd4b01 100644 --- a/app/controllers/v0/burial_claims_controller.rb +++ b/app/controllers/v0/burial_claims_controller.rb @@ -1,52 +1,61 @@ # frozen_string_literal: true require 'pension_burial/tag_sentry' +require 'burials/monitor' module V0 class BurialClaimsController < ClaimsBaseController service_tag 'burial-application' def show - submission_attempt = determine_submission_attempt + claim = claim_class.find_by!(guid: params[:id]) + form_submission = claim&.form_submissions&.last + submission_attempt = form_submission&.form_submission_attempts&.last if submission_attempt state = submission_attempt.aasm_state == 'failure' ? 'failure' : 'success' render(json: { data: { attributes: { state: } } }) elsif central_mail_submission render json: CentralMailSubmissionSerializer.new(central_mail_submission) - else - Rails.logger.error("ActiveRecord::RecordNotFound: Claim submission not found for claim_id: #{params[:id]}") - render(json: { data: { attributes: { state: 'not found' } } }, status: :not_found) end + rescue ActiveRecord::RecordNotFound => e + monitor.track_show404(params[:id], current_user, e) + render(json: { data: { attributes: { state: 'not found' } } }, status: :not_found) rescue => e - Rails.logger.error(e.to_s) + monitor.track_show_error(params[:id], current_user, e) render(json: { data: { attributes: { state: 'error processing request' } } }, status: :unprocessable_entity) end def create PensionBurial::TagSentry.tag_sentry - claim = if Flipper.enabled?(:va_burial_v2) - # cannot parse a nil form, to pass unit tests do a check for form presence - form = filtered_params[:form] - claim_class.new(form:, formV2: form.present? ? JSON.parse(form)['formV2'] : nil) - else - claim_class.new(form: filtered_params[:form]) - end - - unless claim.save - StatsD.increment("#{stats_key}.failure") - Sentry.set_tags(team: 'benefits-memorial-1') # tag sentry logs with team name - Rails.logger.error('Burial claim was not saved', { error_messages: claim.errors, - user_uuid: current_user&.uuid, - in_progress_form_id: in_progress_form&.id }) - raise Common::Exceptions::ValidationErrors, claim - end + claim = create_claim + monitor.track_create_attempt(claim, current_user) + + track_claim_save_failure(claim) unless claim.save + # this method also calls claim.process_attachments! claim.submit_to_structured_data_services! Rails.logger.info "ClaimID=#{claim.confirmation_number} Form=#{claim.form_id}" + + in_progress_form = current_user ? InProgressForm.form_for_user(claim.form_id, current_user) : nil + claim.form_start_date = in_progress_form.created_at if in_progress_form + monitor.track_create_success(in_progress_form, claim, current_user) + clear_saved_form(claim.form_id) render json: SavedClaimSerializer.new(claim) + rescue => e + monitor.track_create_error(in_progress_form, claim, current_user, e) + raise e + end + + def create_claim + if Flipper.enabled?(:va_burial_v2) + form = filtered_params[:form] + claim_class.new(form:, formV2: form.present? ? JSON.parse(form)['formV2'] : nil) + else + claim_class.new(form: filtered_params[:form]) + end end def short_name @@ -59,12 +68,6 @@ def claim_class private - def determine_submission_attempt - claim = claim_class.find_by(guid: params[:id]) - form_submission = claim&.form_submissions&.last - form_submission&.form_submission_attempts&.last - end - def central_mail_submission CentralMailSubmission.joins(:central_mail_claim).find_by(saved_claims: { guid: params[:id] }) end @@ -72,5 +75,39 @@ def central_mail_submission def in_progress_form current_user ? InProgressForm.form_for_user(claim.form_id, current_user) : nil end + + def track_claim_save_failure(claim) + StatsD.increment("#{stats_key}.failure") + Sentry.set_tags(team: 'benefits-memorial-1') # tag sentry logs with team name + Rails.logger.error('Burial claim was not saved', { error_messages: claim.errors, + user_uuid: current_user&.uuid, + in_progress_form_id: in_progress_form&.id }) + log_validation_error_to_metadata(in_progress_form, claim) + raise Common::Exceptions::ValidationErrors, claim + end + + ## + # include validation error on in_progress_form metadata. + # `noop` if in_progress_form is `blank?` + # + # @param in_progress_form [InProgressForm] + # @param claim [Pensions::SavedClaim] + # + def log_validation_error_to_metadata(in_progress_form, claim) + return if in_progress_form.blank? + + metadata = in_progress_form.metadata + metadata['submission']['error_message'] = claim&.errors&.errors&.to_s + in_progress_form.update(metadata:) + end + + ## + # retreive a monitor for tracking + # + # @return [Burials::Monitor] + # + def monitor + @monitor ||= Burials::Monitor.new + end end end diff --git a/app/models/backend_status.rb b/app/models/backend_status.rb index 25e176f53f4..cfca2b7c2b9 100644 --- a/app/models/backend_status.rb +++ b/app/models/backend_status.rb @@ -5,14 +5,29 @@ class BackendStatus include ActiveModel::Serialization include ActiveModel::Validations - include Virtus.model(nullify_blank: true) - attribute :name, String - attribute :service_id, String - attribute :is_available, Boolean - attribute :uptime_remaining, Integer + attr_reader :name, :service_id validates :name, presence: true validates :is_available, presence: true validates :uptime_remaining, presence: true + + def initialize(name:, service_id: nil) + @name = name + @service_id = service_id + end + + def available? + gibs_service? ? BenefitsEducation::Service.within_scheduled_uptime? : true + end + + def uptime_remaining + gibs_service? ? BenefitsEducation::Service.seconds_until_downtime.to_i : 0 + end + + private + + def gibs_service? + @name == BackendServices::GI_BILL_STATUS + end end diff --git a/app/models/form_submission_attempt.rb b/app/models/form_submission_attempt.rb index b670547ab92..b6319b5d3aa 100644 --- a/app/models/form_submission_attempt.rb +++ b/app/models/form_submission_attempt.rb @@ -74,7 +74,7 @@ def log_status_change def enqueue_result_email(notification_type) config = { - form_data: form_submission.form_data, + form_data: JSON.parse(form_submission.form_data), form_number: form_submission.form_type, confirmation_number: form_submission.benefits_intake_uuid, date_submitted: created_at.strftime('%B %d, %Y'), diff --git a/app/serializers/backend_status_serializer.rb b/app/serializers/backend_status_serializer.rb index 2e3cce21318..6f714574d29 100644 --- a/app/serializers/backend_status_serializer.rb +++ b/app/serializers/backend_status_serializer.rb @@ -7,6 +7,6 @@ class BackendStatusSerializer attribute :name attribute :service_id - attribute :is_available + attribute :is_available, &:available? attribute :uptime_remaining end diff --git a/app/services/mhv/user_account/creator.rb b/app/services/mhv/user_account/creator.rb index 4ae03b32ac4..6e18cc43ea6 100644 --- a/app/services/mhv/user_account/creator.rb +++ b/app/services/mhv/user_account/creator.rb @@ -5,11 +5,11 @@ module MHV module UserAccount class Creator - attr_reader :user_verification, :cached + attr_reader :user_verification, :break_cache - def initialize(user_verification:, cached: true) + def initialize(user_verification:, break_cache: false) @user_verification = user_verification - @cached = cached + @break_cache = break_cache end def perform @@ -33,8 +33,9 @@ def create_mhv_user_account! end def mhv_account_creation_response - MHV::AccountCreation::Service.new - .create_account(icn:, email:, tou_occurred_at: current_tou_agreement.created_at) + tou_occurred_at = current_tou_agreement.created_at + + mhv_client.create_account(icn:, email:, tou_occurred_at:, break_cache:) end def icn @@ -53,6 +54,10 @@ def user_account @user_account ||= user_verification.user_account end + def mhv_client + MHV::AccountCreation::Service.new + end + def validate! errors = [ ('ICN must be present' if icn.blank?), diff --git a/app/sidekiq/benefits_intake_remediation_status_job.rb b/app/sidekiq/benefits_intake_remediation_status_job.rb index 3d7c304ab54..f92a317b628 100644 --- a/app/sidekiq/benefits_intake_remediation_status_job.rb +++ b/app/sidekiq/benefits_intake_remediation_status_job.rb @@ -24,12 +24,15 @@ def initialize(batch_size: BATCH_SIZE) # search all submissions for outstanding failures # poll LH endpoint to see if status has changed (case if endpoint had an error initially) # report stats on submissions, grouped by form-type - def perform + def perform(form_id = nil) Rails.logger.info('BenefitsIntakeRemediationStatusJob started') form_submissions = FormSubmission.includes(:form_submission_attempts) failures = outstanding_failures(form_submissions.all) + @form_id = form_id + failures.select! { |f| f.form_type == form_id } if form_id + batch_process(failures) unless failures.empty? submission_audit @@ -39,7 +42,7 @@ def perform private - attr_reader :batch_size, :total_handled + attr_reader :batch_size, :total_handled, :form_id # determine if a claim has an outstanding failure # each claim can have multiple FormSubmission, which can have multiple FormSubmissionAttempt @@ -118,10 +121,12 @@ def submission_audit form_submissions = FormSubmission.includes(:form_submission_attempts) form_submission_groups = form_submissions.all.group_by(&:form_type) - form_submission_groups.each do |form_id, submissions| + form_submission_groups.each do |form_type, submissions| + next if form_id && form_id != form_type + fs_saved_claim_ids = submissions.map(&:saved_claim_id).uniq - claims = SavedClaim.where(form_id:).where('id >= ?', fs_saved_claim_ids.min) + claims = SavedClaim.where(form_id: form_type).where('id >= ?', fs_saved_claim_ids.min) claim_ids = claims.map(&:id).uniq unsubmitted = claim_ids - fs_saved_claim_ids @@ -133,9 +138,9 @@ def submission_audit { claim_id: fs.saved_claim_id, uuid: fs.benefits_intake_uuid, error_message: last_attempt.error_message } end - StatsD.set("#{STATS_KEY}.#{form_id}.unsubmitted_claims", unsubmitted.length) - StatsD.set("#{STATS_KEY}.#{form_id}.orphaned_submissions", orphaned.length) - StatsD.set("#{STATS_KEY}.#{form_id}.outstanding_failures", failures.length) + StatsD.gauge("#{STATS_KEY}.unsubmitted_claims", unsubmitted.length, tags: ["form_id:#{form_type}"]) + StatsD.gauge("#{STATS_KEY}.orphaned_submissions", orphaned.length, tags: ["form_id:#{form_type}"]) + StatsD.gauge("#{STATS_KEY}.outstanding_failures", failures.length, tags: ["form_id:#{form_type}"]) Rails.logger.info("BenefitsIntakeRemediationStatusJob submission audit #{form_id}", form_id:, unsubmitted:, orphaned:, failures:) end diff --git a/app/sidekiq/form526_failure_state_snapshot_job.rb b/app/sidekiq/form526_failure_state_snapshot_job.rb new file mode 100644 index 00000000000..ed895299abd --- /dev/null +++ b/app/sidekiq/form526_failure_state_snapshot_job.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +# Log information about Form526Submission state to populate an admin facing Datadog dashboard +class Form526FailureStateSnapshotJob + include Sidekiq::Job + sidekiq_options retry: false + + STATSD_PREFIX = 'form526.state.snapshot' + + def perform + write_failure_snapshot + rescue => e + Rails.logger.error('Error logging 526 state snapshot', + class: self.class.name, + message: e.try(:message)) + end + + def write_failure_snapshot + state_as_counts.each do |description, count| + StatsD.gauge("#{STATSD_PREFIX}.#{description}", count) + end + end + + def state_as_counts + @state_as_counts ||= {}.tap do |abbreviation| + snapshot_state.each do |dp, ids| + abbreviation[:"#{dp}_count"] = ids.count + end + end + end + + def snapshot_state + @snapshot_state ||= load_snapshot_state + end + + def load_snapshot_state + { + total_awaiting_backup_status: Form526Submission.pending_backup.pluck(:id).sort, + total_incomplete_type: Form526Submission.incomplete_type.pluck(:id).sort, + total_failure_type: Form526Submission.failure_type.pluck(:id).sort + } + end +end diff --git a/app/sidekiq/form526_state_logging_job.rb b/app/sidekiq/form526_submission_processing_report_job.rb similarity index 66% rename from app/sidekiq/form526_state_logging_job.rb rename to app/sidekiq/form526_submission_processing_report_job.rb index 6de183f9372..a53623d357e 100644 --- a/app/sidekiq/form526_state_logging_job.rb +++ b/app/sidekiq/form526_submission_processing_report_job.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # Log information about Form526Submission state to populate an admin facing Datadog dashboard -class Form526StateLoggingJob +class Form526SubmissionProcessingReportJob include Sidekiq::Job sidekiq_options retry: false @@ -9,7 +9,6 @@ class Form526StateLoggingJob END_DATE = Time.zone.today.beginning_of_day START_DATE = END_DATE - 1.week - STATSD_PREFIX = 'form526.state' def initialize(start_date: START_DATE, end_date: END_DATE) @start_date = start_date @@ -17,7 +16,6 @@ def initialize(start_date: START_DATE, end_date: END_DATE) end def perform - write_as_gauges write_as_log rescue => e Rails.logger.error('Error logging 526 state data', @@ -29,43 +27,23 @@ def perform def write_as_log Rails.logger.info('Form 526 State Data', - state_log: counts_with_failures, + state_log: state_as_counts, start_date:, end_date:) end - def counts_with_failures - counts = state_as_counts - counts[:total_failure_type_ids] = total_failure_type - counts - end - - def base_state - @base_state ||= timeboxed_state.merge(all_time_state) - end - def state_as_counts @state_as_counts ||= {}.tap do |abbreviation| - base_state.each do |dp, ids| + timeboxed_state.each do |dp, ids| abbreviation[:"#{dp}_count"] = ids.count end end end - def write_as_gauges - state_as_counts.each do |description, count| - StatsD.gauge("#{STATSD_PREFIX}.#{description}", count) - end - end - def timeboxed_state @timeboxed_state ||= load_timeboxed_state end - def all_time_state - @all_time_state ||= load_all_time_state - end - def load_timeboxed_state { timeboxed: timeboxed_submissions.pluck(:id).sort, @@ -76,27 +54,10 @@ def load_timeboxed_state } end - def load_all_time_state - { - total_awaiting_backup_status: Form526Submission.pending_backup.pluck(:id).sort, - total_incomplete_type: Form526Submission.incomplete_type.pluck(:id).sort, - total_failure_type: - } - end - - def total_failure_type - @total_failure_type ||= Form526Submission.failure_type.pluck(:id).sort - end - def sub_arel @sub_arel ||= Form526Submission.arel_table end - def combined_pending_types_for(submissions) - submissions.incomplete.pluck(:id) + - submissions.in_process.pluck(:id) - end - def backup_submissions @backup_submissions ||= timeboxed_submissions .joins(:form526_job_statuses) diff --git a/app/sidekiq/lighthouse/submit_benefits_intake_claim.rb b/app/sidekiq/lighthouse/submit_benefits_intake_claim.rb index 34c35f76142..b14e09424f1 100644 --- a/app/sidekiq/lighthouse/submit_benefits_intake_claim.rb +++ b/app/sidekiq/lighthouse/submit_benefits_intake_claim.rb @@ -29,7 +29,7 @@ class BenefitsIntakeClaimError < StandardError; end StatsD.increment("#{STATSD_KEY_PREFIX}.exhausted") end - def perform(saved_claim_id) + def perform(saved_claim_id) # rubocop:disable Metrics/MethodLength init(saved_claim_id) @pdf_path = if @claim.form_id == '21P-530V2' @@ -46,7 +46,10 @@ def perform(saved_claim_id) Rails.logger.info('Lighthouse::SubmitBenefitsIntakeClaim succeeded', generate_log_details) StatsD.increment("#{STATSD_KEY_PREFIX}.success") - @claim.send_confirmation_email if @claim.respond_to?(:send_confirmation_email) + + send_confirmation_email + + @lighthouse_service.uuid rescue => e Rails.logger.warn('Lighthouse::SubmitBenefitsIntakeClaim failed, retrying...', generate_log_details(e)) StatsD.increment("#{STATSD_KEY_PREFIX}.failure") @@ -165,5 +168,13 @@ def cleanup_file_paths def check_zipcode(address) address['country'].upcase.in?(%w[USA US]) end + + def send_confirmation_email + @claim.respond_to?(:send_confirmation_email) && @claim.send_confirmation_email + rescue => e + Rails.logger.warn('Lighthouse::SubmitBenefitsIntakeClaim send_confirmation_email failed', + generate_log_details(e)) + StatsD.increment("#{STATSD_KEY_PREFIX}.send_confirmation_email.failure") + end end end diff --git a/app/swagger/swagger/requests/my_va/submission_statuses.rb b/app/swagger/swagger/requests/my_va/submission_statuses.rb new file mode 100644 index 00000000000..dff3632dd59 --- /dev/null +++ b/app/swagger/swagger/requests/my_va/submission_statuses.rb @@ -0,0 +1,122 @@ +# app/controllers/v0/my_va/submission_statuses_controller.rb + +# frozen_string_literal: true + +module Swagger + module Requests + module MyVA + class SubmissionStatuses + include Swagger::Blocks + + swagger_path '/v0/my_va/submission_statuses' do + operation :get do + key :description, 'Get list of submitted forms for the current session' + key :operationId, 'getSubmissionStatuses' + key :tags, %w[ + my_va + ] + + parameter :authorization + + key :produces, ['application/json'] + + response 200 do + key :description, 'submitted forms and statuses' + + schema do + key :type, :object + + property :data do + key :type, :array + items do + key :required, %i[ + id + type + attributes + ] + property :id, type: :string, example: '3b03b5a0-3ad9-4207-b61e-3a13ed1c8b80', + description: 'Form submission UID' + property :type, type: :string, example: 'submission_status', description: 'type of request' + property :attributes do + key :$ref, :SubmissionStatusAttrs + end + end + end + end + end + + response 296 do + key :description, 'submitted forms but errors occured looking up statuses from lighthouse' + + schema do + key :type, :object + key :required, %i[data errors] + property :data do + key :type, :array + items do + key :required, %i[ + id + type + attributes + ] + property :id, type: :string, example: '3b03b5a0-3ad9-4207-b61e-3a13ed1c8b80', + description: 'Form submission UID' + property :type, type: :string, example: 'submission_status', description: 'type of request' + property :attributes do + key :$ref, :SubmissionStatusAttrs + end + end + end + + property :errors do + key :type, :array + items do + key :required, %i[ + status + source + title + detail + ] + property :status, type: :integer, example: 429, description: 'Error code' + property :source, type: :string, example: 'Lighthouse - Benefits Intake API', + description: 'Error source' + property :title, type: :string, example: 'Form Submission Status: Too Many Requests', + description: 'Error description' + property :detail, type: :string, example: 'API rate limit exceeded', description: 'Error details' + end + end + end + end + end + end + + swagger_schema :SubmissionStatusAttrs do + key :type, :object + key :description, 'submitted form attributes' + + property :id, type: :string, example: '3b03b5a0-3ad9-4207-b61e-3a13ed1c8b80', + description: 'Submitted form UID from lighthouse' + property :detail, type: [:string, 'null'], example: '', + description: 'Error details (only when errors are present)' + property :form_type, type: :string, example: '21-0845', description: 'The type of form' + property :message, type: [:string, 'null'] + property :status, type: [:string, 'null'], enum: [ + nil, + 'pending', + 'uploaded', + 'received', + 'processing', + 'success', + 'vbms', + 'error', + 'expired' + ], example: 'received', description: 'The current status of the submission' + property :created_at, type: :string, example: '2023-12-15T20:40:47.583Z', + description: 'The submission record created in VA.gov' + property :updated_at, type: [:string, 'null'], example: '2023-12-15T20:40:54.474Z', + description: 'The last time the submission status was updated' + end + end + end + end +end diff --git a/config/betamocks/services_config.yml b/config/betamocks/services_config.yml index 08ee0647473..b4bd3a93623 100644 --- a/config/betamocks/services_config.yml +++ b/config/betamocks/services_config.yml @@ -595,6 +595,15 @@ :path: "/api/v2/search/i14y" :file_path: "search/default" +# Search GSA +- :name: 'Search GSA' + :base_uri: <%= "#{URI(Settings.search.gsa_url).host}:#{URI(Settings.search.gsa_url).port}" %> + :endpoints: + # Search results + - :method: :get + :path: "/technology/searchgov/v2/results/i14y" + :file_path: "search/default" + #GIS - :name: 'GIS' :base_uri: <%= "#{URI(Settings.locators.gis_base_path).host}:#{URI(Settings.locators.gis_base_path).port}" %> diff --git a/config/features.yml b/config/features.yml index dbff51ecca9..8eff7f67da5 100644 --- a/config/features.yml +++ b/config/features.yml @@ -66,6 +66,9 @@ features: caregiver_browser_monitoring_enabled: actor_type: user description: Enables Datadog Real Time User Monitoring + caregiver_carma_submitted_at: + actor_type: user + description: Enables sending CARMA the creation timestamp of a claim as a metadata submitted_at value hca_browser_monitoring_enabled: actor_type: user description: Enables browser monitoring for the health care application. @@ -830,6 +833,9 @@ features: mhv_secure_messaging_edit_contact_list: actor_type: user description: Disables/Enables Secure Messaging edit contact list page + mhv_secure_messaging_triage_group_plain_language: + actor_type: user + description: Disables/Enables Secure Messaging recipients group plain language design enable_in_development: true mhv_medical_records_allow_txt_downloads: actor_type: user @@ -989,18 +995,6 @@ features: profile_show_credential_retirement_messaging: actor_type: user description: Show/hide MHV and DS Logon credential retirement messaging in profile - profile_show_direct_deposit_single_form: - actor_type: user - description: Show/hide the single direct deposit form in profile for all users - profile_show_direct_deposit_single_form_uat: - actor_type: user - description: Show/hide the single direct deposit form just for users during UAT only - profile_show_direct_deposit_single_form_alert: - actor_type: user - description: Show/hide an alert with information around migrating to a single direct deposit form in profile - profile_show_direct_deposit_single_form_edu_downtime: - actor_type: user - description: Show/hide the edu direct deposit form and instead display a downtime message in its place profile_show_payments_notification_setting: actor_type: user description: Show/Hide the payments section of notifications in profile @@ -1703,3 +1697,7 @@ features: mgib_verifications_maintenance: actor_type: user description: Used to show maintenance alert for MGIB Verifications + search_use_v2_gsa: + actor_type: cookie_id + description: Swaps the Search Service's configuration url with an updated api.gsa.gov address + enabled_in_development: true diff --git a/config/locales/exceptions.en.yml b/config/locales/exceptions.en.yml index e339cf41181..2c17524a937 100644 --- a/config/locales/exceptions.en.yml +++ b/config/locales/exceptions.en.yml @@ -798,6 +798,30 @@ en: code: 'SEARCH_504' detail: 'Did not receive a timely response from Search.gov' status: 504 + SEARCH_GSA_400: + <<: *external_defaults + title: Bad Request + code: 'SEARCH_GSA_400' + detail: 'api.gsa.gov service responded with a Bad Request' + status: 400 + SEARCH_GSA_429: + <<: *external_defaults + title: Exceeded rate limit + code: 'SEARCH_GSA_429' + detail: 'Exceeded api.gsa.gov rate limit' + status: 429 + SEARCH_GSA_503: + <<: *external_defaults + title: Service Unavailable + code: 'SEARCH_GSA_503' + detail: 'api.gsa.gov service is currently unavailable' + status: 503 + SEARCH_GSA_504: + <<: *external_defaults + title: Gateway Timeout + code: 'SEARCH_GSA_504' + detail: 'Did not receive a timely response from api.gsa.gov' + status: 504 SEARCH_TYPEAHEAD_400: <<: *external_defaults title: Bad Request diff --git a/config/settings.yml b/config/settings.yml index 1cc10b7c9b2..001202f4978 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -900,7 +900,8 @@ search: access_key: SEARCH_GOV_ACCESS_KEY affiliate: va mock_search: false - url: https://search.usa.gov/api/v2 + gsa_url: https://api.gsa.gov/technology/searchgov/v2/results/i14y + url: https://search.usa.gov/api/v2/search/i14y # Settings for search-typeahead search_typeahead: @@ -908,7 +909,7 @@ search_typeahead: name: va url: https://api.gsa.gov/technology/searchgov/v1 - # Settings for search-click-tracking +# Settings for search-click-tracking search_click_tracking: access_key: SEARCH_GOV_ACCESS_KEY affiliate: va diff --git a/config/settings/test.yml b/config/settings/test.yml index 36d2843c03d..8bfb03d4867 100644 --- a/config/settings/test.yml +++ b/config/settings/test.yml @@ -72,7 +72,8 @@ directory: search: access_key: TESTKEY affiliate: va - url: https://search.usa.gov/api/v2 + gsa_url: https://api.gsa.gov/technology/searchgov/v2/results/i14y + url: https://search.usa.gov/api/v2/search/i14y search_typeahead: api_key: TEST_KEY diff --git a/helmCharts/vets-api/templates/postman-test.yaml b/helmCharts/vets-api/templates/postman-test.yaml new file mode 100644 index 00000000000..cad4b6ebaed --- /dev/null +++ b/helmCharts/vets-api/templates/postman-test.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "vets-api-postman-test" + annotations: + "helm.sh/hook": post-install,post-upgrade,test +spec: + containers: + - name: postman + image: "{{ .Values.image.testRepository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + env: + - name: "API_URL" + value: "http://vets-api-service" + restartPolicy: Never + \ No newline at end of file diff --git a/helmCharts/vets-api/values.yaml b/helmCharts/vets-api/values.yaml index 0a15f726682..c4e5e9c4484 100644 --- a/helmCharts/vets-api/values.yaml +++ b/helmCharts/vets-api/values.yaml @@ -3,5 +3,6 @@ replicaCount: 1 image: repository: 008577686731.dkr.ecr.us-gov-west-1.amazonaws.com/dsva/preview-environment/vets-api + testRepository: 008577686731.dkr.ecr.us-gov-west-1.amazonaws.com/dsva/vets-api-postman pullPolicy: IfNotPresent tag: "0.0.1" diff --git a/lib/burials/monitor.rb b/lib/burials/monitor.rb new file mode 100644 index 00000000000..66d1436b735 --- /dev/null +++ b/lib/burials/monitor.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +module Burials + ## + # Monitor functions for Rails logging and StatsD + # + class Monitor + CLAIM_STATS_KEY = 'api.burial_claim' + + ## + # log GET 404 from controller + # @see BurialClaimsController + # + # @param confirmation_number [UUID] saved_claim guid + # @param current_user [User] + # @param e [ActiveRecord::RecordNotFound] + # + def track_show404(confirmation_number, current_user, e) + Rails.logger.error('21P-530EZ submission not found', + { confirmation_number:, user_uuid: current_user&.uuid, message: e&.message }) + end + + ## + # log GET 500 from controller + # @see BurialClaimsController + # + # @param confirmation_number [UUID] saved_claim guid + # @param current_user [User] + # @param e [Error] + # + def track_show_error(confirmation_number, current_user, e) + Rails.logger.error('21P-530EZ fetching submission failed', + { confirmation_number:, user_uuid: current_user&.uuid, message: e&.message }) + end + + ## + # log POST processing started + # @see BurialClaimsController + # + # @param claim [Pension::SavedClaim] + # @param current_user [User] + # + def track_create_attempt(claim, current_user) + StatsD.increment("#{CLAIM_STATS_KEY}.attempt") + Rails.logger.info('21P-530EZ submission to Sidekiq begun', + { confirmation_number: claim&.confirmation_number, user_uuid: current_user&.uuid }) + end + + ## + # log POST processing failure + # @see BurialClaimsController + # + # @param in_progress_form [InProgressForm] + # @param claim [SavedClaim::Burial] + # @param current_user [User] + # @param e [Error] + # + def track_create_error(in_progress_form, claim, current_user, e = nil) + StatsD.increment("#{CLAIM_STATS_KEY}.failure") + Rails.logger.error('21P-530EZ submission to Sidekiq failed', + { confirmation_number: claim&.confirmation_number, user_uuid: current_user&.uuid, + in_progress_form_id: in_progress_form&.id, errors: claim&.errors&.errors, + message: e&.message }) + end + + ## + # log POST processing success + # @see BurialClaimsController + # + # @param in_progress_form [InProgressForm] + # @param claim [SavedClaim::Burial] + # @param current_user [User] + # + def track_create_success(in_progress_form, claim, current_user) + StatsD.increment("#{CLAIM_STATS_KEY}.success") + if claim.form_start_date + claim_duration = claim.created_at - claim.form_start_date + tags = ["form_id:#{claim.form_id}"] + StatsD.measure('saved_claim.time-to-file', claim_duration, tags:) + end + context = { + confirmation_number: claim&.confirmation_number, + user_uuid: current_user&.uuid, + in_progress_form_id: in_progress_form&.id + } + Rails.logger.info('21P-530EZ submission to Sidekiq success', context) + end + end +end diff --git a/lib/carma/models/metadata.rb b/lib/carma/models/metadata.rb index 09cb870de3c..296188a822a 100644 --- a/lib/carma/models/metadata.rb +++ b/lib/carma/models/metadata.rb @@ -9,13 +9,15 @@ module Models class Metadata < Base request_payload_key :claim_id, :claim_guid, + :submitted_at, :veteran, :primary_caregiver, :secondary_caregiver_one, :secondary_caregiver_two attr_accessor :claim_id, - :claim_guid + :claim_guid, + :submitted_at attr_reader :veteran, :primary_caregiver, @@ -25,6 +27,7 @@ class Metadata < Base def initialize(args = {}) @claim_id = args[:claim_id] @claim_guid = args[:claim_guid] + @submitted_at = args[:submitted_at] self.veteran = args[:veteran] || {} self.primary_caregiver = args[:primary_caregiver] diff --git a/lib/carma/models/submission.rb b/lib/carma/models/submission.rb index 9b2c3525ee9..c95b88d290b 100644 --- a/lib/carma/models/submission.rb +++ b/lib/carma/models/submission.rb @@ -19,6 +19,17 @@ class Submission < Base # @return [CARMA::Models::Submission] A CARMA Submission model object # def self.from_claim(claim, metadata = {}) + if Flipper.enabled?(:caregiver_carma_submitted_at) + return new( + data: claim.parsed_form, + metadata: metadata.merge( + claim_id: claim.id, + claim_guid: claim.guid, + submitted_at: claim.created_at&.iso8601 + ) + ) + end + new( data: claim.parsed_form, metadata: metadata.merge(claim_id: claim.id, claim_guid: claim.guid) diff --git a/lib/evss/disability_compensation_form/form526_to_lighthouse_transform.rb b/lib/evss/disability_compensation_form/form526_to_lighthouse_transform.rb index 2952d774100..222924e28cb 100644 --- a/lib/evss/disability_compensation_form/form526_to_lighthouse_transform.rb +++ b/lib/evss/disability_compensation_form/form526_to_lighthouse_transform.rb @@ -291,6 +291,17 @@ def transform_toxic_exposure(toxic_exposure_source) # rubocop:disable Metrics/Me MULTIPLE_EXPOSURES_TYPE[:hazard]) end + # multiple exposures could have repeated values that LH will not accept in the primary path. + # remove them! + multiple_exposures.uniq! do |exposure| + [ + exposure.exposure_dates.begin_date, + exposure.exposure_dates.end_date, + exposure.exposure_location, + exposure.hazard_exposed_to + ] + end + toxic_exposure_target.multiple_exposures = multiple_exposures toxic_exposure_target diff --git a/lib/mhv/account_creation/service.rb b/lib/mhv/account_creation/service.rb index 4251b1f0deb..6128714188e 100644 --- a/lib/mhv/account_creation/service.rb +++ b/lib/mhv/account_creation/service.rb @@ -7,13 +7,13 @@ module AccountCreation class Service < Common::Client::Base configuration Configuration - def create_account(icn:, email:, tou_occurred_at:) + def create_account(icn:, email:, tou_occurred_at:, break_cache: false) params = build_create_account_params(icn:, email:, tou_occurred_at:) - response = perform(:post, config.account_creation_path, params, authenticated_header(icn:)) - Rails.logger.info("#{config.logging_prefix} create_account success", icn:) - - normalize_response_body(response.body) + create_account_with_cache(icn:, force: break_cache, expires_in: 1.day) do + response = perform(:post, config.account_creation_path, params, authenticated_header(icn:)) + normalize_response_body(response.body) + end rescue Common::Client::Errors::ParsingError, Common::Client::Errors::ClientError => e Rails.logger.error("#{config.logging_prefix} create_account #{e.class.name.demodulize.underscore}", { error_message: e.message, body: e.body, icn: }) @@ -22,6 +22,17 @@ def create_account(icn:, email:, tou_occurred_at:) private + def create_account_with_cache(icn:, force:, expires_in:, &request) + cache_hit = true + account = Rails.cache.fetch("#{config.service_name}_#{icn}", force:, expires_in:) do + cache_hit = false + request.call + end + Rails.logger.info("#{config.logging_prefix} create_account success", { icn:, account:, from_cache: cache_hit }) + + account + end + def build_create_account_params(icn:, email:, tou_occurred_at:) { icn:, diff --git a/lib/periodic_jobs.rb b/lib/periodic_jobs.rb index 828491aa91c..e090cb8bf0e 100644 --- a/lib/periodic_jobs.rb +++ b/lib/periodic_jobs.rb @@ -66,8 +66,11 @@ # Checks all 'success' type submissions in LH to ensure they haven't changed mgr.register('0 2 * * 0', 'Form526ParanoidSuccessPollingJob') - # Log the state of Form 526 submissions to hydrate Datadog monitor - mgr.register('0 3 * * *', 'Form526StateLoggingJob') + # Log a report of 526 submission processing for a given timebox + mgr.register('5 4 * * 7', 'Form526SubmissionProcessingReportJob') + + # Log a snapshot of everything in a full failure type state + mgr.register('5 * * * *', 'Form526FailureStateSnapshotJob') # Clear out processed 22-1990 applications that are older than 1 month mgr.register('0 0 * * *', 'EducationForm::DeleteOldApplications') diff --git a/lib/scopes/form526_submission_state.rb b/lib/scopes/form526_submission_state.rb index a0b4373ed84..141261c06d9 100644 --- a/lib/scopes/form526_submission_state.rb +++ b/lib/scopes/form526_submission_state.rb @@ -112,7 +112,7 @@ module Form526SubmissionState scope :failure_type, lambda { # filtering in stages avoids timeouts. see doc for more info - allids = all.pluck(:id) + allids = where(submitted_claim_id: nil).pluck(:id) filter1 = where(id: allids - accepted_to_primary_path.pluck(:id)).pluck(:id) filter2 = where(id: filter1 - accepted_to_backup_path.pluck(:id)).pluck(:id) filter3 = where(id: filter2 - remediated.pluck(:id)).pluck(:id) diff --git a/lib/search/configuration.rb b/lib/search/configuration.rb index ea802406877..e56178d8df6 100644 --- a/lib/search/configuration.rb +++ b/lib/search/configuration.rb @@ -23,7 +23,16 @@ def mock_enabled? end def base_path - "#{Settings.search.url}/search/i14y" + flipper_enabled? ? Settings.search.gsa_url : Settings.search.url + end + + # Breakers initialization requires this configuration which means the #base_path + # is required when building the DBs in CI that flipper uses for checking toggles. + # The NoDatabaseError rescue handles times we're building new DBs. + def flipper_enabled? + Flipper.enabled?(:search_use_v2_gsa) + rescue ActiveRecord::NoDatabaseError + false end def service_name diff --git a/lib/search/service.rb b/lib/search/service.rb index 1b8d4ac018d..876f6bae028 100644 --- a/lib/search/service.rb +++ b/lib/search/service.rb @@ -7,10 +7,11 @@ require 'search/configuration' module Search - # This class builds a wrapper around Search.gov web results API. Creating a new instance of class + # This class builds a wrapper around Search.gov or api.gsa.gov web results API. Creating a new instance of class # will and calling #results will return a ResultsResponse upon success or an exception upon failure. # # @see https://search.usa.gov/sites/7378/api_instructions + # @see https://open.gsa.gov/api/searchgov-results/ # class Service < Common::Client::Base include Common::Client::Concerns::Monitoring @@ -48,6 +49,7 @@ def results_url # Optional params [enable_highlighting, limit, offset, sort_by] # # @see https://search.usa.gov/sites/7378/api_instructions + # @see https://open.gsa.gov/api/searchgov-results/ # def query_params { @@ -92,7 +94,7 @@ def handle_error(error) message = parse_messages(error).first save_error_details(message) handle_429!(error) - raise_backend_exception('SEARCH_400', self.class, error) if error.status >= 400 + raise_backend_exception(error_code_name(400), self.class, error) if error.status >= 400 else raise error end @@ -114,20 +116,22 @@ def handle_429!(error) return unless error.status == 429 StatsD.increment("#{Search::Service::STATSD_KEY_PREFIX}.exceptions", tags: ['exception:429']) - raise_backend_exception('SEARCH_429', self.class, error) + raise_backend_exception(error_code_name(error.status), self.class, error) end def handle_server_error!(error) return unless [503, 504].include?(error.status) - exceptions = { - 503 => 'SEARCH_503', - 504 => 'SEARCH_504' - } # Catch when the error's structure doesn't match what's usually expected. - message = error.body.is_a?(Hash) ? parse_messages(error).first : 'Search.gov is down' + message = error.body.is_a?(Hash) ? parse_messages(error).first : 'Search API is down' save_error_details(message) - raise_backend_exception(exceptions[error.status], self.class, error) + raise_backend_exception(error_code_name(error.status), self.class, error) + end + + def error_code_name(error_status) + error_code_prefix = self.class.configuration.flipper_enabled? ? 'SEARCH_GSA' : 'SEARCH' + + "#{error_code_prefix}_#{error_status}" end end end diff --git a/modules/accredited_representative_portal/app/controllers/accredited_representative_portal/application_controller.rb b/modules/accredited_representative_portal/app/controllers/accredited_representative_portal/application_controller.rb index 4faeb258113..4f5072b7075 100644 --- a/modules/accredited_representative_portal/app/controllers/accredited_representative_portal/application_controller.rb +++ b/modules/accredited_representative_portal/app/controllers/accredited_representative_portal/application_controller.rb @@ -9,9 +9,17 @@ class ApplicationController < SignIn::ApplicationController validates_access_token_audience Settings.sign_in.arp_client_id before_action :verify_pilot_enabled_for_user + around_action :handle_exceptions private + def handle_exceptions + yield + rescue => e + Rails.logger.error("ARP: Unexpected error occurred for user with user_uuid=#{@current_user&.uuid} - #{e.message}") + raise e + end + def verify_pilot_enabled_for_user unless Flipper.enabled?(:accredited_representative_portal_pilot, @current_user) message = 'The accredited_representative_portal_pilot feature flag is disabled ' \ diff --git a/modules/accredited_representative_portal/app/controllers/accredited_representative_portal/v0/form21a_controller.rb b/modules/accredited_representative_portal/app/controllers/accredited_representative_portal/v0/form21a_controller.rb index a1f4618aec1..7c9905816fd 100644 --- a/modules/accredited_representative_portal/app/controllers/accredited_representative_portal/v0/form21a_controller.rb +++ b/modules/accredited_representative_portal/app/controllers/accredited_representative_portal/v0/form21a_controller.rb @@ -12,13 +12,10 @@ class Form21aController < ApplicationController # Parses the request body and submits the form. # Renders the appropriate response based on the service's outcome. def submit - response = AccreditationService.submit_form21a(parsed_request_body) + response = AccreditationService.submit_form21a(parsed_request_body, @current_user&.uuid) InProgressForm.form_for_user(FORM_ID, @current_user)&.destroy if response.success? render_ogc_service_response(response) - rescue => e - Rails.logger.error("Form21aController: Unexpected error occurred - #{e.message}") - render json: { errors: 'Unexpected error' }, status: :internal_server_error end private @@ -30,19 +27,30 @@ def submit def parse_request_body @parsed_request_body = JSON.parse(request.raw_post) rescue JSON::ParserError - Rails.logger.error('Form21aController: Invalid JSON in request body') + Rails.logger.error( + "Form21aController: Invalid JSON in request body for user with user_uuid=#{@current_user&.uuid}" + ) render json: { errors: 'Invalid JSON' }, status: :bad_request end # Renders the response based on the service call's success or failure. def render_ogc_service_response(response) if response.success? + Rails.logger.info( + 'Form21aController: Form 21a successfully submitted to OGC service ' \ + "by user with user_uuid=#{@current_user&.uuid} - Response: #{response.body}" + ) render json: response.body, status: response.status elsif response.body.blank? - Rails.logger.info('Form21aController: Blank response from OGC service') + Rails.logger.info( + "Form21aController: Blank response from OGC service for user with user_uuid=#{@current_user&.uuid}" + ) render status: :no_content else - Rails.logger.error('Form21aController: Failed to parse response from external OGC service') + Rails.logger.error( + 'Form21aController: Failed to parse response from external OGC service ' \ + "for user with user_uuid=#{@current_user&.uuid}" + ) render json: { errors: 'Failed to parse response' }, status: :bad_gateway end end diff --git a/modules/accredited_representative_portal/app/services/accreditation_service.rb b/modules/accredited_representative_portal/app/services/accreditation_service.rb index fd0a2a7a4a3..bf0b8356793 100644 --- a/modules/accredited_representative_portal/app/services/accreditation_service.rb +++ b/modules/accredited_representative_portal/app/services/accreditation_service.rb @@ -8,16 +8,20 @@ class AccreditationService # self.submit_form21a(parsed_body): Submits the given parsed body as JSON to the accreditation service. # - Parameters: # - parsed_body: A Hash representing the parsed form data. + # - user_uuid: A String representing the user's UUID, which is also stored in the in_progress_forms DB entry. # - Returns: A Faraday::Response object containing the service response. - def self.submit_form21a(parsed_body) + def self.submit_form21a(parsed_body, user_uuid) + Rails.logger.info("Accreditation Service attempting submit_form21a with service_url: #{service_url}") connection.post do |req| req.body = parsed_body.to_json end rescue Faraday::ConnectionFailed => e - Rails.logger.error("Accreditation Service connection failed: #{e.message}, URL: #{service_url}") + Rails.logger.error( + "Accreditation Service connection failed for user with user_uuid=#{user_uuid}: #{e.message}, URL: #{service_url}" + ) Faraday::Response.new(status: :service_unavailable, body: { errors: 'Accreditation Service unavailable' }.to_json) rescue Faraday::TimeoutError => e - Rails.logger.error("Accreditation Service request timed out: #{e.message}") + Rails.logger.error("Accreditation Service request timed out for user with user_uuid=#{user_uuid}: #{e.message}") Faraday::Response.new(status: :request_timeout, body: { errors: 'Accreditation Service request timed out' }.to_json) end @@ -39,7 +43,7 @@ def self.service_url case Rails.env when 'development', 'test' # NOTE: the below is a temporary URL for development purposes only. - # TODO: Update this once ESECC request goes through. See: https://github.com/department-of-veterans-affairs/va.gov-team/ + # TODO: Update this once ESECC request goes through. See: https://github.com/department-of-veterans-affairs/va.gov-team/issues/88288 'http://localhost:5000/api/v1/accreditation/applications/form21a' when 'production' # TODO: Update this once MOU has been signed and the ESECC request has gone through. See: diff --git a/modules/accredited_representative_portal/config/routes.rb b/modules/accredited_representative_portal/config/routes.rb index 55381a28919..c5a94e8365e 100644 --- a/modules/accredited_representative_portal/config/routes.rb +++ b/modules/accredited_representative_portal/config/routes.rb @@ -2,13 +2,6 @@ AccreditedRepresentativePortal::Engine.routes.draw do namespace :v0, defaults: { format: :json } do - resources :power_of_attorney_requests, only: [:index] do - member do - post :accept - post :decline - end - end - get 'user', to: 'representative_users#show' post 'form21a', to: 'form21a#submit' diff --git a/modules/accredited_representative_portal/spec/requests/accredited_representative_portal/v0/form21a_spec.rb b/modules/accredited_representative_portal/spec/requests/accredited_representative_portal/v0/form21a_spec.rb index 8c53592bdd0..bb190cdb8c5 100644 --- a/modules/accredited_representative_portal/spec/requests/accredited_representative_portal/v0/form21a_spec.rb +++ b/modules/accredited_representative_portal/spec/requests/accredited_representative_portal/v0/form21a_spec.rb @@ -16,7 +16,7 @@ context 'with valid JSON' do let!(:in_progress_form) { create(:in_progress_form, form_id: '21a', user_uuid: representative_user.uuid) } - it 'returns a successful response from the service and destroys in progress form' do + it 'logs a successful submission and destroys in-progress form' do get('/accredited_representative_portal/v0/in_progress_forms/21a') expect(response).to have_http_status(:ok) expect(parsed_response.keys).to contain_exactly('formData', 'metadata') @@ -25,6 +25,11 @@ instance_double(Faraday::Response, success?: true, body: { result: 'success' }.to_json, status: 200) ) + expect(Rails.logger).to receive(:info).with( + 'Form21aController: Form 21a successfully submitted to OGC service ' \ + "by user with user_uuid=#{representative_user.uuid} - Response: {\"result\":\"success\"}" + ) + headers = { 'Content-Type' => 'application/json' } post('/accredited_representative_portal/v0/form21a', params: valid_json, headers:) @@ -38,7 +43,11 @@ end context 'with invalid JSON' do - it 'returns a bad request status' do + it 'logs the error and returns a bad request status' do + expect(Rails.logger).to receive(:error).with( + "Form21aController: Invalid JSON in request body for user with user_uuid=#{representative_user.uuid}" + ) + headers = { 'Content-Type' => 'application/json' } post('/accredited_representative_portal/v0/form21a', params: invalid_json, headers:) @@ -48,11 +57,15 @@ end context 'when service returns a blank response' do - it 'returns no content status' do + it 'logs the error and returns no content status' do allow(AccreditationService).to receive(:submit_form21a).and_return( instance_double(Faraday::Response, success?: false, body: nil, status: 204) ) + expect(Rails.logger).to receive(:info).with( + "Form21aController: Blank response from OGC service for user with user_uuid=#{representative_user.uuid}" + ) + headers = { 'Content-Type' => 'application/json' } post('/accredited_representative_portal/v0/form21a', params: valid_json, headers:) @@ -61,12 +74,17 @@ end context 'when service fails to parse response' do - it 'returns a bad gateway status' do + it 'logs the error and returns a bad gateway status' do allow(AccreditationService).to receive(:submit_form21a).and_return( instance_double(Faraday::Response, success?: false, body: { errors: 'Failed to parse response' }.to_json, status: 502) ) + expect(Rails.logger).to receive(:error).with( + 'Form21aController: Failed to parse response from external OGC service ' \ + "for user with user_uuid=#{representative_user.uuid}" + ) + headers = { 'Content-Type' => 'application/json' } post('/accredited_representative_portal/v0/form21a', params: valid_json, headers:) @@ -76,12 +94,18 @@ end context 'when an unexpected error occurs' do - it 'returns an internal server error status' do + it 'logs the error and returns an internal server error status' do allow_any_instance_of(AccreditedRepresentativePortal::V0::Form21aController) .to receive(:parse_request_body).and_raise(StandardError, 'Unexpected error') + allow(Rails.logger).to receive(:error).and_call_original + post '/accredited_representative_portal/v0/form21a' + expect(Rails.logger).to have_received(:error).with( + "ARP: Unexpected error occurred for user with user_uuid=#{representative_user.uuid} - Unexpected error" + ) + expect(response).to have_http_status(:internal_server_error) expect(parsed_response).to match( 'errors' => [ diff --git a/modules/accredited_representative_portal/spec/services/accreditation_service_spec.rb b/modules/accredited_representative_portal/spec/services/accreditation_service_spec.rb index 9d0f7df10d8..591074e8ad3 100644 --- a/modules/accredited_representative_portal/spec/services/accreditation_service_spec.rb +++ b/modules/accredited_representative_portal/spec/services/accreditation_service_spec.rb @@ -6,6 +6,7 @@ RSpec.describe AccreditationService do let(:parsed_body) { { field: 'value' } } + let(:user_uuid) { 'test-user-uuid' } describe '#submit_form21a' do context 'when the request is successful' do @@ -13,7 +14,7 @@ stub_request(:post, 'http://localhost:5000/api/v1/accreditation/applications/form21a') .to_return(status: 200, body: parsed_body.to_json, headers: { 'Content-Type' => 'application/json' }) - response = described_class.submit_form21a(parsed_body) + response = described_class.submit_form21a(parsed_body, user_uuid) expect(response.status).to eq(200) expect(response.body).to eq(parsed_body.stringify_keys) @@ -21,11 +22,16 @@ end context 'when the connection fails' do - it 'returns a service unavailable status' do + it 'logs the error and returns a service unavailable status' do stub_request(:post, 'http://localhost:5000/api/v1/accreditation/applications/form21a') .to_raise(Faraday::ConnectionFailed.new('Accreditation Service connection failed')) - response = described_class.submit_form21a(parsed_body) + expect(Rails.logger).to receive(:error).with( + "Accreditation Service connection failed for user with user_uuid=#{user_uuid}: " \ + 'Accreditation Service connection failed, URL: http://localhost:5000/api/v1/accreditation/applications/form21a' + ) + + response = described_class.submit_form21a(parsed_body, user_uuid) expect(response.status).to eq(:service_unavailable) expect(JSON.parse(response.body)['errors']).to eq('Accreditation Service unavailable') @@ -33,11 +39,15 @@ end context 'when the request times out' do - it 'returns a request timeout status' do + it 'logs the error and returns a request timeout status' do stub_request(:post, 'http://localhost:5000/api/v1/accreditation/applications/form21a') .to_raise(Faraday::TimeoutError.new('Request timed out')) - response = described_class.submit_form21a(parsed_body) + expect(Rails.logger).to receive(:error).with( + "Accreditation Service request timed out for user with user_uuid=#{user_uuid}: Request timed out" + ) + + response = described_class.submit_form21a(parsed_body, user_uuid) expect(response.status).to eq(:request_timeout) expect(JSON.parse(response.body)['errors']).to eq('Accreditation Service request timed out') 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 62cd19ad72c..2651623c496 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 @@ -141,10 +141,13 @@ def validate 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? - service = ClaimsApi::DependentClaimantVerificationService.new(target_veteran.participant_id, - form_attributes.dig('claimant', 'firstName'), - form_attributes.dig('claimant', 'lastName'), - poa_code) + 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 diff --git a/modules/claims_api/app/controllers/claims_api/v2/veterans/power_of_attorney/base_controller.rb b/modules/claims_api/app/controllers/claims_api/v2/veterans/power_of_attorney/base_controller.rb index a5bb909ecea..db5d33537c1 100644 --- a/modules/claims_api/app/controllers/claims_api/v2/veterans/power_of_attorney/base_controller.rb +++ b/modules/claims_api/app/controllers/claims_api/v2/veterans/power_of_attorney/base_controller.rb @@ -42,12 +42,16 @@ def status private def shared_form_validation(form_number) - target_veteran + base = form_number == '2122' ? 'serviceOrganization' : 'representative' + poa_code = form_attributes.dig(base, 'poaCode') + # Custom validations for POA submission, we must check this first - @claims_api_forms_validation_errors = validate_form_2122_and_2122a_submission_values(user_profile) + @claims_api_forms_validation_errors = validate_form_2122_and_2122a_submission_values( + target_veteran.participant_id, user_profile, poa_code + ) # JSON validations for POA submission, will combine with previously captured errors and raise validate_json_schema(form_number.upcase) - @rep_id = validate_registration_number!(form_number) + @rep_id = validate_registration_number!(base, poa_code) add_claimant_data_to_form if user_profile # if we get here there were only validations file errors @@ -57,10 +61,8 @@ def shared_form_validation(form_number) end end - def validate_registration_number!(form_number) - base = form_number == '2122' ? 'serviceOrganization' : 'representative' + def validate_registration_number!(base, poa_code) rn = form_attributes.dig(base, 'registrationNumber') - poa_code = form_attributes.dig(base, 'poaCode') rep = ::Veteran::Service::Representative.where('? = ANY(poa_codes) AND representative_id = ?', poa_code, rn).order(created_at: :desc).first diff --git a/modules/claims_api/app/controllers/claims_api/v2/veterans/power_of_attorney/request_controller.rb b/modules/claims_api/app/controllers/claims_api/v2/veterans/power_of_attorney/request_controller.rb index 40725d8bd2f..092ecb7d5d3 100644 --- a/modules/claims_api/app/controllers/claims_api/v2/veterans/power_of_attorney/request_controller.rb +++ b/modules/claims_api/app/controllers/claims_api/v2/veterans/power_of_attorney/request_controller.rb @@ -10,12 +10,15 @@ class PowerOfAttorney::RequestController < ClaimsApi::V2::Veterans::PowerOfAttor FORM_NUMBER = 'POA_REQUEST' def request_representative - target_veteran - @claims_api_forms_validation_errors = validate_form_2122_and_2122a_submission_values(user_profile) + poa_code = form_attributes.dig('poa', 'poaCode') + @claims_api_forms_validation_errors = validate_form_2122_and_2122a_submission_values( + target_veteran.participant_id, user_profile, poa_code + ) + validate_json_schema(FORM_NUMBER) validate_accredited_representative(form_attributes.dig('poa', 'registrationNumber'), - form_attributes.dig('poa', 'poaCode')) - validate_accredited_organization(form_attributes.dig('poa', 'poaCode')) + poa_code) + validate_accredited_organization(poa_code) # if we get here, the only errors not raised are form value validation errors if @claims_api_forms_validation_errors diff --git a/modules/claims_api/app/controllers/concerns/claims_api/disability_compensation_validations.rb b/modules/claims_api/app/controllers/concerns/claims_api/disability_compensation_validations.rb index 140664f0d87..4fdd2eb80c6 100644 --- a/modules/claims_api/app/controllers/concerns/claims_api/disability_compensation_validations.rb +++ b/modules/claims_api/app/controllers/concerns/claims_api/disability_compensation_validations.rb @@ -21,6 +21,8 @@ def validate_form_526_submission_values! validate_form_526_service_information_confinements! # ensure conflicting homelessness values are not provided validate_form_526_veteran_homelessness! + # ensure that the active duty start date is not prior to the claimants 13th birthday + validate_service_after_13th_birthday! # ensure 'militaryRetiredPay.receiving' and 'militaryRetiredPay.willReceiveInFuture' are not same non-null values validate_form_526_service_pay! # ensure 'title10ActivationDate' if provided, is after the earliest servicePeriod.activeDutyBeginDate and on or before the current date # rubocop:disable Layout/LineLength @@ -232,6 +234,23 @@ def validate_form_526_veteran_homelessness! end end + def validate_service_after_13th_birthday! + service_periods = form_attributes&.dig('serviceInformation', 'servicePeriods') + age_thirteen = auth_headers['va_eauth_birthdate'].to_datetime.next_year(13).to_date + + return if age_thirteen.nil? || service_periods.nil? + + started_before_age_thirteen = service_periods.any? do |period| + Date.parse(period['activeDutyBeginDate']) < age_thirteen + end + if started_before_age_thirteen + raise ::Common::Exceptions::UnprocessableEntity.new( + detail: "If any 'serviceInformation.servicePeriods.activeDutyBeginDate' is "\ + "before the Veteran's 13th birthdate: #{age_thirteen}, the claim can not be processed." + ) + end + end + def validate_form_526_service_pay! validate_form_526_military_retired_pay! validate_form_526_separation_pay! diff --git a/modules/claims_api/app/controllers/concerns/claims_api/v2/disability_compensation_validation.rb b/modules/claims_api/app/controllers/concerns/claims_api/v2/disability_compensation_validation.rb index 9d006adca09..c4e66bd16ac 100644 --- a/modules/claims_api/app/controllers/concerns/claims_api/v2/disability_compensation_validation.rb +++ b/modules/claims_api/app/controllers/concerns/claims_api/v2/disability_compensation_validation.rb @@ -982,13 +982,25 @@ def validate_federal_activation_values(service_information) form_obj_desc = '/serviceInformation/federalActivation' + # For a valid BDD EP code to be assigned we need these values + validate_required_values_for_federal_activation(federal_activation_date, anticipated_separation_date) + + validate_federal_activation_date(federal_activation_date, form_obj_desc) + + validate_federal_activation_date_order(federal_activation_date) if federal_activation_date.present? + if anticipated_separation_date.present? + validate_anticipated_separation_date_in_past(anticipated_separation_date) + end + end + + def validate_federal_activation_date(federal_activation_date, form_obj_desc) if federal_activation_date.blank? collect_error_if_value_not_present('federal activation date', form_obj_desc) end + end - return if anticipated_separation_date.blank? - + def validate_federal_activation_date_order(federal_activation_date) # we know the dates are present if activation_date_not_after_duty_begin_date?(federal_activation_date) collect_error_messages( @@ -996,9 +1008,41 @@ def validate_federal_activation_values(service_information) detail: 'The federalActivation date must be after the earliest service period active duty begin date.' ) end + end - validate_anticipated_separation_date_in_past(anticipated_separation_date) + # rubocop:disable Metrics/MethodLength + def validate_required_values_for_federal_activation(activation_date, separation_date) + activation_form_obj_desc = 'serviceInformation/federalActivation/' + reserves_dates_form_obj_desc = 'serviceInformation/reservesNationalGuardServce/obligationTermsOfService/' + reserves_unit_form_obj_desc = 'serviceInformation/reservesNationalGuardServce/' + + reserves = form_attributes.dig('serviceInformation', 'reservesNationalGuardService') + tos_start_date = reserves&.dig('obligationTermsOfService', 'beginDate') + tos_end_date = reserves&.dig('obligationTermsOfService', 'endDate') + unit_name = reserves&.dig('unitName') + + if activation_date.blank? + collect_error_messages(detail: 'activationDate is missing or blank', + source: activation_form_obj_desc) + end + if separation_date.blank? + collect_error_messages(detail: 'anticipatedSeparationDate is missing or blank', + source: activation_form_obj_desc) + end + if tos_start_date.blank? + collect_error_messages(detail: 'beginDate is missing or blank', + source: reserves_dates_form_obj_desc) + end + if tos_end_date.blank? + collect_error_messages(detail: 'endDate is missing or blank', + source: reserves_dates_form_obj_desc) + end + if unit_name.blank? + collect_error_messages(detail: 'unitName is missing or blank', + source: reserves_unit_form_obj_desc) + end end + # rubocop:enable Metrics/MethodLength def activation_date_not_after_duty_begin_date?(activation_date) service_information = form_attributes['serviceInformation'] diff --git a/modules/claims_api/app/controllers/concerns/claims_api/v2/power_of_attorney_validation.rb b/modules/claims_api/app/controllers/concerns/claims_api/v2/power_of_attorney_validation.rb index c1001eb50b0..9defb298c25 100644 --- a/modules/claims_api/app/controllers/concerns/claims_api/v2/power_of_attorney_validation.rb +++ b/modules/claims_api/app/controllers/concerns/claims_api/v2/power_of_attorney_validation.rb @@ -5,20 +5,21 @@ module ClaimsApi module V2 module PowerOfAttorneyValidation - def validate_form_2122_and_2122a_submission_values(user_profile) - validate_claimant(user_profile) + def validate_form_2122_and_2122a_submission_values(veteran_participant_id, user_profile, poa_code) + validate_claimant(veteran_participant_id, user_profile, poa_code) # collect errors and pass back to the controller raise_error_collection if @errors end private - def validate_claimant(user_profile) + def validate_claimant(veteran_participant_id, user_profile, poa_code) return if form_attributes['claimant'].blank? validate_claimant_id_included(user_profile) validate_address validate_relationship + validate_dependent_claimant(veteran_participant_id, user_profile, poa_code) end def validate_address @@ -95,6 +96,38 @@ def validate_relationship end end + # rubocop:disable Metrics/MethodLength + def validate_dependent_claimant(veteran_participant_id, user_profile, poa_code) + unless Flipper.enabled?(:lighthouse_claims_api_poa_dependent_claimants) && form_attributes['claimant'].present? + return + end + + claimant = user_profile.profile + service = ClaimsApi::DependentClaimantVerificationService.new(veteran_participant_id:, + claimant_first_name: claimant.given_names.first, + claimant_last_name: claimant.family_name, + claimant_participant_id: claimant.participant_id, + poa_code:) + begin + service.validate_poa_code_exists! + rescue ::Common::Exceptions::UnprocessableEntity + collect_error_messages( + source: '/poaCode', + detail: ClaimsApi::DependentClaimantVerificationService::POA_CODE_NOT_FOUND_ERROR_MESSAGE + ) + end + + begin + service.validate_dependent_by_participant_id! + rescue ::Common::Exceptions::UnprocessableEntity + collect_error_messages( + source: '/claimant/claimantId', + detail: ClaimsApi::DependentClaimantVerificationService::CLAIMANT_NOT_A_DEPENDENT_ERROR_MESSAGE + ) + end + end + # rubocop:enable Metrics/MethodLength + def validate_claimant_id_included(user_profile) claimant_icn = form_attributes.dig('claimant', 'claimantId') if (user_profile.blank? || user_profile&.status == :not_found) && claimant_icn 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 40c4dd6610b..db4c41612a3 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 @@ -5,25 +5,28 @@ module ClaimsApi class DependentClaimantVerificationService - def initialize(participant_id, dependent_first_name, dependent_last_name, poa_code) - @participant_id = participant_id - @dependent_first_name = dependent_first_name - @dependent_last_name = dependent_last_name - @poa_code = poa_code + CLAIMANT_NOT_A_DEPENDENT_ERROR_MESSAGE = 'The claimant is not listed as a dependent for the specified Veteran. ' \ + '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 = {}) + @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] + @poa_code = options[:poa_code] end def validate_dependent_by_participant_id! return if valid_participant_dependent_combo? - detail = 'The claimant is not listed as a dependent for the specified Veteran. Please submit VA Form 21-686c ' \ - 'to add this dependent.' - raise ::Common::Exceptions::UnprocessableEntity.new(detail:) + raise ::Common::Exceptions::UnprocessableEntity.new(detail: CLAIMANT_NOT_A_DEPENDENT_ERROR_MESSAGE) end def validate_poa_code_exists! return if poa_code_exists? - raise ::Common::Exceptions::UnprocessableEntity.new(detail: 'The requested POA code could not be found.') + raise ::Common::Exceptions::UnprocessableEntity.new(detail: POA_CODE_NOT_FOUND_ERROR_MESSAGE) end private @@ -33,27 +36,33 @@ def normalize(item) end def valid_participant_dependent_combo? - return false if @participant_id.blank? + return false if @veteran_participant_id.blank? 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(@participant_id) + 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] Array.wrap(dependents).any? do |dependent| - normalized_first_name_to_verify = normalize(@dependent_first_name) - normalized_last_name_to_verify = normalize(@dependent_last_name) - normalized_first_name_service = normalize(dependent[:first_nm]) - normalized_last_name_service = normalize(dependent[:last_nm]) - - return false if [normalized_first_name_to_verify, normalized_last_name_to_verify, normalized_first_name_service, - normalized_last_name_service].any?(&:blank?) - - normalized_first_name_to_verify == normalized_first_name_service && - normalized_last_name_to_verify == normalized_last_name_service + # 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 + + # 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) + normalized_claimant_last_name = normalize(@claimant_last_name) + normalized_dependent_first_name = normalize(dependent[:first_nm]) + normalized_dependent_last_name = normalize(dependent[:last_nm]) + + 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 end end 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 3970e8d0a47..94bc2f29977 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 @@ -28,6 +28,13 @@ def generate(claim_id, middle_initial) # rubocop:disable Metrics/MethodLength file_name = "#{SecureRandom.hex}.pdf" path = ::Common::FileHelpers.generate_clamav_temp_file(pdf_string, file_name) + # temporary debugging of clam av + log_job_progress( + auto_claim.id, + "526EZ PDF generator PDF existence check: #{File.exist?(path)}, file size : #{File.size?(path)}", + auto_claim.transaction_id + ) + upload = ActionDispatch::Http::UploadedFile.new({ filename: file_name, type: 'application/pdf', diff --git a/modules/claims_api/app/sidekiq/claims_api/service_base.rb b/modules/claims_api/app/sidekiq/claims_api/service_base.rb index 397ca0ce012..89b136e3d93 100644 --- a/modules/claims_api/app/sidekiq/claims_api/service_base.rb +++ b/modules/claims_api/app/sidekiq/claims_api/service_base.rb @@ -34,6 +34,10 @@ class ServiceBase end end + def retry_limits_for_notification + [11] + end + protected def preserve_original_form_data(form_data) diff --git a/modules/claims_api/app/swagger/claims_api/v1/swagger.json b/modules/claims_api/app/swagger/claims_api/v1/swagger.json index db27c9362af..39778110f32 100644 --- a/modules/claims_api/app/swagger/claims_api/v1/swagger.json +++ b/modules/claims_api/app/swagger/claims_api/v1/swagger.json @@ -979,6 +979,71 @@ } } } + }, + "422": { + "description": "Unprocessable Entity", + "content": { + "application/json": { + "example": { + "errors": [ + { + "title": "Unprocessable Entity", + "detail": "Unknown EVSS Async Error", + "code": "422", + "status": "422" + } + ] + }, + "schema": { + "required": [ + "errors" + ], + "properties": { + "errors": { + "type": "array", + "items": { + "additionalProperties": false, + "required": [ + "title", + "detail", + "code", + "status" + ], + "properties": { + "title": { + "type": "string", + "description": "HTTP error title" + }, + "detail": { + "type": "string", + "description": "HTTP error detail" + }, + "code": { + "type": "string", + "description": "HTTP error code" + }, + "status": { + "type": "string", + "description": "HTTP error code" + }, + "source": { + "type": "object", + "additionalProperties": false, + "description": "Source of error", + "properties": { + "pointer": { + "type": "string", + "description": "Pointer to source of error" + } + } + } + } + } + } + } + } + } + } } } } 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 ab775f33925..bb73cfb51dd 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 @@ -2947,6 +2947,7 @@ } }, "federalActivation": { + "description": "If federalActivation is present then reservesNationalGuardService.obligationTermsOfService.beginDate, reservesNationalGuardService.obligationTermsOfService.endDate and reservesNationalGuardService.unitName are required", "type": "object", "nullable": true, "additionalProperties": false, @@ -4330,6 +4331,7 @@ } }, "federalActivation": { + "description": "If federalActivation is present then reservesNationalGuardService.obligationTermsOfService.beginDate, reservesNationalGuardService.obligationTermsOfService.endDate and reservesNationalGuardService.unitName are required", "type": "object", "nullable": true, "additionalProperties": false, @@ -5075,7 +5077,7 @@ "202 without a transactionId": { "value": { "data": { - "id": "a51ca7e0-180e-4792-b5a4-148a10a9bdae", + "id": "74d7a3b6-8164-49c2-9dfb-fdb3fa019477", "type": "forms/526", "attributes": { "claimId": "600442191", @@ -5260,7 +5262,7 @@ }, "federalActivation": { "activationDate": "2023-10-01", - "anticipatedSeparationDate": "2024-09-18" + "anticipatedSeparationDate": "2024-09-21" }, "confinements": [ { @@ -5306,7 +5308,7 @@ "202 with a transactionId": { "value": { "data": { - "id": "3b5398d2-5851-4f40-9b81-9f03717acbff", + "id": "3356c8ae-350b-4a23-aea1-9bb00de306e1", "type": "forms/526", "attributes": { "claimId": "600442191", @@ -6488,6 +6490,7 @@ } }, "federalActivation": { + "description": "If federalActivation is present then reservesNationalGuardService.obligationTermsOfService.beginDate, reservesNationalGuardService.obligationTermsOfService.endDate and reservesNationalGuardService.unitName are required", "type": "object", "nullable": true, "additionalProperties": false, @@ -7871,6 +7874,7 @@ } }, "federalActivation": { + "description": "If federalActivation is present then reservesNationalGuardService.obligationTermsOfService.beginDate, reservesNationalGuardService.obligationTermsOfService.endDate and reservesNationalGuardService.unitName are required", "type": "object", "nullable": true, "additionalProperties": false, @@ -10031,6 +10035,7 @@ } }, "federalActivation": { + "description": "If federalActivation is present then reservesNationalGuardService.obligationTermsOfService.beginDate, reservesNationalGuardService.obligationTermsOfService.endDate and reservesNationalGuardService.unitName are required", "type": "object", "nullable": true, "additionalProperties": false, @@ -10521,7 +10526,7 @@ "application/json": { "example": { "data": { - "id": "9195a402-a9df-43ea-a62d-715e59db90c9", + "id": "e788a384-ee89-4e9e-9055-b81fa61b166d", "type": "forms/526", "attributes": { "claimProcessType": "STANDARD_CLAIM_PROCESS", @@ -14181,8 +14186,8 @@ "id": "1", "type": "intent_to_file", "attributes": { - "creationDate": "2024-09-16", - "expirationDate": "2025-09-16", + "creationDate": "2024-09-19", + "expirationDate": "2025-09-19", "type": "compensation", "status": "active" } @@ -15078,7 +15083,7 @@ "application/json": { "example": { "data": { - "id": "c231aa7f-e16b-4616-9f79-c5c5b4d1f1e3", + "id": "c97b8c40-558a-4d35-af92-57362329173a", "type": "individual", "attributes": { "code": "067", @@ -15771,7 +15776,7 @@ "application/json": { "example": { "data": { - "id": "1b2bcb3f-8af7-46d1-ba2f-cecbcd87a451", + "id": "8d551fa6-0088-4910-bbd7-08492f1f1dfc", "type": "organization", "attributes": { "code": "083", @@ -17722,10 +17727,10 @@ "application/json": { "example": { "data": { - "id": "4636c375-2259-4a3c-afe0-79bf01664c7d", + "id": "17c1d357-0b69-4ce1-a7e2-deb13931a6c6", "type": "claimsApiPowerOfAttorneys", "attributes": { - "dateRequestAccepted": "2024-09-16", + "dateRequestAccepted": "2024-09-19", "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 0ee1c313d3d..9fe4dd2c154 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 @@ -1560,6 +1560,7 @@ } }, "federalActivation": { + "description": "If federalActivation is present then reservesNationalGuardService.obligationTermsOfService.beginDate, reservesNationalGuardService.obligationTermsOfService.endDate and reservesNationalGuardService.unitName are required", "type": "object", "nullable": true, "additionalProperties": false, @@ -2943,6 +2944,7 @@ } }, "federalActivation": { + "description": "If federalActivation is present then reservesNationalGuardService.obligationTermsOfService.beginDate, reservesNationalGuardService.obligationTermsOfService.endDate and reservesNationalGuardService.unitName are required", "type": "object", "nullable": true, "additionalProperties": false, @@ -3688,7 +3690,7 @@ "202 without a transactionId": { "value": { "data": { - "id": "b636aeb7-88c0-4451-bd9b-73fd63697834", + "id": "c57050e5-558e-4f9f-9a6b-68ea79752541", "type": "forms/526", "attributes": { "claimId": "600442191", @@ -3873,7 +3875,7 @@ }, "federalActivation": { "activationDate": "2023-10-01", - "anticipatedSeparationDate": "2024-09-18" + "anticipatedSeparationDate": "2024-09-21" }, "confinements": [ { @@ -3919,7 +3921,7 @@ "202 with a transactionId": { "value": { "data": { - "id": "bf5dc2c7-c4f6-497c-b263-c9cf2ef097d0", + "id": "e981c089-c851-4c90-8a76-87a956381c2e", "type": "forms/526", "attributes": { "claimId": "600442191", @@ -5101,6 +5103,7 @@ } }, "federalActivation": { + "description": "If federalActivation is present then reservesNationalGuardService.obligationTermsOfService.beginDate, reservesNationalGuardService.obligationTermsOfService.endDate and reservesNationalGuardService.unitName are required", "type": "object", "nullable": true, "additionalProperties": false, @@ -6484,6 +6487,7 @@ } }, "federalActivation": { + "description": "If federalActivation is present then reservesNationalGuardService.obligationTermsOfService.beginDate, reservesNationalGuardService.obligationTermsOfService.endDate and reservesNationalGuardService.unitName are required", "type": "object", "nullable": true, "additionalProperties": false, @@ -8644,6 +8648,7 @@ } }, "federalActivation": { + "description": "If federalActivation is present then reservesNationalGuardService.obligationTermsOfService.beginDate, reservesNationalGuardService.obligationTermsOfService.endDate and reservesNationalGuardService.unitName are required", "type": "object", "nullable": true, "additionalProperties": false, @@ -9134,7 +9139,7 @@ "application/json": { "example": { "data": { - "id": "cc98ad06-eee9-4a56-b089-f891410097a5", + "id": "18853a53-9cc4-479a-8443-0f3c9bdea0af", "type": "forms/526", "attributes": { "claimProcessType": "STANDARD_CLAIM_PROCESS", @@ -12794,8 +12799,8 @@ "id": "1", "type": "intent_to_file", "attributes": { - "creationDate": "2024-09-16", - "expirationDate": "2025-09-16", + "creationDate": "2024-09-19", + "expirationDate": "2025-09-19", "type": "compensation", "status": "active" } @@ -13691,7 +13696,7 @@ "application/json": { "example": { "data": { - "id": "46dd81ce-150a-46af-ab26-fd631d7a8c30", + "id": "b06d7ca0-bb23-4481-b9e1-6a15351e0d63", "type": "individual", "attributes": { "code": "067", @@ -14384,7 +14389,7 @@ "application/json": { "example": { "data": { - "id": "25afc8c7-aeba-47c9-aaec-31654a9c954e", + "id": "4d21ac8b-52eb-48ea-930e-6277a1a272be", "type": "organization", "attributes": { "code": "083", @@ -16335,10 +16340,10 @@ "application/json": { "example": { "data": { - "id": "0d361496-a409-41d3-9946-5c796ba0febe", + "id": "42002b9e-354f-40b4-a7c2-5b816a934c6b", "type": "claimsApiPowerOfAttorneys", "attributes": { - "dateRequestAccepted": "2024-09-16", + "dateRequestAccepted": "2024-09-19", "previousPoa": null, "representative": { "serviceOrganization": { diff --git a/modules/claims_api/config/schemas/v2/526.json b/modules/claims_api/config/schemas/v2/526.json index b587dfc94e2..4c98b55e3c1 100644 --- a/modules/claims_api/config/schemas/v2/526.json +++ b/modules/claims_api/config/schemas/v2/526.json @@ -872,6 +872,7 @@ } }, "federalActivation": { + "description": "If federalActivation is present then reservesNationalGuardService.obligationTermsOfService.beginDate, reservesNationalGuardService.obligationTermsOfService.endDate and reservesNationalGuardService.unitName are required", "type": ["object", "null"], "nullable": true, "additionalProperties": false, diff --git a/modules/claims_api/lib/bd/bd.rb b/modules/claims_api/lib/bd/bd.rb index 0d42c6bfc2f..501acc771e4 100644 --- a/modules/claims_api/lib/bd/bd.rb +++ b/modules/claims_api/lib/bd/bd.rb @@ -122,9 +122,9 @@ def generate_upload_body(claim:, doc_type:, pdf_path:, action:, original_filenam end def determine_birls_file_number(doc_type, auth_headers) - if %w[L122 L705].include?(doc_type) + if %w[L122].include?(doc_type) birls_file_num = auth_headers['va_eauth_birlsfilenumber'] - elsif %w[L075 L190].include?(doc_type) + elsif %w[L075 L190 L705].include?(doc_type) birls_file_num = nil end birls_file_num diff --git a/modules/claims_api/lib/bgs_service/local_bgs.rb b/modules/claims_api/lib/bgs_service/local_bgs.rb index ed982050e53..b29ea73d914 100644 --- a/modules/claims_api/lib/bgs_service/local_bgs.rb +++ b/modules/claims_api/lib/bgs_service/local_bgs.rb @@ -437,6 +437,10 @@ def jrn private + def builder_to_xml(builder) + builder.to_xml(save_with: Nokogiri::XML::Node::SaveOptions::NO_DECLARATION) + end + def transform_keys(hash_or_array) transformer = lambda do |object| case object 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 1fde82af939..b108d4c70c9 100644 --- a/modules/claims_api/lib/bgs_service/person_web_service.rb +++ b/modules/claims_api/lib/bgs_service/person_web_service.rb @@ -7,15 +7,33 @@ def bean_name end def find_dependents_by_ptcpnt_id(id) - body = Nokogiri::XML::DocumentFragment.parse <<~EOXML - - EOXML - - { ptcpntId: id }.each do |k, v| - body.xpath("./*[local-name()='#{k}']").first.content = v + builder = Nokogiri::XML::Builder.new do + ptcpntId id end + body = builder_to_xml(builder) + make_request(endpoint: bean_name, action: 'findDependentsByPtcpntId', body:, key: 'DependentDTO') end + + # ptcpntIdA is the veteranʼs or dependentʼs participant id + # ptcpntIdB is the poaʼs participant id + def manage_ptcpnt_rlnshp_poa(options = {}) + builder = Nokogiri::XML::Builder.new do + PtcpntRlnshpDTO do + authznChangeClmantAddrsInd 'Y' if options[:authzn_change_clmant_addrs_ind].present? + authznPoaAccessInd 'Y' if options[:authzn_poa_access_ind].present? + compId do + ptcpntIdA options[:ptcpnt_id_a] + ptcpntIdB options[:ptcpnt_id_b] + end + statusTypeCd options[:status_type_cd] || 'CURR' + end + end + + body = builder_to_xml(builder) + + make_request(endpoint: bean_name, action: 'managePtcpntRlnshpPoa', body:, key: 'PtcpntRlnshpDTO') + end end end diff --git a/modules/claims_api/lib/claims_api/error/soap_error_handler.rb b/modules/claims_api/lib/claims_api/error/soap_error_handler.rb index aa5c0870722..88aa92e1da7 100644 --- a/modules/claims_api/lib/claims_api/error/soap_error_handler.rb +++ b/modules/claims_api/lib/claims_api/error/soap_error_handler.rb @@ -35,6 +35,10 @@ def get_exception raise ::Common::Exceptions::UnprocessableEntity.new( detail: 'Please try again after checking your input values.' ) + elsif participant_has_open_claims? + raise ::Common::Exceptions::ServiceError.new( + detail: 'PtcpntIdA has open claims.' + ) else soap_logging('500') raise ::Common::Exceptions::ServiceError.new(detail: 'An external server is experiencing difficulty.') @@ -64,6 +68,13 @@ def unprocessable? has_errors end + def participant_has_open_claims? + has_error = @fault_string.include?('PtcpntIdA has open claims') + soap_logging('422') if has_error + + has_error + end + def soap_logging(status_code) ClaimsApi::Logger.log('soap_error_handler', detail: "Returning #{status_code} via local_bgs & soap_error_handler, " \ diff --git a/modules/claims_api/lib/claims_api/v2/disability_compensation_evss_mapper.rb b/modules/claims_api/lib/claims_api/v2/disability_compensation_evss_mapper.rb index 9906c87f67c..f7bcb28bf1b 100644 --- a/modules/claims_api/lib/claims_api/v2/disability_compensation_evss_mapper.rb +++ b/modules/claims_api/lib/claims_api/v2/disability_compensation_evss_mapper.rb @@ -36,12 +36,46 @@ def claim_attributes def service_information info = @data[:serviceInformation] + + map_service_periods(info) + map_federal_activation_to_reserves(info) if info&.dig(:federalActivation).present? + map_reserves_title_ten(info) if info&.dig(:reservesNationalGuardService, :title10Activation).present? + map_confinements(info) if info&.dig(:confinements).present? + end + + def map_service_periods(info) service_periods = format_service_periods(info&.dig(:servicePeriods)) - confinements = format_confinements(info&.dig(:confinements)) if info&.dig(:confinements).present? @evss_claim[:serviceInformation] = { servicePeriods: service_periods } + end + + def map_federal_activation_to_reserves(info) + activation_date = info&.dig(:federalActivation, :activationDate) + separation_date = info&.dig(:federalActivation, :anticipatedSeparationDate) + terms_of_service = info&.dig(:reservesNationalGuardService, :obligationTermsOfService) + unit_name = info&.dig(:reservesNationalGuardService, :unitName) + + return if activation_date.blank? && separation_date.blank? + + title_ten = {} + title_ten[:title10ActivationDate] = activation_date if activation_date.present? + title_ten[:anticipatedSeparationDate] = separation_date if separation_date.present? + + begin_date = terms_of_service&.dig(:beginDate) + end_date = terms_of_service&.dig(:endDate) + + @evss_claim[:serviceInformation][:reservesNationalGuardService] = { + unitName: unit_name, + obligationTermOfServiceFromDate: begin_date, + obligationTermOfServiceToDate: end_date, + title10Activation: title_ten + } + end + + def map_confinements(info) + confinements = format_confinements(info&.dig(:confinements)) if confinements.present? @evss_claim[:serviceInformation].merge!( @@ -134,16 +168,16 @@ def veteran_meta end # Convert 12-05-1984 to 1984-12-05 for Docker container - def format_service_periods(service_period_dates) - service_period_dates.each do |sp_date| - next if sp_date[:activeDutyBeginDate].nil? + def format_service_periods(service_periods) + service_periods.each do |sp| + next if sp[:activeDutyBeginDate].nil? - begin_year = Date.strptime(sp_date[:activeDutyBeginDate], '%Y-%m-%d') - sp_date[:activeDutyBeginDate] = begin_year.strftime('%Y-%m-%d') - next if sp_date[:activeDutyEndDate].nil? + begin_year = Date.strptime(sp[:activeDutyBeginDate], '%Y-%m-%d') + sp[:activeDutyBeginDate] = begin_year.strftime('%Y-%m-%d') + next if sp[:activeDutyEndDate].nil? - end_year = Date.strptime(sp_date[:activeDutyEndDate], '%Y-%m-%d') - sp_date[:activeDutyEndDate] = end_year.strftime('%Y-%m-%d') + end_year = Date.strptime(sp[:activeDutyEndDate], '%Y-%m-%d') + sp[:activeDutyEndDate] = end_year.strftime('%Y-%m-%d') end end diff --git a/modules/claims_api/lib/claims_api/v2/error/lighthouse_error_mapper.rb b/modules/claims_api/lib/claims_api/v2/error/lighthouse_error_mapper.rb index deeb2dafc21..8414d62ae17 100644 --- a/modules/claims_api/lib/claims_api/v2/error/lighthouse_error_mapper.rb +++ b/modules/claims_api/lib/claims_api/v2/error/lighthouse_error_mapper.rb @@ -16,7 +16,7 @@ class LighthouseErrorMapper submit: 'The claim could not be established', disabled: 'this claim has been disabled', submit_save_draftForm_MaxEPCode: 'The Maximum number of EP codes have been reached for this benefit type claim code', # rubocop:disable Layout/LineLength - submit_noRetryError: 'This job is no longer able to be re-tried', + submit_noRetryError: 'Claim could not be established. Retries will fail.', header_va_eauth_birlsfilenumber_Invalid: 'There is a problem with your birls file number. Please submit an issue at ask.va.gov or call 1-800-MyVA411 (800-698-2411) for assistance.' # rubocop:disable Layout/LineLength }.freeze diff --git a/modules/claims_api/spec/lib/claims_api/bd_spec.rb b/modules/claims_api/spec/lib/claims_api/bd_spec.rb index d2761e1fe26..4cc0f044879 100644 --- a/modules/claims_api/spec/lib/claims_api/bd_spec.rb +++ b/modules/claims_api/spec/lib/claims_api/bd_spec.rb @@ -203,6 +203,14 @@ expect(js['data']['systemName']).to eq 'VA.gov' expect(js['data']['trackedItemIds']).to eq [234, 235] end + + it 'sends only a participant id and not a file number for 5103' do + result = subject.send(:generate_upload_body, claim: ews, doc_type: 'L705', original_filename: '5103.pdf', + pdf_path:, action: 'post', pctpnt_vet_id: '123456789') + js = JSON.parse(result[:parameters].read) + expect(js['data']['fileNumber']).not_to be_truthy + expect(js['data']['fileNumber']).to eq(nil) + end end describe '#build_body' do 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 a04895d4839..a6e2e7ea249 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 @@ -6,35 +6,79 @@ describe ClaimsApi::PersonWebService do subject { described_class.new external_uid: 'xUid', external_key: 'xKey' } - describe '#find_dependents_by_ptcpnt_id with one dependent' do - it 'responds as expected' do - VCR.use_cassette('claims_api/bgs/person_web_service/find_dependents_by_ptcpnt_id_one_dependent') do - result = subject.find_dependents_by_ptcpnt_id(600052699) # rubocop:disable Style/NumericLiterals - expect(result).to be_a Hash - expect(result[:dependent][:first_nm]).to eq 'MARGIE' - expect(result[:number_of_records]).to eq '1' + describe '#find_dependents_by_ptcpnt_id' do + context 'with a participant that has one dependent' do + it 'responds with one dependent' do + VCR.use_cassette('claims_api/bgs/person_web_service/find_dependents_by_ptcpnt_id_one_dependent') do + result = subject.find_dependents_by_ptcpnt_id('600052699') + expect(result).to be_a Hash + expect(result[:dependent][:first_nm]).to eq 'MARGIE' + expect(result[:number_of_records]).to eq '1' + end end end - end - describe '#find_dependents_by_ptcpnt_id with two dependents' do - it 'responds as expected' do - VCR.use_cassette('claims_api/bgs/person_web_service/find_dependents_by_ptcpnt_id_two_dependents') do - result = subject.find_dependents_by_ptcpnt_id(600049324) # rubocop:disable Style/NumericLiterals - expect(result).to be_a Hash - expect(result[:dependent].size).to eq 2 - expect(result[:dependent].first[:first_nm]).to eq 'MARK' - expect(result[:number_of_records]).to eq '2' + context 'with a participant that has two dependents' do + it 'responds with two dependents' do + VCR.use_cassette('claims_api/bgs/person_web_service/find_dependents_by_ptcpnt_id_two_dependents') do + # integers should work too + result = subject.find_dependents_by_ptcpnt_id(600049324) # rubocop:disable Style/NumericLiterals + expect(result).to be_a Hash + expect(result[:dependent]).to be_an Array + expect(result[:dependent].size).to eq 2 + expect(result[:dependent].first[:first_nm]).to eq 'MARK' + expect(result[:number_of_records]).to eq '2' + end + end + end + + context 'with a participant that has no dependents' do + it 'responds as expected' do + VCR.use_cassette('claims_api/bgs/person_web_service/find_dependents_by_ptcpnt_id_no_dependents') do + result = subject.find_dependents_by_ptcpnt_id(123) + expect(result).to be_a Hash + expect(result[:number_of_records]).to eq '0' + end end end end - describe '#find_dependents_by_ptcpnt_id with no dependents' do - it 'responds as expected' do - VCR.use_cassette('claims_api/bgs/person_web_service/find_dependents_by_ptcpnt_id_no_dependents') do - result = subject.find_dependents_by_ptcpnt_id(123) - expect(result).to be_a Hash - expect(result[:number_of_records]).to eq '0' + describe '#manage_ptcpnt_rlnshp_poa' do + context 'when participant A (the veteran or dependent) has no open claims' do + let(:ptcpnt_id_a) { '601163580' } + let(:ptcpnt_id_b) { '46004' } + + it 'assigns the POA to the participant' do + VCR.use_cassette('claims_api/bgs/person_web_service/manage_ptcpnt_rlnshp_poa_no_open_claims') do + options = { + ptcpnt_id_a:, + ptcpnt_id_b: + } + 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' + end + end + end + + context 'when participant A (the veteran or dependent) has open claims' do + it 'returns an error' do + VCR.use_cassette('claims_api/bgs/person_web_service/manage_ptcpnt_rlnshp_poa_with_open_claims') do + options = { + ptcpnt_id_a: '600052700', + ptcpnt_id_b: '46004' + } + + expect do + 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.' + } + end end end end diff --git a/modules/claims_api/spec/lib/claims_api/v2/disability_compensation_evss_mapper_spec.rb b/modules/claims_api/spec/lib/claims_api/v2/disability_compensation_evss_mapper_spec.rb index 2b322a36fd9..450b1e12c55 100644 --- a/modules/claims_api/spec/lib/claims_api/v2/disability_compensation_evss_mapper_spec.rb +++ b/modules/claims_api/spec/lib/claims_api/v2/disability_compensation_evss_mapper_spec.rb @@ -263,6 +263,16 @@ expect(service_periods[:separationLocationCode]).to eq('98282') end + it 'maps the federalActivation to reserves attributes correctly' do + reserves_addition = evss_data[:serviceInformation][:reservesNationalGuardService] + + expect(reserves_addition[:title10Activation][:title10ActivationDate]).to eq('2023-10-01') + expect(reserves_addition[:title10Activation][:anticipatedSeparationDate]).to eq('2024-10-31') + expect(reserves_addition[:obligationTermOfServiceFromDate]).to eq('2019-06-04') + expect(reserves_addition[:obligationTermOfServiceToDate]).to eq('2020-06-04') + expect(reserves_addition[:unitName]).to eq('National Guard Unit Name') + end + it 'maps the confinements attribute correctly' do first_confinement = evss_data[:serviceInformation][:confinements][0] second_confinement = evss_data[:serviceInformation][:confinements][1] diff --git a/modules/claims_api/spec/lib/claims_api/v2/disability_compensation_validation_spec.rb b/modules/claims_api/spec/lib/claims_api/v2/disability_compensation_validation_spec.rb index ac0db194416..a2e7ac06596 100644 --- a/modules/claims_api/spec/lib/claims_api/v2/disability_compensation_validation_spec.rb +++ b/modules/claims_api/spec/lib/claims_api/v2/disability_compensation_validation_spec.rb @@ -469,4 +469,104 @@ def current_error_array end end end + + describe 'validation for BDD_PROGRAM claim' do + let(:valid_service_info_for_bdd) do + { + 'servicePeriods' => [ + { + 'serviceBranch' => 'Air Force Reserves', + 'serviceComponent' => 'Reserves', + 'activeDutyBeginDate' => '2015-11-14', + 'activeDutyEndDate' => '2024-12-20' + } + ], + 'reservesNationalGuardService' => { + 'component' => 'National Guard', + 'obligationTermsOfService' => { + 'beginDate' => '1990-11-24', + 'endDate' => '1995-11-17' + }, + 'unitName' => 'National Guard Unit Name', + 'unitAddress' => '1243 Main Street', + 'unitPhone' => { + 'areaCode' => '555', + 'phoneNumber' => '5555555' + }, + 'receivingInactiveDutyTrainingPay' => 'YES' + }, + 'federalActivation' => { + 'activationDate' => '2023-10-01', + 'anticipatedSeparationDate' => '2024-12-20' + } + } + end + + def validate_field(field_path, expected_detail, expected_source) + keys = field_path.split('.') + current_hash = valid_service_info_for_bdd + + keys[0..-2].each do |key| + current_hash = current_hash[key] + end + + current_hash[keys.last] = '' # set the specified field to empty string to omit + + invalid_service_info_for_bdd = valid_service_info_for_bdd + subject.form_attributes['serviceInformation'] = invalid_service_info_for_bdd + test_526_validation_instance.send(:validate_federal_activation_values, invalid_service_info_for_bdd) + + expect(current_error_array[0][:detail]).to eq(expected_detail) + expect(current_error_array[0][:source]).to eq(expected_source) + end + + context 'when federalActivation is present' do + it 'and all the required attributes are present' do + test_526_validation_instance.send(:validate_federal_activation_values, valid_service_info_for_bdd) + expect(current_error_array).to eq(nil) + end + + # rubocop:disable RSpec/NoExpectationExample + it 'requires federalActivation.activationDate' do + validate_field( + 'federalActivation.activationDate', + 'activationDate is missing or blank', + 'serviceInformation/federalActivation/' + ) + end + + it 'requires federalActivation.anticipatedSeparationDate' do + validate_field( + 'federalActivation.anticipatedSeparationDate', + 'anticipatedSeparationDate is missing or blank', + 'serviceInformation/federalActivation/' + ) + end + + it 'requires reservesNationalGuardService.obligationTermsOfService.beginDate' do + validate_field( + 'reservesNationalGuardService.obligationTermsOfService.beginDate', + 'beginDate is missing or blank', + 'serviceInformation/reservesNationalGuardServce/obligationTermsOfService/' + ) + end + + it 'requires reservesNationalGuardService.obligationTermsOfService.endDate' do + validate_field( + 'reservesNationalGuardService.obligationTermsOfService.endDate', + 'endDate is missing or blank', + 'serviceInformation/reservesNationalGuardServce/obligationTermsOfService/' + ) + end + + it 'requires reservesNationalGuardService.unitName' do + validate_field( + 'reservesNationalGuardService.unitName', + 'unitName is missing or blank', + 'serviceInformation/reservesNationalGuardServce/' + ) + end + # rubocop:enable RSpec/NoExpectationExample + end + end end diff --git a/modules/claims_api/spec/requests/v1/forms/526_spec.rb b/modules/claims_api/spec/requests/v1/forms/526_spec.rb index 14fcffced46..f270002b005 100644 --- a/modules/claims_api/spec/requests/v1/forms/526_spec.rb +++ b/modules/claims_api/spec/requests/v1/forms/526_spec.rb @@ -9,14 +9,15 @@ 'X-VA-First-Name': 'WESLEY', 'X-VA-Last-Name': 'FORD', 'X-Consumer-Username': 'TestConsumer', - 'X-VA-Birth-Date': '1986-05-06T00:00:00+00:00', + 'X-VA-Birth-Date': '1956-05-06T00:00:00+00:00', 'X-VA-Gender': 'M' } end let(:scopes) { %w[claim.write] } let(:multi_profile) do MPI::Responses::FindProfileResponse.new( status: :ok, - profile: FactoryBot.build(:mpi_profile, participant_id: nil, participant_ids: %w[123456789 987654321]) + profile: FactoryBot.build(:mpi_profile, participant_id: nil, participant_ids: %w[123456789 987654321], + birth_date: '19560506') ) end @@ -538,6 +539,8 @@ VCR.use_cassette('claims_api/brd/countries') do par = json_data par['data']['attributes']['veteran']['changeOfAddress'] = change_of_address + par['data']['attributes']['serviceInformation']['servicePeriods'][0]['activeDutyEndDate'] = + '2007-08-01' post path, params: par.to_json, headers: headers.merge(auth_header) expect(response).to have_http_status(:bad_request) @@ -601,6 +604,24 @@ } end + context "When an activeDutyBeginDate is before a Veteran's 13th birthday" do + it 'raise an error' do + mock_acg(scopes) do |auth_header| + VCR.use_cassette('claims_api/bgs/claims/claims') do + VCR.use_cassette('claims_api/brd/countries') do + headers['X-VA-Birth-Date'] = '1986-05-06T00:00:00+00:00' + par = json_data + par['data']['attributes']['serviceInformation']['servicePeriods'][0]['activeDutyEndDate'] = + '2007-08-01' + + post path, params: par.to_json, headers: headers.merge(auth_header) + expect(response).to have_http_status(:unprocessable_entity) + end + end + end + end + end + context "'title10ActivationDate' validations" do context 'when title10ActivationDate is prior to earliest servicePeriod.activeDutyBeginDate' do let(:title10_activation_date) { '1980-02-04' } @@ -1069,7 +1090,7 @@ def obj.class icn: '1012832025V743496', first_name: 'Wesley', last_name: 'Ford', - birth_date: '19630211', + birth_date: '19590211', loa: { current: 3, highest: 3 }, edipi: nil, ssn: '796043735', @@ -1123,10 +1144,10 @@ def obj.class let(:profile_with_edipi) do MPI::Responses::FindProfileResponse.new( status: 'OK', - profile: FactoryBot.build(:mpi_profile, edipi: '2536798') + profile: FactoryBot.build(:mpi_profile, edipi: '2536798', birth_date: '19560506') ) end - let(:profile) { build(:mpi_profile) } + let(:profile) { build(:mpi_profile, birth_date: '19560506') } let(:mpi_profile_response) { build(:find_profile_response, profile:) } it 'returns a 422 without an edipi' do @@ -1173,7 +1194,7 @@ def obj.class end context 'when consumer is Veteran, but is missing a participant id' do - let(:profile) { build(:mpi_profile) } + let(:profile) { build(:mpi_profile, birth_date: '19560506') } let(:mpi_profile_response) { build(:find_profile_response, profile:) } it 'raises a 422, with message' do @@ -1205,7 +1226,7 @@ def obj.class context 'when Veteran has participant_id' do context 'when Veteran is missing a birls_id' do before do - stub_mpi(build(:mpi_profile, birls_id: nil)) + stub_mpi(build(:mpi_profile, birls_id: nil, birth_date: '19560506')) end it 'returns an unprocessible entity status' do @@ -1221,7 +1242,7 @@ def obj.class context 'when Veteran has multiple participant_ids' do before do - stub_mpi(build(:mpi_profile, birls_id: nil)) + stub_mpi(build(:mpi_profile, birls_id: nil, birth_date: '19560506')) end it 'returns an unprocessible entity status' do diff --git a/modules/claims_api/spec/requests/v1/forms/rswag_526_spec.rb b/modules/claims_api/spec/requests/v1/forms/rswag_526_spec.rb index d6b3b5dbb4f..ed0a17e601f 100644 --- a/modules/claims_api/spec/requests/v1/forms/rswag_526_spec.rb +++ b/modules/claims_api/spec/requests/v1/forms/rswag_526_spec.rb @@ -80,7 +80,7 @@ let(:'X-VA-Last-Name') { 'FORD' } parameter SwaggerSharedComponents::V1.header_params[:veteran_birth_date_header] - let(:'X-VA-Birth-Date') { '1986-05-06T00:00:00+00:00' } + let(:'X-VA-Birth-Date') { '1965-05-06T00:00:00+00:00' } let(:Authorization) { 'Bearer token' } parameter SwaggerSharedComponents::V1.body_examples[:disability_compensation] @@ -272,7 +272,7 @@ def append_example_metadata(example, response) let(:'X-VA-Last-Name') { 'FORD' } parameter SwaggerSharedComponents::V1.header_params[:veteran_birth_date_header] - let(:'X-VA-Birth-Date') { '1986-05-06T00:00:00+00:00' } + let(:'X-VA-Birth-Date') { '1965-05-06T00:00:00+00:00' } let(:Authorization) { 'Bearer token' } attachment_description = <<~VERBIAGE @@ -495,7 +495,7 @@ def append_example_metadata(example, response) let(:'X-VA-Last-Name') { 'FORD' } parameter SwaggerSharedComponents::V1.header_params[:veteran_birth_date_header] - let(:'X-VA-Birth-Date') { '1986-05-06T00:00:00+00:00' } + let(:'X-VA-Birth-Date') { '1965-05-06T00:00:00+00:00' } let(:Authorization) { 'Bearer token' } parameter SwaggerSharedComponents::V1.body_examples[:disability_compensation] @@ -658,7 +658,7 @@ def append_example_metadata(example, response) let(:'X-VA-Last-Name') { 'FORD' } parameter SwaggerSharedComponents::V1.header_params[:veteran_birth_date_header] - let(:'X-VA-Birth-Date') { '1986-05-06T00:00:00+00:00' } + let(:'X-VA-Birth-Date') { '1965-05-06T00:00:00+00:00' } let(:Authorization) { 'Bearer token' } attachment_description = <<~VERBIAGE diff --git a/modules/claims_api/spec/requests/v1/rswag_claims_spec.rb b/modules/claims_api/spec/requests/v1/rswag_claims_spec.rb index 4770b01afee..abb7b36ebfd 100644 --- a/modules/claims_api/spec/requests/v1/rswag_claims_spec.rb +++ b/modules/claims_api/spec/requests/v1/rswag_claims_spec.rb @@ -286,6 +286,44 @@ end end end + + describe 'Getting a 422 response' do + response '422', 'Unprocessable Entity' do + schema JSON.parse(Rails.root.join('spec', 'support', 'schemas', 'claims_api', 'errors', + 'default.json').read) + + let(:scopes) { %w[claim.read] } + let(:claim) do + create(:auto_established_claim_with_supporting_documents, :status_errored) + end + let(:id) { claim.id } + + before do |example| + stub_poa_verification + + claim.evss_response = [] # induce a 422 response + allow(ClaimsApi::AutoEstablishedClaim).to receive(:find_by).and_return(claim) + + mock_acg(scopes) do + VCR.use_cassette('claims_api/bgs/claims/claim') do + submit_request(example.metadata) + end + end + end + + after do |example| + example.metadata[:response][:content] = { + 'application/json' => { + example: JSON.parse(response.body, symbolize_names: true) + } + } + end + + it 'returns a 422 response' do |example| + assert_response_matches_metadata(example.metadata) + end + end + end end end end diff --git a/modules/claims_api/spec/requests/v2/veterans/power_of_attorney/2122_spec.rb b/modules/claims_api/spec/requests/v2/veterans/power_of_attorney/2122_spec.rb index b73cabee120..4197b05d6f0 100644 --- a/modules/claims_api/spec/requests/v2/veterans/power_of_attorney/2122_spec.rb +++ b/modules/claims_api/spec/requests/v2/veterans/power_of_attorney/2122_spec.rb @@ -22,6 +22,8 @@ first_name: 'George', last_name: 'Washington') Veteran::Service::Organization.create!(poa: organization_poa_code, name: "#{organization_poa_code} - DISABLED AMERICAN VETERANS") + + Flipper.disable(:lighthouse_claims_api_poa_dependent_claimants) end describe 'submit2122' do @@ -267,7 +269,7 @@ 'power_of_attorney', '2122', 'valid.json').read end - it 'returns a success response' do + it 'returns an error response' do mock_ccg(scopes) do |auth_header| allow_any_instance_of(local_bgs).to receive(:find_poa_history_by_ptcpnt_id) .and_return({ person_poa_history: nil }) @@ -326,6 +328,95 @@ end end + context 'when the lighthouse_claims_api_poa_dependent_claimants feature is enabled' do + let(:request_body) do + Rails.root.join('modules', 'claims_api', 'spec', 'fixtures', 'v2', 'veterans', + 'power_of_attorney', '2122', 'valid.json').read + end + let(:user_profile) do + MPI::Responses::FindProfileResponse.new( + status: :ok, + profile: MPI::Models::MviProfile.new( + given_names: %w[Not Under], + family_name: 'Test', + participant_id: '123' + ) + ) + end + + before do + Flipper.enable(:lighthouse_claims_api_poa_dependent_claimants) + + allow_any_instance_of(ClaimsApi::V2::Veterans::PowerOfAttorney::BaseController) + .to receive(:user_profile).and_return(user_profile) + end + + context 'and the request includes a claimant' do + it 'calls validate_poa_code_exists! and validate_dependent_by_participant_id!' do + VCR.use_cassette('claims_api/mpi/find_candidate/valid_icn_full') do + mock_ccg(%w[claim.write claim.read]) do |auth_header| + json = JSON.parse(request_body) + json['data']['attributes']['claimant'] = { claimantId: '123' } + request_body = json.to_json + + expect_any_instance_of(ClaimsApi::DependentClaimantVerificationService) + .to receive(:validate_poa_code_exists!) + expect_any_instance_of(ClaimsApi::DependentClaimantVerificationService) + .to receive(:validate_dependent_by_participant_id!) + + post validate2122_path, params: request_body, headers: auth_header + end + end + end + end + + context 'and the request does not include a claimant' do + it 'does not call validate_poa_code_exists! and validate_dependent_by_participant_id!' do + VCR.use_cassette('claims_api/mpi/find_candidate/valid_icn_full') do + mock_ccg(%w[claim.write claim.read]) do |auth_header| + json = JSON.parse(request_body) + request_body = json.to_json + + expect_any_instance_of(ClaimsApi::DependentClaimantVerificationService) + .not_to receive(:validate_poa_code_exists!) + expect_any_instance_of(ClaimsApi::DependentClaimantVerificationService) + .not_to receive(:validate_dependent_by_participant_id!) + + post validate2122_path, params: request_body, headers: auth_header + end + end + end + end + end + + context 'when the lighthouse_claims_api_poa_dependent_claimants feature is disabled' do + let(:request_body) do + Rails.root.join('modules', 'claims_api', 'spec', 'fixtures', 'v2', 'veterans', + 'power_of_attorney', '2122', 'valid.json').read + end + + before do + Flipper.disable(:lighthouse_claims_api_poa_dependent_claimants) + end + + it 'does not call validate_poa_code_exists! and validate_dependent_by_participant_id!' do + VCR.use_cassette('claims_api/mpi/find_candidate/valid_icn_full') do + mock_ccg(%w[claim.write claim.read]) do |auth_header| + json = JSON.parse(request_body) + json['data']['attributes']['claimant'] = { claimantId: '123' } + request_body = json.to_json + + expect_any_instance_of(ClaimsApi::DependentClaimantVerificationService) + .not_to receive(:validate_poa_code_exists!) + expect_any_instance_of(ClaimsApi::DependentClaimantVerificationService) + .not_to receive(:validate_dependent_by_participant_id!) + + post validate2122_path, params: request_body, headers: auth_header + end + end + end + end + context 'when no claimantId is provided and other claimant data is present' do let(:request_body) do Rails.root.join('modules', 'claims_api', 'spec', 'fixtures', 'v2', 'veterans', diff --git a/modules/claims_api/spec/requests/v2/veterans/power_of_attorney/2122a_spec.rb b/modules/claims_api/spec/requests/v2/veterans/power_of_attorney/2122a_spec.rb index 4cda3b35f0d..562f44d9b37 100644 --- a/modules/claims_api/spec/requests/v2/veterans/power_of_attorney/2122a_spec.rb +++ b/modules/claims_api/spec/requests/v2/veterans/power_of_attorney/2122a_spec.rb @@ -21,6 +21,8 @@ first_name: 'Abraham', last_name: 'Lincoln') Veteran::Service::Representative.create!(representative_id: '999999999999', poa_codes: [organization_poa_code], first_name: 'George', last_name: 'Washington') + + Flipper.disable(:lighthouse_claims_api_poa_dependent_claimants) end describe 'appoint_individual' do @@ -509,6 +511,95 @@ end end + context 'when the lighthouse_claims_api_poa_dependent_claimants feature is enabled' do + let(:request_body) do + Rails.root.join('modules', 'claims_api', 'spec', 'fixtures', 'v2', 'veterans', + 'power_of_attorney', '2122a', 'valid.json').read + end + let(:user_profile) do + MPI::Responses::FindProfileResponse.new( + status: :ok, + profile: MPI::Models::MviProfile.new( + given_names: %w[Not Under], + family_name: 'Test', + participant_id: '123' + ) + ) + end + + before do + Flipper.enable(:lighthouse_claims_api_poa_dependent_claimants) + + allow_any_instance_of(ClaimsApi::V2::Veterans::PowerOfAttorney::BaseController) + .to receive(:user_profile).and_return(user_profile) + end + + context 'and the request includes a claimant' do + it 'calls validate_poa_code_exists! and validate_dependent_by_participant_id!' do + VCR.use_cassette('claims_api/mpi/find_candidate/valid_icn_full') do + mock_ccg(%w[claim.write claim.read]) do |auth_header| + json = JSON.parse(request_body) + json['data']['attributes']['claimant'] = { claimantId: '123' } + request_body = json.to_json + + expect_any_instance_of(ClaimsApi::DependentClaimantVerificationService) + .to receive(:validate_poa_code_exists!) + expect_any_instance_of(ClaimsApi::DependentClaimantVerificationService) + .to receive(:validate_dependent_by_participant_id!) + + post validate2122a_path, params: request_body, headers: auth_header + end + end + end + end + + context 'and the request does not include a claimant' do + it 'does not call validate_poa_code_exists! and validate_dependent_by_participant_id!' do + VCR.use_cassette('claims_api/mpi/find_candidate/valid_icn_full') do + mock_ccg(%w[claim.write claim.read]) do |auth_header| + json = JSON.parse(request_body) + request_body = json.to_json + + expect_any_instance_of(ClaimsApi::DependentClaimantVerificationService) + .not_to receive(:validate_poa_code_exists!) + expect_any_instance_of(ClaimsApi::DependentClaimantVerificationService) + .not_to receive(:validate_dependent_by_participant_id!) + + post validate2122a_path, params: request_body, headers: auth_header + end + end + end + end + end + + context 'when the lighthouse_claims_api_poa_dependent_claimants feature is disabled' do + let(:request_body) do + Rails.root.join('modules', 'claims_api', 'spec', 'fixtures', 'v2', 'veterans', + 'power_of_attorney', '2122a', 'valid.json').read + end + + before do + Flipper.disable(:lighthouse_claims_api_poa_dependent_claimants) + end + + it 'does not call validate_poa_code_exists! and validate_dependent_by_participant_id!' do + VCR.use_cassette('claims_api/mpi/find_candidate/valid_icn_full') do + mock_ccg(%w[claim.write claim.read]) do |auth_header| + json = JSON.parse(request_body) + json['data']['attributes']['claimant'] = { claimantId: '123' } + request_body = json.to_json + + expect_any_instance_of(ClaimsApi::DependentClaimantVerificationService) + .not_to receive(:validate_poa_code_exists!) + expect_any_instance_of(ClaimsApi::DependentClaimantVerificationService) + .not_to receive(:validate_dependent_by_participant_id!) + + post validate2122a_path, params: request_body, headers: auth_header + end + end + end + end + context 'when no claimantId is provided and other claimant data is present' do let(:request_body) do Rails.root.join('modules', 'claims_api', 'spec', 'fixtures', 'v2', 'veterans', diff --git a/modules/claims_api/spec/requests/v2/veterans/power_of_attorney/power_of_attorney_request_spec.rb b/modules/claims_api/spec/requests/v2/veterans/power_of_attorney/power_of_attorney_request_spec.rb index 8a0139593d2..4429e03d495 100644 --- a/modules/claims_api/spec/requests/v2/veterans/power_of_attorney/power_of_attorney_request_spec.rb +++ b/modules/claims_api/spec/requests/v2/veterans/power_of_attorney/power_of_attorney_request_spec.rb @@ -17,6 +17,8 @@ first_name: 'Abraham', last_name: 'Lincoln', user_types: ['veteran_service_officer']) Veteran::Service::Organization.create!(poa: '067', name: 'DISABLED AMERICAN VETERANS') + + Flipper.disable(:lighthouse_claims_api_poa_dependent_claimants) end context 'CCG (Client Credentials Grant) flow' do diff --git a/modules/claims_api/spec/services/dependent_claimant_verification_service_spec.rb b/modules/claims_api/spec/services/dependent_claimant_verification_service_spec.rb index ad1585e86c3..2979f236057 100644 --- a/modules/claims_api/spec/services/dependent_claimant_verification_service_spec.rb +++ b/modules/claims_api/spec/services/dependent_claimant_verification_service_spec.rb @@ -7,11 +7,14 @@ let(:valid_participant_id_one_dependent) { 600052699 } # rubocop:disable Style/NumericLiterals let(:valid_participant_id_two_dependents) { 600049324 } # rubocop:disable Style/NumericLiterals - context 'when the dependent name belongs to a participant with one dependent' do + context 'when the claimant name belongs to a participantʼs (one) dependent' do let(:valid_first_name) { 'margie' } # case should not matter let(:valid_last_name) { 'CURTIS' } - subject { described_class.new(valid_participant_id_one_dependent, valid_first_name, valid_last_name, nil) } + subject do + described_class.new(veteran_participant_id: valid_participant_id_one_dependent, + claimant_first_name: valid_first_name, claimant_last_name: valid_last_name) + end it 'returns nil and does not raise an error' do VCR.use_cassette('claims_api/bgs/person_web_service/find_dependents_by_ptcpnt_id_one_dependent') do @@ -23,8 +26,11 @@ end end - context 'when the dependent name does not belong to a participant with one dependent' do - subject { described_class.new(valid_participant_id_one_dependent, 'BAD', 'NAME', nil) } + context 'when the claimant name does not belong to a participantʼs (one) dependent' do + subject do + described_class.new(veteran_participant_id: valid_participant_id_one_dependent, claimant_first_name: 'BAD', + claimant_last_name: 'NAME') + end it 'raises an error' do VCR.use_cassette('claims_api/bgs/person_web_service/find_dependents_by_ptcpnt_id_one_dependent') do @@ -35,11 +41,14 @@ end end - context 'when the dependent name belongs to a participant with two dependents' do + context 'when the claimant name belongs to one of participantʼs (many) dependents' do let(:valid_first_name) { 'MARK' } let(:valid_last_name) { ' bailey ' } # case and whitespace should not matter - subject { described_class.new(valid_participant_id_two_dependents, valid_first_name, valid_last_name, nil) } + subject do + described_class.new(veteran_participant_id: valid_participant_id_two_dependents, + claimant_first_name: valid_first_name, claimant_last_name: valid_last_name) + end it 'returns nil and does not raise an error' do VCR.use_cassette('claims_api/bgs/person_web_service/find_dependents_by_ptcpnt_id_two_dependents') do @@ -51,8 +60,11 @@ end end - context 'when the dependent name does not belong to a participant with two dependents' do - subject { described_class.new(valid_participant_id_two_dependents, 'bad', 'name', nil) } + context 'when the claimant name does not belong to one of participantʼs (many) dependents' do + subject do + described_class.new(veteran_participant_id: valid_participant_id_two_dependents, claimant_first_name: 'bad', + claimant_last_name: 'name') + end it 'raises an error' do VCR.use_cassette('claims_api/bgs/person_web_service/find_dependents_by_ptcpnt_id_two_dependents') do @@ -63,8 +75,68 @@ end end + context 'when the claimant provides a valid participant_id of claimantʼs dependent' do + let(:valid_claimant_participant_id) { 600052700 } # rubocop:disable Style/NumericLiterals + + subject do + described_class.new(veteran_participant_id: valid_participant_id_one_dependent, claimant_first_name: 'any', + claimant_last_name: 'name', claimant_participant_id: valid_claimant_participant_id) + end + + it 'returns nil and does not raise an error regardless of name' do + VCR.use_cassette('claims_api/bgs/person_web_service/find_dependents_by_ptcpnt_id_one_dependent') do + expect do + ret = subject.validate_dependent_by_participant_id! + expect(ret).to eq(nil) + end.not_to raise_error + end + end + end + + context 'when the claimant provides an invalid participant_id but a valid first and last name' do + let(:valid_first_name) { 'MARGIE' } + let(:valid_last_name) { 'CURTIS' } + + subject do + described_class.new(veteran_participant_id: valid_participant_id_one_dependent, + claimant_first_name: valid_first_name, claimant_last_name: valid_last_name, + claimant_participant_id: 'bad') + end + + it 'raises an error' do + VCR.use_cassette('claims_api/bgs/person_web_service/find_dependents_by_ptcpnt_id_one_dependent') do + expect do + subject.validate_dependent_by_participant_id! + end.to raise_error(Common::Exceptions::UnprocessableEntity) + end + end + end + + # NOTE: This test is the same as the first test but with a different description to emphasize that + # we will fall back to name matching if the claimant_participant_id is not provided. + context 'when the claimant provides no claimant_participant_id but a valid first and last name' do + let(:valid_first_name) { 'MARGIE' } + let(:valid_last_name) { 'CURTIS' } + + subject do + described_class.new(veteran_participant_id: valid_participant_id_one_dependent, + claimant_first_name: valid_first_name, claimant_last_name: valid_last_name) + + it 'returns nil and does not raise an error' do + VCR.use_cassette('claims_api/bgs/person_web_service/find_dependents_by_ptcpnt_id_one_dependent') do + expect do + ret = subject.validate_dependent_by_participant_id! + expect(ret).to eq(nil) + end.not_to raise_error + end + end + end + end + context 'when the participant_id is invalid or has no dependents' do - subject { described_class.new(123, 'any', 'name', nil) } + subject do + described_class.new(veteran_participant_id: 'bad', claimant_first_name: 'any', claimant_last_name: 'name') + end it 'raises an error' do VCR.use_cassette('claims_api/bgs/person_web_service/find_dependents_by_ptcpnt_id_no_dependents') do @@ -76,7 +148,9 @@ end context 'when the participant_id is blank' do - subject { described_class.new('', 'any', 'name', nil) } + subject do + described_class.new(veteran_participant_id: '', claimant_first_name: 'any', claimant_last_name: 'name') + end it 'raises an error' do expect do @@ -86,7 +160,9 @@ end context 'when the participant_id is nil' do - subject { described_class.new(nil, 'any', 'name', nil) } + subject do + described_class.new(veteran_participant_id: nil, claimant_first_name: 'any', claimant_last_name: 'name') + end it 'raises an error' do expect do @@ -99,7 +175,7 @@ describe '#validate_poa_code_exists!' do let(:valid_poa_code) { '002' } - subject { described_class.new(nil, nil, nil, valid_poa_code) } + subject { described_class.new(poa_code: valid_poa_code) } context 'when the poa_code is valid' do it 'returns nil and does not raise an error' do @@ -113,7 +189,7 @@ end context 'when the poa_code is invalid' do - subject { described_class.new(nil, nil, nil, 'bad') } + subject { described_class.new(poa_code: 'bad') } it 'raises an error' do VCR.use_cassette('claims_api/bgs/standard_data_web_service/find_poas') do @@ -125,7 +201,7 @@ end context 'when the poa_code is blank' do - subject { described_class.new(nil, nil, nil, '') } + subject { described_class.new(poa_code: '') } it 'raises an error' do expect do @@ -135,7 +211,7 @@ end context 'when the poa_code is nil' do - subject { described_class.new(nil, nil, nil, nil) } + subject { described_class.new(poa_code: nil) } it 'raises an error' do expect do diff --git a/modules/claims_api/spec/sidekiq/evidence_waiver_builder_job_spec.rb b/modules/claims_api/spec/sidekiq/evidence_waiver_builder_job_spec.rb index 927cf96726a..14318ab4b95 100644 --- a/modules/claims_api/spec/sidekiq/evidence_waiver_builder_job_spec.rb +++ b/modules/claims_api/spec/sidekiq/evidence_waiver_builder_job_spec.rb @@ -19,6 +19,14 @@ end end + describe '#retry_limits_for_notification' do + it "provides the method definition for sidekiq 'retry_monitoring.rb'" do + res = described_class.new.retry_limits_for_notification + expect(res).to eq([11]) + expect(described_class.new.respond_to?(:retry_limits_for_notification)).to eq(true) + end + end + describe 'when an errored job has exhausted its retries' do it 'logs to the ClaimsApi Logger' do error_msg = 'An error occurred from the Evidence Waiver Builder Job' diff --git a/modules/mobile/app/controllers/mobile/v0/appointments_controller.rb b/modules/mobile/app/controllers/mobile/v0/appointments_controller.rb index 19c97dd946d..07807756f52 100644 --- a/modules/mobile/app/controllers/mobile/v0/appointments_controller.rb +++ b/modules/mobile/app/controllers/mobile/v0/appointments_controller.rb @@ -137,6 +137,7 @@ def appointments_cache_interface def authorize raise_access_denied unless current_user.authorize(:vaos, :access?) raise_access_denied_no_icn if current_user.icn.blank? + raise_access_denied_no_facilities unless current_user.authorize(:vaos, :facilities_access?) end def raise_access_denied @@ -147,6 +148,10 @@ def raise_access_denied_no_icn raise Common::Exceptions::Forbidden, detail: 'No patient ICN found' end + def raise_access_denied_no_facilities + raise Common::Exceptions::Forbidden, detail: 'No facility associated with user' + end + def staging_custom_error if Settings.vsp_environment != 'production' && @current_user.email == 'vets.gov.user+141@gmail.com' raise Mobile::V0::Exceptions::CustomErrors.new( diff --git a/modules/mobile/app/controllers/mobile/v0/claims_and_appeals_controller.rb b/modules/mobile/app/controllers/mobile/v0/claims_and_appeals_controller.rb index 9f672583f14..b64bfe1738c 100644 --- a/modules/mobile/app/controllers/mobile/v0/claims_and_appeals_controller.rb +++ b/modules/mobile/app/controllers/mobile/v0/claims_and_appeals_controller.rb @@ -62,7 +62,15 @@ def get_claim def get_appeal appeal = evss_claims_proxy.get_appeal(params[:id]) - appeal = appeal_adapter.parse(appeal) if Flipper.enabled?(:mobile_appeal_model, @current_user) + begin + appeal = appeal_adapter.parse(appeal) if Flipper.enabled?(:mobile_appeal_model, @current_user) + rescue => e + PersonalInformationLog.create!( + data: { appeal:, error: e.message }, + error_class: 'MobileAppealModelValidationError' + ) + end + render json: Mobile::V0::AppealSerializer.new(appeal) end diff --git a/modules/mobile/app/models/mobile/v0/appeals/appeal.rb b/modules/mobile/app/models/mobile/v0/appeals/appeal.rb index fa45e241788..e88682f7004 100644 --- a/modules/mobile/app/models/mobile/v0/appeals/appeal.rb +++ b/modules/mobile/app/models/mobile/v0/appeals/appeal.rb @@ -5,6 +5,9 @@ module Mobile module V0 module Appeals + # This model is derived from the following docs: https://developer.va.gov/explore/api/appeals-status/docs?version=current + # We do not use the endpoint that these docs are for but instead share the same upstream service. + # The endpoint does not change the data in anyway so the docs should still be accurate. class Appeal < Common::Resource AOJ_TYPES = Types::String.enum( 'vba', @@ -28,6 +31,7 @@ class Appeal < Common::Resource 'medical', 'burial', 'bva', + 'fiduciary', 'other', 'multiple' ) @@ -43,7 +47,9 @@ class Appeal < Common::Resource 'decision_soon', 'blocked_by_vso', 'scheduled_dro_hearing', - 'dro_hearing_no_show' + 'dro_hearing_no_show', + 'evidentiary_period', + 'ama_post_decision' ) EVENT_TYPES = Types::String.enum( @@ -70,9 +76,19 @@ class Appeal < Common::Resource 'ramp_notice', 'transcript', 'remand_return', - 'dro_hearing_held', - 'dro_hearing_cancelled', - 'dro_hearing_no_show' + 'ama_nod', + 'docket_change', + 'distributed_to_vlj', + 'bva_decision_effectuation', + 'dta_decision', + 'sc_request', + 'sc_decision', + 'sc_other_close', + 'hlr_request', + 'hlr_decision', + 'hlr_dta_error', + 'hlr_other_close', + 'statutory_opt_in' ) LAST_ACTION_TYPES = Types::String.enum( @@ -106,26 +122,53 @@ class Appeal < Common::Resource 'other_close', 'remand_ssoc', 'remand', - 'merged' + 'merged', + 'evidentiary_period', + 'ama_remand', + 'post_bva_dta_decision', + 'bva_decision_effectuation', + 'sc_received', + 'sc_decision', + 'sc_closed', + 'hlr_received', + 'hlr_dta_error', + 'hlr_decision', + 'hlr_closed', + 'statutory_opt_in' + ) + + APPEAL_TYPES = Types::String.enum( + 'legacyAppeal', + 'appeal', + 'supplementalClaim', + 'higherLevelReview' ) attribute :id, Types::String attribute :appealIds, Types::Array.of(Types::String) + attribute :updated, Types::DateTime attribute :active, Types::Bool - attribute :alerts, Types::Array do - attribute :type, ALERT_TYPES - attribute :details, Types::Hash - end - attribute :aod, Types::Bool.optional + attribute :incompleteHistory, Types::Bool attribute :aoj, AOJ_TYPES + attribute :programArea, PROGRAM_AREA_TYPES attribute :description, Types::String - attribute :docket, Docket.optional - attribute :events, Types::Array do - attribute :type, EVENT_TYPES - attribute :date, Types::Date + attribute :type, APPEAL_TYPES + attribute :aod, Types::Bool.optional + attribute :location, LOCATION_TYPES + attribute :status do + attribute :type, STATUS_TYPES + attribute :details do + attribute? :lastSocDate, Types::Date + attribute? :certificationTimeliness, Types::Array.of(Integer) + attribute? :ssocTimeliness, Types::Array.of(Integer) + attribute? :decisionTimeliness, Types::Array.of(Integer) + attribute? :remandTimeliness, Types::Array.of(Integer) + attribute? :socTimeliness, Types::Array.of(Integer) + attribute? :remandSsocTimeliness, Types::Array.of(Integer) + attribute? :returnTimeliness, Types::Array.of(Integer) + end end - attribute :evidence, Types::Array.of(Evidence).optional - attribute :incompleteHistory, Types::Bool + attribute :docket, Docket.optional attribute :issues, Types::Array do attribute :active, Types::Bool attribute :lastAction, LAST_ACTION_TYPES.optional @@ -133,14 +176,15 @@ class Appeal < Common::Resource attribute :diagnosticCode, Types::String.optional attribute :date, Types::Date end - attribute :location, LOCATION_TYPES - attribute :programArea, PROGRAM_AREA_TYPES - attribute :status do - attribute :type, STATUS_TYPES + attribute :alerts, Types::Array do + attribute :type, ALERT_TYPES attribute :details, Types::Hash end - attribute :type, Types::String - attribute :updated, Types::DateTime + attribute :events, Types::Array do + attribute :type, EVENT_TYPES + attribute :date, Types::Date + end + attribute :evidence, Types::Array.of(Evidence).optional end end end diff --git a/modules/mobile/app/models/mobile/v0/appeals/status_detail.rb b/modules/mobile/app/models/mobile/v0/appeals/status_detail.rb new file mode 100644 index 00000000000..d027f9237ec --- /dev/null +++ b/modules/mobile/app/models/mobile/v0/appeals/status_detail.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require 'common/models/resource' + +module Mobile + module V0 + module Appeals + class StatusDetail < Common::Resource + attribute :lastSocDate, Types::Date.optional + attribute :certificationTimeliness, Types::Array.of(Integer).optional + attribute :ssocTimeliness, Types::Array.of(Integer).optional + attribute :decisionTimeliness, Types::Array.of(Integer).optional + attribute :remandTimeliness, Types::Array.of(Integer).optional + attribute :socTimeliness, Types::Array.of(Integer).optional + attribute :remandSsocTimeliness, Types::Array.of(Integer).optional + attribute :returnTimeliness, Types::Array.of(Integer).optional + end + end + end +end diff --git a/modules/mobile/docs/examples/appointments/forbidden_facilities_error.yml b/modules/mobile/docs/examples/appointments/forbidden_facilities_error.yml new file mode 100644 index 00000000000..9f907a19c68 --- /dev/null +++ b/modules/mobile/docs/examples/appointments/forbidden_facilities_error.yml @@ -0,0 +1,5 @@ +errors: + - title: 'Forbidden' + detail: 'No facility associated with user' + code: '403' + status: '403' \ No newline at end of file diff --git a/modules/mobile/docs/examples/appointments/forbidden_no_icn_error.yml b/modules/mobile/docs/examples/appointments/forbidden_no_icn_error.yml new file mode 100644 index 00000000000..d42492771c0 --- /dev/null +++ b/modules/mobile/docs/examples/appointments/forbidden_no_icn_error.yml @@ -0,0 +1,5 @@ +errors: + - title: 'Forbidden' + detail: 'No patient ICN found' + code: '403' + status: '403' \ No newline at end of file diff --git a/modules/mobile/docs/examples/appointments/forbidden_no_vaos_access_error.yml b/modules/mobile/docs/examples/appointments/forbidden_no_vaos_access_error.yml new file mode 100644 index 00000000000..c72018a54b2 --- /dev/null +++ b/modules/mobile/docs/examples/appointments/forbidden_no_vaos_access_error.yml @@ -0,0 +1,5 @@ +errors: + - title: 'Forbidden' + detail: 'You do not have access to online scheduling' + code: '403' + status: '403' \ No newline at end of file diff --git a/modules/mobile/docs/index.html b/modules/mobile/docs/index.html index c5dcc805d50..8299ecb43bf 100755 --- a/modules/mobile/docs/index.html +++ b/modules/mobile/docs/index.html @@ -2174,11 +2174,15 @@ .lbYftx:focus{outline:auto #1d8127;}/*!sc*/ .NAUPn{display:block;border:0;width:100%;text-align:left;padding:10px;border-radius:2px;margin-bottom:4px;line-height:1.5em;cursor:pointer;color:#d41f1c;background-color:rgba(212,31,28,0.07);}/*!sc*/ .NAUPn:focus{outline:auto #d41f1c;}/*!sc*/ +.cGFwjB{display:block;border:0;width:100%;text-align:left;padding:10px;border-radius:2px;margin-bottom:4px;line-height:1.5em;cursor:pointer;color:#d41f1c;background-color:rgba(212,31,28,0.07);cursor:default;}/*!sc*/ +.cGFwjB:focus{outline:auto #d41f1c;}/*!sc*/ +.cGFwjB::before{content:"—";font-weight:bold;width:1.5em;text-align:center;display:inline-block;vertical-align:top;}/*!sc*/ +.cGFwjB:focus{outline:0;}/*!sc*/ .jUGDyD{display:block;border:0;width:100%;text-align:left;padding:10px;border-radius:2px;margin-bottom:4px;line-height:1.5em;cursor:pointer;color:#1d8127;background-color:rgba(29,129,39,0.07);cursor:default;}/*!sc*/ .jUGDyD:focus{outline:auto #1d8127;}/*!sc*/ .jUGDyD::before{content:"—";font-weight:bold;width:1.5em;text-align:center;display:inline-block;vertical-align:top;}/*!sc*/ .jUGDyD:focus{outline:0;}/*!sc*/ -data-styled.g119[id="sc-htmcrh"]{content:"lbYftx,NAUPn,jUGDyD,"}/*!sc*/ +data-styled.g119[id="sc-htmcrh"]{content:"lbYftx,NAUPn,cGFwjB,jUGDyD,"}/*!sc*/ .cMoEZ{vertical-align:top;}/*!sc*/ data-styled.g123[id="sc-fWWYYk"]{content:"cMoEZ,"}/*!sc*/ .DvFer{font-size:1.3em;padding:0.2em 0;margin:3em 0 1.1em;color:#333333;font-weight:normal;}/*!sc*/ @@ -2252,7 +2256,7 @@

Request samples

Content type
application/json
{
  • "environment": "dev",
  • "buildNumber": "10",
  • "os": "android"
}

Response samples

Content type
application/json
{}

/v0/appeal/{id}

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

+

Request samples

Content type
application/json
{
  • "environment": "dev",
  • "buildNumber": "10",
  • "os": "android"
}

Response samples

Content type
application/json
{}

/v0/appeal/{id}

Returns info on all user's claims and appeals for mobile overview page. Should be identical to the following docs https://developer.va.gov/explore/api/appeals-status/docs?version=current

Authorizations:
Bearer
path Parameters
id
required
string

Appeal Id

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

Allows the API to return camelCase keys rather than snake_case

Responses

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

Allows the API to return camelCase keys rather than snake_case

Responses

Response samples

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