diff --git a/app/sidekiq/evss/disability_compensation_form/form0781_document_upload_failure_email.rb b/app/sidekiq/evss/disability_compensation_form/form0781_document_upload_failure_email.rb new file mode 100644 index 00000000000..2d6f2100bfa --- /dev/null +++ b/app/sidekiq/evss/disability_compensation_form/form0781_document_upload_failure_email.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +require 'va_notify/service' + +module EVSS + module DisabilityCompensationForm + class Form0781DocumentUploadFailureEmail < Job + STATSD_METRIC_PREFIX = 'api.form_526.veteran_notifications.form0781_upload_failure_email' + + # retry for one day + sidekiq_options retry: 14 + + sidekiq_retries_exhausted do |msg, _ex| + job_id = msg['jid'] + error_class = msg['error_class'] + error_message = msg['error_message'] + timestamp = Time.now.utc + form526_submission_id = msg['args'].first + + # Job status records are upserted in the JobTracker module + # when the retryable_error_handler is called + form_job_status = Form526JobStatus.find_by(job_id:) + bgjob_errors = form_job_status.bgjob_errors || {} + new_error = { + "#{timestamp.to_i}": { + caller_method: __method__.to_s, + timestamp:, + form526_submission_id: + } + } + form_job_status.update( + status: Form526JobStatus::STATUS[:exhausted], + bgjob_errors: bgjob_errors.merge(new_error) + ) + + Rails.logger.warn( + 'Form0781DocumentUploadFailureEmail retries exhausted', + { + job_id:, + timestamp:, + form526_submission_id:, + error_class:, + error_message: + } + ) + + StatsD.increment("#{STATSD_METRIC_PREFIX}.exhausted") + rescue => e + Rails.logger.error( + 'Failure in Form0781DocumentUploadFailureEmail#sidekiq_retries_exhausted', + { + job_id:, + messaged_content: e.message, + submission_id: form526_submission_id, + pre_exhaustion_failure: { + error_class:, + error_message: + } + } + ) + raise e + end + + def perform(form526_submission_id) + form526_submission = Form526Submission.find(form526_submission_id) + + with_tracking('Form0781DocumentUploadFailureEmail', form526_submission.saved_claim_id, form526_submission_id) do + notify_client = VaNotify::Service.new(Settings.vanotify.services.benefits_disability.api_key) + + email_address = form526_submission.veteran_email_address + first_name = form526_submission.get_first_name + date_submitted = form526_submission.format_creation_time_for_mailers + + notify_client.send_email( + email_address:, + template_id: mailer_template_id, + personalisation: { + first_name:, + date_submitted: + } + ) + + StatsD.increment("#{STATSD_METRIC_PREFIX}.success") + end + rescue => e + retryable_error_handler(e) + end + + private + + def retryable_error_handler(error) + # Needed to log the error properly in the Sidekiq::Form526JobStatusTracker::JobTracker, + # which is included near the top of this job's inheritance tree in EVSS::DisabilityCompensationForm::JobStatus + super(error) + raise error + end + + def mailer_template_id + Settings.vanotify.services + .benefits_disability.template_id.form0781_upload_failure_notification_template_id + end + end + end +end diff --git a/app/sidekiq/evss/disability_compensation_form/submit_form0781.rb b/app/sidekiq/evss/disability_compensation_form/submit_form0781.rb index eac3530139f..48d73e938d3 100644 --- a/app/sidekiq/evss/disability_compensation_form/submit_form0781.rb +++ b/app/sidekiq/evss/disability_compensation_form/submit_form0781.rb @@ -66,6 +66,10 @@ class SubmitForm0781 < Job StatsD.increment("#{STATSD_KEY_PREFIX}.exhausted") + if Flipper.enabled?(:form526_send_0781_failure_notification) + EVSS::DisabilityCompensationForm::Form0781DocumentUploadFailureEmail.perform_async(form526_submission_id) + end + ::Rails.logger.warn( 'Submit Form 0781 Retries exhausted', { job_id:, error_class:, error_message:, timestamp:, form526_submission_id: } diff --git a/app/sidekiq/lighthouse/submit_benefits_intake_claim.rb b/app/sidekiq/lighthouse/submit_benefits_intake_claim.rb index a4b69c3d7ce..2602ed8ff6b 100644 --- a/app/sidekiq/lighthouse/submit_benefits_intake_claim.rb +++ b/app/sidekiq/lighthouse/submit_benefits_intake_claim.rb @@ -29,7 +29,6 @@ class BenefitsIntakeClaimError < StandardError; end StatsD.increment("#{STATSD_KEY_PREFIX}.exhausted") end - # rubocop:disable Metrics/MethodLength def perform(saved_claim_id) @claim = SavedClaim.find(saved_claim_id) @@ -39,20 +38,11 @@ def perform(saved_claim_id) else process_record(@claim) end - @attachment_paths = @claim.persistent_attachments.map do |record| - process_record(record) - end + @attachment_paths = @claim.persistent_attachments.map { |record| process_record(record) } create_form_submission_attempt - payload = { - upload_url: @lighthouse_service.location, - file: split_file_and_path(@pdf_path), - metadata: generate_metadata.to_json, - attachments: @attachment_paths.map(&method(:split_file_and_path)) - } - - response = @lighthouse_service.upload_doc(**payload) + response = @lighthouse_service.upload_doc(**lighthouse_service_upload_payload) raise BenefitsIntakeClaimError, response.body unless response.success? Rails.logger.info('Lighthouse::SubmitBenefitsIntakeClaim succeeded', generate_log_details) @@ -65,7 +55,6 @@ def perform(saved_claim_id) cleanup_file_paths end - # rubocop:enable Metrics/MethodLength def generate_metadata form = @claim.parsed_form veteran_full_name = form['veteranFullName'] @@ -84,7 +73,6 @@ def generate_metadata SimpleFormsApiSubmission::MetadataValidator.validate(metadata, zip_code_is_us_based: check_zipcode(address)) end - # rubocop:disable Metrics/MethodLength def process_record(record, timestamp = nil, form_id = nil) pdf_path = record.to_pdf stamped_path1 = PDFUtilities::DatestampPdf.new(pdf_path).run(text: 'VA.GOV', x: 5, y: 5, timestamp:) @@ -95,29 +83,41 @@ def process_record(record, timestamp = nil, form_id = nil) text_only: true ) if form_id.present? && ['21P-530V2'].include?(form_id) - PDFUtilities::DatestampPdf.new(stamped_path2).run( - text: 'Application Submitted on va.gov', - x: 425, - y: 675, - text_only: true, # passing as text only because we override how the date is stamped in this instance - timestamp:, - page_number: 5, - size: 9, - template: "lib/pdf_fill/forms/pdfs/#{form_id}.pdf", - multistamp: true - ) + stamped_pdf_with_form(form_id, stamped_path2, timestamp) else stamped_path2 end end - # rubocop:enable Metrics/MethodLength def split_file_and_path(path) { file: path, file_name: path.split('/').last } end private + def lighthouse_service_upload_payload + { + upload_url: @lighthouse_service.location, + file: split_file_and_path(@pdf_path), + metadata: generate_metadata.to_json, + attachments: @attachment_paths.map(&method(:split_file_and_path)) + } + end + + def stamped_pdf_with_form(form_id, path, timestamp) + PDFUtilities::DatestampPdf.new(path).run( + text: 'Application Submitted on va.gov', + x: 425, + y: 675, + text_only: true, # passing as text only because we override how the date is stamped in this instance + timestamp:, + page_number: 5, + size: 9, + template: "lib/pdf_fill/forms/pdfs/#{form_id}.pdf", + multistamp: true + ) + end + def generate_log_details(e = nil) details = { claim_id: @claim.id, diff --git a/config/features.yml b/config/features.yml index ef4f5f670ca..830c2729701 100644 --- a/config/features.yml +++ b/config/features.yml @@ -588,6 +588,10 @@ features: actor_type: user description: Enables enqueuing a Form4142DocumentUploadFailureEmail if a SubmitForm4142Job job exhausts its retries enable_in_development: true + form526_send_0781_failure_notification: + actor_type: user + description: Enables enqueuing a Form0781DocumentUploadFailureEmail if a SubmitForm0781Job job exhausts its retries + enable_in_development: true form0994_confirmation_email: actor_type: user description: Enables form 0994 email submission confirmation (VaNotify) @@ -1173,6 +1177,13 @@ features: show_meb_service_history_categorize_disagreement: actor_type: user enable_in_development: false + show_meb_5490_maintenance_alert: + actor_type: user + description: Displays an alert to users on 5490 intro page that the Backend Service is Down. + enable_in_development: false + meb_1606_30_automation: + actor_type: user + description: Enables MEB form to handle Chapter 1606/30 forms as well as Chapter 33. meb_exclusion_period_enabled: actor_type: user description: enables exclusion period checks diff --git a/config/settings.yml b/config/settings.yml index 556f14ddb44..0b00e0d1ba4 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -1356,6 +1356,7 @@ vanotify: template_id: form526_document_upload_failure_notification_template_id: form526_document_upload_failure_notification_template_id form4142_upload_failure_notification_template_id: form4142_upload_failure_notification_template_id + form0781_upload_failure_notification_template_id: form0781_upload_failure_notification_template_id ivc_champva: api_key: fake_secret template_id: diff --git a/lib/bgs/form674.rb b/lib/bgs/form674.rb index 6f18f071228..977e40c5d9e 100644 --- a/lib/bgs/form674.rb +++ b/lib/bgs/form674.rb @@ -14,18 +14,17 @@ module BGS class Form674 include SentryLogging - attr_reader :user, :saved_claim + attr_reader :user, :saved_claim, :proc_id def initialize(user, saved_claim) @user = user + @proc_id = vnp_proc_id @saved_claim = saved_claim @end_product_name = '130 - Automated School Attendance 674' @end_product_code = '130SCHATTEBN' end - # rubocop:disable Metrics/MethodLength def submit(payload) - proc_id = create_proc_id_and_form veteran = VnpVeteran.new(proc_id:, payload:, user:, claim_type: '130SCHATTEBN').create process_relationships(proc_id, veteran, payload) @@ -38,16 +37,7 @@ def submit(payload) # temporary logging to troubleshoot log_message_to_sentry("#{proc_id} - #{@end_product_code}", :warn, '', { team: 'vfs-ebenefits' }) - benefit_claim_record = BenefitClaim.new( - args: { - vnp_benefit_claim: vnp_benefit_claim_record, - veteran:, - user:, - proc_id:, - end_product_name: @end_product_name, - end_product_code: @end_product_code - } - ).create + benefit_claim_record = BenefitClaim.new(args: benefit_claim_args(vnp_benefit_claim_record, veteran)).create begin vnp_benefit_claim.update(benefit_claim_record, vnp_benefit_claim_record) @@ -60,19 +50,23 @@ def submit(payload) bgs_service.update_proc(proc_id, proc_state: 'MANUAL_VAGOV') rescue - Rails.logger.warning('BGS::Form674.submit failed after creating benefit claim in BGS', - { - user_uuid: user.uuid, - saved_claim_id: saved_claim.id, - icn: user.icn, - error: e.message - }) + log_submit_failure(error) end end - # rubocop:enable Metrics/MethodLength private + def benefit_claim_args(vnp_benefit_claim_record, veteran) + { + vnp_benefit_claim: vnp_benefit_claim_record, + veteran:, + user:, + proc_id:, + end_product_name: @end_product_name, + end_product_code: @end_product_code + } + end + def process_relationships(proc_id, veteran, payload) dependent = DependentHigherEdAttendance.new(proc_id:, payload:, user: @user).create @@ -96,7 +90,7 @@ def process_674(proc_id, dependent, payload) ).create end - def create_proc_id_and_form + def vnp_proc_id vnp_response = bgs_service.create_proc(proc_state: 'MANUAL_VAGOV') bgs_service.create_proc_form( vnp_response[:vnp_proc_id], @@ -130,6 +124,16 @@ def set_claim_type(proc_state) end end + def log_submit_failure(error) + Rails.logger.warning('BGS::Form674.submit failed after creating benefit claim in BGS', + { + user_uuid: user.uuid, + saved_claim_id: saved_claim.id, + icn: user.icn, + error: error.message + }) + end + def bgs_service BGS::Service.new(@user) end diff --git a/lib/bgs/marriages.rb b/lib/bgs/marriages.rb index 7fb16382825..348311b8413 100644 --- a/lib/bgs/marriages.rb +++ b/lib/bgs/marriages.rb @@ -22,7 +22,6 @@ def create_all private - # rubocop:disable Metrics/MethodLength def report_marriage_history(type) @dependents_application[type].each do |former_spouse| former_marriage = BGSDependents::MarriageHistory.new(former_spouse) @@ -35,22 +34,25 @@ def report_marriage_history(type) participant, 'Spouse', 'Ex-Spouse', - { - type:, - begin_date: marriage_info['start_date'], - marriage_country: marriage_info['marriage_country'], - marriage_state: marriage_info['marriage_state'], - marriage_city: marriage_info['marriage_city'], - divorce_state: marriage_info['divorce_state'], - divorce_city: marriage_info['divorce_city'], - divorce_country: marriage_info['divorce_country'], - end_date: marriage_info['end_date'], - marriage_termination_type_code: marriage_info['marriage_termination_type_code'] - } + spouse_dependent_optional_fields(type, marriage_info) ) end end - # rubocop:enable Metrics/MethodLength + + def spouse_dependent_optional_fields(type, marriage_info) + { + type:, + begin_date: marriage_info['start_date'], + marriage_country: marriage_info['marriage_country'], + marriage_state: marriage_info['marriage_state'], + marriage_city: marriage_info['marriage_city'], + divorce_state: marriage_info['divorce_state'], + divorce_city: marriage_info['divorce_city'], + divorce_country: marriage_info['divorce_country'], + end_date: marriage_info['end_date'], + marriage_termination_type_code: marriage_info['marriage_termination_type_code'] + } + end def add_spouse spouse = BGSDependents::Spouse.new(@dependents_application) diff --git a/lib/bgs/power_of_attorney_verifier.rb b/lib/bgs/power_of_attorney_verifier.rb index 41e243202d7..3b4e5522109 100644 --- a/lib/bgs/power_of_attorney_verifier.rb +++ b/lib/bgs/power_of_attorney_verifier.rb @@ -19,7 +19,7 @@ def previous_poa_code @previous_poa_code ||= @veteran.previous_power_of_attorney.try(:code) end - def verify(user) # rubocop:disable Metrics/MethodLength + def verify(user) reps = Veteran::Service::Representative.all_for_user(first_name: user.first_name, last_name: user.last_name) raise ::Common::Exceptions::Unauthorized, detail: 'VSO Representative Not Found' if reps.blank? @@ -28,17 +28,7 @@ def verify(user) # rubocop:disable Metrics/MethodLength if user.middle_name.blank? raise ::Common::Exceptions::Unauthorized, detail: 'Ambiguous VSO Representative Results' else - middle_initial = user.middle_name[0] - reps = Veteran::Service::Representative.all_for_user(first_name: user.first_name, - last_name: user.last_name, - middle_initial:) - - if reps.blank? || reps.count > 1 - reps = Veteran::Service::Representative.all_for_user(first_name: user.first_name, - last_name: user.last_name, - poa_code: current_poa_code) - end - + reps = representatives_with_middle_names_for_user(user) raise ::Common::Exceptions::Unauthorized, detail: 'VSO Representative Not Found' if reps.blank? raise ::Common::Exceptions::Unauthorized, detail: 'Ambiguous VSO Representative Results' if reps.count > 1 end @@ -55,5 +45,21 @@ def verify(user) # rubocop:disable Metrics/MethodLength def matches(veteran_poa_code, representative) representative.poa_codes.include?(veteran_poa_code) end + + private + + def representatives_with_middle_names_for_user(user) + middle_initial = user.middle_name[0] + reps = Veteran::Service::Representative.all_for_user(first_name: user.first_name, + last_name: user.last_name, + middle_initial:) + + if reps.blank? || reps.count > 1 + reps = Veteran::Service::Representative.all_for_user(first_name: user.first_name, + last_name: user.last_name, + poa_code: current_poa_code) + end + reps + end end end diff --git a/lib/va_profile/profile/v3/health_benefit_bio_response.rb b/lib/va_profile/profile/v3/health_benefit_bio_response.rb index 1b333021400..93c7e8a4df7 100644 --- a/lib/va_profile/profile/v3/health_benefit_bio_response.rb +++ b/lib/va_profile/profile/v3/health_benefit_bio_response.rb @@ -8,28 +8,35 @@ module VAProfile module Profile module V3 class HealthBenefitBioResponse < VAProfile::Response + attribute :code, String attribute :contacts, Array[VAProfile::Models::AssociatedPerson] attribute :messages, Array[VAProfile::Models::Message] attribute :va_profile_tx_audit_id, String def initialize(response) - body = response.body + body = response&.body contacts = body.dig('profile', 'health_benefit', 'associated_persons') &.select { |p| valid_contact_types.include?(p['contact_type']) } &.sort_by { |p| valid_contact_types.index(p['contact_type']) } messages = body['messages'] + code = messages&.first&.dig('code') va_profile_tx_audit_id = response.response_headers['vaprofiletxauditid'] - super(response.status, { contacts:, messages:, va_profile_tx_audit_id: }) + super(response.status, { code:, contacts:, messages:, va_profile_tx_audit_id: }) end def debug_data { + code:, status:, message:, va_profile_tx_audit_id: } end + def server_error? + status >= 500 + end + private def valid_contact_types diff --git a/lib/va_profile/profile/v3/service.rb b/lib/va_profile/profile/v3/service.rb index 8b06ccc5f59..e9369942c18 100644 --- a/lib/va_profile/profile/v3/service.rb +++ b/lib/va_profile/profile/v3/service.rb @@ -29,6 +29,8 @@ def get_health_benefit_bio service_response = perform(:post, path, { bios: [{ bioPath: 'healthBenefit' }] }) response = VAProfile::Profile::V3::HealthBenefitBioResponse.new(service_response) Sentry.set_extras(response.debug_data) unless response.ok? + code = response.code || 502 + raise_backend_exception("VET360_#{code}", self.class) if response.server_error? response end diff --git a/modules/claims_api/app/clients/claims_api/bgs_client/definitions.rb b/modules/claims_api/app/clients/claims_api/bgs_client/definitions.rb index 96421084068..603d0faea24 100644 --- a/modules/claims_api/app/clients/claims_api/bgs_client/definitions.rb +++ b/modules/claims_api/app/clients/claims_api/bgs_client/definitions.rb @@ -201,6 +201,15 @@ module PersonWebService path: 'PersonWebService' ) + module FindDependentsByPtcpntId + DEFINITION = + Action.new( + service: PersonWebService::DEFINITION, + name: 'findDependentsByPtcpntId', + key: 'DependentDTO' + ) + end + module FindPersonBySSN DEFINITION = Action.new( 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 ba3b011c6bd..85aff8f0da8 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 @@ -131,11 +131,6 @@ def validate_form_526_change_of_address_zip source: '/changeOfAddress/zipFirstFive', detail: 'The zipFirstFive is required if the country is USA.' ) - elsif address['country'] != 'USA' && address['internationalPostalCode'].blank? - collect_error_messages( - source: '/changeOfAddress/internationalPostalCode', - detail: 'The internationalPostalCode is required if the country is not USA.' - ) elsif address['country'] == 'USA' && address['internationalPostalCode'].present? collect_error_messages( source: '/changeOfAddress/internationalPostalCode', @@ -198,11 +193,6 @@ def validate_form_526_current_mailing_address_zip source: '/veteranIdentification/mailingAddress/zipFirstFive', detail: 'The zipFirstFive is required if the country is USA.' ) - elsif mailing_address['country'] != 'USA' && mailing_address['internationalPostalCode'].blank? - collect_error_messages( - source: '/veteranIdentification/mailingAddress/internationalPostalCode', - detail: 'The internationalPostalCode is required if the country is not USA.' - ) elsif mailing_address['country'] == 'USA' && mailing_address['internationalPostalCode'].present? collect_error_messages( source: '/veteranIdentification/mailingAddress/internationalPostalCode', @@ -227,7 +217,7 @@ def validate_disability_name disability_name = disability&.dig('name') if disability_name.blank? collect_error_messages(source: "/disabilities/#{idx}/name", - detail: "The disability name is required for /disabilities/#{idx}/name") + detail: "The disability name (#{idx}) is required.") end end end @@ -243,9 +233,8 @@ def validate_form_526_disability_classification_code validate_form_526_disability_code_enddate(disability['classificationCode'].to_i, idx) else collect_error_messages(source: "/disabilities/#{idx}/classificationCode", - detail: 'The classificationCode must match an active code ' \ - 'returned from the /disabilities endpoint of the Benefits ' \ - 'Reference Data API.') + detail: "The classificationCode (#{idx}) must match an active code " \ + 'returned from the /disabilities endpoint of the Benefits ') end end end @@ -279,7 +268,7 @@ def validate_form_526_disability_approximate_begin_date next if date_is_valid_against_current_time_after_check_on_format?(approx_begin_date) collect_error_messages(source: "disabilities/#{idx}/approximateDate", - detail: 'The approximateDate is not valid.') + detail: "The approximateDate (#{idx}) is not valid.") end end @@ -292,8 +281,8 @@ def validate_form_526_disability_service_relevance service_relevance = disability&.dig('serviceRelevance') if disability_action_type == 'NEW' && service_relevance.blank? collect_error_messages(source: "disabilities/#{idx}/serviceRelevance", - detail: 'The serviceRelevance is required if ' \ - "disabilityActionType' is NEW.") + detail: "The serviceRelevance (#{idx}) is required if " \ + "'disabilityActionType' is NEW.") end end end @@ -307,12 +296,12 @@ def validate_special_issues if disability['specialIssues'].include? 'POW' if confinements.blank? collect_error_messages(source: "disabilities/#{idx}/specialIssues", - detail: 'serviceInformation.confinements is required if ' \ + detail: "serviceInformation.confinements (#{idx}) is required if " \ 'specialIssues includes POW.') elsif disability_action_type == 'INCREASE' collect_error_messages(source: "disabilities/#{idx}/specialIssues", - detail: 'disabilityActionType cannot be INCREASE if ' \ - 'specialIssues includes POW.') + detail: "disabilityActionType (#{idx}) cannot be INCREASE if " \ + 'specialIssues includes POW for.') end end end @@ -322,7 +311,7 @@ def validate_form_526_disability_secondary_disabilities # rubocop:disable Metric form_attributes['disabilities'].each_with_index do |disability, dis_idx| if disability['disabilityActionType'] == 'NONE' && disability['secondaryDisabilities'].blank? collect_error_messages(source: "disabilities/#{dis_idx}/", - detail: 'If the `disabilityActionType` is set to `NONE` ' \ + detail: "If the `disabilityActionType` (#{dis_idx}) is set to `NONE` " \ 'there must be a secondary disability present.') end next if disability['secondaryDisabilities'].blank? @@ -370,7 +359,7 @@ def validate_form_526_disability_secondary_disability_classification_code(second return if brd_classification_ids.include?(secondary_disability['classificationCode'].to_i) collect_error_messages(source: "disabilities/#{dis_idx}/secondaryDisabilities/#{sd_idx}/classificationCode", - detail: 'classificationCode must match an active code ' \ + detail: "classificationCode (#{dis_idx}) must match an active code " \ 'returned from the /disabilities endpoint of the Benefits Reference Data API.') end @@ -382,7 +371,7 @@ def validate_form_526_disability_secondary_disability_approximate_begin_date(sec return if date_is_valid_against_current_time_after_check_on_format?(secondary_disability['approximateDate']) collect_error_messages(source: "/disabilities/#{dis_idx}/secondaryDisability/#{sd_idx}/approximateDate", - detail: 'approximateDate must be a date in the past.') + detail: "approximateDate (#{dis_idx}) must be a date in the past.") end def validate_form_526_veteran_homelessness # rubocop:disable Metrics/MethodLength @@ -636,7 +625,7 @@ def validate_treatment_dates(treatments) # rubocop:disable Metrics/MethodLength collect_error_messages( source: "/treatments/#{idx}/beginDate", - detail: 'Each treatment begin date must be after the first activeDutyBeginDate.' + detail: "Each treatment begin date (#{idx}) must be after the first activeDutyBeginDate" ) end end @@ -706,14 +695,14 @@ def validate_service_periods(service_information, target_veteran) def age_exception(idx) collect_error_messages( source: "/serviceInformation/servicePeriods/#{idx}/activeDutyBeginDate", - detail: "Active Duty Begin Date cannot be on or before Veteran's thirteenth birthday." + detail: "Active Duty Begin Date (#{idx}) cannot be on or before Veteran's thirteenth birthday." ) end def begin_date_exception(idx) collect_error_messages( source: "/serviceInformation/servicePeriods/#{idx}/activeDutyEndDate", - detail: 'activeDutyEndDate needs to be after activeDutyBeginDate' + detail: "activeDutyEndDate (#{idx}) needs to be after activeDutyBeginDate." ) end @@ -732,7 +721,6 @@ def validate_form_526_location_codes(service_information) collect_error_messages( detail: 'The Reference Data Service is unavailable to verify the separation location code for the claimant' ) - return end @@ -743,7 +731,7 @@ def validate_form_526_location_codes(service_information) collect_error_messages( source: "/serviceInformation/servicePeriods/#{idx}/separationLocationCode", - detail: 'The separation location code for the claimant is not a valid value' + detail: "The separation location code (#{idx}) for the claimant is not a valid value." ) end end @@ -775,7 +763,7 @@ def validate_confinements(service_information) # rubocop:disable Metrics/MethodL if begin_date_is_after_end_date?(approximate_begin_date, approximate_end_date) collect_error_messages( source: "/confinements/#{idx}/", - detail: 'Confinement approximate end date must be after approximate begin date.' + detail: "Confinement approximate end date (#{idx}) must be after approximate begin date." ) end @@ -791,7 +779,7 @@ def validate_confinements(service_information) # rubocop:disable Metrics/MethodL approximate_begin_date) collect_error_messages( source: "/confinements/#{idx}/approximateBeginDate", - detail: 'Confinement approximate begin date must be after earliest active duty begin date.' + detail: "Confinement approximate begin date (#{idx}) must be after earliest active duty begin date." ) end @@ -800,14 +788,14 @@ def validate_confinements(service_information) # rubocop:disable Metrics/MethodL if overlapping_confinement_periods?(idx) collect_error_messages( source: "/confinements/#{idx}/approximateBeginDate", - detail: 'Confinement periods may not overlap each other.' + detail: "Confinement periods (#{idx}) may not overlap each other." ) end unless confinement_dates_are_within_service_period?(approximate_begin_date, approximate_end_date, service_periods) collect_error_messages( source: "/confinements/#{idx}", - detail: 'Confinement dates must be within one of the service period dates.' + detail: "Confinement dates (#{idx}) must be within one of the service period dates." ) end end @@ -880,9 +868,9 @@ def validate_service_branch_names(service_information) unless downcase_branches.include?(sp['serviceBranch'].downcase) collect_error_messages( source: "/serviceInformation/servicePeriods/#{idx}/serviceBranch", - detail: 'serviceBranch must match a service branch ' \ + detail: "serviceBranch (#{idx}) must match a service branch " \ 'returned from the /service-branches endpoint of the Benefits ' \ - 'Reference Data API.' + 'Reference Data API.' \ ) end end diff --git a/modules/claims_api/app/swagger/claims_api/v2/dev/swagger.json b/modules/claims_api/app/swagger/claims_api/v2/dev/swagger.json index 38cbb9691dd..70df5321eff 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 @@ -2143,10 +2143,11 @@ "nullable": true }, "internationalPostalCode": { - "description": "International postal code for the Veteran's current mailing address. Required if 'country' is not 'USA'. Do not include if 'country' is 'USA'.", + "description": "International postal code for the Veteran's current mailing address. Do not include if 'country' is 'USA'.", "type": "string", - "maxLength": 100, - "nullable": true + "maxLength": 16, + "nullable": true, + "pattern": "^[a-zA-Z0-9]*$" } } }, @@ -2252,10 +2253,11 @@ "example": "6789" }, "internationalPostalCode": { - "description": "International postal code for the Veteran's new address. Requried if 'country' is not 'USA'. Do not include if 'country' is 'USA'.", + "description": "International postal code for the Veteran's new address. Do not include if 'country' is 'USA'.", "type": "string", - "maxLength": 100, - "nullable": true + "maxLength": 16, + "nullable": true, + "pattern": "^[a-zA-Z0-9]*$" }, "dates": { "type": "object", @@ -3523,10 +3525,11 @@ "nullable": true }, "internationalPostalCode": { - "description": "International postal code for the Veteran's current mailing address. Required if 'country' is not 'USA'. Do not include if 'country' is 'USA'.", + "description": "International postal code for the Veteran's current mailing address. Do not include if 'country' is 'USA'.", "type": "string", - "maxLength": 100, - "nullable": true + "maxLength": 16, + "nullable": true, + "pattern": "^[a-zA-Z0-9]*$" } } }, @@ -3632,10 +3635,11 @@ "example": "6789" }, "internationalPostalCode": { - "description": "International postal code for the Veteran's new address. Requried if 'country' is not 'USA'. Do not include if 'country' is 'USA'.", + "description": "International postal code for the Veteran's new address. Do not include if 'country' is 'USA'.", "type": "string", - "maxLength": 100, - "nullable": true + "maxLength": 16, + "nullable": true, + "pattern": "^[a-zA-Z0-9]*$" }, "dates": { "type": "object", @@ -5069,7 +5073,7 @@ "202 without a transactionId": { "value": { "data": { - "id": "925698c0-acd8-4155-80ba-6aed534825da", + "id": "40167e4f-703f-48bb-9c06-eea907988788", "type": "forms/526", "attributes": { "claimId": "600442191", @@ -5254,7 +5258,7 @@ }, "federalActivation": { "activationDate": "2023-10-01", - "anticipatedSeparationDate": "2024-08-31" + "anticipatedSeparationDate": "2024-09-07" }, "confinements": [ { @@ -5300,7 +5304,7 @@ "202 with a transactionId": { "value": { "data": { - "id": "285b02b0-6a6b-4a57-ba90-77a7cfc01e0c", + "id": "917204da-61f3-4f0d-a2a5-e5a423b44c1a", "type": "forms/526", "attributes": { "claimId": "600442191", @@ -5678,10 +5682,11 @@ "nullable": true }, "internationalPostalCode": { - "description": "International postal code for the Veteran's current mailing address. Required if 'country' is not 'USA'. Do not include if 'country' is 'USA'.", + "description": "International postal code for the Veteran's current mailing address. Do not include if 'country' is 'USA'.", "type": "string", - "maxLength": 100, - "nullable": true + "maxLength": 16, + "nullable": true, + "pattern": "^[a-zA-Z0-9]*$" } } }, @@ -5787,10 +5792,11 @@ "example": "6789" }, "internationalPostalCode": { - "description": "International postal code for the Veteran's new address. Requried if 'country' is not 'USA'. Do not include if 'country' is 'USA'.", + "description": "International postal code for the Veteran's new address. Do not include if 'country' is 'USA'.", "type": "string", - "maxLength": 100, - "nullable": true + "maxLength": 16, + "nullable": true, + "pattern": "^[a-zA-Z0-9]*$" }, "dates": { "type": "object", @@ -7058,10 +7064,11 @@ "nullable": true }, "internationalPostalCode": { - "description": "International postal code for the Veteran's current mailing address. Required if 'country' is not 'USA'. Do not include if 'country' is 'USA'.", + "description": "International postal code for the Veteran's current mailing address. Do not include if 'country' is 'USA'.", "type": "string", - "maxLength": 100, - "nullable": true + "maxLength": 16, + "nullable": true, + "pattern": "^[a-zA-Z0-9]*$" } } }, @@ -7167,10 +7174,11 @@ "example": "6789" }, "internationalPostalCode": { - "description": "International postal code for the Veteran's new address. Requried if 'country' is not 'USA'. Do not include if 'country' is 'USA'.", + "description": "International postal code for the Veteran's new address. Do not include if 'country' is 'USA'.", "type": "string", - "maxLength": 100, - "nullable": true + "maxLength": 16, + "nullable": true, + "pattern": "^[a-zA-Z0-9]*$" }, "dates": { "type": "object", @@ -9215,10 +9223,11 @@ "nullable": true }, "internationalPostalCode": { - "description": "International postal code for the Veteran's current mailing address. Required if 'country' is not 'USA'. Do not include if 'country' is 'USA'.", + "description": "International postal code for the Veteran's current mailing address. Do not include if 'country' is 'USA'.", "type": "string", - "maxLength": 100, - "nullable": true + "maxLength": 16, + "nullable": true, + "pattern": "^[a-zA-Z0-9]*$" } } }, @@ -9324,10 +9333,11 @@ "example": "6789" }, "internationalPostalCode": { - "description": "International postal code for the Veteran's new address. Requried if 'country' is not 'USA'. Do not include if 'country' is 'USA'.", + "description": "International postal code for the Veteran's new address. Do not include if 'country' is 'USA'.", "type": "string", - "maxLength": 100, - "nullable": true + "maxLength": 16, + "nullable": true, + "pattern": "^[a-zA-Z0-9]*$" }, "dates": { "type": "object", @@ -10506,7 +10516,7 @@ "application/json": { "example": { "data": { - "id": "4e010a53-2107-486a-b006-5e98b8394c15", + "id": "555d0e33-d296-465c-b054-ab459381dc3e", "type": "forms/526", "attributes": { "claimProcessType": "STANDARD_CLAIM_PROCESS", @@ -14166,8 +14176,8 @@ "id": "1", "type": "intent_to_file", "attributes": { - "creationDate": "2024-08-29", - "expirationDate": "2025-08-29", + "creationDate": "2024-09-05", + "expirationDate": "2025-09-05", "type": "compensation", "status": "active" } @@ -15063,7 +15073,7 @@ "application/json": { "example": { "data": { - "id": "fb3346c4-25c3-45fb-bb7c-99edeb99c5f5", + "id": "2042b892-b0c9-4551-8c90-55538b3d6324", "type": "individual", "attributes": { "code": "067", @@ -15756,7 +15766,7 @@ "application/json": { "example": { "data": { - "id": "14b7e4ed-1537-479c-ab98-fb65eb97ef18", + "id": "378415a5-f585-44ac-9a66-f97891a7c116", "type": "organization", "attributes": { "code": "083", @@ -17707,10 +17717,10 @@ "application/json": { "example": { "data": { - "id": "7ab0c254-3162-45fd-87dd-dc44d14cb2f9", + "id": "b17376c2-39a2-44a5-a4bd-3a9d2b1de18f", "type": "claimsApiPowerOfAttorneys", "attributes": { - "dateRequestAccepted": "2024-08-29", + "dateRequestAccepted": "2024-09-05", "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 587d31af0d9..b45b19e464d 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 @@ -756,10 +756,11 @@ "nullable": true }, "internationalPostalCode": { - "description": "International postal code for the Veteran's current mailing address. Required if 'country' is not 'USA'. Do not include if 'country' is 'USA'.", + "description": "International postal code for the Veteran's current mailing address. Do not include if 'country' is 'USA'.", "type": "string", - "maxLength": 100, - "nullable": true + "maxLength": 16, + "nullable": true, + "pattern": "^[a-zA-Z0-9]*$" } } }, @@ -865,10 +866,11 @@ "example": "6789" }, "internationalPostalCode": { - "description": "International postal code for the Veteran's new address. Requried if 'country' is not 'USA'. Do not include if 'country' is 'USA'.", + "description": "International postal code for the Veteran's new address. Do not include if 'country' is 'USA'.", "type": "string", - "maxLength": 100, - "nullable": true + "maxLength": 16, + "nullable": true, + "pattern": "^[a-zA-Z0-9]*$" }, "dates": { "type": "object", @@ -2136,10 +2138,11 @@ "nullable": true }, "internationalPostalCode": { - "description": "International postal code for the Veteran's current mailing address. Required if 'country' is not 'USA'. Do not include if 'country' is 'USA'.", + "description": "International postal code for the Veteran's current mailing address. Do not include if 'country' is 'USA'.", "type": "string", - "maxLength": 100, - "nullable": true + "maxLength": 16, + "nullable": true, + "pattern": "^[a-zA-Z0-9]*$" } } }, @@ -2245,10 +2248,11 @@ "example": "6789" }, "internationalPostalCode": { - "description": "International postal code for the Veteran's new address. Requried if 'country' is not 'USA'. Do not include if 'country' is 'USA'.", + "description": "International postal code for the Veteran's new address. Do not include if 'country' is 'USA'.", "type": "string", - "maxLength": 100, - "nullable": true + "maxLength": 16, + "nullable": true, + "pattern": "^[a-zA-Z0-9]*$" }, "dates": { "type": "object", @@ -3682,7 +3686,7 @@ "202 without a transactionId": { "value": { "data": { - "id": "0db55941-4811-4fd9-80cd-f743761a64ea", + "id": "f6c279e0-7e00-4c31-86d3-839c896e8865", "type": "forms/526", "attributes": { "claimId": "600442191", @@ -3867,7 +3871,7 @@ }, "federalActivation": { "activationDate": "2023-10-01", - "anticipatedSeparationDate": "2024-08-31" + "anticipatedSeparationDate": "2024-09-07" }, "confinements": [ { @@ -3913,7 +3917,7 @@ "202 with a transactionId": { "value": { "data": { - "id": "01a05245-ad48-47c0-b044-60bd1a1cf4d5", + "id": "d0634b7d-7919-4546-8730-da0b46241272", "type": "forms/526", "attributes": { "claimId": "600442191", @@ -4291,10 +4295,11 @@ "nullable": true }, "internationalPostalCode": { - "description": "International postal code for the Veteran's current mailing address. Required if 'country' is not 'USA'. Do not include if 'country' is 'USA'.", + "description": "International postal code for the Veteran's current mailing address. Do not include if 'country' is 'USA'.", "type": "string", - "maxLength": 100, - "nullable": true + "maxLength": 16, + "nullable": true, + "pattern": "^[a-zA-Z0-9]*$" } } }, @@ -4400,10 +4405,11 @@ "example": "6789" }, "internationalPostalCode": { - "description": "International postal code for the Veteran's new address. Requried if 'country' is not 'USA'. Do not include if 'country' is 'USA'.", + "description": "International postal code for the Veteran's new address. Do not include if 'country' is 'USA'.", "type": "string", - "maxLength": 100, - "nullable": true + "maxLength": 16, + "nullable": true, + "pattern": "^[a-zA-Z0-9]*$" }, "dates": { "type": "object", @@ -5671,10 +5677,11 @@ "nullable": true }, "internationalPostalCode": { - "description": "International postal code for the Veteran's current mailing address. Required if 'country' is not 'USA'. Do not include if 'country' is 'USA'.", + "description": "International postal code for the Veteran's current mailing address. Do not include if 'country' is 'USA'.", "type": "string", - "maxLength": 100, - "nullable": true + "maxLength": 16, + "nullable": true, + "pattern": "^[a-zA-Z0-9]*$" } } }, @@ -5780,10 +5787,11 @@ "example": "6789" }, "internationalPostalCode": { - "description": "International postal code for the Veteran's new address. Requried if 'country' is not 'USA'. Do not include if 'country' is 'USA'.", + "description": "International postal code for the Veteran's new address. Do not include if 'country' is 'USA'.", "type": "string", - "maxLength": 100, - "nullable": true + "maxLength": 16, + "nullable": true, + "pattern": "^[a-zA-Z0-9]*$" }, "dates": { "type": "object", @@ -7828,10 +7836,11 @@ "nullable": true }, "internationalPostalCode": { - "description": "International postal code for the Veteran's current mailing address. Required if 'country' is not 'USA'. Do not include if 'country' is 'USA'.", + "description": "International postal code for the Veteran's current mailing address. Do not include if 'country' is 'USA'.", "type": "string", - "maxLength": 100, - "nullable": true + "maxLength": 16, + "nullable": true, + "pattern": "^[a-zA-Z0-9]*$" } } }, @@ -7937,10 +7946,11 @@ "example": "6789" }, "internationalPostalCode": { - "description": "International postal code for the Veteran's new address. Requried if 'country' is not 'USA'. Do not include if 'country' is 'USA'.", + "description": "International postal code for the Veteran's new address. Do not include if 'country' is 'USA'.", "type": "string", - "maxLength": 100, - "nullable": true + "maxLength": 16, + "nullable": true, + "pattern": "^[a-zA-Z0-9]*$" }, "dates": { "type": "object", @@ -9119,7 +9129,7 @@ "application/json": { "example": { "data": { - "id": "746039f5-8aa5-49b5-abea-5ed07ac16b35", + "id": "4f521b24-e79b-482f-b85b-7e80ca67701d", "type": "forms/526", "attributes": { "claimProcessType": "STANDARD_CLAIM_PROCESS", @@ -12779,8 +12789,8 @@ "id": "1", "type": "intent_to_file", "attributes": { - "creationDate": "2024-08-29", - "expirationDate": "2025-08-29", + "creationDate": "2024-09-05", + "expirationDate": "2025-09-05", "type": "compensation", "status": "active" } @@ -13676,7 +13686,7 @@ "application/json": { "example": { "data": { - "id": "69b82793-223a-4d60-ab61-eb67ddb77fc6", + "id": "e3f67c59-3391-445d-9629-f215f8495484", "type": "individual", "attributes": { "code": "067", @@ -14369,7 +14379,7 @@ "application/json": { "example": { "data": { - "id": "5cfd4d55-d0c5-4476-87c1-a1956c8dc02b", + "id": "7bdfdf32-45d7-4724-9e97-ad4762cc5642", "type": "organization", "attributes": { "code": "083", @@ -16320,10 +16330,10 @@ "application/json": { "example": { "data": { - "id": "22563f3d-74c4-44b8-b2a0-0da7bfe639a4", + "id": "50a3a1dd-2ec2-40a3-9588-7cad8377b515", "type": "claimsApiPowerOfAttorneys", "attributes": { - "dateRequestAccepted": "2024-08-29", + "dateRequestAccepted": "2024-09-05", "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 39f0449b2a6..b38bc007ddf 100644 --- a/modules/claims_api/config/schemas/v2/526.json +++ b/modules/claims_api/config/schemas/v2/526.json @@ -116,10 +116,11 @@ "nullable": true }, "internationalPostalCode": { - "description": "International postal code for the Veteran's current mailing address. Required if 'country' is not 'USA'. Do not include if 'country' is 'USA'.", + "description": "International postal code for the Veteran's current mailing address. Do not include if 'country' is 'USA'.", "type": ["string", "null"], - "maxLength": 100, - "nullable": true + "maxLength": 16, + "nullable": true, + "pattern": "^[a-zA-Z0-9]*$" } } }, @@ -222,10 +223,11 @@ "example": "6789" }, "internationalPostalCode": { - "description": "International postal code for the Veteran's new address. Requried if 'country' is not 'USA'. Do not include if 'country' is 'USA'.", + "description": "International postal code for the Veteran's new address. Do not include if 'country' is 'USA'.", "type": ["string", "null"], - "maxLength": 100, - "nullable": true + "maxLength": 16, + "nullable": true, + "pattern": "^[a-zA-Z0-9]*$" }, "dates": { "type": "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 new file mode 100644 index 00000000000..1fde82af939 --- /dev/null +++ b/modules/claims_api/lib/bgs_service/person_web_service.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module ClaimsApi + class PersonWebService < ClaimsApi::LocalBGS + def bean_name + 'PersonWebServiceBean/PersonWebService' + 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 + end + + make_request(endpoint: bean_name, action: 'findDependentsByPtcpntId', body:, key: 'DependentDTO') + end + end +end 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 new file mode 100644 index 00000000000..fbdaf09b89f --- /dev/null +++ b/modules/claims_api/spec/lib/claims_api/person_web_service_spec.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'bgs_service/person_web_service' + +describe ClaimsApi::PersonWebService do + subject { described_class.new external_uid: 'xUid', external_key: 'xKey' } + + describe '#find_dependents_by_ptcpnt_id' do + it 'responds as expected' do + VCR.use_cassette('claims_api/bgs/person_web_service/find_dependents_by_ptcpnt_id') do + # rubocop:disable Style/NumericLiterals + result = subject.find_dependents_by_ptcpnt_id(600052699) + # rubocop:enable Style/NumericLiterals + expect(result).to be_a Hash + expect(result[:dependent][:first_nm]).to eq 'MARGIE' + end + end + end +end diff --git a/modules/claims_api/spec/requests/v2/veterans/526_spec.rb b/modules/claims_api/spec/requests/v2/veterans/526_spec.rb index eeee5ae4794..4a517b713da 100644 --- a/modules/claims_api/spec/requests/v2/veterans/526_spec.rb +++ b/modules/claims_api/spec/requests/v2/veterans/526_spec.rb @@ -3107,7 +3107,7 @@ def update_json_and_submit(updated_json_lambda) expect(response).to have_http_status(:unprocessable_entity) response_body = JSON.parse(response.body) expect(response_body['errors'][0]['detail']).to eq( - "The serviceRelevance is required if disabilityActionType' is NEW." + "The serviceRelevance (0) is required if 'disabilityActionType' is NEW." ) end end diff --git a/modules/simple_forms_api/app/controllers/simple_forms_api/v1/uploads_controller.rb b/modules/simple_forms_api/app/controllers/simple_forms_api/v1/uploads_controller.rb index ef8d1115565..15489d9caae 100644 --- a/modules/simple_forms_api/app/controllers/simple_forms_api/v1/uploads_controller.rb +++ b/modules/simple_forms_api/app/controllers/simple_forms_api/v1/uploads_controller.rb @@ -169,26 +169,26 @@ def get_file_paths_and_metadata(parsed_form_data) end def upload_pdf(file_path, metadata, form) - location, uuid = prepare_for_upload(form) + location, uuid = prepare_for_upload(form, file_path) log_upload_details(location, uuid) response = perform_pdf_upload(location, file_path, metadata) [response.status, uuid] end - def prepare_for_upload(form) + def prepare_for_upload(form, file_path) Rails.logger.info('Simple forms api - preparing to request upload location from Lighthouse', form_id: get_form_id) location, uuid = lighthouse_service.request_upload - stamp_pdf_with_uuid(form, uuid) + stamp_pdf_with_uuid(form, uuid, file_path) create_form_submission_attempt(uuid) [location, uuid] end - def stamp_pdf_with_uuid(form, uuid) + def stamp_pdf_with_uuid(form, uuid, stamped_template_path) # Stamp uuid on 40-10007 - pdf_stamper = SimpleFormsApi::PdfStamper.new(stamped_template_path: 'tmp/vba_40_10007-tmp.pdf', form:) + pdf_stamper = SimpleFormsApi::PdfStamper.new(stamped_template_path:, form:) pdf_stamper.stamp_uuid(uuid) end diff --git a/modules/simple_forms_api/app/services/simple_forms_api/pdf_filler.rb b/modules/simple_forms_api/app/services/simple_forms_api/pdf_filler.rb index feb4ef30c57..59dc4d0f7ba 100644 --- a/modules/simple_forms_api/app/services/simple_forms_api/pdf_filler.rb +++ b/modules/simple_forms_api/app/services/simple_forms_api/pdf_filler.rb @@ -31,8 +31,8 @@ def generate(current_loa = nil, timestamp: Time.current) private def prepare_to_generate_pdf - generated_form_path = Rails.root.join("tmp/#{name}-tmp.pdf").to_s - stamped_template_path = Rails.root.join("tmp/#{name}-stamped.pdf").to_s + generated_form_path = Rails.root.join("tmp/#{name}-#{SecureRandom.hex}-tmp.pdf").to_s + stamped_template_path = Rails.root.join("tmp/#{name}-#{SecureRandom.hex}-stamped.pdf").to_s copy_from_tempfile(stamped_template_path) diff --git a/modules/simple_forms_api/app/services/simple_forms_api/pdf_uploader.rb b/modules/simple_forms_api/app/services/simple_forms_api/pdf_uploader.rb index dc7a0277fba..1f52ed924ca 100644 --- a/modules/simple_forms_api/app/services/simple_forms_api/pdf_uploader.rb +++ b/modules/simple_forms_api/app/services/simple_forms_api/pdf_uploader.rb @@ -44,7 +44,7 @@ def get_upload_location_and_uuid(lighthouse_service, form) # Stamp uuid on 40-10007 uuid = upload_location.dig('data', 'id') - SimpleFormsApi::PdfStamper.new(stamped_template_path: 'tmp/vba_40_10007-tmp.pdf', form:).stamp_uuid(uuid) + SimpleFormsApi::PdfStamper.new(stamped_template_path: file_path, form:).stamp_uuid(uuid) { uuid:, location: upload_location.dig('data', 'attributes', 'location') } end diff --git a/modules/simple_forms_api/spec/services/pdf_filler_spec.rb b/modules/simple_forms_api/spec/services/pdf_filler_spec.rb index 127dffe348b..48d9d69fb0e 100644 --- a/modules/simple_forms_api/spec/services/pdf_filler_spec.rb +++ b/modules/simple_forms_api/spec/services/pdf_filler_spec.rb @@ -36,12 +36,14 @@ forms.each do |file_name| context "when mapping the pdf data given JSON file: #{file_name}" do let(:form_number) { file_name.gsub('-min', '') } - let(:expected_pdf_path) { Rails.root.join("tmp/#{name}-tmp.pdf") } - let(:expected_stamped_path) { Rails.root.join("tmp/#{name}-stamped.pdf") } + let(:pseudorandom_value) { 'abc123' } + let(:expected_pdf_path) { Rails.root.join("tmp/#{name}-#{pseudorandom_value}-tmp.pdf") } + let(:expected_stamped_path) { Rails.root.join("tmp/#{name}-#{pseudorandom_value}-stamped.pdf") } let(:data) { JSON.parse(File.read("modules/simple_forms_api/spec/fixtures/form_json/#{file_name}.json")) } let(:form) { "SimpleFormsApi::#{form_number.titleize.gsub(' ', '')}".constantize.new(data) } let(:name) { SecureRandom.hex } + before { allow(SecureRandom).to receive(:hex).and_return(pseudorandom_value) } after { FileUtils.rm_f(expected_pdf_path) } context 'when a legitimate JSON payload is provided' do diff --git a/modules/travel_pay/app/services/travel_pay/client.rb b/modules/travel_pay/app/services/travel_pay/client.rb index 16b45ff8f32..1ed7dd6157a 100644 --- a/modules/travel_pay/app/services/travel_pay/client.rb +++ b/modules/travel_pay/app/services/travel_pay/client.rb @@ -141,29 +141,6 @@ def veis_params } end - def request_ping(veis_token) - btsss_url = Settings.travel_pay.base_url - api_key = Settings.travel_pay.subscription_key - - connection(server_url: btsss_url).get('api/v1/Sample/ping') do |req| - req.headers['Authorization'] = "Bearer #{veis_token}" - req.headers['Ocp-Apim-Subscription-Key'] = api_key - req.headers['X-Correlation-ID'] = SecureRandom.uuid - end - end - - def request_authorized_ping(veis_token, btsss_token) - btsss_url = Settings.travel_pay.base_url - api_key = Settings.travel_pay.subscription_key - - connection(server_url: btsss_url).get('api/v1/Sample/authorized-ping') do |req| - req.headers['Authorization'] = "Bearer #{veis_token}" - req.headers['BTSSS-Access-Token'] = btsss_token - req.headers['Ocp-Apim-Subscription-Key'] = api_key - req.headers['X-Correlation-ID'] = SecureRandom.uuid - end - end - def request_claims(veis_token, btsss_token) btsss_url = Settings.travel_pay.base_url correlation_id = SecureRandom.uuid diff --git a/modules/vye/app/models/concerns/vye/needs_enrollment_verification.rb b/modules/vye/app/models/concerns/vye/needs_enrollment_verification.rb index b6e1bf31dad..cad0186f569 100644 --- a/modules/vye/app/models/concerns/vye/needs_enrollment_verification.rb +++ b/modules/vye/app/models/concerns/vye/needs_enrollment_verification.rb @@ -338,7 +338,7 @@ def eval_case8 def eval_case9 return if dlc_before_ldpm? || date_last_certified.blank? return unless aed_before_today? - return if are_dates_the_same(@award.award_begin_date, @award.award_end_date) + return unless are_dates_the_same(@award.award_begin_date, @award.award_end_date) push_enrollment( award_id: @award.id, diff --git a/modules/vye/spec/models/vye/user_info_spec.rb b/modules/vye/spec/models/vye/user_info_spec.rb index 1c5d990292a..2322b74b93d 100644 --- a/modules/vye/spec/models/vye/user_info_spec.rb +++ b/modules/vye/spec/models/vye/user_info_spec.rb @@ -298,7 +298,7 @@ let!(:award) do cur_award_ind = Vye::Award.cur_award_inds[:future] award_begin_date = Date.parse('2024-07-01') - award_end_date = Date.parse('2024-07-18') + award_end_date = Date.parse('2024-07-01') FactoryBot.create(:vye_award, user_info:, award_begin_date:, award_end_date:, cur_award_ind:) end diff --git a/spec/controllers/v0/profile/contacts_controller_spec.rb b/spec/controllers/v0/profile/contacts_controller_spec.rb index 3dcc9725b4f..0a3ffd2b684 100644 --- a/spec/controllers/v0/profile/contacts_controller_spec.rb +++ b/spec/controllers/v0/profile/contacts_controller_spec.rb @@ -8,14 +8,13 @@ let(:idme_uuid) { 'dd681e7d6dea41ad8b80f8d39284ef29' } let(:user) { build(:user, :loa3, idme_uuid:) } let(:loa1_user) { build(:user, :loa1) } + let(:cassette) { 'va_profile/profile/v3/health_benefit_bio_200' } describe 'GET /v0/profile/contacts' do subject { get :index } around do |ex| - VCR.use_cassette('va_profile/profile/v3/health_benefit_bio_200') do - ex.run - end + VCR.use_cassette(cassette) { ex.run } end context 'successful request' do @@ -38,5 +37,26 @@ expect(subject).to have_http_status(:forbidden) end end + + context '500 Internal Server Error from VA Profile Service' do + let(:idme_uuid) { '88f572d4-91af-46ef-a393-cba6c351e252' } + let(:cassette) { 'va_profile/profile/v3/health_benefit_bio_500' } + + it 'returns a bad request status code' do + sign_in_as user + expect(subject).to have_http_status(:bad_request) + end + end + + context '504 Gateway Timeout from VA Profile Service' do + let(:idme_uuid) { '88f572d4-91af-46ef-a393-cba6c351e252' } + let(:cassette) { 'va_profile/profile/v3/health_benefit_bio_500' } + + it 'returns a gateway timeout status code' do + allow_any_instance_of(Faraday::Connection).to receive(:post).and_raise(Faraday::TimeoutError) + sign_in_as user + expect(subject).to have_http_status(:gateway_timeout) + end + end end end diff --git a/spec/lib/va_profile/profile/v3/service_spec.rb b/spec/lib/va_profile/profile/v3/service_spec.rb index cd37ee8998d..ec9d0ce4f86 100644 --- a/spec/lib/va_profile/profile/v3/service_spec.rb +++ b/spec/lib/va_profile/profile/v3/service_spec.rb @@ -60,8 +60,10 @@ let(:va_profile_tx_audit_id) do cassette_data['http_interactions'][0]['response']['headers']['Vaprofiletxauditid'][0] end + let(:code) { 'MVI203' } let(:debug_data) do { + code:, status:, message:, va_profile_tx_audit_id: @@ -96,6 +98,7 @@ let(:cassette) { 'va_profile/profile/v3/health_benefit_bio_404' } let(:status) { 404 } let(:message) { 'MVI201 MviNotFound The person with the identifier requested was not found in MVI.' } + let(:code) { 'MVI201' } it 'includes messages received from the api' do response = subject.get_health_benefit_bio @@ -113,23 +116,9 @@ context '500 response' do let(:idme_uuid) { '88f572d4-91af-46ef-a393-cba6c351e252' } let(:cassette) { 'va_profile/profile/v3/health_benefit_bio_500' } - let(:status) { 500 } - let(:message) do - result = 'MVI203 MviResponseError MVI returned acknowledgement error code ' - result += 'AE with error detail: More Than One Active Correlation Exists' - result - end - - it 'includes messages recieved from the api' do - response = subject.get_health_benefit_bio - expect(response.status).to eq(500) - expect(response.contacts.size).to eq(0) - expect(response.messages.size).to eq(1) - end - it 'calls Sentry.set_extras' do - expect(Sentry).to receive(:set_extras).once.with(debug_data) - subject.get_health_benefit_bio + it 'raises an error' do + expect { subject.get_health_benefit_bio }.to raise_error(Common::Exceptions::BackendServiceException) end end diff --git a/spec/requests/v0/profile/contacts_spec.rb b/spec/requests/v0/profile/contacts_spec.rb index d61e2d0f5cc..a9bd4edc179 100644 --- a/spec/requests/v0/profile/contacts_spec.rb +++ b/spec/requests/v0/profile/contacts_spec.rb @@ -57,14 +57,14 @@ end end - context '500 response' do + context '500 response from VA Profile' do let(:idme_uuid) { '88f572d4-91af-46ef-a393-cba6c351e252' } let(:cassette) { 'va_profile/profile/v3/health_benefit_bio_500' } - it 'responds with 500 status' do + it 'responds with 400 status (excluding 5xx response from SLO)' do sign_in_as(user) get '/v0/profile/contacts' - expect(response).to have_http_status(:internal_server_error) + expect(response).to have_http_status(:bad_request) end end end diff --git a/spec/sidekiq/evss/disability_compensation_form/form0781_document_upload_failure_email_spec.rb b/spec/sidekiq/evss/disability_compensation_form/form0781_document_upload_failure_email_spec.rb new file mode 100644 index 00000000000..ae5368ae944 --- /dev/null +++ b/spec/sidekiq/evss/disability_compensation_form/form0781_document_upload_failure_email_spec.rb @@ -0,0 +1,119 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe EVSS::DisabilityCompensationForm::Form0781DocumentUploadFailureEmail, type: :job do + subject { described_class } + + let!(:form526_submission) { create(:form526_submission) } + let(:notification_client) { double('Notifications::Client') } + let(:formatted_submit_date) do + # We display dates in mailers in the format "May 1, 2024 3:01 p.m. EDT" + form526_submission.created_at.strftime('%B %-d, %Y %-l:%M %P %Z').sub(/([ap])m/, '\1.m.') + end + + before do + Sidekiq::Job.clear_all + allow(Notifications::Client).to receive(:new).and_return(notification_client) + end + + describe '#perform' do + it 'dispatches a failure notification email' do + expect(notification_client).to receive(:send_email).with( + # Email address and first_name are from our User fixtures + # form0781_upload_failure_notification_template_id is a placeholder in settings.yml + { + email_address: 'test@email.com', + template_id: 'form0781_upload_failure_notification_template_id', + personalisation: { + first_name: 'BEYONCE', + date_submitted: formatted_submit_date + } + } + ) + + subject.perform_async(form526_submission.id) + subject.drain + end + end + + describe 'logging' do + it 'increments a Statsd metric' do + allow(notification_client).to receive(:send_email) + + expect do + subject.perform_async(form526_submission.id) + subject.drain + end.to trigger_statsd_increment( + 'api.form_526.veteran_notifications.form0781_upload_failure_email.success' + ) + end + + it 'creates a Form526JobStatus' do + allow(notification_client).to receive(:send_email) + + expect do + subject.perform_async(form526_submission.id) + subject.drain + end.to change(Form526JobStatus, :count).by(1) + end + + context 'when an error throws when sending an email' do + before do + allow_any_instance_of(VaNotify::Service).to receive(:send_email).and_raise(Common::Client::Errors::ClientError) + end + + it 'passes the error to the included JobTracker retryable_error_handler and re-raises the error' do + # Sidekiq::Form526JobStatusTracker::JobTracker is included in this job's inheritance hierarchy + expect_any_instance_of( + Sidekiq::Form526JobStatusTracker::JobTracker + ).to receive(:retryable_error_handler).with(an_instance_of(Common::Client::Errors::ClientError)) + + expect do + subject.perform_async(form526_submission.id) + subject.drain + end.to raise_error(Common::Client::Errors::ClientError) + end + end + end + + context 'when retries are exhausted' do + let!(:form526_job_status) { create(:form526_job_status, :retryable_error, form526_submission:, job_id: 123) } + let(:retry_params) do + { + 'jid' => 123, + 'error_class' => 'JennyNotFound', + 'error_message' => 'I tried to call you before but I lost my nerve', + 'args' => [form526_submission.id] + } + end + + let(:exhaustion_time) { DateTime.new(1985, 10, 26).utc } + + before do + allow(notification_client).to receive(:send_email) + end + + it 'increments a StatsD exhaustion metric, logs to the Rails logger and updates the job status' do + Timecop.freeze(exhaustion_time) do + described_class.within_sidekiq_retries_exhausted_block(retry_params) do + expect(Rails.logger).to receive(:warn).with( + 'Form0781DocumentUploadFailureEmail retries exhausted', + { + job_id: 123, + error_class: 'JennyNotFound', + error_message: 'I tried to call you before but I lost my nerve', + timestamp: exhaustion_time, + form526_submission_id: form526_submission.id + } + ).and_call_original + expect(StatsD).to receive(:increment).with( + 'api.form_526.veteran_notifications.form0781_upload_failure_email.exhausted' + ) + end + + expect(form526_job_status.reload.status).to eq(Form526JobStatus::STATUS[:exhausted]) + end + end + end +end diff --git a/spec/sidekiq/evss/disability_compensation_form/submit_form0781_spec.rb b/spec/sidekiq/evss/disability_compensation_form/submit_form0781_spec.rb index 65886c81c76..9b0367f4b45 100644 --- a/spec/sidekiq/evss/disability_compensation_form/submit_form0781_spec.rb +++ b/spec/sidekiq/evss/disability_compensation_form/submit_form0781_spec.rb @@ -88,12 +88,53 @@ it 'updates a StatsD counter and updates the status on an exhaustion event' do subject.within_sidekiq_retries_exhausted_block({ 'jid' => form526_job_status.job_id }) do + # Will receieve increment for failure mailer metric + allow(StatsD).to receive(:increment).with( + 'shared.sidekiq.default.EVSS_DisabilityCompensationForm_Form0781DocumentUploadFailureEmail.enqueue' + ) + expect(StatsD).to receive(:increment).with("#{subject::STATSD_KEY_PREFIX}.exhausted") expect(Rails).to receive(:logger).and_call_original end form526_job_status.reload expect(form526_job_status.status).to eq(Form526JobStatus::STATUS[:exhausted]) end + + context 'when the form526_send_0781_failure_notification Flipper is enabled' do + before do + Flipper.enable(:form526_send_0781_failure_notification) + end + + it 'enqueues a failure notification mailer to send to the veteran' do + subject.within_sidekiq_retries_exhausted_block( + { + 'jid' => form526_job_status.job_id, + 'args' => [form526_submission.id] + } + ) do + expect(EVSS::DisabilityCompensationForm::Form0781DocumentUploadFailureEmail) + .to receive(:perform_async).with(form526_submission.id) + end + end + end + + context 'when the form526_send_0781_failure_notification Flipper is disabled' do + before do + Flipper.disable(:form526_send_0781_failure_notification) + end + + it 'does not enqueue a failure notification mailer to send to the veteran' do + subject.within_sidekiq_retries_exhausted_block( + { + 'jid' => form526_job_status.job_id, + 'args' => [form526_submission.id] + } + ) do + expect(EVSS::DisabilityCompensationForm::Form0781DocumentUploadFailureEmail) + .not_to receive(:perform_async) + end + end + end end end end diff --git a/spec/support/vcr_cassettes/claims_api/bgs/person_web_service/find_dependents_by_ptcpnt_id.yml b/spec/support/vcr_cassettes/claims_api/bgs/person_web_service/find_dependents_by_ptcpnt_id.yml new file mode 100644 index 00000000000..f76a1348046 --- /dev/null +++ b/spec/support/vcr_cassettes/claims_api/bgs/person_web_service/find_dependents_by_ptcpnt_id.yml @@ -0,0 +1,70 @@ +--- +http_interactions: +- request: + method: post + uri: "/PersonWebServiceBean/PersonWebService" + body: + encoding: UTF-8 + string: | + + + + + + VAgovAPI + + + 127.0.0.1 + 281 + VAgovAPI + xUid + xKey + + + + + + 600052699 + + + + headers: + User-Agent: + - "" + Content-Type: + - text/xml;charset=UTF-8 + Host: + - ".vba.va.gov" + Soapaction: + - '"findDependentsByPtcpntId"' + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 200 + message: OK + headers: + Date: + - Tue, 03 Sep 2024 17:07:05 GMT + Server: + - Apache + X-Frame-Options: + - SAMEORIGIN + Transfer-Encoding: + - chunked + Content-Type: + - text/xml; charset=utf-8 + Strict-Transport-Security: + - max-age=16000000; includeSubDomains; preload; + body: + encoding: UTF-8 + string: rO0ABXdUAB13ZWJsb2dpYy5hcHAuQ29ycG9yYXRlRGF0YUVBUgAAANYAAAAjd2VibG9naWMud29ya2FyZWEuU3RyaW5nV29ya0NvbnRleHQABjMuMy40MgAAN2013-03-21T06:33:24-05:00U1953-02-11T00:00:00-06:002022-08-29T00:00:00-05:00796163672MARGIEFCURTISTOKYO + TOWERSHIBAKOEN4 + CHOME-2-8TOKYOAfghanistan2024-08-13T15:12:55-05:00Mailing932Unknown70312022-09-22T14:36:01-05:003397136Daytime57112022-09-22T14:36:01-05:005276860NighttimeN600052700Spouse7961636720Y1 + recorded_at: Tue, 03 Sep 2024 17:07:06 GMT +recorded_with: VCR 6.3.1