From ea8a7fd295e1146427e20e621ef0a7c25b84953e Mon Sep 17 00:00:00 2001 From: Nader Kutub Date: Mon, 7 Oct 2024 22:06:30 -0700 Subject: [PATCH] Feature/appeals 49670 uat merge (#23121) 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 * awillis/APPEALS-45152 (#21506) * APPEALS-45152 Updated the logic in create_claimant_on_event.rb to always generate a claimant record. Updated RSPEC. * APPEALS-45152 Cleaned up RSPEC. * APPEALS-45152 Fixed Failing RSPEC within decision_review_created_spec.rb * APPEALS-45152 Updated RSPEC within decision_review_created_spec.rb with addtional checks. * APPEALS-44319 (#21449) (#21541) * added logic for legacy issues in DRC * more legacy logic * updated rspec context lang * error cov --------- Co-authored-by: Jonathan Tsang <98970951+jtsangVA@users.noreply.github.com> Co-authored-by: Jonathan Tsang * Konstantin/APPEALS-45175 (#21517) * logical_date_converter re-written to work with yyyymmdd numbers from json payload * comments removed * comments removed2 * method rewitten * json example fixed * date in scenario_b_spec.rb replaced by valid ones, additional check for empty string parameter added to the logical_date_converter * removed filter for non rating request issue dropdown (#21480) * removed filter for non rating request issue dropdown * removed excluded types as we don't need to check this anymore * fixed broken test * added category back * no longer need test to check for missing categories since we're returning them all. --------- Co-authored-by: TuckerRose * Column added to RequestIssues table (#21578) * added migration and simple spec to test * fixed schema deleted issues * updated spec to verify data being set * error was no longer trigger from update. removed check and passed in correct variable --------- Co-authored-by: TuckerRose Co-authored-by: TuckerRose * added bgs_source to parser, controller and serializer along with specs (#21621) Co-authored-by: TuckerRose * Konstantin/appeals 45180 (#21591) * bug is fixed, condition added to DecisionReviewCreatedController * rspec test added * json message updated * comment added * Update decision_review_created_controller.rb comment updated --------- Co-authored-by: Nader Kutub * Konstantin/appeals 45149 (#21644) * RequestIssueSerializer attributes added * development_item_reference_id, same_office, legacy_opt_in_approved attributes were added * merge conflicts resolved * merge conflict resolved * fixed * all datapoints were added * empty line removed * comment removed * refactored * app/controllers/api/docs/v3/ama_issues.yaml updated with new attributes * example of request issue updated app/controllers/api/docs/v3/ama_issues.yaml * null to nil edited in example ama_issues.yaml * example ama_issues.yaml updated * example ama_issues.yaml updated2 * linters fixed * linters fixed2 * linters are fixed * APPEALS-45883 - Remove End Product Establishment records within the Event Records table in Caseflow (#21745) * APPEALS-45883 - changes to remove the creating of Event Records for epe * APPEALS-45883 - remove removed event params from method calls * APPEALS-45883 removed Event Double * APPEALS-48553 updated event_record spec * removed eventing from create claim review (#21718) * removed eventing from create claim review * checking why file is not being found for event_record * removed event var and require dependency * spec fixes * updated specs * fixed revert --------- Co-authored-by: TuckerRose Co-authored-by: Nader Kutub * Nrithner/appeals 45913 (#21716) * Convert from milliseconds to datetime * Add expection to have datetime after parsing * Fix linting issues * APPEALS - 45914 & 45915 (#21744) * Decision review parser and spec changes * remove byebug * Added db seed and testing (#21284) * Added db seed and testing * Change ApiKey seed name to ConsumerApiKey, and associated changes --------- Co-authored-by: Matt Ray Co-authored-by: Chris-Martine Co-authored-by: Nader Kutub * re-merge Cmartine/appeals 46861 (#21855) * Edit DecisionReviewCreatedController to respond with ok status if event exists and is completed, edit test * Move checking if event exists and is completed into event model, edit tests * Fix linter errors --------- Co-authored-by: Chris-Martine * Nrithner/appeals 46860 (#21856) * Set event info back to default default state for info is an empty json object * Correct linting errors * Test happy path with rspec * Refactor rspec example * Converte module to class * Remove module import --------- Co-authored-by: Nico Rithner * Jonathan/appeals 45899/45878 (#21714) * add "from event" value to Store * updated setup guide for M3 * added new badges * added jest * storybook file * additional story * bugfix * snap update * Konstantin/appeals 46175 (#21768) * draft processor method done * process_nonrating method moved to DecisionReviewCreated service * json example added, process_nonrating method updated * 4 rspec test added * useless json example removed * commented lines removed * condition added * condition added. * test updated * first condition moved out of the method * process_nonrating method call moved down * 1 comment removed, 1 added * process nonrating method rafactored, logic transferred into the parser * method refactored, rspec tests updated * rspec test slightly refactored --------- Co-authored-by: Nader Kutub * Edit Issues screen update (#21907) * Edit Issues screen update * relocated row * updated capybara test * updated capybara to check for new added text * cmartine/APPEALS-46905 (#21884) * Remove cache updating call for veteran when veteran is being created for an event * Change seeds api_key.rb to consumer_api_key.rb --------- Co-authored-by: Matt Ray <108031363+mattray-gov@users.noreply.github.com> * Update process_nonrating to process_nonrationg_issue_category Remove extra logic * Update rspec. Remove unused example * Change edited_by_css_id to return users css_id * Konstantin/appeals 48306 (#21998) * ama_eventing_enabled feature check added * toggle renamed * rspec tests added * 2 comments added * toggle renamed, logic trasferred into controller, tests updated * empty line removed --------- Co-authored-by: Matt Ray <108031363+mattray-gov@users.noreply.github.com> * 930 code updates * remove byebug * Change edited_by_css_id to return users css_id (#22062) Co-authored-by: Matt Ray <108031363+mattray-gov@users.noreply.github.com> * Nrithner/appeals 49245 (#22107) * Update process_nonrating to process_nonrationg_issue_category Remove extra logic * Update rspec. Remove unused example --------- Co-authored-by: Matt Ray <108031363+mattray-gov@users.noreply.github.com> * fix extra commas from merge * APPEALS-51223: Disable ClaimantValidator for DecisionReviewCreatedEvents (#22132) * APPEALS-51223 refactored the claimant_validator class method claimant_details_required? to consider the conditional statement * APPEALS-51223 updated Rspec to confirm claimant address_line_1 is nil and error is empty * APPEALS-51223 made changes to to claimant_validator to have all Unit test passing * APPEALS-51223 added suggested changes * Revert "fix merge conflict with AMA" This reverts commit 6775316c85e2c3d8cd94c1baae6d29d75503163d, reversing changes made to 66cf311d1ddb18d11137b173b94ecbafaa022e2b. * added comments * Delete docker-bin/build.sh, I didnt make these changes * Revert "Delete docker-bin/build.sh, I didnt make these changes" This reverts commit 03a3830c5df26e9d4830fcf1d66ea77af4703346. --------- Co-authored-by: nader k * Jonathan/appeals 51926 (#22215) * Hide UI changes behind featureFlag * fixed lint spacing warnings * code climate spec fix (#22211) * code climate spec fix * up argument count * created a parser helper for reusable methods * reset codeclimate back to original and added issue parser * modified methods to take in a hash instead of multiple params * reduced create params down to 3 params * added space inside * swapped to safe navigator for intake check * fixed spec * fixed linting issue * fixed failing spec * fixed merge conflicts with code changes for helper --------- Co-authored-by: TuckerRose Co-authored-by: TuckerRose Co-authored-by: Nader Kutub * Konstantin/appeals 52321 (#22363) * utc conversion method added in app/serializers/api/v3/issues/ama/request_issue_serializer.rb * set_contested_rating_issue_profile_date method updated in RequestIssue model, updated contested_rating_issue_profile_date in DecisionIssue model * decision issue updated, tests added * tests added, converters improve app/models/decision_issue.rb app/serializers/api/v3/issues/ama/request_issue_serializer.rb * set_contested_rating_issue_profile_date restored in request issue model * uneccessary logic removed * 3 more tests added * test added, error handling aaded in format_rating_profile_date method * Update decision_issue.rb * Update request_issue.rb * Appeals 52317 (#22321) * wip * added the parser to skeleton * removed commented code * added in parser logic * added dru_error route, error handling and params * added rspecs and fixed routing for update error to be a post * added empty spec * updated dru params to match intake json * rubocop fixes * more rubocop fixes * fixed routing to post now and updated specs --------- Co-authored-by: TuckerRose * fixed logic for AMA issues API serializer attributes (#22358) * fixed logic for added_by fields * methods for fetching removed_by user * withdrawn_by methods * methods for edited_by & instance vars * more logic for added_by methods * fixed failing tests * rspec updates * rspec for RIU addition * small changes * fix test failure * add metabase to m1 docker compose * rename metabase container and specify a version * Konstantin/appeals 52318 (#22528) * very loose prototype of class created * hopeless variant removed * updated parser per new design and updated specs * request_issues_updated_event updated * some improvments added * unused methods removed, some methods updated * after issues added * tests added * edited issues method returned, invalid tests removed * tests updated * 9 working tests added, RequestIssuesUpdateEvent variable instanciated * tests improved, attribute writers added * more test cases added, error handling in process! method added * added new version of parser and tests to it from Will's PR * uneccesary overrided classes removed * process_job call removed * process_job! restored, 3 more tests added * validate before perfom removed * linter issues addressed * linter issues addressed2 --------- Co-authored-by: TuckerRose * add script for initial metabase setup * script output formatting * add metabase directory * move to a dockerfile, implement VACOLS to setup script * add metabase to original docker-compose * Bug Fix for APPEALS-44115 (#22538) * running ci * testing category * ci with conditional * set back to false * reverted to original spec * running ci * updated intake helpers to account for using none of these match option * ignored metrics abc size * removed from helper * removed comments * updated spec to check on each category * removed single quotes * fixed a flakey test that was failing on a duplicate key * linting fix * pushed up feature flag * lint clean up * swapped to different featureflag * fixing spec feature toggle param * added feature flags back * checking if label is there * flipped feature test to correct order --------- Co-authored-by: TuckerRose * APPEALS-53923 (#22727) * new service class & error message * rspec * Update update_informal_conference_spec.rb * add step to initialize metabase in demo startup * update demo startup.sh to initialize metabase with absolute path * add make command for local metabase setup * fix makefile syntax error * Konstantin/appeals 52982 (#22715) * eligibility-related methods added * methods fixed, date conversion for closed_at added * decision_review_updated_issue_parser created, json example updated, request_issues_update_event updated * tests for parser fixed after json example update * new attribute vbms_id added to request_issues table, decision_review_updated_issue_parser update respectively * request_issues_update_event attr_writers added, decision_review_created_issue_parser class name update and call of that parser updated * veteran removed from DecisionReviewUpdatedParser initilizer, methods marked for potential deletion * decision_review_updated_issue_parser updated and test added for it, comments added to request_issues_update_event * not necessary methods removed from parser, tests for them commented out for now * attributes_from_intake_data method overrided * :id replced by vbms_id, json example, parsers and test for it updated * parsers added, test updated * css_id, station and detail_type commented from the parser * parser cleaned, test for parser fixed, request_issues_update_event cleaned * process_issues! overriden in app/models/request_issues_update_event.rb, linter offenses fixed, testtypo fixed * vbms_id renamed to reference_id * comment typo fixed * Create Audit Tables for SC, HLR, Request Issues Appeals 55403 (#22689) * migration added * schema updated correctly * added audit service and call to update evenr records * logic for update type * fixed typo in audit file and added spec * added insert * moved Insert DRUA to create request issues for created ones * updated event record to take in update type * passing in correct info attribute now * disabled unused params, can enable once we use them * remove << Co-authored-by: TuckerRose * fix merge conflic in seeds.rb * remove cusoner seed files * Konstantin/appeals 57291 (#22853) * decision_review_created/decision_review_created_parser.rb updated * rspec tests for decision_review_created parser added * issue parser updated, tests added * decision review updated parsers updated, tests added * linter issue addressed * Konstantin/appeals 57283 (#22851) * reference_id added to parser, json example and create_request_issues.rb * rspec tests for decision_review_created_issue_parser created * reference_id null error handling added, tests for create_request_issues.rb added * linter offenses fixed * new param permitted added in app/controllers/api/events/v1/decision_review_created_controller.rb * feature tests fixed * APPEALS-55402 (#22768) * service class and error * adding to DRU class method * linter line too long fix * small fix to riue * added rspec * fixed typo * Konstantin/appeals 57279 (#22859) * DecisionReviewUpdateMissingIssueError handling is created * initializer added, typos fixed in request_issues_update_event.rb,more error handlings added, tests updated and added * little optimizationd added to app/models/request_issues_update_event.rb * attributes_from_intake_data fixed * commented code removed * review defined in decision_review_updated.rb * formatted a bit * useless comments removed * linter offenses cleaned, useless comments removed * Update request_issues_update_event.rb Result of auto conflict resolution fixed * useless comment removed * Update decision_review_updated.rb Lines 21,22 uncommented. * rubocop disable/enable added * additional positive tests added, RequestIssuesUpdateEvent updated * 2 tests added * linter issue fixed * Jonathan/appeals 58619 (#22875) * service class and error * adding to DRU class method * linter line too long fix * small fix to riue * added rspec * fixed typo * Added new service class + fixed typo * typos * more typo fixes * additional fields to epe.update * updated sample payload * unit test + time conversion --------- Co-authored-by: Nader Kutub * update scripts to work in AWS * update demo script to use apt-get instead of yum * Process newly added fields passed for DecissionReviewUpdated (#22876) * added parser changes * removed empty line * updated json to match updated design and removed unused methods * remove ep code category * removed unused methods * fixing specs and linting * linting fix * updated json example to match * added edited_description to spec * added new json payload changes that required spec updates * removed binding.pry --------- Co-authored-by: TuckerRose * Refactor Created Events Logic to Handle Contested Issues (#22852) * wip * added check for contested with specs * removed newline * fixed contested setting to if not eligible and fixed specs * removed setter for contested --------- Co-authored-by: TuckerRose * update demo script to use dns for vacols-db * fix vacols DB docker DNS name * issue parser applied (#22904) * issue parser applied * refactor request_issues_update_event * refactor request_issues_update_event- remove temp files * fix lint issues * fix linting issues * fix eligibility fields * add gaurd for missing participant in request issues serializer * add description fields to eligibility updates * bug fixes with controller --------- Co-authored-by: Nader Kutub Co-authored-by: Nader Kutub * renamed epe to singular method call (#22914) Co-authored-by: TuckerRose * Edit DecisionReviewUpdatedController dru_params (#22939) * API fix + epe feature test (#22956) * API fix + epe feature test * add specs --------- Co-authored-by: Nader Kutub * Feature/appeals 59173 (#22995) * fix bug reatletd to new issues * fix DRU bugs * fix linting issue * fix event records bug * fix event records spec * hotfix/APPEALS-23420: search defect Add search query service for the api response for the `/search` page Add issues to legacy appeals Add assigned_to_judge Use correct id for list of request issues export proper attributes for hearing * Revert "hotfix/APPEALS-23420: search defect" (#23011) This reverts commit 25ed7f1037172a2e7ac4385e32372e8d25251297. * bug fixes for error logs, audits, claim id (#23015) * bug fixes for error logs, audits, claim id * fix redi lock on created event * add reference id to AMA API (#23022) * add reference id to AMA API * add withdrawn audit * audit spec fix * Konstantin/appeals 58609 (#22970) * draft test * typo fixed, byebug inside find_or_build_request_issue_from_intake_data * typo fixed, edited_description added to attributes in app/controllers/api/events/v1/decision_review_updated_controller.rb * test file renamed * commented lines removed * Update spec_helper.rb * Update scenario_edited_issues_spec.rb typo * success message updated, any updates check added, edited_description before check added * audit find_by fix and add create scenario --------- Co-authored-by: Nader Kutub Co-authored-by: Nader Kutub * Konstantin/withdrawn issues feature test (#23028) * withdrawn feature test added * more checks added to test * temp change * request_issue.reload removed * fix ama api and closed rspec * remove byebug and fix closed statu rspec * Jonathan/appeals 59166 (#22972) * feature test, update hlr/sc optin value * featuretoggle change * featuretoggle * fix linting issues * Merge `feature/APPEALS-41559` into `release/FY24Q4.6.1` (#23036) * Start feature work tracking PR with a TODO comment * Remove TODO comment for CodeClimate * SeanC/APPEALS-42315 | Create separate Remand table, model and seed data (#22220) * added migration file for adding type column to supplemental claims table * created stub for remand model * added type to supplemental claim factory and new remand trait * added remand sc's to vha seeds * updated migration name and fixed typo * Changed migration to inherit from Caseflow::Mirgration * schema changes * reverting schema * changed the setting of the type column * fixed schema and renamed migration file * fixed migration and schema * updated the remands factory and seeds * fixed comment in schema --------- Co-authored-by: Robert Travis Pierce * TYLERB/APPEALS-42455: Display new and migrated Remands in Decision Review Queue tabs (#22206) * Added support for remands to the business_line.rb model and the index.js util file that parses the decision review type filters for the decision review queue. * Updated the business line spec file, the decision reviews controller spec tests, and the reviews spec feature test to support remands. * Added a decision review polymorphic helper to DRY up some of the polymorphic appeal concerns. * Overrode the remand model association to request issues so active record joins will work correctly. * Added another helper for sti polymorphic relationships. Fixed filtering for STI decision review types for remands. Updated tests and added factories for testing. * Code climate fixes and test fixes. * Fixed more test failures. Removed the remand request issues relationship since it was incorrectly implemented. Added a scope to supplemental claims for remands. Fixed an issue where board grant effectuation tasks without request issues were missing from the new task type count method. * Changed the UNION ALL to a UNION in the new task_type_count method to prevent duplicate row counts. Updated tests and fixed an issue where the board grant effectuation tasks were being counted for pending task queries. * JHoang/APPEALS-42318 (#22270) * some preliminary remand & sc model updates * typo * some preliminary hlr checks? * undo seed change * decision issue and sc updates * attempt code climate fixes * another code climate fix * testing changes * useless variable.. * fix conditional in decision issue * fix undefined find_by method * attempt rspec fix * rspec fix attempt #2 * supplemental_claim_spec fix? * remove comma * code climate fix * added comment for transparency on remand creation, attempt fix for remands route * updated specs for decision_issue, hlr_spec, appeal_spec to account for remand creation type * fix lint error * fix lint error #2 --------- Co-authored-by: Jonathan Hoang * APPEALS-42458 (#22365) * disable edit issues link for remands * add banners explaining that remands are not editable also makes sure that remands are not editable on the edit issues page * TYLERB/APPEALS-44922: Update Generate task report page UI to include Remand in conditions (#22356) * Initial commit. Changed the DecisionReviewType condition filter to a searchable dropdown and added the remands option to it as well. Updated the report page filter parsing to work with the searchable dropdown instead of checkboxes for the decision review type condition filter. * Updated the ReportPage jest test and the report page feature test to work with the decision review type filter as a searchable dropdown instead of checkboxes. * SeanC/APPEALS-42317 | Migrate existing Supplemental Claim Remands to Remand records via script (#22359) * initial verison of update script * remanded migration file * updated sc spec with type * schema * fixed a few issues in the migration * small fix to migration file * moved the type column check into a seperate it block * fixed wrong column in migration file * created a new migration for creating the view * updated the backfill migration file --------- Co-authored-by: Brandon Lee Dorner * J hoang/appeals 44940 (#22428) * initial * some factory and spec updates * change history query update and spec fixes * code climate/lint fix * change @claim_type identifier * fix claim_history_event_spec.rb * test coverage for change history with Remand claim type * spec lint fixes * refactor claim_type filtering to account for remand subtype * business_line_spec fix * decision_reviews_controller_spec update for remand claim type * lint fixes for decision_reviews_controller_spec.rb * comment updates in business_line.rb * renamed mock data in change_history_reporter_spec * trigger GA * refactor claim type filter for less conditional handling * business line spec fix --------- Co-authored-by: Jonathan Hoang * Initial commit for the brakeman sql injection warning fixes. (#22598) * Updated the business line reporter class to properly display Remands as a seperate type than SupplementalClaims. Also added additional testing logic for the business line reporter for hlr and scs since it was only covering appeals. (#22664) Co-authored-by: Robert Travis Pierce * Fixed a bug where the frontend would error due to calling .toLowerCase() on a null or undefined value which could happen sometimes if the api filter options are in an incorrect state. (#22796) Co-authored-by: Robert Travis Pierce * Updated the remand migration files to work with the rails 6 upgrade and the changes to Caseflow::Migration. (#22825) * added fix for failing test in decision_reviews_controller_spec (#22829) --------- Co-authored-by: Robert Travis Pierce Co-authored-by: Sean Craig <110493538+seancva@users.noreply.github.com> Co-authored-by: Tyler Broyles <109369527+TylerBroyles@users.noreply.github.com> Co-authored-by: jonathanh-va <111081469+jonathanh-va@users.noreply.github.com> Co-authored-by: Jonathan Hoang Co-authored-by: Brandon Lee Dorner Co-authored-by: Clay Sheppard * APPEALS-50679 | Reassign Cases To Camo Change History (#23038) * Start feature work tracking PR with a TODO comment * Remove TODO comment for CodeClimate * SeanC/APPEALS-42315 | Create separate Remand table, model and seed data (#22220) * added migration file for adding type column to supplemental claims table * created stub for remand model * added type to supplemental claim factory and new remand trait * added remand sc's to vha seeds * updated migration name and fixed typo * Changed migration to inherit from Caseflow::Mirgration * schema changes * reverting schema * changed the setting of the type column * fixed schema and renamed migration file * fixed migration and schema * updated the remands factory and seeds * fixed comment in schema --------- Co-authored-by: Robert Travis Pierce * TYLERB/APPEALS-42455: Display new and migrated Remands in Decision Review Queue tabs (#22206) * Added support for remands to the business_line.rb model and the index.js util file that parses the decision review type filters for the decision review queue. * Updated the business line spec file, the decision reviews controller spec tests, and the reviews spec feature test to support remands. * Added a decision review polymorphic helper to DRY up some of the polymorphic appeal concerns. * Overrode the remand model association to request issues so active record joins will work correctly. * Added another helper for sti polymorphic relationships. Fixed filtering for STI decision review types for remands. Updated tests and added factories for testing. * Code climate fixes and test fixes. * Fixed more test failures. Removed the remand request issues relationship since it was incorrectly implemented. Added a scope to supplemental claims for remands. Fixed an issue where board grant effectuation tasks without request issues were missing from the new task type count method. * Changed the UNION ALL to a UNION in the new task_type_count method to prevent duplicate row counts. Updated tests and fixed an issue where the board grant effectuation tasks were being counted for pending task queries. * JHoang/APPEALS-42318 (#22270) * some preliminary remand & sc model updates * typo * some preliminary hlr checks? * undo seed change * decision issue and sc updates * attempt code climate fixes * another code climate fix * testing changes * useless variable.. * fix conditional in decision issue * fix undefined find_by method * attempt rspec fix * rspec fix attempt #2 * supplemental_claim_spec fix? * remove comma * code climate fix * added comment for transparency on remand creation, attempt fix for remands route * updated specs for decision_issue, hlr_spec, appeal_spec to account for remand creation type * fix lint error * fix lint error #2 --------- Co-authored-by: Jonathan Hoang * APPEALS-42458 (#22365) * disable edit issues link for remands * add banners explaining that remands are not editable also makes sure that remands are not editable on the edit issues page * TYLERB/APPEALS-44922: Update Generate task report page UI to include Remand in conditions (#22356) * Initial commit. Changed the DecisionReviewType condition filter to a searchable dropdown and added the remands option to it as well. Updated the report page filter parsing to work with the searchable dropdown instead of checkboxes for the decision review type condition filter. * Updated the ReportPage jest test and the report page feature test to work with the decision review type filter as a searchable dropdown instead of checkboxes. * SeanC/APPEALS-42317 | Migrate existing Supplemental Claim Remands to Remand records via script (#22359) * initial verison of update script * remanded migration file * updated sc spec with type * schema * fixed a few issues in the migration * small fix to migration file * moved the type column check into a seperate it block * fixed wrong column in migration file * created a new migration for creating the view * updated the backfill migration file --------- Co-authored-by: Brandon Lee Dorner * J hoang/appeals 44940 (#22428) * initial * some factory and spec updates * change history query update and spec fixes * code climate/lint fix * change @claim_type identifier * fix claim_history_event_spec.rb * test coverage for change history with Remand claim type * spec lint fixes * refactor claim_type filtering to account for remand subtype * business_line_spec fix * decision_reviews_controller_spec update for remand claim type * lint fixes for decision_reviews_controller_spec.rb * comment updates in business_line.rb * renamed mock data in change_history_reporter_spec * trigger GA * refactor claim type filter for less conditional handling * business line spec fix --------- Co-authored-by: Jonathan Hoang * Initial commit for the brakeman sql injection warning fixes. (#22598) * Updated the business line reporter class to properly display Remands as a seperate type than SupplementalClaims. Also added additional testing logic for the business line reporter for hlr and scs since it was only covering appeals. (#22664) Co-authored-by: Robert Travis Pierce * Fixed a bug where the frontend would error due to calling .toLowerCase() on a null or undefined value which could happen sometimes if the api filter options are in an incorrect state. (#22796) Co-authored-by: Robert Travis Pierce * Updated the remand migration files to work with the rails 6 upgrade and the changes to Caseflow::Migration. (#22825) * added fix for failing test in decision_reviews_controller_spec (#22829) * Al/APPEALS-45335 (#22228) * added additional option to check box list * fixed some tests and code clean up * fixed more tests * fixed jest tests * changes per Pr feedback * added custom error display * fixed style for error message --------- Co-authored-by: Sean Craig * adding logic to bring in issue-modification request and updating the … (#22238) * adding logic to bring in issue-modification request and updating the csv logic * refactor logic to get pending status and null to request addition * pending status filter work fixing * adding withdrawal date and adding logic to display request issue components * getting edit of request rows and fixing cancelled data * fixing linter and fixing the query to make test happy * fixing specs failures * removing byebug * removing frontend changes, adding test, fixing the query to replace higherlevel review,adding new field in serializer * making code climate happy by doing some minor refactor * fixing the serializer spec failure * After merging APPEALS-45335, filter for Requested issues were broken, so refactored it and removed the unnecessary methood that i created * fixing code climate and spec was failing because of hard coded date * refactor the edit of request logic to take from version, refactored business line query to remove coalesce and fix test * fixing codeclimate, remove unnecessary field * refactor to make first comment appear first in edit of request * fixing linter issue, spec and refactor to fix the bug * fixing pending status filter issue and adding test for business line * fixing broken spec failure * adding user facilty * fixing the lead and lag function to prevent entry of pending status multiple times * fixing an issue in which in progress event was not being made * adding decision reason that was missed and fixing the event logic one more time * fixing the multiple issue modification request after decision has been made * refactor inprogress logic for approved data * fixing spec failure * fixing the first version logic to be more simpler and fixing name and adding spec * fixing more inprogess event * removing the disabled complexity and taking out the boolean condition to a separate method * Al/APPEALS-45334 Individual change history table which includes Reassign Cases to CAMO events (#22483) * added logic for claim history events table * fixed linting errors * fixing test failures * added change Pr feedback * PR feedback round 2 * changes from round 2 of PR feedback * Added logic event data versions and tests to cover more edgecases * changed RequestedIssueFragment * Updated the individual claim history spec file for test failures. Updated an attribute in the IndividualClaimHistory page that was using the wrong getter. Fixed a code climate linting error. * Removing unnecessary clear filter * Combined some of the individual claim history tests into one test so it will run more quickly. * fixing the withdrawal date versioning issue for different level changes * fixing the spec failure and linter --------- Co-authored-by: Brandon Lee Dorner Co-authored-by: = Co-authored-by: Prajwal Amatya * TYLERB/APPEALS-57367: change-history-updates (#22479) * Initial commit with new tests cases for issue modification requests for change history. * Added two cte queries to the business line change history query to properly work with a lead for the issue modification requests. Added new methods into the claim history event to work with this new lead and modified the existing issue modification in progress and pending event generation methods to better reflect the state of a claim that is going through the issue modficiation process. * Cleaned up unused code and todos. * Updated the claim history event create_imr_in_progress_status_event? with even more logic and edge cases. * Fixed a possible error when with a greater than operator could be used on a null value. * Fixed a few rspec tests. * Renamed a method and fixed a few code climate issues. * Fixed a bug in event status generation if there is a task with versions but no versions that change status. * Updated tests to work with new logic and remove a todo statement. * Fixed code complexity warnings. * Updated serializer spec to work with the bug fix. * Adjusted the pending method to prevent possible race conditions. * Fixed a bug where the incorrect decision event user would be shown for an issue modification request creation if the request has been decided when it should be the requestor as the event user. * Added the correct css ids to a few IMR change history events so that the filtering would work correctly. Also added some additional sql to properly filter based on css id and station id for requestor and decider joins. * Updated the IMR business line logic so that the decided at fields would always account for cancelled status since it uses updated at instead of decided at to determine when the IMR was closed. Fixed a bit of logic in the claim history event IMR in progress event generation to work another edge case. * Added in previous IMR edit versions for displaying it to the user. * Added a test for the change history service class to verify that request edit events have the correct attributes from the previous edits. * Fixed a typo for one of the table aliases in the business line query. * Fixed another typo where the facilities filter was using css id instead of station id from a copy/paste error. * adding changes for previous state array and some test modification * adding test for previous data * Fixed some outstanding test failures. * Fixed a linting error. Removed some duplicated code that happened when merging in the feature branch. Updated a factorybot method that was causing tests to fail. * Fixed a few code climate warnings. * Removed some puts statements in the individual claim history feature test. --------- Co-authored-by: Prajwal Amatya * TYLERB/APPEALS-58656: Change History CSV and Version Parsing fix (#22874) * Fixed a modification request claim history in progress event generation bug that could occur when 3-4 pending requests were all created at the same time and then later cancelled at the same time. Also fixed an issue count bug used when building the issue type filter due to additional duplicate rows being inserted into issue type count query due to the left join to the issue modification requests table. * Altered the version parsing for change history to work with commas in the version strings since that was causing parsing errors. Altered the business line query to use '|||' as a delimeter instead of comma and also changed the array_agg functions to string agg to avoid the '{}' array type casting that was happening between postgres and rails. Updated tests to work with this new parsing. * Updated vha seed data to have associations to intake data so those seeded claims can correctly generate change history. * TYLERB/CHANGE-HISTORY-YAML-QUOTE-FIX (#22910) * Made an update to the claim history parse versions method to work with double quotations after the array_agg to string_agg change. * Updated the tests to work with the new string agg and version parsing method. * Skip flakey tests These are possibly flakey now due to the rails upgrade. We can come back and fix these once the work has been confirmed in UAT. * Add rubocop disables for line length These should be removed when the skips are removed. * Updated the report_page feature test to reduce flakyness in github actions. Also updated the DownloadHelpers module. --------- Co-authored-by: Brandon Dorner --------- Co-authored-by: Robert Travis Pierce Co-authored-by: Robert Travis Pierce Co-authored-by: Sean Craig <110493538+seancva@users.noreply.github.com> Co-authored-by: Tyler Broyles <109369527+TylerBroyles@users.noreply.github.com> Co-authored-by: jonathanh-va <111081469+jonathanh-va@users.noreply.github.com> Co-authored-by: Jonathan Hoang Co-authored-by: Clay Sheppard Co-authored-by: almorbah <149511814+almorbah@users.noreply.github.com> Co-authored-by: Sean Craig Co-authored-by: Prajwal Amatya <122557351+pamatyatake2@users.noreply.github.com> Co-authored-by: = Co-authored-by: Prajwal Amatya * APPEALS-54874: Update PG and Ruby-oci8 gems (#23041) * update minimum version in gemfile and ran bundle install (#22503) * require ruby-oci8 in config/boot.rb to fix SIGV fault * APPEALS-59294: Hearings seed file is creating DistributionTasks for legacy appeals (#23042) * set parent task for legacy appeal hearing tasks to root task * fix whitespace issue * APPEALS-36292: Conditionally render banner alerts based on which environment it is deployed on (#23040) * Conditionally render alerts based on which environment is deployed * setting Env alert banner sitewide * separate colors for different envs using the same alert type * lighter reds for prodtest alert so the text is more readable * give demo banner more contrast * adjust specific language for demo env * adjust logic to accept proper env var. Add style for preprod (yellow) * isolate env alert styles * remove duplicate 'demo' Alert * remove development env banner to fix tests * making conditional logic a little more bulletproof against 'production' * update CaseWorkerIndex snapshot * fix env var * update snapshot from hard 'uat' setting * conditionally render env alert on only the home page * sticky env header statuses * getting development alert banners to work * target correct frontend dependency for NavigationBar changes * connecting new frontend package with better prod exclusion logic * make the prodtest env badge a more readable color * cleaning up testing code * fix linting issues * Remove unnecessary space * new NavigationBar update * yarn install of new FE dependency * remove extra package.json * remove development env logic due to NODE_ENV bug * fixing broken logo link padding and colors * hardening logic to exclude other inputs * correcting operator logic * remove testing var * point caseflow-frontend to master * point at most recent caseflow-frontend master * linting issues fix * update Route and CaseWorker snapshot * update snapshot for process.env.DEPLOY_ENV --------- Co-authored-by: Sean Parker * Master to Main documentation changes (#23034) * Update WINDOWS_11.md replacing master with main * References to master are changed to main where applicable * fix DRU removed and DRUs for eventing feature, added feature tests an… (#23055) * fix DRU removed and DRUs for eventing feature, added feature tests and RIUs checks * fix linting errors * fix mutex error spec * lint fixes * Hotfix/appeals 37269 (#23039) * adds script and tests for the script * adjust the way scheduled_for is created since the previous method was removed * refactor some of the script * rename test file to include _spec * Empty-Commit --------- Co-authored-by: Zackary Borges-Rowe Co-authored-by: Raymond Hughes <131811099+raymond-hughes@users.noreply.github.com> * disable DRU audit (#23064) * Revert "Release R2.6.0 FY24Q4.6.0" * Nader/audit sync fix (#23079) * remove sync attemps on RequestIssuesUpdateEvent and add auditing, fix nil discription * add audit record check on add issue scenario spec * fix changes? call * ermove audit mock * fix HIGHER_LEVEL_REVIEW check * Revert "fix HIGHER_LEVEL_REVIEW check" This reverts commit 1499727ceb3211d4b2f910ab6a42dd5483fa3481. * Konstantin/appeals 59181 (#22992) * eligible to ineligible test added * eligible_to_ineligible test added * one added, 2 updated * empty line removed * spec_helper restored * success message updated * empty line removed * succes message expectation update in tests * wording updated * typo fixed * linter addressed * scenario_create_issues_spec.rb deleted, it was an invalid draft accidently pushed in previous PR * add last sync to RIU's, update contention updated and remoted at , ac… (#23092) * add last sync to RIU's, update contention updated and remoted at , account for CF intaked RIs based on original_casflow_request_issue_id * fix rspec * fix discription texts * fix missing descriptions * fix epe's getting created and error on missing ineligible_due_to (#23103) * update contention text fix for eligibility changes * fix the unidentified_text and contention_refference_id (#23119) * fix the unidentified_text and contention_refference_id * fix rspec and lint --------- 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 Co-authored-by: (Jeffrey) Aaron Willis <98484567+Aaron-Willis@users.noreply.github.com> Co-authored-by: Konstantin Shevtsov <166068160+KonstantinShevtsov@users.noreply.github.com> Co-authored-by: TuckerRose Co-authored-by: nicorithner-bah <148365039+nicorithner-bah@users.noreply.github.com> Co-authored-by: Kodi Shiflett Co-authored-by: Matt Ray Co-authored-by: Chris-Martine Co-authored-by: Nico Rithner Co-authored-by: Chris-Martine <135330019+Chris-Martine@users.noreply.github.com> Co-authored-by: Matt Ray <108031363+mattray-gov@users.noreply.github.com> Co-authored-by: Craig Reese <109101548+craigrva@users.noreply.github.com> Co-authored-by: ryanpmessner <163380175+ryanpmessner@users.noreply.github.com> Co-authored-by: Ron Wabukenda <130374706+ronwabVa@users.noreply.github.com> Co-authored-by: Robert Travis Pierce Co-authored-by: Robert Travis Pierce Co-authored-by: Sean Craig <110493538+seancva@users.noreply.github.com> Co-authored-by: Tyler Broyles <109369527+TylerBroyles@users.noreply.github.com> Co-authored-by: jonathanh-va <111081469+jonathanh-va@users.noreply.github.com> Co-authored-by: Jonathan Hoang Co-authored-by: Brandon Lee Dorner Co-authored-by: Clay Sheppard Co-authored-by: almorbah <149511814+almorbah@users.noreply.github.com> Co-authored-by: Sean Craig Co-authored-by: Prajwal Amatya <122557351+pamatyatake2@users.noreply.github.com> Co-authored-by: = Co-authored-by: Prajwal Amatya Co-authored-by: Sean Parker Co-authored-by: Raymond Hughes <131811099+raymond-hughes@users.noreply.github.com> Co-authored-by: Zackary Borges-Rowe --- .vscode/settings.json | 1 + .../v1/decision_review_created_controller.rb | 3 +- .../v1/decision_review_updated_controller.rb | 116 ++++ app/models/claim_review.rb | 2 +- .../events/decision_review_updated_event.rb | 5 + app/models/events/event_record.rb | 1 + app/models/higher_level_review.rb | 2 +- app/models/legacy_issue.rb | 2 +- app/models/legacy_issue_optin.rb | 2 +- app/models/request_issue.rb | 12 +- app/models/request_issues_update_event.rb | 357 ++++++++++++ app/models/supplemental_claim.rb | 2 +- .../v3/issues/ama/request_issue_serializer.rb | 4 +- .../events/decision_review_created.rb | 36 +- .../create_request_issues.rb | 14 +- .../decision_review_created_example.json | 5 +- .../decision_review_created_issue_parser.rb | 37 +- .../decision_review_created_parser.rb | 48 +- .../events/decision_review_updated.rb | 66 +++ .../decision_review_updated_example.json | 250 ++++++++ .../decision_review_updated_issue_parser.rb | 131 +++++ .../decision_review_updated_parser.rb | 213 +++++++ .../update_claim_review.rb | 20 + .../update_end_product_establishment.rb | 23 + .../update_informal_conference.rb | 24 + .../events/decision_review_updated_error.rb | 44 ++ config/routes.rb | 2 + ...0240829142914_add_info_to_event_records.rb | 13 + ...4831_add_reference_id_to_request_issues.rb | 5 + db/schema.rb | 5 +- lib/caseflow/error.rb | 16 +- ...decision_review_created_controller_spec.rb | 2 +- ...decision_review_updated_controller_spec.rb | 163 ++++++ .../scenario_a_spec.rb | 1 + .../scenario_c_spec.rb | 1 + .../scenario_create_issues_spec.rb | 124 ++++ .../scenario_edited_issues_spec.rb | 229 ++++++++ .../scenario_removed_issues_spec.rb | 144 +++++ .../scenario_update_eligibility_spec.rb | 384 +++++++++++++ .../scenario_update_epe_spec.rb | 62 ++ .../scenario_update_soc_optin_spec.rb | 89 +++ .../scenario_withdrawn_issues.rb | 136 +++++ spec/models/events/event_record_spec.rb | 6 +- .../request_issues_update_event_spec.rb | 518 +++++++++++++++++ .../ama/request_issue_serializer_spec.rb | 1 + .../create_request_issues_spec.rb | 36 +- ...cision_review_created_issue_parser_spec.rb | 334 +++++++++++ .../decision_review_created_parser_spec.rb | 60 +- .../events/decision_review_created_spec.rb | 29 +- ...cision_review_updated_issue_parser_spec.rb | 314 ++++++++++ .../decision_review_updated_parser_spec.rb | 534 ++++++++++++++++++ .../update_claim_review_spec.rb | 49 ++ .../update_end_product_establishment_spec.rb | 34 ++ .../update_informal_conference_spec.rb | 88 +++ .../events/decision_review_updated_spec.rb | 152 +++++ 55 files changed, 4859 insertions(+), 92 deletions(-) create mode 100644 app/controllers/api/events/v1/decision_review_updated_controller.rb create mode 100644 app/models/events/decision_review_updated_event.rb create mode 100644 app/models/request_issues_update_event.rb rename app/services/{ => events/decision_review_created}/decision_review_created_issue_parser.rb (64%) create mode 100644 app/services/events/decision_review_updated.rb create mode 100644 app/services/events/decision_review_updated/decision_review_updated_example.json create mode 100644 app/services/events/decision_review_updated/decision_review_updated_issue_parser.rb create mode 100644 app/services/events/decision_review_updated/decision_review_updated_parser.rb create mode 100644 app/services/events/decision_review_updated/update_claim_review.rb create mode 100644 app/services/events/decision_review_updated/update_end_product_establishment.rb create mode 100644 app/services/events/decision_review_updated/update_informal_conference.rb create mode 100644 app/services/events/decision_review_updated_error.rb create mode 100644 db/migrate/20240829142914_add_info_to_event_records.rb create mode 100644 db/migrate/20240906174831_add_reference_id_to_request_issues.rb create mode 100644 spec/controllers/api/events/v1/decision_review_updated_controller_spec.rb create mode 100644 spec/feature/events/decision_review_updated/scenario_create_issues_spec.rb create mode 100644 spec/feature/events/decision_review_updated/scenario_edited_issues_spec.rb create mode 100644 spec/feature/events/decision_review_updated/scenario_removed_issues_spec.rb create mode 100644 spec/feature/events/decision_review_updated/scenario_update_eligibility_spec.rb create mode 100644 spec/feature/events/decision_review_updated/scenario_update_epe_spec.rb create mode 100644 spec/feature/events/decision_review_updated/scenario_update_soc_optin_spec.rb create mode 100644 spec/feature/events/decision_review_updated/scenario_withdrawn_issues.rb create mode 100644 spec/models/request_issues_update_event_spec.rb create mode 100644 spec/services/events/decision_review_created/decision_review_created_issue_parser_spec.rb create mode 100644 spec/services/events/decision_review_updated/decision_review_updated_issue_parser_spec.rb create mode 100644 spec/services/events/decision_review_updated/decision_review_updated_parser_spec.rb create mode 100644 spec/services/events/decision_review_updated/update_claim_review_spec.rb create mode 100644 spec/services/events/decision_review_updated/update_end_product_establishment_spec.rb create mode 100644 spec/services/events/decision_review_updated/update_informal_conference_spec.rb create mode 100644 spec/services/events/decision_review_updated_spec.rb diff --git a/.vscode/settings.json b/.vscode/settings.json index 849abe2a50f..0a380d1f4d5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,4 +8,5 @@ "files.insertFinalNewline": true, "files.trimTrailingWhitespace": true, "git.autofetch": true, + "makefile.configureOnOpen": false, } diff --git a/app/controllers/api/events/v1/decision_review_created_controller.rb b/app/controllers/api/events/v1/decision_review_created_controller.rb index 31c0dcb4ff2..83dd78678b7 100644 --- a/app/controllers/api/events/v1/decision_review_created_controller.rb +++ b/app/controllers/api/events/v1/decision_review_created_controller.rb @@ -25,7 +25,7 @@ def decision_review_created claim_id = drc_params[:claim_id] headers = request.headers - consumer_and_claim_ids = { consumer_event_id: consumer_event_id, reference_id: claim_id } + consumer_and_claim_ids = { consumer_event_id: consumer_event_id, claim_id: claim_id } ::Events::DecisionReviewCreated.create!(consumer_and_claim_ids, headers, drc_params) render json: { message: "DecisionReviewCreatedEvent successfully processed and backfilled" }, status: :created rescue Caseflow::Error::RedisLockFailed => error @@ -88,6 +88,7 @@ def drc_params :contested_rating_issue_diagnostic_code, :ramp_claim_id, :rating_issue_associated_at, + :decision_review_issue_id, :nonrating_issue_bgs_id, :nonrating_issue_bgs_source]) end diff --git a/app/controllers/api/events/v1/decision_review_updated_controller.rb b/app/controllers/api/events/v1/decision_review_updated_controller.rb new file mode 100644 index 00000000000..cfca0617e6f --- /dev/null +++ b/app/controllers/api/events/v1/decision_review_updated_controller.rb @@ -0,0 +1,116 @@ +# frozen_string_literal: true + +class Api::Events::V1::DecisionReviewUpdatedController < Api::ApplicationController + # Checks if API is disabled + before_action do + if FeatureToggle.enabled?(:disable_ama_eventing) + render json: { + errors: [ + { + status: "501", + title: "API is disabled", + detail: "This endpoint is not supported." + } + ] + }, + status: :not_implemented + end + end + + REQUEST_ISSUE_ATTRIBUTES = [ + :original_caseflow_request_issue_id, + :contested_rating_decision_reference_id, + :contested_rating_issue_reference_id, + :contested_decision_issue_id, + :untimely_exemption, + :untimely_exemption_notes, + :edited_description, + :vacols_id, + :vacols_sequence_id, + :nonrating_issue_bgs_id, + :type, + :decision_review_issue_id, + :contention_reference_id, + :benefit_type, + :contested_issue_description, + :contested_rating_issue_profile_date, + :decision_date, + :ineligible_due_to_id, + :ineligible_reason, + :unidentified_issue_text, + :nonrating_issue_category, + :nonrating_issue_description, + :closed_at, + :closed_status, + :contested_rating_issue_diagnostic_code, + :rating_issue_associated_at, + :ramp_claim_id, + :is_unidentified, + :nonrating_issue_bgs_source + ].freeze + + def decision_review_updated + consumer_event_id = dru_params[:event_id] + claim_id = dru_params[:claim_id] + headers = request.headers + consumer_and_claim_ids = { consumer_event_id: consumer_event_id, reference_id: claim_id } + ::Events::DecisionReviewUpdated.update!(consumer_and_claim_ids, headers, dru_params) + render json: { message: "DecisionReviewUpdatedEvent successfully processed" }, status: :ok + 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_updated_error + event_id = dru_error_params[:event_id] + errored_claim_id = dru_error_params[:errored_claim_id] + error_message = dru_error_params[:error] + ::Events::DecisionReviewUpdatedError.handle_service_error(event_id, errored_claim_id, error_message) + render json: { message: "Decision Review Updated 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 + + # rubocop:disable Metrics/MethodLength + + def dru_error_params + params.permit(:event_id, :errored_claim_id, :error) + end + + def dru_params + params.permit( + :event_id, + :claim_id, + :css_id, + :detail_type, + :station, + claim_review: [ + :auto_remand, + :remand_source_id, + :informal_conference, + :same_office, + :legacy_opt_in_approved + ], + end_product_establishment: [ + :code, + :development_item_reference_id, + :reference_id, + :synced_status, + :last_synced_at + ], + added_issues: REQUEST_ISSUE_ATTRIBUTES, + updated_issues: REQUEST_ISSUE_ATTRIBUTES, + removed_issues: REQUEST_ISSUE_ATTRIBUTES, + withdrawn_issues: REQUEST_ISSUE_ATTRIBUTES, + ineligible_to_eligible_issues: REQUEST_ISSUE_ATTRIBUTES, + eligible_to_ineligible_issues: REQUEST_ISSUE_ATTRIBUTES, + ineligible_to_ineligible_issues: REQUEST_ISSUE_ATTRIBUTES + ) + end + # rubocop:enable Metrics/MethodLength +end diff --git a/app/models/claim_review.rb b/app/models/claim_review.rb index f25bfc609ac..ff8d3b0bfe0 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 + has_many :event_records, as: :evented_record with_options if: :saving_review do validate :validate_receipt_date validate :validate_veteran diff --git a/app/models/events/decision_review_updated_event.rb b/app/models/events/decision_review_updated_event.rb new file mode 100644 index 00000000000..8e56714837e --- /dev/null +++ b/app/models/events/decision_review_updated_event.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +# This class represents the DecisionReviewUpdatedEvent info that is POSTED to Caseflow +class DecisionReviewUpdatedEvent < Event +end diff --git a/app/models/events/event_record.rb b/app/models/events/event_record.rb index 3c21e4f5cff..901d42f6399 100644 --- a/app/models/events/event_record.rb +++ b/app/models/events/event_record.rb @@ -3,6 +3,7 @@ class EventRecord < CaseflowRecord belongs_to :event belongs_to :evented_record, polymorphic: true + store_accessor :info validate :valid_evented_record diff --git a/app/models/higher_level_review.rb b/app/models/higher_level_review.rb index 37f4a9d13ea..eb52a93ddb5 100644 --- a/app/models/higher_level_review.rb +++ b/app/models/higher_level_review.rb @@ -10,7 +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 + has_many :event_records, as: :evented_record attr_accessor :appeal_split_process diff --git a/app/models/legacy_issue.rb b/app/models/legacy_issue.rb index 3db514328ee..6e9065d2170 100644 --- a/app/models/legacy_issue.rb +++ b/app/models/legacy_issue.rb @@ -3,7 +3,7 @@ class LegacyIssue < CaseflowRecord belongs_to :request_issue has_one :legacy_issue_optin - has_one :event_record, as: :evented_record + has_many :event_records, as: :evented_record validates :request_issue, presence: true diff --git a/app/models/legacy_issue_optin.rb b/app/models/legacy_issue_optin.rb index 43e0d01e9c3..a29ed2d1d8b 100644 --- a/app/models/legacy_issue_optin.rb +++ b/app/models/legacy_issue_optin.rb @@ -3,7 +3,7 @@ class LegacyIssueOptin < CaseflowRecord belongs_to :request_issue belongs_to :legacy_issue - has_one :event_record, as: :evented_record + has_many :event_records, as: :evented_record VACOLS_DISPOSITION_CODE = "O" # oh not zero REMAND_DISPOSITION_CODES = %w[3 L].freeze diff --git a/app/models/request_issue.rb b/app/models/request_issue.rb index e42d2df3b1c..4c10e754ce6 100644 --- a/app/models/request_issue.rb +++ b/app/models/request_issue.rb @@ -38,7 +38,7 @@ class RequestIssue < CaseflowRecord has_one :legacy_issue_optin has_many :legacy_issues has_many :issue_modification_requests, dependent: :destroy - has_one :event_record, as: :evented_record + has_many :event_records, 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" @@ -62,7 +62,8 @@ class RequestIssue < CaseflowRecord appeal_to_higher_level_review: "appeal_to_higher_level_review", before_ama: "before_ama", legacy_issue_not_withdrawn: "legacy_issue_not_withdrawn", - legacy_appeal_not_eligible: "legacy_appeal_not_eligible" + legacy_appeal_not_eligible: "legacy_appeal_not_eligible", + contested: "contested" } enum closed_status: { @@ -212,7 +213,6 @@ def from_intake_data(data, decision_review: nil) def attributes_from_intake_data(data) contested_issue_present = attributes_look_like_contested_issue?(data) issue_text = (data[:is_unidentified] || data[:verified_unidentified_issue]) ? data[:decision_text] : nil - { contested_rating_issue_reference_id: data[:rating_issue_reference_id], contested_rating_issue_diagnostic_code: data[:rating_issue_diagnostic_code], @@ -243,7 +243,8 @@ def attributes_from_intake_data(data) mst_status_update_reason_notes: data[:mst_status_update_reason_notes], pact_status: data[:pact_status], vbms_pact_status: data[:vbms_pact_status], - pact_status_update_reason_notes: data[:pact_status_update_reason_notes] + pact_status_update_reason_notes: data[:pact_status_update_reason_notes], + reference_id: data[:reference_id] } end # rubocop:enable Metrics/MethodLength, Metrics/AbcSize @@ -264,7 +265,6 @@ def create_for_claim_review!(request_issues_update = nil) epe = decision_review.end_product_establishment_for_issue(self, request_issues_update) update!(end_product_establishment: epe) if epe - RequestIssueCorrectionCleaner.new(self).remove_dta_request_issue! if correction? handle_legacy_issues! end @@ -830,7 +830,7 @@ def remand_type end def title_of_active_review - duplicate_of_issue_in_active_review? ? ineligible_due_to.review_title : nil + duplicate_of_issue_in_active_review? ? ineligible_due_to&.review_title : nil end def handle_legacy_issues! diff --git a/app/models/request_issues_update_event.rb b/app/models/request_issues_update_event.rb new file mode 100644 index 00000000000..c80e3c6706c --- /dev/null +++ b/app/models/request_issues_update_event.rb @@ -0,0 +1,357 @@ +# frozen_string_literal: true + +# This class is used to update request issues for a decision review based on event data +# The event data is parsed into an object that is in the same format as an intake dataset from the UI +# The base class is used to update the request issues and calculate the before and after issues +# This class is used to update the request issues with the event data and process additional updates +# that are specific to event updates + +# Special logic is needed for exisiting issues that are not included in the event data +# but need to be part of the data sent to the base class to ensure +# the correct before and after issues are calculated +class RequestIssuesUpdateEvent < RequestIssuesUpdate + def initialize(review:, user:, parser:, event:) + @event = event + @parser = parser + @review = review + build_request_issues_data + super( + user: user, + review: review, + request_issues_data: @request_issues_data + ) + end + + def perform! + # Call the base class's perform! method + result = super + if result || !changes? + process_eligible_to_ineligible_issues! + process_ineligible_to_eligible_issues! + process_ineligible_to_ineligible_issues! + process_request_issues_data! + update_removed_issues! + process_audit_records! + true + else + false + end + end + + # This method may create more than one audit record for a single issue + # if the issue is updated in multiple ways such as a desctiption change + # and a withdrawal + def process_audit_records! + return true if all_updated_issues.empty? + + edited_issues.each do |request_issue| + add_event_record(request_issue, "E") + end + + added_issues.each do |request_issue| + add_event_record(request_issue, "A") + end + + removed_issues.each do |request_issue| + add_event_record(request_issue, "R") + end + + withdrawn_issues.each do |request_issue| + add_event_record(request_issue, "W") + end + end + + # Override the base class's process_job method to set the status to attempted and processed + def process_job + update!(last_submitted_at: @parser.end_product_establishment_last_synced_at) + update!(submitted_at: @parser.end_product_establishment_last_synced_at) + update!(attempted_at: @parser.end_product_establishment_last_synced_at) + update!(processed_at: @parser.end_product_establishment_last_synced_at) + end + + # Process aditional updates for all data that was passed to base class but not processed by it + # This impliments additional logic for event updates that was not added for intakes + def process_request_issues_data! + return true if after_issues.empty? + + all_updated_issues.each do |request_issue| + issue_data = + @request_issues_data.find { |data| data[:reference_id] == request_issue.reference_id } + + next if issue_data.nil? + + request_issue.update( + contested_issue_description: + issue_data[:contested_issue_description] || request_issue.contested_issue_description, + nonrating_issue_category: + issue_data[:nonrating_issue_category] || request_issue.nonrating_issue_category, + nonrating_issue_description: + issue_data[:nonrating_issue_description] || request_issue.nonrating_issue_description, + contention_updated_at: @parser.end_product_establishment_last_synced_at, + contention_reference_id: issue_data[:contention_reference_id] + ) + end + true + end + + # Set the closed_at date and closed_status for removed issues based on the event data + def update_removed_issues! + removed_issues.each do |request_issue| + issue_data = + @parser.removed_issues.find do |data| + parser_issue = Events::DecisionReviewUpdated::DecisionReviewUpdatedIssueParser.new(data) + parser_issue.ri_reference_id == request_issue.reference_id + end + + next if issue_data.nil? + + parser_issue = Events::DecisionReviewUpdated::DecisionReviewUpdatedIssueParser.new(issue_data) + request_issue.update( + closed_at: parser_issue.ri_closed_at, + closed_status: parser_issue.ri_closed_status, + contention_removed_at: @parser.end_product_establishment_last_synced_at, + contention_updated_at: @parser.end_product_establishment_last_synced_at + ) + end + true + end + + def process_eligible_to_ineligible_issues! + return if @parser.eligible_to_ineligible_issues.empty? + + @parser.eligible_to_ineligible_issues.each do |issue_data| + parser_issue = Events::DecisionReviewUpdated::DecisionReviewUpdatedIssueParser.new(issue_data) + request_issue = find_request_issue(parser_issue) + + request_issue.update( + ineligible_reason: parser_issue.ri_ineligible_reason, + closed_at: parser_issue.ri_closed_at, + contested_issue_description: parser_issue.ri_contested_issue_description || + request_issue.contested_issue_description, + nonrating_issue_category: parser_issue.ri_nonrating_issue_category || + request_issue.nonrating_issue_category, + nonrating_issue_description: parser_issue.ri_nonrating_issue_description || + request_issue.nonrating_issue_description, + contention_removed_at: @parser.end_product_establishment_last_synced_at, + contention_updated_at: @parser.end_product_establishment_last_synced_at, + contention_reference_id: parser_issue.ri_contention_reference_id + ) + add_event_record(request_issue, "E2I") + end + end + + def process_ineligible_to_eligible_issues! + return if @parser.ineligible_to_eligible_issues.empty? + + @parser.ineligible_to_eligible_issues.each do |issue_data| + parser_issue = Events::DecisionReviewUpdated::DecisionReviewUpdatedIssueParser.new(issue_data) + request_issue = find_request_issue(parser_issue) + request_issue.update( + ineligible_reason: nil, + closed_status: nil, + closed_at: nil, + contention_reference_id: parser_issue.ri_contention_reference_id, + contention_removed_at: nil, + contested_issue_description: parser_issue.ri_contested_issue_description || + request_issue.contested_issue_description, + nonrating_issue_category: parser_issue.ri_nonrating_issue_category || + request_issue.nonrating_issue_category, + nonrating_issue_description: parser_issue.ri_nonrating_issue_description || + request_issue.nonrating_issue_description, + contention_updated_at: @parser.end_product_establishment_last_synced_at + ) + add_event_record(request_issue, "I2E") + end + end + + def process_ineligible_to_ineligible_issues! + return if @parser.ineligible_to_ineligible_issues.empty? + + @parser.ineligible_to_ineligible_issues.each do |issue_data| + parser_issue = Events::DecisionReviewUpdated::DecisionReviewUpdatedIssueParser.new(issue_data) + request_issue = find_request_issue(parser_issue) + + request_issue.update( + ineligible_reason: parser_issue.ri_ineligible_reason, + closed_at: parser_issue.ri_closed_at, + contested_issue_description: parser_issue.ri_contested_issue_description || + request_issue.contested_issue_description, + nonrating_issue_category: parser_issue.ri_nonrating_issue_category || + request_issue.nonrating_issue_category, + nonrating_issue_description: parser_issue.ri_nonrating_issue_description || + request_issue.nonrating_issue_description, + contention_removed_at: @parser.end_product_establishment_last_synced_at, + contention_updated_at: @parser.end_product_establishment_last_synced_at, + contention_reference_id: parser_issue.ri_contention_reference_id + ) + add_event_record(request_issue, "I2I") + end + end + + # Assymble the request issues data in the format expected by the base class + def build_request_issues_data + @request_issues_data = [] + + # Add updated issues + @parser.updated_issues.each do |issue| + parser_issue = Events::DecisionReviewUpdated::DecisionReviewUpdatedIssueParser.new(issue) + @request_issues_data << build_issue_data(parser_issue: parser_issue) + end + + # Add withdrawn issues + @parser.withdrawn_issues.each do |issue| + parser_issue = Events::DecisionReviewUpdated::DecisionReviewUpdatedIssueParser.new(issue) + @request_issues_data << build_issue_data(parser_issue: parser_issue, is_withdrawn: true) + end + + # Add added issues + @parser.added_issues.each do |issue| + parser_issue = Events::DecisionReviewUpdated::DecisionReviewUpdatedIssueParser.new(issue) + @request_issues_data << build_issue_data(parser_issue: parser_issue, is_new: true) + end + + # This is to ensure all request issues associated with the review are included in the after_issues + add_existing_review_issues + + @request_issues_data + end + + # Add all review issue ids and reference ids not included in the request_issues_data + # but not included in the removed_issues + # Note that removed issues are not included in the request_issues_data + # This is to ensure all removed issues are derived from the (before - after) comparison in base class + def add_existing_review_issues + @review.request_issues.each do |request_issue| + # Skip if the request issue is already in the request_issues_data + next if @request_issues_data.find do |data| + data[:reference_id] == request_issue.reference_id || + data[:original_caseflow_request_issue_id] == request_issue.id + end + + # Skip if the request issue is in the removed_issues + next if @parser.removed_issues.find do |data| + parser_issue = Events::DecisionReviewUpdated::DecisionReviewUpdatedIssueParser.new(data) + parser_issue.ri_reference_id == request_issue.reference_id || + parser_issue.ri_original_caseflow_request_issue_id == request_issue.id + end + + # Only add the reference_id and request_issue_id to the request_issues_data + # The base class does not need to update existing issues not included in event data + @request_issues_data << { + request_issue_id: request_issue.id, + reference_id: request_issue.reference_id + } + end + @request_issues_data + end + + # Orveride removed_issues to explicity set the removed issues received from the event + # Thechnically these issues are calculated by the base class, but this is more direct + def removed_issues + @parser.removed_issues.map do |issue| + parser_issue = Events::DecisionReviewUpdated::DecisionReviewUpdatedIssueParser.new(issue) + find_request_issue(parser_issue) + end + end + + # Build the request issue data in the format expected by the base class + # This is for event updates to follow the same logic as an intake that created from the UI + def build_issue_data(parser_issue:, is_withdrawn: false, is_new: false) + return {} if parser_issue.nil? + + issue_data = base_issue_data(parser_issue) + issue_data.merge!(contested_issue_data(parser_issue)) + issue_data.merge!(nonrating_issue_data(parser_issue)) + issue_data.merge!(conditional_issue_data(parser_issue, is_withdrawn, is_new)) + issue_data + end + + # Add base issue data to the issue data + def base_issue_data(parser_issue) + { + benefit_type: parser_issue.ri_benefit_type, + closed_date: parser_issue.ri_closed_at, + closed_status: parser_issue.ri_closed_status, + unidentified_issue_text: parser_issue.ri_unidentified_issue_text, + issue_text: parser_issue.ri_unidentified_issue_text, + decision_date: parser_issue.ri_decision_date, + is_unidentified: parser_issue.ri_is_unidentified, + untimely_exemption: parser_issue.ri_untimely_exemption, + untimely_exemption_notes: parser_issue.ri_untimely_exemption_notes, + ramp_claim_id: parser_issue.ri_ramp_claim_id, + vacols_id: parser_issue.ri_vacols_id, + vacols_sequence_id: parser_issue.ri_vacols_sequence_id, + ineligible_reason: parser_issue.ri_ineligible_reason, + ineligible_due_to_id: parser_issue.ri_ineligible_due_to_id, + reference_id: parser_issue.ri_reference_id, + type: parser_issue.ri_type, + rating_issue_associated_at: parser_issue.ri_rating_issue_associated_at, + edited_description: parser_issue.ri_edited_description + } + end + + # Add the request issue id to the issue data if it is a new issue + # Add addtional fields that are userd buy the base class to identify the type of update + def conditional_issue_data(parser_issue, is_withdrawn, is_new) + { + request_issue_id: is_new ? nil : find_request_issue(parser_issue).id, + withdrawal_date: is_withdrawn ? parser_issue.ri_closed_at : nil, + decision_text: parser_issue.ri_nonrating_issue_description, + end_product_code: @parser.end_product_establishment_code + } + end + + # Add nonrating issue data to the issue data + def nonrating_issue_data(parser_issue) + { + nonrating_issue_category: parser_issue.ri_nonrating_issue_category, + nonrating_issue_description: parser_issue.ri_nonrating_issue_description, + nonrating_issue_bgs_source: parser_issue.ri_nonrating_issue_bgs_source, + nonrating_issue_bgs_id: parser_issue.ri_nonrating_issue_bgs_id + } + end + + # Add contested issue data to the issue data + def contested_issue_data(parser_issue) + { + contention_reference_id: parser_issue.ri_contention_reference_id, + contested_decision_issue_id: parser_issue.ri_contested_decision_issue_id, + contested_rating_issue_reference_id: parser_issue.ri_contested_rating_issue_reference_id, + contested_rating_issue_diagnostic_code: parser_issue.ri_contested_rating_issue_diagnostic_code, + contested_rating_decision_reference_id: parser_issue.ri_contested_rating_decision_reference_id, + contested_issue_description: parser_issue.ri_contested_issue_description, + contested_rating_issue_profile_date: parser_issue.ri_contested_rating_issue_profile_date + } + end + + def find_request_issue(parser_issue) + request_issue = RequestIssue.find_by(reference_id: parser_issue.ri_reference_id) + + if request_issue.nil? + original_request_issue = RequestIssue.find_by(id: parser_issue.ri_original_caseflow_request_issue_id) + + if original_request_issue + original_request_issue.update!(reference_id: parser_issue.ri_reference_id) + request_issue = original_request_issue + end + end + + if request_issue.nil? + fail( + Caseflow::Error::DecisionReviewUpdateMissingIssueError, + "Reference ID: #{parser_issue.ri_reference_id}, " \ + "Original Reference ID: #{parser_issue.ri_original_caseflow_request_issue_id}" + ) + end + + request_issue + end + + def add_event_record(request_issue, update_type) + EventRecord.create!( + event: @event, + evented_record: request_issue, + info: { update_type: update_type, record_data: request_issue } + ) + end +end diff --git a/app/models/supplemental_claim.rb b/app/models/supplemental_claim.rb index ab9312d8758..44360d2a3d9 100644 --- a/app/models/supplemental_claim.rb +++ b/app/models/supplemental_claim.rb @@ -4,7 +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 + has_many :event_records, as: :evented_record scope :updated_since_for_appeals, lambda { |since| select(:decision_review_remanded_id) 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 6a931938b72..61047c32676 100644 --- a/app/serializers/api/v3/issues/ama/request_issue_serializer.rb +++ b/app/serializers/api/v3/issues/ama/request_issue_serializer.rb @@ -25,7 +25,7 @@ class Api::V3::Issues::Ama::RequestIssueSerializer :nonrating_issue_bgs_source, :nonrating_issue_description, :notes, :ramp_claim_id, :split_issue_status, :unidentified_issue_text, :untimely_exemption, :untimely_exemption_notes, :updated_at, :vacols_id, - :vacols_sequence_id, :verified_unidentified_issue, :veteran_participant_id + :vacols_sequence_id, :verified_unidentified_issue, :veteran_participant_id, :reference_id attribute :caseflow_considers_decision_review_active, &:status_active? attribute :caseflow_considers_issue_active, &:active? @@ -33,7 +33,7 @@ class Api::V3::Issues::Ama::RequestIssueSerializer attribute :caseflow_considers_eligible, &:eligible? attribute :claimant_participant_id do |object| - object.decision_review.claimant.participant_id + object&.decision_review&.claimant&.participant_id end attribute :claim_id do |object| diff --git a/app/services/events/decision_review_created.rb b/app/services/events/decision_review_created.rb index d5f9c665391..e8e37f7c6d1 100644 --- a/app/services/events/decision_review_created.rb +++ b/app/services/events/decision_review_created.rb @@ -12,25 +12,25 @@ class Events::DecisionReviewCreated # # with the one who held the lock and failed to unlock. (default: 10) class << self - # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Lint/UselessAssignment + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength def create!(params, headers, payload) consumer_event_id = params[:consumer_event_id] - reference_id = params[:reference_id] + claim_id = params[:claim_id] 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}") + if redis.exists("RedisMutex:EndProductEstablishment:#{claim_id}") fail Caseflow::Error::RedisLockFailed, - message: "Key RedisMutex:EndProductEstablishment:#{reference_id} is already in the Redis Cache" + message: "Key RedisMutex:EndProductEstablishment:#{claim_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) + # 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) + RedisMutex.with_lock("EndProductEstablishment:#{claim_id}", block: 60, expire: 100) do ActiveRecord::Base.transaction do # Initialize the Parser object that will be passed around as an argument parser = Events::DecisionReviewCreated::DecisionReviewCreatedParser.new(headers, payload) @@ -72,24 +72,30 @@ def create!(params, headers, payload) Events::DecisionReviewCreated::UpdateVacolsOnOptin.process!(decision_review: decision_review) # Update the Event after all backfills have completed - event.update!(completed_at: Time.now.in_time_zone, error: nil, info: {}) + event.update!(completed_at: Time.now.in_time_zone, error: nil, info: { "event_payload" => payload }) 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) + Rails.logger.error("Key RedisMutex:EndProductEstablishment:#{claim_id} is already in the Redis Cache") 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"\ + Rails.logger.error("Failed to acquire lock for Claim ID: #{claim_id}! This Event is being"\ " processed. Please try again later.") + event&.update!(error: error.message) + raise error 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 }) + event&.update!(error: "#{error.class} : #{error.message}", info: + { + "failed_claim_id" => claim_id, + "error" => error.message, + "error_class" => error.class.name, + "error_backtrace" => error.backtrace + }) raise error end - # rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Lint/UselessAssignment + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength # 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 diff --git a/app/services/events/decision_review_created/create_request_issues.rb b/app/services/events/decision_review_created/create_request_issues.rb index 427677ff511..d3ecbb6389c 100644 --- a/app/services/events/decision_review_created/create_request_issues.rb +++ b/app/services/events/decision_review_created/create_request_issues.rb @@ -24,8 +24,14 @@ def create_request_issue_backfill(params) request_issues&.each do |issue| # create backfill RI object using extracted values - parser_issues = DecisionReviewCreatedIssueParser.new(issue) + parser_issues = Events::DecisionReviewCreated::DecisionReviewCreatedIssueParser.new(issue) + + if parser_issues.ri_reference_id.nil? + fail Caseflow::Error::DecisionReviewCreatedRequestIssuesError, "reference_id cannot be null" + end + ri = RequestIssue.create!( + reference_id: parser_issues.ri_reference_id, benefit_type: parser_issues.ri_benefit_type, contested_issue_description: parser_issues.ri_contested_issue_description, contention_reference_id: parser_issues.ri_contention_reference_id, @@ -73,7 +79,11 @@ def create_request_issue_backfill(params) # rubocop:enable Metrics/MethodLength, Metrics/AbcSize def create_event_record(event, issue) - EventRecord.create!(event: event, evented_record: issue) + EventRecord.create!( + event: event, + evented_record: issue, + info: { update_type: "I", record_data: issue } + ) end # Legacy issue checks 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 index b1e9092a472..fb965e12971 100644 --- a/app/services/events/decision_review_created/decision_review_created_example.json +++ b/app/services/events/decision_review_created/decision_review_created_example.json @@ -54,8 +54,9 @@ }, "request_issues": [ { + "decision_review_issue_id": "1", "benefit_type": "compensation", - "contested_issue_description": "some contested description", + "contested_issue_description": null, "contention_reference_id": 7905752, "contested_rating_decision_reference_id": null, "contested_rating_issue_profile_date": null, @@ -82,5 +83,3 @@ } ] } - - diff --git a/app/services/decision_review_created_issue_parser.rb b/app/services/events/decision_review_created/decision_review_created_issue_parser.rb similarity index 64% rename from app/services/decision_review_created_issue_parser.rb rename to app/services/events/decision_review_created/decision_review_created_issue_parser.rb index 09cfb3e31cd..40a454cc478 100644 --- a/app/services/decision_review_created_issue_parser.rb +++ b/app/services/events/decision_review_created/decision_review_created_issue_parser.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class DecisionReviewCreatedIssueParser +class Events::DecisionReviewCreated::DecisionReviewCreatedIssueParser include ParserHelper attr_reader :issue @@ -8,12 +8,16 @@ def initialize(issue) @issue = issue end + def ri_reference_id + @issue.dig(:decision_review_issue_id) + end + def ri_benefit_type - @issue.dig(:benefit_type) + @issue.dig(:benefit_type).presence end def ri_contested_issue_description - @issue.dig(:contested_issue_description) + @issue.dig(:contested_issue_description).presence end def ri_contention_reference_id @@ -21,15 +25,15 @@ def ri_contention_reference_id end def ri_contested_rating_decision_reference_id - @issue.dig(:contested_rating_decision_reference_id) + @issue.dig(:contested_rating_decision_reference_id).presence end def ri_contested_rating_issue_profile_date - @issue.dig(:contested_rating_issue_profile_date) + @issue.dig(:contested_rating_issue_profile_date).presence end def ri_contested_rating_issue_reference_id - @issue.dig(:contested_rating_issue_reference_id) + @issue.dig(:contested_rating_issue_reference_id).presence end def ri_contested_decision_issue_id @@ -46,7 +50,7 @@ def ri_ineligible_due_to_id end def ri_ineligible_reason - @issue.dig(:ineligible_reason) + @issue.dig(:ineligible_reason).presence end def ri_is_unidentified @@ -54,7 +58,7 @@ def ri_is_unidentified end def ri_unidentified_issue_text - @issue.dig(:unidentified_issue_text) + @issue.dig(:unidentified_issue_text).presence end def ri_nonrating_issue_category @@ -62,7 +66,7 @@ def ri_nonrating_issue_category end def ri_nonrating_issue_description - @issue.dig(:nonrating_issue_description) + @issue.dig(:nonrating_issue_description).presence end def ri_untimely_exemption @@ -70,11 +74,11 @@ def ri_untimely_exemption end def ri_untimely_exemption_notes - @issue.dig(:untimely_exemption_notes) + @issue.dig(:untimely_exemption_notes).presence end def ri_vacols_id - @issue.dig(:vacols_id) + @issue.dig(:vacols_id).presence end def ri_vacols_sequence_id @@ -82,11 +86,12 @@ def ri_vacols_sequence_id end def ri_closed_at - @issue.dig(:closed_at) + ri_closed_at_in_ms = @issue.dig(:closed_at) + convert_milliseconds_to_datetime(ri_closed_at_in_ms) end def ri_closed_status - @issue.dig(:closed_status) + @issue.dig(:closed_status).presence end def ri_contested_rating_issue_diagnostic_code @@ -94,7 +99,7 @@ def ri_contested_rating_issue_diagnostic_code end def ri_ramp_claim_id - @issue.dig(:ramp_claim_id) + @issue.dig(:ramp_claim_id).presence end def ri_rating_issue_associated_at @@ -103,10 +108,10 @@ def ri_rating_issue_associated_at end def ri_nonrating_issue_bgs_id - @issue.dig(:nonrating_issue_bgs_id) + @issue.dig(:nonrating_issue_bgs_id).presence end def ri_nonrating_issue_bgs_source - @issue.dig(:nonrating_issue_bgs_source) + @issue.dig(:nonrating_issue_bgs_source).presence end end 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 index 8bf110ab982..5c2cd1d8b95 100644 --- a/app/services/events/decision_review_created/decision_review_created_parser.rb +++ b/app/services/events/decision_review_created/decision_review_created_parser.rb @@ -41,23 +41,23 @@ def initialize(headers, payload_json) end def css_id - @payload.dig(:css_id) + @payload.dig(:css_id).presence end def detail_type - @payload.dig(:detail_type) + @payload.dig(:detail_type).presence end def station_id - @payload.dig(:station) + @payload.dig(:station).presence end def event_id - @payload.dig(:event_id) + @payload.dig(:event_id).presence end def claim_id - @payload.dig(:claim_id) + @payload.dig(:claim_id).presence end # Intake attributes @@ -81,15 +81,15 @@ def intake_completed_at end def intake_completion_status - @payload.dig(:intake, :completion_status) + @payload.dig(:intake, :completion_status).presence end def intake_type - @payload.dig(:intake, :type) + @payload.dig(:intake, :type).presence end def intake_detail_type - @payload.dig(:intake, :detail_type) + @payload.dig(:intake, :detail_type).presence end # Veteran attributes @@ -143,7 +143,7 @@ def person_ssn end def veteran_participant_id - @payload.dig(:veteran, :participant_id) + @payload.dig(:veteran, :participant_id).presence end def veteran_bgs_last_synced_at @@ -152,7 +152,7 @@ def veteran_bgs_last_synced_at end def veteran_name_suffix - @payload.dig(:veteran, :name_suffix) + @payload.dig(:veteran, :name_suffix).presence end def veteran_date_of_death @@ -166,19 +166,19 @@ def claimant end def claimant_payee_code - @payload.dig(:claimant, :payee_code) + @payload.dig(:claimant, :payee_code).presence end def claimant_type - @payload.dig(:claimant, :type) + @payload.dig(:claimant, :type).presence end def claimant_participant_id - @payload.dig(:claimant, :participant_id) + @payload.dig(:claimant, :participant_id).presence end def claimant_name_suffix - @payload.dig(:claimant, :name_suffix) + @payload.dig(:claimant, :name_suffix).presence end # ClaimReview attributes @@ -187,7 +187,7 @@ def claim_review end def claim_review_benefit_type - @payload.dig(:claim_review, :benefit_type) + @payload.dig(:claim_review, :benefit_type).presence end def claim_review_filed_by_va_gov @@ -241,7 +241,7 @@ def epe end def epe_benefit_type_code - @payload.dig(:end_product_establishment, :benefit_type_code) + @payload.dig(:end_product_establishment, :benefit_type_code).presence end def epe_claim_date @@ -250,27 +250,27 @@ def epe_claim_date end def epe_code - @payload.dig(:end_product_establishment, :code) + @payload.dig(:end_product_establishment, :code).presence end def epe_modifier - @payload.dig(:end_product_establishment, :modifier) + @payload.dig(:end_product_establishment, :modifier).presence end def epe_payee_code - @payload.dig(:end_product_establishment, :payee_code) + @payload.dig(:end_product_establishment, :payee_code).presence end def epe_reference_id - @payload.dig(:end_product_establishment, :reference_id) + @payload.dig(:end_product_establishment, :reference_id).presence end def epe_limited_poa_access - @payload.dig(:end_product_establishment, :limited_poa_access) + @payload.dig(:end_product_establishment, :limited_poa_access).presence end def epe_limited_poa_code - @payload.dig(:end_product_establishment, :limited_poa_code) + @payload.dig(:end_product_establishment, :limited_poa_code).presence end def epe_committed_at @@ -289,11 +289,11 @@ def epe_last_synced_at end def epe_synced_status - @payload.dig(:end_product_establishment, :synced_status) + @payload.dig(:end_product_establishment, :synced_status).presence end def epe_development_item_reference_id - @payload.dig(:end_product_establishment, :development_item_reference_id) + @payload.dig(:end_product_establishment, :development_item_reference_id).presence end # RequestIssues attr diff --git a/app/services/events/decision_review_updated.rb b/app/services/events/decision_review_updated.rb new file mode 100644 index 00000000000..212902f8a1b --- /dev/null +++ b/app/services/events/decision_review_updated.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +class Events::DecisionReviewUpdated + include RedisMutex::Macro + class << self + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + def update!(params, headers, payload) + consumer_event_id = params[:consumer_event_id] + claim_id = params[:claim_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:#{claim_id}") + fail Caseflow::Error::RedisLockFailed, + message: "Key RedisMutex:EndProductEstablishment:#{claim_id} is already in the Redis Cache" + end + + # key => "EndProductEstablishment:reference_id" aka "claim ID" + # Use the consumer_event_id to retrieve/create the Event object + event = DecisionReviewUpdatedEvent.find_or_create_by(reference_id: consumer_event_id) + + RedisMutex.with_lock("EndProductEstablishment:#{claim_id}", block: 60, expire: 100) do + ActiveRecord::Base.transaction do + parser = Events::DecisionReviewUpdated::DecisionReviewUpdatedParser.new(headers, payload) + + user = Events::CreateUserOnEvent.handle_user_creation_on_event( + event: event, css_id: parser.css_id, + station_id: parser.station_id + ) + + review = EndProductEstablishment.find_by( + reference_id: parser.end_product_establishment_reference_id + )&.source + + Events::DecisionReviewUpdated::UpdateInformalConference.process!(event: event, parser: parser) + Events::DecisionReviewUpdated::UpdateClaimReview.process!(event: event, parser: parser) + Events::DecisionReviewUpdated::UpdateEndProductEstablishment.process!(event: event, parser: parser) + RequestIssuesUpdateEvent.new(user: user, review: review, parser: parser, event: event).perform! + # Update the Event after all operations have completed + event.update!(completed_at: Time.now.in_time_zone, error: nil, info: { "event_payload" => payload }) + end + end + rescue Caseflow::Error::RedisLockFailed => error + Rails.logger.error("Key RedisMutex:EndProductEstablishment:#{params[:claim_id]} is already in the Redis Cache") + event&.update!(error: error.message) + raise error + rescue RedisMutex::LockError => error + Rails.logger.error("Failed to acquire lock for Claim ID: #{params[:claim_id]}! This Event is being"\ + " processed. Please try again later.") + raise error + rescue StandardError => error + Rails.logger.error("#{error.class} : #{error.message}") + event&.update!(error: "#{error.class} : #{error.message}", info: + { + "failed_claim_id" => params[:claim_id], + "error" => error.message, + "error_class" => error.class.name, + "error_backtrace" => error.backtrace, + "event_payload" => payload + }) + raise error + end + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength + end +end diff --git a/app/services/events/decision_review_updated/decision_review_updated_example.json b/app/services/events/decision_review_updated/decision_review_updated_example.json new file mode 100644 index 00000000000..3a37c3bf249 --- /dev/null +++ b/app/services/events/decision_review_updated/decision_review_updated_example.json @@ -0,0 +1,250 @@ +{ + "event_id": 214706, + "claim_id": 1234567, + "css_id": "BVADWISE101", + "detail_type": "HigherLevelReview", + "station": "101", + "claim_review": { + "informal_conference": false, + "same_office": false, + "legacy_opt_in_approved": false + }, + "end_product_establishment": { + "code": "030HLRR", + "development_item_reference_id": "1", + "reference_id": "1234567", + "synced_status": "RFD", + "last_synced_at": 1726688419000 + }, + "added_issues": [ + { + "original_caseflow_request_issue_id": 12345, + "contested_rating_decision_reference_id": null, + "contested_rating_issue_reference_id": null, + "contested_decision_issue_id": null, + "untimely_exemption": false, + "untimely_exemption_notes": null, + "edited_description": "DIC: Service connection denied (UPDATED)", + "vacols_id": null, + "vacols_sequence_id": null, + "nonrating_issue_bgs_id": null, + "type": "RequestIssue", + "decision_review_issue_id": 77237, + "contention_reference_id": 123456, + "benefit_type": "compensation", + "contested_issue_description": null, + "contested_rating_issue_profile_date": null, + "decision_date": null, + "ineligible_due_to_id": null, + "ineligible_reason": null, + "unidentified_issue_text": "An unidentified issue added during the edit", + "nonrating_issue_category": null, + "nonrating_issue_description": null, + "closed_at": null, + "closed_status": null, + "contested_rating_issue_diagnostic_code": null, + "rating_issue_associated_at": null, + "ramp_claim_id": null, + "is_unidentified": true, + "nonrating_issue_bgs_source": null + } + ], + "updated_issues": [ + { + "original_caseflow_request_issue_id": 12345, + "contested_rating_decision_reference_id": null, + "contested_rating_issue_reference_id": null, + "contested_decision_issue_id": null, + "untimely_exemption": false, + "untimely_exemption_notes": null, + "edited_description": "DIC: Service connection denied (UPDATED)", + "vacols_id": null, + "vacols_sequence_id": null, + "nonrating_issue_bgs_id": null, + "type": "RequestIssue", + "decision_review_issue_id": 908, + "contention_reference_id": 123456, + "benefit_type": "compensation", + "contested_issue_description": null, + "contested_rating_issue_profile_date": null, + "decision_date": null, + "ineligible_due_to_id": null, + "ineligible_reason": null, + "unidentified_issue_text": "An unidentified issue added during the edit", + "nonrating_issue_category": null, + "nonrating_issue_description": null, + "closed_at": null, + "closed_status": null, + "contested_rating_issue_diagnostic_code": null, + "rating_issue_associated_at": null, + "ramp_claim_id": null, + "is_unidentified": true, + "nonrating_issue_bgs_source": null + } + ], + "removed_issues": [ + { + "original_caseflow_request_issue_id": 12345, + "contested_rating_decision_reference_id": null, + "contested_rating_issue_reference_id": null, + "contested_decision_issue_id": null, + "untimely_exemption": false, + "untimely_exemption_notes": null, + "edited_description": "DIC: Service connection denied (UPDATED)", + "vacols_id": null, + "vacols_sequence_id": null, + "nonrating_issue_bgs_id": null, + "type": "RequestIssue", + "decision_review_issue_id": 8755, + "contention_reference_id": 123456, + "benefit_type": "compensation", + "contested_issue_description": null, + "contested_rating_issue_profile_date": null, + "decision_date": null, + "ineligible_due_to_id": null, + "ineligible_reason": null, + "unidentified_issue_text": "An unidentified issue added during the edit", + "nonrating_issue_category": null, + "nonrating_issue_description": null, + "closed_at": null, + "closed_status": null, + "contested_rating_issue_diagnostic_code": null, + "rating_issue_associated_at": null, + "ramp_claim_id": null, + "is_unidentified": true, + "nonrating_issue_bgs_source": null + } + ], + "withdrawn_issues": [ + { + "original_caseflow_request_issue_id": 12345, + "contested_rating_decision_reference_id": null, + "contested_rating_issue_reference_id": null, + "contested_decision_issue_id": null, + "untimely_exemption": false, + "untimely_exemption_notes": null, + "edited_description": "DIC: Service connection denied (UPDATED)", + "vacols_id": null, + "vacols_sequence_id": null, + "nonrating_issue_bgs_id": null, + "type": "RequestIssue", + "decision_review_issue_id": 9876, + "contention_reference_id": 123456, + "benefit_type": "compensation", + "contested_issue_description": null, + "contested_rating_issue_profile_date": null, + "decision_date": null, + "ineligible_due_to_id": null, + "ineligible_reason": null, + "unidentified_issue_text": "An unidentified issue added during the edit", + "nonrating_issue_category": null, + "nonrating_issue_description": null, + "closed_at": null, + "closed_status": null, + "contested_rating_issue_diagnostic_code": null, + "rating_issue_associated_at": null, + "ramp_claim_id": null, + "is_unidentified": true, + "nonrating_issue_bgs_source": null + } + ], + "ineligible_to_eligible_issues": [ + { + "original_caseflow_request_issue_id": 12345, + "contested_rating_decision_reference_id": null, + "contested_rating_issue_reference_id": null, + "contested_decision_issue_id": null, + "untimely_exemption": false, + "untimely_exemption_notes": null, + "edited_description": "DIC: Service connection denied (UPDATED)", + "vacols_id": null, + "vacols_sequence_id": null, + "nonrating_issue_bgs_id": null, + "type": "RequestIssue", + "decision_review_issue_id": 876, + "contention_reference_id": 123456, + "benefit_type": "compensation", + "contested_issue_description": null, + "contested_rating_issue_profile_date": null, + "decision_date": null, + "ineligible_due_to_id": null, + "ineligible_reason": null, + "unidentified_issue_text": "An unidentified issue added during the edit", + "nonrating_issue_category": null, + "nonrating_issue_description": null, + "closed_at": null, + "closed_status": null, + "contested_rating_issue_diagnostic_code": null, + "rating_issue_associated_at": null, + "ramp_claim_id": null, + "is_unidentified": true, + "nonrating_issue_bgs_source": null + } + ], + "eligible_to_ineligible_issues": [ + { + "original_caseflow_request_issue_id": 12345, + "contested_rating_decision_reference_id": null, + "contested_rating_issue_reference_id": null, + "contested_decision_issue_id": null, + "untimely_exemption": false, + "untimely_exemption_notes": null, + "edited_description": "DIC: Service connection denied (UPDATED)", + "vacols_id": null, + "vacols_sequence_id": null, + "nonrating_issue_bgs_id": null, + "type": "RequestIssue", + "decision_review_issue_id": 123, + "contention_reference_id": 123456, + "benefit_type": "compensation", + "contested_issue_description": null, + "contested_rating_issue_profile_date": null, + "decision_date": null, + "ineligible_due_to_id": null, + "ineligible_reason": null, + "unidentified_issue_text": "An unidentified issue added during the edit", + "nonrating_issue_category": null, + "nonrating_issue_description": null, + "closed_at": null, + "closed_status": null, + "contested_rating_issue_diagnostic_code": null, + "rating_issue_associated_at": null, + "ramp_claim_id": null, + "is_unidentified": true, + "nonrating_issue_bgs_source": null + } + ], + "ineligible_to_ineligible_issues": [ + { + "original_caseflow_request_issue_id": 12345, + "contested_rating_decision_reference_id": null, + "contested_rating_issue_reference_id": null, + "contested_decision_issue_id": null, + "untimely_exemption": false, + "untimely_exemption_notes": null, + "edited_description": "DIC: Service connection denied (UPDATED)", + "vacols_id": null, + "vacols_sequence_id": null, + "nonrating_issue_bgs_id": null, + "type": "RequestIssue", + "decision_review_issue_id": 234, + "contention_reference_id": 123456, + "benefit_type": "compensation", + "contested_issue_description": null, + "contested_rating_issue_profile_date": null, + "decision_date": null, + "ineligible_due_to_id": null, + "ineligible_reason": null, + "unidentified_issue_text": "An unidentified issue added during the edit", + "nonrating_issue_category": null, + "nonrating_issue_description": null, + "closed_at": null, + "closed_status": null, + "contested_rating_issue_diagnostic_code": null, + "rating_issue_associated_at": null, + "ramp_claim_id": null, + "is_unidentified": true, + "nonrating_issue_bgs_source": null + } + ] +} diff --git a/app/services/events/decision_review_updated/decision_review_updated_issue_parser.rb b/app/services/events/decision_review_updated/decision_review_updated_issue_parser.rb new file mode 100644 index 00000000000..44d7c617d20 --- /dev/null +++ b/app/services/events/decision_review_updated/decision_review_updated_issue_parser.rb @@ -0,0 +1,131 @@ +# frozen_string_literal: true + +class Events::DecisionReviewUpdated::DecisionReviewUpdatedIssueParser + include ParserHelper + attr_reader :issue + + def initialize(issue) + @issue = issue + end + + # this is new reference_id column added to requestIssues table, come from appeals-consumer as decision_review_issue_id + def ri_reference_id + @issue.dig(:decision_review_issue_id) + end + + def ri_benefit_type + @issue.dig(:benefit_type).presence + end + + def ri_closed_at + closed_at_in_ms = @issue.dig(:closed_at) + convert_milliseconds_to_datetime(closed_at_in_ms) + end + + def ri_closed_status + @issue.dig(:closed_status) + end + + def ri_contested_issue_description + @issue.dig(:contested_issue_description).presence + end + + def ri_contention_reference_id + @issue.dig(:contention_reference_id) + end + + def ri_contested_rating_issue_diagnostic_code + @issue.dig(:contested_rating_issue_diagnostic_code).presence + end + + def ri_contested_rating_decision_reference_id + @issue.dig(:contested_rating_decision_reference_id).presence + end + + def ri_contested_rating_issue_profile_date + @issue.dig(:contested_rating_issue_profile_date).presence + end + + def ri_contested_rating_issue_reference_id + @issue.dig(:contested_rating_issue_reference_id).presence + end + + def ri_contested_decision_issue_id + @issue.dig(:contested_decision_issue_id) + end + + # probably we have the wrong type of passed decision_date in json eample, needs to be clarified + def ri_decision_date + decision_date_int = @issue.dig(:decision_date) + logical_date_converter(decision_date_int) + end + + def ri_edited_description + @issue.dig(:edited_description) + end + + def ri_ineligible_due_to_id + @issue.dig(:ineligible_due_to_id) + end + + def ri_ineligible_reason + @issue.dig(:ineligible_reason).presence + end + + def ri_is_unidentified + @issue.dig(:is_unidentified) + end + + def ri_unidentified_issue_text + @issue.dig(:unidentified_issue_text).presence + end + + def ri_nonrating_issue_category + @issue.dig(:nonrating_issue_category).presence + end + + def ri_nonrating_issue_description + @issue.dig(:nonrating_issue_description).presence + end + + def ri_nonrating_issue_bgs_id + @issue.dig(:nonrating_issue_bgs_id).presence + end + + def ri_nonrating_issue_bgs_source + @issue.dig(:nonrating_issue_bgs_source).presence + end + + def ri_ramp_claim_id + @issue.dig(:ramp_claim_id).presence + end + + def ri_rating_issue_associated_at + ri_rating_issue_associated_at_in_ms = @issue.dig(:rating_issue_associated_at) + convert_milliseconds_to_datetime(ri_rating_issue_associated_at_in_ms) + end + + def ri_untimely_exemption + @issue.dig(:untimely_exemption) + end + + def ri_untimely_exemption_notes + @issue.dig(:untimely_exemption_notes).presence + end + + def ri_vacols_id + @issue.dig(:vacols_id).presence + end + + def ri_vacols_sequence_id + @issue.dig(:vacols_sequence_id) + end + + def ri_type + @issue.dig(:type).presence + end + + def ri_original_caseflow_request_issue_id + @issue.dig(:original_caseflow_request_issue_id) + end +end diff --git a/app/services/events/decision_review_updated/decision_review_updated_parser.rb b/app/services/events/decision_review_updated/decision_review_updated_parser.rb new file mode 100644 index 00000000000..ea0f832f025 --- /dev/null +++ b/app/services/events/decision_review_updated/decision_review_updated_parser.rb @@ -0,0 +1,213 @@ +# frozen_string_literal: true + +# Parser Class that will be used to extract out datapoints from headers & payload for use with +# DecisionReviewUpdated and it's service Classes +class Events::DecisionReviewUpdated::DecisionReviewUpdatedParser + include Events::VeteranExtractorInterface + include ParserHelper + + 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_updated", + "decision_review_updated_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 + end + + def event_id + @payload[:event_id] + end + + def claim_id + @payload[:claim_id] + end + + def claim_creation_time + @payload[:claim_creation_time] + end + + def css_id + @payload[:css_id].presence + end + + def detail_type + @payload[:detail_type].presence + end + + def station_id + @payload[:station].presence + 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 + + def claim_review_legacy_opt_in_approved + @payload.dig(:claim_review, :legacy_opt_in_approved) + end + + def end_product_establishment_development_item_reference_id + @payload.dig(:end_product_establishment, :development_item_reference_id).presence + end + + def end_product_establishment_reference_id + @payload.dig(:end_product_establishment, :reference_id).presence + end + + def end_product_establishment_code + @payload.dig(:end_product_establishment, :code).presence + end + + def end_product_establishment_synced_status + @payload.dig(:end_product_establishment, :synced_status).presence + end + + def end_product_establishment_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 original_source + @payload.dig(:original_source) + end + + def decision_review_type + @payload.dig(:decision_review_type) + end + + def veteran_first_name + @payload.dig(:veteran_first_name) + end + + def veteran_last_name + @payload.dig(:veteran_last_name) + end + + def veteran_participant_id + @payload.dig(:veteran_participant_id) + end + + def file_number + @payload.dig(:file_number) + end + + def claimant_participant_id + @payload.dig(:claimant_participant_id) + end + + def claim_category + @payload.dig(:claim_category) + end + + def claim_received_date + @payload.dig(:claim_received_date) + end + + def claim_lifecycle_status + @payload.dig(:claim_lifecycle_status) + end + + def payee_code + @payload.dig(:payee_code) + end + + def ols_issue + @payload.dig(:ols_issue) + end + + def originated_from_vacols_issue + @payload.dig(:originated_from_vacols_issue) + end + + def limited_poa_code + @payload.dig(:limited_poa_code) + end + + def tracked_item_action + @payload.dig(:tracked_item_action) + end + + def tracked_item_id + @payload.dig(:tracked_item_id) + end + + def informal_conference_requested + @payload.dig(:informal_conference_requested) + end + + def same_station_review_requested + @payload.dig(:same_station_review_requested) + end + + def claim_time + @payload.dig(:claim_time) + end + + def catror_username + @payload.dig(:catror_username) + end + + def catror_application + @payload.dig(:catror_application) + end + + def auto_remand + @payload.dig(:auto_remand) + end + + def added_issues + @payload[:added_issues] || [] + end + + def updated_issues + @payload[:updated_issues] || [] + end + + def eligible_to_ineligible_issues + @payload[:eligible_to_ineligible_issues] || [] + end + + def ineligible_to_eligible_issues + @payload[:ineligible_to_eligible_issues] || [] + end + + def ineligible_to_ineligible_issues + @payload[:ineligible_to_ineligible_issues] || [] + end + + def withdrawn_issues + @payload[:withdrawn_issues] || [] + end + + def removed_issues + @payload[:removed_issues] || [] + end +end diff --git a/app/services/events/decision_review_updated/update_claim_review.rb b/app/services/events/decision_review_updated/update_claim_review.rb new file mode 100644 index 00000000000..e417e68721e --- /dev/null +++ b/app/services/events/decision_review_updated/update_claim_review.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +# Service class to handle updates to the ClaimReview (I.E. HigherLevelReview or SupplementalClaim) +# Currently being used to change the 'legacy_opt_in_approved' flag +class Events::DecisionReviewUpdated::UpdateClaimReview + class << self + def process!(params) + event = params[:event] + parser = params[:parser] + claim_review = EndProductEstablishment.find_by( + reference_id: parser.end_product_establishment_reference_id + )&.source + claim_review.update!(legacy_opt_in_approved: parser.claim_review_legacy_opt_in_approved) + EventRecord.create!(event: event, evented_record: claim_review) + claim_review + rescue StandardError => error + raise Caseflow::Error::DecisionReviewUpdatedClaimReviewError, error.message + end + end +end diff --git a/app/services/events/decision_review_updated/update_end_product_establishment.rb b/app/services/events/decision_review_updated/update_end_product_establishment.rb new file mode 100644 index 00000000000..fc21110d151 --- /dev/null +++ b/app/services/events/decision_review_updated/update_end_product_establishment.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +# Service class to handle updates to the EPE +class Events::DecisionReviewUpdated::UpdateEndProductEstablishment + class << self + def process!(params) + event = params[:event] + parser = params[:parser] + epe = EndProductEstablishment.find_by( + reference_id: parser.end_product_establishment_reference_id + ) + epe.update!( + code: parser.end_product_establishment_code, + synced_status: parser.end_product_establishment_synced_status, + last_synced_at: parser.end_product_establishment_last_synced_at + ) + EventRecord.create!(event: event, evented_record: epe) + epe + rescue StandardError => error + raise Caseflow::Error::DecisionReviewUpdatedEndProductEstablishmentError, error.message + end + end +end diff --git a/app/services/events/decision_review_updated/update_informal_conference.rb b/app/services/events/decision_review_updated/update_informal_conference.rb new file mode 100644 index 00000000000..fd0bf270f35 --- /dev/null +++ b/app/services/events/decision_review_updated/update_informal_conference.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +# Service class to handle changes to the informal_conference and/or same_office values on a HLR that +# was previously created through DecisionReviewCreated +class Events::DecisionReviewUpdated::UpdateInformalConference + class << self + def process!(params) + event = params[:event] + parser = params[:parser] + if parser.detail_type == "HigherLevelReview" + # fetch the EPE and use it to get the source (i.e. HLR) + hlr = EndProductEstablishment.find_by(reference_id: parser.end_product_establishment_reference_id)&.source + hlr.update!( + informal_conference: parser.claim_review_informal_conference, + same_office: parser.claim_review_same_office + ) + EventRecord.create!(event: event, evented_record: hlr) + hlr + end + rescue StandardError => error + raise Caseflow::Error::DecisionReviewUpdatedInformalConferenceError, error.message + end + end +end diff --git a/app/services/events/decision_review_updated_error.rb b/app/services/events/decision_review_updated_error.rb new file mode 100644 index 00000000000..b29ee4d7b28 --- /dev/null +++ b/app/services/events/decision_review_updated_error.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +# DecisionReviewUpdatedError 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::DecisionReviewUpdatedError + # 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_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/config/routes.rb b/config/routes.rb index e0f15ef5319..26a4fb7c9a4 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -102,6 +102,8 @@ 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' + post '/decision_review_updated', to: 'decision_review_updated#decision_review_updated' + post '/decision_review_updated_error', to: 'decision_review_updated#decision_review_updated_error' end end diff --git a/db/migrate/20240829142914_add_info_to_event_records.rb b/db/migrate/20240829142914_add_info_to_event_records.rb new file mode 100644 index 00000000000..222df5c14d0 --- /dev/null +++ b/db/migrate/20240829142914_add_info_to_event_records.rb @@ -0,0 +1,13 @@ +class AddInfoToEventRecords < ActiveRecord::Migration[6.0] + disable_ddl_transaction! + + def up + add_column :event_records, :info, :jsonb, default: {} + add_index :event_records, :info, using: :gin, algorithm: :concurrently + end + + def down + remove_index :event_records, column: :info, algorithm: :concurrently + remove_column :event_records, :info + end +end diff --git a/db/migrate/20240906174831_add_reference_id_to_request_issues.rb b/db/migrate/20240906174831_add_reference_id_to_request_issues.rb new file mode 100644 index 00000000000..27878935856 --- /dev/null +++ b/db/migrate/20240906174831_add_reference_id_to_request_issues.rb @@ -0,0 +1,5 @@ +class AddReferenceIdToRequestIssues < ActiveRecord::Migration[6.0] + def change + add_column :request_issues, :reference_id, :string, comment: "The ID of the decision review issue record internal to C&P." + end +end diff --git a/db/schema.rb b/db/schema.rb index fb88838b8ac..acc867149ec 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2024_08_28_165652) do +ActiveRecord::Schema.define(version: 2024_09_06_174831) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -885,8 +885,10 @@ 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.jsonb "info", default: {} 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" + t.index ["info"], name: "index_event_records_on_info", using: :gin end create_table "events", comment: "Stores events from the Appeals-Consumer application that are processed by Caseflow", force: :cascade do |t| @@ -1691,6 +1693,7 @@ t.text "pact_status_update_reason_notes", comment: "The reason for why Request Issue is Promise to Address Comprehensive Toxics (PACT) Act" t.string "ramp_claim_id", comment: "If a rating issue was created as a result of an issue intaken for a RAMP Review, it will be connected to the former RAMP issue by its End Product's claim ID." t.datetime "rating_issue_associated_at", comment: "Timestamp when a contention and its contested rating issue are associated in VBMS." + t.string "reference_id", comment: "The ID of the decision review issue record internal to C&P." t.string "split_issue_status", comment: "If a request issue is part of a split, on_hold status applies to the original request issues while active are request issues on splitted appeals" t.string "type", default: "RequestIssue", comment: "Determines whether the issue is a rating issue or a nonrating issue" t.string "unidentified_issue_text", comment: "User entered description if the request issue is neither a rating or a nonrating issue" diff --git a/lib/caseflow/error.rb b/lib/caseflow/error.rb index 69f46c0bdfb..4d03858018a 100644 --- a/lib/caseflow/error.rb +++ b/lib/caseflow/error.rb @@ -504,7 +504,21 @@ class DecisionReviewCreatedIntakeError < StandardError; end class DecisionReviewCreatedCreateClaimReviewError < StandardError; end class DecisionReviewCreatedEpEstablishmentError < StandardError; end class DecisionReviewCreatedRequestIssuesError < StandardError; end - + class DecisionReviewUpdatedRequestIssuesError < StandardError; end + class DecisionReviewUpdatedInformalConferenceError < StandardError; end + class DecisionReviewUpdateMissingReviewError < StandardError; end + class DecisionReviewUpdateMissingIssueError < StandardError + def initialize(reference_id) + super("Request issue not found for REFERENCE_ID: #{reference_id}") + end + end + class DecisionReviewUpdateMismatchedRemovedIssuesError < StandardError + def initialize(msg = "Decision review update mismatched removed issues") + super(msg) + end + end + class DecisionReviewUpdatedClaimReviewError < StandardError; end + class DecisionReviewUpdatedEndProductEstablishmentError < StandardError; end class MaximumBatchSizeViolationError < StandardError def initialize(msg = "The batch size of jobs must not exceed 10") super(msg) 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 index 153cbc933de..dedeafc6a99 100644 --- a/spec/controllers/api/events/v1/decision_review_created_controller_spec.rb +++ b/spec/controllers/api/events/v1/decision_review_created_controller_spec.rb @@ -30,7 +30,7 @@ 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" + lock_key = "RedisMutex:EndProductEstablishment:#{JSON.parse(payload)['claim_id']}" 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) diff --git a/spec/controllers/api/events/v1/decision_review_updated_controller_spec.rb b/spec/controllers/api/events/v1/decision_review_updated_controller_spec.rb new file mode 100644 index 00000000000..b1d3cc75d0d --- /dev/null +++ b/spec/controllers/api/events/v1/decision_review_updated_controller_spec.rb @@ -0,0 +1,163 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe Api::Events::V1::DecisionReviewUpdatedController, type: :controller do + let!(:current_user) { User.authenticate! } + let(:api_key) { ApiKey.create!(consumer_name: "API TEST TOKEN") } + let(:event_id) { 1 } + let(:fake_event_id) { 1738 } + + describe "before_action :check_api_disabled" do + context "when API is disabled" do + before do + allow(FeatureToggle).to receive(:enabled?).with(:disable_ama_eventing).and_return(true) + request.headers["Authorization"] = "Token token=#{api_key.key_string}" + end + + # rubocop:disable Layout/HashAlignment + it "returns a 501 status and error message" do + post :decision_review_updated, params: { event_id: event_id } + expect(response).to have_http_status(:not_implemented) + expect(JSON.parse(response.body)).to eq( + "errors" => [ + { + "status" => "501", + "title" => "API is disabled", + "detail" => "This endpoint is not supported." + } + ] + ) + end + # rubocop:enable Layout/HashAlignment + end + + context "when API is enabled" do + before do + allow(FeatureToggle).to receive(:enabled?).with(:disable_ama_eventing).and_return(false) + request.headers["Authorization"] = "Token token=#{api_key.key_string}" + end + + it "allows the action to proceed" do + expect(controller).to receive(:decision_review_updated) + post :decision_review_updated, params: { event_id: event_id } + end + end + end + + describe "POST #decision_review_updated" do + let(:dru_params) { { event_id: event_id } } + let(:event) { double("DecisionReviewCreatedEvent") } + + before do + request.headers["Authorization"] = "Token token=#{api_key.key_string}" + allow(controller).to receive(:dru_params).and_return(dru_params) + end + + context "when event exists" do + before do + allow(Event).to receive(:exists_and_is_completed?).with(event_id).and_return(true) + allow(DecisionReviewCreatedEvent).to receive(:find_by).with(id: event_id).and_return(event) + end + + context "when update is successful" do + before do + load_headers + allow(Events::DecisionReviewUpdated).to receive(:update!).with(event, request.headers, dru_params) + end + + # skipping this test until we implement update! method + xit "returns a 200 status and success message" do + post :decision_review_updated, params: { event_id: event_id } + expect(response).to have_http_status(:ok) + expect(JSON.parse(response.body)).to eq( + { "message" => "DecisionReviewUpdatedEvent successfully processed" } + ) + end + end + + context "when RedisLockFailed error occurs" do + before do + allow(Events::DecisionReviewUpdated).to receive(:update!).and_raise( + Caseflow::Error::RedisLockFailed, "Lock failed" + ) + end + + it "returns a 409 status and error message" do + post :decision_review_updated, params: { event_id: event_id } + expect(response).to have_http_status(:conflict) + expect(JSON.parse(response.body)).to eq({ "message" => "Lock failed" }) + end + end + + context "when StandardError occurs" do + before do + allow(Events::DecisionReviewUpdated).to receive(:update!).and_raise(StandardError, "Something went wrong") + end + + it "returns a 422 status and error message" do + post :decision_review_updated, params: { event_id: event_id } + expect(response).to have_http_status(:unprocessable_entity) + expect(JSON.parse(response.body)).to eq({ "message" => "Something went wrong" }) + end + end + end + end + + describe "POST #decision_review_updated_error" do + let(:dru_error_params) { { event_id: event_id, errored_claim_id: 2, error: "some error" } } + + before do + request.headers["Authorization"] = "Token token=#{api_key.key_string}" + allow(controller).to receive(:dru_error_params).and_return(dru_error_params) + end + + context "when service error handling is successful" do + before do + allow(Events::DecisionReviewUpdatedError).to receive(:handle_service_error).with(event_id, 2, "some error") + end + + it "returns a 201 status and success message" do + post :decision_review_updated_error, params: { event_id: event_id } + expect(response).to have_http_status(:created) + expect(JSON.parse(response.body)).to eq({ "message" => "Decision Review Updated Error Saved in Caseflow" }) + end + end + + context "when RedisLockFailed error occurs" do + before do + allow(Events::DecisionReviewUpdatedError).to receive(:handle_service_error).and_raise( + Caseflow::Error::RedisLockFailed, "Lock failed" + ) + end + + it "returns a 409 status and error message" do + post :decision_review_updated_error, params: { event_id: event_id } + expect(response).to have_http_status(:conflict) + expect(JSON.parse(response.body)).to eq({ "message" => "Lock failed" }) + end + end + + context "when StandardError occurs" do + before do + allow(Events::DecisionReviewUpdatedError).to receive(:handle_service_error).and_raise( + StandardError, "Something went wrong" + ) + end + + it "returns a 422 status and error message" do + post :decision_review_updated_error, params: { event_id: event_id } + expect(response).to have_http_status(:unprocessable_entity) + expect(JSON.parse(response.body)).to eq({ "message" => "Something went wrong" }) + end + end + 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 +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 index f74ee9cf095..62de3e1f7ee 100644 --- a/spec/feature/events/decision_review_created/scenario_a_spec.rb +++ b/spec/feature/events/decision_review_created/scenario_a_spec.rb @@ -68,6 +68,7 @@ def json_test_payload }, "request_issues": [ { + "decision_review_issue_id": 1, "benefit_type": "compensation", "contested_issue_description": nil, "contention_reference_id": 7905752, diff --git a/spec/feature/events/decision_review_created/scenario_c_spec.rb b/spec/feature/events/decision_review_created/scenario_c_spec.rb index 8bb7c6fe408..38d12511e2e 100644 --- a/spec/feature/events/decision_review_created/scenario_c_spec.rb +++ b/spec/feature/events/decision_review_created/scenario_c_spec.rb @@ -64,6 +64,7 @@ def json_payload }, "request_issues": [ { + "decision_review_issue_id": 1, "benefit_type": "compensation", "contested_issue_description": nil, "contention_reference_id": 7905752, diff --git a/spec/feature/events/decision_review_updated/scenario_create_issues_spec.rb b/spec/feature/events/decision_review_updated/scenario_create_issues_spec.rb new file mode 100644 index 00000000000..060bc4d69d8 --- /dev/null +++ b/spec/feature/events/decision_review_updated/scenario_create_issues_spec.rb @@ -0,0 +1,124 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe Api::Events::V1::DecisionReviewUpdatedController, type: :controller do + describe "POST #decision_review_updated" do + let!(:current_user) { User.authenticate! } + let(:api_key) { ApiKey.create!(consumer_name: "API TEST TOKEN") } + let!(:epe) { create(:end_product_establishment, :active_hlr, reference_id: 12_345_678) } + let(:review) { epe.source } + let!(:existing_request_issue) { create(:request_issue, decision_review: review, reference_id: "6789") } + + def json_test_payload + { + "event_id": 214_786, + "claim_id": 12_345_678, + "css_id": "BVADWISE101", + "detail_type": "HigherLevelReview", + "station": "101", + "intake": { + "started_at": 1_702_067_143_435, + "completion_started_at": 1_702_067_145_000, + "completed_at": 1_702_067_145_000, + "completion_status": "success", + "type": "HigherLevelReviewIntake", + "detail_type": "HigherLevelReview" + }, + "veteran": { + "participant_id": "1826209", + "bgs_last_synced_at": 1_708_533_584_000, + "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 + }, + "end_product_establishment": { + "code": "030HLRNR", + "reference_id": "12345678", + "last_synced_at": 1_702_067_145_000, + "synced_status": "RFD", + "development_item_reference_id": "1" + }, + "added_issues": [ + { + "original_caseflow_request_issue_id": nil, + "contested_rating_decision_reference_id": nil, + "contested_rating_issue_reference_id": nil, + "contested_decision_issue_id": nil, + "untimely_exemption": false, + "untimely_exemption_notes": "some notes", + "edited_description": nil, + "vacols_id": "some_id", + "vacols_sequence_id": "some_sequence_id", + "nonrating_issue_bgs_id": "some_bgs_id", + "type": "RequestIssue", + "decision_review_issue_id": 1234, + "contention_reference_id": 123_456, + "benefit_type": "compensation", + "contested_issue_description": "some_description", + "contested_rating_issue_profile_date": "122255", + "decision_date": 19_568, + "ineligible_due_to_id": nil, + "ineligible_reason": nil, + "unidentified_issue_text": "An unidentified issue added during the edit", + "nonrating_issue_category": nil, + "nonrating_issue_description": nil, + "closed_at": nil, + "closed_status": nil, + "contested_rating_issue_diagnostic_code": "9411", + "rating_issue_associated_at": nil, + "ramp_claim_id": nil, + "is_unidentified": true, + "nonrating_issue_bgs_source": nil + } + ], + "updated_issues": [], + "removed_issues": [], + "withdrawn_issues": [], + "ineligible_to_eligible_issues": [], + "eligible_to_ineligible_issues": [], + "ineligible_to_ineligible_issues": [] + } + end + + let!(:valid_params) do + json_test_payload + end + + context "add issue with already existing issue" do + before do + request.headers["Authorization"] = "Token token=#{api_key.key_string}" + end + + it "returns success response whith updated edited_description" do + request.headers["Authorization"] = "Token token=#{api_key.key_string}" + expect(existing_request_issue.edited_description).to_not eq("DIC: Service connection denied (UPDATED)") + post :decision_review_updated, params: valid_params + expect(response).to have_http_status(:ok) + expect(response.body).to include("DecisionReviewUpdatedEvent successfully processed") + existing_request_issue.reload + expect(existing_request_issue.any_updates?).to eq(true) + new_request_issue = RequestIssue.find_by(reference_id: "1234") + expect(new_request_issue).to be + request_issue_update = review.request_issues_updates.first + expect(request_issue_update).to be + expect(request_issue_update.before_request_issue_ids).to eq([existing_request_issue.id]) + expect(request_issue_update.after_request_issue_ids).to eq([new_request_issue.id, existing_request_issue.id]) + expect(request_issue_update.edited_request_issue_ids).to eq([]) + expect(request_issue_update.withdrawn_request_issue_ids).to eq([]) + expect(new_request_issue.event_records.last.info["update_type"]).to eq("A") + expect(new_request_issue.event_records.last.info["record_data"]["id"]).to eq(new_request_issue.id) + end + end + end +end diff --git a/spec/feature/events/decision_review_updated/scenario_edited_issues_spec.rb b/spec/feature/events/decision_review_updated/scenario_edited_issues_spec.rb new file mode 100644 index 00000000000..bde0f7bbf6a --- /dev/null +++ b/spec/feature/events/decision_review_updated/scenario_edited_issues_spec.rb @@ -0,0 +1,229 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe Api::Events::V1::DecisionReviewUpdatedController, type: :controller do + describe "POST #decision_review_updated" do + let!(:current_user) { User.authenticate! } + let(:api_key) { ApiKey.create!(consumer_name: "API TEST TOKEN") } + let!(:epe) { create(:end_product_establishment, :active_hlr, reference_id: 12_345_678) } + let(:review) { epe.source } + let!(:existing_request_issue) { create(:request_issue, decision_review: review, reference_id: "1234") } + + def json_test_payload + { + "event_id": 214_786, + "claim_id": 12_345_678, + "css_id": "BVADWISE101", + "detail_type": "HigherLevelReview", + "station": "101", + "intake": { + "started_at": 1_702_067_143_435, + "completion_started_at": 1_702_067_145_000, + "completed_at": 1_702_067_145_000, + "completion_status": "success", + "type": "HigherLevelReviewIntake", + "detail_type": "HigherLevelReview" + }, + "veteran": { + "participant_id": "1826209", + "bgs_last_synced_at": 1_708_533_584_000, + "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 + }, + "end_product_establishment": { + "code": "030HLRNR", + "reference_id": "12345678", + "last_synced_at": 1_702_067_145_000, + "synced_status": "RFD", + "development_item_reference_id": "1" + }, + "updated_issues": [ + { + "original_caseflow_request_issue_id": 1, + "contested_rating_decision_reference_id": 1, + "contested_rating_issue_reference_id": 2, + "contested_decision_issue_id": 3, + "untimely_exemption": false, + "untimely_exemption_notes": "some notes", + "edited_description": "DIC: Service connection denied (UPDATED)", + "vacols_id": "some_id", + "vacols_sequence_id": "some_sequence_id", + "nonrating_issue_bgs_id": "some_bgs_id", + "type": "RequestIssue", + "decision_review_issue_id": 1234, + "contention_reference_id": 123_457, + "benefit_type": "compensation", + "contested_issue_description": "some_description", + "contested_rating_issue_profile_date": "122255", + "decision_date": 19_568, + "ineligible_due_to_id": nil, + "ineligible_reason": nil, + "unidentified_issue_text": "An unidentified issue added during the edit", + "nonrating_issue_category": nil, + "nonrating_issue_description": nil, + "closed_at": nil, + "closed_status": nil, + "contested_rating_issue_diagnostic_code": "9411", + "rating_issue_associated_at": nil, + "ramp_claim_id": nil, + "is_unidentified": true, + "nonrating_issue_bgs_source": nil + } + ], + "removed_issues": [], + "withdrawn_issues": [], + "ineligible_to_eligible_issues": [], + "eligible_to_ineligible_issues": [], + "ineligible_to_ineligible_issues": [] + } + end + + let!(:valid_params) do + json_test_payload + end + + context "updates issue" do + before do + request.headers["Authorization"] = "Token token=#{api_key.key_string}" + end + + it "returns success response whith updated edited_description" do + expect(existing_request_issue.edited_description).to_not eq("DIC: Service connection denied (UPDATED)") + post :decision_review_updated, params: valid_params + expect(response).to have_http_status(:ok) + expect(response.body).to include("DecisionReviewUpdatedEvent successfully processed") + existing_request_issue.reload + expect(existing_request_issue.edited_description).to eq("DIC: Service connection denied (UPDATED)") + expect(existing_request_issue.any_updates?).to eq(true) + request_issue_update = review.request_issues_updates.first + expect(request_issue_update).to be + expect(request_issue_update.before_request_issue_ids).to eq([existing_request_issue.id]) + expect(request_issue_update.after_request_issue_ids).to eq([existing_request_issue.id]) + end + end + + context "updated issue with other issues already on the review" do + let!(:existing_request_issue_2) { create(:request_issue, decision_review: review, reference_id: "6789") } + before do + request.headers["Authorization"] = "Token token=#{api_key.key_string}" + end + + it "returns success response whith updated edited_description" do + expect(RequestIssue.find_by(reference_id: "6789")).to be + expect(existing_request_issue.edited_description).to_not eq("DIC: Service connection denied (UPDATED)") + post :decision_review_updated, params: valid_params + expect(response).to have_http_status(:ok) + expect(response.body).to include("DecisionReviewUpdatedEvent successfully processed") + existing_request_issue.reload + expect(existing_request_issue.edited_description).to eq("DIC: Service connection denied (UPDATED)") + expect(existing_request_issue.any_updates?).to eq(true) + expect(RequestIssue.find_by(reference_id: "6789")).to be + request_issue_update = review.request_issues_updates.first + expect(request_issue_update).to be + expect(request_issue_update.before_request_issue_ids).to eq( + [existing_request_issue.id, existing_request_issue_2.id] + ) + expect(request_issue_update.after_request_issue_ids).to eq( + [existing_request_issue.id, existing_request_issue_2.id] + ) + expect(request_issue_update.withdrawn_request_issue_ids).to eq([]) + expect(request_issue_update.edited_request_issue_ids).to eq([existing_request_issue.id]) + end + end + + context "does not update on error" do + before do + request.headers["Authorization"] = "Token token=#{api_key.key_string}" + allow_any_instance_of(RequestIssuesUpdateEvent).to receive(:perform!).and_raise(StandardError.new("Error")) + end + + it "returns success response whith updated edited_description" do + expect(existing_request_issue.edited_description).to_not eq("DIC: Service connection denied (UPDATED)") + post :decision_review_updated, params: valid_params + expect(response).to have_http_status(:unprocessable_entity) + expect(response.body).to include("Error") + existing_request_issue.reload + expect(existing_request_issue.edited_description).to_not eq("DIC: Service connection denied (UPDATED)") + expect(existing_request_issue.any_updates?).to eq(false) + end + end + + context "updates multiple issues with already existing issues not edited" do + let!(:existing_request_issue_2) { create(:request_issue, decision_review: review, reference_id: "6789") } + let!(:existing_request_issue_3) { create(:request_issue, decision_review: review, reference_id: "123456789") } + before do + request.headers["Authorization"] = "Token token=#{api_key.key_string}" + valid_params[:updated_issues] << { + "original_caseflow_request_issue_id": 2, + "contested_rating_decision_reference_id": 1, + "contested_rating_issue_reference_id": 2, + "contested_decision_issue_id": 3, + "untimely_exemption": false, + "untimely_exemption_notes": "some notes", + "edited_description": "DIC: Service connection denied 2 (UPDATED)", + "vacols_id": "some_id", + "vacols_sequence_id": "some_sequence_id", + "nonrating_issue_bgs_id": "some_bgs_id", + "type": "RequestIssue", + "decision_review_issue_id": 6789, + "contention_reference_id": 123_456, + "benefit_type": "compensation", + "contested_issue_description": "some_description", + "contested_rating_issue_profile_date": "122255", + "decision_date": 19_568, + "ineligible_due_to_id": nil, + "ineligible_reason": nil, + "unidentified_issue_text": "An unidentified issue added during the edit", + "nonrating_issue_category": nil, + "nonrating_issue_description": nil, + "closed_at": nil, + "closed_status": nil, + "contested_rating_issue_diagnostic_code": "9411", + "rating_issue_associated_at": nil, + "ramp_claim_id": nil, + "is_unidentified": true, + "nonrating_issue_bgs_source": nil + } + end + + it "returns success response with updated edited_description" do + expect(existing_request_issue.edited_description).to_not eq("DIC: Service connection denied (UPDATED)") + expect(existing_request_issue_2.edited_description).to_not eq("DIC: Service connection denied 2 (UPDATED)") + post :decision_review_updated, params: valid_params + expect(response).to have_http_status(:ok) + expect(response.body).to include("DecisionReviewUpdatedEvent successfully processed") + existing_request_issue.reload + existing_request_issue_2.reload + expect(existing_request_issue.edited_description).to eq("DIC: Service connection denied (UPDATED)") + expect(existing_request_issue.any_updates?).to eq(true) + expect(existing_request_issue_2.edited_description).to eq("DIC: Service connection denied 2 (UPDATED)") + expect(existing_request_issue_2.any_updates?).to eq(true) + expect(RequestIssue.find_by(reference_id: "123456789")).to be + request_issue_update = review.request_issues_updates.first + expect(request_issue_update).to be + expect(request_issue_update.before_request_issue_ids).to eq( + [existing_request_issue.id, existing_request_issue_2.id, existing_request_issue_3.id] + ) + expect(request_issue_update.after_request_issue_ids).to eq( + [existing_request_issue.id, existing_request_issue_2.id, existing_request_issue_3.id] + ) + expect(request_issue_update.withdrawn_request_issue_ids).to eq([]) + expect(request_issue_update.edited_request_issue_ids).to eq( + [existing_request_issue.id, existing_request_issue_2.id] + ) + end + end + end +end diff --git a/spec/feature/events/decision_review_updated/scenario_removed_issues_spec.rb b/spec/feature/events/decision_review_updated/scenario_removed_issues_spec.rb new file mode 100644 index 00000000000..b449f98a362 --- /dev/null +++ b/spec/feature/events/decision_review_updated/scenario_removed_issues_spec.rb @@ -0,0 +1,144 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe Api::Events::V1::DecisionReviewUpdatedController, type: :controller do + let!(:current_user) { User.authenticate! } + let(:api_key) { ApiKey.create!(consumer_name: "API TEST TOKEN") } + let!(:epe) { create(:end_product_establishment, :active_hlr, reference_id: 12_345_678) } + let(:review) { epe.source } + + context "tests processing removed request issues" do + describe "POST #decision_review_updated" do + let!(:removed_request_issue) do + create(:request_issue, + decision_review: review, reference_id: "1234", closed_status: nil, closed_at: nil, + contention_removed_at: nil, contention_reference_id: 100_500) + end + + def json_test_payload + { + "event_id": 214_786, + "claim_id": 12_345_678, + "css_id": "BVADWISE101", + "detail_type": "HigherLevelReview", + "station": "101", + "intake": { + "started_at": 1_702_067_143_435, + "completion_started_at": 1_702_067_145_000, + "completed_at": 1_702_067_145_000, + "completion_status": "success", + "type": "HigherLevelReviewIntake", + "detail_type": "HigherLevelReview" + }, + "veteran": { + "participant_id": "1826209", + "bgs_last_synced_at": 1_708_533_584_000, + "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 + }, + "end_product_establishment": { + "code": "030HLRNR", + "reference_id": "12345678", + "last_synced_at": 1_702_067_145_000, + "synced_status": "RFD", + "development_item_reference_id": "1" + }, + "updated_issues": [], + "removed_issues": [ + { + "original_caseflow_request_issue_id": 1, + "decision_review_issue_id": 1234, + "closed_at": 1_702_000_145_000, + "closed_status": "removed" + } + ], + "withdrawn_issues": [], + "ineligible_to_eligible_issues": [], + "eligible_to_ineligible_issues": [], + "ineligible_to_ineligible_issues": [] + } + end + + let!(:valid_params) do + json_test_payload + end + + context "removed issue with already existing issue not edited or removed" do + let!(:existing_request_issue) do + create(:request_issue, decision_review: review, reference_id: "6789", edited_description: "edited") + end + before do + request.headers["Authorization"] = "Token token=#{api_key.key_string}" + end + + it "returns success response whith updated closed_at date" do + expect(removed_request_issue.reference_id).to eq("1234") + expect(removed_request_issue.closed_at).to eq(nil) + expect(removed_request_issue.contention_reference_id).to eq(100_500) + expect(removed_request_issue.any_updates?).to eq(false) + expect(existing_request_issue.edited_description).to eq("edited") + post :decision_review_updated, params: valid_params + expect(response).to have_http_status(:ok) + expect(response.body).to include("DecisionReviewUpdatedEvent successfully processed") + removed_request_issue.reload + expect(removed_request_issue.closed_at).to eq("2023-12-07 20:49:05.000000000 -0500") + expect(removed_request_issue.closed_status).to eq("removed") + expect(removed_request_issue.any_updates?).to eq(true) + expect(removed_request_issue.updated_at).to be_within(100.seconds).of(DateTime.now) + request_issue_update = review.request_issues_updates.first + expect(request_issue_update.withdrawn_request_issue_ids).to eq([]) + expect(request_issue_update.before_request_issue_ids).to eq( + [removed_request_issue.id, existing_request_issue.id] + ) + expect(request_issue_update.after_request_issue_ids).to eq( + [existing_request_issue.id] + ) + expect(request_issue_update.edited_request_issue_ids).to eq([]) + expect(existing_request_issue.edited_description).to eq("edited") + end + + it "returns success response with updated closed_at date and has edits to description" do + valid_params[:updated_issues] << + { + "decision_review_issue_id": "6789", + "edited_description": "edited again" + } + expect(removed_request_issue.reference_id).to eq("1234") + expect(removed_request_issue.closed_at).to eq(nil) + expect(removed_request_issue.contention_reference_id).to eq(100_500) + expect(removed_request_issue.any_updates?).to eq(false) + post :decision_review_updated, params: valid_params + expect(response).to have_http_status(:ok) + expect(response.body).to include("DecisionReviewUpdatedEvent successfully processed") + removed_request_issue.reload + existing_request_issue.reload + expect(removed_request_issue.closed_at).to eq("2023-12-07 20:49:05.000000000 -0500") + expect(removed_request_issue.closed_status).to eq("removed") + expect(existing_request_issue.edited_description).to eq("edited again") + expect(existing_request_issue.updated_at).to be_within(100.seconds).of(DateTime.now) + request_issue_update = review.request_issues_updates.first + expect(request_issue_update.withdrawn_request_issue_ids).to eq([]) + expect(request_issue_update.before_request_issue_ids).to eq( + [removed_request_issue.id, existing_request_issue.id] + ) + expect(request_issue_update.after_request_issue_ids).to eq( + [existing_request_issue.id] + ) + expect(request_issue_update.edited_request_issue_ids).to eq([existing_request_issue.id]) + end + end + end + end +end diff --git a/spec/feature/events/decision_review_updated/scenario_update_eligibility_spec.rb b/spec/feature/events/decision_review_updated/scenario_update_eligibility_spec.rb new file mode 100644 index 00000000000..40002114302 --- /dev/null +++ b/spec/feature/events/decision_review_updated/scenario_update_eligibility_spec.rb @@ -0,0 +1,384 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe Api::Events::V1::DecisionReviewUpdatedController, type: :controller do + let!(:current_user) { User.authenticate! } + let(:api_key) { ApiKey.create!(consumer_name: "API TEST TOKEN") } + let!(:epe) { create(:end_product_establishment, :active_hlr, reference_id: 12_345_678) } + let(:review) { epe.source } + + context "tests processing ineligible_to_eligible request issues" do + describe "POST #decision_review_updated" do + let!(:ineligible_to_eligible_request_issue) do + create(:request_issue, + decision_review: review, reference_id: "1234", closed_status: "ineligible", closed_at: DateTime.now, + ineligible_reason: "appeal_to_appeal", contention_removed_at: DateTime.now, + contested_issue_description: "original description", nonrating_issue_category: "original category", + nonrating_issue_description: "original nonrating description", contention_reference_id: 100_500) + end + + def json_test_payload + { + "event_id": 214_786, + "claim_id": 12_345_678, + "css_id": "BVADWISE101", + "detail_type": "HigherLevelReview", + "station": "101", + "intake": { + "started_at": 1_702_067_143_435, + "completion_started_at": 1_702_067_145_000, + "completed_at": 1_702_067_145_000, + "completion_status": "success", + "type": "HigherLevelReviewIntake", + "detail_type": "HigherLevelReview" + }, + "veteran": { + "participant_id": "1826209", + "bgs_last_synced_at": 1_708_533_584_000, + "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 + }, + "end_product_establishment": { + "code": "030HLRNR", + "reference_id": "12345678", + "last_synced_at": 1_702_067_145_000, + "synced_status": "RFD", + "development_item_reference_id": "1" + }, + "updated_issues": [], + "removed_issues": [], + "withdrawn_issues": [], + "ineligible_to_eligible_issues": [ + { + "original_caseflow_request_issue_id": 12_345, + "contested_rating_decision_reference_id": nil, + "contested_rating_issue_reference_id": nil, + "contested_decision_issue_id": nil, + "untimely_exemption": false, + "untimely_exemption_notes": nil, + "edited_description": "DIC: Service connection denied (UPDATED)", + "vacols_id": nil, + "vacols_sequence_id": nil, + "nonrating_issue_bgs_id": nil, + "type": "RequestIssue", + "decision_review_issue_id": 1234, + "contention_reference_id": 123_456, + "benefit_type": "compensation", + "contested_issue_description": "UPDATED DESCRIPTION", + "contested_rating_issue_profile_date": nil, + "decision_date": nil, + "ineligible_due_to_id": nil, + "ineligible_reason": nil, + "unidentified_issue_text": "An unidentified issue added during the edit", + "nonrating_issue_category": "Military Retired Pay UPDATED", + "nonrating_issue_description": "UPDATED TESTING", + "closed_at": 1_725_000, + "closed_status": "status", + "contested_rating_issue_diagnostic_code": nil, + "rating_issue_associated_at": nil, + "ramp_claim_id": nil, + "is_unidentified": true, + "nonrating_issue_bgs_source": nil + } + ], + "eligible_to_ineligible_issues": [], + "ineligible_to_ineligible_issues": [] + } + end + + let!(:valid_params) do + json_test_payload + end + + context "ineligible_to_eligible updates issue" do + before do + request.headers["Authorization"] = "Token token=#{api_key.key_string}" + end + + it "returns success response whith eligible request_issue" do + request.headers["Authorization"] = "Token token=#{api_key.key_string}" + expect(ineligible_to_eligible_request_issue.reference_id).to eq("1234") + expect(ineligible_to_eligible_request_issue.ineligible_reason).to eq("appeal_to_appeal") + expect(ineligible_to_eligible_request_issue.contested_issue_description).to eq("original description") + expect(ineligible_to_eligible_request_issue.nonrating_issue_category).to eq("original category") + expect(ineligible_to_eligible_request_issue.nonrating_issue_description) + .to eq("original nonrating description") + expect(ineligible_to_eligible_request_issue.closed_at).to be_within(100.seconds).of(DateTime.now) + expect(ineligible_to_eligible_request_issue.closed_status).to eq("ineligible") + expect(ineligible_to_eligible_request_issue.contention_removed_at).to be_within(1.second).of(DateTime.now) + expect(ineligible_to_eligible_request_issue.contention_reference_id).to eq(100_500) + post :decision_review_updated, params: valid_params + expect(response).to have_http_status(:ok) + expect(response.body).to include("DecisionReviewUpdatedEvent successfully processed") + ineligible_to_eligible_request_issue.reload + + expect(ineligible_to_eligible_request_issue.ineligible_reason).to eq(nil) + expect(ineligible_to_eligible_request_issue.contested_issue_description).to eq("UPDATED DESCRIPTION") + expect(ineligible_to_eligible_request_issue.nonrating_issue_category).to eq("Military Retired Pay UPDATED") + expect(ineligible_to_eligible_request_issue.nonrating_issue_description).to eq("UPDATED TESTING") + expect(ineligible_to_eligible_request_issue.closed_at).to eq(nil) + expect(ineligible_to_eligible_request_issue.closed_status).to eq(nil) + expect(ineligible_to_eligible_request_issue.contention_removed_at).to eq(nil) + expect(ineligible_to_eligible_request_issue.contention_reference_id).to eq(123_456) + end + end + end + end + + context "tests processing eligible_to_ineligible request issues" do + describe "POST #decision_review_updated" do + let!(:eligible_to_ineligible_request_issue) do + create(:request_issue, + decision_review: review, reference_id: "1234", closed_at: nil, + ineligible_reason: nil, contested_issue_description: nil, + nonrating_issue_category: nil, nonrating_issue_description: nil) + end + + def json_test_payload + { + "event_id": 214_786, + "claim_id": 12_345_678, + "css_id": "BVADWISE101", + "detail_type": "HigherLevelReview", + "station": "101", + "intake": { + "started_at": 1_702_067_143_435, + "completion_started_at": 1_702_067_145_000, + "completed_at": 1_702_067_145_000, + "completion_status": "success", + "type": "HigherLevelReviewIntake", + "detail_type": "HigherLevelReview" + }, + "veteran": { + "participant_id": "1826209", + "bgs_last_synced_at": 1_708_533_584_000, + "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 + }, + "end_product_establishment": { + "code": "030HLRNR", + "reference_id": "12345678", + "last_synced_at": 1_702_067_145_000, + "synced_status": "RFD", + "development_item_reference_id": "1" + }, + "updated_issues": [], + "removed_issues": [], + "withdrawn_issues": [], + "ineligible_to_eligible_issues": [], + "eligible_to_ineligible_issues": [ + { + "original_caseflow_request_issue_id": 12_345, + "contested_rating_decision_reference_id": nil, + "contested_rating_issue_reference_id": nil, + "contested_decision_issue_id": nil, + "untimely_exemption": false, + "untimely_exemption_notes": nil, + "edited_description": "DIC: Service connection denied (UPDATED)", + "vacols_id": nil, + "vacols_sequence_id": nil, + "nonrating_issue_bgs_id": nil, + "type": "RequestIssue", + "decision_review_issue_id": 1234, + "contention_reference_id": 123_456, + "benefit_type": "compensation", + "contested_issue_description": "Eligible UPDATED", + "contested_rating_issue_profile_date": nil, + "decision_date": nil, + "ineligible_due_to_id": nil, + "ineligible_reason": "appeal_to_appeal", + "unidentified_issue_text": "An unidentified issue added during the edit", + "nonrating_issue_category": "Military Retired Pay ELIGIBLE", + "nonrating_issue_description": "UPDATED ELIGIBLE", + "closed_at": 1_702_000_145_000, + "closed_status": "removed", + "contested_rating_issue_diagnostic_code": nil, + "rating_issue_associated_at": nil, + "ramp_claim_id": nil, + "is_unidentified": true, + "nonrating_issue_bgs_source": nil + } + ], + "ineligible_to_ineligible_issues": [] + } + end + + let!(:valid_params) do + json_test_payload + end + + context "eligible_to_ineligible updates issue" do + before do + request.headers["Authorization"] = "Token token=#{api_key.key_string}" + end + + it "returns success response whith ineligible request_issue" do + request.headers["Authorization"] = "Token token=#{api_key.key_string}" + expect(eligible_to_ineligible_request_issue.ineligible_reason).to eq(nil) + expect(eligible_to_ineligible_request_issue.contested_issue_description).to eq(nil) + expect(eligible_to_ineligible_request_issue.nonrating_issue_category).to eq(nil) + expect(eligible_to_ineligible_request_issue.closed_at).to eq(nil) + expect(eligible_to_ineligible_request_issue.nonrating_issue_description).to eq(nil) + expect(eligible_to_ineligible_request_issue.closed_status).to eq(nil) + expect(eligible_to_ineligible_request_issue.reference_id).to eq("1234") + post :decision_review_updated, params: valid_params + expect(response).to have_http_status(:ok) + expect(response.body).to include("DecisionReviewUpdatedEvent successfully processed") + eligible_to_ineligible_request_issue.reload + expect(eligible_to_ineligible_request_issue.ineligible_reason).to eq("appeal_to_appeal") + expect(eligible_to_ineligible_request_issue.contested_issue_description).to eq("Eligible UPDATED") + expect(eligible_to_ineligible_request_issue.nonrating_issue_category).to eq("Military Retired Pay ELIGIBLE") + expect(eligible_to_ineligible_request_issue.nonrating_issue_description).to eq("UPDATED ELIGIBLE") + expect(eligible_to_ineligible_request_issue.closed_at).to eq("2023-12-07 20:49:05.000000000 -0500") + end + end + end + end + + context "tests processing ineligible_to_ineligible request issues" do + describe "POST #decision_review_updated" do + let!(:ineligible_to_ineligible_request_issue) do + create(:request_issue, + decision_review: review, reference_id: "1234", closed_status: "ineligible", closed_at: DateTime.now, + ineligible_reason: "appeal_to_appeal", contention_removed_at: DateTime.now, + contested_issue_description: "original description", nonrating_issue_category: "original category", + nonrating_issue_description: "original nonrating description", contention_reference_id: 100_500) + end + + def json_test_payload + { + "event_id": 214_786, + "claim_id": 12_345_678, + "css_id": "BVADWISE101", + "detail_type": "HigherLevelReview", + "station": "101", + "intake": { + "started_at": 1_702_067_143_435, + "completion_started_at": 1_702_067_145_000, + "completed_at": 1_702_067_145_000, + "completion_status": "success", + "type": "HigherLevelReviewIntake", + "detail_type": "HigherLevelReview" + }, + "veteran": { + "participant_id": "1826209", + "bgs_last_synced_at": 1_708_533_584_000, + "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 + }, + "end_product_establishment": { + "code": "030HLRNR", + "reference_id": "12345678", + "last_synced_at": 1_702_067_145_000, + "synced_status": "RFD", + "development_item_reference_id": "1" + }, + "updated_issues": [], + "removed_issues": [], + "withdrawn_issues": [], + "ineligible_to_eligible_issues": [], + "eligible_to_ineligible_issues": [], + "ineligible_to_ineligible_issues": [ + { + "original_caseflow_request_issue_id": 12_345, + "contested_rating_decision_reference_id": nil, + "contested_rating_issue_reference_id": nil, + "contested_decision_issue_id": nil, + "untimely_exemption": false, + "untimely_exemption_notes": nil, + "edited_description": "DIC: Service connection denied (UPDATED)", + "vacols_id": nil, + "vacols_sequence_id": nil, + "nonrating_issue_bgs_id": nil, + "type": "RequestIssue", + "decision_review_issue_id": 1234, + "contention_reference_id": 123_456, + "benefit_type": "compensation", + "contested_issue_description": "UPDATED contested issue deascription", + "contested_rating_issue_profile_date": nil, + "decision_date": nil, + "ineligible_due_to_id": nil, + "ineligible_reason": "appeal_to_appeal", + "unidentified_issue_text": "An unidentified issue added during the edit", + "nonrating_issue_category": "UPDATED category", + "nonrating_issue_description": "UPDATED ELIGIBLE", + "closed_at": 1_702_000_145_000, + "closed_status": "appeal_to_appeal", + "contested_rating_issue_diagnostic_code": nil, + "rating_issue_associated_at": nil, + "ramp_claim_id": nil, + "is_unidentified": true, + "nonrating_issue_bgs_source": nil + } + ] + } + end + + let!(:valid_params) do + json_test_payload + end + + context "ineligible_to_ineligible updates issue" do + before do + request.headers["Authorization"] = "Token token=#{api_key.key_string}" + end + + it "returns success response whith updated ineligible request_issue" do + request.headers["Authorization"] = "Token token=#{api_key.key_string}" + expect(ineligible_to_ineligible_request_issue.ineligible_reason).to eq("appeal_to_appeal") + expect(ineligible_to_ineligible_request_issue.contested_issue_description).to eq("original description") + expect(ineligible_to_ineligible_request_issue.nonrating_issue_category).to eq("original category") + expect(ineligible_to_ineligible_request_issue.closed_at).to be_within(100.seconds).of(DateTime.now) + expect(ineligible_to_ineligible_request_issue.nonrating_issue_description) + .to eq("original nonrating description") + expect(ineligible_to_ineligible_request_issue.closed_status).to eq("ineligible") + expect(ineligible_to_ineligible_request_issue.reference_id).to eq("1234") + post :decision_review_updated, params: valid_params + expect(response).to have_http_status(:ok) + expect(response.body).to include("DecisionReviewUpdatedEvent successfully processed") + ineligible_to_ineligible_request_issue.reload + expect(ineligible_to_ineligible_request_issue.ineligible_reason).to eq("appeal_to_appeal") + expect(ineligible_to_ineligible_request_issue.contested_issue_description) + .to eq("UPDATED contested issue deascription") + expect(ineligible_to_ineligible_request_issue.nonrating_issue_category).to eq("UPDATED category") + expect(ineligible_to_ineligible_request_issue.nonrating_issue_description).to eq("UPDATED ELIGIBLE") + expect(ineligible_to_ineligible_request_issue.closed_at).to eq("2023-12-07 20:49:05.000000000 -0500") + end + end + end + end +end diff --git a/spec/feature/events/decision_review_updated/scenario_update_epe_spec.rb b/spec/feature/events/decision_review_updated/scenario_update_epe_spec.rb new file mode 100644 index 00000000000..98f6e5a6f35 --- /dev/null +++ b/spec/feature/events/decision_review_updated/scenario_update_epe_spec.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +# rubocop:disable Style/NumericLiterals + +RSpec.describe Api::Events::V1::DecisionReviewUpdatedController, type: :controller do + describe "POST #decision_review_updated" do + let!(:current_user) { User.authenticate! } + let(:api_key) { ApiKey.create!(consumer_name: "API TEST TOKEN") } + let!(:epe) { create(:end_product_establishment, :active_hlr, reference_id: 1234567) } + + def json_test_payload + { + "event_id": 214706, + "claim_id": 1234567, + "css_id": "BVADWISE101", + "detail_type": "HigherLevelReview", + "station": "101", + "claim_review": { + "informal_conference": false, + "same_office": false, + "legacy_opt_in_approved": false + }, + "end_product_establishment": { + "code": "030HLRR", + "development_item_reference_id": "1", + "reference_id": "1234567", + "synced_status": "RFD", + "last_synced_at": 1726688419000 + }, + "added_issues": nil, + "updated_issues": nil, + "removed_issues": nil, + "withdrawn_issues": nil, + "ineligible_to_eligible_issues": nil, + "eligible_to_ineligible_issues": nil, + "ineligible_to_ineligible_issues": nil + } + end + + let!(:valid_params) do + json_test_payload + end + + context "a payload that ONLY affects the EPE" do + before do + FeatureToggle.disable!(:disable_ama_eventing) + request.headers["Authorization"] = "Token token=#{api_key.key_string}" + end + it "updates the EPE accordingly" do + request.headers["Authorization"] = "Token #{api_key.key_string}" + post :decision_review_updated, params: valid_params + expect(response).to have_http_status(:ok) + epe.reload + expect(epe.code).to eq("030HLRR") + expect(epe.synced_status).to eq("RFD") + expect(epe.last_synced_at).to_not be_nil + end + end + end +end + +# rubocop:enable Style/NumericLiterals diff --git a/spec/feature/events/decision_review_updated/scenario_update_soc_optin_spec.rb b/spec/feature/events/decision_review_updated/scenario_update_soc_optin_spec.rb new file mode 100644 index 00000000000..acbf2b0c7e1 --- /dev/null +++ b/spec/feature/events/decision_review_updated/scenario_update_soc_optin_spec.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +# rubocop:disable Style/NumericLiterals + +RSpec.describe Api::Events::V1::DecisionReviewUpdatedController, type: :controller do + describe "POST #decision_review_updated" do + let!(:current_user) { User.authenticate! } + let(:api_key) { ApiKey.create!(consumer_name: "API TEST TOKEN") } + let!(:epe) { create(:end_product_establishment, :active_hlr, reference_id: 1234567) } + let!(:hlr) { epe.source } + let!(:epe_2) { create(:end_product_establishment, :active_supp, reference_id: 9876543) } + let!(:sc) { epe_2.source } + + def json_test_payload + { + "event_id": 214706, + "claim_id": 1234567, + "css_id": "BVADWISE101", + "detail_type": "HigherLevelReview", + "station": "101", + "claim_review": { + "informal_conference": false, + "same_office": false, + "legacy_opt_in_approved": true + }, + "end_product_establishment": { + "code": "030HLRR", + "development_item_reference_id": "1", + "reference_id": "1234567" + }, + "added_issues": nil, + "updated_issues": nil, + "removed_issues": nil, + "withdrawn_issues": nil, + "ineligible_to_eligible_issues": nil, + "eligible_to_ineligible_issues": nil, + "ineligible_to_ineligible_issues": nil + } + end + + def json_test_payload_2 + { + "event_id": 214707, + "claim_id": 9876543, + "css_id": "BVADWISE101", + "detail_type": "SupplementalClaim", + "station": "101", + "claim_review": { + "informal_conference": false, + "same_office": false, + "legacy_opt_in_approved": true + }, + "end_product_establishment": { + "reference_id": "9876543" + } + } + end + + let!(:valid_params) do + json_test_payload + end + + let!(:valid_params_2) do + json_test_payload_2 + end + + context "a payload that ONLY affects the soc optin on the claim review" do + before do + FeatureToggle.disable!(:disable_ama_eventing) + end + it "updates the HLR accordingly" do + request.headers["Authorization"] = "Token #{api_key.key_string}" + post :decision_review_updated, params: valid_params + expect(response).to have_http_status(:ok) + hlr.reload + expect(hlr.legacy_opt_in_approved).to eq(true) + end + it "also updates SC's accordingly" do + request.headers["Authorization"] = "Token #{api_key.key_string}" + post :decision_review_updated, params: valid_params_2 + expect(response).to have_http_status(:ok) + sc.reload + expect(sc.legacy_opt_in_approved).to eq(true) + end + end + end +end + +# rubocop:enable Style/NumericLiterals diff --git a/spec/feature/events/decision_review_updated/scenario_withdrawn_issues.rb b/spec/feature/events/decision_review_updated/scenario_withdrawn_issues.rb new file mode 100644 index 00000000000..2dc9d68c135 --- /dev/null +++ b/spec/feature/events/decision_review_updated/scenario_withdrawn_issues.rb @@ -0,0 +1,136 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe Api::Events::V1::DecisionReviewUpdatedController, type: :controller do + let!(:current_user) { User.authenticate! } + let(:api_key) { ApiKey.create!(consumer_name: "API TEST TOKEN") } + let!(:epe) { create(:end_product_establishment, :active_hlr, reference_id: 12_345_678) } + let(:review) { epe.source } + + context "tests processing withdrawn request issues" do + describe "POST #decision_review_updated" do + let!(:withdrawn_request_issue) do + create(:request_issue, + decision_review: review, reference_id: "1234", closed_status: nil, closed_at: nil, + contention_removed_at: nil, contention_reference_id: 100_500) + end + + def json_test_payload + { + "event_id": 214_786, + "claim_id": 12_345_678, + "css_id": "BVADWISE101", + "detail_type": "HigherLevelReview", + "station": "101", + "intake": { + "started_at": 1_702_067_143_435, + "completion_started_at": 1_702_067_145_000, + "completed_at": 1_702_067_145_000, + "completion_status": "success", + "type": "HigherLevelReviewIntake", + "detail_type": "HigherLevelReview" + }, + "veteran": { + "participant_id": "1826209", + "bgs_last_synced_at": 1_708_533_584_000, + "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 + }, + "end_product_establishment": { + "code": "030HLRNR", + "reference_id": "12345678", + "last_synced_at": 1_702_067_145_000, + "synced_status": "RFD", + "development_item_reference_id": "1" + }, + "updated_issues": [], + "removed_issues": [], + "withdrawn_issues": [ + { + "original_caseflow_request_issue_id": 12_345, + "contested_rating_decision_reference_id": nil, + "contested_rating_issue_reference_id": nil, + "contested_decision_issue_id": nil, + "untimely_exemption": false, + "untimely_exemption_notes": nil, + "vacols_id": nil, + "vacols_sequence_id": nil, + "nonrating_issue_bgs_id": nil, + "type": "RequestIssue", + "decision_review_issue_id": 1234, + "contention_reference_id": 123_456, + "benefit_type": "compensation", + "contested_issue_description": nil, + "contested_rating_issue_profile_date": nil, + "decision_date": nil, + "ineligible_due_to_id": nil, + "ineligible_reason": nil, + "unidentified_issue_text": "An unidentified issue added during the edit", + "nonrating_issue_category": nil, + "nonrating_issue_description": nil, + "closed_at": 1_702_000_145_000, + "closed_status": nil, + "contested_rating_issue_diagnostic_code": nil, + "rating_issue_associated_at": nil, + "ramp_claim_id": nil, + "is_unidentified": true, + "nonrating_issue_bgs_source": nil + } + ], + "ineligible_to_eligible_issues": [], + "eligible_to_ineligible_issues": [], + "ineligible_to_ineligible_issues": [] + } + end + + let!(:valid_params) do + json_test_payload + end + + context "withdrawn issue with already existing issue not edited or withdrawn" do + let!(:existing_request_issue) do + create(:request_issue, decision_review: review, reference_id: "6789") + end + before do + request.headers["Authorization"] = "Token token=#{api_key.key_string}" + end + + it "returns success response whith updated closed_at date" do + expect(withdrawn_request_issue.reference_id).to eq("1234") + expect(withdrawn_request_issue.closed_at).to eq(nil) + expect(withdrawn_request_issue.contention_reference_id).to eq(100_500) + expect(withdrawn_request_issue.any_updates?).to eq(false) + post :decision_review_updated, params: valid_params + expect(response).to have_http_status(:ok) + expect(response.body).to include("DecisionReviewUpdatedEvent successfully processed") + withdrawn_request_issue.reload + expect(withdrawn_request_issue.closed_at).to eq("2023-12-07 20:49:05.000000000 -0500") + expect(withdrawn_request_issue.closed_status).to eq("withdrawn") + expect(withdrawn_request_issue.any_updates?).to eq(true) + expect(withdrawn_request_issue.updated_at).to be_within(100.seconds).of(DateTime.now) + request_issue_update = review.request_issues_updates.first + expect(request_issue_update.withdrawn_request_issue_ids).to eq([withdrawn_request_issue.id]) + expect(request_issue_update.before_request_issue_ids).to eq( + [withdrawn_request_issue.id, existing_request_issue.id] + ) + expect(request_issue_update.after_request_issue_ids).to eq( + [withdrawn_request_issue.id, existing_request_issue.id] + ) + expect(request_issue_update.edited_request_issue_ids).to eq([]) + end + end + end + end +end diff --git a/spec/models/events/event_record_spec.rb b/spec/models/events/event_record_spec.rb index 335acf30c7a..be1cfb7ff27 100644 --- a/spec/models/events/event_record_spec.rb +++ b/spec/models/events/event_record_spec.rb @@ -86,17 +86,17 @@ 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.event_records.first).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.event_records.first).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.event_records.first).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") diff --git a/spec/models/request_issues_update_event_spec.rb b/spec/models/request_issues_update_event_spec.rb new file mode 100644 index 00000000000..11a7141a1eb --- /dev/null +++ b/spec/models/request_issues_update_event_spec.rb @@ -0,0 +1,518 @@ +# frozen_string_literal: true + +RSpec.describe RequestIssuesUpdateEvent, type: :model do + let(:event) { DecisionReviewUpdatedEvent.new(reference_id: "1234567890") } + let(:user) { create(:user) } + let(:review) { create(:higher_level_review) } + let!(:existing_request_issue) do + create(:request_issue_with_epe, decision_review: review, reference_id: "some_reference_id") + end + let(:last_synced_at) { Time.zone.local(2023, 7, 1, 12, 0, 0) } + let(:parser) do + instance_double(Events::DecisionReviewUpdated::DecisionReviewUpdatedParser).tap do |parser| + allow(parser).to receive(:updated_issues).and_return([]) + allow(parser).to receive(:withdrawn_issues).and_return([]) + allow(parser).to receive(:added_issues).and_return([]) + allow(parser).to receive(:removed_issues).and_return([]) + allow(parser).to receive(:eligible_to_ineligible_issues).and_return([]) + allow(parser).to receive(:ineligible_to_eligible_issues).and_return([]) + allow(parser).to receive(:ineligible_to_ineligible_issues).and_return([]) + allow(parser).to receive(:end_product_establishment_last_synced_at).and_return(last_synced_at) + allow(parser).to receive(:end_product_establishment_code).and_return("some_end_product_code") + end + end + + let(:parser_issue) do + instance_double(Events::DecisionReviewUpdated::DecisionReviewUpdatedIssueParser).tap do |issue| + allow(issue).to receive(:ri_reference_id).and_return("some_reference_id") + allow(issue).to receive(:ri_original_caseflow_request_issue_id).and_return(nil) + allow(issue).to receive(:ri_benefit_type).and_return("some_benefit_type") + allow(issue).to receive(:ri_closed_at).and_return(last_synced_at) + allow(issue).to receive(:ri_closed_status).and_return("some_status") + allow(issue).to receive(:ri_contested_issue_description).and_return("some_description") + allow(issue).to receive(:ri_contention_reference_id).and_return("some_contention_id") + allow(issue).to receive(:ri_contested_rating_issue_diagnostic_code).and_return("some_diagnostic_code") + allow(issue).to receive(:ri_contested_rating_decision_reference_id).and_return("some_decision_id") + allow(issue).to receive(:ri_contested_rating_issue_profile_date).and_return(Time.zone.today) + allow(issue).to receive(:ri_contested_rating_issue_reference_id).and_return("some_issue_id") + allow(issue).to receive(:ri_contested_decision_issue_id).and_return("some_decision_issue_id") + allow(issue).to receive(:ri_decision_date).and_return(Time.zone.today) + allow(issue).to receive(:ri_ineligible_due_to_id).and_return("some_ineligible_id") + allow(issue).to receive(:ri_ineligible_reason).and_return("some_reason") + allow(issue).to receive(:ri_is_unidentified).and_return(false) + allow(issue).to receive(:ri_unidentified_issue_text).and_return("some_text") + allow(issue).to receive(:ri_nonrating_issue_category).and_return("some_category") + allow(issue).to receive(:ri_nonrating_issue_description).and_return("some_description") + allow(issue).to receive(:ri_nonrating_issue_bgs_id).and_return("some_bgs_id") + allow(issue).to receive(:ri_nonrating_issue_bgs_source).and_return("some_source") + allow(issue).to receive(:ri_ramp_claim_id).and_return("some_claim_id") + allow(issue).to receive(:ri_rating_issue_associated_at).and_return(last_synced_at) + allow(issue).to receive(:ri_untimely_exemption).and_return(false) + allow(issue).to receive(:ri_untimely_exemption_notes).and_return("some_notes") + allow(issue).to receive(:ri_vacols_id).and_return("some_vacols_id") + allow(issue).to receive(:ri_vacols_sequence_id).and_return("some_sequence_id") + allow(issue).to receive(:ri_type).and_return("some_type") + allow(issue).to receive(:ri_edited_description).and_return("Edited description") + end + end + + let(:issue_payload) do + { + decision_review_issue_id: "some_reference_id", + benefit_type: "compensation", + closed_at: 1_625_151_600, + closed_status: "withdrawn", + contention_reference_id: 7_905_752, + contested_decision_issue_id: 201, + contested_issue_description: "Service connection for PTSD", + contested_rating_decision_reference_id: nil, + contested_rating_issue_diagnostic_code: "9411", + contested_rating_issue_profile_date: 1_625_076_000, + contested_rating_issue_reference_id: "REF9411", + type: "RequestIssue", + decision: [ + { + award_event_id: 679, + category: "decision", + contention_id: 35, + decision_finalized_time: nil, + decision_recorded_time: nil, + decision_source: "the source", + decision_text: "", + description: nil, + disposition: nil, + dta_error_explanation: nil, + id: 1738, + rating_profile_date: nil + } + ], + decision_date: 19_568, + ineligible_due_to_id: 301, + ineligible_reason: nil, + is_unidentified: false, + nonrating_issue_bgs_id: "13", + nonrating_issue_bgs_source: "CORP_AWARD_ATTORNEY_FEE", + nonrating_issue_category: "Accrued Benefits", + nonrating_issue_description: "Chapter 35 benefits", + ramp_claim_id: "RAMP123", + rating_issue_associated_at: 1_625_076_000, + unidentified_issue_text: nil, + untimely_exemption: nil, + untimely_exemption_notes: nil, + vacols_id: "VAC123", + vacols_sequence_id: nil, + edited_description: "Edited description" + } + end + + describe "#initialize" do + it "calls build_request_issues_data" do + expect_any_instance_of(described_class).to receive(:build_request_issues_data) + described_class.new(review: review, user: user, parser: parser, event: event) + end + end + + describe "#find_request_issue" do + it "returns the request issue id based on reference_id" do + result = described_class.new( + review: review, user: user, parser: parser, event: event + ).find_request_issue(parser_issue) + expect(result).to eq(existing_request_issue) + end + + it "returns the request issue id based on original_caseflow_request_issue_id and updates the reference_id" do + existing_request_issue.update(reference_id: nil) + allow(parser_issue).to receive(:ri_original_caseflow_request_issue_id).and_return(existing_request_issue.id) + result = described_class.new( + review: review, user: user, parser: parser, event: event + ).find_request_issue(parser_issue) + expect(result).to eq(existing_request_issue) + expect(result.reference_id).to eq(parser_issue.ri_reference_id) + end + + it "raises an error if the request issue is not found" do + allow(RequestIssue).to receive(:find_by).and_return(nil) + expect do + described_class.new( + review: review, + user: user, + parser: parser, + event: event + ).find_request_issue(parser_issue) + end.to raise_error(Caseflow::Error::DecisionReviewUpdateMissingIssueError) + end + end + + describe "#build_issue_data" do + it "returns an empty hash if the parser issue is nil" do + result = described_class.new( + review: review, + user: user, + parser: parser, + event: event + ).build_issue_data(parser_issue: nil) + expect(result).to eq({}) + end + + it "returns a hash of request issue data" do + result = described_class.new(review: review, user: user, parser: parser, event: event).build_issue_data( + parser_issue: parser_issue + ) + expect(result).to eq( + { + request_issue_id: existing_request_issue.id, + benefit_type: parser_issue.ri_benefit_type, + closed_date: parser_issue.ri_closed_at, + withdrawal_date: nil, + closed_status: parser_issue.ri_closed_status, + contention_reference_id: parser_issue.ri_contention_reference_id, + contested_decision_issue_id: parser_issue.ri_contested_decision_issue_id, + contested_rating_issue_reference_id: parser_issue.ri_contested_rating_issue_reference_id, + contested_rating_issue_diagnostic_code: parser_issue.ri_contested_rating_issue_diagnostic_code, + contested_rating_decision_reference_id: parser_issue.ri_contested_rating_decision_reference_id, + contested_rating_issue_profile_date: parser_issue.ri_contested_rating_issue_profile_date, + contested_issue_description: parser_issue.ri_contested_issue_description, + unidentified_issue_text: parser_issue.ri_unidentified_issue_text, + decision_date: parser_issue.ri_decision_date, + nonrating_issue_category: parser_issue.ri_nonrating_issue_category, + nonrating_issue_description: parser_issue.ri_nonrating_issue_description, + is_unidentified: parser_issue.ri_is_unidentified, + untimely_exemption: parser_issue.ri_untimely_exemption, + untimely_exemption_notes: parser_issue.ri_untimely_exemption_notes, + ramp_claim_id: parser_issue.ri_ramp_claim_id, + vacols_id: parser_issue.ri_vacols_id, + vacols_sequence_id: parser_issue.ri_vacols_sequence_id, + ineligible_reason: parser_issue.ri_ineligible_reason, + ineligible_due_to_id: parser_issue.ri_ineligible_due_to_id, + reference_id: parser_issue.ri_reference_id, + type: parser_issue.ri_type, + rating_issue_associated_at: parser_issue.ri_rating_issue_associated_at, + nonrating_issue_bgs_source: parser_issue.ri_nonrating_issue_bgs_source, + nonrating_issue_bgs_id: parser_issue.ri_nonrating_issue_bgs_id, + edited_description: parser_issue.ri_edited_description, + decision_text: parser_issue.ri_nonrating_issue_description, + end_product_code: parser.end_product_establishment_code, + issue_text: parser_issue.ri_unidentified_issue_text + } + ) + end + + it "returns a hash of request issue data with a withdrawal date equal to the closed date" do + result = described_class.new( + review: review, user: user, parser: parser, event: event + ).build_issue_data(parser_issue: parser_issue, is_withdrawn: true) + expect(result[:withdrawal_date]).to eq(result[:closed_date]) + end + end + + describe "#build_request_issues_data" do + it "returns an array of request issue data for updated issues" do + allow(parser).to receive(:updated_issues).and_return([issue_payload]) + allow(Events::DecisionReviewUpdated::DecisionReviewUpdatedIssueParser).to receive(:new).and_return(parser_issue) + issue_data = described_class.new(review: review, user: user, parser: parser, event: event).build_issue_data( + parser_issue: parser_issue + ) + result = described_class.new(review: review, user: user, parser: parser, event: event).build_request_issues_data + expect(result).to eq([issue_data]) + end + end + + describe "#perform!" do + it "returns true if the base perform! is successful" do + allow_any_instance_of(RequestIssuesUpdate).to receive(:perform!).and_return(true) + allow_any_instance_of(described_class).to receive(:process_eligible_to_ineligible_issues!).and_return(true) + allow_any_instance_of(described_class).to receive(:process_ineligible_to_eligible_issues!).and_return(true) + allow_any_instance_of(described_class).to receive(:process_ineligible_to_ineligible_issues!).and_return(true) + allow_any_instance_of(described_class).to receive(:process_request_issues_data!).and_return(true) + subject = described_class.new(review: review, user: user, parser: parser, event: event) + expect(subject).to receive(:process_eligible_to_ineligible_issues!) + expect(subject).to receive(:process_ineligible_to_eligible_issues!) + expect(subject).to receive(:process_ineligible_to_ineligible_issues!) + expect(subject).to receive(:process_request_issues_data!) + expect(subject).to receive(:update_removed_issues!) + expect(subject.perform!).to be_truthy + end + + it "saves a new request issue" do + issue_payload[:ineligible_due_to_id] = nil + issue_payload[:contested_decision_issue_id] = nil + issue_payload[:decision_review_issue_id] = "some_new_reference_id" + allow(parser).to receive(:added_issues).and_return([issue_payload]) + subject = described_class.new(review: review, user: user, parser: parser, event: event) + expect(subject.perform!).to be_truthy + request_issue = RequestIssue.find_by(reference_id: "some_new_reference_id") + expect(request_issue).to be + expect(request_issue.contested_issue_description).to eq(issue_payload[:contested_issue_description]) + expect(request_issue.nonrating_issue_category).to eq(issue_payload[:nonrating_issue_category]) + expect(request_issue.nonrating_issue_description).to eq(issue_payload[:nonrating_issue_description]) + expect(request_issue.reference_id).to eq(issue_payload[:decision_review_issue_id]) + # expect(request_issue.contention_reference_id).to eq(issue_payload[:contention_reference_id]) + expect(request_issue.contested_decision_issue_id).to eq(issue_payload[:contested_decision_issue_id]) + expect(request_issue.ineligible_due_to_id).to eq(issue_payload[:ineligible_due_to_id]) + expect(request_issue.is_unidentified).to eq(issue_payload[:is_unidentified]) + expect(request_issue.unidentified_issue_text).to eq(issue_payload[:unidentified_issue_text]) + expect(request_issue.vacols_id).to eq(issue_payload[:vacols_id]) + expect(request_issue.vacols_sequence_id).to eq(issue_payload[:vacols_sequence_id]) + expect(request_issue.type).to eq(issue_payload[:type]) + expect(request_issue.edited_description).to eq(issue_payload[:edited_description]) + expect(request_issue.benefit_type).to eq(issue_payload[:benefit_type]) + expect(request_issue.untimely_exemption).to eq(issue_payload[:untimely_exemption]) + expect(request_issue.untimely_exemption_notes).to eq(issue_payload[:untimely_exemption_notes]) + expect(request_issue.ramp_claim_id).to eq(issue_payload[:ramp_claim_id]) + # TODO: Need to validate these fields are set correctly for new request issues + # expect(request_issue.ineligible_reason).to eq(issue_payload[:ineligible_reason]) + # expect(request_issue.rating_issue_associated_at).to eq(issue_payload[:rating_issue_associated_at]) + # expect(request_issue.contested_rating_issue_reference_id).to + # eq(issue_payload[:contested_rating_issue_reference_id]) + # expect(request_issue.contested_rating_issue_diagnostic_code).to + # eq(issue_payload[:contested_rating_issue_diagnostic_code]) + # expect(request_issue.contested_rating_decision_reference_id).to + # eq(issue_payload[:contested_rating_decision_reference_id]) + # expect(request_issue.decision_date).to eq(issue_payload[:decision_date]) + end + end + + describe "#process_eligible_to_ineligible_issues!" do + it "updates the request issues to ineligible" do + issue_payload[:ineligible_reason] = "untimely" + issue_payload[:contention_reference_id] = nil + issue_payload[:closed_at] = 1_625_151_600 + issue_payload[:nonrating_issue_description] = "some_nonrating_issue_description" + allow(parser).to receive(:eligible_to_ineligible_issues).and_return([issue_payload]) + expect( + described_class.new( + review: review, + user: user, + parser: parser, + event: event + ).process_eligible_to_ineligible_issues! + ).to be_truthy + request_issue = RequestIssue.find(existing_request_issue.id) + expect(request_issue.ineligible_reason).to eq(issue_payload[:ineligible_reason]) + expect(request_issue.closed_at).to eq("1970-01-19 14:25:51.000000000 -0500") + expect(request_issue.contention_removed_at).to be + expect(request_issue.contested_issue_description).to eq(issue_payload[:contested_issue_description]) + expect(request_issue.nonrating_issue_category).to eq(issue_payload[:nonrating_issue_category]) + expect(request_issue.nonrating_issue_description).to eq(issue_payload[:nonrating_issue_description]) + expect(request_issue.contention_removed_at).to eq(parser.end_product_establishment_last_synced_at) + expect(request_issue.event_records.last.info["update_type"]).to eq("E2I") + expect(request_issue.event_records.last.info["record_data"]["id"]).to eq(request_issue.id) + expect(request_issue.contention_reference_id).to eq(issue_payload[:contention_reference_id]) + end + end + + describe "#process_ineligible_to_eligible_issues!" do + it "updates the request issues to eligible" do + existing_request_issue.update( + ineligible_reason: "untimely", + closed_status: "ineligible", + closed_at: Time.zone.now, + contention_reference_id: nil, + contention_removed_at: nil + ) + allow(parser).to receive(:ineligible_to_eligible_issues).and_return([issue_payload]) + expect( + described_class.new( + review: review, + user: user, + parser: parser, + event: event + ).process_ineligible_to_eligible_issues! + ).to be_truthy + existing_request_issue.reload + expect(existing_request_issue.ineligible_reason).to eq(nil) + expect(existing_request_issue.closed_status).to eq(nil) + expect(existing_request_issue.closed_at).to eq(nil) + expect(existing_request_issue.contention_reference_id).to eq(issue_payload[:contention_reference_id]) + expect(existing_request_issue.contention_removed_at).to eq(nil) + expect(existing_request_issue.contested_issue_description).to eq(issue_payload[:contested_issue_description]) + expect(existing_request_issue.nonrating_issue_category).to eq(issue_payload[:nonrating_issue_category]) + expect(existing_request_issue.nonrating_issue_description).to eq(issue_payload[:nonrating_issue_description]) + expect(existing_request_issue.event_records.last.info["update_type"]).to eq("I2E") + expect(existing_request_issue.event_records.last.info["record_data"]["id"]).to eq(existing_request_issue.id) + expect(existing_request_issue.contention_reference_id).to eq(issue_payload[:contention_reference_id]) + end + end + + describe "#process_ineligible_to_ineligible_issues!" do + it "updates the request issues to ineligible" do + existing_request_issue.update( + ineligible_reason: "untimely", + closed_status: "ineligible", + closed_at: Time.zone.now + ) + issue_payload[:ineligible_reason] = "before_ama" + issue_payload[:closed_at] = 1_625_151_600 + allow(parser).to receive(:ineligible_to_ineligible_issues).and_return([issue_payload]) + expect( + described_class.new( + review: review, + user: user, + parser: parser, + event: event + ).process_ineligible_to_ineligible_issues! + ).to be_truthy + existing_request_issue.reload + expect(existing_request_issue.ineligible_reason).to eq(issue_payload[:ineligible_reason]) + expect(existing_request_issue.closed_at).to eq("1970-01-19 14:25:51.000000000 -0500") + expect(existing_request_issue.contested_issue_description).to eq(issue_payload[:contested_issue_description]) + expect(existing_request_issue.nonrating_issue_category).to eq(issue_payload[:nonrating_issue_category]) + expect(existing_request_issue.nonrating_issue_description).to eq(issue_payload[:nonrating_issue_description]) + expect(existing_request_issue.event_records.last.info["update_type"]).to eq("I2I") + expect(existing_request_issue.event_records.last.info["record_data"]["id"]).to eq(existing_request_issue.id) + expect(existing_request_issue.contention_removed_at).to eq(parser.end_product_establishment_last_synced_at) + expect(existing_request_issue.contention_reference_id).to eq(issue_payload[:contention_reference_id]) + end + end + + describe "#process_request_issues_data!" do + it "updates addtional fields" do + allow(parser).to receive(:updated_issues).and_return([issue_payload]) + expect( + described_class.new(review: review, user: user, parser: parser, event: event).process_request_issues_data! + ).to be_truthy + existing_request_issue.reload + expect(existing_request_issue.contested_issue_description).to eq(issue_payload[:contested_issue_description]) + expect(existing_request_issue.nonrating_issue_category).to eq(issue_payload[:nonrating_issue_category]) + expect(existing_request_issue.nonrating_issue_description).to eq(issue_payload[:nonrating_issue_description]) + expect(existing_request_issue.contention_updated_at).to eq(parser.end_product_establishment_last_synced_at) + expect(existing_request_issue.contention_reference_id).to eq(issue_payload[:contention_reference_id]) + end + + it "does not update fields if they are not in the payload" do + issue_payload.delete(:contested_issue_description) + issue_payload.delete(:nonrating_issue_category) + issue_payload.delete(:nonrating_issue_description) + allow(parser).to receive(:updated_issues).and_return([issue_payload]) + existing_request_issue.update(contested_issue_description: "original description") + existing_request_issue.update(nonrating_issue_category: "original category") + existing_request_issue.update(nonrating_issue_description: "original description") + expect( + described_class.new(review: review, user: user, parser: parser, event: event).process_request_issues_data! + ).to be_truthy + existing_request_issue.reload + expect(existing_request_issue.contested_issue_description).to eq("original description") + expect(existing_request_issue.nonrating_issue_category).to eq("original category") + expect(existing_request_issue.nonrating_issue_description).to eq("original description") + expect(existing_request_issue.contention_reference_id).to eq(issue_payload[:contention_reference_id]) + end + end + + describe "#removed_issues" do + it "returns an array of request issue data for removed issues" do + allow(parser).to receive(:removed_issues).and_return([issue_payload]) + subject = described_class.new(review: review, user: user, parser: parser, event: event) + expect(subject.removed_issues).to eq([existing_request_issue]) + end + end + + describe "#add_existing_review_issues" do + it "adds existing issues to the request issues data" do + subject = described_class.new(review: review, user: user, parser: parser, event: event) + expect(subject.add_existing_review_issues).to eq( + [ + { + request_issue_id: existing_request_issue.id, + reference_id: existing_request_issue.reference_id + } + ] + ) + end + + it "adds existing issues to the request issue that were originally in CF" do + existing_request_issue.update(reference_id: nil) + allow(parser_issue).to receive(:ri_original_caseflow_request_issue_id).and_return(existing_request_issue.id) + + subject = described_class.new(review: review, user: user, parser: parser, event: event) + result = subject.add_existing_review_issues + + expect(result).to eq( + [ + { + request_issue_id: existing_request_issue.id, + reference_id: nil + } + ] + ) + end + + it "adds existing issues to the request issues data but not removed issues" do + allow(parser).to receive(:removed_issues).and_return([issue_payload]) + subject = described_class.new(review: review, user: user, parser: parser, event: event) + expect(subject.add_existing_review_issues).to eq([]) + end + + it "adds existing issues to the request issues data but not removed issues that are in the parser" do + existing_request_issue.update(reference_id: nil) + issue_payload[:original_caseflow_request_issue_id] = existing_request_issue.id + allow(parser).to receive(:removed_issues).and_return([issue_payload]) + subject = described_class.new(review: review, user: user, parser: parser, event: event) + expect(subject.add_existing_review_issues).to eq([]) + end + end + + describe "#update_removed_issues!" do + it "updates the closed_at date and closed_status for removed issues" do + allow(parser).to receive(:removed_issues).and_return([issue_payload]) + subject = described_class.new(review: review, user: user, parser: parser, event: event) + expect(subject.update_removed_issues!).to be_truthy + existing_request_issue.reload + expect(existing_request_issue.closed_at).to eq("1970-01-19 14:25:51.000000000 -0500") + expect(existing_request_issue.closed_status).to eq(issue_payload[:closed_status]) + expect(existing_request_issue.contention_removed_at).to eq(parser.end_product_establishment_last_synced_at) + expect(existing_request_issue.contention_updated_at).to eq(parser.end_product_establishment_last_synced_at) + end + end + + describe "#process_audit_records!" do + it "updates the audit records for added issues" do + issue_payload[:ineligible_due_to_id] = nil + issue_payload[:contested_decision_issue_id] = nil + issue_payload[:decision_review_issue_id] = "some_new_reference_id" + allow(parser).to receive(:added_issues).and_return([issue_payload]) + subject = described_class.new(review: review, user: user, parser: parser, event: event) + expect(subject.process_audit_records!).to be_truthy + request_issue = RequestIssue.find_by(reference_id: "some_new_reference_id") + expect(request_issue.event_records.last.info["update_type"]).to eq("A") + expect(request_issue.event_records.last.info["record_data"]["id"]).to eq(request_issue.id) + end + + it "updates the audit records for removed issues" do + allow(parser).to receive(:removed_issues).and_return([issue_payload]) + subject = described_class.new(review: review, user: user, parser: parser, event: event) + expect(subject.process_audit_records!).to be_truthy + existing_request_issue.reload + expect(existing_request_issue.event_records.last.info["update_type"]).to eq("R") + expect(existing_request_issue.event_records.last.info["record_data"]["id"]).to eq(existing_request_issue.id) + end + + it "updates the audit records for withdrawn issues" do + allow(parser).to receive(:withdrawn_issues).and_return([issue_payload]) + subject = described_class.new(review: review, user: user, parser: parser, event: event) + expect(subject.process_audit_records!).to be_truthy + existing_request_issue.reload + expect(existing_request_issue.event_records.last.info["update_type"]).to eq("W") + expect(existing_request_issue.event_records.last.info["record_data"]["id"]).to eq(existing_request_issue.id) + end + + it "updates the audit records for edited issues" do + allow(parser).to receive(:updated_issues).and_return([issue_payload]) + subject = described_class.new(review: review, user: user, parser: parser, event: event) + expect(subject.process_audit_records!).to be_truthy + existing_request_issue.reload + expect(existing_request_issue.event_records.last.info["update_type"]).to eq("E") + expect(existing_request_issue.event_records.last.info["record_data"]["id"]).to eq(existing_request_issue.id) + end + end + + describe "#process_job" do + it "updates the statuses to attempted, submitted, processed" do + allow(parser).to receive(:updated_issues).and_return([issue_payload]) + subject = described_class.new(review: review, user: user, parser: parser, event: event) + expect(subject.perform!).to be_truthy + expect(subject.attempted_at).to eq(parser.end_product_establishment_last_synced_at) + expect(subject.submitted_at).to eq(parser.end_product_establishment_last_synced_at) + expect(subject.processed_at).to eq(parser.end_product_establishment_last_synced_at) + end + end +end 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 fa781164453..219cf88fef9 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 @@ -85,6 +85,7 @@ 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 + expect(serialized_request_issue.key?(:reference_id)).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/decision_review_created/create_request_issues_spec.rb b/spec/services/events/decision_review_created/create_request_issues_spec.rb index 46ac93be150..42049af6e37 100644 --- a/spec/services/events/decision_review_created/create_request_issues_spec.rb +++ b/spec/services/events/decision_review_created/create_request_issues_spec.rb @@ -21,8 +21,8 @@ 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) + expect(backfilled_issues.first.event_records.first).to eq(EventRecord.first) + expect(backfilled_issues.last.event_records.first).to eq(EventRecord.last) # check if attributes match ri1 = backfilled_issues.first @@ -91,6 +91,8 @@ expect(LegacyIssue.count).to eq(1) expect(LegacyIssueOptin.count).to eq(1) expect(EventRecord.count).to eq(3) + expect(backfilled_issues.first.event_records.first.info["update_type"]).to eq("I") + expect(backfilled_issues.first.event_records.first.info["record_data"]["reference_id"]).to eq("1") end end @@ -103,10 +105,39 @@ end end + context "when parser_issues.ri_reference_id is nil" do + it "raises Caseflow::Error::DecisionReviewCreatedRequestIssuesError" do + invalid_payload = retrieve_payload + invalid_payload[:request_issues][0][:decision_review_issue_id] = nil + + parser = Events::DecisionReviewCreated::DecisionReviewCreatedParser.new({}, invalid_payload) + + expect do + described_class.process!(event: event, parser: parser, epe: epe, decision_review: higher_level_review) + end.to raise_error(Caseflow::Error::DecisionReviewCreatedRequestIssuesError, "reference_id cannot be null") + end + + it "does not create any RequestIssues when ri_reference_id is nil" do + invalid_payload = retrieve_payload + invalid_payload[:request_issues][0][:decision_review_issue_id] = nil + + parser = Events::DecisionReviewCreated::DecisionReviewCreatedParser.new({}, invalid_payload) + + expect do + described_class.process!(event: event, parser: parser, epe: epe, decision_review: higher_level_review) + end.to raise_error(Caseflow::Error::DecisionReviewCreatedRequestIssuesError) + + # Ensure no RequestIssues or EventRecords are created + expect(RequestIssue.count).to eq(0) + expect(EventRecord.count).to eq(0) + end + end + def retrieve_payload { "request_issues": [ { + "decision_review_issue_id": "1", "benefit_type": "pension", "contested_issue_description": "service connection for arthritis denied", "contention_reference_id": 4_542_785, @@ -134,6 +165,7 @@ def retrieve_payload "nonrating_issue_bgs_source": "Test Source" }, { + "decision_review_issue_id": "2", "benefit_type": "pension", "contested_issue_description": "PTSD", "contention_reference_id": 123_456, diff --git a/spec/services/events/decision_review_created/decision_review_created_issue_parser_spec.rb b/spec/services/events/decision_review_created/decision_review_created_issue_parser_spec.rb new file mode 100644 index 00000000000..dd91900fbcd --- /dev/null +++ b/spec/services/events/decision_review_created/decision_review_created_issue_parser_spec.rb @@ -0,0 +1,334 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe Events::DecisionReviewCreated::DecisionReviewCreatedIssueParser, type: :service do + let(:issue_data) do + { + decision_review_issue_id: "123", + benefit_type: "compensation", + contested_issue_description: "Description", + contention_reference_id: "456", + contested_rating_decision_reference_id: "789", + contested_rating_issue_profile_date: "2023-01-01", + contested_rating_issue_reference_id: "321", + contested_decision_issue_id: "654", + decision_date: "20230101", + ineligible_due_to_id: "987", + ineligible_reason: "duplicate_of_rating_issue_in_active_review", + is_unidentified: false, + unidentified_issue_text: "", + nonrating_issue_category: "Category A", + nonrating_issue_description: "Non-rating issue description", + untimely_exemption: true, + untimely_exemption_notes: "Some notes", + vacols_id: "VAC123", + vacols_sequence_id: "SEQ456", + closed_at: 1_683_072_000_000, # Example timestamp + closed_status: "withdrawn", + contested_rating_issue_diagnostic_code: "7890", + ramp_claim_id: "RAMP123", + rating_issue_associated_at: 1_683_072_000_000, # Example timestamp + nonrating_issue_bgs_id: "BGS987", + nonrating_issue_bgs_source: "source" + } + end + + let(:parser) { described_class.new(issue_data) } + + describe "#ri_reference_id" do + it "returns the decision review issue id" do + expect(parser.ri_reference_id).to eq("123") + end + end + + describe "#ri_benefit_type" do + it "returns the benefit type" do + expect(parser.ri_benefit_type).to eq("compensation") + end + end + + describe "#ri_contested_issue_description" do + it "returns the contested issue description" do + expect(parser.ri_contested_issue_description).to eq("Description") + end + end + + describe "#ri_contention_reference_id" do + it "returns the contention reference id" do + expect(parser.ri_contention_reference_id).to eq("456") + end + end + + describe "#ri_contested_rating_decision_reference_id" do + it "returns the contested rating decision reference id" do + expect(parser.ri_contested_rating_decision_reference_id).to eq("789") + end + end + + describe "#ri_contested_rating_issue_profile_date" do + it "returns the contested rating issue profile date" do + expect(parser.ri_contested_rating_issue_profile_date).to eq("2023-01-01") + end + end + + describe "#ri_contested_rating_issue_reference_id" do + it "returns the contested rating issue reference id" do + expect(parser.ri_contested_rating_issue_reference_id).to eq("321") + end + end + + describe "#ri_contested_decision_issue_id" do + it "returns the contested decision issue id" do + expect(parser.ri_contested_decision_issue_id).to eq("654") + end + end + + describe "#ri_decision_date" do + it "returns the parsed decision date" do + allow(parser).to receive(:logical_date_converter).with("20230101").and_return(Date.new(2023, 1, 1)) + expect(parser.ri_decision_date).to eq(Date.new(2023, 1, 1)) + end + end + + describe "#ri_ineligible_due_to_id" do + it "returns the ineligible due to id" do + expect(parser.ri_ineligible_due_to_id).to eq("987") + end + end + + describe "#ri_ineligible_reason" do + it "returns the ineligible reason" do + expect(parser.ri_ineligible_reason).to eq("duplicate_of_rating_issue_in_active_review") + end + end + + describe "#ri_is_unidentified" do + it "returns the unidentified status" do + expect(parser.ri_is_unidentified).to eq(false) + end + end + + describe "#ri_unidentified_issue_text" do + it "returns the unidentified issue text" do + expect(parser.ri_unidentified_issue_text).to eq(nil) + end + end + + describe "#ri_nonrating_issue_category" do + it "returns the nonrating issue category" do + expect(parser.ri_nonrating_issue_category).to eq("Category A") + end + end + + describe "#ri_nonrating_issue_description" do + it "returns the nonrating issue description" do + expect(parser.ri_nonrating_issue_description).to eq("Non-rating issue description") + end + end + + describe "#ri_untimely_exemption" do + it "returns the untimely exemption status" do + expect(parser.ri_untimely_exemption).to eq(true) + end + end + + describe "#ri_untimely_exemption_notes" do + it "returns the untimely exemption notes" do + expect(parser.ri_untimely_exemption_notes).to eq("Some notes") + end + end + + describe "#ri_vacols_id" do + it "returns the vacols id" do + expect(parser.ri_vacols_id).to eq("VAC123") + end + end + + describe "#ri_vacols_sequence_id" do + it "returns the vacols sequence id" do + expect(parser.ri_vacols_sequence_id).to eq("SEQ456") + end + end + + describe "#ri_closed_at" do + it "returns the closed_at datetime converted from milliseconds" do + allow(parser).to receive(:convert_milliseconds_to_datetime).with(1_683_072_000_000) + .and_return(Time.zone.at(1_683_072_000)) + expect(parser.ri_closed_at).to eq(Time.zone.at(1_683_072_000)) + end + end + + describe "#ri_closed_status" do + it "returns the closed status" do + expect(parser.ri_closed_status).to eq("withdrawn") + end + end + + describe "#ri_contested_rating_issue_diagnostic_code" do + it "returns the contested rating issue diagnostic code" do + expect(parser.ri_contested_rating_issue_diagnostic_code).to eq("7890") + end + end + + describe "#ri_ramp_claim_id" do + it "returns the ramp claim id" do + expect(parser.ri_ramp_claim_id).to eq("RAMP123") + end + end + + describe "#ri_rating_issue_associated_at" do + it "returns the rating issue associated datetime converted from milliseconds" do + allow(parser).to receive(:convert_milliseconds_to_datetime) + .with(1_683_072_000_000).and_return(Time.zone.at(1_683_072_000)) + expect(parser.ri_rating_issue_associated_at).to eq(Time.zone.at(1_683_072_000)) + end + end + + describe "#ri_nonrating_issue_bgs_id" do + it "returns the nonrating issue bgs id" do + expect(parser.ri_nonrating_issue_bgs_id).to eq("BGS987") + end + end + + describe "#ri_nonrating_issue_bgs_source" do + it "returns the nonrating issue bgs source" do + expect(parser.ri_nonrating_issue_bgs_source).to eq("source") + end + end + + describe "peresence logic check" do + let(:payload) do + { + benefit_type: "", + contested_issue_description: "", + contention_reference_id: 7_905_752, + contested_rating_decision_reference_id: "", + contested_rating_issue_profile_date: nil, + contested_rating_issue_reference_id: "", + contested_decision_issue_id: nil, + decision_date: 19_568, + 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", + nonrating_issue_bgs_source: "Test Source" + } + end + + subject { described_class.new(payload) } + + it "parses benefit_type correctly" do + expect(subject.ri_benefit_type).to eq(nil) + end + + it "parses contested_issue_description as nil when absent" do + expect(subject.ri_contested_issue_description).to be_nil + end + + it "parses contention_reference_id correctly" do + expect(subject.ri_contention_reference_id).to eq(7_905_752) + end + + it "parses contested_rating_decision_reference_id as nil when absent" do + expect(subject.ri_contested_rating_decision_reference_id).to be_nil + end + + it "parses contested_rating_issue_profile_date as nil when absent" do + expect(subject.ri_contested_rating_issue_profile_date).to be_nil + end + + it "parses contested_rating_issue_reference_id as nil when absent" do + expect(subject.ri_contested_rating_issue_reference_id).to be_nil + end + + it "parses contested_decision_issue_id correctly" do + expect(subject.ri_contested_decision_issue_id).to be_nil + end + + it "parses decision_date correctly using logical_date_converter" do + expect(subject.ri_decision_date).to eq(subject.logical_date_converter(19_568)) + end + + it "parses ineligible_due_to_id correctly" do + expect(subject.ri_ineligible_due_to_id).to be_nil + end + + it "parses ineligible_reason as nil when absent" do + expect(subject.ri_ineligible_reason).to be_nil + end + + it "parses is_unidentified correctly" do + expect(subject.ri_is_unidentified).to eq(false) + end + + it "parses unidentified_issue_text as nil when absent" do + expect(subject.ri_unidentified_issue_text).to be_nil + end + + it "parses nonrating_issue_category correctly" do + expect(subject.ri_nonrating_issue_category).to eq("Accrued Benefits") + end + + it "parses nonrating_issue_description correctly" do + expect(subject.ri_nonrating_issue_description) + .to eq("The user entered description if the issue is a nonrating issue") + end + + it "parses untimely_exemption correctly" do + expect(subject.ri_untimely_exemption).to be_nil + end + + it "parses untimely_exemption_notes as nil when absent" do + expect(subject.ri_untimely_exemption_notes).to be_nil + end + + it "parses vacols_id as nil when absent" do + expect(subject.ri_vacols_id).to be_nil + end + + it "parses vacols_sequence_id correctly" do + expect(subject.ri_vacols_sequence_id).to be_nil + end + + it "parses closed_at as nil when absent" do + expect(subject.ri_closed_at).to be_nil + end + + it "parses closed_status as nil when absent" do + expect(subject.ri_closed_status).to be_nil + end + + it "parses contested_rating_issue_diagnostic_code correctly" do + expect(subject.ri_contested_rating_issue_diagnostic_code).to be_nil + end + + it "parses ramp_claim_id as nil when absent" do + expect(subject.ri_ramp_claim_id).to be_nil + end + + it "parses rating_issue_associated_at as nil when absent" do + expect(subject.ri_rating_issue_associated_at).to be_nil + end + + it "parses nonrating_issue_bgs_id correctly" do + expect(subject.ri_nonrating_issue_bgs_id).to eq("13") + end + + it "parses nonrating_issue_bgs_source correctly" do + expect(subject.ri_nonrating_issue_bgs_source).to eq("Test Source") + 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 index 92c5d09e6db..084b0eaf949 100644 --- 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 @@ -101,7 +101,7 @@ total_issues = parser.request_issues expect(total_issues.count).to eq(1) issue = total_issues.first - parser_issues = DecisionReviewCreatedIssueParser.new(issue) + parser_issues = Events::DecisionReviewCreated::DecisionReviewCreatedIssueParser.new(issue) expect(parser_issues.ri_benefit_type).to eq response_hash.request_issues.first["benefit_type"] expect(parser_issues.ri_benefit_type).to eq response_hash.request_issues.first["benefit_type"] expect(parser_issues.ri_contested_issue_description).to eq response_hash.request_issues.first["contested_issue_description"] @@ -196,6 +196,64 @@ end end + context "when parsing payload with empty string values" do + let(:empty_string_payload) do + { + css_id: "", + detail_type: "", + station: "", + veteran: { participant_id: "" }, + claimant: { payee_code: "", name_suffix: "" }, + claim_review: { benefit_type: "", receipt_date: "", establishment_submitted_at: "" }, + end_product_establishment: { benefit_type_code: "", claim_date: "", code: "" } + } + end + + let(:headers) { sample_headers } + + subject { described_class.new(headers, empty_string_payload) } + + it "parses css_id as nil when empty string" do + expect(subject.css_id).to be_nil + end + + it "parses detail_type as nil when empty string" do + expect(subject.detail_type).to be_nil + end + + it "parses station_id as nil when empty string" do + expect(subject.station_id).to be_nil + end + + it "parses veteran participant_id as nil when empty string" do + expect(subject.veteran_participant_id).to be_nil + end + + it "parses claimant payee_code as nil when empty string" do + expect(subject.claimant_payee_code).to be_nil + end + + it "parses claimant name_suffix as nil when empty string" do + expect(subject.claimant_name_suffix).to be_nil + end + + it "parses claim_review benefit_type as nil when empty string" do + expect(subject.claim_review_benefit_type).to be_nil + end + + it "parses end_product_establishment benefit_type_code as nil when empty string" do + expect(subject.epe_benefit_type_code).to be_nil + end + + it "parses end_product_establishment claim_date as nil when empty string" do + expect(subject.epe_claim_date).to be_nil + end + + it "parses end_product_establishment code as nil when empty string" do + expect(subject.epe_code).to be_nil + end + end + def read_json_payload JSON.parse(File.read(Rails.root.join("app", "services", diff --git a/spec/services/events/decision_review_created_spec.rb b/spec/services/events/decision_review_created_spec.rb index 9733cef4e86..49e6c1b74c3 100644 --- a/spec/services/events/decision_review_created_spec.rb +++ b/spec/services/events/decision_review_created_spec.rb @@ -3,22 +3,22 @@ describe Events::DecisionReviewCreated do let!(:consumer_event_id) { "123" } let!(:event) { instance_double(Event) } - let!(:reference_id) { "2001" } + let!(:claim_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 } - let!(:params) { { consumer_event_id: consumer_event_id, reference_id: reference_id } } + let!(:params) { { consumer_event_id: consumer_event_id, claim_id: claim_id } } describe "#create!" do subject { described_class.create!(params, headers, read_json_payload) } context "When event is completed info field returns to default state" do - it "event field is an empty json object" do + it "event field is only payload" do subject # runs and completes the process completed = DecisionReviewCreatedEvent.find_by(reference_id: consumer_event_id) - expect(completed.info).to eq({}) + expect(completed.info).to eq({ "event_payload" => json_payload }) end end @@ -29,16 +29,16 @@ 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"\ + .with("Failed to acquire lock for Claim ID: #{claim_id}! This Event is being"\ " processed. Please try again later.") - subject + expect { subject }.to raise_error(RedisMutex::LockError) 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}" + lock_key = "RedisMutex:EndProductEstablishment:#{claim_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) @@ -78,14 +78,25 @@ expect(Rails.logger).to receive(:error) do |message| expect(message).to include(standard_error.message) end - expect { described_class.create!(params, headers, json_payload) } + expect { described_class.create!(params, headers, read_json_payload) } .to raise_error(StandardError) end it "logs the error and updates the event" do expect(Rails.logger).to receive(:error).with(/#{standard_error}/) - expect { subject.create!(consumer_event_id, reference_id) }.to raise_error(StandardError) + expect { described_class.create!(params, headers, read_json_payload) }.to raise_error(StandardError) + end + + it "records an error at the event level" do + expect { described_class.create!(params, headers, read_json_payload) } + .to raise_error(standard_error) + event = DecisionReviewCreatedEvent.find_by(reference_id: consumer_event_id) + expect(event.error).to eq("#{standard_error.class} : #{standard_error.message}") + expect(event.info["failed_claim_id"]).to eq(claim_id) + expect(event.info["error"]).to eq(standard_error.message) + expect(event.info["error_class"]).to eq("StandardError") + expect(event.info["error_backtrace"]).to be_present end end end diff --git a/spec/services/events/decision_review_updated/decision_review_updated_issue_parser_spec.rb b/spec/services/events/decision_review_updated/decision_review_updated_issue_parser_spec.rb new file mode 100644 index 00000000000..ecda640edee --- /dev/null +++ b/spec/services/events/decision_review_updated/decision_review_updated_issue_parser_spec.rb @@ -0,0 +1,314 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe Events::DecisionReviewUpdated::DecisionReviewUpdatedIssueParser do + let(:issue_data) do + { + decision_review_issue_id: 1, + benefit_type: "compensation", + contested_decision_issue_id: 201, + contested_issue_description: "Service connection for PTSD", + contention_reference_id: 7_905_752, + contested_rating_decision_reference_id: nil, + contested_rating_issue_profile_date: 1_625_076_000, + contested_rating_issue_reference_id: "REF9411", + decision_date: 19_568, + ineligible_due_to_id: 301, + ineligible_reason: nil, + is_unidentified: false, + unidentified_issue_text: nil, + nonrating_issue_category: "Accrued Benefits", + nonrating_issue_description: "Chapter 35 benefits", + untimely_exemption: nil, + untimely_exemption_notes: nil, + vacols_id: "VAC123", + vacols_sequence_id: nil, + closed_at: 1_625_151_600, + closed_status: "withdrawn", + contested_rating_issue_diagnostic_code: "9411", + ramp_claim_id: "RAMP123", + rating_issue_associated_at: 1_625_076_000, + nonrating_issue_bgs_id: "13", + nonrating_issue_bgs_source: "CORP_AWARD_ATTORNEY_FEE", + type: "RequestIssue", + original_caseflow_request_issue_id: 679, + decision: [ + { + id: 1738, + award_event_id: 679, + category: "decision", + contention_id: 35, + decision_source: "the source", + decision_recorded_time: nil, + decision_text: "", + description: nil, + disposition: nil, + dta_error_explanation: nil, + rating_profile_date: nil, + decision_finalized_time: nil + } + ] + } + end + + subject { described_class.new(issue_data) } + + describe "#ri_reference_id" do + it "returns the decision_review_issue_id" do + expect(subject.ri_reference_id).to eq(1) + end + end + + describe "#ri_benefit_type" do + it "returns the benefit_type" do + expect(subject.ri_benefit_type).to eq("compensation") + end + end + + describe "#ri_closed_at" do + it "converts closed_at from milliseconds to datetime" do + expect(subject.ri_closed_at).to eq(Time.at(1_625_151_600 / 1000).utc) + end + end + + describe "#ri_closed_status" do + it "returns the closed_status" do + expect(subject.ri_closed_status).to eq("withdrawn") + end + end + + describe "#ri_contested_issue_description" do + it "returns the contested_issue_description" do + expect(subject.ri_contested_issue_description).to eq("Service connection for PTSD") + end + end + + describe "#ri_contention_reference_id" do + it "returns the contention_reference_id" do + expect(subject.ri_contention_reference_id).to eq(7_905_752) + end + end + + describe "#ri_contested_rating_issue_diagnostic_code" do + it "returns the contested_rating_issue_diagnostic_code" do + expect(subject.ri_contested_rating_issue_diagnostic_code).to eq("9411") + end + end + + describe "#ri_contested_rating_decision_reference_id" do + it "returns the contested_rating_decision_reference_id" do + expect(subject.ri_contested_rating_decision_reference_id).to be_nil + end + end + + describe "#ri_contested_rating_issue_profile_date" do + it "returns the contested_rating_issue_profile_date as timestamp" do + expect(subject.ri_contested_rating_issue_profile_date).to eq(1_625_076_000) + end + end + + describe "#ri_contested_rating_issue_reference_id" do + it "returns the contested_rating_issue_reference_id" do + expect(subject.ri_contested_rating_issue_reference_id).to eq("REF9411") + end + end + + describe "#ri_contested_decision_issue_id" do + it "returns the contested_decision_issue_id" do + expect(subject.ri_contested_decision_issue_id).to eq(201) + end + end + + describe "#ri_decision_date" do + it "returns the decision_date as logical date" do + expect(subject.ri_decision_date).to eq(Date.new(2023, 7, 30)) + end + end + + describe "#ri_ineligible_due_to_id" do + it "returns the ineligible_due_to_id" do + expect(subject.ri_ineligible_due_to_id).to eq(301) + end + end + + describe "#ri_ineligible_reason" do + it "returns the ineligible_reason" do + expect(subject.ri_ineligible_reason).to be_nil + end + end + + describe "#ri_is_unidentified" do + it "returns the is_unidentified" do + expect(subject.ri_is_unidentified).to be false + end + end + + describe "#ri_unidentified_issue_text" do + it "returns the unidentified_issue_text" do + expect(subject.ri_unidentified_issue_text).to be_nil + end + end + + describe "#ri_nonrating_issue_category" do + it "returns the nonrating_issue_category" do + expect(subject.ri_nonrating_issue_category).to eq("Accrued Benefits") + end + end + + describe "#ri_nonrating_issue_description" do + it "returns the nonrating_issue_description" do + expect(subject.ri_nonrating_issue_description).to eq("Chapter 35 benefits") + end + end + + describe "#ri_nonrating_issue_bgs_id" do + it "returns the nonrating_issue_bgs_id" do + expect(subject.ri_nonrating_issue_bgs_id).to eq("13") + end + end + + describe "#ri_nonrating_issue_bgs_source" do + it "returns the nonrating_issue_bgs_source" do + expect(subject.ri_nonrating_issue_bgs_source).to eq("CORP_AWARD_ATTORNEY_FEE") + end + end + + describe "#ri_ramp_claim_id" do + it "returns the ramp_claim_id" do + expect(subject.ri_ramp_claim_id).to eq("RAMP123") + end + end + + describe "#ri_rating_issue_associated_at" do + it "converts rating_issue_associated_at from milliseconds to datetime" do + expect(subject.ri_rating_issue_associated_at).to eq(Time.at(1_625_076_000 / 1000).utc) + end + end + + describe "#ri_untimely_exemption" do + it "returns the untimely_exemption" do + expect(subject.ri_untimely_exemption).to be_nil + end + end + + describe "#ri_untimely_exemption_notes" do + it "returns the untimely_exemption_notes" do + expect(subject.ri_untimely_exemption_notes).to be_nil + end + end + + describe "#ri_vacols_id" do + it "returns the vacols_id" do + expect(subject.ri_vacols_id).to eq("VAC123") + end + end + + describe "#ri_vacols_sequence_id" do + it "returns the vacols_sequence_id" do + expect(subject.ri_vacols_sequence_id).to be_nil + end + end + + describe "#ri_type" do + it "returns the type" do + expect(subject.ri_type).to eq("RequestIssue") + end + end + + describe "#original_caseflow_request_issue_id" do + it "returns the original caseflow request issue id" do + expect(subject.ri_original_caseflow_request_issue_id).to eq(679) + end + end + + context "when attributes use .presence and values are empty strings" do + let(:empty_issue_data) do + issue_data.merge( + benefit_type: "", + contested_issue_description: "", + contested_rating_issue_diagnostic_code: "", + contested_rating_decision_reference_id: "", + contested_rating_issue_profile_date: "", + contested_rating_issue_reference_id: "", + ineligible_reason: "", + unidentified_issue_text: "", + nonrating_issue_category: "", + nonrating_issue_description: "", + nonrating_issue_bgs_id: "", + nonrating_issue_bgs_source: "", + ramp_claim_id: "", + untimely_exemption_notes: "", + vacols_id: "", + veteran_participant_id: "", + type: "" + ) + end + + subject { described_class.new(empty_issue_data) } + + it "returns nil for benefit_type if the value is an empty string" do + expect(subject.ri_benefit_type).to be_nil + end + + it "returns nil for contested_issue_description if the value is an empty string" do + expect(subject.ri_contested_issue_description).to be_nil + end + + it "returns nil for contested_rating_issue_diagnostic_code if the value is an empty string" do + expect(subject.ri_contested_rating_issue_diagnostic_code).to be_nil + end + + it "returns nil for contested_rating_decision_reference_id if the value is an empty string" do + expect(subject.ri_contested_rating_decision_reference_id).to be_nil + end + + it "returns nil for contested_rating_issue_profile_date if the value is an empty string" do + expect(subject.ri_contested_rating_issue_profile_date).to be_nil + end + + it "returns nil for contested_rating_issue_reference_id if the value is an empty string" do + expect(subject.ri_contested_rating_issue_reference_id).to be_nil + end + + it "returns nil for ineligible_reason if the value is an empty string" do + expect(subject.ri_ineligible_reason).to be_nil + end + + it "returns nil for unidentified_issue_text if the value is an empty string" do + expect(subject.ri_unidentified_issue_text).to be_nil + end + + it "returns nil for nonrating_issue_category if the value is an empty string" do + expect(subject.ri_nonrating_issue_category).to be_nil + end + + it "returns nil for nonrating_issue_description if the value is an empty string" do + expect(subject.ri_nonrating_issue_description).to be_nil + end + + it "returns nil for nonrating_issue_bgs_id if the value is an empty string" do + expect(subject.ri_nonrating_issue_bgs_id).to be_nil + end + + it "returns nil for nonrating_issue_bgs_source if the value is an empty string" do + expect(subject.ri_nonrating_issue_bgs_source).to be_nil + end + + it "returns nil for ramp_claim_id if the value is an empty string" do + expect(subject.ri_ramp_claim_id).to be_nil + end + + it "returns nil for untimely_exemption_notes if the value is an empty string" do + expect(subject.ri_untimely_exemption_notes).to be_nil + end + + it "returns nil for vacols_id if the value is an empty string" do + expect(subject.ri_vacols_id).to be_nil + end + + it "returns nil for type if the value is an empty string" do + expect(subject.ri_type).to be_nil + end + end +end diff --git a/spec/services/events/decision_review_updated/decision_review_updated_parser_spec.rb b/spec/services/events/decision_review_updated/decision_review_updated_parser_spec.rb new file mode 100644 index 00000000000..68978163d13 --- /dev/null +++ b/spec/services/events/decision_review_updated/decision_review_updated_parser_spec.rb @@ -0,0 +1,534 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe Events::DecisionReviewUpdated::DecisionReviewUpdatedParser do + include ParserHelper + let(:headers) do + { + "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 + + let(:payload) do + file_path = Rails.root.join("app", "services", "events", "decision_review_updated", + "decision_review_updated_example.json") + JSON.parse(File.read(file_path)) + end + + let(:added_issues_payload) do + [{ + benefit_type: "compensation", + closed_at: nil, + closed_status: nil, + contention_reference_id: 123_456, + contested_decision_issue_id: nil, + contested_issue_description: nil, + contested_rating_decision_reference_id: nil, + contested_rating_issue_diagnostic_code: nil, + contested_rating_issue_profile_date: nil, + contested_rating_issue_reference_id: nil, + edited_description: "DIC: Service connection denied (UPDATED)", + decision_date: nil, + decision_review_issue_id: 772_37, + ineligible_due_to_id: nil, + ineligible_reason: nil, + is_unidentified: true, + nonrating_issue_bgs_id: nil, + nonrating_issue_bgs_source: nil, + nonrating_issue_category: nil, + nonrating_issue_description: nil, + original_caseflow_request_issue_id: 123_45, + ramp_claim_id: nil, + rating_issue_associated_at: nil, + type: "RequestIssue", + unidentified_issue_text: "An unidentified issue added during the edit", + untimely_exemption: false, + untimely_exemption_notes: nil, + vacols_id: nil, + vacols_sequence_id: nil + }] + end + + let(:eligible_to_ineligible_issues_payload) do + [{ + benefit_type: "compensation", + closed_at: nil, + closed_status: nil, + contention_reference_id: 123_456, + contested_decision_issue_id: nil, + contested_issue_description: nil, + contested_rating_decision_reference_id: nil, + contested_rating_issue_diagnostic_code: nil, + contested_rating_issue_profile_date: nil, + contested_rating_issue_reference_id: nil, + edited_description: "DIC: Service connection denied (UPDATED)", + decision_date: nil, + decision_review_issue_id: 123, + ineligible_due_to_id: nil, + ineligible_reason: nil, + is_unidentified: true, + nonrating_issue_bgs_id: nil, + nonrating_issue_bgs_source: nil, + nonrating_issue_category: nil, + nonrating_issue_description: nil, + original_caseflow_request_issue_id: 123_45, + ramp_claim_id: nil, + rating_issue_associated_at: nil, + type: "RequestIssue", + unidentified_issue_text: "An unidentified issue added during the edit", + untimely_exemption: false, + untimely_exemption_notes: nil, + vacols_id: nil, + vacols_sequence_id: nil + }] + end + + let(:updated_issues) do + [{ + benefit_type: "compensation", + closed_at: nil, + closed_status: nil, + contention_reference_id: 123_456, + contested_decision_issue_id: nil, + contested_issue_description: nil, + contested_rating_decision_reference_id: nil, + contested_rating_issue_diagnostic_code: nil, + contested_rating_issue_profile_date: nil, + contested_rating_issue_reference_id: nil, + edited_description: "DIC: Service connection denied (UPDATED)", + decision_date: nil, + decision_review_issue_id: 908, + ineligible_due_to_id: nil, + ineligible_reason: nil, + is_unidentified: true, + nonrating_issue_bgs_id: nil, + nonrating_issue_bgs_source: nil, + nonrating_issue_category: nil, + nonrating_issue_description: nil, + original_caseflow_request_issue_id: 123_45, + ramp_claim_id: nil, + rating_issue_associated_at: nil, + type: "RequestIssue", + unidentified_issue_text: "An unidentified issue added during the edit", + untimely_exemption: false, + untimely_exemption_notes: nil, + vacols_id: nil, + vacols_sequence_id: nil + }] + end + + let(:removed_issues) do + [{ + benefit_type: "compensation", + closed_at: nil, + closed_status: nil, + contention_reference_id: 123_456, + contested_decision_issue_id: nil, + contested_issue_description: nil, + contested_rating_decision_reference_id: nil, + contested_rating_issue_diagnostic_code: nil, + contested_rating_issue_profile_date: nil, + contested_rating_issue_reference_id: nil, + edited_description: "DIC: Service connection denied (UPDATED)", + decision_date: nil, + decision_review_issue_id: 8755, + ineligible_due_to_id: nil, + ineligible_reason: nil, + is_unidentified: true, + nonrating_issue_bgs_id: nil, + nonrating_issue_bgs_source: nil, + nonrating_issue_category: nil, + nonrating_issue_description: nil, + original_caseflow_request_issue_id: 123_45, + ramp_claim_id: nil, + rating_issue_associated_at: nil, + type: "RequestIssue", + unidentified_issue_text: "An unidentified issue added during the edit", + untimely_exemption: false, + untimely_exemption_notes: nil, + vacols_id: nil, + vacols_sequence_id: nil + }] + end + + let(:withdrawn_issues) do + [{ + benefit_type: "compensation", + closed_at: nil, + closed_status: nil, + contention_reference_id: 123_456, + contested_decision_issue_id: nil, + contested_issue_description: nil, + contested_rating_decision_reference_id: nil, + contested_rating_issue_diagnostic_code: nil, + contested_rating_issue_profile_date: nil, + contested_rating_issue_reference_id: nil, + edited_description: "DIC: Service connection denied (UPDATED)", + decision_date: nil, + decision_review_issue_id: 9876, + ineligible_due_to_id: nil, + ineligible_reason: nil, + is_unidentified: true, + nonrating_issue_bgs_id: nil, + nonrating_issue_bgs_source: nil, + nonrating_issue_category: nil, + nonrating_issue_description: nil, + original_caseflow_request_issue_id: 123_45, + ramp_claim_id: nil, + rating_issue_associated_at: nil, + type: "RequestIssue", + unidentified_issue_text: "An unidentified issue added during the edit", + untimely_exemption: false, + untimely_exemption_notes: nil, + vacols_id: nil, + vacols_sequence_id: nil + }] + end + + let(:ineligible_to_ineligible_issues_payload) do + [{ + benefit_type: "compensation", + closed_at: nil, + closed_status: nil, + contention_reference_id: 123_456, + contested_decision_issue_id: nil, + contested_issue_description: nil, + contested_rating_decision_reference_id: nil, + contested_rating_issue_diagnostic_code: nil, + contested_rating_issue_profile_date: nil, + contested_rating_issue_reference_id: nil, + edited_description: "DIC: Service connection denied (UPDATED)", + decision_date: nil, + decision_review_issue_id: 234, + ineligible_due_to_id: nil, + ineligible_reason: nil, + is_unidentified: true, + nonrating_issue_bgs_id: nil, + nonrating_issue_bgs_source: nil, + nonrating_issue_category: nil, + nonrating_issue_description: nil, + original_caseflow_request_issue_id: 123_45, + ramp_claim_id: nil, + rating_issue_associated_at: nil, + type: "RequestIssue", + unidentified_issue_text: "An unidentified issue added during the edit", + untimely_exemption: false, + untimely_exemption_notes: nil, + vacols_id: nil, + vacols_sequence_id: nil + }] + end + + let(:ineligible_to_eligible_issues_payload) do + [{ + benefit_type: "compensation", + closed_at: nil, + closed_status: nil, + contention_reference_id: 123_456, + contested_decision_issue_id: nil, + contested_issue_description: nil, + contested_rating_decision_reference_id: nil, + contested_rating_issue_diagnostic_code: nil, + contested_rating_issue_profile_date: nil, + contested_rating_issue_reference_id: nil, + edited_description: "DIC: Service connection denied (UPDATED)", + decision_date: nil, + decision_review_issue_id: 876, + ineligible_due_to_id: nil, + ineligible_reason: nil, + is_unidentified: true, + nonrating_issue_bgs_id: nil, + nonrating_issue_bgs_source: nil, + nonrating_issue_category: nil, + nonrating_issue_description: nil, + original_caseflow_request_issue_id: 123_45, + ramp_claim_id: nil, + rating_issue_associated_at: nil, + type: "RequestIssue", + unidentified_issue_text: "An unidentified issue added during the edit", + untimely_exemption: false, + untimely_exemption_notes: nil, + vacols_id: nil, + vacols_sequence_id: nil + }] + end + + subject { described_class.new(headers, payload) } + + describe "attributes" do + it "returns the correct event_id" do + expect(subject.event_id).to eq(214_706) + end + + it "returns the correct css_id" do + expect(subject.css_id).to eq("BVADWISE101") + end + + it "returns the correct claim_id" do + expect(subject.claim_id).to eq(123_456_7) + end + + it "returns the correct detail_type" do + expect(subject.detail_type).to eq("HigherLevelReview") + end + + it "returns the correct station" do + expect(subject.station_id).to eq("101") + end + + describe "claim_review" do + it "returns the correct informal_conference" do + expect(subject.claim_review_informal_conference).to eq(false) + end + + it "returns the correct same_office" do + expect(subject.claim_review_same_office).to eq(false) + end + + it "returns the correct legacy_opt_in_approved" do + expect(subject.claim_review_legacy_opt_in_approved).to eq(false) + end + end + + describe "end_product_establishment" do + it "returns the correct development_item_reference_id" do + expect(subject.end_product_establishment_development_item_reference_id).to eq("1") + end + + it "returns the correct reference_id" do + expect(subject.end_product_establishment_reference_id).to eq("1234567") + end + end + + # We are testing that each attribute returns the correct value + describe "updated_issues" do + it "returns an empty array if no updated issues" do + expect(subject.updated_issues).to eq(updated_issues) + end + end + + describe "added_issues" do + it "returns an empty array if no updated issues" do + expect(subject.added_issues).to eq(added_issues_payload) + end + end + + describe "eligible_to_ineligible_issues" do + it "returns an empty array if no eligible_to_ineligible_issues" do + expect(subject.eligible_to_ineligible_issues).to eq(eligible_to_ineligible_issues_payload) + end + end + + describe "ineligible_to_eligible_issues" do + it "returns an empty array if no ineligible_to_eligible_issues" do + expect(subject.ineligible_to_eligible_issues).to eq(ineligible_to_eligible_issues_payload) + end + end + + describe "ineligible_to_ineligible_issues" do + it "returns an empty array if no ineligible_to_ineligible_issues" do + expect(subject.ineligible_to_ineligible_issues).to eq(ineligible_to_ineligible_issues_payload) + end + end + + describe "withdrawn_issues" do + it "returns an empty array if no uwithdrawn_issues" do + expect(subject.withdrawn_issues).to eq(withdrawn_issues) + end + end + + describe "removed_issues" do + it "returns an empty array if no removed_issues" do + expect(subject.removed_issues).to eq(removed_issues) + end + end + + describe "end_product_establishment_code" do + it "returns the correct end_product_establishment_code" do + expect(subject.end_product_establishment_code).to eq(payload["end_product_establishment"]["code"]) + end + end + + describe "end_product_establishment_synced_status" do + it "returns the correct end_product_establishment_synced_status" do + expect(subject.end_product_establishment_synced_status) + .to eq(payload["end_product_establishment"]["synced_status"]) + end + end + + describe "end_product_establishment_last_synced_at" do + it "returns the correct end_product_establishment_last_synced_at" do + expect(subject.end_product_establishment_last_synced_at) + .to eq(convert_milliseconds_to_datetime(payload["end_product_establishment"]["last_synced_at"])) + end + end + + describe "original_source" do + it "returns the correct original_source" do + expect(subject.original_source).to eq(payload["original_source"]) + end + end + + describe "decision_review_type" do + it "returns the correct decision_review_type" do + expect(subject.decision_review_type).to eq(payload["decision_review_type"]) + end + end + + describe "veteran_first_name" do + it "returns the correct veteran_first_name" do + expect(subject.veteran_first_name).to eq(payload["veteran_first_name"]) + end + end + + describe "veteran_last_name" do + it "returns the correct veteran_last_name" do + expect(subject.veteran_last_name).to eq(payload["veteran_last_name"]) + end + end + + describe "veteran_participant_id" do + it "returns the correct veteran_participant_id" do + expect(subject.veteran_participant_id).to eq(payload["veteran_participant_id"]) + end + end + + describe "file_number" do + it "returns the correct file_number" do + expect(subject.file_number).to eq(payload["file_number"]) + end + end + + describe "claimant_participant_id" do + it "returns the correct claimant_participant_id" do + expect(subject.claimant_participant_id).to eq(payload["claimant_participant_id"]) + end + end + + describe "claim_category" do + it "returns the correct claim_category" do + expect(subject.claim_category).to eq(payload["claim_category"]) + end + end + + describe "claim_received_date" do + it "returns the correct claim_received_date" do + expect(subject.claim_received_date).to eq(payload["claim_received_date"]) + end + end + + describe "claim_lifecycle_status" do + it "returns the correct claim_lifecycle_status" do + expect(subject.claim_lifecycle_status).to eq(payload["claim_lifecycle_status"]) + end + end + + describe "payee_code" do + it "returns the correct payee_code" do + expect(subject.payee_code).to eq(payload["payee_code"]) + end + end + + describe "ols_issue" do + it "returns the correct ols_issue" do + expect(subject.ols_issue).to eq(payload["ols_issue"]) + end + end + + describe "originated_from_vacols_issue" do + it "returns the correct originated_from_vacols_issue" do + expect(subject.originated_from_vacols_issue).to eq(payload["originated_from_vacols_issue"]) + end + end + + describe "limited_poa_code" do + it "returns the correct limited_poa_code" do + expect(subject.limited_poa_code).to eq(payload["limited_poa_code"]) + end + end + + describe "tracked_item_action" do + it "returns the correct tracked_item_action" do + expect(subject.tracked_item_action).to eq(payload["tracked_item_action"]) + end + end + + describe "tracked_item_id" do + it "returns the correct tracked_item_id" do + expect(subject.tracked_item_id).to eq(payload["tracked_item_id"]) + end + end + + describe "informal_conference_requested" do + it "returns the correct informal_conference_requested" do + expect(subject.informal_conference_requested).to eq(payload["informal_conference_requested"]) + end + end + + describe "same_station_review_requested" do + it "returns the correct same_station_review_requested" do + expect(subject.same_station_review_requested).to eq(payload["same_station_review_requested"]) + end + end + + describe "claim_time" do + it "returns the correct claim_time" do + expect(subject.claim_time).to eq(payload["claim_time"]) + end + end + + describe "catror_username" do + it "returns the correct catror_username" do + expect(subject.catror_username).to eq(payload["catror_username"]) + end + end + + describe "catror_application" do + it "returns the correct catror_application" do + expect(subject.catror_application).to eq(payload["catror_application"]) + end + end + + describe "auto_remand" do + it "returns the correct auto_remand" do + expect(subject.auto_remand).to eq(payload["auto_remand"]) + end + end + end + + context "when attributes use .presence and values are empty strings" do + let(:empty_payload) do + payload.merge( + css_id: "", + detail_type: "", + end_product_establishment: { + development_item_reference_id: "", + reference_id: "" + } + ) + end + + subject { described_class.new(headers, empty_payload) } + + it "returns nil for css_id if the value is an empty string" do + expect(subject.css_id).to be_nil + end + + it "returns nil for detail_type if the value is an empty string" do + expect(subject.detail_type).to be_nil + end + + it "returns not nil for development_item_reference_id if the value is not an empty string" do + expect(subject.end_product_establishment_development_item_reference_id).to be_nil + end + + it "returns not nil for reference_id if the value is not an empty string" do + expect(subject.end_product_establishment_reference_id).to be_nil + end + end +end diff --git a/spec/services/events/decision_review_updated/update_claim_review_spec.rb b/spec/services/events/decision_review_updated/update_claim_review_spec.rb new file mode 100644 index 00000000000..53a3fb51050 --- /dev/null +++ b/spec/services/events/decision_review_updated/update_claim_review_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +RSpec.describe Events::DecisionReviewUpdated::UpdateClaimReview do + let!(:event) { DecisionReviewUpdatedEvent.create!(reference_id: "1") } + let!(:epe) { create(:end_product_establishment, :active_hlr) } + let!(:hlr) { epe.source } + let!(:payload) { Events::DecisionReviewUpdated::DecisionReviewUpdatedParser.example_response } + + describe ".process" do + context "legacy_opt_in_approved true to false" do + before do + hlr.update!(legacy_opt_in_approved: true) + end + + it "updates the value to false" do + hash = JSON.parse(payload) + hash["detail_type"] = "HigherLevelReview" + hash["end_product_establishment"]["reference_id"] = epe.reference_id.to_s + hash["claim_review"]["legacy_opt_in_approved"] = false + parser = Events::DecisionReviewUpdated::DecisionReviewUpdatedParser.new({}, hash) + expect do + described_class.process!(event: event, parser: parser) + end.to change { EventRecord.count }.by(1) + + hlr.reload + expect(hlr.legacy_opt_in_approved).to eq(false) + end + end + context "legacy_opt_in_approved false to true" do + before do + hlr.update!(legacy_opt_in_approved: false) + end + + it "updates the value to true" do + hash = JSON.parse(payload) + hash["detail_type"] = "HigherLevelReview" + hash["end_product_establishment"]["reference_id"] = epe.reference_id.to_s + hash["claim_review"]["legacy_opt_in_approved"] = true + parser = Events::DecisionReviewUpdated::DecisionReviewUpdatedParser.new({}, hash) + expect do + described_class.process!(event: event, parser: parser) + end.to change { EventRecord.count }.by(1) + + hlr.reload + expect(hlr.legacy_opt_in_approved).to eq(true) + end + end + end +end diff --git a/spec/services/events/decision_review_updated/update_end_product_establishment_spec.rb b/spec/services/events/decision_review_updated/update_end_product_establishment_spec.rb new file mode 100644 index 00000000000..bff1603ed8c --- /dev/null +++ b/spec/services/events/decision_review_updated/update_end_product_establishment_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +RSpec.describe Events::DecisionReviewUpdated::UpdateEndProductEstablishment do + let!(:event) { DecisionReviewUpdatedEvent.create!(reference_id: "1") } + let!(:epe) { create(:end_product_establishment, :active_hlr) } + let!(:payload) { Events::DecisionReviewUpdated::DecisionReviewUpdatedParser.example_response } + + describe ".process" do + context "updating code, synced_status and last_synced_at" do + before do + epe.update!( + code: nil, + synced_status: nil, + last_synced_at: nil + ) + end + + it "updates the value to false" do + hash = JSON.parse(payload) + hash["detail_type"] = "HigherLevelReview" + hash["end_product_establishment"]["reference_id"] = epe.reference_id.to_s + parser = Events::DecisionReviewUpdated::DecisionReviewUpdatedParser.new({}, hash) + expect do + described_class.process!(event: event, parser: parser) + end.to change { EventRecord.count }.by(1) + + epe.reload + expect(epe.code).to eq(parser.end_product_establishment_code) + expect(epe.synced_status).to eq(parser.end_product_establishment_synced_status) + expect(epe.last_synced_at).to eq(parser.end_product_establishment_last_synced_at) + end + end + end +end diff --git a/spec/services/events/decision_review_updated/update_informal_conference_spec.rb b/spec/services/events/decision_review_updated/update_informal_conference_spec.rb new file mode 100644 index 00000000000..f14028e16c0 --- /dev/null +++ b/spec/services/events/decision_review_updated/update_informal_conference_spec.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +RSpec.describe Events::DecisionReviewUpdated::UpdateInformalConference do + let!(:event) { DecisionReviewUpdatedEvent.create!(reference_id: "1") } + let!(:epe) { create(:end_product_establishment, :active_hlr) } + let!(:hlr) { epe.source } + let!(:payload) { Events::DecisionReviewUpdated::DecisionReviewUpdatedParser.example_response } + + describe ".process" do + context "informal_conference true to false" do + before do + hlr.update!(informal_conference: true) + end + + it "updates the value to false" do + hash = JSON.parse(payload) + hash["detail_type"] = "HigherLevelReview" + hash["end_product_establishment"]["reference_id"] = epe.reference_id.to_s + hash["claim_review"]["informal_conference"] = false + parser = Events::DecisionReviewUpdated::DecisionReviewUpdatedParser.new({}, hash) + expect do + described_class.process!(event: event, parser: parser) + end.to change { EventRecord.count }.by(1) + + hlr.reload + expect(hlr.informal_conference).to eq(false) + end + end + context "informal_conference false to true" do + before do + hlr.update!(informal_conference: false) + end + + it "updates the value to true" do + hash = JSON.parse(payload) + hash["detail_type"] = "HigherLevelReview" + hash["end_product_establishment"]["reference_id"] = epe.reference_id.to_s + hash["claim_review"]["informal_conference"] = true + parser = Events::DecisionReviewUpdated::DecisionReviewUpdatedParser.new({}, hash) + expect do + described_class.process!(event: event, parser: parser) + end.to change { EventRecord.count }.by(1) + + hlr.reload + expect(hlr.informal_conference).to eq(true) + end + end + + context "same_office true to false" do + before do + hlr.update!(same_office: true) + end + + it "updates the value to false" do + hash = JSON.parse(payload) + hash["detail_type"] = "HigherLevelReview" + hash["end_product_establishment"]["reference_id"] = epe.reference_id.to_s + hash["claim_review"]["same_office"] = false + parser = Events::DecisionReviewUpdated::DecisionReviewUpdatedParser.new({}, hash) + expect do + described_class.process!(event: event, parser: parser) + end.to change { EventRecord.count }.by(1) + + hlr.reload + expect(hlr.same_office).to eq(false) + end + end + context "same_office false to true" do + before do + hlr.update!(same_office: false) + end + + it "updates the value to true" do + hash = JSON.parse(payload) + hash["detail_type"] = "HigherLevelReview" + hash["end_product_establishment"]["reference_id"] = epe.reference_id.to_s + hash["claim_review"]["same_office"] = true + parser = Events::DecisionReviewUpdated::DecisionReviewUpdatedParser.new({}, hash) + expect do + described_class.process!(event: event, parser: parser) + end.to change { EventRecord.count }.by(1) + + hlr.reload + expect(hlr.same_office).to eq(true) + end + end + end +end diff --git a/spec/services/events/decision_review_updated_spec.rb b/spec/services/events/decision_review_updated_spec.rb new file mode 100644 index 00000000000..13ca8563464 --- /dev/null +++ b/spec/services/events/decision_review_updated_spec.rb @@ -0,0 +1,152 @@ +# frozen_string_literal: true + +describe Events::DecisionReviewUpdated do + let(:consumer_event_id) { "1234567890" } + let(:claim_id) { "12201" } + let(:event) { DecisionReviewUpdatedEvent.new(reference_id: consumer_event_id) } + let(:parser) do + double( + "Parser", + css_id: "css_id", + station_id: "station_id", + end_product_establishment_reference_id: "ep_ref_id" + ) + end + let(:review) { double("Review", reference_id: "1234567890") } + let(:params) { { consumer_event_id: consumer_event_id, claim_id: claim_id } } + let(:headers) { {} } + let(:payload) { {} } + let(:user) { double("User") } + let(:end_product_establishment) do + double("EndProductEstablishment", source: review) + end + subject { described_class.update!(params, headers, payload) } + + before do + allow(DecisionReviewUpdatedEvent).to receive(:find_or_create_by) + .with(reference_id: consumer_event_id).and_return(event) + allow(Events::DecisionReviewUpdated::DecisionReviewUpdatedParser).to receive(:new) + .with(headers, payload).and_return(parser) + allow(Events::CreateUserOnEvent).to receive(:handle_user_creation_on_event) + .with(event: event, css_id: parser.css_id, station_id: parser.station_id).and_return(user) + allow(EndProductEstablishment).to receive(:find_by) + .with(reference_id: parser.end_product_establishment_reference_id).and_return(end_product_establishment) + allow(Events::DecisionReviewUpdated::UpdateInformalConference).to receive(:process!) + .with(event: event, parser: parser).and_return(nil) + allow(Events::DecisionReviewUpdated::UpdateClaimReview).to receive(:process!) + .with(event: event, parser: parser).and_return(nil) + allow(Events::DecisionReviewUpdated::UpdateEndProductEstablishment).to receive(:process!) + .with(event: event, parser: parser).and_return(nil) + allow(RequestIssuesUpdateEvent).to receive(:new) + .with(user: user, review: review, parser: parser, event: event) + .and_return(double("RequestIssuesUpdateEvent", perform!: nil)) + end + + describe ".update!" do + subject { described_class.update!(params, headers, 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: #{claim_id}! This Event is being"\ + " processed. Please try again later.") + expect { subject }.to raise_error(RedisMutex::LockError) + 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:#{claim_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 + + it "finds or creates an event" do + expect(DecisionReviewUpdatedEvent).to receive(:find_or_create_by).with(reference_id: consumer_event_id) + subject + end + + it "creates a user" do + expect(Events::CreateUserOnEvent).to receive(:handle_user_creation_on_event) + .with(event: event, css_id: parser.css_id, station_id: parser.station_id) + subject + end + + it "finds an end product establishment" do + expect(EndProductEstablishment).to receive(:find_by) + .with(reference_id: parser.end_product_establishment_reference_id) + subject + end + + it "updates informal conference" do + expect(Events::DecisionReviewUpdated::UpdateInformalConference).to receive(:process!) + .with(event: event, parser: parser) + subject + end + + it "updates claim review" do + expect(Events::DecisionReviewUpdated::UpdateClaimReview).to receive(:process!).with(event: event, parser: parser) + subject + end + + it "updates end product establishment" do + expect(Events::DecisionReviewUpdated::UpdateEndProductEstablishment).to receive(:process!) + .with(event: event, parser: parser) + subject + end + + it "updates request issues" do + expect(RequestIssuesUpdateEvent).to receive(:new) + .with(user: user, review: review, parser: parser, event: event) + .and_return(double("RequestIssuesUpdateEvent", perform!: nil)) + subject + end + + it "updates the event" do + expect(event).to receive(:update!) + subject + end + end + + context "when a StandardError occurs" do + let(:standard_error) { StandardError.new("Lions, tigers, and bears, OH MY!") } + + before do + allow(RequestIssuesUpdateEvent).to receive(:new) + .with(user: user, review: review, parser: parser, event: event).and_raise(standard_error) + allow(Rails.logger).to receive(:error) + end + + it "the error is logged" do + expect(Rails.logger).to receive(:error) do |message| + expect(message).to include(standard_error.message) + end + expect { described_class.update!(params, headers, payload) } + .to raise_error(standard_error) + end + + it "logs the error and updates the event" do + expect(Rails.logger).to receive(:error).with(/#{standard_error}/) + + expect { described_class.update!(params, headers, payload) } + .to raise_error(standard_error) + end + + it "records an error at the event level" do + expect { described_class.update!(params, headers, payload) } + .to raise_error(standard_error) + expect(event.error).to eq("#{standard_error.class} : #{standard_error.message}") + expect(event.info["failed_claim_id"]).to eq(claim_id) + expect(event.info["error"]).to eq(standard_error.message) + expect(event.info["error_class"]).to eq("StandardError") + expect(event.info["error_backtrace"]).to be_present + end + end +end