From 94896adaf1cd82bfec1f3c7a9fea43bc5308417d Mon Sep 17 00:00:00 2001 From: Nader Kutub Date: Fri, 3 May 2024 07:31:00 -0700 Subject: [PATCH] Feature/appeals 28087 36678 fy24 q3.2.0 merge (#21543) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * APPEALS-36688- Build out Decision Review Created API route & Controller (#20569) * added skeleton for api route * removed duplicate code * removed development envs for api and moved to creating an ApiKey * removing any changes to development.rb * removed extra auth code * removed before action --------- Co-authored-by: TuckerRose * APPEALS-38232 -Build out Decision Review Created Event Failure API route & Controller (#20601) * APPEALS-38232 - renamed controller to AC req, added decision_review_created_error endpoint, added decision_review_created_error method, and added RSpec for new method * APPEALS-38232 - add comments to RSpec * Jonathan/appeals 36684 (#20516) * initial Events migration and model creation * created model for DecisionReviewCreatedEvent * updated comment with example * added spec for DRCE model * APPEALS-36684 created event_records migration and added polymorphic associations to specific models, and added rspec for the event_record model * APPEALS-36684 - Updated RSpec tests and updated variables and got unhappy path to pass * cleaned lint * saving DRCE spec changes * fixed spec test * changed has_many to has_one * updated event model spec * added validation for ER poly associations * changed association to has_one * added new method and updated tests * added foreign key after running checks * some PR comment changes * refactored methods in EventConcern --------- Co-authored-by: Jonathan Tsang Co-authored-by: Enrilo Ugalde * added migration, scopes and specs for events (#20707) * fixed migration to update existing events table * rollbacked to fix schema * schema fixes --------- Co-authored-by: TuckerRose * Jr/APPEALS-38926 (#20714) * APPEALS-38926 - Created DecisionReviewCreatedError Service Class, added logic for handling service error, updated DecisonReviewCreatedController, and DecisionReviewCreatedController spec, with the updated service logic * APPEALS-38926- created RSpec Test for DecisionReviewCreatedError Service Class and edited the Rails.logger for the service class * APPEALS-38926 Added new info column to update transaction and added it to the RSpec test * APPEALS-38926 - added comments to the Service Class * APPEALS-38926 - Code Changes from TL Code Review, added rescues and fails * APPEALS-38926 - fixed lint * Jonathan/appeals 36689 (#20671) * created new service class * add rspec test cases * service class methods * controller action and spec * controller update * CC fixes * removed accidental line * changed to find_or_create_by * reworked error for redis lock * additional rspec for controller * fixed test * rspec fix * delete lock key afterwards * moved Event creation back into lock block --------- Co-authored-by: Jonathan Tsang * APPEALS-39663 Create CreateUserOnEvent service class and add logic to create user if needed (#20838) * added user creation class & test * removed extra lines * add comment to class * added context for args from avro --------- Co-authored-by: Jonathan Tsang * Jr/APPEALS-39664 (#20898) * APPEALS-39664 - Created updated_vacols_on_optin module class, and removed extra private * APPEALS-39664 - created UpdatedVacolsOnOptin module, RSpec file, as well as sudo code for SOC and SSOC optin check in main service class DecisionReviewCreated * APPEALS-39664 - Created RSpec Test - PASS, Updated method name. * APPEALS-39664 - Added Error Handling to Sub Service Class * APPEALS-39664 - Removed un-needed comments * APPEALS-39664 - added include for the module UpdateVacolsOnOptin inside decision_review_created service * APPEALS-39664 - Added Custom Error , and updated all .perform! to .process! * APPEALS-39664 - Updated RSpec Test to reflect changes - all pass * Updated comment for decision_review_created Service Class * Will/appeals 36691 (#20909) * Created attribute for failed claims on event and displaying failed claim * passing all failed events back to serializer * added controller tests for failed_claims and added class method for finding claims on events * renamed failed_claims to claim_errors --------- Co-authored-by: TuckerRose * JR/APPEALS-40954 Create CreateIntake service class and add logic to create Intake (#20967) * APPEALS-40954 - Added Sudo Code for CreateIntake Logic * APPEALS-40954 - added logic to CreateIntake module * APPEALS-40954 - added Create Intake spec with error handling - All pass * APPEALS-40954 - Added CreateIntake Module to DecisionReviewCreated Main Service * APPEALS-40954 - updated folder name and namespace * APPEALS-40954 - Updated RSpec to match folder * APPEALS-40954- Upated decision review created servie include to match folder * Fixed failing test due to folder structure change * Jonathan/appeals 40950 (#20965) * Added new veteran creation module * saving test changes * rspec * fixed datetime assign * renamed var --------- Co-authored-by: Jonathan Tsang * APPEALS-40950 - update var veteran to vbms_veteran (#21050) * APPEALS-40950 - update var veteran to vbms_veteran * updated private method in controller to match happy path params dcr to drc * Create CreateClaimantOnEvent service class and add logic to create claimant if needed (#21044) * created service class and basic unit tests * added conditionals for veteran claimants and will create veteran claimant now * modified create claimant to use eventing data * change to a class * creating claimant correctly * test fixes * removed old comments * fixed type for veteran_is_not_claimant * changed specs to match pulling out hash params and removed event.reference id * moved back to dot notation * updated comments * fixed create claimant issue * updated process to use bang method and returning claimant --------- Co-authored-by: TuckerRose * APPEALS-41968 Modify Issues Endpoints & Update Metric Service Logic (#21108) * moved metricsService call to top of method * modified rspec case * moved metric logging spec case higher up --------- Co-authored-by: Jonathan Tsang * Resolved merge conflicts while merging master into feature/APPEALS-28… (#21167) * Jonathan/APPEALS-41957 (#21171) * added interface + parser class * edited veteran parse methods * renamed var to "payload" * started refactor * more refactor + rspec * added EPE attr * dateTime conversions * added class comments * rubocop lint changes * fixed test case --------- Co-authored-by: Jonathan Tsang * Jr/appeals 41931 (#21192) * APPEALS-41931 - Created create_ep_establishment file and class * APPEALS-41931 - added process! method that creates epe from payload * APPEALS-41931 - added logic for EventRecord being created and error handling * APPEALS-41931 - added comments to process method * APPEALS-41931 - set up rspec test * APPEALS-41931 - removed lint * APPEALS-41931-created Rspec and Test Pass * APPEALS-41931 - cleaned lint and added error test 100% code coverage * APPEALS-41931- fixed lint * APPEALS-41931- fixed lint and fixed %100 code cov * APPEALS-41931 - cleaned up lint and warn for Service Class * APPEALS-41931 - Added CreateEpEstablishment.process! to the decision_review_created Parent Service Class * APPEALS-41931 - refactor code to implement new parser * fixed linting issues * APPEALS-41931 - Updated Comments for CreateEpEstablishment * APPEALS-41931 - moved logical date int to parser and refactored code in Class and RSepc * edits * Will/appeals 41929 (#21205) * added an error and commented out call for createclaim * added new parser logic * merge request changes * fixed rubocop issues and added checks for claim review attributes --------- Co-authored-by: TuckerRose * APPEALS-42631- Create Rspec for Parser with sample payload method, and add additional parser methods (#21294) * APPEALS-42631 - create example.json * APPEALS-42631 - implemented example_response and load_example method and works as expected * APPEALS-42631 - created RSpec for DecisionReviewCreatedParser * APPEALS-42631 - Refactored parser and Added RSpec for current praser * APPEALS-42631 - added methods to parser for intake, claimant, and claim_review and added matching rspec for new methods * APPEALS-42631 - updated code per TL Comments * APPEALS-421631- added additional comments and fixing lint * APPEALS-41934 (#21251) * initial commit * implementation & error * renamed method * rspec * saving refactor progress * finished refactoring class * added comment * minor parser/rspec updates --------- Co-authored-by: Jonathan Tsang * added RI parser methods to rspec (#21322) * added RI parser methods to rspec * updated config for Consumer --------- Co-authored-by: Jonathan Tsang * Update DecisionReviewCreated to make all calls and link all intake records (#21334) * updated decision review created to uncomment actions and updated specs * remove binding.pry * removed comments * fixed a bunch of broken tests * fixed last broken tests * fixed params for methods and specs --------- Co-authored-by: TuckerRose * APPEALS-43446- Change name of BackfillRecord polymorphic model to EventedRecord (#21382) * APPEALS-43446 - renamed columns for backfill record to evented record migrate and rollback work as intended * APPEALS-43446 replaced all backfill_record with evented_record within the models associations and updated Rspec tests * APPEALS-43446 fixed error message * Jr/ama controller refactor (#21365) * fixed test and refactored controller * saving changes * init commit passing all the way to request_issues * all pass and functions as expected * lint * lint * updated initializer to use deep_symbolize_keys * updated headers to be more dry * added missing Intake attributes * fixed failing tests * updated createIntake test * fixed veteran rspec * fixed RI test * APPEALS-43446- Change name of BackfillRecord polymorphic model to EventedRecord (#21382) * APPEALS-43446 - renamed columns for backfill record to evented record migrate and rollback work as intended * APPEALS-43446 replaced all backfill_record with evented_record within the models associations and updated Rspec tests * APPEALS-43446 fixed error message * updated intake * modified intake --------- Co-authored-by: Jonathan Tsang * redo init commit * updated imp. logic * attorney widget fix * Create end to end , happy path rspec's for Decision Created event Feature (#21395) * updated more tests and fixed user creation * added in person creation * corrected headers and fixed broken tests * added type * fixed failing spec * creating event when person is created * updated spec to account for both events being created * ignored long lines for spec file * linting fixes * fixed more linting errors/ ignored long lines --------- Co-authored-by: TuckerRose * Jonathan/appeals 43589 (#21397) * saving * saving user class error progress * error handling for user creation * updated error handling/raises * validator methods * validations * update logical date converter * changed veteran service class to use file_number * added fields to hlr * updated epe data * removed byebug * fixed typos * Edit 5: adding detail_id to Intake * EDIT 6: add claimant_participant_id to epe * Edit 8: Intake is correctly linked to veteran * Edit 7: RI additions * fixed spacing * fixed typo * saving rspec changes * rspec updates * added datetime conversion for person dob * fixed rspec * remove unused methods --------- Co-authored-by: Jonathan Tsang * feature/APPEALS-35707-29633-29632 (uat) (#21435) * 🔀 Squash merge AlecK/APPEALS-35707 - Replace `database_cleaner` with `database_cleaner-active_record` * 🔀 Squash merge jcroteau/APPEALS-29632-fix-deprecation-action-view-base-instances * 🔀 Squash merge jcroteau/APPEALS-29633-fix-deprecation-warning-active_record-result-to_hash --------- Co-authored-by: Will Love Co-authored-by: TuckerRose Co-authored-by: Enrilo Ugalde <71367882+Jruuuu@users.noreply.github.com> Co-authored-by: Jonathan Tsang <98970951+jtsangVA@users.noreply.github.com> Co-authored-by: Jonathan Tsang Co-authored-by: Enrilo Ugalde Co-authored-by: isaiahsaucedo Co-authored-by: Calvin Co-authored-by: Craig Reese Co-authored-by: Jeremy Croteau --- .../v1/decision_review_created_controller.rb | 71 +++ .../api/v3/issues/ama/veterans_controller.rb | 14 +- .../v3/issues/vacols/veterans_controller.rb | 16 +- app/models/claim_review.rb | 7 +- app/models/claimant.rb | 6 + app/models/concerns/event_concern.rb | 24 + app/models/end_product_establishment.rb | 6 + .../events/decision_review_created_event.rb | 7 + app/models/events/event.rb | 19 + app/models/events/event_record.rb | 28 ++ app/models/higher_level_review.rb | 1 + app/models/intake.rb | 2 + app/models/legacy_issue.rb | 6 + app/models/legacy_issue_optin.rb | 6 + app/models/person.rb | 2 + app/models/request_issue.rb | 6 + app/models/supplemental_claim.rb | 1 + app/models/user.rb | 5 + app/models/veteran.rb | 2 + .../v3/issues/ama/request_issue_serializer.rb | 9 + .../api/v3/issues/ama/vbms_ama_dto_builder.rb | 4 +- .../events/create_claimant_on_event.rb | 35 ++ app/services/events/create_user_on_event.rb | 23 + .../events/create_veteran_on_event.rb | 48 ++ .../events/decision_review_created.rb | 97 +++++ .../create_claim_review.rb | 57 +++ .../create_ep_establishment.rb | 41 ++ .../decision_review_created/create_intake.rb | 29 ++ .../create_request_issues.rb | 61 +++ .../decision_review_created_example.json | 85 ++++ .../decision_review_created_parser.rb | 411 ++++++++++++++++++ .../update_vacols_on_optin.rb | 15 + .../events/decision_review_created_error.rb | 44 ++ .../events/veteran_extractor_interface.rb | 41 ++ config/environments/development.rb | 3 + config/routes.rb | 8 + db/migrate/20240116174509_create_events.rb | 12 + .../20240116211523_create_event_records.rb | 11 + ...0240123191625_add_foreign_key_to_events.rb | 5 + ...3192715_validate_foreign_keys_on_events.rb | 5 + .../20240205154329_add_info_to_events.rb | 15 + ...ename_backfill_record_to_evented_record.rb | 9 + db/schema.rb | 21 + lib/caseflow/error.rb | 12 + ...decision_review_created_controller_spec.rb | 111 +++++ .../decision_review_created_event.rb | 7 + spec/factories/events.rb | 14 + ...ision_review_created_event_failure_spec.rb | 122 ++++++ .../scenario_a_spec.rb | 146 +++++++ .../scenario_b_spec.rb | 109 +++++ .../scenario_c_spec.rb | 139 ++++++ .../decision_review_created_event_spec.rb | 50 +++ spec/models/events/event_record_spec.rb | 134 ++++++ spec/models/events/event_spec.rb | 28 ++ .../v3/issues/ama/veterans_controller_spec.rb | 32 +- .../ama/request_issue_serializer_spec.rb | 1 + .../events/create_claimant_on_event_spec.rb | 50 +++ .../events/create_user_on_event_spec.rb | 26 ++ .../events/create_veteran_on_event_spec.rb | 66 +++ .../create_claim_review_spec.rb | 78 ++++ .../create_ep_establishment_spec.rb | 83 ++++ .../create_intake_spec.rb | 44 ++ .../create_request_issues_spec.rb | 112 +++++ .../decision_review_created_parser_spec.rb | 149 +++++++ .../update_vacols_on_optin_spec.rb | 36 ++ .../decision_review_created_error_spec.rb | 46 ++ .../events/decision_review_created_spec.rb | 111 +++++ 67 files changed, 3000 insertions(+), 24 deletions(-) create mode 100644 app/controllers/api/events/v1/decision_review_created_controller.rb create mode 100644 app/models/concerns/event_concern.rb create mode 100644 app/models/events/decision_review_created_event.rb create mode 100644 app/models/events/event.rb create mode 100644 app/models/events/event_record.rb create mode 100644 app/services/events/create_claimant_on_event.rb create mode 100644 app/services/events/create_user_on_event.rb create mode 100644 app/services/events/create_veteran_on_event.rb create mode 100644 app/services/events/decision_review_created.rb create mode 100644 app/services/events/decision_review_created/create_claim_review.rb create mode 100644 app/services/events/decision_review_created/create_ep_establishment.rb create mode 100644 app/services/events/decision_review_created/create_intake.rb create mode 100644 app/services/events/decision_review_created/create_request_issues.rb create mode 100644 app/services/events/decision_review_created/decision_review_created_example.json create mode 100644 app/services/events/decision_review_created/decision_review_created_parser.rb create mode 100644 app/services/events/decision_review_created/update_vacols_on_optin.rb create mode 100644 app/services/events/decision_review_created_error.rb create mode 100644 app/services/events/veteran_extractor_interface.rb create mode 100644 db/migrate/20240116174509_create_events.rb create mode 100644 db/migrate/20240116211523_create_event_records.rb create mode 100644 db/migrate/20240123191625_add_foreign_key_to_events.rb create mode 100644 db/migrate/20240123192715_validate_foreign_keys_on_events.rb create mode 100644 db/migrate/20240205154329_add_info_to_events.rb create mode 100644 db/migrate/20240411185612_rename_backfill_record_to_evented_record.rb create mode 100644 spec/controllers/api/events/v1/decision_review_created_controller_spec.rb create mode 100644 spec/factories/decision_review_created_event.rb create mode 100644 spec/factories/events.rb create mode 100644 spec/feature/events/decision_review_created/decision_review_created_event_failure_spec.rb create mode 100644 spec/feature/events/decision_review_created/scenario_a_spec.rb create mode 100644 spec/feature/events/decision_review_created/scenario_b_spec.rb create mode 100644 spec/feature/events/decision_review_created/scenario_c_spec.rb create mode 100644 spec/models/events/decision_review_created_event_spec.rb create mode 100644 spec/models/events/event_record_spec.rb create mode 100644 spec/models/events/event_spec.rb create mode 100644 spec/services/events/create_claimant_on_event_spec.rb create mode 100644 spec/services/events/create_user_on_event_spec.rb create mode 100644 spec/services/events/create_veteran_on_event_spec.rb create mode 100644 spec/services/events/decision_review_created/create_claim_review_spec.rb create mode 100644 spec/services/events/decision_review_created/create_ep_establishment_spec.rb create mode 100644 spec/services/events/decision_review_created/create_intake_spec.rb create mode 100644 spec/services/events/decision_review_created/create_request_issues_spec.rb create mode 100644 spec/services/events/decision_review_created/decision_review_created_parser_spec.rb create mode 100644 spec/services/events/decision_review_created/update_vacols_on_optin_spec.rb create mode 100644 spec/services/events/decision_review_created_error_spec.rb create mode 100644 spec/services/events/decision_review_created_spec.rb diff --git a/app/controllers/api/events/v1/decision_review_created_controller.rb b/app/controllers/api/events/v1/decision_review_created_controller.rb new file mode 100644 index 00000000000..467451ff372 --- /dev/null +++ b/app/controllers/api/events/v1/decision_review_created_controller.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +class Api::Events::V1::DecisionReviewCreatedController < Api::ApplicationController + def decision_review_created + consumer_event_id = drc_params[:event_id] + claim_id = drc_params[:claim_id] + headers = request.headers + ::Events::DecisionReviewCreated.create!(consumer_event_id, claim_id, headers, drc_params) + render json: { message: "DecisionReviewCreatedEvent successfully processed and backfilled" }, status: :created + rescue Caseflow::Error::RedisLockFailed => error + render json: { message: error.message }, status: :conflict + rescue StandardError => error + render json: { message: error.message }, status: :unprocessable_entity + end + + def decision_review_created_error + event_id = drc_error_params[:event_id] + errored_claim_id = drc_error_params[:errored_claim_id] + error_message = drc_error_params[:error] + ::Events::DecisionReviewCreatedError.handle_service_error(event_id, errored_claim_id, error_message) + render json: { message: "Decision Review Created Error Saved in Caseflow" }, status: :created + rescue Caseflow::Error::RedisLockFailed => error + render json: { message: error.message }, status: :conflict + rescue StandardError => error + render json: { message: error.message }, status: :unprocessable_entity + end + + private + + def drc_error_params + params.permit(:event_id, :errored_claim_id, :error) + end + + def drc_params + params.permit(:event_id, + :claim_id, + :css_id, + :detail_type, + :station, + intake: {}, + veteran: {}, + claimant: {}, + claim_review: {}, + end_product_establishment: {}, + request_issues: [:benefit_type, + :contested_issue_description, + :contention_reference_id, + :contested_rating_decision_reference_id, + :contested_rating_issue_profile_date, + :contested_rating_issue_reference_id, + :contested_decision_issue_id, + :decision_date, + :ineligible_due_to_id, + :ineligible_reason, + :is_unidentified, + :unidentified_issue_text, + :nonrating_issue_category, + :nonrating_issue_description, + :untimely_exemption, + :untimely_exemption_notes, + :vacols_id, + :vacols_sequence_id, + :closed_at, + :closed_status, + :contested_rating_issue_diagnostic_code, + :ramp_claim_id, + :rating_issue_associated_at, + :nonrating_issue_bgs_id] + ) + end +end diff --git a/app/controllers/api/v3/issues/ama/veterans_controller.rb b/app/controllers/api/v3/issues/ama/veterans_controller.rb index 2c5c8caad4b..ea7916c6ef3 100644 --- a/app/controllers/api/v3/issues/ama/veterans_controller.rb +++ b/app/controllers/api/v3/issues/ama/veterans_controller.rb @@ -9,13 +9,13 @@ class Api::V3::Issues::Ama::VeteransController < Api::V3::BaseController end def show - veteran = find_veteran - page = init_page - per_page = init_per - if veteran - MetricsService.record("Retrieving AMA Request Issues for Veteran: #{veteran.participant_id}", - service: "AMA Request Issue endpoint", - name: "VeteransController.show") do + MetricsService.record("Retrieving AMA Request Issues for Veteran with participant ID: #{params[:participant_id]}", + service: "AMA Request Issue endpoint", + name: "VeteransController.show") do + veteran = find_veteran + page = init_page + per_page = init_per + if veteran render_request_issues(Api::V3::Issues::Ama::VbmsAmaDtoBuilder.new(veteran, page, per_page).hash_response) end end diff --git a/app/controllers/api/v3/issues/vacols/veterans_controller.rb b/app/controllers/api/v3/issues/vacols/veterans_controller.rb index a9d7ee8b018..0b9bea1ca11 100644 --- a/app/controllers/api/v3/issues/vacols/veterans_controller.rb +++ b/app/controllers/api/v3/issues/vacols/veterans_controller.rb @@ -44,15 +44,15 @@ def file_number end def show - page = ActiveRecord::Base.sanitize_sql(params[:page].to_i) if params[:page] - # per_page uses the default value defined in the DtoBuilder unless a param is given, - # but it cannot exceed the upper bound - per_page = [params[:per_page].to_i, DEFAULT_UPPER_BOUND_PER_PAGE].min if params[:per_page]&.to_i&.positive? - # Disallow page(0) since page(0) == page(1) in kaminari. This is to avoid confusion. - (page.nil? || page <= 0) ? page = 1 : page ||= 1 - MetricsService.record("VACOLS: Get VACOLS Issues information for Veteran", - name: "Api::V3::Issues::Vacols::VeteransController.show") do + name: "Api::V3::Issues::Vacols::VeteransController.show") do + page = ActiveRecord::Base.sanitize_sql(params[:page].to_i) if params[:page] + # per_page uses the default value defined in the DtoBuilder unless a param is given, + # but it cannot exceed the upper bound + per_page = [params[:per_page].to_i, DEFAULT_UPPER_BOUND_PER_PAGE].min if params[:per_page]&.to_i&.positive? + # Disallow page(0) since page(0) == page(1) in kaminari. This is to avoid confusion. + (page.nil? || page <= 0) ? page = 1 : page ||= 1 + render_vacols_issues(Api::V3::Issues::Vacols::VbmsVacolsDtoBuilder.new(@veteran, page, per_page)) end end diff --git a/app/models/claim_review.rb b/app/models/claim_review.rb index 261bc9c09f7..70b2d470e00 100644 --- a/app/models/claim_review.rb +++ b/app/models/claim_review.rb @@ -8,7 +8,7 @@ class ClaimReview < DecisionReview has_many :end_product_establishments, as: :source has_many :messages, as: :detail - + has_one :event_record, as: :evented_record with_options if: :saving_review do validate :validate_receipt_date validate :validate_veteran @@ -301,6 +301,11 @@ def cleared_nonrating_ep? processed? && cleared_end_products.any?(&:nonrating?) end + def from_decision_review_created_event? + # refer back to the associated Intake to see if both objects came from DRCE + intake ? intake.from_decision_review_created_event? : false + end + def sct_appeal? false end diff --git a/app/models/claimant.rb b/app/models/claimant.rb index bc6b001a8a0..6f09484163e 100644 --- a/app/models/claimant.rb +++ b/app/models/claimant.rb @@ -13,6 +13,7 @@ class Claimant < CaseflowRecord has_one :unrecognized_appellant, lambda { |claimant| where(id: UnrecognizedAppellant.order(:id).find_by(claimant: claimant)&.id) }, dependent: :destroy + has_one :event_record, as: :evented_record # rubocop:disable Rails/UniqueValidationWithoutIndex validates :participant_id, @@ -86,6 +87,11 @@ def find_power_of_attorney # no-op except on BgsRelatedClaimants end + def from_decision_review_created_event? + # refer back to the associated Person record to see if both objects came from DRCE + person.from_decision_review_created_event? + end + private def bgs_address_service diff --git a/app/models/concerns/event_concern.rb b/app/models/concerns/event_concern.rb new file mode 100644 index 00000000000..2c352395121 --- /dev/null +++ b/app/models/concerns/event_concern.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +# This concern is used to identify objects associated with Appeals-Consumer Events. +module EventConcern + extend ActiveSupport::Concern + + # Check if this object is associated with any Event, regardless of type + # check if this object exists in the Event Records table + def from_event? + event_record.present? + end + + # Check if this object is associated with a DecisionReviewCreatedEvent + def from_decision_review_created_event? + if from_event? + # retrieve the record and the event the record is tied to + event = event_record.event + + event.type == DecisionReviewCreatedEvent.name + else + false + end + end +end diff --git a/app/models/end_product_establishment.rb b/app/models/end_product_establishment.rb index 08cdca2983c..18981186780 100644 --- a/app/models/end_product_establishment.rb +++ b/app/models/end_product_establishment.rb @@ -20,6 +20,7 @@ class EndProductEstablishment < CaseflowRecord has_many :effectuations, class_name: "BoardGrantEffectuation" has_many :end_product_updates has_one :priority_end_product_sync_queue + has_one :event_record, as: :evented_record belongs_to :vbms_ext_claim, foreign_key: "reference_id", primary_key: "claim_id", optional: true # :block => 1 # Specify in seconds how long you want to wait for the lock to be released. @@ -383,6 +384,11 @@ def status_type_code end end + def from_decision_review_created_event? + # refer back to the associated Intake to see if both objects came from DRCE + source.intake.from_decision_review_created_event? + end + private def status_type diff --git a/app/models/events/decision_review_created_event.rb b/app/models/events/decision_review_created_event.rb new file mode 100644 index 00000000000..74d3752cebf --- /dev/null +++ b/app/models/events/decision_review_created_event.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +# This class represents the DecisionReviewCreatedEvent info that is POSTed to Caseflow +# Represents a single "event" and is tied to "event records" that contain info regarding +# the different objects that Caseflow performs backfill creations for after VBMS Intake. +class DecisionReviewCreatedEvent < Event +end diff --git a/app/models/events/event.rb b/app/models/events/event.rb new file mode 100644 index 00000000000..ae9d15ca62d --- /dev/null +++ b/app/models/events/event.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +# This class is the parent class for different events that Caseflow receives from appeals-consumer +class Event < CaseflowRecord + has_many :event_records + store_accessor :info, :errored_claim_id + + scope :with_errored_claim_id, -> { where.not("info -> 'errored_claim_id' IS NULL") } + + def completed? + completed_at? + end + + def self.find_errors_by_claim_id(claim_id) + with_errored_claim_id + .where("info ->> 'errored_claim_id' = ?", claim_id) + .pluck(:error) + end +end diff --git a/app/models/events/event_record.rb b/app/models/events/event_record.rb new file mode 100644 index 00000000000..3c21e4f5cff --- /dev/null +++ b/app/models/events/event_record.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +class EventRecord < CaseflowRecord + belongs_to :event + belongs_to :evented_record, polymorphic: true + + validate :valid_evented_record + + def valid_evented_record + unless %w[ + Intake + ClaimReview + HigherLevelReview + SupplementalClaim + EndProductEstablishment + Claimant + Veteran + Person + RequestIssue + LegacyIssue + LegacyIssueOptin + User + ].include?(evented_record_type) + + errors.add(:evented_record_type, "is not a valid evented record") + end + end +end diff --git a/app/models/higher_level_review.rb b/app/models/higher_level_review.rb index 39e9d2f309c..37f4a9d13ea 100644 --- a/app/models/higher_level_review.rb +++ b/app/models/higher_level_review.rb @@ -10,6 +10,7 @@ class HigherLevelReview < ClaimReview end has_many :remand_supplemental_claims, as: :decision_review_remanded, class_name: "SupplementalClaim" + has_one :event_record, as: :evented_record attr_accessor :appeal_split_process diff --git a/app/models/intake.rb b/app/models/intake.rb index 0d96d5c78dd..97f9a039dca 100644 --- a/app/models/intake.rb +++ b/app/models/intake.rb @@ -2,10 +2,12 @@ class Intake < CaseflowRecord class FormTypeNotSupported < StandardError; end + include EventConcern belongs_to :user belongs_to :veteran belongs_to :detail, polymorphic: true + has_one :event_record, as: :evented_record COMPLETION_TIMEOUT = 5.minutes IN_PROGRESS_EXPIRES_AFTER = 1.day diff --git a/app/models/legacy_issue.rb b/app/models/legacy_issue.rb index 22006687207..3db514328ee 100644 --- a/app/models/legacy_issue.rb +++ b/app/models/legacy_issue.rb @@ -3,6 +3,12 @@ class LegacyIssue < CaseflowRecord belongs_to :request_issue has_one :legacy_issue_optin + has_one :event_record, as: :evented_record validates :request_issue, presence: true + + def from_decision_review_created_event? + # refer back to the associated Intake to see if both objects came from DRCE + request_issue&.from_decision_review_created_event? + end end diff --git a/app/models/legacy_issue_optin.rb b/app/models/legacy_issue_optin.rb index 22e623b43fc..43e0d01e9c3 100644 --- a/app/models/legacy_issue_optin.rb +++ b/app/models/legacy_issue_optin.rb @@ -3,6 +3,7 @@ class LegacyIssueOptin < CaseflowRecord belongs_to :request_issue belongs_to :legacy_issue + has_one :event_record, as: :evented_record VACOLS_DISPOSITION_CODE = "O" # oh not zero REMAND_DISPOSITION_CODES = %w[3 L].freeze @@ -109,6 +110,11 @@ def vacols_issue AppealRepository.issues(vacols_id).find { |issue| issue.vacols_sequence_id == vacols_sequence_id } end + def from_decision_review_created_event? + # refer back to the associated Intake to see if both objects came from DRCE + request_issue&.from_decision_review_created_event? + end + private def revert_open_remand_issues diff --git a/app/models/person.rb b/app/models/person.rb index d3d6c7c7277..94866057eb4 100644 --- a/app/models/person.rb +++ b/app/models/person.rb @@ -3,9 +3,11 @@ class Person < CaseflowRecord include AssociatedBgsRecord include BgsService + include EventConcern has_many :advance_on_docket_motions has_many :claimants, primary_key: :participant_id, foreign_key: :participant_id + has_one :event_record, as: :evented_record validates :participant_id, presence: true CACHED_BGS_ATTRIBUTES = [ diff --git a/app/models/request_issue.rb b/app/models/request_issue.rb index d9d4bd5ca08..b34018a6816 100644 --- a/app/models/request_issue.rb +++ b/app/models/request_issue.rb @@ -34,6 +34,7 @@ class RequestIssue < CaseflowRecord has_many :hearing_issue_notes has_one :legacy_issue_optin has_many :legacy_issues + has_one :event_record, as: :evented_record belongs_to :correction_request_issue, class_name: "RequestIssue", foreign_key: "corrected_by_request_issue_id" belongs_to :ineligible_due_to, class_name: "RequestIssue", foreign_key: "ineligible_due_to_id" belongs_to :contested_decision_issue, class_name: "DecisionIssue" @@ -753,6 +754,11 @@ def timely_issue?(receipt_date) decision_date >= (receipt_date - Rating::ONE_YEAR_PLUS_DAYS) end + def from_decision_review_created_event? + # refer back to the associated Intake to see if both objects came from DRCE + decision_review&.from_decision_review_created_event? + end + private def create_legacy_issue! diff --git a/app/models/supplemental_claim.rb b/app/models/supplemental_claim.rb index cc33992cb6c..014dea14a0b 100644 --- a/app/models/supplemental_claim.rb +++ b/app/models/supplemental_claim.rb @@ -4,6 +4,7 @@ class SupplementalClaim < ClaimReview END_PRODUCT_MODIFIERS = %w[040 041 042 043 044 045 046 047 048 049].freeze belongs_to :decision_review_remanded, polymorphic: true + has_one :event_record, as: :evented_record scope :updated_since_for_appeals, lambda { |since| select(:decision_review_remanded_id) diff --git a/app/models/user.rb b/app/models/user.rb index 962a87471bd..ee8c3fcffd8 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -2,7 +2,11 @@ class User < CaseflowRecord # rubocop:disable Metrics/ClassLength include BgsService +<<<<<<< HEAD + include EventConcern +======= include ConferenceableConcern +>>>>>>> uat/FY24Q3.2.0 has_many :dispatch_tasks, class_name: "Dispatch::Task" has_many :document_views @@ -18,6 +22,7 @@ class User < CaseflowRecord # rubocop:disable Metrics/ClassLength has_many :decided_membership_requests, class_name: "MembershipRequest", foreign_key: :decider_id has_many :messages has_many :unrecognized_appellants, foreign_key: :created_by_id + has_one :event_record, as: :evented_record has_one :vacols_user, class_name: "CachedUser", foreign_key: :sdomainid, primary_key: :css_id has_one :vacols_staff, class_name: "VACOLS::Staff", foreign_key: :sdomainid, primary_key: :css_id diff --git a/app/models/veteran.rb b/app/models/veteran.rb index b7bb1bda08b..97644ea0fcd 100644 --- a/app/models/veteran.rb +++ b/app/models/veteran.rb @@ -7,11 +7,13 @@ # rubocop:disable Metrics/ClassLength class Veteran < CaseflowRecord include AssociatedBgsRecord + include EventConcern has_many :correspondences has_many :available_hearing_locations, foreign_key: :veteran_file_number, primary_key: :file_number, class_name: "AvailableHearingLocations" + has_one :event_record, as: :evented_record bgs_attr_accessor :ptcpnt_id, :sex, :address_line1, :address_line2, :address_line3, :city, :state, :country, :zip_code, diff --git a/app/serializers/api/v3/issues/ama/request_issue_serializer.rb b/app/serializers/api/v3/issues/ama/request_issue_serializer.rb index b1345f0bd80..9dfde0486b8 100644 --- a/app/serializers/api/v3/issues/ama/request_issue_serializer.rb +++ b/app/serializers/api/v3/issues/ama/request_issue_serializer.rb @@ -38,6 +38,15 @@ class Api::V3::Issues::Ama::RequestIssueSerializer object&.end_product_establishment&.reference_id end + attribute :claim_errors do |object| + claim_id = object&.end_product_establishment&.reference_id + if claim_id + Event.find_errors_by_claim_id(claim_id) + else + [] + end + end + attribute :decision_issues do |object| object.decision_issues.map do |di| { diff --git a/app/services/api/v3/issues/ama/vbms_ama_dto_builder.rb b/app/services/api/v3/issues/ama/vbms_ama_dto_builder.rb index da19e43676b..cdf3ff43f71 100644 --- a/app/services/api/v3/issues/ama/vbms_ama_dto_builder.rb +++ b/app/services/api/v3/issues/ama/vbms_ama_dto_builder.rb @@ -21,7 +21,7 @@ def initialize(veteran, page, per_page) def total_request_issue_count RequestIssue.where(veteran_participant_id: @veteran_participant_id) - .where(benefit_type: %w[compensation pension fiduciary]) + .where(benefit_type: %w[compensation pension]) .count end @@ -29,7 +29,7 @@ def serialized_request_issues(page = @page, per_page = @per_page) serialized_data = Api::V3::Issues::Ama::RequestIssueSerializer.new( RequestIssue.includes(:decision_issues, :decision_review) .where(veteran_participant_id: @veteran_participant_id) - .where(benefit_type: %w[compensation pension fiduciary]) + .where(benefit_type: %w[compensation pension]) .page(page).per(per_page) ).serializable_hash[:data] diff --git a/app/services/events/create_claimant_on_event.rb b/app/services/events/create_claimant_on_event.rb new file mode 100644 index 00000000000..3b0ca4cb9b7 --- /dev/null +++ b/app/services/events/create_claimant_on_event.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +class Events::CreateClaimantOnEvent + class << self + def process!(event:, parser:, decision_review:) + if parser.claim_review_veteran_is_not_claimant + create_person(event, parser) unless Person.find_by(participant_id: parser.claimant_participant_id) + + claimant = Claimant.find_or_create_by!( + decision_review: decision_review, + participant_id: parser.claimant_participant_id, + payee_code: parser.claimant_payee_code, + type: parser.claimant_type + ) + EventRecord.create!(event: event, evented_record: claimant) + claimant + end + rescue StandardError => error + raise Caseflow::Error::DecisionReviewCreatedClaimantError, error.message + end + + def create_person(event, parser) + person = Person.create(date_of_birth: parser.person_date_of_birth, + email_address: parser.person_email_address, + first_name: parser.person_first_name, + last_name: parser.person_last_name, + middle_name: parser.person_middle_name, + name_suffix: parser.claimant_name_suffix, + ssn: parser.person_ssn, + participant_id: parser.claimant_participant_id) + + EventRecord.create!(event: event, evented_record: person) + end + end +end diff --git a/app/services/events/create_user_on_event.rb b/app/services/events/create_user_on_event.rb new file mode 100644 index 00000000000..50ae8a30ddc --- /dev/null +++ b/app/services/events/create_user_on_event.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +# Service Class that will be utilized by Events::DecisionReviewCreated to create a new User +# when an Event is received and that specific User does not already exist in Caseflow +class Events::CreateUserOnEvent + class << self + def handle_user_creation_on_event(event:, css_id:, station_id:) + user = User.find_by(css_id: css_id) + return user if user + + create_inactive_user(event, css_id, station_id) + rescue StandardError => error + raise Caseflow::Error::DecisionReviewCreatedUserError, error.message + end + + def create_inactive_user(event, css_id, station_id) + user = User.create!(css_id: css_id.upcase, station_id: station_id, status: Constants.USER_STATUSES.inactive) + # create Event record indicating this is a backfilled User + EventRecord.create!(event: event, evented_record: user) + user + end + end +end diff --git a/app/services/events/create_veteran_on_event.rb b/app/services/events/create_veteran_on_event.rb new file mode 100644 index 00000000000..9563d4a555a --- /dev/null +++ b/app/services/events/create_veteran_on_event.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +# Service Class that will be utilized by Events::DecisionReviewCreated to create a new Veteran +# when an Event is received and that specific Veteran does not already exist in Caseflow +class Events::CreateVeteranOnEvent + class << self + def handle_veteran_creation_on_event(event:, parser:) + unless veteran_exist?(parser.veteran_file_number) + create_backfill_veteran(event, parser) + else + # return existing Veteran + Veteran.find_by(file_number: parser.veteran_file_number) + end + rescue StandardError => error + raise Caseflow::Error::DecisionReviewCreatedVeteranError, error.message + end + + def veteran_exist?(veteran_file_number) + Veteran.where(file_number: veteran_file_number).exists? + end + + private + + def create_backfill_veteran(event, parser) + # Create Veteran without calling BGS + vet = Veteran.create!( + file_number: parser.veteran_file_number, + ssn: parser.veteran_ssn, + first_name: parser.veteran_first_name, + last_name: parser.veteran_last_name, + middle_name: parser.veteran_middle_name, + participant_id: parser.veteran_participant_id, + bgs_last_synced_at: parser.veteran_bgs_last_synced_at, + name_suffix: parser.veteran_name_suffix, + date_of_death: parser.veteran_date_of_death + ) + + # Update the CF cache + if vet.cached_attributes_updatable? + vet.update_cached_attributes! + end + # create EventRecord indicating this is a backfilled Veteran + EventRecord.create!(event: event, evented_record: vet) + + vet + end + end +end diff --git a/app/services/events/decision_review_created.rb b/app/services/events/decision_review_created.rb new file mode 100644 index 00000000000..06b72466323 --- /dev/null +++ b/app/services/events/decision_review_created.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +class Events::DecisionReviewCreated + include RedisMutex::Macro + include Events::DecisionReviewCreated::UpdateVacolsOnOptin + include Events::DecisionReviewCreated::CreateIntake + # Default options for RedisMutex#with_lock + # :block => 1 # Specify in seconds how long you want to wait for the lock to be released. + # # Specify 0 if you need non-blocking sematics and return false immediately. (default: 1) + # :sleep => 0.1 # Specify in seconds how long the polling interval should be when :block is given. + # # It is NOT recommended to go below 0.01. (default: 0.1) + # :expire => 10 # Specify in seconds when the lock should be considered stale when something went wrong + # # with the one who held the lock and failed to unlock. (default: 10) + + class << self + def create!(consumer_event_id, reference_id, headers, payload) + return if event_exists_and_is_completed?(consumer_event_id) + + redis = Redis.new(url: Rails.application.secrets.redis_url_cache) + + # exit out if Key is already in Redis Cache + if redis.exists("RedisMutex:EndProductEstablishment:#{reference_id}") + fail Caseflow::Error::RedisLockFailed, message: "Key RedisMutex:EndProductEstablishment:#{reference_id} is already in the Redis Cache" + end + + RedisMutex.with_lock("EndProductEstablishment:#{reference_id}", block: 60, expire: 100) do + # key => "EndProductEstablishment:reference_id" aka "claim ID" + # Use the consumer_event_id to retrieve/create the Event object + event = find_or_create_event(consumer_event_id) + + ActiveRecord::Base.transaction do + # Initialize the Parser object that will be passed around as an argument + parser = Events::DecisionReviewCreated::DecisionReviewCreatedParser.new(headers, payload) + + # Note: createdByStation == station_id, createdByUsername == css_id + user = Events::CreateUserOnEvent.handle_user_creation_on_event(event: event, css_id: parser.css_id, + station_id: parser.station_id) + + # Create the Veteran. PII Info is stored in the headers + vet = Events::CreateVeteranOnEvent.handle_veteran_creation_on_event(event: event, parser: parser) + + # Note Create Claim Review, parsed schema info passed through claim_review and intake + decision_review = Events::DecisionReviewCreated::CreateClaimReview.process!(event: event, parser: parser) + + # Note: decision_review arg can either be a HLR or SC object. process! will only run if + # decision_review.legacy_opt_in_approved is true + Events::DecisionReviewCreated::UpdateVacolsOnOptin.process!(decision_review: decision_review) + + # Note: Create the Claimant, parsed schema info passed through vbms_claimant + Events::CreateClaimantOnEvent.process!(event: event, parser: parser, + decision_review: decision_review) + + # Note: event, user, and veteran need to be before this call. + Events::DecisionReviewCreated::CreateIntake.process!(event: event, user: user, veteran: vet, parser: parser, + decision_review: decision_review) + + # Note: end_product_establishment & station_id is coming from the payload + # claim_review can either be a higher_level_revew or supplemental_claim + epe = Events::DecisionReviewCreated::CreateEpEstablishment.process!(parser: parser, + claim_review: decision_review, + user: user, event: event) + + # Note: 'epe' arg is the obj created as a result of the CreateEpEstablishment service class + Events::DecisionReviewCreated::CreateRequestIssues.process!(event: event, parser: parser, epe: epe, + decision_review: decision_review) + # Update the Event after all backfills have completed + event.update!(completed_at: Time.now.in_time_zone, error: nil) + end + end + rescue Caseflow::Error::RedisLockFailed => error + Rails.logger.error("Key RedisMutex:EndProductEstablishment:#{reference_id} is already in the Redis Cache") + event = Event.find_by(reference_id: consumer_event_id) + event&.update!(error: error.message) + raise error + rescue RedisMutex::LockError => error + Rails.logger.error("Failed to acquire lock for Claim ID: #{reference_id}! This Event is being"\ + " processed. Please try again later.") + rescue StandardError => error + Rails.logger.error("#{error.class} : #{error.message}") + event = Event.find_by(reference_id: consumer_event_id) + event&.update!(error: "#{error.class} : #{error.message}", info: { "failed_claim_id" => reference_id }) + raise error + end + + # Check if there's already a CF Event that references that Appeals-Consumer EventID and + # was successfully completed + def event_exists_and_is_completed?(consumer_event_id) + Event.where(reference_id: consumer_event_id).where.not(completed_at: nil).exists? + end + + # Check if there's already a CF Event that references that Appeals-Consumer EventID + # We will update the existing Event instead of creating a new one + def find_or_create_event(consumer_event_id) + DecisionReviewCreatedEvent.find_or_create_by(reference_id: consumer_event_id) + end + end +end diff --git a/app/services/events/decision_review_created/create_claim_review.rb b/app/services/events/decision_review_created/create_claim_review.rb new file mode 100644 index 00000000000..4ab5b8fb0cd --- /dev/null +++ b/app/services/events/decision_review_created/create_claim_review.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +class Events::DecisionReviewCreated::CreateClaimReview + class << self + def process!(event:, parser:) + if parser.detail_type == "HigherLevelReview" + high_level_review = create_high_level_review(parser) + create_event_record(event, high_level_review) + high_level_review + else + supplemental_claim = create_supplemental_claim(parser) + create_event_record(event, supplemental_claim) + supplemental_claim + end + rescue StandardError => error + raise Caseflow::Error::DecisionReviewCreatedCreateClaimReviewError, error.message + end + + private + + def create_high_level_review(parser) + HigherLevelReview.create( + benefit_type: parser.claim_review_benefit_type, + filed_by_va_gov: parser.claim_review_filed_by_va_gov, + legacy_opt_in_approved: parser.claim_review_legacy_opt_in_approved, + receipt_date: parser.claim_review_receipt_date, + veteran_is_not_claimant: parser.claim_review_veteran_is_not_claimant, + establishment_attempted_at: parser.claim_review_establishment_attempted_at, + establishment_last_submitted_at: parser.claim_review_establishment_last_submitted_at, + establishment_processed_at: parser.claim_review_establishment_processed_at, + establishment_submitted_at: parser.claim_review_establishment_submitted_at, + veteran_file_number: parser.veteran_file_number, + informal_conference: parser.claim_review_informal_conference, + same_office: parser.claim_review_same_office + ) + end + + def create_supplemental_claim(parser) + SupplementalClaim.create( + benefit_type: parser.claim_review_benefit_type, + filed_by_va_gov: parser.claim_review_filed_by_va_gov, + legacy_opt_in_approved: parser.claim_review_legacy_opt_in_approved, + receipt_date: parser.claim_review_receipt_date, + veteran_is_not_claimant: parser.claim_review_veteran_is_not_claimant, + establishment_attempted_at: parser.claim_review_establishment_attempted_at, + establishment_last_submitted_at: parser.claim_review_establishment_last_submitted_at, + establishment_processed_at: parser.claim_review_establishment_processed_at, + establishment_submitted_at: parser.claim_review_establishment_submitted_at, + veteran_file_number: parser.veteran_file_number + ) + end + + def create_event_record(event, claim) + EventRecord.create!(event: event, evented_record: claim) + end + end +end diff --git a/app/services/events/decision_review_created/create_ep_establishment.rb b/app/services/events/decision_review_created/create_ep_establishment.rb new file mode 100644 index 00000000000..1045d0328cf --- /dev/null +++ b/app/services/events/decision_review_created/create_ep_establishment.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +# This is the Sub Service Class that holds the process! of starting the +# creation of an End Product Establishment from an event. +class Events::DecisionReviewCreated::CreateEpEstablishment + class << self + # This starts the creation of End Product Establishment from an event. + # This is a sub service class that returns the End Product Establishment + # that was created from the event. Arguments claim_review, user and event + # are referring to the backfill objects being created from other sub service + # class. claim_review can be either a supplemental claim or higher level review + # rubocop:disable Metrics/MethodLength + def process!(parser:, claim_review:, user:, event:) + end_product_establishment = EndProductEstablishment.create!( + payee_code: parser.epe_payee_code, + source: claim_review, + veteran_file_number: claim_review.veteran_file_number, + benefit_type_code: parser.epe_benefit_type_code, + claim_date: parser.epe_claim_date, + code: parser.epe_code, + committed_at: parser.epe_committed_at, + development_item_reference_id: parser.epe_development_item_reference_id, + established_at: parser.epe_established_at, + last_synced_at: parser.epe_last_synced_at, + limited_poa_access: parser.epe_limited_poa_access, + limited_poa_code: parser.epe_limited_poa_code, + modifier: parser.epe_modifier, + reference_id: parser.epe_reference_id, + station: parser.station_id, + synced_status: parser.epe_synced_status, + user_id: user.id, + claimant_participant_id: parser.claimant_participant_id + ) + EventRecord.create!(event: event, evented_record: end_product_establishment) + end_product_establishment + rescue StandardError => error + raise Caseflow::Error::DecisionReviewCreatedEpEstablishmentError, error.message + end + # rubocop:enable Metrics/MethodLength + end +end diff --git a/app/services/events/decision_review_created/create_intake.rb b/app/services/events/decision_review_created/create_intake.rb new file mode 100644 index 00000000000..f931eb39a8d --- /dev/null +++ b/app/services/events/decision_review_created/create_intake.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +# This module starts the intial Intake creation and backfills the EventRecord +# when a Decision Review Created Event is triggered +module Events::DecisionReviewCreated::CreateIntake + # This starts the process of the Intake creation and EventRecord backfill by passing in the event, user, and veteran + # that was created in the DecisionReviewCreated Service. + def self.process!(event:, user:, veteran:, parser:, decision_review:) + # create Intake + intake = Intake.create!(veteran_file_number: veteran.file_number, + user: user, + started_at: parser.intake_started_at, + completion_started_at: parser.intake_completion_started_at, + completed_at: parser.intake_completed_at, + completion_status: parser.intake_completion_status, + type: parser.intake_type, + detail_type: parser.intake_detail_type, + detail_id: decision_review.id, + veteran: veteran) + # create EventRecord + EventRecord.create!(event: event, evented_record: intake) + + intake + + # Error Handling + rescue StandardError => error + raise Caseflow::Error::DecisionReviewCreatedIntakeError, error.message + end +end diff --git a/app/services/events/decision_review_created/create_request_issues.rb b/app/services/events/decision_review_created/create_request_issues.rb new file mode 100644 index 00000000000..b7a0d3a4427 --- /dev/null +++ b/app/services/events/decision_review_created/create_request_issues.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +# Service Class that will be utilized by Events::DecisionReviewCreated to create Request Issues +# when an Event is received using the data sent from VBMS +class Events::DecisionReviewCreated::CreateRequestIssues + class << self + def process!(event:, parser:, epe:, decision_review:) + create_request_issue_backfill(event, parser, epe, decision_review) + rescue StandardError => error + raise Caseflow::Error::DecisionReviewCreatedRequestIssuesError, error.message + end + + private + + # iterate through the array of issues and create backfill object from each one + def create_request_issue_backfill(event, parser, epe, decision_review) + request_issues = parser.request_issues + newly_created_issues = [] + + request_issues&.each do |issue| + # create backfill RI object using extracted values + ri = RequestIssue.create!( + benefit_type: parser.ri_benefit_type(issue), + contested_issue_description: parser.ri_contested_issue_description(issue), + contention_reference_id: parser.ri_contention_reference_id(issue), + contested_rating_decision_reference_id: parser.ri_contested_rating_decision_reference_id(issue), + contested_rating_issue_profile_date: parser.ri_contested_rating_issue_profile_date(issue), + contested_rating_issue_reference_id: parser.ri_contested_rating_issue_reference_id(issue), + contested_decision_issue_id: parser.ri_contested_decision_issue_id(issue), + decision_date: parser.ri_decision_date(issue), + ineligible_due_to_id: parser.ri_ineligible_due_to_id(issue), + ineligible_reason: parser.ri_ineligible_reason(issue), + is_unidentified: parser.ri_is_unidentified(issue), + unidentified_issue_text: parser.ri_unidentified_issue_text(issue), + nonrating_issue_category: parser.ri_nonrating_issue_category(issue), + nonrating_issue_description: parser.ri_nonrating_issue_description(issue), + untimely_exemption: parser.ri_untimely_exemption(issue), + untimely_exemption_notes: parser.ri_untimely_exemption_notes(issue), + vacols_id: parser.ri_vacols_id(issue), + vacols_sequence_id: parser.ri_vacols_sequence_id(issue), + closed_at: parser.ri_closed_at(issue), + closed_status: parser.ri_closed_status(issue), + contested_rating_issue_diagnostic_code: parser.ri_contested_rating_issue_diagnostic_code(issue), + ramp_claim_id: parser.ri_ramp_claim_id(issue), + rating_issue_associated_at: parser.ri_rating_issue_associated_at(issue), + nonrating_issue_bgs_id: parser.ri_nonrating_issue_bgs_id(issue), + end_product_establishment_id: epe.id, + veteran_participant_id: parser.veteran_participant_id, + decision_review: decision_review + ) + create_request_issue_event_record(event, ri) + newly_created_issues.push(ri) + end + newly_created_issues + end + + def create_request_issue_event_record(event, issue) + EventRecord.create!(event: event, evented_record: issue) + end + end +end diff --git a/app/services/events/decision_review_created/decision_review_created_example.json b/app/services/events/decision_review_created/decision_review_created_example.json new file mode 100644 index 00000000000..2a45a80b0dd --- /dev/null +++ b/app/services/events/decision_review_created/decision_review_created_example.json @@ -0,0 +1,85 @@ +{ + "css_id": "BVADWISE", + "detail_type": "HigherLevelReview", + "station": "101", + "claim_id": "123566", + "event_id": "1", + "intake": { + "started_at": 1702067143435, + "completion_started_at": 1702067145000, + "completed_at": 1702067145000, + "completion_status": "success", + "type": "HigherLevelReviewIntake", + "detail_type": "HigherLevelReview" + }, + "veteran": { + "participant_id": "1826209", + "bgs_last_synced_at": 1708533584000, + "name_suffix": null, + "date_of_death": null + }, + "claimant": { + "payee_code": "00", + "type": "VeteranClaimant", + "participant_id": "1826209", + "name_suffix": null + }, + "claim_review": { + "benefit_type": "compensation", + "filed_by_va_gov": false, + "legacy_opt_in_approved": false, + "receipt_date": 20231208, + "veteran_is_not_claimant": false, + "establishment_attempted_at": 1702067145000, + "establishment_last_submitted_at": 1702067145000, + "establishment_processed_at": 1702067145000, + "establishment_submitted_at": 1702067145000, + "informal_conference": false, + "same_office": false + }, + "end_product_establishment": { + "benefit_type_code": "1", + "claim_date": 20231208, + "code": "030HLRNR", + "modifier": "030", + "payee_code": "00", + "reference_id": "337534", + "limited_poa_access": null, + "limited_poa_code": null, + "committed_at": 1702067145000, + "established_at": 1702067145000, + "last_synced_at": 1702067145000, + "synced_status": "RW", + "development_item_reference_id": null + }, + "request_issues": [ + { + "benefit_type": "compensation", + "contested_issue_description": null, + "contention_reference_id": 7905752, + "contested_rating_decision_reference_id": null, + "contested_rating_issue_profile_date": null, + "contested_rating_issue_reference_id": null, + "contested_decision_issue_id": null, + "decision_date": 20231220, + "ineligible_due_to_id": null, + "ineligible_reason": null, + "is_unidentified": false, + "unidentified_issue_text": null, + "nonrating_issue_category": "Accrued Benefits", + "nonrating_issue_description": "The user entered description if the issue is a nonrating issue", + "untimely_exemption": null, + "untimely_exemption_notes": null, + "vacols_id": null, + "vacols_sequence_id": null, + "closed_at": null, + "closed_status": null, + "contested_rating_issue_diagnostic_code": null, + "ramp_claim_id": null, + "rating_issue_associated_at": null, + "nonrating_issue_bgs_id": "13" + } + ] +} + + diff --git a/app/services/events/decision_review_created/decision_review_created_parser.rb b/app/services/events/decision_review_created/decision_review_created_parser.rb new file mode 100644 index 00000000000..01994e68951 --- /dev/null +++ b/app/services/events/decision_review_created/decision_review_created_parser.rb @@ -0,0 +1,411 @@ +# frozen_string_literal: true + +# Parser Class that will be used to extract out datapoints from headers & payload for use with +# DecisionReviewCreated and it's service Classes +class Events::DecisionReviewCreated::DecisionReviewCreatedParser + include Events::VeteranExtractorInterface + + attr_reader :headers, :payload + + class << self + # This method reads the drc_example.json file for our load_example method + def example_response + File.read(Rails.root.join("app", + "services", + "events", + "decision_review_created", + "decision_review_created_example.json")) + end + + # This method creates a new instance of DecisionReviewCreatedParser in order to + # mimic the parsing of a payload recieved by appeals-consumer + # arguments being passed in are the sample_header and example_response + def load_example + sample_header = { + "X-VA-Vet-SSN" => "123456789", + "X-VA-File-Number" => "77799777", + "X-VA-Vet-First-Name" => "John", + "X-VA-Vet-Last-Name" => "Smith", + "X-VA-Vet-Middle-Name" => "Alexander" + } + new(sample_header, JSON.parse(example_response)) + end + end + + def initialize(headers, payload_json) + @payload = payload_json.to_h.deep_symbolize_keys + @headers = headers + @veteran = @payload.dig(:veteran) + end + + # Generic/universal methods + def convert_milliseconds_to_datetime(milliseconds) + milliseconds.nil? ? nil : Time.at(milliseconds.to_i / 1000).to_datetime + end + + # convert logical date int to date + def logical_date_converter(logical_date_int) + logical_date_int.nil? ? nil : Time.at(logical_date_int.to_i).to_date + end + + def css_id + @payload.dig(:css_id) + end + + def detail_type + @payload.dig(:detail_type) + end + + def station_id + @payload.dig(:station) + end + + def event_id + @payload.dig(:event_id) + end + + def claim_id + @payload.dig(:claim_id) + end + + # Intake attributes + def intake + @payload.dig(:intake) + end + + def intake_started_at + intake_started_at_milliseconds = @payload.dig(:intake, :started_at) + convert_milliseconds_to_datetime(intake_started_at_milliseconds) + end + + def intake_completion_started_at + intake_completetion_start_at_milliseconds = @payload.dig(:intake, :completion_started_at) + convert_milliseconds_to_datetime(intake_completetion_start_at_milliseconds) + end + + def intake_completed_at + intake_completed_at_milliseconds = @payload.dig(:intake, :completed_at) + convert_milliseconds_to_datetime(intake_completed_at_milliseconds) + end + + def intake_completion_status + @payload.dig(:intake, :completion_status) + end + + def intake_type + @payload.dig(:intake, :type) + end + + def intake_detail_type + @payload.dig(:intake, :detail_type) + end + + # Veteran attributes + def veteran + @payload.dig(:veteran) + end + + def veteran_file_number + @veteran_file_number ||= @headers["X-VA-File-Number"].presence + end + + def veteran_ssn + @veteran_ssn ||= @headers["X-VA-Vet-SSN"].presence + end + + def veteran_first_name + @headers["X-VA-Vet-First-Name"] + end + + def veteran_last_name + @headers["X-VA-Vet-Last-Name"] + end + + def veteran_middle_name + @headers["X-VA-Vet-Middle-Name"] + end + + def person_date_of_birth + dob = @headers["X-VA-Claimant-DOB"] + convert_milliseconds_to_datetime(dob) + end + + def person_email_address + @headers["X-VA-Claimant-Email"] + end + + def person_first_name + @headers["X-VA-Claimant-First-Name"] + end + + def person_last_name + @headers["X-VA-Claimant-Last-Name"] + end + + def person_middle_name + @headers["X-VA-Claimant-Middle-Name"] + end + + def person_ssn + @headers["X-VA-Claimant-SSN"] + end + + def veteran_participant_id + @payload.dig(:veteran, :participant_id) + end + + def veteran_bgs_last_synced_at + bgs_last_synced_at_milliseconds = @payload.dig(:veteran, :bgs_last_synced_at) + convert_milliseconds_to_datetime(bgs_last_synced_at_milliseconds) + end + + def veteran_name_suffix + @payload.dig(:veteran, :name_suffix) + end + + def veteran_date_of_death + date_of_death = @payload.dig(:veteran, :date_of_death) + logical_date_converter(date_of_death) + end + + # Claimant attributes + def claimant + @payload.dig(:claimant) + end + + def claimant_payee_code + @payload.dig(:claimant, :payee_code) + end + + def claimant_type + @payload.dig(:claimant, :type) + end + + def claimant_participant_id + @payload.dig(:claimant, :participant_id) + end + + def claimant_name_suffix + @payload.dig(:claimant, :name_suffix) + end + + # ClaimReview attributes + def claim_review + @payload.dig(:claim_review) + end + + def claim_review_benefit_type + @payload.dig(:claim_review, :benefit_type) + end + + def claim_review_filed_by_va_gov + @payload.dig(:claim_review, :filed_by_va_gov) + end + + def claim_review_legacy_opt_in_approved + @payload.dig(:claim_review, :legacy_opt_in_approved) + end + + def claim_review_receipt_date + receipt_date_logical_int_date = @payload.dig(:claim_review, :receipt_date) + logical_date_converter(receipt_date_logical_int_date) + end + + def claim_review_veteran_is_not_claimant + @payload.dig(:claim_review, :veteran_is_not_claimant) + end + + def claim_review_establishment_attempted_at + establishment_attempted_at_in_milliseconds = @payload.dig(:claim_review, :establishment_attempted_at) + convert_milliseconds_to_datetime(establishment_attempted_at_in_milliseconds) + end + + def claim_review_establishment_last_submitted_at + establishment_last_submitted_at_in_milliseconds = @payload.dig(:claim_review, :establishment_last_submitted_at) + convert_milliseconds_to_datetime(establishment_last_submitted_at_in_milliseconds) + end + + def claim_review_establishment_processed_at + establishment_processed_at_in_milliseconds = @payload.dig(:claim_review, :establishment_processed_at) + convert_milliseconds_to_datetime(establishment_processed_at_in_milliseconds) + end + + def claim_review_establishment_submitted_at + establishment_submitted_at_in_milliseconds = @payload.dig(:claim_review, :establishment_submitted_at) + convert_milliseconds_to_datetime(establishment_submitted_at_in_milliseconds) + end + + def claim_review_informal_conference + @payload.dig(:claim_review, :informal_conference) + end + + def claim_review_same_office + @payload.dig(:claim_review, :same_office) + end + + # EndProductEstablishment attr + def epe + @payload.dig(:end_product_establishment) + end + + def epe_benefit_type_code + @payload.dig(:end_product_establishment, :benefit_type_code) + end + + def epe_claim_date + logical_date_int = @payload.dig(:end_product_establishment, :claim_date) + logical_date_converter(logical_date_int) + end + + def epe_code + @payload.dig(:end_product_establishment, :code) + end + + def epe_modifier + @payload.dig(:end_product_establishment, :modifier) + end + + def epe_payee_code + @payload.dig(:end_product_establishment, :payee_code) + end + + def epe_reference_id + @payload.dig(:end_product_establishment, :reference_id) + end + + def epe_limited_poa_access + @payload.dig(:end_product_establishment, :limited_poa_access) + end + + def epe_limited_poa_code + @payload.dig(:end_product_establishment, :limited_poa_code) + end + + def epe_committed_at + committed_at_milliseconds = @payload.dig(:end_product_establishment, :committed_at) + convert_milliseconds_to_datetime(committed_at_milliseconds) + end + + def epe_established_at + established_at_milliseconds = @payload.dig(:end_product_establishment, :established_at) + convert_milliseconds_to_datetime(established_at_milliseconds) + end + + def epe_last_synced_at + last_synced_at_milliseconds = @payload.dig(:end_product_establishment, :last_synced_at) + convert_milliseconds_to_datetime(last_synced_at_milliseconds) + end + + def epe_synced_status + @payload.dig(:end_product_establishment, :synced_status) + end + + def epe_development_item_reference_id + @payload.dig(:end_product_establishment, :development_item_reference_id) + end + + # RequestIssues attr + # return the array of RI objects + def request_issues + @payload.dig(:request_issues) + end + + # Individual RequestIssue parsing methods + # An RI instance will be passed in as you iterate through the array + def ri_benefit_type(issue) + issue.dig(:benefit_type) + end + + def ri_contested_issue_description(issue) + issue.dig(:contested_issue_description) + end + + def ri_contention_reference_id(issue) + issue.dig(:contention_reference_id) + end + + def ri_contested_rating_decision_reference_id(issue) + issue.dig(:contested_rating_decision_reference_id) + end + + def ri_contested_rating_issue_profile_date(issue) + issue.dig(:contested_rating_issue_profile_date) + end + + def ri_contested_rating_issue_reference_id(issue) + issue.dig(:contested_rating_issue_reference_id) + end + + def ri_contested_decision_issue_id(issue) + issue.dig(:contested_decision_issue_id) + end + + def ri_decision_date(issue) + decision_date_int = issue.dig(:decision_date) + logical_date_converter(decision_date_int) + end + + def ri_ineligible_due_to_id(issue) + issue.dig(:ineligible_due_to_id) + end + + def ri_ineligible_reason(issue) + issue.dig(:ineligible_reason) + end + + def ri_is_unidentified(issue) + issue.dig(:is_unidentified) + end + + def ri_unidentified_issue_text(issue) + issue.dig(:unidentified_issue_text) + end + + def ri_nonrating_issue_category(issue) + issue.dig(:nonrating_issue_category) + end + + def ri_nonrating_issue_description(issue) + issue.dig(:nonrating_issue_description) + end + + def ri_untimely_exemption(issue) + issue.dig(:untimely_exemption) + end + + def ri_untimely_exemption_notes(issue) + issue.dig(:untimely_exemption_notes) + end + + def ri_vacols_id(issue) + issue.dig(:vacols_id) + end + + def ri_vacols_sequence_id(issue) + issue.dig(:vacols_sequence_id) + end + + def ri_closed_at(issue) + issue.dig(:closed_at) + end + + def ri_closed_status(issue) + issue.dig(:closed_status) + end + + def ri_contested_rating_issue_diagnostic_code(issue) + issue.dig(:contested_rating_issue_diagnostic_code) + end + + def ri_ramp_claim_id(issue) + issue.dig(:ramp_claim_id) + end + + def ri_rating_issue_associated_at(issue) + issue.dig(:rating_issue_associated_at) + end + + def ri_nonrating_issue_bgs_id(issue) + issue.dig(:nonrating_issue_bgs_id) + end +end diff --git a/app/services/events/decision_review_created/update_vacols_on_optin.rb b/app/services/events/decision_review_created/update_vacols_on_optin.rb new file mode 100644 index 00000000000..1096e3aa4f1 --- /dev/null +++ b/app/services/events/decision_review_created/update_vacols_on_optin.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Events::DecisionReviewCreated::UpdateVacolsOnOptin + # Updates the Caseflow and VACOLS DB when Legacy Issues Optin to AMA + # the decision review argument being passed in can either be a Higher Level Review or a Supplemental Claim + # the decision review hash must have a benefit type. + def self.process!(decision_review:) + if decision_review.legacy_opt_in_approved + LegacyOptinManager.new(decision_review: decision_review).process! + end + # Catch the error and raise + rescue StandardError => error + raise Caseflow::Error::DecisionReviewCreateVacolsOnOptinError, error.message + end +end diff --git a/app/services/events/decision_review_created_error.rb b/app/services/events/decision_review_created_error.rb new file mode 100644 index 00000000000..709e8a88f01 --- /dev/null +++ b/app/services/events/decision_review_created_error.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +# DecisionReviewCreatedError Service. This handles the service error payload from the appeals-consumer. +# Payload requires event_id, errored_claim_id, and the error_message within the request +# This service also uses the RedisMutex.with_lock to make sure parallel transactions related to the claim_id +# does not make database changes at the same time. +class Events::DecisionReviewCreatedError + # Using macro-style definition. The locking scope will be TheClass#method and only one method can run at any + # given time. + include RedisMutex::Macro + + # Default options for RedisMutex#with_lock + # :block => 1 # Specify in seconds how long you want to wait for the lock to be released. + # # Specify 0 if you need non-blocking sematics and return false immediately. (default: 1) + # :sleep => 0.1 # Specify in seconds how long the polling interval should be when :block is given. + # # It is NOT recommended to go below 0.01. (default: 0.1) + # :expire => 10 # Specify in seconds when the lock should be considered stale when something went wrong + # # with the one who held the lock and failed to unlock. (default: 10) + class << self + def handle_service_error(consumer_event_id, errored_claim_id, error_message) + # check if consumer_event_id Event.reference_id exist if not Create DecisionReviewCreated Event + event = DecisionReviewCreatedEvent.find_or_create_by(reference_id: consumer_event_id) + + redis = Redis.new(url: Rails.application.secrets.redis_url_cache) + + if redis.exists("RedisMutex:EndProductEstablishment:#{errored_claim_id}") + fail Caseflow::Error::RedisLockFailed, message: "Key RedisMutex:EndProductEstablishment:#{errored_claim_id} + is already in the Redis Cache" + end + + RedisMutex.with_lock("EndProductEstablishment:#{errored_claim_id}", block: 60, expire: 100) do + ActiveRecord::Base.transaction do + event&.update!(error: error_message, info: { "errored_claim_id" => errored_claim_id }) + end + end + rescue RedisMutex::LockError => error + Rails.logger.error("LockError occurred: #{error.message}") + rescue StandardError => error + Rails.logger.error(error.message) + event&.update!(error: error.message) + raise error + end + end +end diff --git a/app/services/events/veteran_extractor_interface.rb b/app/services/events/veteran_extractor_interface.rb new file mode 100644 index 00000000000..76e0e594af1 --- /dev/null +++ b/app/services/events/veteran_extractor_interface.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +# Interface for use with Events::DecisionReviewCreated::DecisionReviewCreatedParser +# to extract Veteran information +module Events::VeteranExtractorInterface + def veteran_file_number + fail NotImplementedError, "#{self.class} has not implemented method '#{__method__}'" + end + + def veteran_ssn + fail NotImplementedError, "#{self.class} has not implemented method '#{__method__}'" + end + + def veteran_first_name + fail NotImplementedError, "#{self.class} has not implemented method '#{__method__}'" + end + + def veteran_last_name + fail NotImplementedError, "#{self.class} has not implemented method '#{__method__}'" + end + + def veteran_middle_name + fail NotImplementedError, "#{self.class} has not implemented method '#{__method__}'" + end + + def veteran_participant_id + fail NotImplementedError, "#{self.class} has not implemented method '#{__method__}'" + end + + def veteran_bgs_last_synced_at + fail NotImplementedError, "#{self.class} has not implemented method '#{__method__}'" + end + + def veteran_name_suffix + fail NotImplementedError, "#{self.class} has not implemented method '#{__method__}'" + end + + def veteran_date_of_death + fail NotImplementedError, "#{self.class} has not implemented method '#{__method__}'" + end +end diff --git a/config/environments/development.rb b/config/environments/development.rb index a8e681a4b22..d6e1a827684 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -155,4 +155,7 @@ config.efolder_key = "token" config.google_analytics_account = "UA-74789258-5" + + # Appeals Consumer + config.hosts << "host.docker.internal" end diff --git a/config/routes.rb b/config/routes.rb index 9779a1314c1..d072e39bafb 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -84,6 +84,14 @@ get "vacols_issues", to: "docs#vacols_issues" end end + + namespace :events do + namespace :v1 do + post '/decision_review_created', to: 'decision_review_created#decision_review_created' + post '/decision_review_created_error', to: 'decision_review_created#decision_review_created_error' + end + end + get "metadata", to: 'metadata#index' end diff --git a/db/migrate/20240116174509_create_events.rb b/db/migrate/20240116174509_create_events.rb new file mode 100644 index 00000000000..295cf209420 --- /dev/null +++ b/db/migrate/20240116174509_create_events.rb @@ -0,0 +1,12 @@ +class CreateEvents < Caseflow::Migration + def change + create_table :events, comment: "Stores events from the Appeals-Consumer application that are processed by Caseflow" do |t| + t.string :reference_id, null: false, comment: "Id of Event Record being referenced within the Appeals Consumer Application" + t.string :type, null: false, comment: "Type of Event (e.g. DecisionReviewCreatedEvent)" + t.timestamp :created_at, null: false, comment: "Automatic timestamp when row was created" + t.timestamp :updated_at, null: false, comment: "Automatic timestamp whenever the record changes" + t.timestamp :completed_at, comment: "Timestamp of when event was successfully completed" + t.string :error, comment: "Error message captured during a failed event" + end + end +end diff --git a/db/migrate/20240116211523_create_event_records.rb b/db/migrate/20240116211523_create_event_records.rb new file mode 100644 index 00000000000..fb16081ed4b --- /dev/null +++ b/db/migrate/20240116211523_create_event_records.rb @@ -0,0 +1,11 @@ +class CreateEventRecords < Caseflow::Migration + def change + create_table :event_records, comment: "Stores records that are created or updated by an event from the Appeals-Consumer application." do |t| + t.integer :event_id, null: false, foreign_key: { to_table: :events }, comment: "ID of the Event that created or updated this record." + t.timestamp :created_at, null: false, comment: "Automatic timestamp when row was created" + t.timestamp :updated_at, null: false, comment: "Automatic timestamp whenever the record changes" + + t.references :backfill_record, null: false, polymorphic: true, index: {:name => "index_event_record_on_backfill_record"} + end + end +end diff --git a/db/migrate/20240123191625_add_foreign_key_to_events.rb b/db/migrate/20240123191625_add_foreign_key_to_events.rb new file mode 100644 index 00000000000..44a7f15a630 --- /dev/null +++ b/db/migrate/20240123191625_add_foreign_key_to_events.rb @@ -0,0 +1,5 @@ +class AddForeignKeyToEvents < ActiveRecord::Migration[5.2] + def change + add_foreign_key "event_records", "events", name: "event_records_event_id_fk", validate: false + end +end diff --git a/db/migrate/20240123192715_validate_foreign_keys_on_events.rb b/db/migrate/20240123192715_validate_foreign_keys_on_events.rb new file mode 100644 index 00000000000..46ad395bb4a --- /dev/null +++ b/db/migrate/20240123192715_validate_foreign_keys_on_events.rb @@ -0,0 +1,5 @@ +class ValidateForeignKeysOnEvents < ActiveRecord::Migration[5.2] + def change + validate_foreign_key "event_records", name: "event_records_event_id_fk" + end +end diff --git a/db/migrate/20240205154329_add_info_to_events.rb b/db/migrate/20240205154329_add_info_to_events.rb new file mode 100644 index 00000000000..05a65484d06 --- /dev/null +++ b/db/migrate/20240205154329_add_info_to_events.rb @@ -0,0 +1,15 @@ +class AddInfoToEvents < ActiveRecord::Migration[5.2] + disable_ddl_transaction! + + def up + add_column :events, :info, :jsonb, default: {} + safety_assured { execute(<<-SQL) } + CREATE INDEX CONCURRENTLY index_events_on_info + ON events USING gin (info); + SQL + end + + def down + remove_column :events, :info + end +end diff --git a/db/migrate/20240411185612_rename_backfill_record_to_evented_record.rb b/db/migrate/20240411185612_rename_backfill_record_to_evented_record.rb new file mode 100644 index 00000000000..98f5f0a10ce --- /dev/null +++ b/db/migrate/20240411185612_rename_backfill_record_to_evented_record.rb @@ -0,0 +1,9 @@ +class RenameBackfillRecordToEventedRecord < ActiveRecord::Migration[6.0] + def change + safety_assured do + rename_column :event_records, :backfill_record_id, :evented_record_id + rename_column :event_records, :backfill_record_type, :evented_record_type + rename_index :event_records, "index_event_record_on_backfill_record", "index_event_record_on_evented_record" + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 72058063bb0..e90e330306f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -978,6 +978,26 @@ t.index ["user_id"], name: "index_end_product_updates_on_user_id" end + create_table "event_records", comment: "Stores records that are created or updated by an event from the Appeals-Consumer application.", force: :cascade do |t| + t.datetime "created_at", null: false, comment: "Automatic timestamp when row was created" + t.integer "event_id", null: false, comment: "ID of the Event that created or updated this record." + t.bigint "evented_record_id", null: false + t.string "evented_record_type", null: false + t.datetime "updated_at", null: false, comment: "Automatic timestamp whenever the record changes" + t.index ["evented_record_type", "evented_record_id"], name: "index_event_record_on_evented_record" + end + + create_table "events", comment: "Stores events from the Appeals-Consumer application that are processed by Caseflow", force: :cascade do |t| + t.datetime "completed_at", comment: "Timestamp of when event was successfully completed" + t.datetime "created_at", null: false, comment: "Automatic timestamp when row was created" + t.string "error", comment: "Error message captured during a failed event" + t.jsonb "info", default: {} + t.string "reference_id", null: false, comment: "Id of Event Record being referenced within the Appeals Consumer Application" + t.string "type", null: false, comment: "Type of Event (e.g. DecisionReviewCreatedEvent)" + t.datetime "updated_at", null: false, comment: "Automatic timestamp whenever the record changes" + t.index ["info"], name: "index_events_on_info", using: :gin + end + create_table "form8s", id: :serial, force: :cascade do |t| t.string "_initial_appellant_name" t.string "_initial_appellant_relationship" @@ -2476,6 +2496,7 @@ add_foreign_key "end_product_establishments", "users" add_foreign_key "end_product_updates", "end_product_establishments" add_foreign_key "end_product_updates", "users" + add_foreign_key "event_records", "events", name: "event_records_event_id_fk" add_foreign_key "hearing_appeal_stream_snapshots", "legacy_appeals", column: "appeal_id" add_foreign_key "hearing_appeal_stream_snapshots", "legacy_hearings", column: "hearing_id" add_foreign_key "hearing_days", "users", column: "created_by_id" diff --git a/lib/caseflow/error.rb b/lib/caseflow/error.rb index bcee413e964..41c0be42683 100644 --- a/lib/caseflow/error.rb +++ b/lib/caseflow/error.rb @@ -491,9 +491,21 @@ class PacmanForbiddenError < PacmanApiError; end class PacmanNotFoundError < PacmanApiError; end class PacmanInternalServerError < PacmanApiError; end + # Redis Lock errors class SyncLockFailed < StandardError def ignorable? true end end + class RedisLockFailed < StandardError; end + + # Event Decision Review Create Errors + class DecisionReviewCreatedUserError < StandardError; end + class DecisionReviewCreatedVeteranError < StandardError; end + class DecisionReviewCreateVacolsOnOptinError < StandardError; end + class DecisionReviewCreatedClaimantError < StandardError; end + class DecisionReviewCreatedIntakeError < StandardError; end + class DecisionReviewCreatedCreateClaimReviewError < StandardError; end + class DecisionReviewCreatedEpEstablishmentError < StandardError; end + class DecisionReviewCreatedRequestIssuesError < StandardError; end end diff --git a/spec/controllers/api/events/v1/decision_review_created_controller_spec.rb b/spec/controllers/api/events/v1/decision_review_created_controller_spec.rb new file mode 100644 index 00000000000..d4ee65c8b6b --- /dev/null +++ b/spec/controllers/api/events/v1/decision_review_created_controller_spec.rb @@ -0,0 +1,111 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe Api::Events::V1::DecisionReviewCreatedController, type: :controller do + describe "POST #decision_review_created" do + let!(:current_user) { User.authenticate! } + let(:api_key) { ApiKey.create!(consumer_name: "API TEST TOKEN") } + let!(:payload) { Events::DecisionReviewCreated::DecisionReviewCreatedParser.example_response } + + context "with a valid token" do + it "returns success response" do + request.headers["Authorization"] = "Token token=#{api_key.key_string}" + load_headers + post :decision_review_created, params: JSON.parse(payload) + expect(response).to have_http_status(:created) + end + end + + context "when claim_id is already in Redis Cache" do + it "throws a Redis error and returns a 409 status" do + request.headers["Authorization"] = "Token token=#{api_key.key_string}" + load_headers + redis = Redis.new(url: Rails.application.secrets.redis_url_cache) + lock_key = "RedisMutex:EndProductEstablishment:123566" + redis.set(lock_key, "lock is set", nx: true, ex: 5.seconds) + post :decision_review_created, params: JSON.parse(payload) + expect(response).to have_http_status(:conflict) + redis.del(lock_key) + end + end + + context "with an invalid token" do + it "returns unauthorized response" do + request.headers["Authorization"] = "invalid_token" + post :decision_review_created, params: JSON.parse(payload) + expect(response).to have_http_status(:unauthorized) + end + end + + context "without a token" do + it "returns unauthorized response" do + # Omitting Authorization header to simulate missing token + post :decision_review_created, params: JSON.parse(payload) + expect(response).to have_http_status(:unauthorized) + end + end + end + + describe "POST #decision_review_created_error" do + let!(:current_user) { User.authenticate! } + let(:api_key) { ApiKey.create!(consumer_name: "Appeals-Consumer") } + let!(:appeals_consumer_paylod) { { "event_id": 3333, "errored_claim_id": 1345, "error": "this was an error" } } + context "with an Authorized Token" do + it "renders message Error Creating Decision Review and returns a method not allowed status" do + request.headers["Authorization"] = "Token #{api_key.key_string}" + post :decision_review_created_error, params: appeals_consumer_paylod + expect(response).to have_http_status(:created) + expect(JSON.parse(response.body)["message"]).to eq("Decision Review Created Error Saved in Caseflow") + end + end + context "with an invalid Token" do + it "returns unauthorized response" do + request.headers["Authorization"] = "unAuthToken" + post :decision_review_created_error + expect(response).to have_http_status(:unauthorized) + end + end + context "with no token" do + it "returns unauthorized response" do + # Omitting Authorization header to simulate missing token + post :decision_review_created_error + expect(response).to have_http_status(:unauthorized) + end + end + context "catches errors" do + it "raises a RedisLockFailed Error" do + allow(Events::DecisionReviewCreatedError).to receive(:handle_service_error) + .and_raise(Caseflow::Error::RedisLockFailed.new("Lock Failure")) + request.headers["Authorization"] = "Token #{api_key.key_string}" + post :decision_review_created_error, params: appeals_consumer_paylod + expect(response).to have_http_status(:conflict) + expect(response.body).to eq({ message: "Lock Failure" }.to_json) + end + it "raises a Standard Error" do + allow(Events::DecisionReviewCreatedError).to receive(:handle_service_error) + .and_raise(StandardError.new("Standard Failure")) + request.headers["Authorization"] = "Token #{api_key.key_string}" + post :decision_review_created_error, params: appeals_consumer_paylod + expect(response).to have_http_status(:unprocessable_entity) + expect(response.body).to eq({ message: "Standard Failure" }.to_json) + end + end + end +end + +def json_payload + JSON.parse(File.read(Rails.root.join("app", + "services", + "events", + "decision_review_created", + "decision_review_created_example.json"))) +end + +def load_headers + request.headers["X-VA-Vet-SSN"] = "123456789" + request.headers["X-VA-File-Number"] = "77799777" + request.headers["X-VA-Vet-First-Name"] = "John" + request.headers["X-VA-Vet-Last-Name"] = "Smith" + request.headers["X-VA-Vet-Middle-Name"] = "Alexander" +end diff --git a/spec/factories/decision_review_created_event.rb b/spec/factories/decision_review_created_event.rb new file mode 100644 index 00000000000..468149fc87e --- /dev/null +++ b/spec/factories/decision_review_created_event.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :decision_review_created_event do + reference_id { "1" } + end +end diff --git a/spec/factories/events.rb b/spec/factories/events.rb new file mode 100644 index 00000000000..8ac25ada852 --- /dev/null +++ b/spec/factories/events.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :event do + sequence(:id) { |n| n } + reference_id { 2 } + type { "DecisionReviewCreatedEvent" } + created_at { DateTime.now } + updated_at { DateTime.now } + completed_at { nil } + error { nil } + info { {} } + end +end diff --git a/spec/feature/events/decision_review_created/decision_review_created_event_failure_spec.rb b/spec/feature/events/decision_review_created/decision_review_created_event_failure_spec.rb new file mode 100644 index 00000000000..a5d1126e084 --- /dev/null +++ b/spec/feature/events/decision_review_created/decision_review_created_event_failure_spec.rb @@ -0,0 +1,122 @@ +# frozen_string_literal: true + +RSpec.describe Api::Events::V1::DecisionReviewCreatedController, type: :controller do + describe "POST #decision_review_created" do + let!(:current_user) { User.authenticate! } + let(:api_key) { ApiKey.create!(consumer_name: "API TEST TOKEN") } + let!(:payload) { Events::DecisionReviewCreated::DecisionReviewCreatedParser.example_response } + let(:parser) { Events::DecisionReviewCreated::DecisionReviewCreatedParser.load_example } + + # Each context will load the payload and then overwrite certain values with "nil" to simulate missing data + context "when there are invalid top level params" do + it "raises an error when trying to create a User" do + hash = JSON.parse(payload) + hash["css_id"] = nil + hash["station"] = nil + load_headers + post :decision_review_created, params: hash, as: :json + expect(response).to have_http_status(:unprocessable_entity) + expect(EventRecord.count).to eq(0) + expect(Event.count).to eq(1) + event = Event.last + expect(event.error).to include("DecisionReviewCreatedUserError") + end + end + + context "when there are missing Veteran params" do + it "raises an error when trying to create a Veteran" do + hash = JSON.parse(payload) + load_headers + request.headers["X-VA-Vet-SSN"] = nil + request.headers["X-VA-File-Number"] = nil + post :decision_review_created, params: hash, as: :json + expect(response).to have_http_status(:unprocessable_entity) + expect(EventRecord.count).to eq(0) + expect(Event.count).to eq(1) + event = Event.last + expect(event.error).to include("DecisionReviewCreatedVeteranError") + end + end + + context "when there are missing ClaimReview params (veteran_file_number)" do + it "raises an error when trying to create a ClaimReview" do + hash = JSON.parse(payload) + load_headers + request.headers["X-VA-File-Number"] = nil + post :decision_review_created, params: hash, as: :json + expect(response).to have_http_status(:unprocessable_entity) + expect(EventRecord.count).to eq(0) + expect(Event.count).to eq(1) + event = Event.last + # failure occurs when trying to create Veteran, which happens in an earlier step than ClaimReview + expect(event.error).to include("DecisionReviewCreatedVeteranError") + end + end + + context "when there are missing Claimant params" do + it "raises an error when trying to create a Claimant" do + hash = JSON.parse(payload) + load_headers + hash["claim_review"]["veteran_is_not_claimant"] = true + hash["claimant"]["participant_id"] = nil + post :decision_review_created, params: hash, as: :json + expect(response).to have_http_status(:unprocessable_entity) + expect(EventRecord.count).to eq(0) + expect(Event.count).to eq(1) + event = Event.last + expect(event.error).to include("DecisionReviewCreatedClaimantError") + end + end + + context "when there is bad data in Intake params" do + it "raises an error when trying to create an Intake" do + hash = JSON.parse(payload) + load_headers + hash["intake"]["type"] = 999 + post :decision_review_created, params: hash, as: :json + expect(response).to have_http_status(:unprocessable_entity) + expect(EventRecord.count).to eq(0) + expect(Event.count).to eq(1) + event = Event.last + expect(event.error).to include("DecisionReviewCreatedIntakeError") + end + end + + context "when there are missing EPE params" do + it "raises an error when trying to create an EPE" do + hash = JSON.parse(payload) + load_headers + hash["end_product_establishment"]["payee_code"] = nil + post :decision_review_created, params: hash, as: :json + expect(response).to have_http_status(:unprocessable_entity) + expect(EventRecord.count).to eq(0) + expect(Event.count).to eq(1) + event = Event.last + expect(event.error).to include("DecisionReviewCreatedEpEstablishmentError") + end + end + + context "when there are missing Request Issue params" do + it "raises an error when trying to create Request Issues" do + hash = JSON.parse(payload) + load_headers + hash["request_issues"][0]["benefit_type"] = nil + post :decision_review_created, params: hash, as: :json + expect(response).to have_http_status(:unprocessable_entity) + expect(EventRecord.count).to eq(0) + expect(Event.count).to eq(1) + event = Event.last + expect(event.error).to include("DecisionReviewCreatedRequestIssuesError") + end + end + end +end + +def load_headers + request.headers["Authorization"] = "Token token=#{api_key.key_string}" + request.headers["X-VA-Vet-SSN"] = "123456789" + request.headers["X-VA-File-Number"] = "77799777" + request.headers["X-VA-Vet-First-Name"] = "John" + request.headers["X-VA-Vet-Last-Name"] = "Smith" + request.headers["X-VA-Vet-Middle-Name"] = "Alexander" +end diff --git a/spec/feature/events/decision_review_created/scenario_a_spec.rb b/spec/feature/events/decision_review_created/scenario_a_spec.rb new file mode 100644 index 00000000000..0726b335861 --- /dev/null +++ b/spec/feature/events/decision_review_created/scenario_a_spec.rb @@ -0,0 +1,146 @@ +# frozen_string_literal: true + +# rubocop:disable Style/NumericLiterals + +RSpec.describe Api::Events::V1::DecisionReviewCreatedController, type: :controller do + describe "POST #decision_review_created" do + let!(:current_user) { User.authenticate! } + let(:api_key) { ApiKey.create!(consumer_name: "API TEST TOKEN") } + let!(:person) do + Person.create(participant_id: "1826209", first_name: "Jimmy", last_name: "Longstocks", + middle_name: "Goob", ssn: "989773212", name_suffix: "") + end + + def json_payload + { + "event_id": "123", + "claim_id": "9999", + "css_id": "BVADWISE", + "detail_type": "HigherLevelReview", + "station": "101", + "intake": { + "started_at": 1702067143435, + "completion_started_at": 1702067145000, + "completed_at": 1702067145000, + "completion_status": "success", + "type": "HigherLevelReviewIntake", + "detail_type": "HigherLevelReview" + }, + "veteran": { + "participant_id": "1826209", + "bgs_last_synced_at": 1708533584000, + "name_suffix": nil, + "date_of_death": nil + }, + "claimant": { + "payee_code": "00", + "type": "VeteranClaimant", + "participant_id": "1826209", + "name_suffix": nil + }, + "claim_review": { + "benefit_type": "compensation", + "filed_by_va_gov": false, + "legacy_opt_in_approved": false, + "receipt_date": 20231208, + "veteran_is_not_claimant": true, + "establishment_attempted_at": 1702067145000, + "establishment_last_submitted_at": 1702067145000, + "establishment_processed_at": 1702067145000, + "establishment_submitted_at": 1702067145000, + "informal_conference": false, + "same_office": false + }, + "end_product_establishment": { + "benefit_type_code": "1", + "claim_date": 20231208, + "code": "030HLRNR", + "modifier": "030", + "payee_code": "00", + "reference_id": "337534", + "limited_poa_access": nil, + "limited_poa_code": nil, + "committed_at": 1702067145000, + "established_at": 1702067145000, + "last_synced_at": 1702067145000, + "synced_status": "RW", + "development_item_reference_id": nil + }, + "request_issues": [ + { + "benefit_type": "compensation", + "contested_issue_description": nil, + "contention_reference_id": 7905752, + "contested_rating_decision_reference_id": nil, + "contested_rating_issue_profile_date": nil, + "contested_rating_issue_reference_id": nil, + "contested_decision_issue_id": nil, + "decision_date": 20231220, + "ineligible_due_to_id": nil, + "ineligible_reason": nil, + "is_unidentified": false, + "unidentified_issue_text": nil, + "nonrating_issue_category": "Accrued Benefits", + "nonrating_issue_description": "The user entered description if the issue is a nonrating issue", + "untimely_exemption": nil, + "untimely_exemption_notes": nil, + "vacols_id": nil, + "vacols_sequence_id": nil, + "closed_at": nil, + "closed_status": nil, + "contested_rating_issue_diagnostic_code": nil, + "ramp_claim_id": nil, + "rating_issue_associated_at": nil, + "nonrating_issue_bgs_id": "13" + } + ] + } + end + + let!(:valid_params) do + json_payload + end + + context "with a valid token and user exists" do + it "returns success response when user exists, veteran exists, is claimant, + is HLR, person exists and request issues exist" do + vet = Veteran.create!( + file_number: "77799777", + ssn: "123456789", + first_name: "John", + last_name: "Smith", + middle_name: "Alexander", + participant_id: "1826209", + bgs_last_synced_at: 1708533584000, + name_suffix: nil, + date_of_death: nil + ) + user = User.create(css_id: "BVADWISE", station_id: 101, status: Constants.USER_STATUSES.inactive) + expect(Person.find_by(participant_id: "1826209")).to be_present + expect(Person.count).to eq(1) + request.headers["Authorization"] = "Token #{api_key.key_string}" + request.headers["X-VA-Vet-SSN"] = "123456789" + request.headers["X-VA-File-Number"] = "77799777" + request.headers["X-VA-Vet-First-Name"] = "John" + request.headers["X-VA-Vet-Last-Name"] = "Smith" + request.headers["X-VA-Vet-Middle-Name"] = "Alexander" + request.headers["X-VA-Claimant-DOB"] = DateTime.now - 30.years + request.headers["X-VA-Claimant-Email"] = "jim@google.com" + request.headers["X-VA-Claimant-First-Name"] = "Jimmy" + request.headers["X-VA-Claimant-Last-Name"] = "Longstocks" + request.headers["X-VA-Claimant-Middle-Name"] = "Goob" + request.headers["X-VA-Claimant-SSN"] = "989773212" + post :decision_review_created, params: valid_params + expect(response).to have_http_status(:created) + expect(User.find_by(css_id: "BVADWISE")).to eq(user) + expect(Veteran.find_by(file_number: "77799777")).to eq(vet) + expect(Claimant.find_by(participant_id: "1826209")).to be_present + expect(HigherLevelReview.find_by(veteran_file_number: vet.file_number)).to be_present + expect(RequestIssue.find_by(contention_reference_id: 7905752)).to be_present + expect(Person.count).to eq(1) + end + end + end +end + +# rubocop:enable Style/NumericLiterals diff --git a/spec/feature/events/decision_review_created/scenario_b_spec.rb b/spec/feature/events/decision_review_created/scenario_b_spec.rb new file mode 100644 index 00000000000..31a30769e60 --- /dev/null +++ b/spec/feature/events/decision_review_created/scenario_b_spec.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +# rubocop:disable Style/NumericLiterals + +RSpec.describe Api::Events::V1::DecisionReviewCreatedController, type: :controller do + describe "POST #decision_review_created" do + let!(:current_user) { User.authenticate! } + let(:api_key) { ApiKey.create!(consumer_name: "API TEST TOKEN") } + let!(:person) do + Person.create(participant_id: "1826209", first_name: "Jimmy", last_name: "Longstocks", + middle_name: "Goob", ssn: "989773212", name_suffix: "") + end + + def json_payload + { + "event_id": "123", + "claim_id": "9999", + "css_id": "BVADWISE", + "detail_type": "SupplementalClaim", + "station": "101", + "intake": { + "started_at": 1702067143435, + "completion_started_at": 1702067145000, + "completed_at": 1702067145000, + "completion_status": "success", + "type": "SupplementalClaimIntake", + "detail_type": "SupplementalClaim" + }, + "veteran": { + "participant_id": "1826209", + "bgs_last_synced_at": 1708533584000, + "name_suffix": nil, + "date_of_death": nil + }, + "claimant": { + "payee_code": "00", + "type": "Claimant", + "participant_id": "1826209", + "name_suffix": nil + }, + "claim_review": { + "benefit_type": "compensation", + "filed_by_va_gov": false, + "legacy_opt_in_approved": false, + "receipt_date": 20231208, + "veteran_is_not_claimant": true, + "establishment_attempted_at": 1702067145000, + "establishment_last_submitted_at": 1702067145000, + "establishment_processed_at": 1702067145000, + "establishment_submitted_at": 1702067145000, + "informal_conference": false, + "same_office": false + }, + "end_product_establishment": { + "benefit_type_code": "1", + "claim_date": 20231208, + "code": "030HLRNR", + "modifier": "030", + "payee_code": "00", + "reference_id": "337534", + "limited_poa_access": nil, + "limited_poa_code": nil, + "committed_at": 1702067145000, + "established_at": 1702067145000, + "last_synced_at": 1702067145000, + "synced_status": "RW", + "development_item_reference_id": nil + }, + "request_issues": [] + } + end + + let!(:valid_params) do + json_payload + end + + context "with a valid token and user exists" do + it "returns success response when user does not exist, veteran does not exists, is claimant, + is Supplemental claim, person exists and request issues don't exist" do + request.headers["Authorization"] = "Token #{api_key.key_string}" + request.headers["X-VA-Vet-SSN"] = "123456789" + request.headers["X-VA-File-Number"] = "77799777" + request.headers["X-VA-Vet-First-Name"] = "John" + request.headers["X-VA-Vet-Last-Name"] = "Smith" + request.headers["X-VA-Vet-Middle-Name"] = "Alexander" + request.headers["X-VA-Claimant-DOB"] = DateTime.now - 30.years + request.headers["X-VA-Claimant-Email"] = "jim@google.com" + request.headers["X-VA-Claimant-First-Name"] = "Jimmy" + request.headers["X-VA-Claimant-Last-Name"] = "Longstocks" + request.headers["X-VA-Claimant-Middle-Name"] = "Goob" + request.headers["X-VA-Claimant-SSN"] = "989773212" + expect(Person.find_by(participant_id: "1826209")).to be_present + expect(Person.count).to eq(1) + expect(User.find_by(css_id: "BVADWISE")).to eq(nil) + expect(Veteran.find_by(file_number: "77799777")).to eq(nil) + post :decision_review_created, params: valid_params + expect(response).to have_http_status(:created) + expect(User.find_by(css_id: "BVADWISE")).to be_present + expect(Veteran.find_by(file_number: "77799777")).to be_present + expect(Claimant.find_by(participant_id: "1826209")).to be_present + expect(SupplementalClaim.find_by(veteran_file_number: "77799777")).to be_present + expect(RequestIssue.find_by(contention_reference_id: 7905752)).to be_nil + expect(Person.count).to eq(1) + end + end + end +end + +# rubocop:enable Style/NumericLiterals diff --git a/spec/feature/events/decision_review_created/scenario_c_spec.rb b/spec/feature/events/decision_review_created/scenario_c_spec.rb new file mode 100644 index 00000000000..14ec51702cc --- /dev/null +++ b/spec/feature/events/decision_review_created/scenario_c_spec.rb @@ -0,0 +1,139 @@ +# frozen_string_literal: true + +# rubocop:disable Style/NumericLiterals + +RSpec.describe Api::Events::V1::DecisionReviewCreatedController, type: :controller do + describe "POST #decision_review_created" do + let!(:current_user) { User.authenticate! } + let(:api_key) { ApiKey.create!(consumer_name: "API TEST TOKEN") } + + def json_payload + { + "event_id": "123", + "claim_id": "9999", + "css_id": "BVADWISE", + "detail_type": "HigherLevelReview", + "station": "101", + "intake": { + "started_at": 1702067143435, + "completion_started_at": 1702067145000, + "completed_at": 1702067145000, + "completion_status": "success", + "type": "HigherLevelReviewIntake", + "detail_type": "HigherLevelReview" + }, + "veteran": { + "participant_id": "1826209", + "bgs_last_synced_at": 1708533584000, + "name_suffix": nil, + "date_of_death": nil + }, + "claimant": { + "payee_code": "00", + "type": "VeteranClaimant", + "participant_id": "1826209", + "name_suffix": nil + }, + "claim_review": { + "benefit_type": "compensation", + "filed_by_va_gov": false, + "legacy_opt_in_approved": false, + "receipt_date": 20231208, + "veteran_is_not_claimant": true, + "establishment_attempted_at": 1702067145000, + "establishment_last_submitted_at": 1702067145000, + "establishment_processed_at": 1702067145000, + "establishment_submitted_at": 1702067145000, + "informal_conference": false, + "same_office": false + }, + "end_product_establishment": { + "benefit_type_code": "1", + "claim_date": 20231208, + "code": "030HLRNR", + "modifier": "030", + "payee_code": "00", + "reference_id": "337534", + "limited_poa_access": nil, + "limited_poa_code": nil, + "committed_at": 1702067145000, + "established_at": 1702067145000, + "last_synced_at": 1702067145000, + "synced_status": "RW", + "development_item_reference_id": nil + }, + "request_issues": [ + { + "benefit_type": "compensation", + "contested_issue_description": nil, + "contention_reference_id": 7905752, + "contested_rating_decision_reference_id": nil, + "contested_rating_issue_profile_date": nil, + "contested_rating_issue_reference_id": nil, + "contested_decision_issue_id": nil, + "decision_date": 20231220, + "ineligible_due_to_id": nil, + "ineligible_reason": nil, + "is_unidentified": false, + "unidentified_issue_text": nil, + "nonrating_issue_category": "Accrued Benefits", + "nonrating_issue_description": "The user entered description if the issue is a nonrating issue", + "untimely_exemption": nil, + "untimely_exemption_notes": nil, + "vacols_id": nil, + "vacols_sequence_id": nil, + "closed_at": nil, + "closed_status": nil, + "contested_rating_issue_diagnostic_code": nil, + "ramp_claim_id": nil, + "rating_issue_associated_at": nil, + "nonrating_issue_bgs_id": "13" + } + ] + } + end + + let!(:valid_params) do + json_payload + end + + context "with a valid token and user exists" do + it "returns success response when veteran exists, is claimant, is HLR, + Person does not exist and request issues exist" do + vet = Veteran.create!( + file_number: "77799777", + ssn: "123456789", + first_name: "John", + last_name: "Smith", + middle_name: "Alexander", + participant_id: "1826209", + bgs_last_synced_at: 1708533584000, + name_suffix: nil, + date_of_death: nil + ) + request.headers["Authorization"] = "Token #{api_key.key_string}" + request.headers["X-VA-Vet-SSN"] = "123456789" + request.headers["X-VA-File-Number"] = "77799777" + request.headers["X-VA-Vet-First-Name"] = "John" + request.headers["X-VA-Vet-Last-Name"] = "Smith" + request.headers["X-VA-Vet-Middle-Name"] = "Alexander" + request.headers["X-VA-Claimant-DOB"] = DateTime.now - 30.years + request.headers["X-VA-Claimant-Email"] = "jim@google.com" + request.headers["X-VA-Claimant-First-Name"] = "Jimmy" + request.headers["X-VA-Claimant-Last-Name"] = "Longstocks" + request.headers["X-VA-Claimant-Middle-Name"] = "Goob" + request.headers["X-VA-Claimant-SSN"] = "989773212" + expect(Person.find_by(participant_id: "1826209")).to eq(nil) + post :decision_review_created, params: valid_params + expect(response).to have_http_status(:created) + expect(Veteran.find_by(file_number: "77799777")).to eq(vet) + expect(Claimant.find_by(participant_id: "1826209")).to be_present + expect(Person.find_by(participant_id: "1826209")).to be_present + expect(HigherLevelReview.find_by(veteran_file_number: vet.file_number)).to be_present + expect(RequestIssue.find_by(contention_reference_id: 7905752)).to be_present + end + end + end +end + +# rubocop:enable Style/NumericLiterals diff --git a/spec/models/events/decision_review_created_event_spec.rb b/spec/models/events/decision_review_created_event_spec.rb new file mode 100644 index 00000000000..77be6710712 --- /dev/null +++ b/spec/models/events/decision_review_created_event_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +RSpec.describe DecisionReviewCreatedEvent, type: :model do + describe "inheritance" do + subject { create(:decision_review_created_event) } + it "is a child class of Event and is the correct type" do + subject.reload + expect(Event.count).to eq(1) + expect(Event.first.id).to eq(subject.id) + expect(Event.first.type).to eq(subject.class.name) + end + end + + describe "#completed?" do + let!(:event) { create(:decision_review_created_event) } + + context "when an event is in progress" do + it "should not be completed" do + expect(event.completed?).to eq false + end + end + + context "when an event is completed" do + it "should return completed status" do + event.update(completed_at: Time.current) + expect(event.completed?).to eq true + end + end + end + + describe "event records association" do + let!(:my_event) { create(:decision_review_created_event) } + let!(:intake) { create(:intake) } + let!(:event_record) { EventRecord.create!(event_id: my_event.id, evented_record: intake) } + let(:veteran_file_number) { "64205050" } + let!(:higher_level_review) { HigherLevelReview.new(veteran_file_number: veteran_file_number) } + let!(:higher_level_review_event_record) do + EventRecord.create!(event_id: my_event.id, evented_record: higher_level_review) + end + + it "should associate with it's event_records" do + expect(my_event.event_records.count).to eq 2 + end + + it "should have event_records that have a bi-directional relationship with itself" do + expect(my_event.event_records.first.event_id).to eq my_event.id + expect(my_event.event_records.last.event_id).to eq my_event.id + end + end +end diff --git a/spec/models/events/event_record_spec.rb b/spec/models/events/event_record_spec.rb new file mode 100644 index 00000000000..08cfe8c0e06 --- /dev/null +++ b/spec/models/events/event_record_spec.rb @@ -0,0 +1,134 @@ +# frozen_string_literal: true + +describe EventRecord, :postgres do + context "One Event with One Event Record with One Intake" do + let(:user) { Generators::User.build } + let(:veteran_file_number) { "64205050" } + let!(:event1) { DecisionReviewCreatedEvent.create!(reference_id: "1") } + let!(:intake) { Intake.create!(veteran_file_number: veteran_file_number, user: user) } + let!(:intake_event_record) { EventRecord.create!(event: event1, evented_record: intake) } + it "Event Record backfill ID and type should match Intake ID and type" do + expect(intake_event_record.evented_record_type).to eq("Intake") + expect(intake_event_record.evented_record_id).to eq(intake.id) + expect(intake.event_record).to eq intake_event_record + expect(intake.from_decision_review_created_event?).to eq(true) + end + end + + context "One Event with 10 Different Event Records to simulate a VBMS backfill" do + let(:user) { Generators::User.build } + let(:veteran_file_number) { "64205050" } + let!(:event2) { DecisionReviewCreatedEvent.create!(reference_id: "2") } + # Intake + let!(:intake) { Intake.create!(veteran_file_number: veteran_file_number, user: user) } + let!(:intake_event_record) { EventRecord.create!(event: event2, evented_record: intake) } + # HLR + let!(:higher_level_review) { HigherLevelReview.new(veteran_file_number: veteran_file_number) } + let!(:higher_level_review_event_record) do + EventRecord.create!(event: event2, evented_record: higher_level_review) + end + # SC, not tied to Event + let!(:supplemental_claim) { SupplementalClaim.new(veteran_file_number: veteran_file_number) } + + # End Product Establishment + let!(:end_product_establishment) do + EndProductEstablishment.new( + payee_code: "00", + source: higher_level_review, + veteran_file_number: veteran_file_number + ) + end + let!(:end_product_establishment_event_record) do + EventRecord.create!(event: event2, evented_record: end_product_establishment) + end + # Claimant + let!(:appeal) { create(:appeal, receipt_date: 1.year.ago) } + let!(:claimant) { create(:claimant, decision_review: appeal) } + let!(:claimant_event_record) { EventRecord.create!(event: event2, evented_record: claimant) } + # Veteran + let!(:veteran) { Veteran.new(file_number: veteran_file_number) } + let!(:veteran_event_record) { EventRecord.create!(event: event2, evented_record: veteran) } + # Person + let!(:person) { create(:person, participant_id: "1129318238") } + let!(:person_event_record) { EventRecord.create!(event: event2, evented_record: person) } + # Request Issue + let!(:request_issue) { RequestIssue.new(benefit_type: "compensation", decision_review: higher_level_review) } + let!(:request_issue_event_record) { EventRecord.create!(event: event2, evented_record: request_issue) } + # Legacy Issue + let!(:legacy_issue) { LegacyIssue.new(request_issue_id: request_issue.id, vacols_id: "vacols111", vacols_sequence_id: 1) } + let!(:legacy_issue_event_record) { EventRecord.create!(event: event2, evented_record: legacy_issue) } + # Legacy Issue Optin + let!(:legacy_issue_optin) { LegacyIssueOptin.new(request_issue_id: request_issue.id) } + let!(:legacy_issue_optin_event_record) do + EventRecord.create!(event: event2, evented_record: legacy_issue_optin) + end + # User + let(:session) { { "user" => { "id" => "BrockPurdy", "station_id" => "310", "name" => "Brock Purdy" } } } + let(:user) { User.from_session(session) } + let!(:user_event_record) { EventRecord.create!(event: event2, evented_record: user) } + it "10 Event Records Backfilled ID and Type correctly match" do + expect(higher_level_review_event_record.evented_record_type).to eq("HigherLevelReview") + expect(higher_level_review_event_record.evented_record_id).to eq(higher_level_review.id) + expect(higher_level_review.event_record).to eq higher_level_review_event_record + + intake.update!(detail: higher_level_review) + expect(higher_level_review.from_decision_review_created_event?).to eq(true) + + expect(end_product_establishment_event_record.evented_record_type).to eq("EndProductEstablishment") + expect(end_product_establishment_event_record.evented_record_id).to eq(end_product_establishment.id) + expect(end_product_establishment.event_record).to eq end_product_establishment_event_record + expect(end_product_establishment.from_decision_review_created_event?).to eq(true) + + expect(claimant_event_record.evented_record_type).to eq("Claimant") + expect(claimant_event_record.evented_record_id).to eq(claimant.id) + expect(claimant.event_record).to eq claimant_event_record + expect(end_product_establishment.from_decision_review_created_event?).to eq(true) + + expect(veteran_event_record.evented_record_type).to eq("Veteran") + expect(veteran_event_record.evented_record_id).to eq(veteran.id) + expect(veteran.event_record).to eq veteran_event_record + expect(veteran.from_decision_review_created_event?).to eq(true) + + expect(person_event_record.evented_record_type).to eq("Person") + expect(person_event_record.evented_record_id).to eq(person.id) + expect(person.event_record).to eq person_event_record + expect(person.from_decision_review_created_event?).to eq(true) + + expect(request_issue_event_record.evented_record_type).to eq("RequestIssue") + expect(request_issue_event_record.evented_record_id).to eq(request_issue.id) + expect(request_issue.event_record).to eq request_issue_event_record + expect(request_issue.from_decision_review_created_event?).to eq(true) + + expect(legacy_issue_event_record.evented_record_type).to eq("LegacyIssue") + expect(legacy_issue_event_record.evented_record_id).to eq(legacy_issue.id) + expect(legacy_issue.event_record).to eq legacy_issue_event_record + expect(legacy_issue.from_decision_review_created_event?).to eq(true) + + expect(legacy_issue_optin_event_record.evented_record_type).to eq("LegacyIssueOptin") + expect(legacy_issue_optin_event_record.evented_record_id).to eq(legacy_issue_optin.id) + expect(legacy_issue_optin.event_record).to eq legacy_issue_optin_event_record + expect(legacy_issue_optin.from_decision_review_created_event?).to eq(true) + + expect(user_event_record.evented_record_type).to eq("User") + expect(user_event_record.evented_record_id).to eq(user.id) + expect(user.event_record).to eq user_event_record + expect(user.from_decision_review_created_event?).to eq(true) + + expect(EventRecord.count).to eq 10 + end + + it "SupplementalClaim not associated to a backfill Intake should fail #from_decision_review_created_event?" do + expect(supplemental_claim.from_decision_review_created_event?).to eq(false) + end + end + + # create an failing Event Record association + context "EventRecord does not have a bi-directional association with non related models" do + let!(:attorney) { create(:bgs_attorney, name: "Brock Purdy") } + let!(:event3) { DecisionReviewCreatedEvent.create!(reference_id: "3") } + it "should not create an EventRecord and should raise an error" do + expect { EventRecord.create!(event_id: event3.id, evented_record: attorney) }.to raise_error(ActiveRecord::RecordInvalid) + expect { attorney.event_record }.to raise_error(NoMethodError) + end + end +end diff --git a/spec/models/events/event_spec.rb b/spec/models/events/event_spec.rb new file mode 100644 index 00000000000..2fc7dbf6b5e --- /dev/null +++ b/spec/models/events/event_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe Event, type: :model do + describe "attributes" do + it { expect(described_class.new.info).to be_an_instance_of(Hash) } + + it "allows nil value for errored_claim_id" do + event = described_class.new(errored_claim_id: nil) + expect(event.errored_claim_id).to be_nil + end + end + + describe "scopes" do + describe ".with_errored_claim_id" do + it "includes events with non-null errored_claim_id" do + event_with_errored_claim = create(:decision_review_created_event, info: { "errored_claim_id" => "12345" }) + event_without_errored_claim = create(:decision_review_created_event, info: { "created" => "Yay!" }) + + events = described_class.with_errored_claim_id + + expect(events).to include(event_with_errored_claim) + expect(events).not_to include(event_without_errored_claim) + end + end + end +end diff --git a/spec/requests/api/v3/issues/ama/veterans_controller_spec.rb b/spec/requests/api/v3/issues/ama/veterans_controller_spec.rb index 2d95a1dbe40..c1c2a0cc26d 100644 --- a/spec/requests/api/v3/issues/ama/veterans_controller_spec.rb +++ b/spec/requests/api/v3/issues/ama/veterans_controller_spec.rb @@ -30,7 +30,11 @@ end context "when feature is enabled" do - before { FeatureToggle.enable!(:api_v3_ama_issues) } + before do + FeatureToggle.enable!(:api_v3_ama_issues) + allow(Rails.logger).to receive(:info) + expect(Rails.logger).to receive(:info) + end after { FeatureToggle.disable!(:api_v3_ama_issues) } context "when the api key is missing in the header" do @@ -86,11 +90,6 @@ end context "when there is no error" do - before do - allow(Rails.logger).to receive(:info) - expect(Rails.logger).to receive(:info).with(/FINISHED Retrieving AMA Request Issues for Veteran:/) - end - context "when a veteran is found - but has no request issues" do let(:vet) { create(:veteran) } it "should return empty request issues array for veteran" do @@ -104,6 +103,27 @@ end end + context "when a veteran is found - has request issues" do + let(:vet) { create(:veteran) } + let!(:request_issues) do + [ + create(:request_issue, :with_associated_decision_issue, veteran_participant_id: vet.participant_id), + create(:request_issue, :with_associated_decision_issue, decision_date: Time.zone.now, veteran_participant_id: vet.participant_id) + ] + end + + it "should return request issues array" do + get( + "/api/v3/issues/ama/find_by_veteran/#{vet.participant_id}", + headers: authorization_header + ) + response_hash = JSON.parse(response.body) + expect(response).to have_http_status(200) + expect(response_hash["request_issues"].empty?).to eq false + expect(response_hash["request_issues"][0]["claim_errors"]).to eq [] + end + end + context "when a veteran has a legacy appeal" do context "when a veteran has multiple request issues with multiple decision issues" do let_it_be(:vet) { create(:veteran, file_number: "123456789") } diff --git a/spec/serializers/api/v3/issues/ama/request_issue_serializer_spec.rb b/spec/serializers/api/v3/issues/ama/request_issue_serializer_spec.rb index fa5ca4cbef3..5ba41896bb9 100644 --- a/spec/serializers/api/v3/issues/ama/request_issue_serializer_spec.rb +++ b/spec/serializers/api/v3/issues/ama/request_issue_serializer_spec.rb @@ -52,6 +52,7 @@ expect(serialized_request_issue.key?(:claimant_participant_id)).to eq true expect(serialized_request_issue.key?(:decision_issues)).to eq true expect(serialized_request_issue.key?(:claim_id)).to eq true + expect(serialized_request_issue.key?(:claim_errors)).to eq true serialized_decision_issue = serialized_request_issue[:decision_issues].first expect(serialized_decision_issue.key?(:id)).to eq true diff --git a/spec/services/events/create_claimant_on_event_spec.rb b/spec/services/events/create_claimant_on_event_spec.rb new file mode 100644 index 00000000000..4300d636723 --- /dev/null +++ b/spec/services/events/create_claimant_on_event_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +# rubocop:disable Layout/LineLength + +RSpec.describe Events::CreateClaimantOnEvent do + let!(:event) { DecisionReviewCreatedEvent.create!(reference_id: "1") } + let(:decision_review) { create(:higher_level_review, veteran_file_number: create(:veteran).file_number) } + let(:parser) do + instance_double("ParserDouble", + person_first_name: "Sam", + person_last_name: "Jackson", + person_middle_name: "L", + person_ssn: "543627321", + person_date_of_birth: DateTime.now - 30.years, + person_email_address: "samjackson@pulpfiction.com", + claimant_name_suffix: "", + claim_review_veteran_is_not_claimant: true, + claimant_participant_id: "7479234", + claimant_type: "Claimant", + claimant_payee_code: "0002") + end + + describe ".process" do + context "when veteran is not the claimant" do + it "creates a new claimant and returns its id" do + expect(Person.find_by(participant_id: parser.claimant_participant_id)).to eq(nil) + expect { + described_class.process!(event: event, parser: parser, decision_review: decision_review) + }.to change { EventRecord.count }.by(2).and change { Claimant.count }.by(1) + + expect(described_class.process!(event: event, parser: parser, decision_review: decision_review)).to eq(Claimant.last) + expect(Person.find_by(participant_id: parser.claimant_participant_id)).to be_present + end + + it "does not create a new claimant if veteran is the claimant" do + allow(parser).to receive(:claim_review_veteran_is_not_claimant).and_return(false) + + expect(Claimant).not_to receive(:find_or_create_by!) + + expect(EventRecord).not_to receive(:create!) + + expect(described_class.process!(event: event, parser: parser, decision_review: decision_review)).to be_nil + + expect(Person.find_by(participant_id: parser.claimant_participant_id)).to eq(nil) + end + end + end +end + +# rubocop:enable Layout/LineLength diff --git a/spec/services/events/create_user_on_event_spec.rb b/spec/services/events/create_user_on_event_spec.rb new file mode 100644 index 00000000000..22557146867 --- /dev/null +++ b/spec/services/events/create_user_on_event_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +describe Events::CreateUserOnEvent do + let!(:css_id) { "NEWUSER" } + let!(:old_user) { create(:user, css_id: "OLDUSER") } + let!(:station_id) { "101" } + let!(:event) { DecisionReviewCreatedEvent.create!(reference_id: "1") } + + describe "#handle_user_creation_on_event" do + subject { described_class.handle_user_creation_on_event(event: event, css_id: css_id, station_id: station_id) } + + context "When an Event is received and no User exists" do + it "should create an Inactive User and Event Record" do + subject + user2 = User.find_by_css_id(css_id) + user_event_record = EventRecord.find_by(event_id: event.id) + expect(User.count).to eq(2) + expect(user2.status).to eq(Constants.USER_STATUSES.inactive) + expect(user2.event_record).to eq(user_event_record) + expect(EventRecord.count).to eq(1) + expect(EventRecord.first).to eq(user_event_record) + expect(user_event_record.evented_record).to eq(user2) + end + end + end +end diff --git a/spec/services/events/create_veteran_on_event_spec.rb b/spec/services/events/create_veteran_on_event_spec.rb new file mode 100644 index 00000000000..f603a05f19e --- /dev/null +++ b/spec/services/events/create_veteran_on_event_spec.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +# rubocop:disable Layout/LineLength + +describe Events::CreateVeteranOnEvent do + let!(:veteran) { create(:veteran) } + let!(:non_cf_veteran) { double("Veteran", file_number: "12345678X", participant_id: "1826209", bgs_last_synced_at: 1_708_533_584_000, name_suffix: nil, date_of_death: nil) } + let!(:event) { DecisionReviewCreatedEvent.create!(reference_id: "1") } + let(:parser) { Events::DecisionReviewCreated::DecisionReviewCreatedParser.load_example } + + describe "#veteran_exist?" do + subject { described_class } + + context "when there is no existing Veteran" do + it "should return false" do + expect(subject.veteran_exist?("111111111")).to be_falsey + end + end + + context "when a Veteran already exists" do + it "should return true" do + expect(subject.veteran_exist?(veteran.file_number)).to be_truthy + end + end + end + + describe "#handle_veteran_creation_on_event" do + subject { described_class } + + context "when creating a new Veteran" do + it "should create successfully without calling BGS and also create an EventRecord" do + headers = retrieve_headers + + backfilled_veteran = subject.handle_veteran_creation_on_event(event: event, parser: parser) + + expect(backfilled_veteran.ssn).to eq headers["X-VA-Vet-SSN"] + expect(backfilled_veteran.file_number).to eq headers["X-VA-File-Number"] + expect(backfilled_veteran.first_name).to eq headers["X-VA-Vet-First-Name"] + expect(backfilled_veteran.last_name).to eq headers["X-VA-Vet-Last-Name"] + expect(backfilled_veteran.middle_name).to eq headers["X-VA-Vet-Middle-Name"] + + expect(backfilled_veteran.participant_id).to eq non_cf_veteran.participant_id + expect(backfilled_veteran.bgs_last_synced_at).to eq parser.convert_milliseconds_to_datetime(non_cf_veteran.bgs_last_synced_at) + expect(backfilled_veteran.name_suffix).to eq nil + expect(backfilled_veteran.date_of_death).to eq nil + + expect(EventRecord.count).to eq 1 + event_record = EventRecord.first + + expect(event_record.evented_record).to eq(backfilled_veteran) + end + end + + def retrieve_headers + { + "X-VA-Vet-SSN" => "123456789", + "X-VA-File-Number" => "77799777", + "X-VA-Vet-First-Name" => "John", + "X-VA-Vet-Last-Name" => "Smith", + "X-VA-Vet-Middle-Name" => "Alexander" + } + end + end +end + +# rubocop:enable Layout/LineLength diff --git a/spec/services/events/decision_review_created/create_claim_review_spec.rb b/spec/services/events/decision_review_created/create_claim_review_spec.rb new file mode 100644 index 00000000000..6eea087d289 --- /dev/null +++ b/spec/services/events/decision_review_created/create_claim_review_spec.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +RSpec.describe Events::DecisionReviewCreated::CreateClaimReview do + let!(:event) { DecisionReviewCreatedEvent.create!(reference_id: "1") } + let(:parser) do + instance_double("ParserDouble", + claim_review_benefit_type: "benefit type", + detail_type: "HigherLevelReview", + claim_review_filed_by_va_gov: true, + claim_review_legacy_opt_in_approved: true, + claim_review_receipt_date: DateTime.now.to_s, + claim_review_veteran_is_not_claimant: false, + claim_review_establishment_attempted_at: nil, + claim_review_establishment_last_submitted_at: nil, + claim_review_establishment_processed_at: nil, + claim_review_establishment_submitted_at: nil, + veteran_file_number: "7479234", + claim_review_informal_conference: nil, + claim_review_same_office: nil) + end + + describe ".process" do + context "when intake is not HigherLevelReview" do + it "creates a new supplemental claim" do + allow(parser).to receive(:detail_type).and_return("NotHighLevelReview") + + expect do + described_class.process!(event: event, parser: parser) + end.to change { EventRecord.count }.by(1).and change { SupplementalClaim.count }.by(1) + + expect(described_class.process!(event: event, parser: parser)).to eq(SupplementalClaim.last) + + claim_review = SupplementalClaim.last + expect(claim_review.benefit_type).to eq(parser.claim_review_benefit_type) + expect(claim_review.filed_by_va_gov).to eq(parser.claim_review_filed_by_va_gov) + expect(claim_review.legacy_opt_in_approved).to eq(parser.claim_review_legacy_opt_in_approved) + expect(claim_review.veteran_is_not_claimant).to eq(parser.claim_review_veteran_is_not_claimant) + expect(claim_review.establishment_attempted_at).to eq(parser.claim_review_establishment_attempted_at) + expect(claim_review.establishment_last_submitted_at).to eq(parser.claim_review_establishment_last_submitted_at) + expect(claim_review.establishment_processed_at).to eq(parser.claim_review_establishment_processed_at) + expect(claim_review.establishment_submitted_at).to eq(parser.claim_review_establishment_submitted_at) + expect(claim_review.veteran_file_number).to eq(parser.veteran_file_number) + end + end + + context "when intake is a HigherLevelReview" do + it "creates a new supplemental claim" do + expect do + described_class.process!(event: event, parser: parser) + end.to change { EventRecord.count }.by(1).and change { HigherLevelReview.count }.by(1) + + expect(described_class.process!(event: event, parser: parser)).to eq(HigherLevelReview.last) + + claim_review = HigherLevelReview.last + expect(claim_review.benefit_type).to eq(parser.claim_review_benefit_type) + expect(claim_review.filed_by_va_gov).to eq(parser.claim_review_filed_by_va_gov) + expect(claim_review.legacy_opt_in_approved).to eq(parser.claim_review_legacy_opt_in_approved) + expect(claim_review.veteran_is_not_claimant).to eq(parser.claim_review_veteran_is_not_claimant) + expect(claim_review.establishment_attempted_at).to eq(parser.claim_review_establishment_attempted_at) + expect(claim_review.establishment_last_submitted_at).to eq(parser.claim_review_establishment_last_submitted_at) + expect(claim_review.establishment_processed_at).to eq(parser.claim_review_establishment_processed_at) + expect(claim_review.establishment_submitted_at).to eq(parser.claim_review_establishment_submitted_at) + expect(claim_review.veteran_file_number).to eq(parser.veteran_file_number) + end + end + + context "when an error occurs" do + it "raises DecisionReviewCreatedCreateClaimReviewError" do + allow(HigherLevelReview).to receive(:create) + .and_raise(Caseflow::Error::DecisionReviewCreatedCreateClaimReviewError, "Error message") + + expect do + described_class.process!(event: event, parser: parser) + end.to raise_error(Caseflow::Error::DecisionReviewCreatedCreateClaimReviewError, "Error message") + end + end + end +end diff --git a/spec/services/events/decision_review_created/create_ep_establishment_spec.rb b/spec/services/events/decision_review_created/create_ep_establishment_spec.rb new file mode 100644 index 00000000000..5eb7973019b --- /dev/null +++ b/spec/services/events/decision_review_created/create_ep_establishment_spec.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +# rubocop:disable Layout/LineLength + +describe Events::DecisionReviewCreated::CreateEpEstablishment do + context "Events::DecisionReviewCreated::CreateEpEstablishment.process!" do + # set up variables station_id, end_product_establishment, claim_review, user, event + let!(:user_double) { double("User", id: 1) } + let!(:event_double) { double("Event") } + let!(:claim_review) { create(:higher_level_review) } + # conversions to mimic parser logic + let!(:converted_long) { Time.zone.at(171_046_496_764_2) } + let!(:converted_claim_date) { logical_date_converter(202_403_14) } + let!(:parser_double) do + double("ParserDouble", + station_id: "101", + epe_payee_code: "00", + epe_claim_date: converted_claim_date, + epe_code: "030HLRRPMC", + epe_committed_at: converted_long, + epe_established_at: converted_long, + epe_last_synced_at: converted_long, + epe_limited_poa_access: nil, + epe_limited_poa_code: nil, + epe_modifier: "030", + epe_reference_id: "337534", + epe_synced_status: "RW", + epe_benefit_type_code: "1", + epe_development_item_reference_id: nil, + claimant_participant_id: "1826209") + end + let(:event_record_double) { double("EventRecord") } + it "creates an a End Product Establishment and Event Record" do + allow(EndProductEstablishment).to receive(:create!).and_return(parser_double) + allow(EventRecord).to receive(:create!).and_return(event_record_double) + expect(EndProductEstablishment).to receive(:create!).with( + payee_code: "00", + source: claim_review, + veteran_file_number: claim_review.veteran_file_number, + benefit_type_code: "1", + claim_date: converted_claim_date, + code: "030HLRRPMC", + committed_at: converted_long, + established_at: converted_long, + last_synced_at: converted_long, + limited_poa_access: nil, + limited_poa_code: nil, + modifier: "030", + reference_id: "337534", + station: "101", + synced_status: "RW", + user_id: 1, + development_item_reference_id: nil, + claimant_participant_id: "1826209" + ).and_return(parser_double) + expect(EventRecord).to receive(:create!) + .with(event: event_double, evented_record: parser_double).and_return(event_record_double) + described_class.process!(parser: parser_double, claim_review: claim_review, user: user_double, event: event_double) + end + + # needed to convert the logical date int for the expect block + def logical_date_converter(logical_date_int) + # Extract year, month, and day components + year = logical_date_int / 100_00 + month = (logical_date_int % 100_00) / 100 + day = logical_date_int % 100 + date = Date.new(year, month, day) + date + end + + context "when an error occurs" do + it "raises the error" do + allow(EndProductEstablishment).to receive(:create!).and_raise(Caseflow::Error::DecisionReviewCreatedEpEstablishmentError) + expect do + described_class.process!(parser: parser_double, + claim_review: claim_review, user: user_double, event: event_double) + end.to raise_error(Caseflow::Error::DecisionReviewCreatedEpEstablishmentError) + end + end + end +end + +# rubocop:enable Layout/LineLength diff --git a/spec/services/events/decision_review_created/create_intake_spec.rb b/spec/services/events/decision_review_created/create_intake_spec.rb new file mode 100644 index 00000000000..1e250a9e23d --- /dev/null +++ b/spec/services/events/decision_review_created/create_intake_spec.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +# rubocop:disable Layout/LineLength + +describe Events::DecisionReviewCreated::CreateIntake do + context "Events::DecisionReviewCreated::CreateIntake.process!" do + let(:event_double) { double("Event") } + let(:user_double) { double("User") } + let(:veteran_double) { double("Veteran", file_number: "DCR02272024", id: "2000932150") } + let(:parser) { Events::DecisionReviewCreated::DecisionReviewCreatedParser.load_example } + let(:intake_double) { double("Intake") } + let(:event_record_double) { double("EventRecord") } + let(:decision_review_double) { double("DecisionReview", id: "1") } + + it "creates an intake and event record" do + allow(Intake).to receive(:create!).and_return(intake_double) + allow(EventRecord).to receive(:create!).and_return(event_record_double) + expect(Intake).to receive(:create!).with(veteran_file_number: "DCR02272024", + user: user_double, + started_at: parser.intake_started_at, + completion_started_at: parser.intake_completion_started_at, + completed_at: parser.intake_completed_at, + completion_status: parser.intake_completion_status, + type: parser.intake_type, + detail_type: parser.intake_detail_type, + detail_id: decision_review_double.id, + veteran: veteran_double) + .and_return(intake_double) + expect(EventRecord).to receive(:create!).with(event: event_double, evented_record: intake_double) + .and_return(event_record_double) + described_class.process!(event: event_double, user: user_double, veteran: veteran_double, parser: parser, + decision_review: decision_review_double) + end + context "when an error occurs" do + it "raises the error" do + allow(Intake).to receive(:create!).and_raise(Caseflow::Error::DecisionReviewCreatedIntakeError) + expect { described_class.process!(event: event_double, user: user_double, veteran: veteran_double, parser: parser, + decision_review: decision_review_double) }.to raise_error(Caseflow::Error::DecisionReviewCreatedIntakeError) + end + end + end +end + +# rubocop:enable Layout/LineLength diff --git a/spec/services/events/decision_review_created/create_request_issues_spec.rb b/spec/services/events/decision_review_created/create_request_issues_spec.rb new file mode 100644 index 00000000000..0c0bb2c4a03 --- /dev/null +++ b/spec/services/events/decision_review_created/create_request_issues_spec.rb @@ -0,0 +1,112 @@ +# frozen_string_literal: true + +require "json" + +describe Events::DecisionReviewCreated::CreateRequestIssues do + let!(:event) { DecisionReviewCreatedEvent.create!(reference_id: "1") } + let!(:epe) { create(:end_product_establishment) } + let!(:higher_level_review) { create(:higher_level_review) } + + describe "#process!" do + subject { described_class } + + context "when receiving an Event with request_issues" do + it "should create CF RequestIssues and backfill records" do + parser = Events::DecisionReviewCreated::DecisionReviewCreatedParser.new({}, retrieve_payload) + + backfilled_issues = subject.process!(event: event, parser: parser, epe: epe, decision_review: higher_level_review) + + expect(backfilled_issues.count).to eq(2) + expect(RequestIssue.count).to eq(2) + expect(EventRecord.count).to eq(2) + expect(backfilled_issues.first.event_record).to eq(EventRecord.first) + expect(backfilled_issues.last.event_record).to eq(EventRecord.last) + + # check if attributes match + ri1 = backfilled_issues.first + ri2 = backfilled_issues.last + + expect(ri1.benefit_type).to eq("pension") + expect(ri1.contested_issue_description).to eq("service connection for arthritis denied") + expect(ri1.contention_reference_id).to eq(4_542_785) + expect(ri1.nonrating_issue_category).to eq("DIC") + expect(ri1.decision_date).to eq(parser.logical_date_converter(20_240_314)) + expect(ri1.nonrating_issue_bgs_id).to eq("12") + expect(ri1.end_product_establishment_id).to eq(epe.id) + expect(ri1.decision_review_id).to eq(higher_level_review.id) + expect(ri1.decision_review_type).to eq("HigherLevelReview") + expect(ri1.veteran_participant_id).to eq(parser.veteran_participant_id) + + expect(ri2.benefit_type).to eq("pension") + expect(ri2.contested_issue_description).to eq("PTSD") + expect(ri2.contention_reference_id).to eq(123_456) + expect(ri2.nonrating_issue_category).to eq(nil) + expect(ri2.decision_date).to eq(parser.logical_date_converter(20_240_314)) + expect(ri2.nonrating_issue_bgs_id).to eq(nil) + expect(ri2.end_product_establishment_id).to eq(epe.id) + expect(ri2.decision_review_id).to eq(higher_level_review.id) + expect(ri2.decision_review_type).to eq("HigherLevelReview") + expect(ri2.veteran_participant_id).to eq(parser.veteran_participant_id) + end + end + + def retrieve_payload + { + "request_issues": [ + { + "benefit_type": "pension", + "contested_issue_description": "service connection for arthritis denied", + "contention_reference_id": 4_542_785, + "contested_rating_decision_reference_id": nil, + "contested_rating_issue_profile_date": nil, + "contested_rating_issue_reference_id": nil, + "contested_decision_issue_id": nil, + "decision_date": 20_240_314, + "ineligible_due_to_id": nil, + "ineligible_reason": nil, + "is_unidentified": true, + "unidentified_issue_text": nil, + "nonrating_issue_category": "DIC", + "nonrating_issue_description": nil, + "untimely_exemption": nil, + "untimely_exemption_notes": nil, + "vacols_id": nil, + "vacols_sequence_id": nil, + "closed_at": nil, + "closed_status": nil, + "contested_rating_issue_diagnostic_code": nil, + "ramp_claim_id": nil, + "rating_issue_associated_at": nil, + "nonrating_issue_bgs_id": "12" + }, + { + "benefit_type": "pension", + "contested_issue_description": "PTSD", + "contention_reference_id": 123_456, + "contested_rating_decision_reference_id": "12", + "contested_rating_issue_profile_date": nil, + "contested_rating_issue_reference_id": nil, + "contested_decision_issue_id": nil, + "decision_date": 20_240_314, + "ineligible_due_to_id": nil, + "ineligible_reason": nil, + "is_unidentified": false, + "unidentified_issue_text": nil, + "nonrating_issue_category": nil, + "nonrating_issue_description": nil, + "untimely_exemption": false, + "untimely_exemption_notes": nil, + "vacols_id": nil, + "vacols_sequence_id": nil, + "closed_at": nil, + "closed_status": nil, + "contested_rating_issue_diagnostic_code": nil, + "ramp_claim_id": nil, + "rating_issue_associated_at": nil, + "nonrating_issue_bgs_id": nil + } + ] + } + end + end +end diff --git a/spec/services/events/decision_review_created/decision_review_created_parser_spec.rb b/spec/services/events/decision_review_created/decision_review_created_parser_spec.rb new file mode 100644 index 00000000000..d040d2651d7 --- /dev/null +++ b/spec/services/events/decision_review_created/decision_review_created_parser_spec.rb @@ -0,0 +1,149 @@ +# frozen_string_literal: true + +# rubocop:disable Layout/LineLength + +require "ostruct" + +describe Events::DecisionReviewCreated::DecisionReviewCreatedParser do + context "Events::DecisionReviewCreated::DecisionReviewCreatedParser.load_example" do + let!(:json_payload) { read_json_payload } + let!(:response_hash) { OpenStruct.new(json_payload) } + let!(:headers) { sample_headers } + # mimic when we recieve an example_response + parser = described_class.load_example + it "has css_id, detail_type and station_id" do + expect(parser.css_id).to eq(response_hash.css_id) + expect(parser.detail_type).to eq(response_hash.detail_type) + expect(parser.station_id).to eq(response_hash.station) + end + it "has Intake attributes" do + expect(parser.intake_started_at).to eq parser.convert_milliseconds_to_datetime( + response_hash.intake["started_at"] + ) + expect(parser.intake_completion_started_at).to eq parser.convert_milliseconds_to_datetime( + response_hash.intake["completion_started_at"] + ) + expect(parser.intake_completed_at).to eq parser.convert_milliseconds_to_datetime( + response_hash.intake["completed_at"] + ) + expect(parser.intake_completion_status).to eq response_hash.intake["completion_status"] + expect(parser.intake_type).to eq response_hash.intake["type"] + expect(parser.intake_detail_type).to eq response_hash.intake["detail_type"] + end + it "has Veteran attributes" do + expect(parser.veteran_file_number).to eq(headers["X-VA-File-Number"]) + expect(parser.veteran_ssn).to eq(headers["X-VA-Vet-SSN"]) + expect(parser.veteran_first_name).to eq(headers["X-VA-Vet-First-Name"]) + expect(parser.veteran_last_name).to eq(headers["X-VA-Vet-Last-Name"]) + expect(parser.veteran_middle_name).to eq(headers["X-VA-Vet-Middle-Name"]) + expect(parser.veteran_participant_id).to eq(response_hash.veteran["participant_id"]) + expect(parser.veteran_bgs_last_synced_at).to eq parser.convert_milliseconds_to_datetime( + response_hash.veteran["bgs_last_synced_at"] + ) + expect(parser.veteran_name_suffix).to eq(response_hash.veteran["name_suffix"]) + expect(parser.veteran_date_of_death).to eq(response_hash.veteran["date_of_death"]) + end + it "has Claimant attributes" do + expect(parser.claimant_payee_code).to eq response_hash.claimant["payee_code"] + expect(parser.claimant_type).to eq response_hash.claimant["type"] + expect(parser.claimant_participant_id).to eq response_hash.claimant["participant_id"] + expect(parser.claimant_name_suffix).to eq response_hash.claimant["name_suffix"] + end + it "has Claim Review attributes" do + expect(parser.claim_review_benefit_type).to eq response_hash.claim_review["benefit_type"] + expect(parser.claim_review_filed_by_va_gov).to eq response_hash.claim_review["filed_by_va_gov"] + expect(parser.claim_review_legacy_opt_in_approved).to eq response_hash.claim_review["legacy_opt_in_approved"] + expect(parser.claim_review_receipt_date).to eq parser.logical_date_converter( + response_hash.claim_review["receipt_date"] + ) + expect(parser.claim_review_veteran_is_not_claimant).to eq response_hash.claim_review["veteran_is_not_claimant"] + expect(parser.claim_review_establishment_attempted_at).to eq parser.convert_milliseconds_to_datetime( + response_hash.claim_review["establishment_attempted_at"] + ) + expect(parser.claim_review_establishment_last_submitted_at).to eq parser.convert_milliseconds_to_datetime( + response_hash.claim_review["establishment_last_submitted_at"] + ) + expect(parser.claim_review_establishment_processed_at).to eq parser.convert_milliseconds_to_datetime( + response_hash.claim_review["establishment_processed_at"] + ) + expect(parser.claim_review_establishment_submitted_at).to eq parser.convert_milliseconds_to_datetime( + response_hash.claim_review["establishment_submitted_at"] + ) + expect(parser.claim_review_informal_conference).to eq response_hash.claim_review["informal_conference"] + expect(parser.claim_review_same_office).to eq response_hash.claim_review["same_office"] + end + it "has End Product Establishment attributes" do + expect(parser.epe_benefit_type_code).to eq response_hash.end_product_establishment["benefit_type_code"] + expect(parser.epe_claim_date).to eq parser.logical_date_converter( + response_hash.end_product_establishment["claim_date"] + ) + expect(parser.epe_code).to eq response_hash.end_product_establishment["code"] + expect(parser.epe_modifier).to eq response_hash.end_product_establishment["modifier"] + expect(parser.epe_payee_code).to eq response_hash.end_product_establishment["payee_code"] + expect(parser.epe_reference_id).to eq response_hash.end_product_establishment["reference_id"] + expect(parser.epe_limited_poa_access).to eq response_hash.end_product_establishment["limited_poa_access"] + expect(parser.epe_limited_poa_code).to eq response_hash.end_product_establishment["limited_poa_code"] + expect(parser.epe_committed_at).to eq parser.convert_milliseconds_to_datetime( + response_hash.end_product_establishment["committed_at"] + ) + expect(parser.epe_established_at).to eq parser.convert_milliseconds_to_datetime( + response_hash.end_product_establishment["established_at"] + ) + expect(parser.epe_last_synced_at).to eq parser.convert_milliseconds_to_datetime( + response_hash.end_product_establishment["last_synced_at"] + ) + expect(parser.epe_synced_status).to eq response_hash.end_product_establishment["synced_status"] + expect(parser.epe_development_item_reference_id).to eq( + response_hash.end_product_establishment["development_item_reference_id"] + ) + end + it "has Request Issue attributes" do + total_issues = parser.request_issues + expect(total_issues.count).to eq(1) + issue = total_issues.first + expect(parser.ri_benefit_type(issue)).to eq response_hash.request_issues.first["benefit_type"] + expect(parser.ri_contested_issue_description(issue)).to eq response_hash.request_issues.first["contested_issue_description"] + expect(parser.ri_contention_reference_id(issue)).to eq response_hash.request_issues.first["contention_reference_id"] + expect(parser.ri_contested_rating_decision_reference_id(issue)).to eq response_hash.request_issues.first["contested_rating_decision_reference_id"] + expect(parser.ri_contested_rating_issue_profile_date(issue)).to eq response_hash.request_issues.first["contested_rating_issue_profile_date"] + expect(parser.ri_contested_rating_issue_reference_id(issue)).to eq response_hash.request_issues.first["contested_rating_issue_reference_id"] + expect(parser.ri_contested_decision_issue_id(issue)).to eq response_hash.request_issues.first["contested_decision_issue_id"] + expect(parser.ri_decision_date(issue)).to eq parser.logical_date_converter(response_hash.request_issues.first["decision_date"]) + expect(parser.ri_ineligible_due_to_id(issue)).to eq response_hash.request_issues.first["ineligible_due_to_id"] + expect(parser.ri_ineligible_reason(issue)).to eq response_hash.request_issues.first["ineligible_reason"] + expect(parser.ri_is_unidentified(issue)).to eq response_hash.request_issues.first["is_unidentified"] + expect(parser.ri_unidentified_issue_text(issue)).to eq response_hash.request_issues.first["unidentified_issue_text"] + expect(parser.ri_nonrating_issue_category(issue)).to eq response_hash.request_issues.first["nonrating_issue_category"] + expect(parser.ri_nonrating_issue_description(issue)).to eq response_hash.request_issues.first["nonrating_issue_description"] + expect(parser.ri_untimely_exemption(issue)).to eq response_hash.request_issues.first["untimely_exemption"] + expect(parser.ri_untimely_exemption_notes(issue)).to eq response_hash.request_issues.first["untimely_exemption_notes"] + expect(parser.ri_vacols_id(issue)).to eq response_hash.request_issues.first["vacols_id"] + expect(parser.ri_vacols_sequence_id(issue)).to eq response_hash.request_issues.first["vacols_sequence_id"] + expect(parser.ri_closed_at(issue)).to eq response_hash.request_issues.first["closed_at"] + expect(parser.ri_closed_status(issue)).to eq response_hash.request_issues.first["closed_status"] + expect(parser.ri_contested_rating_issue_diagnostic_code(issue)).to eq response_hash.request_issues.first["contested_rating_issue_diagnostic_code"] + expect(parser.ri_ramp_claim_id(issue)).to eq response_hash.request_issues.first["ramp_claim_id"] + expect(parser.ri_rating_issue_associated_at(issue)).to eq response_hash.request_issues.first["rating_issue_associated_at"] + expect(parser.ri_nonrating_issue_bgs_id(issue)).to eq response_hash.request_issues.first["nonrating_issue_bgs_id"] + end + end + def read_json_payload + JSON.parse(File.read(Rails.root.join("app", + "services", + "events", + "decision_review_created", + "decision_review_created_example.json"))) + end + + def sample_headers + { + "X-VA-Vet-SSN" => "123456789", + "X-VA-File-Number" => "77799777", + "X-VA-Vet-First-Name" => "John", + "X-VA-Vet-Last-Name" => "Smith", + "X-VA-Vet-Middle-Name" => "Alexander" + } + end +end + +# rubocop:enable Layout/LineLength diff --git a/spec/services/events/decision_review_created/update_vacols_on_optin_spec.rb b/spec/services/events/decision_review_created/update_vacols_on_optin_spec.rb new file mode 100644 index 00000000000..57f6342786a --- /dev/null +++ b/spec/services/events/decision_review_created/update_vacols_on_optin_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +describe Events::DecisionReviewCreated::UpdateVacolsOnOptin do + context "Events::DecisionReviewCreated::UpdateVacolsOnOptin.process" do + # Setup a mock decision_review object with necessary properties + let!(:legacy_decision_review) { double("DecisionReview", legacy_opt_in_approved: true) } + describe "when legacy_opt_in_approved is true" do + it "calls process on LegacyOptinManager" do + # Setup a mock LegacyOptinManager and expect peform! to be called + legacy_optin_manager_double = double("LegacyOptinManager") + expect(LegacyOptinManager).to receive(:new) + .with(decision_review: legacy_decision_review) + .and_return(legacy_optin_manager_double) + expect(legacy_optin_manager_double).to receive(:process!) + # Call the method under test + described_class.process!(decision_review: legacy_decision_review) + end + end + describe "when legacy_opt_in_approved is false" do + it "does not call process! on " do + # Setup a mock decision_review object with necessary properties + decision_review_double = double("DecisionReview", legacy_opt_in_approved: false) + expect(described_class.process!(decision_review: decision_review_double)).to be_nil + end + end + describe "when an error occurs" do + it "logs an error and raises if an standard error occurs" do + allow(described_class).to receive(:process!) + .and_raise(Caseflow::Error::DecisionReviewCreateVacolsOnOptinError) + expect { described_class.process!(decision_review: legacy_decision_review) }.to raise_error( + Caseflow::Error::DecisionReviewCreateVacolsOnOptinError + ) + end + end + end +end diff --git a/spec/services/events/decision_review_created_error_spec.rb b/spec/services/events/decision_review_created_error_spec.rb new file mode 100644 index 00000000000..33323ade24f --- /dev/null +++ b/spec/services/events/decision_review_created_error_spec.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +describe Events::DecisionReviewCreatedError do + let!(:consumer_event_id) { "999" } + let!(:errored_claim_id) { "8888" } + let!(:error_message) { "The DecisionReviewCreation had an error" } + describe "#handle_service_error" do + subject { described_class.handle_service_error(consumer_event_id, errored_claim_id, error_message) } + + context "When Decision Review Creation Error is Saved in Caseflow" do + it "should create a new event with an updated Error" do + subject + new_event = Event.find_by(reference_id: "999") + expect(new_event.reference_id).to eq(consumer_event_id) + expect(new_event.error).to eq(error_message) + expect(new_event.info).to eq("errored_claim_id" => "8888") + expect(new_event.errored_claim_id).to eq(errored_claim_id) + end + end + context "when lock acquisition fails" do + before do + allow(RedisMutex).to receive(:with_lock).and_raise(RedisMutex::LockError) + end + it "logs the error message" do + expect(Rails.logger).to receive(:error) + .with("LockError occurred: RedisMutex::LockError") + subject + end + end + context "when standard error is raised" do + it "logs an error and raises if an standard error occurs" do + allow_any_instance_of(DecisionReviewCreatedEvent).to receive(:update!).and_raise(StandardError) + expect { subject }.to raise_error(StandardError) + end + end + context "when Redis key exists" do + let!(:redis_lock_failed) { Caseflow::Error::RedisLockFailed } + it "logs error message that Redis key exists" do + redis = Redis.new(url: Rails.application.secrets.redis_url_cache) + lock_key = "RedisMutex:EndProductEstablishment:#{errored_claim_id}" + redis.set(lock_key, "lock is set", nx: true, ex: 5.seconds) + expect { subject }.to raise_error(redis_lock_failed) + end + end + end +end diff --git a/spec/services/events/decision_review_created_spec.rb b/spec/services/events/decision_review_created_spec.rb new file mode 100644 index 00000000000..c5ff6cd68da --- /dev/null +++ b/spec/services/events/decision_review_created_spec.rb @@ -0,0 +1,111 @@ +# frozen_string_literal: true + +describe Events::DecisionReviewCreated do + let!(:consumer_event_id) { "123" } + let!(:event) { instance_double(Event) } + let!(:reference_id) { "2001" } + let(:event_created) { DecisionReviewCreatedEvent.create!(reference_id: consumer_event_id, completed_at: nil) } + let!(:completed_event) { DecisionReviewCreatedEvent.create!(reference_id: "999", completed_at: Time.zone.now) } + let!(:json_payload) { read_json_payload } + let!(:headers) { sample_headers } + let!(:parser) { Events::DecisionReviewCreated::DecisionReviewCreatedParser.load_example } + + describe "#event_exists_and_is_completed?" do + subject { described_class.event_exists_and_is_completed?(consumer_event_id) } + + context "When there is no previous Event" do + it "should return false" do + expect(subject).to be_falsey + end + end + + context "Where there is a previous Event that was completed" do + it "should return true" do + expect(Events::DecisionReviewCreated.event_exists_and_is_completed?("999")).to be_truthy + end + end + end + + describe "#create!" do + subject { described_class.create!(consumer_event_id, reference_id, headers, read_json_payload) } + + context "when lock acquisition fails" do + before do + allow(RedisMutex).to receive(:with_lock).and_raise(RedisMutex::LockError) + end + + it "logs the error message" do + expect(Rails.logger).to receive(:error) + .with("Failed to acquire lock for Claim ID: #{reference_id}! This Event is being"\ + " processed. Please try again later.") + subject + end + end + + context "when lock Key is already in the Redis Cache" do + it "throws a RedisLockFailed error" do + redis = Redis.new(url: Rails.application.secrets.redis_url_cache) + lock_key = "RedisMutex:EndProductEstablishment:#{reference_id}" + redis.set(lock_key, "lock is set", nx: true, ex: 5.seconds) + expect { subject }.to raise_error(Caseflow::Error::RedisLockFailed) + redis.del(lock_key) + end + end + + context "when creation is successful" do + it "should create a new Event instance" do + subject + expect(Event.where(reference_id: consumer_event_id).exists?).to eq(true) + end + + it "should call all sub services" do + expect(Events::DecisionReviewCreated::DecisionReviewCreatedParser).to receive(:new) + .with(headers, json_payload).and_call_original + expect(Events::CreateUserOnEvent).to receive(:handle_user_creation_on_event) + .with(event: event_created, css_id: parser.css_id, station_id: parser.station_id).and_call_original + expect(Events::DecisionReviewCreated::CreateClaimReview).to receive(:process!).and_call_original + expect(Events::DecisionReviewCreated::UpdateVacolsOnOptin).to receive(:process!).and_call_original + expect(Events::CreateClaimantOnEvent).to receive(:process!).and_call_original + expect(Events::DecisionReviewCreated::CreateIntake).to receive(:process!).and_call_original + expect(Events::DecisionReviewCreated::CreateEpEstablishment).to receive(:process!).and_call_original + expect(Events::DecisionReviewCreated::CreateRequestIssues).to receive(:process!).and_call_original + subject + end + end + + context "when a StandardError occurs" do + let(:error_message) { "StandardError message" } + + before do + allow(DecisionReviewCreatedEvent).to receive(:create).and_raise(StandardError, error_message) + allow(Event).to receive(:find_by).and_return(event) + allow(event).to receive(:update!) + end + + it "logs the error and updates the event" do + expect(Rails.logger).to receive(:error).with(error_message) + expect(event).to receive(:update!).with(error: error_message, info: { "failed_claim_id" => reference_id }) + + expect { subject.create!(consumer_event_id, reference_id) }.to raise_error(StandardError) + end + end + end +end + +def read_json_payload + JSON.parse(File.read(Rails.root.join("app", + "services", + "events", + "decision_review_created", + "decision_review_created_example.json"))) +end + +def sample_headers + { + "X-VA-Vet-SSN" => "123456789", + "X-VA-File-Number" => "77799777", + "X-VA-Vet-First-Name" => "John", + "X-VA-Vet-Last-Name" => "Smith", + "X-VA-Vet-Middle-Name" => "Alexander" + } +end