From 5033a6472cef5100fd8d1b5a226ab649c68184fb Mon Sep 17 00:00:00 2001 From: Raymond Hughes <131811099+raymond-hughes@users.noreply.github.com> Date: Mon, 16 Sep 2024 09:22:01 -0400 Subject: [PATCH 1/9] feature/APPEALS-50882 - Open Telemetry configuration and implementation (#22833) * feature/appeals-45972 (#21950) * Remove UUID from attrs sent_to metrics. (#21630) * Remove UUID from attrs sent_to metrics. * pushing with some linting issues for visibility. * Change config implementation. * Remove UUID from spec * remove uuid from update_appellant job * diable rubocop for open_telemetry init * OTEL fixes * rescue, errors --------- Co-authored-by: mikefinneran <110622959+mikefinneran@users.noreply.github.com> * add otel reqs * Remove throw * comment out otel file loop * try vendor suggested config * update otel config to match vendor * update pg gem * otel include dt_host file * otel require bundler and rubygems * otel subset of instrumentation * Revert "update pg gem" This reverts commit fc1a45dfeedd3c2b9e299088cf10aea1394bf199. * update redis and minimize otel instrumentation to just rails, rack, and activerecord * otel add use_all except pg and redis * otel require instruments * otel fix typo * otel fix typo * otel comment out net_http * otel silence aws sdk internals * otel silence aws sdk internals * otel add net http * Individually use OTEL instruments (#22082) * Individually use OTEL instruments with options. * disable AwsSdk only. Checking Rack options. * re-require awssdk even while disabled. * disable awssdk * disable datadog for testing * change sequence factory to properly seed * updated opentelemetry and datadog configs * rack context getter initalizer * use one at a time * add curly braces * Revert change * revert change * Revert change * Revert change * Remove gemfile grouping * Remove datadog. * ActionPack and Actionview changes * APPEALS-44287: Excluding disposition held and select that appeal for distribution (#22277) * first run at an SQL query removing duplicate appeals from distribution * code refactor and excluding disposition held and select that appeal for distribution * automated test for the duplicate hearing bug * fix rubocop offense SpaceInsideBlockBraces --------- Co-authored-by: Sean Parker Co-authored-by: samasudhirreddy --------- Co-authored-by: Noelle Adkin <98478937+NoelleAd@users.noreply.github.com> Co-authored-by: Raymond Hughes <131811099+raymond-hughes@users.noreply.github.com> Co-authored-by: Dani Co-authored-by: raymond-hughes Co-authored-by: Craig Reese <109101548+craigrva@users.noreply.github.com> Co-authored-by: Sean Parker Co-authored-by: samasudhirreddy * Update bundler to 2.4.22 * Updating OTEL to use all and remove new relic gem * Remove calls to new relic * removed id attrs in metrics service calls that were causing dimension errors * Update config for otel * Reorder gems to fix linting issues * Updating config and refactoring verbose gems to add all * Reinstall gems * Adding all instruments * Update config to only include basic instruments * Update Gemfile.lock * Adding rake to gemfile * Adding instruments * Comment out PG and ActiveSupport * Suppress AWS logs * Remove redis and turn on actionview * Turn actionview off * Add Redis instrumentation back * Turn ActionPack back on * Disable Redis * Remove mentions of Datadog * Removed extra mentions of Datadog * removed newrelic references and yml file * Test updating workflow * Revert workflow change * Adding simplecov back * Fixing linting error * Removing by: attribute after removing keyword in metric service * Adding simplecov lcov gem and updating workflow * Update workflow * Updating simplecov * Revert simple cov * Adding featureenvy skip for reek * Update service name. (#22762) --------- Co-authored-by: mikefinneran <110622959+mikefinneran@users.noreply.github.com> Co-authored-by: Noelle Adkin <98478937+NoelleAd@users.noreply.github.com> Co-authored-by: Dani Co-authored-by: Craig Reese <109101548+craigrva@users.noreply.github.com> Co-authored-by: Sean Parker Co-authored-by: samasudhirreddy Co-authored-by: Andrew Hadley Co-authored-by: alex-guanipatin Co-authored-by: Drew Hadley <50673809+VandelayUtd@users.noreply.github.com> Co-authored-by: Ron Wabukenda <130374706+ronwabVa@users.noreply.github.com> --- Dockerfile | 4 - Gemfile | 29 +++- Gemfile.lock | 159 ++++++++++++++---- README.md | 7 - app/controllers/api/application_controller.rb | 2 - .../application_base_controller.rb | 2 - app/controllers/concerns/track_request_id.rb | 13 -- .../dependencies_checks_controller.rb | 2 - app/controllers/health_checks_controller.rb | 2 - app/jobs/caseflow_job.rb | 1 - .../update_cached_appeals_attributes_job.rb | 3 - .../virtual_hearings/create_conference_job.rb | 6 +- .../delete_conferences_job.rb | 4 +- app/models/metric.rb | 1 - app/services/collectors/stats_collector.rb | 6 +- app/services/geomatch_service.rb | 1 - app/services/hearings/reminder_service.rb | 1 - app/services/metrics_service.rb | 9 +- app/views/layouts/_head.html.erb | 1 - app/views/layouts/_head_new_relic.html.erb | 13 -- config/initializers/datadog.rb | 14 -- config/initializers/open_telemetry.rb | 73 ++++++++ config/initializers/rack_context.rb | 24 +++ config/initializers/vacols_request_spy.rb | 2 - docker-bin/build.sh | 3 - docker-bin/startup.sh | 6 - newrelic.yml | 49 ------ .../delete_conferences_job_spec.rb | 18 +- spec/services/metrics_service_spec.rb | 2 +- 29 files changed, 265 insertions(+), 192 deletions(-) delete mode 100644 app/controllers/concerns/track_request_id.rb delete mode 100644 app/views/layouts/_head_new_relic.html.erb delete mode 100644 config/initializers/datadog.rb create mode 100644 config/initializers/open_telemetry.rb create mode 100644 config/initializers/rack_context.rb delete mode 100644 newrelic.yml diff --git a/Dockerfile b/Dockerfile index 16d641ac7ce..a774e7e8943 100644 --- a/Dockerfile +++ b/Dockerfile @@ -66,10 +66,6 @@ RUN apt install -y ${CASEFLOW} && \ # install jemalloc RUN apt install -y --no-install-recommends libjemalloc-dev - -# install datadog agent -RUN DD_INSTALL_ONLY=true DD_AGENT_MAJOR_VERSION=7 DD_API_KEY=$(cat config/datadog.key) bash -c "$(curl -L https://raw.githubusercontent.com/DataDog/datadog-agent/master/cmd/agent/install_script.sh)" - RUN rm -rf /var/lib/apt/lists/* # Installing the version of bundler that corresponds to the Gemfile.lock diff --git a/Gemfile b/Gemfile index 92e60188bc8..a6e0728b60e 100644 --- a/Gemfile +++ b/Gemfile @@ -22,8 +22,6 @@ gem "connect_mpi", git: "https://github.com/department-of-veterans-affairs/conne gem "connect_vbms", git: "https://github.com/department-of-veterans-affairs/connect_vbms.git", ref: "9807d9c9f0f3e3494a60b6693dc4f455c1e3e922" gem "console_tree_renderer", git: "https://github.com/department-of-veterans-affairs/console-tree-renderer.git", tag: "v0.1.1" gem "countries" -gem "ddtrace" -gem "dogstatsd-ruby" gem "dry-schema", "~> 1.4" gem "fast_jsonapi" gem "fuzzy_match" @@ -33,8 +31,28 @@ gem "icalendar" gem "kaminari" gem "logstasher" gem "moment_timezone-rails" -gem "newrelic_rpm" gem "nokogiri", ">= 1.11.0.rc4" + +gem "opentelemetry-exporter-otlp", require: false +gem "opentelemetry-sdk", require: false + +gem "opentelemetry-instrumentation-action_pack", require: false +gem "opentelemetry-instrumentation-action_view", require: false +gem "opentelemetry-instrumentation-active_job", require: false +gem "opentelemetry-instrumentation-active_model_serializers", require: false +gem "opentelemetry-instrumentation-active_record", require: false +gem "opentelemetry-instrumentation-aws_sdk", require: false +gem "opentelemetry-instrumentation-concurrent_ruby", require: false +gem "opentelemetry-instrumentation-faraday", require: false +gem "opentelemetry-instrumentation-http", require: false +gem "opentelemetry-instrumentation-http_client", require: false +gem "opentelemetry-instrumentation-net_http", require: false +gem "opentelemetry-instrumentation-pg", require: false +gem "opentelemetry-instrumentation-rack", require: false +gem "opentelemetry-instrumentation-rails", require: false +gem "opentelemetry-instrumentation-rake", require: false +gem "opentelemetry-instrumentation-redis", require: false + gem "paper_trail", "~> 12.0" # Used to speed up reporting gem "parallel" @@ -59,7 +77,7 @@ gem "rainbow" # React gem "react_on_rails", "11.3.0" gem "redis-mutex" -gem "redis-namespace" +gem "redis-namespace", "~> 1.11.0" gem "redis-rails", "~> 5.0.2" gem "request_store" gem "roo", "~> 2.7" @@ -119,7 +137,8 @@ group :test, :development, :demo, :make_docs do gem "rubocop-performance" gem "rubocop-rails" gem "scss_lint", require: false - gem "simplecov", git: "https://github.com/colszowka/simplecov.git", require: false + gem "simplecov", require: false + gem "simplecov-lcov", require: false gem "single_cov" gem "sniffybara", git: "https://github.com/department-of-veterans-affairs/sniffybara.git" gem "sql_tracker" diff --git a/Gemfile.lock b/Gemfile.lock index f2db3b52b7f..3377b991095 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,12 +1,3 @@ -GIT - remote: https://github.com/colszowka/simplecov.git - revision: 783c9d7e9995f3ea9baf9fbb517c1d0ceb12acdb - specs: - simplecov (0.15.1) - docile (~> 1.1.0) - json (>= 1.8, < 3) - simplecov-html (~> 0.10.0) - GIT remote: https://github.com/department-of-veterans-affairs/caseflow-commons revision: 9bd3635fbd8094d25160669f38d8699e2f1d7a98 @@ -1458,7 +1449,7 @@ GEM backport (1.2.0) base64 (0.2.0) benchmark (0.3.0) - benchmark-ips (2.7.2) + benchmark-ips (2.14.0) bootsnap (1.7.5) msgpack (~> 1.0) brakeman (4.7.1) @@ -1529,8 +1520,6 @@ GEM database_cleaner-core (~> 2.0.0) database_cleaner-core (2.0.1) date (3.3.3) - ddtrace (0.34.1) - msgpack dead_end (4.0.0) debase (0.2.4.1) debase-ruby_core_source (>= 0.10.2) @@ -1548,8 +1537,7 @@ GEM ruby-statistics (>= 2.1) thor (>= 0.19, < 2) diff-lcs (1.3) - docile (1.1.5) - dogstatsd-ruby (4.4.0) + docile (1.4.1) dotenv (2.8.1) dotenv-rails (2.8.1) dotenv (= 2.8.1) @@ -1614,13 +1602,16 @@ GEM foreman (0.87.2) formatador (0.2.5) fuzzy_match (2.1.0) - get_process_mem (0.2.4) + get_process_mem (0.2.7) ffi (~> 1.0) git (1.13.2) addressable (~> 2.8) rchardet (~> 1.8) globalid (1.2.1) activesupport (>= 6.1) + google-protobuf (3.25.4) + googleapis-common-protos-types (1.15.0) + google-protobuf (>= 3.18, < 5.a) govdelivery-tms (2.8.4) activesupport faraday @@ -1644,7 +1635,8 @@ GEM builder (>= 2.1.2) hana (1.3.6) hashdiff (1.0.0) - heapy (0.1.4) + heapy (0.2.0) + thor holidays (6.6.1) httpclient (2.8.3) httpi (2.4.4) @@ -1664,7 +1656,6 @@ GEM rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) - json (2.3.0) json-schema (4.3.0) addressable (>= 2.8) json_schemer (0.2.16) @@ -1709,7 +1700,7 @@ GEM net-pop net-smtp marcel (1.0.4) - memory_profiler (0.9.14) + memory_profiler (1.0.2) meta_request (0.7.2) rack-contrib (>= 1.1, < 3) railties (>= 3.0.0, < 7) @@ -1719,7 +1710,6 @@ GEM mime-types-data (3.2019.1009) mini_histogram (0.3.1) mini_mime (1.1.2) - mini_portile2 (2.8.5) minitest (5.19.0) moment_timezone-rails (0.5.0) momentjs-rails (2.29.4.1) @@ -1737,11 +1727,9 @@ GEM timeout net-smtp (0.3.3) net-protocol - newrelic_rpm (6.5.0.357) nio4r (2.5.9) no_proxy_fix (0.1.2) - nokogiri (1.15.5) - mini_portile2 (~> 2.8.2) + nokogiri (1.15.5-x86_64-darwin) racc (~> 1.4) nori (2.6.0) notiffany (0.1.1) @@ -1751,6 +1739,92 @@ GEM faraday (>= 0.9) sawyer (~> 0.8.0, >= 0.5.3) open4 (1.3.4) + opentelemetry-api (1.1.0) + opentelemetry-common (0.19.7) + opentelemetry-api (~> 1.0) + opentelemetry-exporter-otlp (0.24.2) + google-protobuf (~> 3.19) + googleapis-common-protos-types (~> 1.3) + opentelemetry-api (~> 1.1) + opentelemetry-common (~> 0.19.6) + opentelemetry-sdk (~> 1.2) + opentelemetry-semantic_conventions + opentelemetry-instrumentation-action_pack (0.5.0) + opentelemetry-api (~> 1.0) + opentelemetry-instrumentation-base (~> 0.21.0) + opentelemetry-instrumentation-rack (~> 0.21) + opentelemetry-instrumentation-action_view (0.4.0) + opentelemetry-api (~> 1.0) + opentelemetry-instrumentation-active_support (~> 0.1) + opentelemetry-instrumentation-base (~> 0.20) + opentelemetry-instrumentation-active_job (0.4.0) + opentelemetry-api (~> 1.0) + opentelemetry-instrumentation-base (~> 0.21.0) + opentelemetry-instrumentation-active_model_serializers (0.19.1) + opentelemetry-api (~> 1.0) + opentelemetry-instrumentation-base (~> 0.21.0) + opentelemetry-instrumentation-active_record (0.5.0) + opentelemetry-api (~> 1.0) + opentelemetry-instrumentation-base (~> 0.21.0) + ruby2_keywords + opentelemetry-instrumentation-active_support (0.3.0) + opentelemetry-api (~> 1.0) + opentelemetry-instrumentation-base (~> 0.21.0) + opentelemetry-instrumentation-aws_sdk (0.3.2) + opentelemetry-api (~> 1.0) + opentelemetry-instrumentation-base (~> 0.21.0) + opentelemetry-instrumentation-base (0.21.1) + opentelemetry-api (~> 1.0) + opentelemetry-registry (~> 0.1) + opentelemetry-instrumentation-concurrent_ruby (0.20.1) + opentelemetry-api (~> 1.0) + opentelemetry-instrumentation-base (~> 0.21.0) + opentelemetry-instrumentation-faraday (0.22.0) + opentelemetry-api (~> 1.0) + opentelemetry-common (~> 0.19.3) + opentelemetry-instrumentation-base (~> 0.21.0) + opentelemetry-instrumentation-http (0.21.0) + opentelemetry-api (~> 1.0) + opentelemetry-instrumentation-base (~> 0.21.0) + opentelemetry-instrumentation-http_client (0.21.0) + opentelemetry-api (~> 1.0) + opentelemetry-common (~> 0.19.3) + opentelemetry-instrumentation-base (~> 0.21.0) + opentelemetry-instrumentation-net_http (0.21.1) + opentelemetry-api (~> 1.0) + opentelemetry-common (~> 0.19.3) + opentelemetry-instrumentation-base (~> 0.21.0) + opentelemetry-instrumentation-pg (0.23.0) + opentelemetry-api (~> 1.0) + opentelemetry-instrumentation-base (~> 0.21.0) + opentelemetry-instrumentation-rack (0.22.1) + opentelemetry-api (~> 1.0) + opentelemetry-common (~> 0.19.3) + opentelemetry-instrumentation-base (~> 0.21.0) + opentelemetry-instrumentation-rails (0.25.0) + opentelemetry-api (~> 1.0) + opentelemetry-instrumentation-action_pack (~> 0.5.0) + opentelemetry-instrumentation-action_view (~> 0.4.0) + opentelemetry-instrumentation-active_job (~> 0.4.0) + opentelemetry-instrumentation-active_record (~> 0.5.0) + opentelemetry-instrumentation-active_support (~> 0.3.0) + opentelemetry-instrumentation-base (~> 0.21.0) + opentelemetry-instrumentation-rake (0.1.1) + opentelemetry-api (~> 1.0) + opentelemetry-instrumentation-base (~> 0.21.0) + opentelemetry-instrumentation-redis (0.24.1) + opentelemetry-api (~> 1.0) + opentelemetry-common (~> 0.19.3) + opentelemetry-instrumentation-base (~> 0.21.0) + opentelemetry-registry (0.2.0) + opentelemetry-api (~> 1.1) + opentelemetry-sdk (1.2.1) + opentelemetry-api (~> 1.1) + opentelemetry-common (~> 0.19.3) + opentelemetry-registry (~> 0.2) + opentelemetry-semantic_conventions + opentelemetry-semantic_conventions (1.10.0) + opentelemetry-api (~> 1.0) paper_trail (12.3.0) activerecord (>= 5.2) request_store (~> 1.1) @@ -1840,8 +1914,8 @@ GEM redis-namespace (~> 1.0) redis-mutex (4.0.2) redis-classy (~> 2.0) - redis-namespace (1.6.0) - redis (>= 3.0.4) + redis-namespace (1.11.0) + redis (>= 4) redis-rack (2.0.4) rack (>= 1.5, < 3) redis-store (>= 1.2, < 2) @@ -1919,6 +1993,7 @@ GEM ruby-prof (1.4.1) ruby-progressbar (1.13.0) ruby-statistics (3.0.2) + ruby2_keywords (0.0.5) ruby_dep (1.5.0) ruby_parser (3.20.3) sexp_processor (~> 4.16) @@ -1963,7 +2038,13 @@ GEM thor shoulda-matchers (5.3.0) activesupport (>= 5.2.0) - simplecov-html (0.10.2) + simplecov (0.22.0) + docile (~> 1.1) + simplecov-html (~> 0.11) + simplecov_json_formatter (~> 0.1) + simplecov-html (0.13.1) + simplecov-lcov (0.8.0) + simplecov_json_formatter (0.1.4) single_cov (1.3.2) sixarm_ruby_unaccent (1.2.0) socksify (1.7.1) @@ -2045,7 +2126,7 @@ GEM ziptz (2.1.6) PLATFORMS - ruby + x86_64-darwin-22 DEPENDENCIES aasm (= 4.11.0) @@ -2072,10 +2153,8 @@ DEPENDENCIES countries danger (~> 6.2.2) database_cleaner-active_record (= 2.0.0) - ddtrace debase derailed_benchmarks - dogstatsd-ruby dotenv-rails dry-schema (~> 1.4) factory_bot_rails (~> 5.2) @@ -2095,8 +2174,25 @@ DEPENDENCIES logstasher meta_request moment_timezone-rails - newrelic_rpm nokogiri (>= 1.11.0.rc4) + opentelemetry-exporter-otlp + opentelemetry-instrumentation-action_pack + opentelemetry-instrumentation-action_view + opentelemetry-instrumentation-active_job + opentelemetry-instrumentation-active_model_serializers + opentelemetry-instrumentation-active_record + opentelemetry-instrumentation-aws_sdk + opentelemetry-instrumentation-concurrent_ruby + opentelemetry-instrumentation-faraday + opentelemetry-instrumentation-http + opentelemetry-instrumentation-http_client + opentelemetry-instrumentation-net_http + opentelemetry-instrumentation-pg + opentelemetry-instrumentation-rack + opentelemetry-instrumentation-rails + opentelemetry-instrumentation-rake + opentelemetry-instrumentation-redis + opentelemetry-sdk paper_trail (~> 12.0) parallel paranoia (~> 2.2) @@ -2115,7 +2211,7 @@ DEPENDENCIES rb-readline react_on_rails (= 11.3.0) redis-mutex - redis-namespace + redis-namespace (~> 1.11.0) redis-rails (~> 5.0.2) request_store roo (~> 2.7) @@ -2139,7 +2235,8 @@ DEPENDENCIES sentry-raven shoryuken (= 3.1.11) shoulda-matchers - simplecov! + simplecov + simplecov-lcov single_cov sniffybara! solargraph diff --git a/README.md b/README.md index a9f82e9f92d..281703aa1e2 100644 --- a/README.md +++ b/README.md @@ -137,13 +137,6 @@ See debugging steps as well as more information about FACOLS in our [wiki](https Review the [FACOLS documentation](docs/FACOLS.md) for details. ## Monitoring ####################################################### -We use NewRelic to monitor the app. By default, it's disabled locally. To enable it, do: - -``` -NEW_RELIC_LICENSE_KEY='' NEW_RELIC_AGENT_ENABLED=true bundle exec foreman start -``` - -You may wish to do this if you are debugging our NewRelic integration, for instance. --- diff --git a/app/controllers/api/application_controller.rb b/app/controllers/api/application_controller.rb index adb315f0d93..33b11c6b809 100644 --- a/app/controllers/api/application_controller.rb +++ b/app/controllers/api/application_controller.rb @@ -3,8 +3,6 @@ class Api::ApplicationController < ActionController::Base protect_from_forgery with: :null_session - include TrackRequestId - before_action :strict_transport_security before_action :setup_fakes, diff --git a/app/controllers/application_base_controller.rb b/app/controllers/application_base_controller.rb index f9b1304a804..7062f96811d 100644 --- a/app/controllers/application_base_controller.rb +++ b/app/controllers/application_base_controller.rb @@ -6,8 +6,6 @@ class ApplicationBaseController < ActionController::Base # For APIs, you may want to use :null_session instead. protect_from_forgery with: :exception - include TrackRequestId - before_action :check_out_of_service before_action :strict_transport_security diff --git a/app/controllers/concerns/track_request_id.rb b/app/controllers/concerns/track_request_id.rb deleted file mode 100644 index 188256c88f0..00000000000 --- a/app/controllers/concerns/track_request_id.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -module TrackRequestId - extend ActiveSupport::Concern - - included do - before_action :track_request_id - end - - def track_request_id - ::NewRelic::Agent.add_custom_attributes(request_id: request.uuid) - end -end diff --git a/app/controllers/dependencies_checks_controller.rb b/app/controllers/dependencies_checks_controller.rb index ee42e93b5c8..a39f57a4be0 100644 --- a/app/controllers/dependencies_checks_controller.rb +++ b/app/controllers/dependencies_checks_controller.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true class DependenciesChecksController < ApplicationBaseController - newrelic_ignore_apdex - skip_before_action :check_out_of_service def show diff --git a/app/controllers/health_checks_controller.rb b/app/controllers/health_checks_controller.rb index 4ee3acc4440..4451296b081 100644 --- a/app/controllers/health_checks_controller.rb +++ b/app/controllers/health_checks_controller.rb @@ -2,11 +2,9 @@ # rubocop:disable Rails/ApplicationController class HealthChecksController < ActionController::Base - include TrackRequestId include CollectCustomMetrics protect_from_forgery with: :exception - newrelic_ignore_apdex def show body = { diff --git a/app/jobs/caseflow_job.rb b/app/jobs/caseflow_job.rb index b9717f403b0..c03e375a05c 100644 --- a/app/jobs/caseflow_job.rb +++ b/app/jobs/caseflow_job.rb @@ -7,7 +7,6 @@ class CaseflowJob < ApplicationJob job.start_time = Time.zone.now end - # Automatically report runtime to DataDog if job does not explicitly report to DataDog. # Note: This block is not called if an error occurs when `perform` is executed -- # see https://stackoverflow.com/questions/50263787/does-active-job-call-after-perform-when-perform-raises-an-error after_perform do |job| diff --git a/app/jobs/update_cached_appeals_attributes_job.rb b/app/jobs/update_cached_appeals_attributes_job.rb index 3bf31ad2eff..ad78ee9e564 100644 --- a/app/jobs/update_cached_appeals_attributes_job.rb +++ b/app/jobs/update_cached_appeals_attributes_job.rb @@ -124,9 +124,6 @@ def log_error(start_time, err) # We do not log every job failure since we expect the job to occasionally fail when we lose # database connections. Since this job runs regularly, we will continue to cache appeals and we # have set up alerts to notify us if we have cached too few appeals over the past day: - # * (Too little Postgres data cached) https://app.datadoghq.com/monitors/41421962 - # * (Too little VACOLS data cached) https://app.datadoghq.com/monitors/41234223 - # * (Job has not succeeded in the past day) https://app.datadoghq.com/monitors/41423568 record_error_in_metrics_service metrics_service_report_runtime(metric_group_name: METRIC_GROUP_NAME) diff --git a/app/jobs/virtual_hearings/create_conference_job.rb b/app/jobs/virtual_hearings/create_conference_job.rb index e88973d77c0..0c390164315 100644 --- a/app/jobs/virtual_hearings/create_conference_job.rb +++ b/app/jobs/virtual_hearings/create_conference_job.rb @@ -123,7 +123,7 @@ def log_virtual_hearing_state(virtual_hearing) Rails.logger.info("Establishment Updated At: (#{virtual_hearing.establishment.updated_at})") end - def create_conference_datadog_tags + def create_conference_tags custom_metric_info.merge(attrs: { hearing_id: virtual_hearing.hearing_id }) end @@ -149,12 +149,12 @@ def create_conference virtual_hearing.establishment.update_error!(error_display) - MetricsService.increment_counter(metric_name: "created_conference.failed", **create_conference_datadog_tags) + MetricsService.increment_counter(metric_name: "created_conference.failed", **create_conference_tags) fail pexip_response.error end - MetricsService.increment_counter(metric_name: "created_conference.successful", **create_conference_datadog_tags) + MetricsService.increment_counter(metric_name: "created_conference.successful", **create_conference_tags) virtual_hearing.update(conference_id: pexip_response.data[:conference_id]) end diff --git a/app/jobs/virtual_hearings/delete_conferences_job.rb b/app/jobs/virtual_hearings/delete_conferences_job.rb index fcc77df526f..9f713c64cf5 100644 --- a/app/jobs/virtual_hearings/delete_conferences_job.rb +++ b/app/jobs/virtual_hearings/delete_conferences_job.rb @@ -117,13 +117,13 @@ def count_deleted_and_log(enumerable) if removed > 0 MetricsService.increment_counter( - metric_name: "deleted_conferences.successful", by: removed, ** custom_metric_info + metric_name: "deleted_conferences.successful", ** custom_metric_info ) end if failed > 0 MetricsService.increment_counter( - metric_name: "deleted_conferences.failed", by: failed, ** custom_metric_info + metric_name: "deleted_conferences.failed", ** custom_metric_info ) end end diff --git a/app/models/metric.rb b/app/models/metric.rb index 3e3f515bfa2..ecdf62164b4 100644 --- a/app/models/metric.rb +++ b/app/models/metric.rb @@ -7,7 +7,6 @@ class Metric < CaseflowRecord METRIC_TYPES = { error: "error", log: "log", performance: "performance", info: "info" }.freeze LOG_SYSTEMS = { dynatrace: "dynatrace", - datadog: "datadog", rails_console: "rails_console", javascript_console: "javascript_console" }.freeze diff --git a/app/services/collectors/stats_collector.rb b/app/services/collectors/stats_collector.rb index 5eebb3a2ad5..f3148196c5c 100644 --- a/app/services/collectors/stats_collector.rb +++ b/app/services/collectors/stats_collector.rb @@ -11,7 +11,7 @@ def flatten_stats(metric_name_prefix, stats_hash) stats_hash.each do |metric_name, counts_hash| unless valid_metric_name?(metric_name) fail "Invalid metric name #{metric_name}; "\ - "see https://docs.datadoghq.com/developers/metrics/#naming-custom-metrics" + "see https://docs.dynatrace.com/docs/extend-dynatrace/extend-metrics/reference/custom-metric-metadata" end stats.concat add_tags_to_group_counts(metric_name_prefix, metric_name, counts_hash) @@ -29,7 +29,7 @@ def add_tags_to_group_counts(prefix, metric_name, group_counts) end end - # See valid tag name rules at https://docs.datadoghq.com/tagging/#defining-tags + # See valid tag name rules at https://docs.dynatrace.com/docs/manage/tags-and-metadata/setup/how-to-define-tags def to_valid_tag(name) name.gsub(/[^a-zA-Z_\-\:\.\d\/]/, "__") end @@ -41,7 +41,7 @@ def to_valid_tag_key(name) end def valid_metric_name?(metric_name) - # Actual limit is 200 but since the actual metric name in DataDog has + # Actual limit is 200 but since the actual metric name has # "dsva_appeals.stats_collector_job." prepended, let's just stick with a 150 character limit. return false if metric_name.length > 150 diff --git a/app/services/geomatch_service.rb b/app/services/geomatch_service.rb index 0d3e53a13e4..6d8645a5e1a 100644 --- a/app/services/geomatch_service.rb +++ b/app/services/geomatch_service.rb @@ -73,7 +73,6 @@ def record_geomatched_appeal(status) metric_name: "geomatched_appeals", attrs: { status: status, - appeal_external_id: appeal.external_id, hearing_request_type: appeal.current_hearing_request_type } ) diff --git a/app/services/hearings/reminder_service.rb b/app/services/hearings/reminder_service.rb index 936f60aff4c..4076b053f65 100644 --- a/app/services/hearings/reminder_service.rb +++ b/app/services/hearings/reminder_service.rb @@ -51,7 +51,6 @@ def send_to_metrics_service(type) metric_name: "emails.would_be_sent", attrs: { reminder_type: type, - hearing_id: hearing.id, request_type: hearing.hearing_request_type, hearing_type: hearing.class.name } diff --git a/app/services/metrics_service.rb b/app/services/metrics_service.rb index 08c2dbb68c4..82fff42f6d9 100644 --- a/app/services/metrics_service.rb +++ b/app/services/metrics_service.rb @@ -1,18 +1,14 @@ # frozen_string_literal: true require "benchmark" -require "datadog/statsd" require "statsd-instrument" # see https://dropwizard.github.io/metrics/3.1.0/getting-started/ for abstractions on metric types class MetricsService - @statsd = Datadog::Statsd.new - # :reek:LongParameterList - def self.increment_counter(metric_group:, metric_name:, app_name:, attrs: {}, by: 1) + def self.increment_counter(metric_group:, metric_name:, app_name:, attrs: {}) tags = get_tags(app_name, attrs) stat_name = get_stat_name(metric_group, metric_name) - @statsd.increment(stat_name, tags: tags, by: by) # Dynatrace statD implementation StatsD.increment(stat_name, tags: tags) @@ -34,7 +30,6 @@ def self.record_runtime(metric_group:, app_name:, start_time: Time.zone.now) def self.emit_gauge(metric_group:, metric_name:, metric_value:, app_name:, attrs: {}) tags = get_tags(app_name, attrs) stat_name = get_stat_name(metric_group, metric_name) - @statsd.gauge(stat_name, metric_value, tags: tags) # Dynatrace statD implementation StatsD.gauge(stat_name, metric_value, tags: tags) @@ -45,7 +40,6 @@ def self.emit_gauge(metric_group:, metric_name:, metric_value:, app_name:, attrs def self.histogram(metric_group:, metric_name:, metric_value:, app_name:, attrs: {}) tags = get_tags(app_name, attrs) stat_name = get_stat_name(metric_group, metric_name) - @statsd.histogram(stat_name, metric_value, tags: tags) # Dynatrace statD implementation StatsD.histogram(stat_name, metric_value, tags: tags) @@ -98,7 +92,6 @@ def self.record(description, service: nil, name: "unknown", caller: nil) } MetricsService.emit_gauge(sent_to_info) - sent_to << Metric::LOG_SYSTEMS[:datadog] sent_to << Metric::LOG_SYSTEMS[:dynatrace] end diff --git a/app/views/layouts/_head.html.erb b/app/views/layouts/_head.html.erb index d502c1fb968..279790378b6 100644 --- a/app/views/layouts/_head.html.erb +++ b/app/views/layouts/_head.html.erb @@ -12,7 +12,6 @@ <%= render "layouts/head_sentry" %> diff --git a/app/views/layouts/_head_new_relic.html.erb b/app/views/layouts/_head_new_relic.html.erb deleted file mode 100644 index 067774b1547..00000000000 --- a/app/views/layouts/_head_new_relic.html.erb +++ /dev/null @@ -1,13 +0,0 @@ - - // To avoid sending PII to New Relic, we will disable its error logging functionality. - // In anecdotal testing, the error logging functionality didn't even seem to work - // particularly well. - // - // We wrap this in a conditional because newrelic will not be defined when browser - // monitoring is not enabled. This will occur in local development when the - // NEW_RELIC_AGENT_ENABLED env var is not set, for instance. - if (window.newrelic) { - window.newrelic.setErrorHandler(function() { - return true; - }); - } diff --git a/config/initializers/datadog.rb b/config/initializers/datadog.rb deleted file mode 100644 index 033ae4f776f..00000000000 --- a/config/initializers/datadog.rb +++ /dev/null @@ -1,14 +0,0 @@ -unless Rails.env.test? - Datadog.configure do |c| - options = { analytics_enabled: true } - - c.analytics_enabled = true - c.use :rails, options - c.use :active_record, options - c.use :rack, options - c.use :redis, options - c.use :shoryuken, options - - c.env = ENV['DEPLOY_ENV'] - end -end diff --git a/config/initializers/open_telemetry.rb b/config/initializers/open_telemetry.rb new file mode 100644 index 00000000000..1eaf079c9e9 --- /dev/null +++ b/config/initializers/open_telemetry.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true +require 'rubygems' +require 'bundler/setup' + +require 'opentelemetry/sdk' +require 'opentelemetry/exporter/otlp' + +require "opentelemetry-instrumentation-action_pack" +require "opentelemetry-instrumentation-action_view" +require "opentelemetry-instrumentation-active_job" +require "opentelemetry-instrumentation-active_record" +require "opentelemetry-instrumentation-active_support" +require "opentelemetry-instrumentation-aws_sdk" +require "opentelemetry-instrumentation-concurrent_ruby" +require "opentelemetry-instrumentation-faraday" +require "opentelemetry-instrumentation-http_client" +require "opentelemetry-instrumentation-net_http" +require "opentelemetry-instrumentation-pg" +require "opentelemetry-instrumentation-rack" +require "opentelemetry-instrumentation-rails" +require "opentelemetry-instrumentation-rake" +require "opentelemetry-instrumentation-redis" + +# rubocop:disable Layout/LineLength + +DT_API_URL = ENV["DT_API_URL"] +DT_API_TOKEN = ENV["DT_API_TOKEN"] + +Rails.logger.info("DT_API_TOKEN is set to #{DT_API_TOKEN}") + +if !Rails.env.development? && !Rails.env.test? && !Rails.env.demo? + OpenTelemetry::SDK.configure do |c| + c.service_name = 'caseflow' + c.service_version = '1.0.1' + + c.use 'OpenTelemetry::Instrumentation::ActiveRecord' + c.use 'OpenTelemetry::Instrumentation::Rack', { untraced_endpoints: ['/health-check', '/sample', '/logs'] } + c.use 'OpenTelemetry::Instrumentation::Rails' + + # c.use 'OpenTelemetry::Instrumentation::PG' + # c.use 'OpenTelemetry::Instrumentation::ActionView' + # c.use 'OpenTelemetry::Instrumentation::Redis' + + c.use 'OpenTelemetry::Instrumentation::ActionPack' + c.use 'OpenTelemetry::Instrumentation::ActiveSupport' + c.use 'OpenTelemetry::Instrumentation::ActiveJob' + c.use 'OpenTelemetry::Instrumentation::AwsSdk', { suppress_internal_instrumentation: true } + c.use 'OpenTelemetry::Instrumentation::ConcurrentRuby' + c.use 'OpenTelemetry::Instrumentation::Faraday' + c.use 'OpenTelemetry::Instrumentation::HttpClient' + c.use 'OpenTelemetry::Instrumentation::Net::HTTP' + + Rails.logger.info("Loaded instruments") + + %w[dt_metadata_e617c525669e072eebe3d0f08212e8f2.properties /var/lib/dynatrace/enrichment/dt_host_metadata.properties].each { |name| + begin + c.resource = OpenTelemetry::SDK::Resources::Resource.create(Hash[*File.read(name.start_with?("/var") ? name : File.read(name)).split(/[=\n]+/)]) + rescue + end + } + c.add_span_processor( + OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new( + OpenTelemetry::Exporter::OTLP::Exporter.new( + endpoint: DT_API_URL + "/v1/traces", + headers: { + "Authorization": "Api-Token " + DT_API_TOKEN + } + ) + ) + ) + end +end + # rubocop:enable Layout/LineLength diff --git a/config/initializers/rack_context.rb b/config/initializers/rack_context.rb new file mode 100644 index 00000000000..974951fd6ed --- /dev/null +++ b/config/initializers/rack_context.rb @@ -0,0 +1,24 @@ +class RackContextGetter < OpenTelemetry::Context::Propagation::RackEnvGetter + + # :reek:FeatureEnvy + def get(carrier, key) + carrier[to_rack_key(key)] || carrier[key] + end + + protected + + def to_rack_key(key) + ret = +"HTTP_#{key}" + ret.tr!('-', '_') + ret.upcase! + ret + end +end + +RACK_ENV_GETTER = RackContextGetter.new + +OpenTelemetry::Common::Propagation.instance_eval do + def rack_env_getter + RACK_ENV_GETTER + end +end diff --git a/config/initializers/vacols_request_spy.rb b/config/initializers/vacols_request_spy.rb index 939f77dac4b..d79b92df7d6 100644 --- a/config/initializers/vacols_request_spy.rb +++ b/config/initializers/vacols_request_spy.rb @@ -32,8 +32,6 @@ def simulate_vacols_latency # $> REACT_ON_RAILS_ENV=HOT SIMULATE_VACOLS_LATENCY=true bundle exec rails s -p 3000 return unless ENV["SIMULATE_VACOLS_LATENCY"] - # Default determined from metrics sent to Datadog: - # https://app.datadoghq.com/dashboard/54w-efy-r5d/va-systems?fullscreen_widget=399796003 latency = ENV["VACOLS_DELAY_MS"] || 80 sleep(latency / 1000.0) end diff --git a/docker-bin/build.sh b/docker-bin/build.sh index 797ebaff5ab..435d00482d5 100755 --- a/docker-bin/build.sh +++ b/docker-bin/build.sh @@ -41,8 +41,6 @@ fi cd ../../ printf "commit: `git rev-parse HEAD`\ndate: `git log -1 --format=%cd`" > config/build_version.yml -credstash -t appeals-credstash get datadog.api.key > config/datadog.key - cp /etc/ssl/certs/ca-certificates.crt docker-bin/ca-certs/cacert.pem # Build Docker @@ -50,7 +48,6 @@ echo -e "\tCreating Caseflow App Docker Image" docker build -t caseflow . result=$? echo -e "\tCleaning Up..." -rm -rf config/datadog.key rm -rf docker-bin/oracle_libs if [ $result == 0 ]; then echo -e "\tBuilding Caseflow Docker App: Completed" diff --git a/docker-bin/startup.sh b/docker-bin/startup.sh index 5689eecc68d..238677951e6 100644 --- a/docker-bin/startup.sh +++ b/docker-bin/startup.sh @@ -8,12 +8,6 @@ source $THIS_SCRIPT_DIR/env.sh echo "Start DBus" dbus-daemon --system -echo "Start Datadog" -nohup /opt/datadog-agent/bin/agent/agent run -p /opt/datadog-agent/run/agent.pid > dd-agent.out & -nohup /opt/datadog-agent/embedded/bin/trace-agent --config /etc/datadog-agent/datadog.yaml --pid /opt/datadog-agent/run/trace-agent.pid > dd-trace.out & -nohup /opt/datadog-agent/embedded/bin/system-probe --config=/etc/datadog-agent/system-probe.yaml --pid=/opt/datadog-agent/run/system-probe.pid > dd-probe.out & -nohup /opt/datadog-agent/embedded/bin/process-agent --config=/etc/datadog-agent/datadog.yaml --sysprobe-config=/etc/datadog-agent/system-probe.yaml --pid=/opt/datadog-agent/run/process-agent.pid > dd-system-probe.out & - echo "Waiting for dependencies to properly start up - 240 seconds" date sleep 240 diff --git a/newrelic.yml b/newrelic.yml deleted file mode 100644 index c074e6089ed..00000000000 --- a/newrelic.yml +++ /dev/null @@ -1,49 +0,0 @@ -# -# Generated November 03, 2017 -# -# For full documentation of agent configuration options, please refer to -# https://docs.newrelic.com/docs/agents/ruby-agent/installation-configuration/ruby-agent-configuration -common: &default_settings - app_name: Caseflow - - # Logging level for log/newrelic_agent.log - log_level: info - - # Exception messages may have PII, so we won't send them. - # If we are sure that certain exceptions will not have PII, then we can whitelist them in this config file. - strip_exception_messages.enabled: true - - # To avoid sending PII, we explicitly deny all headers and parameters. We then whitelist known safe attributes. - attributes.exclude: [response.headers.*, request.headers.*, request.parameters.*] - attributes.include: [ - response.headers.contentType, - response.headers.contentLength, - request.headers.userAgent, - request.headers.accept, - request.headers.host, - request.headers.contentType, - ] - -# Environment-specific settings are in this section. -# RAILS_ENV or RACK_ENV (as appropriate) is used to determine the environment. -# If your application has other named environments, configure them here. -development: - <<: *default_settings - app_name: Caseflow (Local Dev) - agent_enabled: false - -demo: - <<: *default_settings - app_name: Caseflow (Demo) - -test: - <<: *default_settings - # It doesn't make sense to report to New Relic from automated test runs. - monitor_mode: false - -staging: - <<: *default_settings - app_name: Caseflow (Staging) - -production: - <<: *default_settings diff --git a/spec/jobs/virtual_hearings/delete_conferences_job_spec.rb b/spec/jobs/virtual_hearings/delete_conferences_job_spec.rb index 527fda1e772..55bf82f74b1 100644 --- a/spec/jobs/virtual_hearings/delete_conferences_job_spec.rb +++ b/spec/jobs/virtual_hearings/delete_conferences_job_spec.rb @@ -188,8 +188,7 @@ expect(MetricsService).to receive(:increment_counter).with( hash_including( metric_name: "deleted_conferences.successful", - metric_group: Constants.DATADOG_METRICS.HEARINGS.VIRTUAL_HEARINGS_GROUP_NAME, - by: 2 + metric_group: Constants.DATADOG_METRICS.HEARINGS.VIRTUAL_HEARINGS_GROUP_NAME ) ) subject @@ -213,8 +212,7 @@ expect(MetricsService).to receive(:increment_counter).with( hash_including( metric_name: "deleted_conferences.failed", - metric_group: Constants.DATADOG_METRICS.HEARINGS.VIRTUAL_HEARINGS_GROUP_NAME, - by: 2 + metric_group: Constants.DATADOG_METRICS.HEARINGS.VIRTUAL_HEARINGS_GROUP_NAME ) ) subject @@ -241,8 +239,7 @@ expect(MetricsService).to receive(:increment_counter).with( hash_including( metric_name: "deleted_conferences.successful", - metric_group: Constants.DATADOG_METRICS.HEARINGS.VIRTUAL_HEARINGS_GROUP_NAME, - by: 2 + metric_group: Constants.DATADOG_METRICS.HEARINGS.VIRTUAL_HEARINGS_GROUP_NAME ) ) subject @@ -270,8 +267,7 @@ expect(MetricsService).to receive(:increment_counter).with( hash_including( metric_name: "deleted_conferences.failed", - metric_group: Constants.DATADOG_METRICS.HEARINGS.VIRTUAL_HEARINGS_GROUP_NAME, - by: 2 + metric_group: Constants.DATADOG_METRICS.HEARINGS.VIRTUAL_HEARINGS_GROUP_NAME ) ) subject @@ -295,16 +291,14 @@ expect(MetricsService).to receive(:increment_counter).with( hash_including( metric_name: "deleted_conferences.successful", - metric_group: Constants.DATADOG_METRICS.HEARINGS.VIRTUAL_HEARINGS_GROUP_NAME, - by: 2 + metric_group: Constants.DATADOG_METRICS.HEARINGS.VIRTUAL_HEARINGS_GROUP_NAME ) ) expect(MetricsService).to receive(:increment_counter).with( hash_including( metric_name: "deleted_conferences.failed", - metric_group: Constants.DATADOG_METRICS.HEARINGS.VIRTUAL_HEARINGS_GROUP_NAME, - by: 2 + metric_group: Constants.DATADOG_METRICS.HEARINGS.VIRTUAL_HEARINGS_GROUP_NAME ) ) diff --git a/spec/services/metrics_service_spec.rb b/spec/services/metrics_service_spec.rb index 9b349e84ebe..da2df30db23 100644 --- a/spec/services/metrics_service_spec.rb +++ b/spec/services/metrics_service_spec.rb @@ -63,7 +63,7 @@ service: service, endpoint: name }, - sent_to: [["rails_console"], "datadog", "dynatrace"], + sent_to: [["rails_console"], "dynatrace"], sent_to_info: { metric_group: "service", metric_name: "request_latency", From 7ee78b7ef5906f9527f0ec2a8b66d34e68ab5370 Mon Sep 17 00:00:00 2001 From: Will Medders <93014155+wmedders21@users.noreply.github.com> Date: Mon, 16 Sep 2024 10:34:56 -0500 Subject: [PATCH 2/9] Hotfix/appeals 57839 (#22838) * Add fix for time slot buttons * update jest test to have post meridiem time --------- Co-authored-by: Matthew Thornton <99351305+ThorntonMatthew@users.noreply.github.com> --- client/app/hearings/utils.js | 14 +++++++++----- .../scheduleHearing/TimeSlotButton.test.js | 2 +- .../__snapshots__/TimeSlotButton.test.js.snap | 12 ++++++------ 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/client/app/hearings/utils.js b/client/app/hearings/utils.js index ef69f920b3d..b4a38677e5a 100644 --- a/client/app/hearings/utils.js +++ b/client/app/hearings/utils.js @@ -663,8 +663,12 @@ const calculateAvailableTimeslots = ({ }) => { // Extract the hearing time, add the hearing_day date from beginsAt, set the timezone be the ro timezone const hearingTimes = scheduledHearings.map((hearing) => { - const [hearingHour, hearingMinute] = hearing.hearingTime.split(':'); - const hearingTimeMoment = beginsAt.clone().set({ hour: hearingHour, minute: hearingMinute }); + const hearingClockTime = splitSelectedTime(hearing.hearingTime)[0]; + const parsedClockTime = moment(hearingClockTime, 'h:mm A'); + + const hearingTimeMoment = beginsAt.clone().set({ + hour: parsedClockTime.get('Hour'), minute: parsedClockTime.get('Minute') + }); // Change which zone the time is in but don't convert, "08:15 EDT" -> "08:15 PDT" return hearingTimeMoment.tz(roTimezone, true); @@ -725,11 +729,11 @@ const combineSlotsAndHearings = ({ roTimezone, availableSlots, scheduledHearings key: `${slot?.slotId}-${slot?.time_string}`, full: false, // This is a moment object, always in "America/New_York" - hearingTime: slot.time.format('HH:mm') + hearingTime: slot.time.format('HH:mm A') })); const formattedHearings = scheduledHearings.map((hearing) => { - const time = moment.tz(`${hearing?.hearingTime} ${hearingDayDate}`, 'HH:mm YYYY-MM-DD', roTimezone).clone(). + const time = moment.tz(`${hearing?.hearingTime} ${hearingDayDate}`, 'HH:mm A YYYY-MM-DD', roTimezone).clone(). tz('America/New_York'); return { @@ -740,7 +744,7 @@ const combineSlotsAndHearings = ({ roTimezone, availableSlots, scheduledHearings time, // The hearingTime is in roTimezone, but it looks like "09:30", this takes that "09:30" // in roTimezone, and converts it to Eastern zone because slots are always in eastern. - hearingTime: time.format('HH:mm') + hearingTime: time.format('HH:mm A') }; }); diff --git a/client/test/app/hearings/components/scheduleHearing/TimeSlotButton.test.js b/client/test/app/hearings/components/scheduleHearing/TimeSlotButton.test.js index 6d8022fb9b0..9b7b75902d3 100644 --- a/client/test/app/hearings/components/scheduleHearing/TimeSlotButton.test.js +++ b/client/test/app/hearings/components/scheduleHearing/TimeSlotButton.test.js @@ -4,7 +4,7 @@ import { TimeSlotButton } from 'app/hearings/components/scheduleHearing/TimeSlot import { render } from '@testing-library/react'; import { roTimezones, formatTimeSlotLabel } from 'app/hearings/utils'; -const time = '08:15'; +const time = '15:15 PM'; const hearingDayDate = '2025-01-01'; const issueCount = 2; const poaName = 'Something'; diff --git a/client/test/app/hearings/components/scheduleHearing/__snapshots__/TimeSlotButton.test.js.snap b/client/test/app/hearings/components/scheduleHearing/__snapshots__/TimeSlotButton.test.js.snap index 9c8f1cd3ac4..fcc008289f6 100644 --- a/client/test/app/hearings/components/scheduleHearing/__snapshots__/TimeSlotButton.test.js.snap +++ b/client/test/app/hearings/components/scheduleHearing/__snapshots__/TimeSlotButton.test.js.snap @@ -14,7 +14,7 @@ Object {
- 8:15 AM EST + 3:15 PM EST
@@ -32,7 +32,7 @@ Object {
- 8:15 AM EST + 3:15 PM EST
@@ -108,7 +108,7 @@ Object {
- 8:15 AM EST + 3:15 PM EST
- 8:15 AM EST + 3:15 PM EST
- 8:15 AM EST + 3:15 PM EST
- 8:15 AM EST + 3:15 PM EST
Date: Mon, 16 Sep 2024 11:06:26 -0500 Subject: [PATCH 3/9] DateUtil fix (#22818) * pass in AM/PM value since we are no longer using 24-hour time string * update to be DST aware for the hearing day * add jests tests --------- Co-authored-by: Matthew Thornton <99351305+ThorntonMatthew@users.noreply.github.com> --- .../dailyDocket/DailyDocketPrinted.jsx | 8 +- client/app/util/DateUtil.js | 6 +- .../dailyDocket/DailyDocketPrinted.test.js | 99 +++++++++++++++++++ 3 files changed, 108 insertions(+), 5 deletions(-) diff --git a/client/app/hearings/components/dailyDocket/DailyDocketPrinted.jsx b/client/app/hearings/components/dailyDocket/DailyDocketPrinted.jsx index 1d794ddd4e1..bf0de26b02a 100644 --- a/client/app/hearings/components/dailyDocket/DailyDocketPrinted.jsx +++ b/client/app/hearings/components/dailyDocket/DailyDocketPrinted.jsx @@ -5,7 +5,7 @@ import _ from 'lodash'; import moment from 'moment'; import { getDate, getDisplayTime } from '../../../util/DateUtil'; -import { isPreviouslyScheduledHearing, sortHearings, dispositionLabel } from '../../utils'; +import { isPreviouslyScheduledHearing, sortHearings, dispositionLabel, timeWithTimeZone } from '../../utils'; import { openPrintDialogue } from '../../../util/PrintUtil'; import AOD_CODE_TO_LABEL_MAP from '../../../../constants/AOD_CODE_TO_LABEL_MAP'; import Table from '../../../components/Table'; @@ -27,9 +27,13 @@ export class DailyDocketPrinted extends React.Component { { header: 'Time', valueFunction: (hearing) => { + if (hearing.scheduledInTimezone) { + return timeWithTimeZone(hearing.scheduledFor, hearing.scheduledInTimezone); + } + const localTimezone = hearing.regionalOfficeTimezone || 'America/New_York'; - return getDisplayTime(hearing.scheduledTimeString, localTimezone); + return getDisplayTime(this.props.docket.scheduledFor, hearing.scheduledTimeString, localTimezone); } }, { diff --git a/client/app/util/DateUtil.js b/client/app/util/DateUtil.js index 3cd690bbb34..24a5e7d8851 100644 --- a/client/app/util/DateUtil.js +++ b/client/app/util/DateUtil.js @@ -145,11 +145,11 @@ export const getDate = (date) => { return moment(date).format('YYYY-MM-DD'); }; -export const getDisplayTime = (scheduledTimeString, timezone) => { - const val = scheduledTimeString ? moment(scheduledTimeString, 'HH:mm').format('h:mm a') : ''; +export const getDisplayTime = (dateString, scheduledTimeString, timezone) => { + const val = scheduledTimeString ? moment(scheduledTimeString, 'HH:mm a').format('h:mm A') : ''; if (timezone) { - const tz = moment().tz(timezone). + const tz = moment(dateString).tz(timezone). format('z'); return `${val} ${tz}`; diff --git a/client/test/app/hearings/components/dailyDocket/DailyDocketPrinted.test.js b/client/test/app/hearings/components/dailyDocket/DailyDocketPrinted.test.js index 86eddf54b1e..01d4d85c8c8 100644 --- a/client/test/app/hearings/components/dailyDocket/DailyDocketPrinted.test.js +++ b/client/test/app/hearings/components/dailyDocket/DailyDocketPrinted.test.js @@ -48,4 +48,103 @@ describe('DailyDocketPrinted', () => { renderDailyDocketPrinted(mockProps); expect(await screen.queryByText(/Note:\s*This\s*is\s*a\s*note/)).not.toBeInTheDocument(); }); + + it('displays post meridiem time for DST time with scheduledInTimezone null', () => { + const mockProps = { + user: { userIsNonBoardEmployee: false }, + docket: { scheduledFor: '06-17-2024' }, + hearings: [ + { + scheduledTimeString: '3:30 PM Eastern Time (US & Canada)', + scheduledInTimezone: null, + regionalOfficeTimezone: 'America/New_York' + } + ] + }; + + renderDailyDocketPrinted(mockProps); + expect(screen.getByText('3:30 PM EDT')).toBeInTheDocument(); + }); + + it('displays post meridiem time for DST time with scheduledInTimezone provided', () => { + const mockProps = { + user: { userIsNonBoardEmployee: false }, + docket: { scheduledFor: '06-17-2024' }, + hearings: [ + { + scheduledFor: '2024-06-17T15:30:00.000-04:00', + scheduledInTimezone: 'America/New_York', + } + ] + }; + + renderDailyDocketPrinted(mockProps); + expect(screen.getByText('3:30 PM EDT')).toBeInTheDocument(); + }); + + it('displays post meridiem time in winter with scheduledInTimezone null', () => { + const mockProps = { + user: { userIsNonBoardEmployee: false }, + docket: { scheduledFor: '12-17-2024' }, + hearings: [ + { + scheduledTimeString: '3:30 PM Eastern Time (US & Canada)', + scheduledInTimezone: null, + regionalOfficeTimezone: 'America/New_York' + } + ] + }; + + renderDailyDocketPrinted(mockProps); + expect(screen.getByText('3:30 PM EST')).toBeInTheDocument(); + }); + + it('displays post meridiem time in winter with scheduledInTimezone provided', () => { + const mockProps = { + user: { userIsNonBoardEmployee: false }, + docket: { scheduledFor: '12-17-2024' }, + hearings: [ + { + scheduledFor: '2024-12-17T15:30:00.000-05:00', + scheduledInTimezone: 'America/New_York', + } + ] + }; + + renderDailyDocketPrinted(mockProps); + expect(screen.getByText('3:30 PM EST')).toBeInTheDocument(); + }); + + it('displays post meridiem time in summer with scheduledInTimezone null, ro timezone does not observe DST', () => { + const mockProps = { + user: { userIsNonBoardEmployee: false }, + docket: { scheduledFor: '06-17-2024' }, + hearings: [ + { + scheduledTimeString: '3:30 PM Hawaii', + scheduledInTimezone: null, + regionalOfficeTimezone: 'Pacific/Honolulu' + } + ] + }; + + renderDailyDocketPrinted(mockProps); + expect(screen.getByText('3:30 PM HST')).toBeInTheDocument(); + }); + + it('displays post meridiem time in summer with scheduledInTimezone provided, timezone does not observe DST', () => { + const mockProps = { + user: { userIsNonBoardEmployee: false }, + docket: { scheduledFor: '06-17-2024' }, + hearings: [ + { + scheduledFor: '2024-06-17T15:30:00.000-10:00', + scheduledInTimezone: 'Pacific/Honolulu', + } + ] + }; + + renderDailyDocketPrinted(mockProps); + expect(screen.getByText('3:30 PM HST')).toBeInTheDocument(); + }); }); From 24416bf5d92e3ef5d4abda9f039f93265674c303 Mon Sep 17 00:00:00 2001 From: Amy Detwiler <133032208+amybids@users.noreply.github.com> Date: Mon, 16 Sep 2024 13:22:06 -0400 Subject: [PATCH 4/9] Feature/appeals 44915.release/fy24 q4.5.0 (#22845) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add in appeals_tied_to_non_ssc_avljs methods and SQL * Update case_docket.rb * Update case_docket.rb * APPEALS-51263 added the lever to the seeds file (#22149) * Amybids/appeals 51263 (#22169) * APPEALS-51263 added the lever to the seeds file * APPEALS-51263 test cleanup for case_docket, case_distribution_levers_controller_spec and case_distribution_test_data_spec.rb * APPEALS-51262 (#22176) * creates trait in factory for non_ssc_avlj user * updates user in user factory with new traits in staff factory * adds randomized judge method * replaces user create with a vacols staff create and limits css_id to 12 using slogid, adds judge_role for active smemgrp * updates naming --------- Co-authored-by: Michael Beard * APPEALS-45232. Added table in the DB and added requested columns (#22223) * APPEALS-45232. Added table in the DB and added requested columns * APPEALS-45232. Fix lint issues * APPEALS-45232. Fixed the class name * APPEALS-45232. Updated the column names from feedback --------- Co-authored-by: SHarshain * Sharsha/appeals 45200 (#22233) * APPEALS-45200. create Return Appeals tied to non-SSC AVLJs job * APPEALS-45200. WIP --------- Co-authored-by: SHarshain * Chrisbdetlef/appeals 45208 (#22305) * Framework for seeds * Add comments and fill in further methods * Further shared dev work * Update names * Expanded possible functions * More functions for shared work * Combine everyones work into main branch * Combined work - testing * Combined work - testing complete * rubocop fix --------- Co-authored-by: Christopher Detlef <> * APPEALS-45202.Added button to trigger the job and return the appeals (#22375) Co-authored-by: SHarshain * Updates slack_service.rb to include local/demo console printout message (#22343) * APPEALS-51487 Location 63 Query (#22398) * APPEALS-51487 Location 63 Query * fix naming and change query * Working code * Add 2 day limit to query * Remove commented out code * Fix lint issues * Remove binding.pry --------- Co-authored-by: Christopher Detlef <> * APPEALS-45248 ACD Controls Test Page Alerts for Run Seed Fils and Case Movement Section Buttons (#22386) * APPEALS-45248 Add success banner alerts to Run seed files and case movement section buttons * APPEALS-45248 add fail job button and refactored code * Ricky/APPEALS-45204 (#22373) * Added button, and initial csv generation of appeals tied to non ssc avljs * WIP. APPEALS-45204 tweaks to Push data into CSV * APPEALS-45204. fix the method error * APPEALS-45204. Lint fix * Updated query to fetch hearing and grab non ssc avlj names properly * Fixed outstanding issues and cleaned up seeds * implements new hearing_judge, separates assigned_avlj and signing_avlj * Fixed linting issues and other clean up --------- Co-authored-by: SHarshain <133917878+SHarshain@users.noreply.github.com> Co-authored-by: SHarshain Co-authored-by: Michael Beard * Ricky/APPEALS-45204 (#22452) * Added button, and initial csv generation of appeals tied to non ssc avljs * WIP. APPEALS-45204 tweaks to Push data into CSV * APPEALS-45204. fix the method error * APPEALS-45204. Lint fix * Updated query to fetch hearing and grab non ssc avlj names properly * Fixed outstanding issues and cleaned up seeds * implements new hearing_judge, separates assigned_avlj and signing_avlj * Fixed linting issues and other clean up * Updated name of CSV download button --------- Co-authored-by: SHarshain <133917878+SHarshain@users.noreply.github.com> Co-authored-by: SHarshain Co-authored-by: Michael Beard * Chrisbdetlef/appeals 54138 (#22460) * APPEALS-51487 Location 63 Query * fix naming and change query * Working code * Add 2 day limit to query * Remove commented out code * Fix lint issues * Remove binding.pry * APPEALS-54138 Add button to seed non-SSC AVLJs --------- Co-authored-by: Christopher Detlef <> * Fix for routing table (#22466) Co-authored-by: Christopher Detlef <> * Chrisbdetlef/appeals 54138.1 (#22486) * Fix for routing table * Randomly select docket id number --------- Co-authored-by: Christopher Detlef <> * Updated query to correctly grab seed data (#22489) Co-authored-by: Amy Detwiler <133032208+amybids@users.noreply.github.com> * Ricky/APPEALS-51260 (#22497) * Updated query to correctly grab seed data * Replaced AOD and CAVC fields with Priority, fixed csv file name --------- Co-authored-by: Amy Detwiler <133032208+amybids@users.noreply.github.com> * APPEALS-54884. spec fix push priority appeals to judges job (#22505) Co-authored-by: SHarshain * exchanges spaces for hyphens, updates method to filter by unique veteran file numbers (#22524) * updates seed file names to match AC and adds hyphens instead of spaces (#22530) * Chrisbdetlef/appeals 54152 (#22531) * APPEALS-54152 Move Appeals to loc 63 * Fix minor issues with the query and data displayed in the CSV * APPEALS-54152 Move qualifying appeals to Loc 63 --------- Co-authored-by: Christopher Detlef <> * APPEALS-54152.1 Fix issues with the process of making and moving appeals tied to nonSSC AVLJs (#22541) Co-authored-by: Christopher Detlef <> * Chrisbdetlef/appeals 54152.1 (#22544) * APPEALS-54152.1 Fix issues with the process of making and moving appeals tied to nonSSC AVLJs * TEST ONLY DO NOT MERGE --------- Co-authored-by: Christopher Detlef <> * Chrisbdetlef/appeals 54152.2 (#22554) * APPEALS-54152.2 Change movement to loc 63 to 2 per AVLJ * Fix lint issues and errors * Remove artifact code * Fix errors --------- Co-authored-by: Christopher Detlef <> * Code changes to account for duplicates from base query (#22582) * Code changes to account for duplicates from base query * Add warning to join constant * Fix issue with empty array --------- Co-authored-by: Christopher Detlef <> Co-authored-by: Amy Detwiler <133032208+amybids@users.noreply.github.com> * mbeard/APPEALS-45203 (#22565) * refactored to include new move_qualifying_appeals method * updated filtered methods and eligible appeals, moved appeals variables * moves logic for eligible and moved appeals into new method * cleaned up filter_method, simplified other methods * updates to failing test * ensures appeals isn't nil when job is complete * Fix the last name issue on loc 63 CSV (#22591) Co-authored-by: Christopher Detlef <> * APPEALS-45248 v2 ACD Controls Test Page Alerts for Run Seed Fils and Case Movement Section Buttons (#22449) * APPEALS-45248 Add success banner alerts to Run seed files and case movement section buttons * APPEALS-45248 add fail job button and refactored code * APPEALS-45248 address ac2 of the story and set timer for banner alerts * APPEALS-45248-v2 update button verbiage * APPEALS-54061 added collapsible table to see the 15 latest ReturnLegacyAppealsToBoard log on test page * APPEALS-54061 refactor code to address modified AC in the story * fixed linting issues * some lint fixes --------- Co-authored-by: 631966 Co-authored-by: Amy Detwiler <133032208+amybids@users.noreply.github.com> * APPEALS-APPEALS-55680 added nil checking to the return legacy to appeā€¦ (#22607) * APPEALS-APPEALS-55680 added nil checking to the return legacy to appeals to board job * APPEALS-55680 finish job if no moved appeals * APPEALS-55680 made the no record slack message into an array * APPEALS-55680 fixed rubocop issues * mbeard/APPEALS-55179 (#22615) * adds constants and updates method to use constant * changes lever_group * APPEALS-55179. Fix the query and update the Constant value * updates method and puts constants to lowercase * adds nonsscavlj_number_of_appeals_to_move to failing spec array --------- Co-authored-by: SHarshain * mbeard/APPEALS-45203 (#22626) * removes avljs from the slack message * updates to slack messages * APPEALS-54290 CSV for all tied VLJ cases (#22624) Co-authored-by: Christopher Detlef <> * APPEALS-56033 fixed duplicate, nil, and error handling * APPEALS-56033 fixed duplicate, nil, and error handling (#22630) * APPEALS-56033 fix job running on frontend * APPEALS-56033 fixed linting error * Amybids/appeals 56033 (#22635) * APPEALS-56033 fixed duplicate, nil, and error handling * APPEALS-56033 fix job running on frontend * APPEALS-56033 fixed linting error * Fix off by 1 error for returntoboard job (#22648) * Fix off by 1 error for returntoboard job * removed the extraneous space * refactored the variables * refactored variable names to fix rubocop issues --------- Co-authored-by: Christopher Detlef <> Co-authored-by: 631966 * mbeard/APPEALS-55660 (#22653) * Updates test coverage for full perform method * fixed linting issues * updates to receive and return a number of appeals moved * updates value from 10 to 2 moved appeals * adds trait to factory and lever to push priority spec * APPEALS-56980 Fix for .perform_later functionality in higher environmā€¦ (#22671) * APPEALS-56980 Fix for .perform_later functionality in higher environments * Remove lines of code that aren't needed and add built in checker * Fix 1 of 2 errors * Move local variables out of intialize or callback methods --------- Co-authored-by: Christopher Detlef <> * APPEALS-55158 removed the skipped specs to make the test suite pass (#22651) * Chrisbdetlef/appeals 54290 (#22647) * APPEALS-54290 CSV for all tied VLJ cases * Added support for AMA appeals * Fix linting issues --------- Co-authored-by: Christopher Detlef <> * ricky/APPEALS-55123 (#22617) * Added VLJ and Inactive Non-SSC AVLJ seeds * Updated seeds to include signed and unsigned AMA appeals * Fixed AMA hearing to not error on creation --------- Co-authored-by: Amy Detwiler <133032208+amybids@users.noreply.github.com> * Added docket back because sym doesn't provide the same information * APPEALS-57322 Add logging for Not Ready to Distribute appeals (#22722) * APPEALS-57322 Add logging for Not Ready to Distribute appeals * Remove binding pry --------- Co-authored-by: Christopher Detlef <> * Change the button label (#22723) * Change the button label * Remove binding pry --------- Co-authored-by: Christopher Detlef <> * fix appeals ready to distribute * Change the name of the SSC AVLJ (#22728) * Change the name of the SSC AVLJ * Remove binding pry --------- Co-authored-by: Christopher Detlef <> * APPEALS-57428. Update job log when job completes with no records (#22736) * APPEALS-57428. Update job log when job completes with no records * APPEALS-57428. Job stops if the returned appeals is nil --------- Co-authored-by: SHarshain * reverted work so that the legacy rows show up as NA (#22744) * Sharsha/tes branch master (#22772) * APPEALS-57428.Update job log when job completes with no records * Upmerge master * spec fix * fixed some bug errors * fixed linting issue --------- Co-authored-by: SHarshain Co-authored-by: 631966 * APPEALS-57711. fix the ama_rows logc (#22775) Co-authored-by: SHarshain * APPEALS-57801 Fix merging error (#22781) * APPEALS-57801 Fix merging error * rubocop fix --------- Co-authored-by: Christopher Detlef <> * APPEALS-57058. Automated Tests for the Queries (#22791) * APPEALS-57058. WIP - check loc_63_appeals * APPEALS-57058. more test coverage --------- Co-authored-by: SHarshain * Ricky/APPEALS-57057 (#22801) * Created outline for CSV test for appeals tied to non ssc avljs * Updated rspec for appeals tied to non ssc avlj to have full coverage * Created initial spec file for appeeals tied to avljs and vljs query * Added testing support appeals_tied_to_avljs_and_vljs_query * Updated testing on appeals_tied_to_avlj_and_vljs * Updated framework on location 63 appeals CSV rspec testing * Fixed base test for appeals in location 63 CSV * Updated data structure to properly check test * Updated location 63 CSV to have full coverage * Fixed linting * Chrisbdetlef/appeals 55660 (#22798) * Psuedo code for specs * Framework for tests * Split work * APPEALS-55660. Test expansion (#22714) Co-authored-by: SHarshain * APPEALS-55660 Expanded test coverage for ReturnToBaordJob * Combine work * Fix linting and add some error handling --------- Co-authored-by: Christopher Detlef <> Co-authored-by: SHarshain <133917878+SHarshain@users.noreply.github.com> Co-authored-by: SHarshain * Chrisbdetlef/appeals 58192 (#22832) * APPEALS-58192 Fix location 63 query test * Fix rubocop issues --------- Co-authored-by: Christopher Detlef <> * updated migration for rails 6 --------- Co-authored-by: Matthew Roth Co-authored-by: Blake Manus <33578594+Blake-Manus@users.noreply.github.com> Co-authored-by: Michael Beard Co-authored-by: SHarshain <133917878+SHarshain@users.noreply.github.com> Co-authored-by: SHarshain Co-authored-by: cdetlefva <133903625+cdetlefva@users.noreply.github.com> Co-authored-by: Michael Beard <131783726+mbeardy@users.noreply.github.com> Co-authored-by: kristeja <112115264+kristeja@users.noreply.github.com> --- ...se_distribution_levers_tests_controller.rb | 59 ++ .../push_priority_appeals_to_judges_job.rb | 1 + .../return_legacy_appeals_to_board_job.rb | 252 +++++++ app/models/case_distribution_lever.rb | 1 + app/models/concerns/distribution_scopes.rb | 5 + app/models/docket.rb | 6 + app/models/dockets/legacy_docket.rb | 12 + app/models/returned_appeal_job.rb | 4 + app/models/vacols/case_docket.rb | 114 +++- app/models/vacols/staff.rb | 15 + .../appeals_in_location_63_in_past_2_days.rb | 104 +++ app/queries/appeals_ready_for_distribution.rb | 54 +- .../appeals_tied_to_avljs_and_vljs_query.rb | 165 +++++ .../appeals_tied_to_non_ssc_avlj_query.rb | 113 ++++ app/repositories/appeal_repository.rb | 25 + .../case_distribution_levers/index.html.erb | 3 +- .../case_distribution_levers/test.html.erb | 3 +- .../components/CollapsibleTable.jsx | 68 ++ client/app/caseDistribution/test.jsx | 139 +++- .../styles/caseDistribution/_test_seeds.scss | 9 + client/constants/ACD_LEVERS.json | 3 +- client/constants/DISTRIBUTION.json | 7 +- config/routes.rb | 7 +- ...40717034659_create_returned_appeal_jobs.rb | 13 + db/schema.rb | 10 + db/seeds/case_distribution_levers.rb | 25 +- db/seeds/non_ssc_avlj_legacy_appeals.rb | 472 +++++++++++++ ...ase_distribution_levers_controller_spec.rb | 2 +- spec/factories/case_distribution_lever.rb | 11 + spec/factories/returned_appeal_job.rb | 11 + spec/factories/user.rb | 24 + spec/factories/vacols/case.rb | 108 +++ spec/factories/vacols/staff.rb | 35 + ...ush_priority_appeals_to_judges_job_spec.rb | 100 +-- ...return_legacy_appeals_to_board_job_spec.rb | 620 ++++++++++++++++++ spec/models/case_distribution_lever_spec.rb | 3 +- spec/models/returned_appeal_job_spec.rb | 9 + ...eals_in_location_63_in_past_2_days_spec.rb | 307 +++++++++ ...peals_tied_to_avljs_and_vljs_query_spec.rb | 172 +++++ ...appeals_tied_to_non_ssc_avlj_query_spec.rb | 157 +++++ .../seeds/case_distribution_test_data_spec.rb | 2 +- 41 files changed, 3126 insertions(+), 124 deletions(-) create mode 100644 app/jobs/return_legacy_appeals_to_board_job.rb create mode 100644 app/models/returned_appeal_job.rb create mode 100644 app/queries/appeals_in_location_63_in_past_2_days.rb create mode 100644 app/queries/appeals_tied_to_avljs_and_vljs_query.rb create mode 100644 app/queries/appeals_tied_to_non_ssc_avlj_query.rb create mode 100644 client/app/caseDistribution/components/CollapsibleTable.jsx create mode 100644 db/migrate/20240717034659_create_returned_appeal_jobs.rb create mode 100644 db/seeds/non_ssc_avlj_legacy_appeals.rb create mode 100644 spec/factories/returned_appeal_job.rb create mode 100644 spec/jobs/return_legacy_appeals_to_board_job_spec.rb create mode 100644 spec/models/returned_appeal_job_spec.rb create mode 100644 spec/queries/appeals_in_location_63_in_past_2_days_spec.rb create mode 100644 spec/queries/appeals_tied_to_avljs_and_vljs_query_spec.rb create mode 100644 spec/queries/appeals_tied_to_non_ssc_avlj_query_spec.rb diff --git a/app/controllers/case_distribution_levers_tests_controller.rb b/app/controllers/case_distribution_levers_tests_controller.rb index 11ccd90e0c8..e7eed173af9 100644 --- a/app/controllers/case_distribution_levers_tests_controller.rb +++ b/app/controllers/case_distribution_levers_tests_controller.rb @@ -8,6 +8,7 @@ class CaseDistributionLeversTestsController < ApplicationController def acd_lever_index_test @acd_levers = CaseDistributionLever.all @acd_history = CaseDistributionAuditLeverEntry.lever_history + @returned_appeal_jobs = ReturnedAppealJob.all.order(created_at: :desc).limit(15) render "case_distribution_levers/test" end @@ -40,6 +41,13 @@ def run_demo_docket_priority head :ok end + def run_demo_non_avlj_appeals + Rake::Task["db:seed:non_ssc_avlj_legacy_appeals"].reenable + Rake::Task["db:seed:non_ssc_avlj_legacy_appeals"].invoke + + head :ok + end + def appeals_ready_to_distribute csv_data = AppealsReadyForDistribution.process @@ -66,6 +74,17 @@ def appeals_non_priority_ready_to_distribute send_data csv_data, filename: filename end + def run_return_legacy_appeals_to_board + result = ReturnLegacyAppealsToBoardJob.perform_now + + unless result + render json: { error: "Job failed" }, status: :unprocessable_entity + return + end + + head :ok + end + def appeals_distributed # change this to the correct class csv_data = AppealsDistributed.process @@ -80,6 +99,20 @@ def appeals_distributed send_data csv_data, filename: filename end + def appeals_in_location_63_in_past_2_days + # change this to the correct class + csv_data = AppealsInLocation63InPast2Days.process + + # Get the current date and time for dynamic filename + current_datetime = Time.zone.now.strftime("%Y%m%d-%H%M") + + # Set dynamic filename with current date and time + filename = "appeals_in_location_63_past_2_days_#{current_datetime}.csv" + + # Send CSV as a response with dynamic filename + send_data csv_data, filename: filename + end + def ineligible_judge_list # change this to the correct class csv_data = IneligibleJudgeList.process @@ -94,6 +127,32 @@ def ineligible_judge_list send_data csv_data, filename: filename end + def appeals_tied_to_non_ssc_avlj + csv_data = AppealsTiedToNonSscAvljQuery.process + + # Get the current date and time for dynamic filename + current_datetime = Time.zone.now.strftime("%Y%m%d-%H%M") + + # Set dynamic filename with current date and time + filename = "appeals_tied_to_non_ssc_avljs_#{current_datetime}.csv" + + # Send CSV as a response with dynamic filename + send_data csv_data, filename: filename + end + + def appeals_tied_to_avljs_and_vljs + csv_data = AppealsTiedToAvljsAndVljsQuery.process + + # Get the current date and time for dynamic filename + current_datetime = Time.zone.now.strftime("%Y%m%d-%H%M") + + # Set dynamic filename with current date and time + filename = "appeals_tied_to_avljs_and_vljs#{current_datetime}.csv" + + # Send CSV as a response with dynamic filename + send_data csv_data, filename: filename + end + private def check_environment diff --git a/app/jobs/push_priority_appeals_to_judges_job.rb b/app/jobs/push_priority_appeals_to_judges_job.rb index 7907a9eca08..8a1e8cfff68 100644 --- a/app/jobs/push_priority_appeals_to_judges_job.rb +++ b/app/jobs/push_priority_appeals_to_judges_job.rb @@ -20,6 +20,7 @@ def perform @genpop_distributions = distribute_genpop_priority_appeals perform_later_or_now(UpdateAppealAffinityDatesJob) + perform_later_or_now(ReturnLegacyAppealsToBoardJob) slack_service.send_notification(generate_report.join("\n"), self.class.name) rescue StandardError => error diff --git a/app/jobs/return_legacy_appeals_to_board_job.rb b/app/jobs/return_legacy_appeals_to_board_job.rb new file mode 100644 index 00000000000..a5ee1baba90 --- /dev/null +++ b/app/jobs/return_legacy_appeals_to_board_job.rb @@ -0,0 +1,252 @@ +# frozen_string_literal: true + +class ReturnLegacyAppealsToBoardJob < CaseflowJob + # For time_ago_in_words() + include ActionView::Helpers::DateHelper + + queue_with_priority :low_priority + application_attr :queue + + NO_RECORDS_FOUND_MESSAGE = [Constants.DISTRIBUTION.no_records_moved_message].freeze + + def perform + catch(:abort) do + begin + returned_appeal_job = create_returned_appeal_job + + appeals, moved_appeals = eligible_and_moved_appeals + + check_appeals_available(moved_appeals, returned_appeal_job) + + complete_returned_appeal_job(returned_appeal_job, "Job completed successfully", moved_appeals) + + # The rest of your code continues here + # Filter the appeals and send the filtered report + @filtered_appeals = filter_appeals(appeals, moved_appeals) + send_job_slack_report(slack_report) + rescue StandardError => error + handle_error(error, returned_appeal_job) + ensure + metrics_service_report_runtime(metric_group_name: "return_legacy_appeals_to_board_job") + end + end + end + + def filter_appeals(appeals, moved_appeals) + priority_appeals_moved, non_priority_appeals_moved = separate_by_priority(moved_appeals) + + remaining_priority_appeals, + remaining_non_priority_appeals = calculate_remaining_appeals( + appeals, + priority_appeals_moved, + non_priority_appeals_moved + ) + + { + priority_appeals_count: count_unique_bfkeys(priority_appeals_moved), + non_priority_appeals_count: count_unique_bfkeys(non_priority_appeals_moved), + remaining_priority_appeals_count: count_unique_bfkeys(remaining_priority_appeals), + remaining_non_priority_appeals_count: count_unique_bfkeys(remaining_non_priority_appeals), + grouped_by_avlj: grouped_by_avlj(moved_appeals) + } + end + + def eligible_and_moved_appeals + appeals = LegacyDocket.new.appeals_tied_to_non_ssc_avljs + moved_appeals = move_qualifying_appeals(appeals) + [appeals, moved_appeals] + end + + def grouped_by_avlj(moved_appeals) + return [] if moved_appeals.nil? + + moved_appeals.group_by { |appeal| VACOLS::Staff.find_by(sattyid: appeal["vlj"])&.sattyid }.keys.compact + end + + def count_unique_bfkeys(appeals) + appeals.map { |appeal| appeal["bfkey"] }.uniq.size + end + + private + + def move_qualifying_appeals(appeals) + qualifying_appeals_bfkeys = [] + + non_ssc_avljs.each do |non_ssc_avlj| + tied_appeals = appeals.select { |appeal| appeal["vlj"] == non_ssc_avlj.sattyid } + tied_appeals_bfkeys = get_tied_appeal_bfkeys(tied_appeals) + qualifying_appeals_bfkeys = update_qualifying_appeals_bfkeys(tied_appeals_bfkeys, qualifying_appeals_bfkeys) + end + + unless qualifying_appeals_bfkeys.empty? + qualifying_appeals = appeals + .select { |q_appeal| qualifying_appeals_bfkeys.include? q_appeal["bfkey"] } + .flatten + .sort_by { |appeal| [-appeal["priority"], appeal["bfd19"]] } + VACOLS::Case.batch_update_vacols_location("63", qualifying_appeals.map { |q_appeal| q_appeal["bfkey"] }) + end + + qualifying_appeals || [] + end + + def get_tied_appeal_bfkeys(tied_appeals) + tied_appeals_bfkeys = [] + + unless tied_appeals.empty? + tied_appeals_bfkeys = tied_appeals + .sort_by { |t_appeal| [-t_appeal["priority"], t_appeal["bfd19"]] } + .map { |t_appeal| t_appeal["bfkey"] } + .uniq + .flatten + end + + tied_appeals_bfkeys + end + + def update_qualifying_appeals_bfkeys(tied_appeals_bfkeys, qualifying_appeals_bfkeys) + if nonsscavlj_number_of_appeals_limit < 0 + fail StandardError, "CaseDistributionLever.nonsscavlj_number_of_appeals_to_move set below 0" + elsif nonsscavlj_number_of_appeals_limit == 0 + return qualifying_appeals_bfkeys + end + + if tied_appeals_bfkeys.any? + if tied_appeals_bfkeys.count < nonsscavlj_number_of_appeals_limit + qualifying_appeals_bfkeys.push(tied_appeals_bfkeys) + else + qualifying_appeals_bfkeys.push(tied_appeals_bfkeys[0..nonsscavlj_number_of_appeals_to_move_index]) + end + end + + qualifying_appeals_bfkeys.flatten + end + + def non_ssc_avljs + VACOLS::Staff.where("sactive = 'A' AND svlj = 'A' AND sattyid <> smemgrp") + end + + # Method to separate appeals by priority + def separate_by_priority(appeals) + return [] if appeals.nil? + + priority_appeals = appeals.select { |appeal| appeal["priority"] == 1 } || [] + non_priority_appeals = appeals.select { |appeal| appeal["priority"] == 0 } || [] + + [priority_appeals, non_priority_appeals] + end + + # Method to calculate remaining eligible appeals + def calculate_remaining_appeals(all_appeals, moved_priority_appeals, moved_non_priority_appeals) + return [] if all_appeals.nil? + + remaining_priority_appeals = calculate_remaining_priority_appeals(all_appeals, moved_priority_appeals) + remaining_non_priority_appeals = calculate_remaining_non_priority_appeals(all_appeals, moved_non_priority_appeals) + + [remaining_priority_appeals, remaining_non_priority_appeals] + end + + def calculate_remaining_priority_appeals(all_appeals, moved_priority_appeals) + starting_priority_appeals = all_appeals.select { |appeal| appeal["priority"] == 1 } + + if (moved_priority_appeals - starting_priority_appeals).empty? + remaining_priority_appeals = (starting_priority_appeals - moved_priority_appeals) || [] + else + fail StandardError, "An invalid priority appeal was detected in the list of moved appeals: "\ + "#{moved_priority_appeals - starting_priority_appeals}" + end + + remaining_priority_appeals + end + + def calculate_remaining_non_priority_appeals(all_appeals, moved_non_priority_appeals) + starting_non_priority_appeals = all_appeals.select { |appeal| appeal["priority"] == 0 } + + if (moved_non_priority_appeals - starting_non_priority_appeals).empty? + remaining_non_priority_appeals = (starting_non_priority_appeals - moved_non_priority_appeals) || [] + else + fail StandardError, "An invalid non-priority appeal was detected in the list of moved appeals: "\ + "#{moved_non_priority_appeals - starting_non_priority_appeals}" + end + + remaining_non_priority_appeals + end + + # Method to fetch non-SSC AVLJs SATTYIDS that appeals were moved to location '63' + def fetch_moved_sattyids(moved_appeals) + return [] if moved_appeals.nil? + + moved_appeals.map { |appeal| VACOLS::Staff.find_by(sattyid: appeal["vlj"]) } + .compact + .uniq + .map(&:sattyid) || [] + end + + def nonsscavlj_number_of_appeals_limit + @nonsscavlj_number_of_appeals_limit ||= CaseDistributionLever.nonsscavlj_number_of_appeals_to_move || 0 + end + + def nonsscavlj_number_of_appeals_to_move_index + @nonsscavlj_number_of_appeals_to_move_index ||= nonsscavlj_number_of_appeals_limit - 1 + end + + def create_returned_appeal_job + ReturnedAppealJob.create!( + started_at: Time.zone.now, + stats: { message: "Job started" }.to_json + ) + end + + def check_appeals_available(moved_appeals, returned_appeal_job) + if moved_appeals.nil? + complete_returned_appeal_job(returned_appeal_job, Constants.DISTRIBUTION.no_records_moved_message, []) + send_job_slack_report(NO_RECORDS_FOUND_MESSAGE) + throw(:abort) + end + end + + def handle_error(error, returned_appeal_job) + @start_time ||= Time.zone.now + message = "Job failed with error: #{error.message}" + errored_returned_appeal_job(returned_appeal_job, message) + duration = time_ago_in_words(@start_time) + slack_service.send_notification("\n [ERROR] after running for #{duration}: #{error.message}", + self.class.name) + log_error(error) + message + end + + def complete_returned_appeal_job(returned_appeal_job, message, appeals) + appeals ||= [] + returned_appeal_job.update!( + completed_at: Time.zone.now, + stats: { message: message }.to_json, + returned_appeals: appeals.map { |appeal| appeal["bfkey"] }.uniq + ) + end + + def errored_returned_appeal_job(returned_appeal_job, message) + returned_appeal_job.update!( + errored_at: Time.zone.now, + stats: { message: message }.to_json + ) + end + + def send_job_slack_report(slack_message) + if slack_message.blank? + fail StandardError, "Slack message cannot be empty or nil" + end + + slack_service.send_notification(slack_message.join("\n"), self.class.name) + end + + def slack_report + report = [] + report << "Job performed successfully" + report << "Total Priority Appeals Moved: #{@filtered_appeals[:priority_appeals_count]}" + report << "Total Non-Priority Appeals Moved: #{@filtered_appeals[:non_priority_appeals_count]}" + report << "Total Remaining Priority Appeals: #{@filtered_appeals[:remaining_priority_appeals_count]}" + report << "Total Remaining Non-Priority Appeals: #{@filtered_appeals[:remaining_non_priority_appeals_count]}" + report << "SATTYIDs of Non-SSC AVLJs Moved: #{@filtered_appeals[:grouped_by_avlj].join(', ')}" + report + end +end diff --git a/app/models/case_distribution_lever.rb b/app/models/case_distribution_lever.rb index 54d9c59ae1e..a2ab54e3a9b 100644 --- a/app/models/case_distribution_lever.rb +++ b/app/models/case_distribution_lever.rb @@ -27,6 +27,7 @@ class CaseDistributionLever < ApplicationRecord #{Constants.DISTRIBUTION.ama_hearing_docket_time_goals} #{Constants.DISTRIBUTION.ama_hearing_start_distribution_prior_to_goals} #{Constants.DISTRIBUTION.ama_evidence_submission_start_distribution_prior_to_goals} + #{Constants.DISTRIBUTION.nonsscavlj_number_of_appeals_to_move} ).freeze FLOAT_LEVERS = %W( diff --git a/app/models/concerns/distribution_scopes.rb b/app/models/concerns/distribution_scopes.rb index 08fbfc2472d..81ea761fa56 100644 --- a/app/models/concerns/distribution_scopes.rb +++ b/app/models/concerns/distribution_scopes.rb @@ -299,6 +299,11 @@ def case_affinity_days_lever_value_is_selected?(lever_value) true end + def tied_to_judges(judge_ids) + with_appeal_affinities + .where(hearings: { judge_id: judge_ids }) + end + def exclude_affinity_and_ineligible_judge_ids judge_ids = JudgeTeam.judges_with_exclude_appeals_from_affinity diff --git a/app/models/docket.rb b/app/models/docket.rb index 73dc54358e6..b830e5fcf41 100644 --- a/app/models/docket.rb +++ b/app/models/docket.rb @@ -114,6 +114,12 @@ def ready_priority_appeal_ids appeals(priority: true, ready: true).pluck(:uuid) end + def tied_to_vljs(judge_ids) + docket_appeals.ready_for_distribution + .most_recent_hearings + .tied_to_judges(judge_ids) + end + # rubocop:disable Metrics/MethodLength, Lint/UnusedMethodArgument, Metrics/PerceivedComplexity # :reek:FeatureEnvy def distribute_appeals(distribution, priority: false, genpop: nil, limit: 1, style: "push") diff --git a/app/models/dockets/legacy_docket.rb b/app/models/dockets/legacy_docket.rb index 96c3eb30f87..44d332291f5 100644 --- a/app/models/dockets/legacy_docket.rb +++ b/app/models/dockets/legacy_docket.rb @@ -14,6 +14,18 @@ def ready_to_distribute_appeals LegacyAppeal.repository.ready_to_distribute_appeals end + def appeals_tied_to_non_ssc_avljs + LegacyAppeal.repository.appeals_tied_to_non_ssc_avljs + end + + def appeals_tied_to_avljs_and_vljs + LegacyAppeal.repository.appeals_tied_to_avljs_and_vljs + end + + def loc_63_appeals + LegacyAppeal.repository.loc_63_appeals + end + # rubocop:disable Metrics/CyclomaticComplexity def count(priority: nil, ready: nil) counts_by_priority_and_readiness.inject(0) do |sum, row| diff --git a/app/models/returned_appeal_job.rb b/app/models/returned_appeal_job.rb new file mode 100644 index 00000000000..90b3ce5aa15 --- /dev/null +++ b/app/models/returned_appeal_job.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +class ReturnedAppealJob < ApplicationRecord +end diff --git a/app/models/vacols/case_docket.rb b/app/models/vacols/case_docket.rb index cbaaed7960c..7810dfbe51b 100644 --- a/app/models/vacols/case_docket.rb +++ b/app/models/vacols/case_docket.rb @@ -91,7 +91,8 @@ class DocketNumberCentennialLoop < StandardError; end " # Judges 000, 888, and 999 are not real judges, but rather VACOLS codes. - + # This query will create multiple records/rows for each BRIEFF if the BRIEFF has multiple hearings + # This may need to be accounted for by making sure the resultant set is filtered by BFKEY JOIN_ASSOCIATED_VLJS_BY_HEARINGS = " left join ( select distinct TITRNUM, TINUM, @@ -114,7 +115,6 @@ class DocketNumberCentennialLoop < StandardError; end F.TITRNUM as PREV_TITRNUM from BRIEFF B inner join FOLDER F on F.TICKNUM = B.BFKEY - where B.BFMPRO = 'HIS' and B.BFMEMID not in ('000', '888', '999') and B.BFATTID is not null ) PREV_APPEAL on PREV_APPEAL.PREV_BFKEY != BRIEFF.BFKEY and PREV_APPEAL.PREV_BFCORLID = BRIEFF.BFCORLID @@ -187,8 +187,28 @@ class DocketNumberCentennialLoop < StandardError; end ) " - # this query should not be used during distribution it is only intended for reporting usage + # selects both priority and non-priority appeals that are ready to distribute SELECT_READY_TO_DISTRIBUTE_APPEALS_ORDER_BY_BFD19 = " + select APPEALS.BFKEY, APPEALS.TINUM, APPEALS.BFD19, APPEALS.BFDLOOUT, + case when APPEALS.BFAC = '7' or APPEALS.AOD = 1 then 1 else 0 end PRIORITY, + APPEALS.VLJ, APPEALS.PREV_DECIDING_JUDGE, APPEALS.HEARING_DATE, APPEALS.PREV_BFDDEC + from ( + select BRIEFF.BFKEY, BRIEFF.TINUM, BFD19, BFDLOOUT, BFAC, AOD, + case when BFHINES is null or BFHINES <> 'GP' then VLJ_HEARINGS.VLJ end VLJ + , PREV_APPEAL.PREV_DECIDING_JUDGE PREV_DECIDING_JUDGE + , VLJ_HEARINGS.HEARING_DATE HEARING_DATE + , PREV_APPEAL.PREV_BFDDEC PREV_BFDDEC + from ( + #{SELECT_READY_APPEALS} + ) BRIEFF + #{JOIN_ASSOCIATED_VLJS_BY_HEARINGS} + #{JOIN_PREVIOUS_APPEALS} + order by BFD19 + ) APPEALS + " + + # this query should not be used during distribution it is only intended for reporting usage + SELECT_READY_TO_DISTRIBUTE_APPEALS_ORDER_BY_BFD19_ADDITIONAL_COLS = " select APPEALS.BFKEY, APPEALS.TINUM, APPEALS.BFD19, APPEALS.BFDLOOUT, APPEALS.AOD, APPEALS.BFCORLID, CORRES.SNAMEF, CORRES.SNAMEL, CORRES.SSN, STAFF.SNAMEF as VLJ_NAMEF, STAFF.SNAMEL as VLJ_NAMEL, @@ -210,7 +230,51 @@ class DocketNumberCentennialLoop < StandardError; end left join STAFF on APPEALS.VLJ = STAFF.SATTYID order by BFD19 " + + FROM_LOC_63_APPEALS = " + from BRIEFF + #{VACOLS::Case::JOIN_AOD} + inner join FOLDER on FOLDER.TICKNUM = BRIEFF.BFKEY + where BRIEFF.BFCURLOC in ('63') + and BRIEFF.BFBOX is null + and BRIEFF.BFAC is not null + and BRIEFF.BFD19 is not null + " + + SELECT_LOC_63_APPEALS = " + select BFKEY, BFD19, BFDLOCIN, BFCORLID, BFDLOOUT, BFMPRO, BFCORKEY, BFCURLOC, BFAC, BFHINES, TINUM, TITRNUM, AOD, + BFMEMID, BFDPDCN + #{FROM_LOC_63_APPEALS} + " + # rubocop:disable Metrics/MethodLength + SELECT_APPEALS_IN_LOCATION_63_FROM_PAST_2_DAYS = " + select APPEALS.BFKEY, APPEALS.TINUM, APPEALS.BFD19, APPEALS.BFMEMID, APPEALS.BFCURLOC, + APPEALS.BFDLOCIN, APPEALS.BFCORLID, APPEALS.BFDLOOUT, + case when APPEALS.BFAC = '7' or APPEALS.AOD = 1 then 1 else 0 end AOD, + case when APPEALS.BFAC = '7' then 1 else 0 end CAVC, + APPEALS.VLJ, APPEALS.PREV_DECIDING_JUDGE, APPEALS.HEARING_DATE, APPEALS.PREV_BFDDEC, + CORRES.SNAMEF, CORRES.SNAMEL, CORRES.SSN, + STAFF.SNAMEF as VLJ_NAMEF, STAFF.SNAMEL as VLJ_NAMEL + from ( + select BRIEFF.BFKEY, BRIEFF.TINUM, BFD19, BFDLOOUT, BFAC, BFCORKEY, BFMEMID, BFCURLOC, + BRIEFF.BFDLOCIN, BFCORLID, AOD, + case when BFHINES is null or BFHINES <> 'GP' then VLJ_HEARINGS.VLJ end VLJ + , PREV_APPEAL.PREV_DECIDING_JUDGE PREV_DECIDING_JUDGE + , VLJ_HEARINGS.HEARING_DATE HEARING_DATE + , PREV_APPEAL.PREV_BFDDEC PREV_BFDDEC + from ( + #{SELECT_LOC_63_APPEALS} + ) BRIEFF + #{JOIN_ASSOCIATED_VLJS_BY_HEARINGS} + #{JOIN_PREVIOUS_APPEALS} + where BRIEFF.BFDLOCIN >= TRUNC(CURRENT_DATE) - 2 + order by BFD19 + ) APPEALS + left join CORRES on APPEALS.BFCORKEY = CORRES.STAFKEY + left join STAFF on APPEALS.VLJ = STAFF.SATTYID + " + def self.counts_by_priority_and_readiness query = <<-SQL select count(*) N, PRIORITY, READY @@ -486,7 +550,51 @@ def self.priority_ready_appeal_vacols_ids def self.ready_to_distribute_appeals query = <<-SQL + #{SELECT_READY_TO_DISTRIBUTE_APPEALS_ORDER_BY_BFD19_ADDITIONAL_COLS} + SQL + + fmtd_query = sanitize_sql_array([query]) + connection.exec_query(fmtd_query).to_a + end + + def self.loc_63_appeals + query = <<-SQL + #{SELECT_APPEALS_IN_LOCATION_63_FROM_PAST_2_DAYS} + SQL + + fmtd_query = sanitize_sql_array([query]) + connection.exec_query(fmtd_query).to_a + end + + def self.appeals_tied_to_non_ssc_avljs + query = <<-SQL + with non_ssc_avljs as ( + #{VACOLS::Staff::NON_SSC_AVLJS} + ) #{SELECT_READY_TO_DISTRIBUTE_APPEALS_ORDER_BY_BFD19} + where APPEALS.VLJ in (select * from non_ssc_avljs) + and ( + APPEALS.PREV_DECIDING_JUDGE is null or + ( + APPEALS.PREV_DECIDING_JUDGE = APPEALS.VLJ + AND APPEALS.HEARING_DATE <= APPEALS.PREV_BFDDEC + ) + ) + order by BFD19 + SQL + + fmtd_query = sanitize_sql_array([query]) + connection.exec_query(fmtd_query).to_a + end + + def self.appeals_tied_to_avljs_and_vljs + query = <<-SQL + with all_avljs_andvljs as ( + #{VACOLS::Staff::ALL_AVLJS_AND_VLJS} + ) + #{SELECT_READY_TO_DISTRIBUTE_APPEALS_ORDER_BY_BFD19} + where APPEALS.VLJ in (select * from all_avljs_andvljs) + order by BFD19 SQL fmtd_query = sanitize_sql_array([query]) diff --git a/app/models/vacols/staff.rb b/app/models/vacols/staff.rb index 3a7bc6100de..a0f09fcb72e 100644 --- a/app/models/vacols/staff.rb +++ b/app/models/vacols/staff.rb @@ -14,6 +14,21 @@ class VACOLS::Staff < VACOLS::Record scope :judge, -> { pure_judge.or(acting_judge) } scope :attorney, -> { pure_attorney.or(acting_judge) } + NON_SSC_AVLJS = " + select sattyid + from staff + where sattyid <> smemgrp + and svlj = 'A' + and sactive = 'A' + " + + ALL_AVLJS_AND_VLJS = " + select sattyid + from staff + where svlj in ('A', 'J') + and sactive in ('A', 'I') + " + def self.find_by_css_id(css_id) find_by(sdomainid: css_id) end diff --git a/app/queries/appeals_in_location_63_in_past_2_days.rb b/app/queries/appeals_in_location_63_in_past_2_days.rb new file mode 100644 index 00000000000..a038f01d67d --- /dev/null +++ b/app/queries/appeals_in_location_63_in_past_2_days.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +class AppealsInLocation63InPast2Days + HEADERS = { + docket_number: "Docket Number", + aod: "AOD", + cavc: "CAVC", + receipt_date: "Receipt Date", + ready_for_distribution_at: "Ready for Distribution at", + veteran_file_number: "Veteran File number", + veteran_name: "Veteran", + hearing_judge_id: "Most Recent Hearing Judge ID", + hearing_judge_name: "Most Recent Hearing Judge Name", + deciding_judge_id: "Most Recent Deciding Judge ID", + deciding_judge_name: "Most Recent Deciding Judge Name", + affinity_start_date: "Affinity Start Date", + moved_date_time: "Date/Time Moved", + bfcurloc: "BFCURLOC" + }.freeze + + def self.generate_rows(record) + HEADERS.keys.map { |key| record[key] } + end + + def self.process + # Convert results to CSV format + + CSV.generate(headers: true) do |csv| + # Add headers to CSV + csv << HEADERS.values + + # Iterate through results and add each row to CSV + loc_63_appeals.each do |record| + csv << generate_rows(record) + end + end + end + + def self.loc_63_appeals + docket_coordinator = DocketCoordinator.new + + docket_coordinator.dockets + .flat_map do |sym, docket| + if sym == :legacy + appeals = docket.loc_63_appeals + legacy_rows(appeals).uniq { |record| record[:docket_number] } + else + [] + end + end + end + + def self.legacy_rows(appeals) + unsorted_result = appeals.map do |appeal| + calculated_values = calculate_field_values(appeal) + { + docket_number: appeal["tinum"], + aod: appeal["aod"] == 1, + cavc: appeal["cavc"] == 1, + receipt_date: appeal["bfd19"], + ready_for_distribution_at: appeal["bfdloout"], + veteran_file_number: appeal["ssn"] || appeal["bfcorlid"], + veteran_name: calculated_values[:veteran_name], + hearing_judge_id: calculated_values[:hearing_judge_id], + hearing_judge_name: calculated_values[:hearing_judge_name], + deciding_judge_id: calculated_values[:deciding_judge_id], + deciding_judge_name: calculated_values[:deciding_judge_name], + affinity_start_date: calculated_values[:appeal_affinity]&.affinity_start_date, + moved_date_time: appeal["bfdlocin"], + bfcurloc: appeal["bfcurloc"] + } + end + + unsorted_result.sort_by { |appeal| appeal[:moved_date_time] }.reverse + end + + def self.calculate_field_values(appeal) + vlj_name = FullName.new(appeal["vlj_namef"], nil, appeal["vlj_namel"]).to_s + { + veteran_name: FullName.new(appeal["snamef"], nil, appeal["snamel"]).to_s, + hearing_judge_id: appeal["vlj"].blank? ? nil : legacy_hearing_judge(appeal), + hearing_judge_name: vlj_name.empty? ? nil : vlj_name, + deciding_judge_id: appeal["prev_deciding_judge"].blank? ? nil : legacy_original_deciding_judge(appeal), + deciding_judge_name: appeal["prev_deciding_judge"].blank? ? nil : legacy_original_deciding_judge_name(appeal), + appeal_affinity: AppealAffinity.find_by(case_id: appeal["bfkey"], case_type: "VACOLS::Case") + } + end + + def self.legacy_hearing_judge(appeal) + staff = VACOLS::Staff.find_by(sattyid: appeal["vlj"]) + staff&.sdomainid || appeal["vlj"] + end + + def self.legacy_original_deciding_judge(appeal) + staff = VACOLS::Staff.find_by(sattyid: appeal["prev_deciding_judge"]) + staff&.sdomainid || appeal["prev_deciding_judge"] + end + + def self.legacy_original_deciding_judge_name(appeal) + staff = VACOLS::Staff.find_by(sattyid: appeal["prev_deciding_judge"]) + deciding_judge_name = staff.nil? ? "" : FullName.new(staff["snamef"], nil, staff["snamel"]).to_s + deciding_judge_name.empty? ? nil : deciding_judge_name + end +end diff --git a/app/queries/appeals_ready_for_distribution.rb b/app/queries/appeals_ready_for_distribution.rb index 93934c75c8a..4a757857ef6 100644 --- a/app/queries/appeals_ready_for_distribution.rb +++ b/app/queries/appeals_ready_for_distribution.rb @@ -12,7 +12,8 @@ class AppealsReadyForDistribution target_distro_date: "Target Distro Date", days_before_goal_date: "Days Before Goal Date", hearing_judge: "Hearing Judge", - original_judge: "Original Deciding Judge", + original_judge_id: "Original Deciding Judge ID", + original_judge_name: "Original Deciding Judge", veteran_file_number: "Veteran File number", veteran_name: "Veteran", affinity_start_date: "Affinity Start Date" @@ -54,26 +55,37 @@ def self.ready_appeals def self.legacy_rows(appeals, sym) appeals.map do |appeal| - appeal_affinity = AppealAffinity.find_by(case_id: appeal["bfkey"], case_type: "VACOLS::Case") - - { - docket_number: appeal["tinum"], - docket: sym.to_s, - aod: appeal["aod"] == 1, - cavc: appeal["cavc"] == 1, - receipt_date: appeal["bfd19"], - ready_for_distribution_at: appeal["bfdloout"], - target_distro_date: "N/A", - days_before_goal_date: "N/A", - hearing_judge: FullName.new(appeal["vlj_namef"], nil, appeal["vlj_namel"]).to_s, - original_judge: appeal["prev_deciding_judge"].nil? ? nil : legacy_original_deciding_judge(appeal), - veteran_file_number: appeal["ssn"] || appeal["bfcorlid"], - veteran_name: FullName.new(appeal["snamef"], nil, appeal["snamel"]).to_s, - affinity_start_date: appeal_affinity&.affinity_start_date - } + build_legacy_appeal_row(appeal, sym) end end + def self.build_legacy_appeal_row(appeal, sym) + hearing_judge = FullName.new(appeal["vlj_namef"], nil, appeal["vlj_namel"]).to_s + veteran_name = FullName.new(appeal["snamef"], nil, appeal["snamel"]).to_s + + { + docket_number: appeal["tinum"], + docket: sym.to_s, + aod: appeal["aod"] == 1, + cavc: appeal["cavc"] == 1, + receipt_date: appeal["bfd19"], + ready_for_distribution_at: appeal["bfdloout"], + target_distro_date: "N/A", + days_before_goal_date: "N/A", + hearing_judge: hearing_judge, + original_judge_id: legacy_original_deciding_judge(appeal), + original_judge_name: legacy_original_deciding_judge_name(appeal), + veteran_file_number: appeal["ssn"] || appeal["bfcorlid"], + veteran_name: veteran_name, + affinity_start_date: fetch_affinity_start_date(appeal["bfkey"]) + } + end + + def self.fetch_affinity_start_date(case_id) + appeal_affinity = AppealAffinity.find_by(case_id: case_id, case_type: "VACOLS::Case") + appeal_affinity&.affinity_start_date + end + def self.ama_rows(appeals, docket, sym) appeals.map do |appeal| # This comes from the DistributionTask's assigned_at date @@ -81,6 +93,7 @@ def self.ama_rows(appeals, docket, sym) # only look for hearings that were held hearing_judge = with_held_hearings(appeal) priority_appeal = appeal.aod || appeal.cavc + { docket_number: appeal.docket_number, docket: sym.to_s, @@ -140,4 +153,9 @@ def self.legacy_original_deciding_judge(appeal) staff = VACOLS::Staff.find_by(sattyid: appeal["prev_deciding_judge"]) staff&.sdomainid || appeal["prev_deciding_judge"] end + + def self.legacy_original_deciding_judge_name(appeal) + staff = VACOLS::Staff.find_by(sattyid: appeal["prev_deciding_judge"]) + FullName.new(staff["snamef"], nil, staff["snamel"]).to_s if !staff.nil? + end end diff --git a/app/queries/appeals_tied_to_avljs_and_vljs_query.rb b/app/queries/appeals_tied_to_avljs_and_vljs_query.rb new file mode 100644 index 00000000000..12c8c1abb98 --- /dev/null +++ b/app/queries/appeals_tied_to_avljs_and_vljs_query.rb @@ -0,0 +1,165 @@ +# frozen_string_literal: true + +class AppealsTiedToAvljsAndVljsQuery + # define CSV headers and use this to pull fields to maintain order + + HEADERS = { + docket_number: "Docket number", + docket: "Docket type", + priority: "Priority", + receipt_date: "Receipt Date", + veteran_file_number: "File Number", + veteran_name: "Veteran Name", + vlj: "VLJ Name", + hearing_judge: "Most-recent hearing judge", + most_recent_signing_judge: "Most-recent judge who signed decision name (May be blank if no decision was signed)", + bfcurloc: "Current Location" + }.freeze + + def self.generate_rows(record) + HEADERS.keys.map { |key| record[key] } + end + + def self.process + # Convert results to CSV format + + CSV.generate(headers: true) do |csv| + # Add headers to CSV + csv << HEADERS.values + + # Iterate through results and add each row to CSV + tied_appeals.each do |record| + csv << generate_rows(record) + end + end + end + + # Uses DocketCoordinator to pull appeals ready for distribution + # DocketCoordinator is used by Automatic Case Distribution so this will give us the most accurate list of appeals + def self.tied_appeals + docket_coordinator = DocketCoordinator.new + + docket_coordinator.dockets + .flat_map do |sym, docket| + if sym == :legacy + appeals = docket.appeals_tied_to_avljs_and_vljs + unique_appeals = legacy_rows(appeals, sym).uniq { |record| record[:docket_number] } + + unique_appeals + else + appeals = docket.tied_to_vljs(vlj_user_ids) + + ama_rows(appeals, sym) + end + end + end + + def self.legacy_rows(appeals, sym) + appeals.map do |appeal| + calculated_values = calculate_field_values(appeal) + { + docket_number: appeal["tinum"], + docket: sym.to_s, + priority: appeal["priority"] == 1 ? "True" : "", + receipt_date: appeal["bfd19"], + veteran_file_number: calculated_values[:veteran_file_number], + veteran_name: calculated_values[:veteran_name], + vlj: calculated_values[:vlj], + hearing_judge: calculated_values[:hearing_judge], + most_recent_signing_judge: calculated_values[:most_recent_signing_judge], + bfcurloc: calculated_values[:bfcurloc] + } + end + end + + def self.ama_rows(appeals, sym) + appeals.map do |appeal| + # # This comes from the DistributionTask's assigned_at date + # ready_for_distribution_at = distribution_task_query(appeal) + # only look for hearings that were held + hearing_judge = ama_hearing_judge(appeal) + signing_judge = ama_cavc_original_deciding_judge(appeal) + { + docket_number: appeal.docket_number, + docket: sym.to_s, + priority: appeal.aod || appeal.cavc, + receipt_date: appeal.receipt_date, + veteran_file_number: appeal.veteran_file_number, + veteran_name: appeal.veteran&.name.to_s, + vlj: hearing_judge, + hearing_judge: hearing_judge, + most_recent_signing_judge: signing_judge, + bfcurloc: nil + } + end + end + + def self.vlj_user_ids + staff_domainids = VACOLS::Staff.where("svlj in ('A','J') AND sactive in ('A','I') ") + .pluck(:sdomainid) + .uniq + .compact + + User.where(css_id: staff_domainids).pluck(:id) + end + + def self.calculate_field_values(appeal) + vlj_name = get_vlj_name(appeal) + prev_judge_name = get_prev_judge_name(appeal) + vacols_case = VACOLS::Case.find_by(bfkey: appeal["bfkey"]) + veteran_record = VACOLS::Correspondent.find_by(stafkey: vacols_case.bfcorkey) + { + veteran_file_number: veteran_record.ssn || vacols_case&.bfcorlid, + veteran_name: get_name_from_record(veteran_record), + vlj: vlj_name, + hearing_judge: vlj_name, + most_recent_signing_judge: prev_judge_name, + bfcurloc: vacols_case&.bfcurloc + } + end + + def self.get_vlj_name(appeal) + if appeal["vlj"].nil? + vlj_name = nil + else + vlj_record = VACOLS::Staff.find_by(sattyid: appeal["vlj"]) + vlj_name = get_name_from_record(vlj_record) + end + + vlj_name + end + + def self.get_prev_judge_name(appeal) + if appeal["prev_deciding_judge"].nil? + prev_judge_name = nil + else + prev_judge_record = VACOLS::Staff.find_by(sattyid: appeal["prev_deciding_judge"]) + prev_judge_name = get_name_from_record(prev_judge_record) + end + + prev_judge_name + end + + def self.get_name_from_record(record) + FullName.new(record["snamef"], nil, record["snamel"]).to_s + end + + def self.ama_hearing_judge(appeal) + appeal.hearings + .filter { |hearing| hearing.disposition = Constants.HEARING_DISPOSITION_TYPES.held } + .first&.judge&.full_name + end + + def self.ama_cavc_original_deciding_judge(appeal) + return nil if appeal.cavc_remand.nil? + + source_appeal_id = CavcRemand.find_by(remand_appeal: appeal).source_appeal_id + judge_css_id = Task.find_by( + appeal_id: source_appeal_id, + appeal_type: Appeal.name, + type: JudgeDecisionReviewTask.name + )&.assigned_to&.css_id + + User.find_by_css_id(judge_css_id)&.full_name + end +end diff --git a/app/queries/appeals_tied_to_non_ssc_avlj_query.rb b/app/queries/appeals_tied_to_non_ssc_avlj_query.rb new file mode 100644 index 00000000000..62aff71e308 --- /dev/null +++ b/app/queries/appeals_tied_to_non_ssc_avlj_query.rb @@ -0,0 +1,113 @@ +# frozen_string_literal: true + +class AppealsTiedToNonSscAvljQuery + # define CSV headers and use this to pull fields to maintain order + + HEADERS = { + docket_number: "Docket number", + docket: "Docket type", + priority: "Priority", + receipt_date: "Receipt Date", + veteran_file_number: "File Number", + veteran_name: "Veteran Name", + non_ssc_avlj: "Non-SSC AVLJ's Name", + hearing_judge: "Most-recent hearing judge", + most_recent_signing_judge: "Most-recent judge who signed decision", + bfcurloc: "BFCURLOC" + }.freeze + + def self.generate_rows(record) + HEADERS.keys.map { |key| record[key] } + end + + def self.process + # Convert results to CSV format + + CSV.generate(headers: true) do |csv| + # Add headers to CSV + csv << HEADERS.values + + # Iterate through results and add each row to CSV + tied_appeals.each do |record| + csv << generate_rows(record) + end + end + end + + # Uses DocketCoordinator to pull appeals ready for distribution + # DocketCoordinator is used by Automatic Case Distribution so this will give us the most accurate list of appeals + def self.tied_appeals + docket_coordinator = DocketCoordinator.new + + docket_coordinator.dockets + .flat_map do |sym, docket| + if sym == :legacy + appeals = docket.appeals_tied_to_non_ssc_avljs + unique_appeals = legacy_rows(appeals, sym).uniq { |record| record[:docket_number] } + + unique_appeals + else + [] + end + end + end + + def self.legacy_rows(appeals, sym) + appeals.map do |appeal| + calculated_values = calculate_field_values(appeal) + { + docket_number: appeal["tinum"], + docket: sym.to_s, + priority: appeal["priority"] == 1 ? "True" : "", + receipt_date: appeal["bfd19"], + veteran_file_number: calculated_values[:veteran_file_number], + veteran_name: calculated_values[:veteran_name], + non_ssc_avlj: calculated_values[:non_ssc_avlj], + hearing_judge: calculated_values[:hearing_judge], + most_recent_signing_judge: calculated_values[:most_recent_signing_judge], + bfcurloc: calculated_values[:bfcurloc] + } + end + end + + def self.calculate_field_values(appeal) + avlj_name = get_avlj_name(appeal) + prev_judge_name = get_prev_judge_name(appeal) + vacols_case = VACOLS::Case.find_by(bfkey: appeal["bfkey"]) + veteran_record = VACOLS::Correspondent.find_by(stafkey: vacols_case.bfcorkey) + { + veteran_file_number: veteran_record.ssn || vacols_case&.bfcorlid, + veteran_name: get_name_from_record(veteran_record), + non_ssc_avlj: avlj_name, + hearing_judge: avlj_name, + most_recent_signing_judge: prev_judge_name, + bfcurloc: vacols_case&.bfcurloc + } + end + + def self.get_avlj_name(appeal) + if appeal["vlj"].nil? + avlj_name = nil + else + avlj_record = VACOLS::Staff.find_by(sattyid: appeal["vlj"]) + avlj_name = get_name_from_record(avlj_record) + end + + avlj_name + end + + def self.get_prev_judge_name(appeal) + if appeal["prev_deciding_judge"].nil? + prev_judge_name = nil + else + prev_judge_record = VACOLS::Staff.find_by(sattyid: appeal["prev_deciding_judge"]) + prev_judge_name = get_name_from_record(prev_judge_record) + end + + prev_judge_name + end + + def self.get_name_from_record(record) + FullName.new(record["snamef"], nil, record["snamel"]).to_s + end +end diff --git a/app/repositories/appeal_repository.rb b/app/repositories/appeal_repository.rb index ba473615590..a5385e95883 100644 --- a/app/repositories/appeal_repository.rb +++ b/app/repositories/appeal_repository.rb @@ -848,6 +848,7 @@ def distribute_nonpriority_appeals(judge, genpop, range, limit, bust_backlog) end end + # currently this is used for reporting needs def ready_to_distribute_appeals MetricsService.record("VACOLS: ready_to_distribute_appeals", name: "ready_to_distribute_appeals", @@ -856,6 +857,30 @@ def ready_to_distribute_appeals end end + def appeals_tied_to_non_ssc_avljs + MetricsService.record("VACOLS: appeals_tied_to_non_ssc_avljs", + name: "appeals_tied_to_non_ssc_avljs", + service: :vacols) do + VACOLS::CaseDocket.appeals_tied_to_non_ssc_avljs + end + end + + def appeals_tied_to_avljs_and_vljs + MetricsService.record("VACOLS: appeals_tied_to_avljs_and_vljs", + name: "appeals_tied_to_avljs_and_vljs", + service: :vacols) do + VACOLS::CaseDocket.appeals_tied_to_avljs_and_vljs + end + end + + def loc_63_appeals + MetricsService.record("VACOLS: loc_63_appeals", + name: "loc_63_appeals", + service: :vacols) do + VACOLS::CaseDocket.loc_63_appeals + end + end + private # NOTE: this should be called within a transaction where you are closing an appeal diff --git a/app/views/case_distribution_levers/index.html.erb b/app/views/case_distribution_levers/index.html.erb index f973dec10bb..c9ba0e386f4 100644 --- a/app/views/case_distribution_levers/index.html.erb +++ b/app/views/case_distribution_levers/index.html.erb @@ -8,7 +8,8 @@ acd_levers: @acd_levers, acd_history: @acd_history, user_is_an_acd_admin: @user_is_an_acd_admin, - acd_exclude_from_affinity: @acd_exclude_from_affinity + acd_exclude_from_affinity: @acd_exclude_from_affinity, + returnedAppealJobs: @returned_appeal_jobs }) %> <% end %> diff --git a/app/views/case_distribution_levers/test.html.erb b/app/views/case_distribution_levers/test.html.erb index 6f0fe70e96e..f6be3c5eb98 100644 --- a/app/views/case_distribution_levers/test.html.erb +++ b/app/views/case_distribution_levers/test.html.erb @@ -5,6 +5,7 @@ applicationUrls: application_urls, feedbackUrl: feedback_url, acdLevers: @acd_levers, - acdHistory: @acd_history + acdHistory: @acd_history, + returnedAppealJobs: @returned_appeal_jobs }) %> <% end %> diff --git a/client/app/caseDistribution/components/CollapsibleTable.jsx b/client/app/caseDistribution/components/CollapsibleTable.jsx new file mode 100644 index 00000000000..81bd0f64741 --- /dev/null +++ b/client/app/caseDistribution/components/CollapsibleTable.jsx @@ -0,0 +1,68 @@ +import React, { useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; + +const CollapsibleTable = (props) => { + const { returnedAppealJobs } = props; + const [expandedRows, setExpandedRows] = useState([]); + const [allExpanded, setAllExpanded] = useState(true); + + useEffect(() => { + const allRowIds = returnedAppealJobs.map((row) => row.id); + + setExpandedRows(allRowIds); + }, [returnedAppealJobs]); + + const toggleAllRows = () => { + + if (allExpanded) { + // Collapse all rows + setExpandedRows([]); + } else { + // Expand all rows + const allRowIds = returnedAppealJobs.map((row) => row.id); + + setExpandedRows(allRowIds); + } + setAllExpanded(!allExpanded); + }; + + const renderRowDetails = (row) => { + return ( + + {row.created_at} + {row.returned_appeals.join(', ')} + {JSON.parse(row.stats).message} + + ); + }; + + return ( +
+ + + + + + + + + + + {returnedAppealJobs.map((row) => ( + + {expandedRows.includes(row.id) && renderRowDetails(row)} + + ))} + +
Created AtReturned AppealsStats
+
+ ); +}; + +CollapsibleTable.propTypes = { + returnedAppealJobs: PropTypes.array, +}; + +export default CollapsibleTable; diff --git a/client/app/caseDistribution/test.jsx b/client/app/caseDistribution/test.jsx index 68b83c16f55..b570a804cdb 100644 --- a/client/app/caseDistribution/test.jsx +++ b/client/app/caseDistribution/test.jsx @@ -1,3 +1,4 @@ +/* eslint-disable max-lines */ /* eslint-disable react/prop-types */ import React from 'react'; @@ -11,6 +12,8 @@ import Footer from '@department-of-veterans-affairs/caseflow-frontend-toolkit/co import CaseSearchLink from '../components/CaseSearchLink'; import ApiUtil from '../util/ApiUtil'; import Button from '../components/Button'; +import Alert from 'app/components/Alert'; +import CollapsibleTable from './components/CollapsibleTable'; class CaseDistributionTest extends React.PureComponent { constructor(props) { @@ -19,20 +22,37 @@ class CaseDistributionTest extends React.PureComponent { isReseedingAod: false, isReseedingNonAod: false, isReseedingAmaDocketGoals: false, - isReseedingDocketPriority: false + isReseedingDocketPriority: false, + isReturnLegacyAppeals: false, + isFailReturnLegacyAppeals: false, + showLegacyAppealsAlert: false, + showAlert: false, + alertType: 'success', }; } + componentDidUpdate() { + // Delay of 5 seconds + setTimeout(() => { + this.setState({ showAlert: false, showLegacyAppealsAlert: false }); + }, 5000); + } + reseedAod = () => { this.setState({ isReseedingAod: true }); ApiUtil.post('/case_distribution_levers_tests/run_demo_aod_hearing_seeds').then(() => { this.setState({ isReseedingAod: false, + showAlert: true, + alertMsg: 'Successfully Completed Seeding Aod Hearing Held Appeals.', }); }, (err) => { console.warn(err); this.setState({ isReseedingAod: false, + showAlert: true, + alertMsg: err, + alertType: 'error', }); }); }; @@ -42,11 +62,16 @@ class CaseDistributionTest extends React.PureComponent { ApiUtil.post('/case_distribution_levers_tests/run_demo_non_aod_hearing_seeds').then(() => { this.setState({ isReseedingNonAod: false, + showAlert: true, + alertMsg: 'Successfully Completed Seeding Non Aod Hearing Held Appeals.', }); }, (err) => { console.warn(err); this.setState({ isReseedingNonAod: false, + showAlert: true, + alertMsg: err, + alertType: 'error', }); }); }; @@ -56,25 +81,74 @@ class CaseDistributionTest extends React.PureComponent { ApiUtil.post('/case_distribution_levers_tests/run-demo-ama-docket-goals').then(() => { this.setState({ isReseedingAmaDocketGoals: false, + showAlert: true, + alertMsg: 'Successfully Completed Seeding Ama Docket Time Goal Non Priority Appeals.', }); }, (err) => { console.warn(err); this.setState({ isReseedingAmaDocketGoals: false, + showAlert: true, + alertMsg: err, + alertType: 'error', }); }); }; reseedDocketPriority = () => { this.setState({ isReseedingDocketPriority: true }); - ApiUtil.post('/case_distribution_levers_tests/run-demo-docket-priority').then(() => { + ApiUtil.post('/case_distribution_levers_tests/run_demo_docket_priority').then(() => { this.setState({ isReseedingDocketPriority: false, + showAlert: true, + alertMsg: 'Successfully Completed Seeding Docket Type Appeals.', }); }, (err) => { console.warn(err); this.setState({ isReseedingDocketPriority: false, + showAlert: true, + alertMsg: err, + alertType: 'error', + }); + }); + }; + + reseedNonSSCAVLJAppeals = () => { + this.setState({ isReseedingNonSSCAVLJAppeals: true }); + ApiUtil.post('/case_distribution_levers_tests/run_demo_non_avlj_appeals').then(() => { + this.setState({ + isReseedingNonSSCAVLJAppeals: false, + showAlert: true, + alertMsg: 'Successfully Completed Seeding non-SSC AVLJ and Appeals.', + }); + }, (err) => { + console.warn(err); + this.setState({ + isReseedingNonSSCAVLJAppeals: false, + showAlert: true, + alertMsg: err, + alertType: 'error', + }); + }); + }; + + returnLegacyAppealsToBoard = () => { + this.setState({ isReturnLegacyAppeals: true }); + ApiUtil.post('/case_distribution_levers_tests/run_return_legacy_appeals_to_board').then(() => { + this.setState({ + isReturnLegacyAppeals: false, + showLegacyAppealsAlert: true, + legacyAppealsAlertType: 'success', + legacyAppealsAlertMsg: 'Successfully Completed Return Legacy Appeals To Board Job.', + }); + }, (err) => { + console.warn(err); + this.setState({ + isReturnLegacyAppeals: false, + showLegacyAppealsAlert: true, + legacyAppealsAlertType: 'error', + legacyAppealsAlertMsg: err }); }); }; @@ -137,6 +211,16 @@ class CaseDistributionTest extends React.PureComponent { +
  • + +
  • +
  • + + + +
  • Case Distribution Levers

    +
  • + + + +
  • +
  • + + + +
  • +
  • + + + +

  • Run Seed Files

    + { this.state.showAlert && + {this.state.alertMsg} + }
    • +
    • +
    • +
    +
    +

    Case Movement

    + { this.state.showLegacyAppealsAlert && + + {this.state.legacyAppealsAlertMsg} + + } +
      +
    • +

    +

    Log of 15 most recent appeals moved to location 63

    + +
    + ); }} /> diff --git a/client/app/styles/caseDistribution/_test_seeds.scss b/client/app/styles/caseDistribution/_test_seeds.scss index a3f5292a83f..257a9e240f8 100644 --- a/client/app/styles/caseDistribution/_test_seeds.scss +++ b/client/app/styles/caseDistribution/_test_seeds.scss @@ -2,6 +2,7 @@ $seed-table-border-color: #d6d7d9; $seed-button-background-color: #0071bc; $seed-button-font-color: #fff; $seed-table-preview-bg-color: #f1f1f1; +$case-movement-button-bg-color: #07648d; .test-seeds-num-field { // width: auto; @@ -122,3 +123,11 @@ $seed-table-preview-bg-color: #f1f1f1; justify-content: flex-end; flex-direction: row; } + +.usa-button-case-movement { + background: $case-movement-button-bg-color; +} + +.usa-button-case-movement:hover { + background: $case-movement-button-bg-color; +} diff --git a/client/constants/ACD_LEVERS.json b/client/constants/ACD_LEVERS.json index 92eb0942637..67b22361cde 100644 --- a/client/constants/ACD_LEVERS.json +++ b/client/constants/ACD_LEVERS.json @@ -25,7 +25,8 @@ "affinity": "affinity", "docket_distribution_prior": "docket_distribution_prior", "docket_time_goal": "docket_time_goal", - "docket_levers": "docket_levers" + "docket_levers": "docket_levers", + "internal": "internal" }, "validation_error_message": { "minimum_not_met": "Please enter a value greater than or equal to 0", diff --git a/client/constants/DISTRIBUTION.json b/client/constants/DISTRIBUTION.json index d4b1809cd9c..f4bcaea1542 100644 --- a/client/constants/DISTRIBUTION.json +++ b/client/constants/DISTRIBUTION.json @@ -54,5 +54,10 @@ "disable_ama_priority_direct_review": "disable_ama_priority_direct_review", "disable_ama_priority_direct_review_title": "ACD Disable AMA Priority Direct Review", "disable_ama_priority_evidence_submission": "disable_ama_priority_evidence_submission", - "disable_ama_priority_evidence_submission_title": "ACD Disable AMA Priority Evidence Submission" + "disable_ama_priority_evidence_submission_title": "ACD Disable AMA Priority Evidence Submission", + "enable_nonsscavlj": "enable_nonsscavlj", + "enable_nonsscavlj_title": "Enable Non-SSC/AVLJ", + "no_records_moved_message": "Job Ran Successfully, No Records Moved", + "nonsscavlj_number_of_appeals_to_move": "nonsscavlj_number_of_appeals_to_move", + "nonsscavlj_number_of_appeals_to_move_title": "Non-SSC/AVLJ Number of Appeals to Move" } diff --git a/config/routes.rb b/config/routes.rb index ef67aeffb8e..3b14fac4202 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -30,11 +30,16 @@ get 'appeals_ready_to_distribute' get 'appeals_non_priority_ready_to_distribute' get 'appeals_distributed' + get 'appeals_in_location_63_in_past_2_days' get 'ineligible_judge_list' + get 'appeals_tied_to_non_ssc_avlj' + get 'appeals_tied_to_avljs_and_vljs' post 'run_demo_aod_hearing_seeds' post 'run_demo_non_aod_hearing_seeds' post 'run-demo-ama-docket-goals' - post 'run-demo-docket-priority' + post 'run_demo_non_avlj_appeals' + post 'run_demo_docket_priority' + post 'run_return_legacy_appeals_to_board' end end diff --git a/db/migrate/20240717034659_create_returned_appeal_jobs.rb b/db/migrate/20240717034659_create_returned_appeal_jobs.rb new file mode 100644 index 00000000000..35ac2499b65 --- /dev/null +++ b/db/migrate/20240717034659_create_returned_appeal_jobs.rb @@ -0,0 +1,13 @@ +class CreateReturnedAppealJobs < ActiveRecord::Migration[6.0] + def change + create_table :returned_appeal_jobs do |t| + t.timestamp :started_at + t.timestamp :completed_at + t.timestamp :errored_at + t.json :stats + t.text :returned_appeals, array: true, default: [] + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 3f0a705c442..a1fcc561b94 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -1738,6 +1738,16 @@ t.index ["user_id"], name: "index_request_issues_updates_on_user_id" end + create_table "returned_appeal_jobs", force: :cascade do |t| + t.datetime "completed_at" + t.datetime "created_at", null: false + t.datetime "errored_at" + t.text "returned_appeals", default: [], array: true + t.datetime "started_at" + t.json "stats" + t.datetime "updated_at", null: false + end + create_table "schedule_periods", force: :cascade do |t| t.datetime "created_at", null: false t.date "end_date", null: false diff --git a/db/seeds/case_distribution_levers.rb b/db/seeds/case_distribution_levers.rb index 043ec9b2a8c..c83ba6d1f88 100644 --- a/db/seeds/case_distribution_levers.rb +++ b/db/seeds/case_distribution_levers.rb @@ -784,6 +784,30 @@ def levers } ] }, + { + item: Constants.DISTRIBUTION.enable_nonsscavlj, + title: Constants.DISTRIBUTION.enable_nonsscavlj_title, + description: "This is the internal lever used to enable and disable Non-SSC AVLJ work.", + data_type: Constants.ACD_LEVERS.data_types.boolean, + value: true, + unit: "", + is_disabled_in_ui: true, + algorithms_used: [], + lever_group: Constants.ACD_LEVERS.lever_groups.internal, + lever_group_order: 0 + }, + { + item: Constants.DISTRIBUTION.nonsscavlj_number_of_appeals_to_move, + title: Constants.DISTRIBUTION.nonsscavlj_number_of_appeals_to_move_title, + description: "This is the internal lever used to alter the number of appeals to be returned for Non-SSC AVLJs", + data_type: Constants.ACD_LEVERS.data_types.number, + value: 2, + unit: "", + is_disabled_in_ui: true, + algorithms_used: [], + lever_group: Constants.ACD_LEVERS.lever_groups.internal, + lever_group_order: 999 + }, ] end @@ -824,7 +848,6 @@ def full_update(item) # DANGER DANGER DANGER DANGER DANGER DANGER DANGER DANGER DANGER DANGER DANGER def full_update_lever(lever) existing_lever = CaseDistributionLever.find_by_item(lever[:item]) - existing_lever.update( title: lever[:title], description: lever[:description], diff --git a/db/seeds/non_ssc_avlj_legacy_appeals.rb b/db/seeds/non_ssc_avlj_legacy_appeals.rb new file mode 100644 index 00000000000..49286f69406 --- /dev/null +++ b/db/seeds/non_ssc_avlj_legacy_appeals.rb @@ -0,0 +1,472 @@ +# frozen_string_literal: true + +module Seeds + class NonSscAvljLegacyAppeals < Base + def initialize + # initialize_np_legacy_appeals_file_number_and_participant_id + # initialize_priority_legacy_appeals_file_number_and_participant_id + end + + def seed! + RequestStore[:current_user] = User.system_user + create_avljs + create_legacy_appeals + end + + private + + def create_avljs + create_non_ssc_avlj("NONSSCAN01", "Four Priority") + create_non_ssc_avlj("NONSSCAN02", "Four non-priority") + create_non_ssc_avlj("NONSSCAN03", "Four-pri h-and-d") + create_non_ssc_avlj("NONSSCAN04", "Four-non-pri h-and-d") + create_non_ssc_avlj("NONSSCAN05", "For-mix-of both-h-only") + create_non_ssc_avlj("NONSSCAN06", "For-mix-of both-h-and-d") + create_non_ssc_avlj("NONSSCAN07", "Do-not-get moved-pri") + create_non_ssc_avlj("NONSSCAN08", "Do-not-get moved-nonpri") + create_non_ssc_avlj("NONSSCAN09", "Do-not-get moved-mix") + create_non_ssc_avlj("NONSSCAN10", "Some-moved some-not") + create_ssc_avlj("SSCA11", "Does-not qualify for-mvmt") + create_non_ssc_avlj("NONSSCAN12", "Two-judges last-is-SSC") + create_non_ssc_avlj("NONSSCAN13", "Two-judges both-non-SSC") + create_inactive_non_ssc_avlj("NONSSCAN14", "Inactive Non") + create_vlj("REGVLJ01", "Regular VLJ1") + create_vlj("REGVLJ02", "Regular VLJ2") + create_non_ssc_avlj("SIGNAVLJLGC", "NonSSC Signing-AVLJ") + create_non_ssc_avlj("AVLJLGC2", "Alternate NonSSC-AVLJ") + create_ssc_avlj("SSCAVLJLGC", "SSC-Two-judges last-is-SSC") + end + + def create_legacy_appeals + # the naming comes from the acceptance criteria of APPEALS-45208 + create_four_priority_appeals_tied_to_a_non_ssc_avlj + create_four_non_priority_appeals_tied_to_a_non_ssc_avlj + create_four_priority_appeals_tied_to_and_signed_by_a_non_ssc_avlj + create_four_non_priority_appeals_tied_to_and_signed_by_a_non_ssc_avlj + create_four_alternating_priority_by_age_appeals_tied_to_a_non_ssc_avlj + create_four_alternating_priority_by_age_appeals_tied_to_and_signed_by_a_non_ssc_avlj + create_four_priority_appeals_tied_to_a_non_ssc_avlj_signed_by_another_avlj + create_four_non_priority_appeals_tied_to_a_non_ssc_avlj_signed_by_another_avlj + create_four_alternating_priority_by_age_appeals_tied_to_a_non_ssc_avlj_signed_by_another_avlj + create_two_sets_of_seven_types_of_appeals_tied_to_a_non_ssc_avlj + create_four_alternating_priority_by_age_appeals_tied_to_a_ssc_avlj + create_four_alternating_priority_by_age_appeals_tied_to_a_non_ssc_avlj_with_a_second_hearing_held_by_a_ssc_avlj + create_four_alternating_priority_by_age_appeals_tied_to_a_non_ssc_avlj_with_a_second_hearing_held_by_another_non_ssc_avlj + create_unsigned_priority_appeal_tied_to_inactive_non_ssc_avlj + create_signed_non_priority_appeal_tied_to_inactive_non_ssc_avlj + create_unsigned_priority_ama_appeal_tied_to_non_ssc_avlj + create_signed_non_priority_ama_appeal_tied_to_non_ssc_avlj + create_signed_priority_appeal_tied_to_vlj + create_unsigned_non_priority_appeal_tied_to_vlj + end + + def create_four_priority_appeals_tied_to_a_non_ssc_avlj + # A non-SSC AVLJ that Only has 4 priority cases where they held the last hearing + avlj = User.find_by(css_id: "NONSSCAN01") + create_legacy_appeal(priority=true, avlj, 300.days.ago) + create_legacy_appeal(priority=true, avlj, 200.days.ago) + create_legacy_appeal(priority=true, avlj, 100.days.ago) + create_legacy_appeal(priority=true, avlj, 30.days.ago) + end + + def create_four_non_priority_appeals_tied_to_a_non_ssc_avlj + # A non-SSC AVLJ that Only has 4 non-priority cases where they held the last hearing + avlj = User.find_by(css_id: "NONSSCAN02") + create_legacy_appeal(priority=false, avlj, 350.days.ago) + create_legacy_appeal(priority=false, avlj, 250.days.ago) + create_legacy_appeal(priority=false, avlj, 150.days.ago) + create_legacy_appeal(priority=false, avlj, 50.days.ago) + end + + def create_four_priority_appeals_tied_to_and_signed_by_a_non_ssc_avlj + assigned_avlj = User.find_by(css_id: "NONSSCAN03") + signing_avlj = User.find_by(css_id: "NONSSCAN03") + create_signed_legacy_appeal(priority=true, signing_avlj, assigned_avlj, 100.days.ago) + create_signed_legacy_appeal(priority=true, signing_avlj, assigned_avlj, 80.days.ago) + create_signed_legacy_appeal(priority=true, signing_avlj, assigned_avlj, 60.days.ago) + create_signed_legacy_appeal(priority=true, signing_avlj, assigned_avlj, 30.days.ago) + end + + def create_four_non_priority_appeals_tied_to_and_signed_by_a_non_ssc_avlj + # A non-SSC AVLJ that Only has 4 non-priority cases where they held the last hearing and signed the most recent decision + assigned_avlj = User.find_by(css_id: "NONSSCAN04") + signing_avlj = User.find_by(css_id: "NONSSCAN04") + create_signed_legacy_appeal(priority=false, signing_avlj, assigned_avlj, 110.days.ago) + create_signed_legacy_appeal(priority=false, signing_avlj, assigned_avlj, 90.days.ago) + create_signed_legacy_appeal(priority=false, signing_avlj, assigned_avlj, 70.days.ago) + create_signed_legacy_appeal(priority=false, signing_avlj, assigned_avlj, 40.days.ago) + end + + def create_four_alternating_priority_by_age_appeals_tied_to_a_non_ssc_avlj + # A non-SSC AVLJ that Has 4 in alternating order by age of BRIEFF.BFD19 (Docket Date) + # priority cases where they held the last hearing + # non-priority cases where they held the last hearing + avlj = User.find_by(css_id: "NONSSCAN05") + create_legacy_appeal(priority=false, avlj, 600.days.ago) #oldest + create_legacy_appeal(priority=true, avlj, 425.days.ago) + create_legacy_appeal(priority=false, avlj, 400.days.ago) + create_legacy_appeal(priority=true, avlj, 40.days.ago) #most recent + end + + def create_four_alternating_priority_by_age_appeals_tied_to_and_signed_by_a_non_ssc_avlj + # A non-SSC AVLJ that Has 4 in alternating order by age of BRIEFF.BFD19 (Docket Date) + # priority cases where they held the last hearing and signed the most recent decision + # non-priority cases where they held the last hearing and signed the most recent decision + signing_avlj = User.find_by(css_id: "NONSSCAN06") + assigned_avlj = User.find_by(css_id: "NONSSCAN06") + create_signed_legacy_appeal(priority=false, signing_avlj, assigned_avlj, 120.days.ago) #oldest + create_signed_legacy_appeal(priority=true, signing_avlj, assigned_avlj, 110.days.ago) + create_signed_legacy_appeal(priority=false, signing_avlj, assigned_avlj, 100.days.ago) + create_signed_legacy_appeal(priority=true, signing_avlj, assigned_avlj, 50.days.ago) #most recent + end + + def create_four_priority_appeals_tied_to_a_non_ssc_avlj_signed_by_another_avlj + # A non-SSC AVLJ that Only has 4 priority cases where they held the last hearing and did NOT sign the most recent decision + # These cases should NOT be returned to the board + assigned_avlj = User.find_by(css_id: "NONSSCAN07") + signing_avlj = User.find_by(css_id: "SIGNAVLJLGC") + create_signed_legacy_appeal(priority=true, signing_avlj, assigned_avlj, 120.days.ago) + create_signed_legacy_appeal(priority=true, signing_avlj, assigned_avlj, 110.days.ago) + create_signed_legacy_appeal(priority=true, signing_avlj, assigned_avlj, 100.days.ago) + create_signed_legacy_appeal(priority=true, signing_avlj, assigned_avlj, 50.days.ago) + end + + def create_four_non_priority_appeals_tied_to_a_non_ssc_avlj_signed_by_another_avlj + # A non-SSC AVLJ that Only has 4 non-priority cases where they held the last hearing and did NOT sign the most recent decision + # These cases should NOT be returned to the board + assigned_avlj = User.find_by(css_id: "NONSSCAN08") + signing_avlj = User.find_by(css_id: "SIGNAVLJLGC") + create_signed_legacy_appeal(priority=false, signing_avlj, assigned_avlj, 120.days.ago) + create_signed_legacy_appeal(priority=false, signing_avlj, assigned_avlj, 110.days.ago) + create_signed_legacy_appeal(priority=false, signing_avlj, assigned_avlj, 100.days.ago) + create_signed_legacy_appeal(priority=false, signing_avlj, assigned_avlj, 50.days.ago) + end + + def create_four_alternating_priority_by_age_appeals_tied_to_a_non_ssc_avlj_signed_by_another_avlj + # A non-SSC AVLJ that Has 4 in alternating order by age of BRIEFF.BFD19 (Docket Date) + # priority cases where they held the last hearing and did NOT sign the most recent decision + # These cases should NOT be returned to the board + # non-priority cases where they held the last hearing and did NOT sign the most recent decision + # These cases should NOT be returned to the board + assigned_avlj = User.find_by(css_id: "NONSSCAN09") + signing_avlj = User.find_by(css_id: "SIGNAVLJLGC") + create_signed_legacy_appeal(priority=false, signing_avlj, assigned_avlj, 220.days.ago) #oldest + create_signed_legacy_appeal(priority=true, signing_avlj, assigned_avlj, 210.days.ago) + create_signed_legacy_appeal(priority=false, signing_avlj, assigned_avlj, 200.days.ago) + create_signed_legacy_appeal(priority=true, signing_avlj, assigned_avlj, 150.days.ago) #most recent + end + + def create_two_sets_of_seven_types_of_appeals_tied_to_a_non_ssc_avlj + # A non-SSC AVLJ that Has 12 appeals + # Notes + # Cycle through the groups before creating the second appeal in the group, make each created appeal newer by BRIEFF.BFD19 (Docket Date) than the previous one + # Appeals in the same group should not be grouped next to each other + # appeals + # 1. priority cases where they held the last hearing and did NOT sign the most recent decision + # These cases should NOT be returned to the board + # 2. non-priority cases where they held the last hearing and did NOT sign the most recent decision + # These cases should NOT be returned to the board + # 3. priority cases where they held the last hearing + # 4. non-priority cases where they held the last hearing + # 5. priority cases where they held the last hearing and signed the most recent decision + # 6. non-priority cases where they held the last hearing and signed the most recent decision + # 7. has an appeal with a hearing where they were the judge but the appeal is NOT ready to distribute + # This case would NOT show up in the ready to distribute query, but we could look it up by veteran ID to verify that it didn't get moved. + + assigned_avlj = User.find_by(css_id: "NONSSCAN10") + signing_avlj = User.find_by(css_id: "SIGNAVLJLGC") + create_signed_legacy_appeal(priority=false, signing_avlj, assigned_avlj, 220.days.ago) #oldest + create_signed_legacy_appeal(priority=true, signing_avlj, assigned_avlj, 210.days.ago) + create_legacy_appeal(priority=true, assigned_avlj, 200.days.ago) + create_legacy_appeal(priority=false, assigned_avlj, 190.days.ago) + create_signed_legacy_appeal(priority=false, assigned_avlj, assigned_avlj, 180.days.ago) + create_signed_legacy_appeal(priority=true, assigned_avlj, assigned_avlj, 170.days.ago) + legacy_appeal = create_legacy_appeal(priority=true, assigned_avlj, 160.days.ago) + make_legacy_appeal_not_ready_for_distribution(legacy_appeal) + + create_signed_legacy_appeal(priority=false, signing_avlj, assigned_avlj, 150.days.ago) + create_signed_legacy_appeal(priority=true, signing_avlj, assigned_avlj, 140.days.ago) + create_legacy_appeal(priority=true, assigned_avlj, 130.days.ago) + create_legacy_appeal(priority=false, assigned_avlj, 120.days.ago) + create_signed_legacy_appeal(priority=false, assigned_avlj, assigned_avlj, 110.days.ago) + create_signed_legacy_appeal(priority=true, assigned_avlj, assigned_avlj, 100.days.ago) + legacy_appeal = create_legacy_appeal(priority=true, assigned_avlj, 90.days.ago) + make_legacy_appeal_not_ready_for_distribution(legacy_appeal)#most recent + end + + def create_four_alternating_priority_by_age_appeals_tied_to_a_ssc_avlj + # A SSC AVLJ that has 4 appeals for which they held the last hearing. + # These cases should NOT be returned to the board + ssc_avlj = User.find_by(css_id: "SSCA11") + create_legacy_appeal(priority=true, ssc_avlj, 325.days.ago) + create_legacy_appeal(priority=false, ssc_avlj, 275.days.ago) + create_legacy_appeal(priority=true, ssc_avlj, 175.days.ago) + create_legacy_appeal(priority=false, ssc_avlj, 75.days.ago) + end + + def create_four_alternating_priority_by_age_appeals_tied_to_a_non_ssc_avlj_with_a_second_hearing_held_by_a_ssc_avlj + # A non-SSC AVLJ that has 4 appeals where the non-SSC AVLJ held a hearing first, but a second hearing was held by an SSC AVLJ. + # These cases should NOT be returned to the board + avlj = User.find_by(css_id: "NONSSCAN12") + ssc_avlj = User.find_by(css_id: "SSCAVLJLGC") + legacy_appeal = create_legacy_appeal(priority=true, avlj, 90.days.ago) + create_second_hearing_for_legacy_appeal(legacy_appeal, 90.days.ago, ssc_avlj) + + legacy_appeal = create_legacy_appeal(priority=false, avlj, 60.days.ago) + create_second_hearing_for_legacy_appeal(legacy_appeal, 30.days.ago, ssc_avlj) + + legacy_appeal = create_legacy_appeal(priority=true, avlj, 30.days.ago) + create_second_hearing_for_legacy_appeal(legacy_appeal, 15.days.ago, ssc_avlj) + + legacy_appeal = create_legacy_appeal(priority=false, avlj, 15.days.ago) + create_second_hearing_for_legacy_appeal(legacy_appeal, 5.days.ago, ssc_avlj) + end + + def create_four_alternating_priority_by_age_appeals_tied_to_a_non_ssc_avlj_with_a_second_hearing_held_by_another_non_ssc_avlj + # A non-SSC AVLJ that has 4 appeals where the non-SSC AVLJ held a hearing first, but a second hearing was held by different non-SSC AVLJ. + avlj = User.find_by(css_id: "NONSSCAN13") + avlj2 = User.find_by(css_id: "AVLJLGC2") + legacy_appeal = create_legacy_appeal(priority=true, avlj, 95.days.ago) + create_second_hearing_for_legacy_appeal(legacy_appeal, 65.days.ago, avlj2) + + legacy_appeal = create_legacy_appeal(priority=false, avlj, 65.days.ago) + create_second_hearing_for_legacy_appeal(legacy_appeal, 35.days.ago, avlj2) + + legacy_appeal = create_legacy_appeal(priority=true, avlj, 35.days.ago) + create_second_hearing_for_legacy_appeal(legacy_appeal, 25.days.ago, avlj2) + + legacy_appeal = create_legacy_appeal(priority=false, avlj, 20.days.ago) + create_second_hearing_for_legacy_appeal(legacy_appeal, 10.days.ago, avlj2) + end + + def create_unsigned_priority_appeal_tied_to_inactive_non_ssc_avlj + inactive_avlj = User.find_by(css_id: "NONSSCAN14") + docket_date = Date.new(1999, 1, 1) + create_legacy_appeal(priority=true, inactive_avlj, docket_date) + end + + def create_signed_non_priority_appeal_tied_to_inactive_non_ssc_avlj + inactive_avlj = User.find_by(css_id: "NONSSCAN14") + docket_date = Date.new(1999, 1, 2) + create_signed_legacy_appeal(priority=false, inactive_avlj, inactive_avlj, docket_date) + end + + def create_unsigned_priority_ama_appeal_tied_to_non_ssc_avlj + non_ssc_avlj = User.find_by(css_id: "NONSSCAN01") + docket_date = Date.new(2020, 1, 3) + create_ama_appeal(priority=true, non_ssc_avlj, docket_date) + end + + def create_signed_non_priority_ama_appeal_tied_to_non_ssc_avlj + non_ssc_avlj = User.find_by(css_id: "NONSSCAN01") + docket_date = Date.new(2020, 1, 4) + create_signed_ama_appeal(priority=false, non_ssc_avlj, non_ssc_avlj, docket_date) + end + + def create_signed_priority_appeal_tied_to_vlj + vlj = User.find_by(css_id: "REGVLJ01") + docket_date = Date.new(1999, 1, 5) + create_signed_legacy_appeal(priority=true, vlj, vlj, docket_date) + end + + def create_unsigned_non_priority_appeal_tied_to_vlj + vlj = User.find_by(css_id: "REGVLJ02") + docket_date = Date.new(1999, 1, 6) + create_legacy_appeal(priority=false, vlj, docket_date) + end + + def create_non_ssc_avlj(css_id, full_name) + User.find_by_css_id(css_id) || + create(:user, :non_ssc_avlj_user, css_id: css_id, full_name: full_name) + end + + def create_ssc_avlj(css_id, full_name) + User.find_by_css_id(css_id) || + create(:user, :ssc_avlj_user, css_id: css_id, full_name: full_name) + end + + def create_inactive_non_ssc_avlj(css_id, full_name) + # same as a regular non_ssc_avlj except their sactive = 'I' instead of 'A' + User.find_by_css_id(css_id) || + create(:user, :inactive_non_ssc_avlj_user, css_id: css_id, full_name: full_name) + end + + def create_vlj(css_id, full_name) + # same as a ssc_avlj except thier svlj = 'J' instead of 'A' + User.find_by_css_id(css_id) || + create(:user, :vlj_user, css_id: css_id, full_name: full_name) + end + + def demo_regional_office + 'RO17' + end + + def create_signed_legacy_appeal(priority, signing_avlj, assigned_avlj, docket_date) + Timecop.travel(docket_date) do + traits = priority ? [:type_cavc_remand] : [:type_original] + create(:legacy_signed_appeal, *traits, signing_avlj: signing_avlj, assigned_avlj: assigned_avlj) + end + end + + def create_legacy_appeal(priority, avlj, docket_date) + Timecop.travel(docket_date) + veteran = create_demo_veteran_for_legacy_appeal + + correspondent = create(:correspondent, + snamef: veteran.first_name, snamel: veteran.last_name, + ssalut: "", ssn: veteran.file_number) + + + vacols_case = priority ? create_priority_video_vacols_case(veteran, + correspondent, + avlj, + docket_date) : + create_non_priority_video_vacols_case(veteran, + correspondent, + avlj, + docket_date) + + legacy_appeal = create( + :legacy_appeal, + :with_root_task, + vacols_case: vacols_case, + closest_regional_office: demo_regional_office + ) + + create(:available_hearing_locations, demo_regional_office, appeal: legacy_appeal) + Timecop.return + + legacy_appeal + end + + def create_ama_appeal(priority, avlj, docket_date) + Timecop.travel(docket_date) + priority ? create( + :appeal, + :hearing_docket, + :with_post_intake_tasks, + :advanced_on_docket_due_to_age, + :held_hearing_and_ready_to_distribute, + :tied_to_judge, + veteran: create_demo_veteran_for_legacy_appeal, + receipt_date: docket_date, + tied_judge: avlj, + adding_user: avlj + ) : create( + :appeal, + :hearing_docket, + :with_post_intake_tasks, + :held_hearing_and_ready_to_distribute, + :tied_to_judge, + veteran: create_demo_veteran_for_legacy_appeal, + receipt_date: docket_date, + tied_judge: avlj, + adding_user: avlj + ) + Timecop.return + end + + def create_signed_ama_appeal(priority, avlj, signing_avlj, docket_date) + + # Go back to when we want the original appeal to have been decided + Timecop.travel(docket_date) + + source = create(:appeal, :dispatched, :hearing_docket, associated_judge: avlj) + remand = create(:cavc_remand, source_appeal: source).remand_appeal + remand.tasks.where(type: SendCavcRemandProcessedLetterTask.name).map(&:completed!) + create(:appeal_affinity, appeal: remand) + + jat = JudgeAssignTaskCreator.new(appeal: remand, judge: avlj, assigned_by_id: avlj.id).call + create(:colocated_task, :schedule_hearing, parent: jat, assigned_by: avlj).completed! + + create(:hearing, :held, appeal: remand, judge: avlj, adding_user: User.system_user) + remand.tasks.where(type: AssignHearingDispositionTask.name).flat_map(&:children).map(&:completed!) + remand.appeal_affinity.update!(affinity_start_date: Time.zone.now) + + remand + Timecop.return + end + + + def create_priority_video_vacols_case(veteran, correspondent, associated_judge, days_ago) + create( + :case, + :aod, + :tied_to_judge, + :video_hearing_requested, + :type_original, + :ready_for_distribution, + tied_judge: associated_judge, + correspondent: correspondent, + bfcorlid: "#{veteran.file_number}S", + case_issues: create_list(:case_issue, 3, :compensation), + bfd19: days_ago + ) + end + + def create_non_priority_video_vacols_case(veteran, correspondent, associated_judge, days_ago) + create( + :case, + :tied_to_judge, + :video_hearing_requested, + :type_original, + :ready_for_distribution, + tied_judge: associated_judge, + correspondent: correspondent, + bfcorlid: "#{veteran.file_number}S", + case_issues: create_list(:case_issue, 3, :compensation), + bfd19: days_ago + ) + end + + def random_demo_file_number_and_participant_id + random_file_number = Random.rand(100_000_000...989_999_999) + random_participant_id = random_file_number + 100000 + + while find_demo_veteran(random_file_number) + random_file_number += 2000 + random_participant_id += 2000 + end + + return random_file_number, random_participant_id + end + + def find_demo_veteran(file_number) + Veteran.find_by(file_number: format("%09d", n: file_number + 1)) + end + + def create_demo_veteran(options = {}) + params = { + file_number: format("%09d", n: options[:file_number]), + participant_id: format("%09d", n: options[:participant_id]) + } + + Veteran.find_by_participant_id(params[:participant_id]) || create(:veteran, params.merge(options)) + end + + def create_demo_veteran_for_legacy_appeal + file_number, participant_id = random_demo_file_number_and_participant_id + create_demo_veteran( + file_number: file_number, + participant_id: participant_id + ) + end + + def create_second_hearing_for_legacy_appeal(legacy_appeal, docket_date, avlj) + case_hearing = create( + :case_hearing, + :disposition_held, + folder_nr: legacy_appeal.vacols_id, + hearing_date: docket_date.to_date, + user: avlj + ) + + create(:legacy_hearing, appeal: legacy_appeal, case_hearing: case_hearing) + end + + def make_legacy_appeal_not_ready_for_distribution(legacy_appeal) + Rails.logger.info("~~~Marking legacy appeal for Veteran ID: #{legacy_appeal.vbms_id} as Not Ready To Distribute~~~") + VACOLS::Case.find(legacy_appeal.vacols_id).update!(bfcurloc: "01") + end + end +end diff --git a/spec/controllers/case_distribution_levers_controller_spec.rb b/spec/controllers/case_distribution_levers_controller_spec.rb index 9ab8fc26579..e4faa69a6e4 100644 --- a/spec/controllers/case_distribution_levers_controller_spec.rb +++ b/spec/controllers/case_distribution_levers_controller_spec.rb @@ -133,7 +133,7 @@ end it "renders a page with the grouped levers and lever history" do - lever_keys = %w[static batch affinity docket_distribution_prior docket_time_goal docket_levers] + lever_keys = %w[static batch affinity docket_distribution_prior docket_time_goal docket_levers internal] User.authenticate!(user: lever_user) OrganizationsUser.make_user_admin(lever_user, CDAControlGroup.singleton) get "levers" diff --git a/spec/factories/case_distribution_lever.rb b/spec/factories/case_distribution_lever.rb index 30789851b51..442dc8b23c9 100644 --- a/spec/factories/case_distribution_lever.rb +++ b/spec/factories/case_distribution_lever.rb @@ -500,5 +500,16 @@ lever_group_order { 103 } control_group { "priority" } end + + trait :nonsscavlj_number_of_appeals_to_move do + item { "nonsscavlj_number_of_appeals_to_move" } + title { "Non-SSC/AVLJ Number of Appeals to Move" } + data_type { "number" } + value { 2 } + unit { "" } + algorithms_used { [] } + lever_group { "internal" } + lever_group_order { 999 } + end end end diff --git a/spec/factories/returned_appeal_job.rb b/spec/factories/returned_appeal_job.rb new file mode 100644 index 00000000000..ac17c3f28d8 --- /dev/null +++ b/spec/factories/returned_appeal_job.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :returned_appeal_job do + started_at { Time.zone.now } + completed_at { Time.zone.now + 1.hour } + errored_at { nil } + stats { { success: true, message: "Job completed successfully" }.to_json } + returned_appeals { [] } + end +end diff --git a/spec/factories/user.rb b/spec/factories/user.rb index bfb5ebe7d85..27263655b0e 100644 --- a/spec/factories/user.rb +++ b/spec/factories/user.rb @@ -181,6 +181,30 @@ end end + trait :non_ssc_avlj_user do + after(:create) do |user| + create(:staff, :non_ssc_avlj, user: user) + end + end + + trait :ssc_avlj_user do + after(:create) do |user| + create(:staff, :ssc_avlj, user: user) + end + end + + trait :vlj_user do + after(:create) do |user| + create(:staff, :vlj, user: user) + end + end + + trait :inactive_non_ssc_avlj_user do + after(:create) do |user| + create(:staff, :inactive_non_ssc_avlj, user: user) + end + end + after(:create) do |user, evaluator| if evaluator.vacols_uniq_id create(:staff, slogid: evaluator.vacols_uniq_id, user: user) diff --git a/spec/factories/vacols/case.rb b/spec/factories/vacols/case.rb index e26209dca6b..1f06c8e1b87 100644 --- a/spec/factories/vacols/case.rb +++ b/spec/factories/vacols/case.rb @@ -197,6 +197,114 @@ end end + # The judge and attorney should be the VACOLS::Staff records of those users + # This factory uses the :aod trait to mark it AOD instead of a transient attribute + # Pass `tied_to: false` to create an original appeal without a previous hearing + factory :legacy_signed_appeal do + transient do + judge { nil } + signing_avlj { nil } + assigned_avlj { nil } + attorney { nil } + cavc { false } + appeal_affinity { true } + affinity_start_date { 2.months.ago } + tied_to { true } + end + + status_active + + bfdpdcn { 1.month.ago } + bfcurloc { "81" } + + after(:create) do |new_case, evaluator| + signing_judge = + if evaluator.signing_avlj.present? + VACOLS::Staff.find_by_sdomainid(evaluator.signing_avlj.css_id) + else + evaluator.judge || create(:user, :judge, :with_vacols_judge_record).vacols_staff + end + + hearing_judge = + if evaluator.assigned_avlj.present? + VACOLS::Staff.find_by_sdomainid(evaluator.assigned_avlj.css_id) + else + evaluator.judge || create(:user, :judge, :with_vacols_judge_record).vacols_staff + end + + signing_sattyid = signing_judge.sattyid + + original_attorney = evaluator.attorney || create(:user, :with_vacols_attorney_record).vacols_staff + + new_case.correspondent.update!(ssn: new_case.bfcorlid.chomp("S")) unless new_case.correspondent.ssn + + veteran = Veteran.find_by_file_number_or_ssn(new_case.correspondent.ssn) + + if veteran + new_case.correspondent.update!(snamef: veteran.first_name, snamel: veteran.last_name) + else + create( + :veteran, + first_name: new_case.correspondent.snamef, + last_name: new_case.correspondent.snamel, + name_suffix: new_case.correspondent.ssalut, + ssn: new_case.correspondent.ssn, + file_number: new_case.correspondent.ssn + ) + end + + # Build these instead of create so the folder after_create hooks don't execute and create another case + # until the original case has been created and the associations saved + original_folder = build( + :folder, + new_case.folder.attributes.except!("ticknum", "tidrecv", "tidcls", "tiaduser", + "tiadtime", "tikeywrd", "tiread2", "tioctime", "tiocuser", + "tidktime", "tidkuser") + ) + + original_issues = new_case.case_issues.map do |issue| + build( + :case_issue, + issue.attributes.except("isskey", "issaduser", "issadtime", "issmduser", "issmdtime", "issdcls"), + issdc: "3" + ) + end + + original_case = create( + :case, + :status_complete, + :disposition_remanded, + bfac: evaluator.cavc ? "7" : "1", + bfcorkey: new_case.bfcorkey, + bfcorlid: new_case.bfcorlid, + bfdnod: new_case.bfdnod, + bfdsoc: new_case.bfdsoc, + bfd19: new_case.bfd19, + bfcurloc: "99", + bfddec: new_case.bfdpdcn, + bfmemid: signing_sattyid, + bfattid: original_attorney.sattyid, + folder: original_folder, + correspondent: new_case.correspondent, + case_issues: original_issues + ) + + if evaluator.tied_to + create( + :case_hearing, + :disposition_held, + folder_nr: original_case.bfkey, + hearing_date: original_case.bfddec - 1.month, + user: User.find_by_css_id(hearing_judge&.sdomainid) + ) + end + + if evaluator.appeal_affinity + create(:appeal_affinity, appeal: new_case, affinity_start_date: evaluator.affinity_start_date) + end + end + end + # You can change the judge, attorney, AOD status, and Appeal Affinity of your Legacy CAVC Appeal. # The Appeal_Affinity is default but the AOD must be toggled on. Example: # "FactoryBot.create(:legacy_cavc_appeal, judge: judge, aod: true, affinity_start_date: 2.weeks.ago)" diff --git a/spec/factories/vacols/staff.rb b/spec/factories/vacols/staff.rb index 77519c47a42..b97ad00b73c 100644 --- a/spec/factories/vacols/staff.rb +++ b/spec/factories/vacols/staff.rb @@ -12,6 +12,16 @@ new_sattyid end + + judge do + judge_staff = VACOLS::Staff.find_by(slogid: "STAFF_FCT_JUDGE") || + create(:staff, :judge_role, slogid: "STAFF_FCT_JUDGE") + judge_staff + end + + generated_smemgrp_not_equal_to_sattyid do + judge.sattyid + end end sequence(:stafkey) do |n| @@ -117,6 +127,31 @@ sattyid { generated_sattyid } end + trait :non_ssc_avlj do + svlj { "A" } + sattyid { generated_sattyid } + smemgrp { generated_smemgrp_not_equal_to_sattyid } + end + + trait :inactive_non_ssc_avlj do + svlj { "A" } + sactive { "I" } + sattyid { generated_sattyid } + smemgrp { generated_smemgrp_not_equal_to_sattyid } + end + + trait :ssc_avlj do + svlj { "A" } + sattyid { generated_sattyid } + smemgrp { sattyid } + end + + trait :vlj do + svlj { "J" } + sattyid { generated_sattyid } + smemgrp { sattyid } + end + after(:build) do |staff, evaluator| if evaluator.user&.full_name staff.snamef = evaluator.user.full_name.split(" ").first diff --git a/spec/jobs/push_priority_appeals_to_judges_job_spec.rb b/spec/jobs/push_priority_appeals_to_judges_job_spec.rb index 4ff0b7d9df8..3c2602cb8a8 100644 --- a/spec/jobs/push_priority_appeals_to_judges_job_spec.rb +++ b/spec/jobs/push_priority_appeals_to_judges_job_spec.rb @@ -14,6 +14,7 @@ create(:case_distribution_lever, :ama_hearing_case_aod_affinity_days) create(:case_distribution_lever, :ama_direct_review_start_distribution_prior_to_goals) create(:case_distribution_lever, :disable_legacy_non_priority) + create(:case_distribution_lever, :nonsscavlj_number_of_appeals_to_move) end def to_judge_hash(arr) @@ -24,8 +25,6 @@ def to_judge_hash(arr) before do expect_any_instance_of(PushPriorityAppealsToJudgesJob) .to receive(:distribute_genpop_priority_appeals).and_return([]) - expect_any_instance_of(PushPriorityAppealsToJudgesJob) - .to receive(:generate_report).and_return([]) end after { FeatureToggle.disable!(:acd_distribute_by_docket_date) } @@ -52,6 +51,13 @@ def to_judge_hash(arr) subject end + + it "calls send_job_report method" do + expect_any_instance_of(PushPriorityAppealsToJudgesJob) + .to receive(:generate_report).and_return([]) + + subject + end end context ".distribute_non_genpop_priority_appeals" do @@ -220,29 +226,6 @@ def to_judge_hash(arr) subject { PushPriorityAppealsToJudgesJob.new.distribute_non_genpop_priority_appeals } - context "using Automatic Case Distribution module" do - before do - create(:case_distribution_lever, :disable_legacy_priority) - allow_any_instance_of(PushPriorityAppealsToJudgesJob).to receive(:eligible_judges).and_return(eligible_judges) - end - - xit "should only distribute the ready priority cases tied to a judge" do - expect(subject.count).to eq eligible_judges.count - expect(subject.map { |dist| dist.statistics["batch_size"] }).to match_array [2, 2, 0, 0] - - # Ensure we only distributed the 2 ready legacy and hearing priority cases that are tied to a judge - distributed_cases = DistributedCase.where(distribution: subject) - expect(distributed_cases.count).to eq 4 - expected_array = [ready_priority_bfkey, ready_priority_bfkey2, ready_priority_uuid, ready_priority_uuid2] - expect(distributed_cases.map(&:case_id)).to match_array expected_array - # Ensure all docket types cases are distributed, including the 5 cavc evidence submission cases - expected_array2 = [Constants.AMA_DOCKETS.hearing, Constants.AMA_DOCKETS.hearing, "legacy", "legacy"] - expect(distributed_cases.map(&:docket)).to match_array expected_array2 - expect(distributed_cases.map(&:priority).uniq).to match_array [true] - expect(distributed_cases.map(&:genpop).uniq).to match_array [false] - end - end - context "using By Docket Date Distribution module" do before do FeatureToggle.enable!(:acd_distribute_by_docket_date) @@ -253,72 +236,6 @@ def to_judge_hash(arr) FeatureToggle.disable!(:acd_distribute_by_docket_date) FeatureToggle.disable!(:acd_exclude_from_affinity) end - context "without using Docket Levers" do - before do - create(:case_distribution_lever, :disable_legacy_priority, value: "false") - end - - xit "should only distribute the ready priority cases tied to a judge" do - expect(subject.count).to eq eligible_judges.count - expect(subject.map { |dist| dist.statistics["batch_size"] }).to match_array [2, 2, 0, 0] - - # Ensure we only distributed the 2 ready legacy and hearing priority cases that are tied to a judge - distributed_cases = DistributedCase.where(distribution: subject) - expect(distributed_cases.count).to eq 4 - expected_array = [ready_priority_bfkey, ready_priority_bfkey2, ready_priority_uuid, ready_priority_uuid2] - expect(distributed_cases.map(&:case_id)).to match_array expected_array - # Ensure all docket types cases are distributed, including the 5 cavc evidence submission cases - expected_array2 = %w[hearing hearing legacy legacy] - expect(distributed_cases.map(&:docket)).to match_array expected_array2 - expect(distributed_cases.map(&:priority).uniq).to match_array [true] - expect(distributed_cases.map(&:genpop).uniq).to match_array [false, true] - end - end - - context "using Excluding Appeals by Docket Type and Priority from Automatic Case Distribution levers" do - context "all Exluding levers turned to Include" do - before do - create(:case_distribution_lever, :disable_legacy_priority, value: "false") - create(:case_distribution_lever, :disable_ama_priority_hearing, value: "false") - create(:case_distribution_lever, :disable_ama_priority_direct_review, value: "false") - create(:case_distribution_lever, :disable_ama_priority_evidence_submission, value: "false") - end - - xit "should distribute the ready priority cases" do - expect(subject.count).to eq eligible_judges.count - expect(subject.map { |dist| dist.statistics["batch_size"] }).to match_array [2, 2, 0, 0] - - distributed_cases = DistributedCase.where(distribution: subject) - expect(distributed_cases.count).to eq 4 - expected_array = [ready_priority_bfkey, ready_priority_bfkey2, ready_priority_uuid, ready_priority_uuid2] - expect(distributed_cases.map(&:case_id)).to match_array expected_array - # Ensure all docket types cases are distributed, including the 5 cavc evidence submission cases - expected_array2 = %w[hearing hearing legacy legacy] - expect(distributed_cases.map(&:docket)).to match_array expected_array2 - expect(distributed_cases.map(&:priority).uniq).to match_array [true] - expect(distributed_cases.map(&:genpop).uniq).to match_array [false, true] - end - end - - context "all Exluding levers turned to Exclude" do - before do - create(:case_distribution_lever, :disable_legacy_priority, value: "true") - create(:case_distribution_lever, :disable_ama_priority_hearing, value: "true") - create(:case_distribution_lever, :disable_ama_priority_direct_review, value: "true") - create(:case_distribution_lever, :disable_ama_priority_evidence_submission, value: "true") - end - - xit "should not distribute the ready priority cases" do - expect(subject.count).to eq eligible_judges.count - expect(subject.map { |dist| dist.statistics["batch_size"] }).to match_array [0, 0, 0, 0] - - distributed_cases = DistributedCase.where(distribution: subject) - expect(distributed_cases.count).to eq 0 - expected_array = [] - expect(distributed_cases).to match_array expected_array - end - end - end end end @@ -627,7 +544,6 @@ def to_judge_hash(arr) it "using By Docket Date Distribution module" do FeatureToggle.enable!(:acd_distribute_by_docket_date) - today = Time.zone.now.to_date legacy_days_waiting = (today - legacy_priority_case.bfd19.to_date).to_i direct_review_days_waiting = (today - ready_priority_direct_case.receipt_date).to_i diff --git a/spec/jobs/return_legacy_appeals_to_board_job_spec.rb b/spec/jobs/return_legacy_appeals_to_board_job_spec.rb new file mode 100644 index 00000000000..7c82e696fe9 --- /dev/null +++ b/spec/jobs/return_legacy_appeals_to_board_job_spec.rb @@ -0,0 +1,620 @@ +# frozen_string_literal: true + +describe ReturnLegacyAppealsToBoardJob, :all_dbs do + describe "#perform" do + let(:job) { described_class.new } + let(:returned_appeal_job) { instance_double("ReturnedAppealJob", id: 1) } + let(:appeals) { [{ "bfkey" => "1", "priority" => 1 }, { "bfkey" => "2", "priority" => 0 }] } + let(:moved_appeals) { [{ "bfkey" => "1", "priority" => 1 }] } + + before do + allow(CaseDistributionLever).to receive(:nonsscavlj_number_of_appeals_to_move).and_return(2) + + allow(job).to receive(:create_returned_appeal_job).and_return(returned_appeal_job) + allow(returned_appeal_job).to receive(:update!) + allow(job).to receive(:eligible_and_moved_appeals).and_return([appeals, moved_appeals]) + allow(job).to receive(:filter_appeals).and_return({}) + allow(job).to receive(:send_job_slack_report) + allow(job).to receive(:complete_returned_appeal_job) + allow(job).to receive(:metrics_service_report_runtime) + end + + context "when the job completes successfully" do + it "creates a ReturnedAppealJob instance, processes appeals, and sends a report" do + allow(job).to receive(:slack_report).and_return(["Job completed successfully"]) + + job.perform + + expect(job).to have_received(:create_returned_appeal_job).once + expect(job).to have_received(:eligible_and_moved_appeals).once + expect(job).to have_received(:complete_returned_appeal_job) + .with(returned_appeal_job, "Job completed successfully", moved_appeals).once + expect(job).to have_received(:send_job_slack_report).with(["Job completed successfully"]).once + expect(job).to have_received(:metrics_service_report_runtime) + .with(metric_group_name: "return_legacy_appeals_to_board_job").once + end + end + + context "when no appeals are moved" do + before do + allow(job).to receive(:eligible_and_moved_appeals).and_return([appeals, nil]) + allow(job).to receive(:complete_returned_appeal_job) + allow(job).to receive(:send_job_slack_report) + end + + it "sends a no records moved Slack report and completes the job" do + job.perform + + # expect(job).to have_received(:send_job_slack_report).with(described_class::NO_RECORDS_FOUND_MESSAGE).once + expect(job).to have_received(:complete_returned_appeal_job) + .with(returned_appeal_job, Constants.DISTRIBUTION.no_records_moved_message, []).once + expect(job).to have_received(:send_job_slack_report).with(described_class::NO_RECORDS_FOUND_MESSAGE).once + expect(job).to have_received(:metrics_service_report_runtime).once + end + end + + context "when an error occurs" do + let(:error_message) { "Unexpected error" } + let(:slack_service_instance) { instance_double(SlackService) } + + before do + allow(job).to receive(:eligible_and_moved_appeals).and_raise(StandardError, error_message) + allow(job).to receive(:log_error) + allow(returned_appeal_job).to receive(:update!) + allow(SlackService).to receive(:new).and_return(slack_service_instance) + allow(slack_service_instance).to receive(:send_notification) + end + + it "handles the error, logs it, and sends a Slack notification" do + job.perform + + expect(job).to have_received(:log_error).with(instance_of(StandardError)) + expect(returned_appeal_job).to have_received(:update!) + .with(hash_including(errored_at: kind_of(Time), + stats: "{\"message\":\"Job failed with error: #{error_message}\"}")).once + expect(slack_service_instance).to have_received(:send_notification).with( + a_string_matching(/\n \[ERROR\]/), job.class.name + ).once + expect(job).to have_received(:metrics_service_report_runtime).once + end + end + end + + describe "#non_ssc_avljs" do + let(:job) { described_class.new } + + context "2 non ssc avljs exist" do + let!(:non_ssc_avlj_user_1) { create(:user, :non_ssc_avlj_user) } + let!(:non_ssc_avlj_user_2) { create(:user, :non_ssc_avlj_user) } + let!(:ssc_avlj_user) { create(:user, :ssc_avlj_user) } + + it "returns both non ssc avljs" do + expect(job.send(:non_ssc_avljs)).to eq([non_ssc_avlj_user_1.vacols_staff, non_ssc_avlj_user_2.vacols_staff]) + end + end + + context "1 each of non ssc avlj, ssc avlj, regular vlj, inactive non ssc avlj exist" do + let!(:non_ssc_avlj_user) { create(:user, :non_ssc_avlj_user) } + let!(:inactive_non_ssc_avlj_user) { create(:user, :inactive, :non_ssc_avlj_user) } + let!(:ssc_avlj_user) { create(:user, :ssc_avlj_user) } + let!(:user) { create(:user, :with_vacols_record) } + + before do + inactive_non_ssc_avlj_user.vacols_staff.update!(sactive: "I") + end + + it "returns only the non ssc avlj" do + expect(job.send(:non_ssc_avljs)).to eq([non_ssc_avlj_user.vacols_staff]) + end + end + + context "no non ssc avljs exist" do + let!(:ssc_avlj_user) { create(:user, :ssc_avlj_user) } + + it "returns an empty array" do + expect(job.send(:non_ssc_avljs)).to eq([]) + end + end + end + + describe "#calculate_remaining_appeals" do + let(:job) { described_class.new } + let(:p1) { { "bfkey" => "1", "priority" => 1 } } + let(:p2) { { "bfkey" => "2", "priority" => 1 } } + let(:np1) { { "bfkey" => "3", "priority" => 0 } } + let(:np2) { { "bfkey" => "4", "priority" => 0 } } + + before do + allow(CaseDistributionLever).to receive(:nonsscavlj_number_of_appeals_to_move).and_return(2) + end + + context "2 priority and 2 non-priority legacy appeals tied to non-ssc avljs exist" do + let(:appeals) { [p1, p2, np1, np2] } + let(:p_appeals_moved) { [p1] } + let(:np_appeals_moved) { [np1] } + + it "should return the unmoved legacy appeals" do + returned_reamining_appeals = job.send(:calculate_remaining_appeals, appeals, p_appeals_moved, np_appeals_moved) + expect(returned_reamining_appeals).to eq([[p2], [np2]]) + end + end + + context "2 priority legacy appeals tied to non-ssc avljs exist" do + let(:appeals) { [p1, p2] } + let(:p_appeals_moved) { [p1] } + let(:np_appeals_moved) { [] } + + it "should return the unmoved legacy priority appeal and an empty array of non-priority appeals" do + returned_reamining_appeals = job.send(:calculate_remaining_appeals, appeals, p_appeals_moved, np_appeals_moved) + expect(returned_reamining_appeals).to eq([[p2], []]) + end + end + + context "2 non-priority legacy appeals tied to non-ssc avljsexist" do + let(:appeals) { [np1, np2] } + let(:p_appeals_moved) { [] } + let(:np_appeals_moved) { [np1] } + + it "should return the unmoved legacy non-priority appeal and an empty array of priority appeals" do + returned_reamining_appeals = job.send(:calculate_remaining_appeals, appeals, p_appeals_moved, np_appeals_moved) + expect(returned_reamining_appeals).to eq([[], [np2]]) + end + end + + context "all appeals are moved" do + let(:appeals) { [p1, p2, np1, np2] } + let(:p_appeals_moved) { [p1, p2] } + let(:np_appeals_moved) { [np1, np2] } + + it "should return 2 empty arrays" do + returned_reamining_appeals = job.send(:calculate_remaining_appeals, appeals, p_appeals_moved, np_appeals_moved) + expect(returned_reamining_appeals).to eq([[], []]) + end + end + + context "no legacy appeals tied to non-ssc avljs exist" do + let(:appeals) { [] } + let(:p_appeals_moved) { [] } + let(:np_appeals_moved) { [] } + + it "returns an empty array" do + returned_reamining_appeals = job.send(:calculate_remaining_appeals, appeals, p_appeals_moved, np_appeals_moved) + expect(returned_reamining_appeals).to_not eq([]) + end + end + end + + describe "#filter_appeals" do + let(:job) { described_class.new } + let(:non_ssc_avlj1) { create_non_ssc_avlj("NONSSCAN1", "NonScc User1") } + let(:non_ssc_avlj2) { create_non_ssc_avlj("NONSSCAN2", "NonScc User2") } + let(:non_ssc_avlj1_sattyid) { non_ssc_avlj1.vacols_staff.sattyid } + let(:non_ssc_avlj2_sattyid) { non_ssc_avlj2.vacols_staff.sattyid } + + let(:p1) { { "bfkey" => "1", "priority" => 1, "vlj" => non_ssc_avlj1_sattyid } } + let(:p2) { { "bfkey" => "2", "priority" => 1, "vlj" => non_ssc_avlj2_sattyid } } + let(:np1) { { "bfkey" => "3", "priority" => 0, "vlj" => non_ssc_avlj2_sattyid } } + let(:np2) { { "bfkey" => "4", "priority" => 0, "vlj" => non_ssc_avlj1_sattyid } } + let(:appeals) { [p1, p2, np1, np2] } + + before do + allow(CaseDistributionLever).to receive(:nonsscavlj_number_of_appeals_to_move).and_return(2) + end + + context "a single appeal from each of 2 non ssc avljs gets moved" do + let(:moved_appeals) { [p1, np1] } + + it "returns hash object with correct attributes that match the expected values" do + returned_filtered_appeals_info = job.send(:filter_appeals, appeals, moved_appeals) + expected_returned_object = { + priority_appeals_count: 1, + non_priority_appeals_count: 1, + remaining_priority_appeals_count: 1, + remaining_non_priority_appeals_count: 1, + grouped_by_avlj: [non_ssc_avlj1.vacols_staff.sattyid, non_ssc_avlj2.vacols_staff.sattyid] + } + expect(returned_filtered_appeals_info).to eq(expected_returned_object) + end + end + + context "all appeals from each of 2 non ssc avljs gets moved" do + let(:moved_appeals) { [p1, p2, np1, np2] } + + it "returns hash object with correct attributes that match the expected values" do + returned_filtered_appeals_info = job.send(:filter_appeals, appeals, moved_appeals) + expected_returned_object = { + priority_appeals_count: 2, + non_priority_appeals_count: 2, + remaining_priority_appeals_count: 0, + remaining_non_priority_appeals_count: 0, + grouped_by_avlj: [non_ssc_avlj1.vacols_staff.sattyid, non_ssc_avlj2.vacols_staff.sattyid] + } + expect(returned_filtered_appeals_info).to eq(expected_returned_object) + end + end + + context "no appeals are moved" do + let(:moved_appeals) { [] } + + it "returns hash object with correct attributes that match the expected values" do + returned_filtered_appeals_info = job.send(:filter_appeals, appeals, moved_appeals) + expected_returned_object = { + priority_appeals_count: 0, + non_priority_appeals_count: 0, + remaining_priority_appeals_count: 2, + remaining_non_priority_appeals_count: 2, + grouped_by_avlj: [] + } + expect(returned_filtered_appeals_info).to eq(expected_returned_object) + end + end + + context "no appeals exist" do + let(:moved_appeals) { [] } + let(:appeals) { [] } + + it "returns hash object with correct attributes that match the expected values" do + returned_filtered_appeals_info = job.send(:filter_appeals, appeals, moved_appeals) + expected_returned_object = { + priority_appeals_count: 0, + non_priority_appeals_count: 0, + remaining_priority_appeals_count: 0, + remaining_non_priority_appeals_count: 0, + grouped_by_avlj: [] + } + expect(returned_filtered_appeals_info).to eq(expected_returned_object) + end + end + + context "an extra priority appeal is moved that wasn't in the original list of appeals" do + let(:extra_priority_appeal) { { "bfkey" => "5", "priority" => 1, "vlj" => non_ssc_avlj1_sattyid } } + let(:moved_appeals) { [p1, np1, extra_priority_appeal] } + + it "raises an ERROR" do + expected_msg = "An invalid priority appeal was detected in the list of moved appeals: "\ + "#{[extra_priority_appeal]}" + + expect { job.send(:filter_appeals, appeals, moved_appeals) }.to raise_error(StandardError, expected_msg) + end + end + + context "an extra non-priority appeal is moved that wasn't in the original list of appeals" do + let(:extra_non_priority_appeal) { { "bfkey" => "5", "priority" => 0, "vlj" => non_ssc_avlj1_sattyid } } + let(:moved_appeals) { [p1, np1, extra_non_priority_appeal] } + + it "raises an ERROR" do + expected_msg = "An invalid non-priority appeal was detected in the list of moved appeals: "\ + "#{[extra_non_priority_appeal]}" + + expect { job.send(:filter_appeals, appeals, moved_appeals) }.to raise_error(StandardError, expected_msg) + end + end + end + + describe "#create_returned_appeal_job" do + let(:job) { described_class.new } + + context "when called" do + it "creates a valid ReturnedAppealJob" do + allow(CaseDistributionLever).to receive(:nonsscavlj_number_of_appeals_to_move).and_return(2) + returned_appeal_job = job.send(:create_returned_appeal_job) + expect(returned_appeal_job.started_at).to be_within(1.second).of(Time.zone.now) + expect(returned_appeal_job.stats).to eq({ message: "Job started" }.to_json) + end + end + end + + describe "#send_job_slack_report" do + let(:job) { described_class.new } + let(:slack_service_instance) { instance_double(SlackService) } + + before do + allow(SlackService).to receive(:new).and_return(slack_service_instance) + allow(slack_service_instance).to receive(:send_notification) + end + + context "is passed a valid message array" do + let(:message) do + [ + "Job performed successfully", + "Total Priority Appeals Moved: 5", + "Total Non-Priority Appeals Moved: 3", + "Total Remaining Priority Appeals: 10", + "Total Remaining Non-Priority Appeals: 7", + "SATTYIDs of Non-SSC AVLJs Moved: AVJL1, AVJL" + ] + end + + it "sends the message successfully" do + expected_report = "Job performed successfully\n"\ + "Total Priority Appeals Moved: 5\n"\ + "Total Non-Priority Appeals Moved: 3\n"\ + "Total Remaining Priority Appeals: 10\n"\ + "Total Remaining Non-Priority Appeals: 7\n"\ + "SATTYIDs of Non-SSC AVLJs Moved: AVJL1, AVJL" + + job.send(:send_job_slack_report, message) + expect(slack_service_instance) + .to have_received(:send_notification) + .with(expected_report, "ReturnLegacyAppealsToBoardJob") + end + end + + context "is passed an empty array" do + let(:message) { [] } + it "sends a notification to Slack with the correct message" do + expected_msg = "Slack message cannot be empty or nil" + + expect { job.send(:send_job_slack_report, message) }.to raise_error(StandardError, expected_msg) + end + end + end + + describe "#move_qualifying_appeals" do + let(:job) { described_class.new } + let(:non_ssc_avlj1) { create_non_ssc_avlj("NONSSCAN1", "NonScc User1") } + let(:non_ssc_avlj2) { create_non_ssc_avlj("NONSSCAN2", "NonScc User2") } + let(:non_ssc_avlj1_sattyid) { non_ssc_avlj1.vacols_staff.sattyid } + let(:non_ssc_avlj2_sattyid) { non_ssc_avlj2.vacols_staff.sattyid } + + let(:s1_p_appeal1) { { "bfkey" => "1", "priority" => 1, "vlj" => non_ssc_avlj1_sattyid, "bfd19" => 2.days.ago } } + let(:s1_p_appeal2) { { "bfkey" => "2", "priority" => 1, "vlj" => non_ssc_avlj1_sattyid, "bfd19" => 2.days.ago } } + let(:s1_np_appeal1) { { "bfkey" => "3", "priority" => 0, "vlj" => non_ssc_avlj1_sattyid, "bfd19" => 10.days.ago } } + let(:s1_np_appeal2) { { "bfkey" => "4", "priority" => 0, "vlj" => non_ssc_avlj1_sattyid, "bfd19" => 10.days.ago } } + + let(:s2_p_appeal1) { { "bfkey" => "5", "priority" => 1, "vlj" => non_ssc_avlj2_sattyid, "bfd19" => 2.days.ago } } + let(:s2_p_appeal2) { { "bfkey" => "6", "priority" => 1, "vlj" => non_ssc_avlj2_sattyid, "bfd19" => 2.days.ago } } + let(:s2_np_appeal1) { { "bfkey" => "7", "priority" => 0, "vlj" => non_ssc_avlj2_sattyid, "bfd19" => 10.days.ago } } + let(:s2_np_appeal2) { { "bfkey" => "8", "priority" => 0, "vlj" => non_ssc_avlj2_sattyid, "bfd19" => 10.days.ago } } + + let(:staff1_p_appeals) { [s1_p_appeal1, s1_p_appeal2] } + let(:staff1_np_appeals) { [s1_np_appeal1, s1_np_appeal2] } + let(:staff2_p_appeals) { [s2_p_appeal1, s2_p_appeal2] } + let(:staff2_np_appeals) { [s2_np_appeal1, s2_np_appeal2] } + let(:appeals) do + [ + s1_p_appeal1, + s1_p_appeal2, + s1_np_appeal1, + s1_np_appeal2, + s2_p_appeal1, + s2_p_appeal2, + s2_np_appeal1, + s2_np_appeal2 + ] + end + + before do + allow(CaseDistributionLever).to receive(:nonsscavlj_number_of_appeals_to_move).and_return(2) + allow(VACOLS::Case).to receive(:batch_update_vacols_location) + end + + context "limit is set to 2 per non ssc avlj" do + it "moves the 2 priority appeals per non ssc avlj" do + expected_moved_appeals = [s1_p_appeal1, s1_p_appeal2, s2_p_appeal1, s2_p_appeal2] + expected_moved_appeal_bf_keys = expected_moved_appeals.map { |m_appeal| m_appeal["bfkey"] } + + moved_appeals = job.send(:move_qualifying_appeals, appeals) + + expect(moved_appeals).to match_array(expected_moved_appeals) + expect(VACOLS::Case).to have_received(:batch_update_vacols_location) + .with("63", match_array(expected_moved_appeal_bf_keys)) + end + end + + context "limit is set to 1 per non ssc avlj" do + before do + allow(CaseDistributionLever).to receive(:nonsscavlj_number_of_appeals_to_move).and_return(1) + end + + it "moves the oldest priority appeals per non ssc avlj" do + s1_p_appeal1.update("bfd19" => 15.days.ago) + s1_p_appeal2.update("bfd19" => 20.days.ago) + s2_p_appeal1.update("bfd19" => 80.days.ago) + s2_p_appeal2.update("bfd19" => 40.days.ago) + + expected_moved_appeals = [s1_p_appeal2, s2_p_appeal1] + expected_moved_appeal_bf_keys = expected_moved_appeals.map { |m_appeal| m_appeal["bfkey"] } + + moved_appeals = job.send(:move_qualifying_appeals, appeals) + expect(moved_appeals).to match_array(expected_moved_appeals) + expect(VACOLS::Case).to have_received(:batch_update_vacols_location) + .with("63", match_array(expected_moved_appeal_bf_keys)) + end + end + + context "limit is set to 10 per non ssc avlj" do + before do + allow(CaseDistributionLever).to receive(:nonsscavlj_number_of_appeals_to_move).and_return(10) + end + + it "moves all appeals" do + expected_moved_appeals = appeals + expected_moved_appeal_bf_keys = expected_moved_appeals.map { |m_appeal| m_appeal["bfkey"] } + + moved_appeals = job.send(:move_qualifying_appeals, appeals) + expect(moved_appeals).to match_array(expected_moved_appeals) + expect(VACOLS::Case).to have_received(:batch_update_vacols_location) + .with("63", match_array(expected_moved_appeal_bf_keys)) + end + end + + context "there are no non_ssc_avljs" do + before do + allow(CaseDistributionLever).to receive(:nonsscavlj_number_of_appeals_to_move).and_return(10) + allow(job).to receive(:non_ssc_avljs).and_return([]) + end + + it "returns and empty array and VACOLS::Case.batch_update_vacols_location does not run doesn't run" do + expected_moved_appeals = [] + + moved_appeals = job.send(:move_qualifying_appeals, appeals) + expect(moved_appeals).to match_array(expected_moved_appeals) + expect(VACOLS::Case).to_not have_received(:batch_update_vacols_location) + end + end + + context "there are no appeals" do + it "returns an empty array and VACOLS::Case.batch_update_vacols_location does not run doesn't run" do + expected_moved_appeals = [] + + moved_appeals = job.send(:move_qualifying_appeals, []) + expect(moved_appeals).to match_array(expected_moved_appeals) + expect(VACOLS::Case).to_not have_received(:batch_update_vacols_location) + end + end + + context "the lever is set with a value 0" do + before do + allow(CaseDistributionLever).to receive(:nonsscavlj_number_of_appeals_to_move).and_return(0) + end + + it "returns an empty array and VACOLS::Case.batch_update_vacols_location does not run doesn't run" do + expected_moved_appeals = [] + + moved_appeals = job.send(:move_qualifying_appeals, appeals) + expect(moved_appeals).to match_array(expected_moved_appeals) + expect(VACOLS::Case).to_not have_received(:batch_update_vacols_location) + end + end + + context "the lever is set with a value below 0" do + before do + allow(CaseDistributionLever).to receive(:nonsscavlj_number_of_appeals_to_move).and_return(-1) + end + + it "it raises an ERROR message and VACOLS::Case.batch_update_vacols_location does not run doesn't run" do + expected_msg = "CaseDistributionLever.nonsscavlj_number_of_appeals_to_move set below 0" + + expect { job.send(:move_qualifying_appeals, appeals) }.to raise_error(StandardError, expected_msg) + expect(VACOLS::Case).to_not have_received(:batch_update_vacols_location) + end + end + end + + describe "#get_tied_appeal_bfkeys" do + let(:job) { described_class.new } + let(:appeal_1) { { "priority" => 0, "bfd19" => 10.days.ago, "bfkey" => "1" } } + let(:appeal_2) { { "priority" => 1, "bfd19" => 8.days.ago, "bfkey" => "2" } } + let(:appeal_3) { { "priority" => 0, "bfd19" => 6.days.ago, "bfkey" => "3" } } + let(:appeal_4) { { "priority" => 1, "bfd19" => 4.days.ago, "bfkey" => "4" } } + + context "with a mix of priority and non-priority appeals" do + let(:tied_appeals) { [appeal_1, appeal_2, appeal_3, appeal_4] } + + it "returns the keys sorted by priority and then bfd19" do + allow(CaseDistributionLever).to receive(:nonsscavlj_number_of_appeals_to_move).and_return(2) + result = job.send(:get_tied_appeal_bfkeys, tied_appeals) + expect(result).to eq(%w[2 4 1 3]) + end + end + end + + describe "#update_qualifying_appeals_bfkeys" do + let(:job) { described_class.new } + let(:nonsscavlj_number_of_appeals_to_move_count) { 2 } + + before do + allow(CaseDistributionLever).to receive(:nonsscavlj_number_of_appeals_to_move) + .and_return(nonsscavlj_number_of_appeals_to_move_count) + end + + context "maximum moved appeals per non ssc avlj is 2 and a starting bfkey list of 2 and a tied list of 4 keys" do + let(:tied_appeals_bfkeys) { %w[3 4 5 6] } + let(:qualifying_appeals_bfkeys) { %w[1 2] } + + it "adds 2 keys to qualifying bfkey list" do + appeals = job.send(:update_qualifying_appeals_bfkeys, tied_appeals_bfkeys, qualifying_appeals_bfkeys) + + expect(appeals).to eq(%w[1 2 3 4]) + end + end + + context "maximum moved appeals per non ssc avlj is 4 and a starting bfkey list of 2 and a tied list of 4 keys" do + let(:nonsscavlj_number_of_appeals_to_move_count) { 4 } + let(:tied_appeals_bfkeys) { %w[3 4 5 6] } + let(:qualifying_appeals_bfkeys) { %w[1 2] } + + it "adds all tied keys to qualifying bfkey list" do + appeals = job.send(:update_qualifying_appeals_bfkeys, tied_appeals_bfkeys, qualifying_appeals_bfkeys) + + expect(appeals).to eq(%w[1 2 3 4 5 6]) + end + end + + context "maximum moved appeals per non ssc avlj is higher than the length of the tied list and a starting bfkey "\ + "list of 2 and a tied list of 4 keys" do + let(:nonsscavlj_number_of_appeals_to_move_count) { 10 } + let(:tied_appeals_bfkeys) { %w[3 4 5 6] } + let(:qualifying_appeals_bfkeys) { %w[1 2] } + + it "adds all tied keys to qualifying bfkey list" do + appeals = job.send(:update_qualifying_appeals_bfkeys, tied_appeals_bfkeys, qualifying_appeals_bfkeys) + + expect(appeals).to eq(%w[1 2 3 4 5 6]) + end + end + + context "maximum moved appeals per non ssc avlj is 2 and starting bfkey list is empty and a tied list of 4 keys" do + let(:tied_appeals_bfkeys) { %w[3 4 5 6] } + let(:qualifying_appeals_bfkeys) { [] } + + it "adds 2 tied keys to qualifying bfkey list" do + appeals = job.send(:update_qualifying_appeals_bfkeys, tied_appeals_bfkeys, qualifying_appeals_bfkeys) + + expect(appeals).to eq(%w[3 4]) + end + end + + context "maximum moved appeals per non ssc avlj is 2 and starting bfkey list of 2 keys and a tied list is empty" do + let(:tied_appeals_bfkeys) { [] } + let(:qualifying_appeals_bfkeys) { %w[1 2] } + + it "adds no tied keys to qualifying bfkey list" do + appeals = job.send(:update_qualifying_appeals_bfkeys, tied_appeals_bfkeys, qualifying_appeals_bfkeys) + + expect(appeals).to eq(%w[1 2]) + end + end + + context "maximum moved appeals per non ssc avlj is 2 and a starting bfkey list is empty and a tied list is empty" do + let(:tied_appeals_bfkeys) { [] } + let(:qualifying_appeals_bfkeys) { [] } + + it "adds no tied keys to qualifying bfkey list and list is empty" do + appeals = job.send(:update_qualifying_appeals_bfkeys, tied_appeals_bfkeys, qualifying_appeals_bfkeys) + + expect(appeals).to eq([]) + end + end + + context "lever is set to 0" do + let(:nonsscavlj_number_of_appeals_to_move_count) { 0 } + let(:tied_appeals_bfkeys) { %w[3 4 5 6] } + let(:qualifying_appeals_bfkeys) { %w[1 2] } + + it "returns an unchanged array" do + appeals = job.send(:update_qualifying_appeals_bfkeys, tied_appeals_bfkeys, qualifying_appeals_bfkeys) + + expect(appeals).to eq(%w[1 2]) + end + end + + context "lever is set to below 0" do + let(:nonsscavlj_number_of_appeals_to_move_count) { -1 } + let(:tied_appeals_bfkeys) { %w[3 4 5 6] } + let(:qualifying_appeals_bfkeys) { %w[1 2] } + let(:message) { "CaseDistributionLever.nonsscavlj_number_of_appeals_to_move set below 0" } + + it "raises an error saying the lever has been set incorrectly" do + expect { job.send(:update_qualifying_appeals_bfkeys, tied_appeals_bfkeys, qualifying_appeals_bfkeys) } + .to raise_error(StandardError, message) + end + end + end + + def create_non_ssc_avlj(css_id, full_name) + User.find_by_css_id(css_id) || + create(:user, :non_ssc_avlj_user, css_id: css_id, full_name: full_name) + end +end diff --git a/spec/models/case_distribution_lever_spec.rb b/spec/models/case_distribution_lever_spec.rb index d0f5850f7a9..2940f835bbd 100644 --- a/spec/models/case_distribution_lever_spec.rb +++ b/spec/models/case_distribution_lever_spec.rb @@ -17,7 +17,8 @@ ama_evidence_submission_docket_time_goals ama_hearing_docket_time_goals ama_hearing_start_distribution_prior_to_goals - ama_evidence_submission_start_distribution_prior_to_goals] + ama_evidence_submission_start_distribution_prior_to_goals + nonsscavlj_number_of_appeals_to_move] end let!(:float_levers) do %w[maximum_direct_review_proportion minimum_legacy_proportion nod_adjustment] diff --git a/spec/models/returned_appeal_job_spec.rb b/spec/models/returned_appeal_job_spec.rb new file mode 100644 index 00000000000..8c8e5989df8 --- /dev/null +++ b/spec/models/returned_appeal_job_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +RSpec.describe ReturnedAppealJob, :all_dbs do + describe "factory" do + it "is valid" do + expect(build(:returned_appeal_job)).to be_valid + end + end +end diff --git a/spec/queries/appeals_in_location_63_in_past_2_days_spec.rb b/spec/queries/appeals_in_location_63_in_past_2_days_spec.rb new file mode 100644 index 00000000000..f6f40916ac6 --- /dev/null +++ b/spec/queries/appeals_in_location_63_in_past_2_days_spec.rb @@ -0,0 +1,307 @@ +# frozen_string_literal: true + +require "./app/queries/appeals_in_location_63_in_past_2_days" + +describe AppealsInLocation63InPast2Days do + let(:job) { described_class } + let(:avlj_name) { "Avlj Judge" } + let(:avlj_fname) { "Avlj" } + let(:avlj_lname) { "Judge" } + + let(:non_ssc_avlj) do + User.find_by_css_id("NONSSCTEST") || + create(:user, :non_ssc_avlj_user, css_id: "NONSSCTEST", full_name: avlj_name) + end + + let(:prev_deciding_judge) do + User.find_by_css_id("PREDECJUDG") || + create(:user, :vlj_user, css_id: "PREDECJUDG", full_name: "Prevdec Judge") + end + + let(:appeal) do + { + "tinum" => "150000999988855", + "aod" => false, + "cavc" => false, + "bfd19" => "2023-01-05 00:00:00 UTC", + "bfdloout" => "2024-08-27 09:19:55 UTC", + "ssn" => "999559999", + "snamef" => "Bob", + "snamel" => "Goodman", + "vlj" => non_ssc_avlj.vacols_staff.sattyid, + "vlj_namef" => avlj_fname, + "vlj_namel" => avlj_lname, + "prev_deciding_judge" => prev_deciding_judge.vacols_staff.sattyid, + "bfkey" => "99", + "bfdlocin" => "2024-09-10 14:40:58 UTC", + "bfcurloc" => "63" + } + end + + context "#process and #tied_appeals" do + it "selects all appeals in location 63 and generates the CSV" do + allow(AppealRepository).to receive(:loc_63_appeals).and_return([appeal]) + expect { described_class.process }.not_to raise_error + expect(described_class.loc_63_appeals.size).to eq 1 + end + end + + context "Test the CSV generation" do + context "where it uses attributes " do + it "to create a hash Legacy rows moved to loc 63" do + subject_legacy = described_class.legacy_rows([appeal]).first + + expect(subject_legacy[:docket_number]).to eq appeal["tinum"] + expect(subject_legacy[:aod]).to eq appeal["aod"] + expect(subject_legacy[:cavc]).to be appeal["cavc"] + expect(subject_legacy[:receipt_date]).to eq appeal["bfd19"] + expect(subject_legacy[:ready_for_distribution_at]).to eq appeal["bfdloout"] + expect(subject_legacy[:veteran_file_number]).to eq appeal["ssn"] + expect(subject_legacy[:veteran_name]).to eq "Bob Goodman" + expect(subject_legacy[:hearing_judge_id]).to eq non_ssc_avlj.vacols_staff.sdomainid + expect(subject_legacy[:hearing_judge_name]).to eq avlj_name + expect(subject_legacy[:deciding_judge_id]).to eq prev_deciding_judge.vacols_staff.sdomainid + expect(subject_legacy[:deciding_judge_name]).to eq prev_deciding_judge.full_name + expect(subject_legacy[:affinity_start_date]).to eq nil + expect(subject_legacy[:moved_date_time]).to eq appeal["bfdlocin"] + expect(subject_legacy[:bfcurloc]).to eq appeal["bfcurloc"] + end + end + end + + describe ".loc_63_appeals" do + let(:non_ssc_avlj1) do + User.find_by_css_id("NONSSCTST1") || + create(:user, :non_ssc_avlj_user, css_id: "NONSSCTST1", full_name: "First AVLJ") + end + + let(:non_ssc_avlj2) do + User.find_by_css_id("NONSSCTST2") || + create(:user, :non_ssc_avlj_user, css_id: "NONSSCTST2", full_name: "Second AVLJ") + end + let(:veteran) { create(:veteran) } + + let(:correspondent) do + create( + :correspondent, + snamef: veteran.first_name, + snamel: veteran.last_name, + ssalut: "", ssn: veteran.file_number + ) + end + + let(:vacols_prio_case) do + create( + :case, + :aod, + :tied_to_judge, + :video_hearing_requested, + :type_original, + :ready_for_distribution, + tied_judge: non_ssc_avlj1, + correspondent: correspondent, + bfcorlid: "#{veteran.file_number}S", + case_issues: create_list(:case_issue, 3, :compensation), + bfd19: 60.days.ago + ) + end + + let(:vacols_non_prio_case) do + create( + :case, + :tied_to_judge, + :video_hearing_requested, + :type_original, + :ready_for_distribution, + tied_judge: non_ssc_avlj2, + correspondent: correspondent, + bfcorlid: "#{veteran.file_number}S", + case_issues: create_list(:case_issue, 3, :compensation), + bfd19: 7.days.ago + ) + end + + let!(:legacy_unsigned_priority_tied_to_non_ssc_avlj1) do + legacy_appeal = create( + :legacy_appeal, + :with_root_task, + vacols_case: vacols_prio_case, + closest_regional_office: "RO17" + ) + create(:available_hearing_locations, "RO17", appeal: legacy_appeal) + end + + let!(:legacy_unsigned_non_priority_tied_to_non_ssc_avlj2) do + legacy_appeal = create( + :legacy_appeal, + :with_root_task, + vacols_case: vacols_non_prio_case, + closest_regional_office: "RO17" + ) + create(:available_hearing_locations, "RO17", appeal: legacy_appeal) + end + + let!(:legacy_signed_non_priority_tied_to_non_ssc_avlj1) do + create(:legacy_signed_appeal, :type_original, signing_avlj: non_ssc_avlj, assigned_avlj: non_ssc_avlj) + end + + let!(:legacy_signed_priority_tied_to_non_ssc_avlj2) do + create(:legacy_signed_appeal, :type_cavc_remand, signing_avlj: non_ssc_avlj2, assigned_avlj: non_ssc_avlj2) + end + + let(:appeals) { [] } + + context "there are 2 appeals still in loc 81" do + let(:vacols_prio_case_81) do + create( + :case, + :aod, + :tied_to_judge, + :video_hearing_requested, + :type_original, + :ready_for_distribution, + tied_judge: non_ssc_avlj1, + correspondent: correspondent, + bfcorlid: "#{veteran.file_number}S", + case_issues: create_list(:case_issue, 3, :compensation), + bfd19: 60.days.ago + ) + end + + let(:vacols_non_prio_case_81) do + create( + :case, + :tied_to_judge, + :video_hearing_requested, + :type_original, + :ready_for_distribution, + tied_judge: non_ssc_avlj2, + correspondent: correspondent, + bfcorlid: "#{veteran.file_number}S", + case_issues: create_list(:case_issue, 3, :compensation), + bfd19: 7.days.ago + ) + end + + let!(:legacy_unsigned_priority_tied_to_non_ssc_avlj1_81) do + legacy_appeal = create( + :legacy_appeal, + :with_root_task, + vacols_case: vacols_prio_case_81, + closest_regional_office: "RO17" + ) + create(:available_hearing_locations, "RO17", appeal: legacy_appeal) + end + + let!(:legacy_unsigned_non_priority_tied_to_non_ssc_avlj2_81) do + legacy_appeal = create( + :legacy_appeal, + :with_root_task, + vacols_case: vacols_non_prio_case_81, + closest_regional_office: "RO17" + ) + create(:available_hearing_locations, "RO17", appeal: legacy_appeal) + end + + it "fetches the correct matching appeals only in loc 63" do + move_to_loc_63(vacols_prio_case, 0.days.ago) + move_to_loc_63(vacols_non_prio_case, 1.day.ago) + move_to_loc_63(legacy_signed_non_priority_tied_to_non_ssc_avlj1, 1.day.ago) + move_to_loc_63(legacy_signed_priority_tied_to_non_ssc_avlj2, 2.days.ago) + + expected_appeals = [ + vacols_prio_case, + vacols_non_prio_case, + legacy_signed_non_priority_tied_to_non_ssc_avlj1, + legacy_signed_priority_tied_to_non_ssc_avlj2 + ] + expected_appeals_appended_bfkeys = expected_appeals.map { |ea| "150000#{ea.bfkey}" } + returned_appeals = job.send(:loc_63_appeals) + + expect(returned_appeals.size).to eq(4) + expect(returned_appeals.map { |ra| ra[:docket_number] }).to match_array(expected_appeals_appended_bfkeys) + end + end + + context "there are 2 appeals still in loc 81" do + let(:vacols_prio_case_3_days) do + create( + :case, + :aod, + :tied_to_judge, + :video_hearing_requested, + :type_original, + :ready_for_distribution, + tied_judge: non_ssc_avlj1, + correspondent: correspondent, + bfcorlid: "#{veteran.file_number}S", + case_issues: create_list(:case_issue, 3, :compensation), + bfd19: 60.days.ago + ) + end + + let(:vacols_non_prio_case_90_days) do + create( + :case, + :tied_to_judge, + :video_hearing_requested, + :type_original, + :ready_for_distribution, + tied_judge: non_ssc_avlj2, + correspondent: correspondent, + bfcorlid: "#{veteran.file_number}S", + case_issues: create_list(:case_issue, 3, :compensation), + bfd19: 7.days.ago + ) + end + + let!(:legacy_unsigned_priority_tied_to_non_ssc_avlj1_3_days) do + legacy_appeal = create( + :legacy_appeal, + :with_root_task, + vacols_case: vacols_prio_case_3_days, + closest_regional_office: "RO17" + ) + create(:available_hearing_locations, "RO17", appeal: legacy_appeal) + end + + let!(:legacy_unsigned_non_priority_tied_to_non_ssc_avlj2_90_days) do + legacy_appeal = create( + :legacy_appeal, + :with_root_task, + vacols_case: vacols_non_prio_case_90_days, + closest_regional_office: "RO17" + ) + create(:available_hearing_locations, "RO17", appeal: legacy_appeal) + end + + it "fetches the correct matching appeals only in loc 63" do + move_to_loc_63(vacols_prio_case, 0.days.ago) + move_to_loc_63(vacols_non_prio_case, 1.day.ago) + move_to_loc_63(legacy_signed_non_priority_tied_to_non_ssc_avlj1, 1.day.ago) + move_to_loc_63(legacy_signed_priority_tied_to_non_ssc_avlj2, 2.days.ago) + move_to_loc_63(vacols_prio_case_3_days, 3.days.ago) + move_to_loc_63(vacols_non_prio_case_90_days, 90.days.ago) + + expected_appeals = [ + vacols_prio_case, + vacols_non_prio_case, + legacy_signed_non_priority_tied_to_non_ssc_avlj1, + legacy_signed_priority_tied_to_non_ssc_avlj2 + ] + expected_appeals_appended_bfkeys = expected_appeals.map { |ea| "150000#{ea.bfkey}" } + returned_appeals = job.send(:loc_63_appeals) + + expect(returned_appeals.size).to eq(4) + expect(returned_appeals.map { |ra| ra[:docket_number] }).to match_array(expected_appeals_appended_bfkeys) + end + end + end + + def move_to_loc_63(legacy_case, date) + value = date.in_time_zone("America/New_York") + date_time = Time.utc(value.year, value.month, value.day, value.hour, value.min, value.sec) + + legacy_case.update!(bfcurloc: 63, bfdlocin: date_time) + end +end diff --git a/spec/queries/appeals_tied_to_avljs_and_vljs_query_spec.rb b/spec/queries/appeals_tied_to_avljs_and_vljs_query_spec.rb new file mode 100644 index 00000000000..6ddd918c5c6 --- /dev/null +++ b/spec/queries/appeals_tied_to_avljs_and_vljs_query_spec.rb @@ -0,0 +1,172 @@ +# frozen_string_literal: true + +describe AppealsTiedToAvljsAndVljsQuery do + let(:hearing_judge) { create(:user, :judge, :with_vacols_judge_record) } + let(:original_deciding_judge) { create(:user, :judge, :with_vacols_judge_record) } + + avlj_name = "John Doe" + let(:non_ssc_avlj) do + User.find_by_css_id("NONSSCTEST") || + create(:user, :non_ssc_avlj_user, css_id: "NONSSCTEST", full_name: avlj_name) + end + + signing_vlj_name = "Smith Cash" + let(:signing_vlj) do + User.find_by_css_id("VLJTEST") || + create(:user, :vlj_user, css_id: "VLJTEST", full_name: signing_vlj_name) + end + let(:veteran) { create(:veteran) } + + let(:correspondent) do + create( + :correspondent, + snamef: veteran.first_name, + snamel: veteran.last_name, + ssalut: "", ssn: veteran.file_number + ) + end + + let(:vacols_prio_case) do + create( + :case, + :aod, + :tied_to_judge, + :video_hearing_requested, + :type_original, + :ready_for_distribution, + tied_judge: non_ssc_avlj, + correspondent: correspondent, + bfcorlid: "#{veteran.file_number}S", + case_issues: create_list(:case_issue, 3, :compensation), + bfd19: 60.days.ago + ) + end + let(:vacols_non_prio_case) do + create( + :case, + :tied_to_judge, + :video_hearing_requested, + :type_original, + :ready_for_distribution, + tied_judge: non_ssc_avlj, + correspondent: correspondent, + bfcorlid: "#{veteran.file_number}S", + case_issues: create_list(:case_issue, 3, :compensation), + bfd19: 7.days.ago + ) + end + + context "#process and #tied_appeals" do + # Base appeals not tied to non ssc avljs that should NOT be grabbed from the query + let!(:not_ready_ama_original_appeal) { create(:appeal, :evidence_submission_docket, :with_post_intake_tasks) } + let!(:ama_original_direct_review_appeal) { create(:appeal, :direct_review_docket, :ready_for_distribution) } + let!(:ama_original_evidence_submission_appeal) do + create(:appeal, :evidence_submission_docket, :ready_for_distribution) + end + + let!(:not_ready_legacy_original_appeal) do + create(:case_with_form_9, :type_original, :travel_board_hearing_requested) + end + let!(:legacy_original_appeal_no_hearing) { create(:case, :type_original, :ready_for_distribution) } + + # Appeals that should be grabbed with the Query + let!(:legacy_unsigned_priority_tied_to_non_ssc_avlj) do + legacy_appeal = create( + :legacy_appeal, + :with_root_task, + vacols_case: vacols_prio_case, + closest_regional_office: "RO17" + ) + create(:available_hearing_locations, "RO17", appeal: legacy_appeal) + end + + let!(:legacy_unsigned_non_priority_tied_to_non_ssc_avlj) do + legacy_appeal = create( + :legacy_appeal, + :with_root_task, + vacols_case: vacols_non_prio_case, + closest_regional_office: "RO17" + ) + create(:available_hearing_locations, "RO17", appeal: legacy_appeal) + end + + let!(:legacy_signed_non_priority_tied_to_non_ssc_avlj) do + create(:legacy_signed_appeal, :type_original, signing_avlj: signing_vlj, assigned_avlj: non_ssc_avlj) + end + + let!(:legacy_signed_priority_tied_to_non_ssc_avlj) do + create(:legacy_signed_appeal, :type_cavc_remand, signing_avlj: signing_vlj, assigned_avlj: non_ssc_avlj) + end + + let!(:legacy_original_appeal_with_hearing) do + create(:case, :type_original, :ready_for_distribution, case_hearings: [legacy_original_appeal_case_hearing]) + end + let(:legacy_original_appeal_case_hearing) { build(:case_hearing, :disposition_held, user: hearing_judge) } + + let!(:ama_original_hearing_appeal) do + create(:appeal, :hearing_docket, :held_hearing_and_ready_to_distribute, tied_judge: hearing_judge) + end + + it "selects all appeals tied to non ssc avlj and generates the CSV" do + expect { described_class.process }.not_to raise_error + expect(described_class.tied_appeals.size).to eq 6 + end + end + + context "Test the CSV generation" do + let!(:legacy_signed_appeal_with_attributes) do + create(:legacy_signed_appeal, :type_original, signing_avlj: signing_vlj, assigned_avlj: non_ssc_avlj) + end + + let!(:ama_appeal) do + create(:appeal, :hearing_docket, :held_hearing_and_ready_to_distribute, tied_judge: hearing_judge) + end + + let(:legacy_query_result) { VACOLS::CaseDocket.appeals_tied_to_avljs_and_vljs } + + let(:docket) { HearingRequestDocket.new } + let(:ama_query_result) { docket.tied_to_vljs(described_class.vlj_user_ids) } + + context "where it uses attributes " do + it "to create a hash for AMA and Legacy rows" do + subject_legacy = described_class.legacy_rows(legacy_query_result, :legacy).first + subject_ama = described_class.ama_rows(ama_query_result, :hearing).first + corres = legacy_signed_appeal_with_attributes.reload.correspondent + corres_ama = ama_appeal.reload.veteran + + expect(subject_legacy[:docket_number]).to eq legacy_signed_appeal_with_attributes.folder.tinum + expect(subject_legacy[:docket]).to eq "legacy" + expect(subject_legacy[:priority]).to be "" + expect(subject_legacy[:veteran_file_number]).to eq corres.ssn + expect(subject_legacy[:veteran_name]).to eq "#{corres.snamef} #{corres.snamel}" + expect(subject_legacy[:vlj]).to eq avlj_name + expect(subject_legacy[:hearing_judge]).to eq avlj_name + expect(subject_legacy[:most_recent_signing_judge]).to eq signing_vlj_name + expect(subject_legacy[:bfcurloc]).to eq legacy_signed_appeal_with_attributes.bfcurloc + + expect(subject_ama[:docket_number]).to eq ama_appeal.docket_number + expect(subject_ama[:docket]).to eq "hearing" + expect(subject_ama[:priority]).to be false + expect(subject_ama[:veteran_file_number]).to eq corres_ama.file_number + expect(subject_ama[:veteran_name]).to eq corres_ama.name.to_s + expect(subject_ama[:vlj]).to eq hearing_judge.full_name + expect(subject_ama[:hearing_judge]).to eq hearing_judge.full_name + expect(subject_ama[:most_recent_signing_judge]).to eq nil + expect(subject_ama[:bfcurloc]).to eq nil + end + + it "to verify that calculate_field_values is returning the correct items" do + subject = described_class.calculate_field_values(legacy_query_result.first) + + corres = legacy_signed_appeal_with_attributes.reload.correspondent + + expect(subject[:veteran_file_number]).to eq corres.ssn + expect(subject[:veteran_name]).to eq "#{corres.snamef} #{corres.snamel}" + expect(subject[:vlj]).to eq avlj_name + expect(subject[:hearing_judge]).to eq avlj_name + expect(subject[:most_recent_signing_judge]).to eq signing_vlj_name + expect(subject[:bfcurloc]).to eq legacy_signed_appeal_with_attributes.bfcurloc + end + end + end +end diff --git a/spec/queries/appeals_tied_to_non_ssc_avlj_query_spec.rb b/spec/queries/appeals_tied_to_non_ssc_avlj_query_spec.rb new file mode 100644 index 00000000000..a4002422312 --- /dev/null +++ b/spec/queries/appeals_tied_to_non_ssc_avlj_query_spec.rb @@ -0,0 +1,157 @@ +# frozen_string_literal: true + +describe AppealsTiedToNonSscAvljQuery do + let(:hearing_judge) { create(:user, :judge, :with_vacols_judge_record) } + let(:original_deciding_judge) { create(:user, :judge, :with_vacols_judge_record) } + + avlj_name = "John Doe" + let(:non_ssc_avlj) do + User.find_by_css_id("NONSSCTEST") || + create(:user, :non_ssc_avlj_user, css_id: "NONSSCTEST", full_name: avlj_name) + end + let(:veteran) { create(:veteran) } + + let(:correspondent) do + create( + :correspondent, + snamef: veteran.first_name, + snamel: veteran.last_name, + ssalut: "", ssn: veteran.file_number + ) + end + + let(:vacols_prio_case) do + create( + :case, + :aod, + :tied_to_judge, + :video_hearing_requested, + :type_original, + :ready_for_distribution, + tied_judge: non_ssc_avlj, + correspondent: correspondent, + bfcorlid: "#{veteran.file_number}S", + case_issues: create_list(:case_issue, 3, :compensation), + bfd19: 60.days.ago + ) + end + let(:vacols_non_prio_case) do + create( + :case, + :tied_to_judge, + :video_hearing_requested, + :type_original, + :ready_for_distribution, + tied_judge: non_ssc_avlj, + correspondent: correspondent, + bfcorlid: "#{veteran.file_number}S", + case_issues: create_list(:case_issue, 3, :compensation), + bfd19: 7.days.ago + ) + end + + context "#process and #tied_appeals" do + # Base appeals not tied to non ssc avljs that should NOT be grabbed from the query + let!(:not_ready_ama_original_appeal) { create(:appeal, :evidence_submission_docket, :with_post_intake_tasks) } + let!(:ama_original_direct_review_appeal) { create(:appeal, :direct_review_docket, :ready_for_distribution) } + let!(:ama_original_evidence_submission_appeal) do + create(:appeal, :evidence_submission_docket, :ready_for_distribution) + end + let!(:ama_original_hearing_appeal) do + create(:appeal, :hearing_docket, :held_hearing_and_ready_to_distribute, tied_judge: hearing_judge) + end + + let!(:not_ready_legacy_original_appeal) do + create(:case_with_form_9, :type_original, :travel_board_hearing_requested) + end + let!(:legacy_original_appeal_no_hearing) { create(:case, :type_original, :ready_for_distribution) } + let!(:legacy_original_appeal_with_hearing) do + create(:case, :type_original, :ready_for_distribution, case_hearings: [legacy_original_appeal_case_hearing]) + end + let(:legacy_original_appeal_case_hearing) { build(:case_hearing, :disposition_held, user: hearing_judge) } + + # Appeals that should be grabbed with the Query + let!(:legacy_unsigned_priority_tied_to_non_ssc_avlj) do + legacy_appeal = create( + :legacy_appeal, + :with_root_task, + vacols_case: vacols_prio_case, + closest_regional_office: "RO17" + ) + create(:available_hearing_locations, "RO17", appeal: legacy_appeal) + end + + let!(:legacy_unsigned_non_priority_tied_to_non_ssc_avlj) do + legacy_appeal = create( + :legacy_appeal, + :with_root_task, + vacols_case: vacols_non_prio_case, + closest_regional_office: "RO17" + ) + create(:available_hearing_locations, "RO17", appeal: legacy_appeal) + end + + let!(:legacy_signed_non_priority_tied_to_non_ssc_avlj) do + create(:legacy_signed_appeal, :type_original, signing_avlj: non_ssc_avlj, assigned_avlj: non_ssc_avlj) + end + + let!(:legacy_signed_priority_tied_to_non_ssc_avlj) do + create(:legacy_signed_appeal, :type_cavc_remand, signing_avlj: non_ssc_avlj, assigned_avlj: non_ssc_avlj) + end + + it "selects all appeals tied to non ssc avlj and generates the CSV" do + expect { described_class.process }.not_to raise_error + expect(described_class.tied_appeals.size).to eq 4 + end + end + + context "Test the CSV generation" do + let!(:legacy_signed_appeal_with_attributes) do + create(:legacy_signed_appeal, :type_original, signing_avlj: non_ssc_avlj, assigned_avlj: non_ssc_avlj) + end + + let(:query_result) { VACOLS::CaseDocket.appeals_tied_to_non_ssc_avljs } + + subject { described_class.legacy_rows(query_result, :legacy).first } + + context "where it uses attributes " do + it "to create a hash for the row" do + corres = legacy_signed_appeal_with_attributes.reload.correspondent + + expect(subject[:docket_number]).to eq legacy_signed_appeal_with_attributes.folder.tinum + expect(subject[:docket]).to eq "legacy" + expect(subject[:priority]).to be "" + expect(subject[:veteran_file_number]).to eq corres.ssn + expect(subject[:veteran_name]).to eq "#{corres.snamef} #{corres.snamel}" + expect(subject[:non_ssc_avlj]).to eq avlj_name + expect(subject[:hearing_judge]).to eq avlj_name + expect(subject[:most_recent_signing_judge]).to eq avlj_name + expect(subject[:bfcurloc]).to eq legacy_signed_appeal_with_attributes.bfcurloc + end + + context "to test getting the avlj name from appeal" do + it "where appeals vlj is nil" do + appeal = query_result.first + appeal["vlj"] = nil + expect(described_class.get_avlj_name(appeal)).to eq nil + end + it "where appeals vlj is not nil" do + appeal = query_result.first + expect(described_class.get_avlj_name(appeal)).to eq avlj_name + end + end + + context "to test getting the prev judges name from appeal" do + it "where appeal has no prev deciding judge" do + appeal = query_result.first + appeal["prev_deciding_judge"] = nil + expect(described_class.get_prev_judge_name(appeal)).to eq nil + end + it "where appeal has a previous deciding judge" do + appeal = query_result.first + expect(described_class.get_prev_judge_name(appeal)).to eq avlj_name + end + end + end + end +end diff --git a/spec/seeds/case_distribution_test_data_spec.rb b/spec/seeds/case_distribution_test_data_spec.rb index 373982fd017..86d44f2186f 100644 --- a/spec/seeds/case_distribution_test_data_spec.rb +++ b/spec/seeds/case_distribution_test_data_spec.rb @@ -45,7 +45,7 @@ seed.seed! # checking CaseDistributionlevers count - expect(CaseDistributionLever.count).to eq 28 + expect(CaseDistributionLever.count).to eq 30 expect(Appeal.where(docket_type: "direct_review").count).to eq 38 expect(Appeal.where(docket_type: "direct_review").first.receipt_date).to eq(Time.zone.today - (20.years + 1.day)) From 8d7b9da94a17b9a06d6081bb108a12ae7657f920 Mon Sep 17 00:00:00 2001 From: ryanpmessner <163380175+ryanpmessner@users.noreply.github.com> Date: Mon, 16 Sep 2024 14:20:10 -0400 Subject: [PATCH 5/9] hotfix APPEALS-23420 and APPEALS-57844 (#22848) * APPEALS-23420 Add search query service for the api response for the `/search` page * APPEALS-50829: Add restricted_statuses solution and initial RSpec test file Fix positioning of restricted_statuses array to under set_type Make modification that only prevents non-board users from viewing the assigned_to_location CSS IDs Only prevent vso employees/Private attorneys from viewing CSS IDs in restricted status state Refactor comment to better explain what is happening, and add proper staging to RSpec file Make restricted statuses array immutable Add more extensive test coverage (every restricted status type) * APPEALS-23420 use new query for veteran_ids endpoint * APPEALS-23420 Add search query service for the api response for the `/search` page * APPEALS-23420 fix linting errors --------- Co-authored-by: AimanK Co-authored-by: Raymond Hughes <131811099+raymond-hughes@users.noreply.github.com> Co-authored-by: Ron Wabukenda <130374706+ronwabVa@users.noreply.github.com> --- .../idt/api/v2/appeals_controller.rb | 4 +- app/decorators/appeal_status_api_decorator.rb | 12 +- app/models/appeal.rb | 16 +- .../work_queue/appeal_search_serializer.rb | 15 +- app/models/task.rb | 6 +- .../tasks/evidence_submission_window_task.rb | 2 +- app/services/bva_appeal_status.rb | 75 ++-- app/services/search_query_service.rb | 96 +++++ .../search_query_service/api_response.rb | 3 + .../search_query_service/appeal_row.rb | 167 ++++++++ .../search_query_service/attributes.rb | 30 ++ .../search_query_service/legacy_appeal_row.rb | 142 +++++++ .../search_query_service/legacy_attributes.rb | 25 ++ .../search_query_service/queried_appeal.rb | 133 ++++++ .../search_query_service/queried_hearing.rb | 33 ++ .../queried_legacy_appeal.rb | 41 ++ app/services/search_query_service/query.rb | 390 ++++++++++++++++++ .../search_query_service/search_response.rb | 9 + .../vso_user_search_results.rb | 68 +++ app/workflows/case_search_results_base.rb | 118 ++++-- ..._search_results_for_caseflow_veteran_id.rb | 22 + .../case_search_results_for_docket_number.rb | 6 +- ..._search_results_for_veteran_file_number.rb | 27 ++ spec/feature/queue/search_spec.rb | 6 +- spec/fixes/assigned_to_search_results_spec.rb | 6 +- spec/fixes/backfill_early_ama_appeal_spec.rb | 4 +- .../appeal_search_serializer_spec.rb | 183 ++++++++ spec/services/bva_appeal_status_spec.rb | 2 +- spec/services/search_query_service_spec.rb | 311 ++++++++++++++ 29 files changed, 1850 insertions(+), 102 deletions(-) create mode 100644 app/services/search_query_service.rb create mode 100644 app/services/search_query_service/api_response.rb create mode 100644 app/services/search_query_service/appeal_row.rb create mode 100644 app/services/search_query_service/attributes.rb create mode 100644 app/services/search_query_service/legacy_appeal_row.rb create mode 100644 app/services/search_query_service/legacy_attributes.rb create mode 100644 app/services/search_query_service/queried_appeal.rb create mode 100644 app/services/search_query_service/queried_hearing.rb create mode 100644 app/services/search_query_service/queried_legacy_appeal.rb create mode 100644 app/services/search_query_service/query.rb create mode 100644 app/services/search_query_service/search_response.rb create mode 100644 app/services/search_query_service/vso_user_search_results.rb create mode 100644 spec/models/serializers/work_queue/appeal_search_serializer_spec.rb create mode 100644 spec/services/search_query_service_spec.rb diff --git a/app/controllers/idt/api/v2/appeals_controller.rb b/app/controllers/idt/api/v2/appeals_controller.rb index 74884231964..b93cf8179dc 100644 --- a/app/controllers/idt/api/v2/appeals_controller.rb +++ b/app/controllers/idt/api/v2/appeals_controller.rb @@ -14,11 +14,11 @@ def details result = if docket_number?(case_search) CaseSearchResultsForDocketNumber.new( docket_number: case_search, user: current_user - ).call + ).api_call else CaseSearchResultsForVeteranFileNumber.new( file_number_or_ssn: case_search, user: current_user - ).call + ).api_call end render_search_results_as_json(result) diff --git a/app/decorators/appeal_status_api_decorator.rb b/app/decorators/appeal_status_api_decorator.rb index a93ed1b9bbf..7be8e8e75aa 100644 --- a/app/decorators/appeal_status_api_decorator.rb +++ b/app/decorators/appeal_status_api_decorator.rb @@ -3,6 +3,12 @@ # Extends the Appeal model with methods for the Appeals Status API class AppealStatusApiDecorator < ApplicationDecorator + def initialize(appeal, scheduled_hearing = nil) + super(appeal) + + @scheduled_hearing = scheduled_hearing + end + def appeal_status_id "A#{id}" end @@ -162,11 +168,11 @@ def remanded_sc_decision_issues end def open_pre_docket_task? - tasks.open.any? { |task| task.is_a?(PreDocketTask) } + open_tasks.any? { |task| task.is_a?(PreDocketTask) } end def pending_schedule_hearing_task? - tasks.open.where(type: ScheduleHearingTask.name).any? + pending_schedule_hearing_tasks.any? end def hearing_pending? @@ -174,7 +180,7 @@ def hearing_pending? end def evidence_submission_hold_pending? - tasks.open.where(type: EvidenceSubmissionWindowTask.name).any? + evidence_submission_hold_pending_tasks.any? end def at_vso? diff --git a/app/models/appeal.rb b/app/models/appeal.rb index 4691f672563..59367fa3a36 100644 --- a/app/models/appeal.rb +++ b/app/models/appeal.rb @@ -308,6 +308,18 @@ def decorated_with_status AppealStatusApiDecorator.new(self) end + def open_tasks + tasks.open + end + + def pending_schedule_hearing_tasks + tasks.open.where(type: ScheduleHearingTask.name) + end + + def evidence_submission_hold_pending_tasks + tasks.open.where(type: EvidenceSubmissionWindowTask.name) + end + # :reek:RepeatedConditionals def active_request_issues_or_decision_issues decision_issues.empty? ? active_request_issues : fetch_all_decision_issues @@ -633,7 +645,7 @@ def direct_review_docket? end def active? - tasks.open.of_type(:RootTask).any? + open_tasks.of_type(:RootTask).any? end def ready_for_distribution? @@ -748,7 +760,7 @@ def substitutions end def status - @status ||= BVAAppealStatus.new(appeal: self) + @status ||= BVAAppealStatus.new(tasks: tasks) end def previously_selected_for_quality_review diff --git a/app/models/serializers/work_queue/appeal_search_serializer.rb b/app/models/serializers/work_queue/appeal_search_serializer.rb index 90be5a6f498..320c5bdb075 100644 --- a/app/models/serializers/work_queue/appeal_search_serializer.rb +++ b/app/models/serializers/work_queue/appeal_search_serializer.rb @@ -6,6 +6,16 @@ class WorkQueue::AppealSearchSerializer set_type :appeal + RESTRICTED_STATUSES = + [ + :distributed_to_judge, + :ready_for_signature, + :on_hold, + :misc, + :unknown, + :assigned_to_attorney + ].freeze + attribute :contested_claim, &:contested_claim? attribute :mst, &:mst? @@ -73,10 +83,11 @@ class WorkQueue::AppealSearchSerializer attribute :veteran_appellant_deceased, &:veteran_appellant_deceased? attribute :assigned_to_location do |object, params| - if object&.status&.status == :distributed_to_judge - if params[:user]&.judge? || params[:user]&.attorney? || User.list_hearing_coordinators.include?(params[:user]) + if RESTRICTED_STATUSES.include?(object&.status&.status) + unless params[:user]&.vso_employee? object.assigned_to_location end + # if not in a restricted status, show assigned location to all users else object.assigned_to_location end diff --git a/app/models/task.rb b/app/models/task.rb index b2244e0f1b0..4c0b7066290 100644 --- a/app/models/task.rb +++ b/app/models/task.rb @@ -101,7 +101,7 @@ class << self; undef_method :open; end # Equivalent to .reject(&:hide_from_queue_table_view) but offloads that to the database. scope :visible_in_queue_table_view, lambda { where.not( - type: Task.descendants.select(&:hide_from_queue_table_view).map(&:name) + type: hidden_task_classes ) } @@ -138,6 +138,10 @@ class << self # With taks that are likely to need Reader to complete READER_PRIORITY_TASK_TYPES = [JudgeAssignTask.name, JudgeDecisionReviewTask.name].freeze + def hidden_task_classes + Task.descendants.select(&:hide_from_queue_table_view).map(&:name) + end + def reader_priority_task_types READER_PRIORITY_TASK_TYPES end diff --git a/app/models/tasks/evidence_submission_window_task.rb b/app/models/tasks/evidence_submission_window_task.rb index 7c9cdb5f8fa..0028fc54366 100644 --- a/app/models/tasks/evidence_submission_window_task.rb +++ b/app/models/tasks/evidence_submission_window_task.rb @@ -11,7 +11,7 @@ class EvidenceSubmissionWindowTask < Task before_validation :set_assignee - def initialize(args) + def initialize(args = {}) @end_date = args&.fetch(:end_date, nil) super(args&.except(:end_date)) end diff --git a/app/services/bva_appeal_status.rb b/app/services/bva_appeal_status.rb index 1577a7ea7c6..c25886c15a3 100644 --- a/app/services/bva_appeal_status.rb +++ b/app/services/bva_appeal_status.rb @@ -3,7 +3,7 @@ # Determine the BVA workflow status of an Appeal (symbol and string) based on its Tasks. class BVAAppealStatus - attr_reader :status + attr_reader :status, :tasks SORT_KEYS = { not_distributed: 1, @@ -69,8 +69,18 @@ def attorney_task_names end end - def initialize(appeal:) - @appeal = appeal + Tasks = Struct.new( + :open, + :active, + :in_progress, + :cancelled, + :completed, + :assigned, + keyword_init: true + ) + + def initialize(tasks:) + @tasks = tasks @status = compute end @@ -86,15 +96,12 @@ def to_i SORT_KEYS[status] end - def as_json(_args) + def as_json(_args = nil) to_sym end private - attr_reader :appeal - - delegate :tasks, to: :appeal # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength def compute if open_pre_docket_task? @@ -113,7 +120,7 @@ def compute :ready_for_signature elsif active_sign_task? :signed - elsif completed_dispatch_task? && open_tasks.empty? + elsif completed_dispatch_task? && tasks.open.empty? :dispatched elsif completed_dispatch_task? :post_dispatch @@ -133,84 +140,60 @@ def compute end # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength - def open_tasks - @open_tasks ||= tasks.open - end - - def active_tasks - @active_tasks ||= tasks.active - end - - def assigned_tasks - @assigned_tasks ||= tasks.assigned - end - - def in_progress_tasks - @in_progress_tasks ||= tasks.in_progress - end - - def cancelled_tasks - @cancelled_tasks ||= tasks.cancelled - end - - def completed_tasks - @completed_tasks ||= tasks.completed - end - def open_pre_docket_task? - open_tasks.any? { |task| task.is_a?(PreDocketTask) } + tasks.open.any? { |task| task.type == "PreDocketTask" } end def open_distribution_task? - open_tasks.any? { |task| task.is_a?(DistributionTask) } + tasks.open.any? { |task| task.type == "DistributionTask" } end def open_timed_hold_task? - open_tasks.any? { |task| task.is_a?(TimedHoldTask) } + tasks.open.any? { |task| task.type == "TimedHoldTask" } end def active_judge_assign_task? - active_tasks.any? { |task| task.is_a?(JudgeAssignTask) } + tasks.active.any? { |task| task.type == "JudgeAssignTask" } end def assigned_attorney_task? - assigned_tasks.any? { |task| self.class.attorney_task_names.include?(task.type) } + tasks.assigned.any? { |task| self.class.attorney_task_names.include?(task.type) } end def active_colocated_task? - active_tasks.any? { |task| self.class.colocated_task_names.include?(task.type) } + tasks.active.any? { |task| self.class.colocated_task_names.include?(task.type) } end def attorney_task_in_progress? - in_progress_tasks.any? { |task| self.class.attorney_task_names.include?(task.type) } + tasks.in_progress.any? { |task| self.class.attorney_task_names.include?(task.type) } end def active_judge_decision_review_task? - active_tasks.any? { |task| task.is_a?(JudgeDecisionReviewTask) } + tasks.active.any? { |task| task.type == "JudgeDecisionReviewTask" } end def active_sign_task? - active_tasks.any? { |task| %w[BvaDispatchTask QualityReviewTask].include?(task.type) } + tasks.active.any? { |task| %w[BvaDispatchTask QualityReviewTask].include?(task.type) } end def completed_dispatch_task? - completed_tasks.any? { |task| task.is_a?(BvaDispatchTask) } + tasks.completed.any? { |task| task.type == "BvaDispatchTask" } end def docket_switched? # TODO: this should be updated to check that there are no active tasks once the task handling is implemented - completed_tasks.any? { |task| task.is_a?(DocketSwitchGrantedTask) } + tasks.completed.any? { |task| task.type == "DocketSwitchGrantedTask" } end def cancelled_root_task? - cancelled_tasks.any? { |task| task.is_a?(RootTask) } + tasks.cancelled.any? { |task| task.type == "RootTask" } end def misc_task? - active_tasks.any? { |task| self.class.misc_task_names.include?(task.type) } + tasks.active.any? { |task| self.class.misc_task_names.include?(task.type) } end def active_specialty_case_team_assign_task? - active_tasks.any? { |task| task.is_a?(SpecialtyCaseTeamAssignTask) } + tasks.active.any? { |task| task.type == "SpecialtyCaseTeamAssignTask" } end end diff --git a/app/services/search_query_service.rb b/app/services/search_query_service.rb new file mode 100644 index 00000000000..11286329925 --- /dev/null +++ b/app/services/search_query_service.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true + +class SearchQueryService + def initialize(file_number: nil, docket_number: nil, veteran_ids: nil) + @docket_number = docket_number + @file_number = file_number + @veteran_ids = veteran_ids + @queries = SearchQueryService::Query.new + end + + def search_by_veteran_file_number + combined_results + end + + def search_by_docket_number + results = ActiveRecord::Base.connection.exec_query( + sanitize([queries.docket_number_query, docket_number]) + ) + + results.map do |row| + AppealRow.new(row).search_response + end + end + + def search_by_veteran_ids + combined_results + end + + private + + attr_reader :docket_number, :file_number, :queries, :veteran_ids + + def combined_results + search_results.map do |row| + if row["type"] != "legacy_appeal" + AppealRow.new(row).search_response + else + vacols_row = vacols_results.find { |result| result["vacols_id"] == row["external_id"] } + LegacyAppealRow.new(row, vacols_row).search_response + end + end + end + + def vacols_ids + legacy_results.map { |result| result["external_id"] } + end + + def legacy_results + search_results.select { |result| result["type"] == "legacy_appeal" } + end + + def search_results + @search_results ||= + if file_number.present? + file_number_search_results + else + veteran_ids_search_results + end + end + + def veteran_ids_search_results + ActiveRecord::Base + .connection + .exec_query( + sanitize([queries.veteran_ids_query, veteran_ids, veteran_ids]) + ) + .uniq { |result| result["external_id"] } + end + + def file_number_search_results + ActiveRecord::Base + .connection + .exec_query(file_number_or_ssn_query) + .uniq { |result| result["external_id"] } + end + + def file_number_or_ssn_query + sanitize( + [ + queries.veteran_file_number_query, + *[file_number].cycle(queries.veteran_file_number_num_params).to_a + ] + ) + end + + def vacols_results + @vacols_results ||= begin + vacols_query = VACOLS::Record.sanitize_sql_array([queries.vacols_query, vacols_ids]) + VACOLS::Record.connection.exec_query(vacols_query) + end + end + + def sanitize(values) + ActiveRecord::Base.sanitize_sql_array(values) + end +end diff --git a/app/services/search_query_service/api_response.rb b/app/services/search_query_service/api_response.rb new file mode 100644 index 00000000000..76a216b678e --- /dev/null +++ b/app/services/search_query_service/api_response.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +SearchQueryService::ApiResponse = Struct.new(:id, :type, :attributes, keyword_init: true) diff --git a/app/services/search_query_service/appeal_row.rb b/app/services/search_query_service/appeal_row.rb new file mode 100644 index 00000000000..e47be978e31 --- /dev/null +++ b/app/services/search_query_service/appeal_row.rb @@ -0,0 +1,167 @@ +# frozen_string_literal: true + +class SearchQueryService::AppealRow + def initialize(query_row) + @query_row = query_row + end + + def search_response + SearchQueryService::SearchResponse.new( + queried_appeal, + :appeal, + SearchQueryService::ApiResponse.new( + id: query_row["id"], + type: "appeal", + attributes: attributes + ) + ) + end + + private + + attr_reader :query_row + + # rubocop:disable Metrics/MethodLength + def attributes + SearchQueryService::Attributes.new( + aod: aod, + appellant_full_name: appellant_full_name, + assigned_to_location: queried_appeal.assigned_to_location, + assigned_attorney: assigned_attorney, + caseflow_veteran_id: query_row["veteran_id"], + contested_claim: contested_claim, + decision_date: decision_date, + decision_issues: decision_issues, + docket_name: query_row["docket_type"], + docket_number: docket_number, + external_id: query_row["external_id"], + hearings: hearings, + issues: issues, + mst: mst_status, + overtime: query_row["overtime"], + pact: pact_status, + paper_case: false, + status: queried_appeal.status, + type: stream_type, + veteran_appellant_deceased: veteran_appellant_deceased, + veteran_file_number: veteran_file_number, + veteran_full_name: veteran_full_name, + withdrawn: withdrawn + ) + end + # rubocop:enable Metrics/MethodLength + + def aod + query_row["aod_granted_for_person"].present? + end + + def decision_issues + json_array("decision_issues") + end + + def docket_number + attrs, = JSON.parse query_row["appeal"] + attrs["stream_docket_number"] + end + + def decision_date + Date.parse(query_row["decision_date"]) + rescue TypeError, Date::Error + nil + end + + def appellant_full_name + FullName.new(query_row["person_first_name"], "", query_row["person_last_name"]).to_s + end + + def veteran_full_name + FullName.new(query_row["veteran_first_name"], "", query_row["veteran_last_name"]).to_s + end + + def veteran_file_number + attrs, = JSON.parse(query_row["appeal"]) + attrs["veteran_file_number"] + end + + def issue(attributes) + unless FeatureToggle.enabled?(:pact_identification) + attributes.delete("pact_status") + end + unless FeatureToggle.enabled?(:mst_identification) + attributes.delete("mst_status") + end + end + + def issues + json_array("request_issues").map do |attributes| + attributes.tap do |attrs| + issue(attrs) + end + end + end + + def hearings + json_array("hearings") + end + + def withdrawn + WithdrawnDecisionReviewPolicy.new( + Struct.new( + :active_request_issues, + :withdrawn_request_issues + ).new( + json_array("active_request_issues"), + json_array("active_request_issues") + ) + ).satisfied? + end + + def stream_type + (query_row["stream_type"] || "Original").titleize + end + + def contested_claim + json_array("active_request_issues").any? do |issue| + %w(Contested Apportionment).any? do |code| + category = issue["nonrating_issue_category"] || "" + category.include?(code) + end + end + end + + def veteran_appellant_deceased + !!query_row["date_of_death"] && !json_array("appeal").first["veteran_is_not_claimant"] + end + + def pact_status + json_array("decision_issues").any? do |issue| + issue["pact_status"] + end + end + + def mst_status + json_array("decision_issues").any? do |issue| + issue["mst_status"] + end + end + + def queried_appeal + @queried_appeal ||= begin + appeal_attrs, = JSON.parse query_row["appeal"] + + SearchQueryService::QueriedAppeal.new( + attributes: appeal_attrs, + tasks_attributes: json_array("tasks"), + hearings_attributes: json_array("hearings") + ) + end + end + + def assigned_attorney + json_array("assigned_attorney").first + end + + def json_array(key) + JSON.parse(query_row[key] || "[]") + end +end diff --git a/app/services/search_query_service/attributes.rb b/app/services/search_query_service/attributes.rb new file mode 100644 index 00000000000..6c4487a9b1e --- /dev/null +++ b/app/services/search_query_service/attributes.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +SearchQueryService::Attributes = Struct.new( + :aod, + :power_of_attorney, + :appellant_full_name, + :assigned_attorney, + :assigned_judge, + :assigned_to_location, + :caseflow_veteran_id, + :decision_date, + :decision_issues, + :docket_name, + :docket_number, + :external_id, + :hearings, + :issues, + :mst, + :overtime, + :pact, + :paper_case, + :status, + :type, + :veteran_appellant_deceased, + :veteran_file_number, + :veteran_full_name, + :contested_claim, + :withdrawn, + keyword_init: true +) diff --git a/app/services/search_query_service/legacy_appeal_row.rb b/app/services/search_query_service/legacy_appeal_row.rb new file mode 100644 index 00000000000..22009feb574 --- /dev/null +++ b/app/services/search_query_service/legacy_appeal_row.rb @@ -0,0 +1,142 @@ +# frozen_string_literal: true + +class SearchQueryService::LegacyAppealRow + def initialize(search_row, vacols_row) + @search_row = search_row + @vacols_row = vacols_row + end + + def search_response + SearchQueryService::SearchResponse.new( + legacy_appeal, + :legacy_appeal, + SearchQueryService::ApiResponse.new( + id: search_row["id"], + type: "legacy_appeal", + attributes: attributes + ) + ) + end + + private + + attr_reader :search_row, :vacols_row + + # rubocop:disable Metrics/MethodLength + def attributes + SearchQueryService::LegacyAttributes.new( + aod: aod, + appellant_full_name: appellant_full_name, + assigned_to_location: vacols_row["bfcurloc"], + caseflow_veteran_id: search_row["veteran_id"], + decision_date: decision_date, + docket_name: "legacy", + docket_number: vacols_row["tinum"], + external_id: vacols_row["vacols_id"], + hearings: hearings, + issues: [{}] * vacols_row["issues_count"], + mst: mst, + overtime: search_row["overtime"], + pact: pact, + paper_case: paper_case, + status: status, + type: stream_type, + veteran_appellant_deceased: veteran_appellant_deceased, + veteran_file_number: search_row["veteran_file_number"], + veteran_full_name: veteran_full_name + ) + end + # rubocop:enable Metrics/MethodLength + + def hearings + vacols_json_array("hearings").map do |attrs| + HearingAttributes.new(attrs).call + end + end + + class HearingAttributes + def initialize(attributes) + @attributes = attributes + end + + def call + { + disposition: VACOLS::CaseHearing::HEARING_DISPOSITIONS[attributes["disposition"].try(:to_sym)], + request_type: attributes["type"], + appeal_type: VACOLS::Case::TYPES[attributes["bfac"]], + external_id: attributes["external_id"], + date: HearingMapper.datetime_based_on_type( + datetime: attributes["date"], + regional_office: regional_office(attributes["venue"]), + type: attributes["type"] + ) + } + end + + private + + attr_reader :attributes + + def regional_office(ro_key) + RegionalOffice.find!(ro_key) + rescue NotFoundError + nil + end + end + + def vacols_json_array(key) + JSON.parse(vacols_row[key] || "[]") + end + + def veteran_appellant_deceased + search_row["date_of_death"].present? && + search_row["person_first_name"].present? + end + + def stream_type + VACOLS::Case::TYPES[vacols_row["bfac"]] + end + + def status + VACOLS::Case::STATUS[vacols_row["bfmpro"]] + end + + def paper_case + folder = Struct.new(:tivbms, :tisubj2).new( + vacols_row["tivbms"], + vacols_row["tisubj2"] + ) + AppealRepository.folder_type_from(folder) + end + + def mst + vacols_row["issues_mst_count"] > 0 + end + + def pact + vacols_row["issues_pact_count"] > 0 + end + + def appellant_full_name + FullName.new(vacols_row["sspare2"], "", vacols_row["sspare1"]).to_s + end + + def veteran_full_name + FullName.new(vacols_row["snamef"], "", vacols_row["snamel"]).to_s + end + + def aod + vacols_row["aod"] == 1 + end + + def decision_date + AppealRepository.normalize_vacols_date(vacols_row["bfddec"]) + end + + def legacy_appeal + @legacy_appeal ||= begin + appeal_attrs, = JSON.parse search_row["appeal"] + SearchQueryService::QueriedLegacyAppeal.new(attributes: appeal_attrs) + end + end +end diff --git a/app/services/search_query_service/legacy_attributes.rb b/app/services/search_query_service/legacy_attributes.rb new file mode 100644 index 00000000000..c4cf999b5f3 --- /dev/null +++ b/app/services/search_query_service/legacy_attributes.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +SearchQueryService::LegacyAttributes = Struct.new( + :aod, + :appellant_full_name, + :assigned_to_location, + :caseflow_veteran_id, + :decision_date, + :docket_name, + :docket_number, + :external_id, + :hearings, + :issues, + :mst, + :overtime, + :pact, + :paper_case, + :status, + :type, + :veteran_appellant_deceased, + :veteran_file_number, + :veteran_full_name, + :withdrawn, + keyword_init: true +) diff --git a/app/services/search_query_service/queried_appeal.rb b/app/services/search_query_service/queried_appeal.rb new file mode 100644 index 00000000000..297ad46cdcf --- /dev/null +++ b/app/services/search_query_service/queried_appeal.rb @@ -0,0 +1,133 @@ +# frozen_string_literal: true + +class SearchQueryService::QueriedAppeal < SimpleDelegator + def initialize(attributes:, tasks_attributes:, hearings_attributes:) + @attributes = OpenStruct.new( + appeal: attributes, + tasks: tasks_attributes, + hearings: hearings_attributes, + root_task: attributes.delete("root_task") || {}, + claimants: attributes.delete("claimants") || [] + ) + + super(appeal) + end + + def assigned_to_location + return COPY::CASE_LIST_TABLE_POST_DECISION_LABEL if root_task&.status == Constants.TASK_STATUSES.completed + + return most_recently_updated_visible_task.assigned_to_label if most_recently_updated_visible_task + + # this condition is no longer needed since we only want active or on hold tasks + return most_recently_updated_task&.assigned_to_label if most_recently_updated_task.present? + + fetch_api_status + end + + def claimant_participant_ids + claimants.map(&:participant_id) + end + + def claimants + @claimants ||= begin + attributes.claimants.map do |attrs| + Struct.new(:participant_id).new(attrs["participant_id"]) + end + end + end + + def root_task + @root_task ||= begin + if attributes.root_task.present? + Task.new.tap do |task| + task.assign_attributes attributes.root_task + end + end + end + end + + def open_tasks + @open_tasks ||= tasks.select do |task| + Task.open_statuses.include?(task.status) + end + end + + def active? + Task.active_statuses.include?(attributes.root_task["status"]) + end + + def pending_schedule_hearing_tasks + open_tasks.select { |task| task.type == "ScheduleHearingTask" } + end + + def evidence_submission_hold_pending_tasks + open_tasks.select { |task| task.type == "EvidenceSubmissionWindowTask" } + end + + def status + BVAAppealStatus.new( + tasks: BVAAppealStatus::Tasks.new( + open: tasks.select(&:open?), + active: tasks.select(&:active?), + in_progress: tasks.select(&:in_progress?), + cancelled: tasks.select(&:cancelled?), + completed: tasks.select(&:completed?), + assigned: tasks.select(&:assigned?) + ) + ).status + end + + private + + attr_reader :attributes + + def appeal + @appeal ||= Appeal.new.tap do |appeal| + appeal.assign_attributes(attributes.appeal) + end + end + + def most_recently_updated_visible_task + visible_tasks.select { |task| Task.active_statuses.include?(task.status) }.max_by(&:updated_at) || + visible_tasks.select { |task| task.status == "on_hold" }.max_by(&:updated_at) + end + + def visible_tasks + @visible_tasks ||= tasks.reject do |task| + Task.hidden_task_classes.include?(task.type) + end + end + + def most_recently_updated_task + tasks.max_by(&:updated_at) + end + + def tasks + @tasks ||= begin + attributes.tasks.map do |attrs| + attrs["type"].constantize.new.tap do |task| + task.assign_attributes attrs + end + end + end + end + + def fetch_api_status + AppealStatusApiDecorator.new( + self, + scheduled_hearing + ).fetch_status.to_s.titleize.to_sym + end + + def scheduled_hearing + @scheduled_hearing ||= begin + hearings = attributes.hearings.map do |attrs| + SearchQueryService::QueriedHearing.new(attrs) + end + + hearings.reject(&:disposition).find do |hearing| + hearing.scheduled_for >= Time.zone.today + end + end + end +end diff --git a/app/services/search_query_service/queried_hearing.rb b/app/services/search_query_service/queried_hearing.rb new file mode 100644 index 00000000000..efd16b8d187 --- /dev/null +++ b/app/services/search_query_service/queried_hearing.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +class SearchQueryService::QueriedHearing < SimpleDelegator + def initialize(attributes) + @attributes = attributes + manage_attributes + + super(hearing) + end + + def hearing_day + OpenStruct.new(hearing_day_attributes) + end + + def updated_by + OpenStruct.new(updated_by_attributes) + end + + private + + attr_reader :attributes, :hearing_day_attributes, :updated_by_attributes + + def manage_attributes + @hearing_day_attributes = attributes.delete("hearing_day") + @updated_by_attributes = attributes.delete("updated_by") + end + + def hearing + Hearing.new.tap do |hearing| + hearing.assign_attributes attributes + end + end +end diff --git a/app/services/search_query_service/queried_legacy_appeal.rb b/app/services/search_query_service/queried_legacy_appeal.rb new file mode 100644 index 00000000000..50aaab31a7f --- /dev/null +++ b/app/services/search_query_service/queried_legacy_appeal.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +class SearchQueryService::QueriedLegacyAppeal < SimpleDelegator + def initialize(attributes:) + @attributes = attributes + @root_task_attributes = attributes.delete("root_task") + @veteran_attributes = attributes.delete("veteran") + + super(legacy_appeal) + end + + def veteran + @veteran ||= Veteran.new.tap do |veteran| + veteran.assign_attributes veteran_attributes + end + end + + def root_task + @root_task ||= begin + if root_task_attributes + RootTask.new.tap do |root_task| + root_task.assign_attributes root_task_attributes + end + end + end + end + + def claimant_participant_ids + veteran.participant_id + end + + private + + attr_reader :attributes, :root_task_attributes, :veteran_attributes + + def legacy_appeal + @legacy_appeal ||= LegacyAppeal.new.tap do |appeal| + appeal.assign_attributes(attributes) + end + end +end diff --git a/app/services/search_query_service/query.rb b/app/services/search_query_service/query.rb new file mode 100644 index 00000000000..c089ac2750e --- /dev/null +++ b/app/services/search_query_service/query.rb @@ -0,0 +1,390 @@ +# frozen_string_literal: true + +class SearchQueryService::Query + def docket_number_query + <<-SQL + #{appeals_internal_query} + where a.stream_docket_number=?; + SQL + end + + def veteran_file_number_num_params + 4 + end + + def veteran_file_number_query + <<-SQL + ( + #{appeals_internal_query} + where v.ssn=? or v.file_number=? + ) + UNION + ( + #{legacy_appeals_internal_query} + where v.ssn=? or v.file_number=? + ) + SQL + end + + def veteran_ids_query + <<-SQL + ( + #{appeals_internal_query} + where v.id in (?) + ) + UNION + ( + #{legacy_appeals_internal_query} + where v.id in (?) + ) + SQL + end + + def vacols_query + <<-SQL + select + aod, + "cases".bfkey vacols_id, + "cases".bfcurloc, + "cases".bfddec, + "cases".bfmpro, + "cases".bfac, + "cases".bfcorlid, + "correspondents".snamef, + "correspondents".snamel, + "correspondents".sspare1, + "correspondents".sspare2, + "correspondents".slogid, + "folders".tinum, + "folders".tivbms, + "folders".tisubj2, + (select + JSON_ARRAYAGG(JSON_OBJECT( + 'venue' value #{case_hearing_venue_select}, + 'external_id' value "h".hearing_pkseq, + 'type' value "h".hearing_type, + 'disposition' value "h".hearing_disp, + 'date' value "h".hearing_date + )) + from hearsched "h" + where "h".folder_nr="cases".bfkey + ) hearings, + (select count("issues".isskey) from issues "issues" where "issues".isskey="cases".bfkey) issues_count, + (select count("hearings".hearing_pkseq) from hearsched "hearings" where "hearings".folder_nr="cases".bfkey) hearing_count, + (select count("issues".isskey) from issues "issues" where "issues".isskey="cases".bfkey and "issues".issmst='Y') issues_mst_count, + (select count("issues".isskey) from issues "issues" where "issues".isskey="cases".bfkey and "issues".isspact='Y') issues_pact_count + from + brieff "cases" + left join folder "folders" + on "cases".bfkey="folders".ticknum + left join corres "correspondents" + on "cases".bfcorkey="correspondents".stafkey + #{VACOLS::Case::JOIN_AOD} + where + "cases".bfkey in (?) + SQL + end + + private + + def case_hearing_venue_select + <<-SQL + case + when "h".hearing_type='#{VACOLS::CaseHearing::HEARING_TYPE_LOOKUP[:video]}' AND + "h".hearing_date < '#{VACOLS::CaseHearing::VACOLS_VIDEO_HEARINGS_END_DATE}' + then #{Rails.application.config.vacols_db_name}.HEARING_VENUE("h".vdkey) + else "cases".bfregoff + end + SQL + end + + def legacy_appeals_internal_query + <<-SQL + select + a.id, + a.vacols_id external_id, + 'legacy_appeal' type, + null aod_granted_for_person, + 'legacy' docket_type, + ( + select + jsonb_agg(la2) + from + ( + select + la.*, + ( + select + row_to_json(t.*) + from tasks t + where + t.appeal_id=a.id and + t.type='RootTask' and + t.appeal_type='Appeal' + order by t.updated_at desc + limit 1 + ) root_task, + (select row_to_json(v.*)) veteran + from legacy_appeals la + where la.id=a.id + ) la2 + ) appeal, + dd.decision_date, + wm.overtime, + pp.first_name person_first_name, + pp.last_name person_last_name, + v.id veteran_id, + v.first_name veteran_first_name, + v.last_name veteran_last_name, + v.file_number as veteran_file_number, + v.date_of_death, + ( + select jsonb_agg(u2) from + ( + select + u.* + from tasks t + left join users u on + t.assigned_to_id=u.id and + t.assigned_to_type='User' + where + t.appeal_type = 'LegacyAppeal' and + t.appeal_id=a.id and + t.type='AttorneyTask' and + t.status != '#{Constants.TASK_STATUSES.cancelled}' + order by t.created_at desc + limit 1 + ) u2 + ) assigned_attorney, + null request_issues, + null active_request_issues, + null withdrawn_request_issues, + null decision_issues, + null hearings_count, + '[]' hearings, + ( + select jsonb_agg(t2) from + ( + select + t.* + from tasks t + left join organizations o on o.id=t.assigned_to_id + left join users u on u.id=t.assigned_to_id + where + t.appeal_id=a.id and + t.appeal_type='LegacyAppeal' + order by updated_at desc + ) t2 + ) tasks + from legacy_appeals a + left join claimants cl on cl.decision_review_id=a.id and cl.decision_review_type='LegacyAppeal' + left join people pp on cl.participant_id=pp.participant_id + left join work_modes wm on wm.appeal_id=a.id and wm.appeal_type='LegacyAppeal' + left join decision_documents dd on dd.appeal_id=a.id and dd.appeal_type='LegacyAppeal' + left join veterans v on v.file_number=( + select + case + when right(a.vbms_id, 1) = 'C' then lpad(regexp_replace(a.vbms_id, '[^0-9]+', '', 'g'), 8, '0') + else regexp_replace(a.vbms_id, '[^0-9]+', '', 'g') + end + ) or v.ssn=( + select + case + when right(a.vbms_id, 1) = 'C' then lpad(regexp_replace(a.vbms_id, '[^0-9]+', '', 'g'), 8, '0') + else regexp_replace(a.vbms_id, '[^0-9]+', '', 'g') + end + ) + SQL + end + + def appeals_internal_query + <<-SQL + select + a.id, + a.uuid::varchar external_id, + a.stream_type type, + aod.id aod_granted_for_person, + a.docket_type, + ( + select jsonb_agg(a2) + from + ( + select + appeals.*, + ( + select + row_to_json(t.*) + from tasks t + where + t.appeal_id=a.id and + t.type='RootTask' and + t.appeal_type='Appeal' + order by updated_at desc + limit 1 + ) root_task, + ( + select jsonb_agg(c2) from + ( + select + c.id, + c.participant_id + from claimants c + where + c.decision_review_type = 'Appeal' and + c.decision_review_id=a.id + ) c2 + ) claimants + from appeals + where id=a.id + ) a2 + ) appeal, + dd.decision_date, + wm.overtime, + pp.first_name person_first_name, + pp.last_name person_last_name, + v.id veteran_id, + v.first_name veteran_first_name, + v.last_name veteran_last_name, + v.file_number as veteran_file_number, + v.date_of_death, + ( + select jsonb_agg(u2) from + ( + select + u.* + from tasks t + left join users u on + t.assigned_to_id=u.id and + t.assigned_to_type='User' + where + t.appeal_type = 'Appeal' and + t.appeal_id=a.id and + t.type='AttorneyTask' and + t.status != '#{Constants.TASK_STATUSES.cancelled}' + order by t.created_at desc + limit 1 + ) u2 + ) assigned_attorney, + ( + select jsonb_agg(ri2) from + ( + select + ri.id, + ri.benefit_type program, + ri.notes, + ri.decision_date, + ri.nonrating_issue_category, + ri.mst_status, + ri.pact_status, + ri.mst_status_update_reason_notes mst_justification, + ri.pact_status_update_reason_notes pact_justification + from request_issues ri + where + ri.decision_review_type='Appeal' and + ri.decision_review_id=a.id + ) ri2 + ) request_issues, + ( + select jsonb_agg(ri2) from + ( + select + nonrating_issue_category + from request_issues ri + where + ri.ineligible_reason is null and + ri.closed_at is null and + (ri.split_issue_status is null or ri.split_issue_status = 'in_progress') and + ri.decision_review_type='Appeal' and + ri.decision_review_id=a.id + ) ri2 + ) active_request_issues, + ( + select jsonb_agg(ri2) from + ( + select + nonrating_issue_category + from request_issues ri + where + ri.ineligible_reason is null and + ri.closed_status = 'widthrawn' and + ri.decision_review_type='Appeal' and + ri.decision_review_id=a.id + ) ri2 + ) withdrawn_request_issues, + ( + select jsonb_agg(di2) from + ( + select + di.id, + di.disposition, + di.description, + di.benefit_type, + di.diagnostic_code, + di.mst_status, + di.pact_status, + array( + select rdi.id + from request_decision_issues rdi + where rdi.decision_issue_id=di.id + ) request_issue_ids, + array( + select rr2 from + ( + select + rr.id, + rr.code, + rr.post_aoj + from remand_reasons rr + where rr.decision_issue_id=di.id + ) rr2 + ) remand_reasons + from decision_issues di + where + di.decision_review_type='Appeal' and + di.decision_review_id = a.id + ) di2 + ) decision_issues, + (select count(id) from hearings h where h.appeal_id=a.id) hearings_count, + ( + select jsonb_agg(h2) from + ( + select + h.*, + ( + select row_to_json(ub) + from (select * from users u where u.id=h.updated_by_id limit 1) ub + ) updated_by, + ( + select row_to_json(hd2) + from (select * from hearing_days hd where hd.id=h.hearing_day_id limit 1) hd2 + ) hearing_day + from + hearings h + where + h.appeal_id=a.id + ) h2 + ) hearings, + ( + select jsonb_agg(t2) from + ( + select + t.* + from tasks t + left join organizations o on o.id=t.assigned_to_id + left join users u on u.id=t.assigned_to_id + where + t.appeal_id=a.id and + t.appeal_type='Appeal' + order by updated_at desc + ) t2 + ) tasks + from appeals a + left join claimants cl on cl.decision_review_id=a.id and cl.decision_review_type='Appeal' + left join people pp on cl.participant_id=pp.participant_id + left join work_modes wm on wm.appeal_id=a.id and wm.appeal_type='Appeal' + left join decision_documents dd on dd.appeal_id=a.id and dd.appeal_type='Appeal' + left join advance_on_docket_motions aod on aod.appeal_id=a.id and aod.person_id=pp.id and aod.appeal_type='Appeal' + left join veterans v on a.veteran_file_number=v.file_number or a.veteran_file_number=v.ssn + SQL + end +end diff --git a/app/services/search_query_service/search_response.rb b/app/services/search_query_service/search_response.rb new file mode 100644 index 00000000000..09ca32c022e --- /dev/null +++ b/app/services/search_query_service/search_response.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +SearchQueryService::SearchResponse = Struct.new(:appeal, :type, :api_response) do + def filter_restricted_info!(statuses) + if statuses.include?(api_response.attributes.status) + api_response.attributes.assigned_to_location = nil + end + end +end diff --git a/app/services/search_query_service/vso_user_search_results.rb b/app/services/search_query_service/vso_user_search_results.rb new file mode 100644 index 00000000000..7b7e313b29e --- /dev/null +++ b/app/services/search_query_service/vso_user_search_results.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +class SearchQueryService::VsoUserSearchResults + def initialize(search_results:, user:) + @user = user + @search_results = search_results + + filter_restricted_results! + end + + def call + established_results.select do |result| + if result.type == :appeal + result.appeal.claimants.any? do |claimant| + vso_participant_ids.include?(poas.dig(claimant.participant_id, :participant_id)) + end + else + vso_participant_ids.include?(poas.dig(result.appeal.veteran.participant_id, :participant_id)) + end + end + end + + private + + attr_reader :search_results, :user + + RESTRICTED_STATUSES = + [ + :distributed_to_judge, + :ready_for_signature, + :on_hold, + :misc, + :unknown, + :assigned_to_attorney + ].freeze + + def filter_restricted_results! + search_results.map do |result| + result.filter_restricted_info!(RESTRICTED_STATUSES) + end + end + + def vso_participant_ids + @vso_participant_ids ||= user.vsos_user_represents.map { |poa| poa[:participant_id] } + end + + def established_results + @established_results ||= search_results.select do |result| + result.type == :legacy_appeal || result.appeal.established_at.present? + end + end + + def claimant_participant_ids + @claimant_participant_ids ||= established_results.flat_map do |result| + result.appeal.claimant_participant_ids + end.uniq + end + + def poas + Rails.logger.info "BGS Called `fetch_poas_by_participant_ids` with \"#{claimant_participant_ids.join('"')}\"" + + @poas ||= bgs.fetch_poas_by_participant_ids(claimant_participant_ids) + end + + def bgs + @bgs ||= BGSService.new + end +end diff --git a/app/workflows/case_search_results_base.rb b/app/workflows/case_search_results_base.rb index f8e4e1a0528..56c7d870c1e 100644 --- a/app/workflows/case_search_results_base.rb +++ b/app/workflows/case_search_results_base.rb @@ -56,16 +56,52 @@ def search_call ) end + def api_call + @success = valid? + + api_search_results if success + + FormResponse.new( + success: success, + errors: errors.messages[:workflow], + extra: error_status_or_api_search_results + ) + end + protected attr_reader :status, :user - def current_user_is_vso_employee? - user.vso_employee? + def search_page_json_appeals(appeals) + ama_appeals, legacy_appeals = appeals.partition { |appeal| appeal.is_a?(Appeal) } + + ama_hash = WorkQueue::AppealSearchSerializer.new( + ama_appeals, is_collection: true, params: { user: user } + ).serializable_hash + + legacy_hash = WorkQueue::LegacyAppealSearchSerializer.new( + legacy_appeals, is_collection: true, params: { user: user } + ).serializable_hash + + ama_hash[:data].concat(legacy_hash[:data]) end - def appeals - AppealFinder.new(user: user).find_appeals_for_veterans(veterans_user_can_access) + def json_appeals(appeals) + ama_appeals, legacy_appeals = appeals.partition { |appeal| appeal.is_a?(Appeal) } + + ama_hash = WorkQueue::AppealSerializer.new( + ama_appeals, is_collection: true, params: { user: user } + ).serializable_hash + + legacy_hash = WorkQueue::LegacyAppealSerializer.new( + legacy_appeals, is_collection: true, params: { user: user } + ).serializable_hash + + ama_hash[:data].concat(legacy_hash[:data]) + end + + def current_user_is_vso_employee? + user.vso_employee? end def claim_reviews @@ -77,10 +113,14 @@ def veterans [] end + def appeals + [] + end + # Users may also view appeals with appellants whom they represent. # We use this to add these appeals back into results when the user is not on the veteran's poa. def additional_appeals_user_can_access - appeals.filter do |appeal| + appeals.map(&:appeal).filter do |appeal| appeal.veteran_is_not_claimant && user.organizations.any? do |uo| appeal.representatives.include?(uo) @@ -92,34 +132,6 @@ def veterans_user_can_access @veterans_user_can_access ||= veterans.select { |veteran| access?(veteran.file_number) } end - def json_appeals(appeals) - ama_appeals, legacy_appeals = appeals.partition { |appeal| appeal.is_a?(Appeal) } - - ama_hash = WorkQueue::AppealSerializer.new( - ama_appeals, is_collection: true, params: { user: user } - ).serializable_hash - - legacy_hash = WorkQueue::LegacyAppealSerializer.new( - legacy_appeals, is_collection: true, params: { user: user } - ).serializable_hash - - ama_hash[:data].concat(legacy_hash[:data]) - end - - def search_page_json_appeals(appeals) - ama_appeals, legacy_appeals = appeals.partition { |appeal| appeal.is_a?(Appeal) } - - ama_hash = WorkQueue::AppealSearchSerializer.new( - ama_appeals, is_collection: true, params: { user: user } - ).serializable_hash - - legacy_hash = WorkQueue::LegacyAppealSearchSerializer.new( - legacy_appeals, is_collection: true, params: { user: user } - ).serializable_hash - - ama_hash[:data].concat(legacy_hash[:data]) - end - private attr_accessor :errors @@ -142,7 +154,11 @@ def valid? def validation_hook; end def access?(file_number) - !current_user_is_vso_employee? || bgs.can_access?(file_number) + return true if !current_user_is_vso_employee? + + Rails.logger.info "BGS Called `can_access?` with \"#{file_number}\"" + + bgs.can_access?(file_number) end def bgs @@ -168,10 +184,40 @@ def error_status_or_case_search_results case_search_results end + def error_status_or_api_search_results + return { status: status } unless success + + api_search_results + end + + def error_status_or_api_case_search_results + return { status: status } unless success + + api_case_search_results + end + + def api_search_results + @api_search_results ||= { + search_results: { + appeals: json_appeals(appeal_finder_appeals), + claim_reviews: claim_reviews.map(&:search_table_ui_hash) + } + } + end + + def api_case_search_results + @api_case_search_results ||= { + case_search_results: { + appeals: search_page_json_appeals(appeal_finder_appeals), + claim_reviews: claim_reviews.map(&:search_table_ui_hash) + } + } + end + def search_results @search_results ||= { search_results: { - appeals: json_appeals(appeals), + appeals: appeals.map(&:api_response), claim_reviews: claim_reviews.map(&:search_table_ui_hash) } } @@ -180,7 +226,7 @@ def search_results def case_search_results @case_search_results ||= { case_search_results: { - appeals: search_page_json_appeals(appeals), + appeals: appeals.map(&:api_response), claim_reviews: claim_reviews.map(&:search_table_ui_hash) } } diff --git a/app/workflows/case_search_results_for_caseflow_veteran_id.rb b/app/workflows/case_search_results_for_caseflow_veteran_id.rb index 3fe2b5604df..a471b76f598 100644 --- a/app/workflows/case_search_results_for_caseflow_veteran_id.rb +++ b/app/workflows/case_search_results_for_caseflow_veteran_id.rb @@ -16,6 +16,28 @@ def veterans attr_reader :caseflow_veteran_ids + def appeal_finder_appeals + AppealFinder.new(user: user).find_appeals_for_veterans(veterans_user_can_access) + end + + def search_results + @search_results ||= SearchQueryService.new( + veteran_ids: veterans_user_can_access.map(&:id) + ).search_by_veteran_ids + end + + def vso_user_search_results + SearchQueryService::VsoUserSearchResults.new(user: user, search_results: search_results).call + end + + def appeals + if user.vso_employee? + vso_user_search_results + else + search_results + end + end + def validation_hook validate_veterans_exist end diff --git a/app/workflows/case_search_results_for_docket_number.rb b/app/workflows/case_search_results_for_docket_number.rb index 6c16a50a46d..78b9dc49656 100644 --- a/app/workflows/case_search_results_for_docket_number.rb +++ b/app/workflows/case_search_results_for_docket_number.rb @@ -13,6 +13,10 @@ def claim_reviews end def appeals + SearchQueryService.new(docket_number: docket_number).search_by_docket_number + end + + def appeal_finder_appeals AppealFinder.find_appeals_by_docket_number(docket_number) end @@ -33,7 +37,7 @@ def not_found_error def veterans # Determine vet that corresponds to docket number so we can validate user can access - @file_numbers_for_appeals ||= appeals.map(&:veteran_file_number) + @file_numbers_for_appeals ||= appeals.map(&:api_response).map(&:attributes).map(&:veteran_file_number) @veterans ||= VeteranFinder.find_or_create_all(@file_numbers_for_appeals) end end diff --git a/app/workflows/case_search_results_for_veteran_file_number.rb b/app/workflows/case_search_results_for_veteran_file_number.rb index 3f0f97fe466..ba03eae1bce 100644 --- a/app/workflows/case_search_results_for_veteran_file_number.rb +++ b/app/workflows/case_search_results_for_veteran_file_number.rb @@ -23,6 +23,33 @@ def validate_file_number_or_ssn_presence @status = :bad_request end + def appeals + if user.vso_employee? + vso_user_search_results + else + search_results + end + end + + def vso_user_search_results + SearchQueryService::VsoUserSearchResults.new(user: user, search_results: search_results).call + end + + def search_results + @search_results ||= SearchQueryService.new(file_number: file_number_or_ssn).search_by_veteran_file_number + end + + def appeal_finder_appeals + AppealFinder.new(user: user).find_appeals_for_veterans(veterans_user_can_access) + end + + def file_number_or_ssn_presence + return if file_number_or_ssn + + errors.add(:workflow, missing_veteran_file_number_or_ssn_error) + @status = :bad_request + end + def missing_veteran_file_number_or_ssn_error { "title": "Veteran file number missing", diff --git a/spec/feature/queue/search_spec.rb b/spec/feature/queue/search_spec.rb index 7be69b901dd..02e393a7350 100644 --- a/spec/feature/queue/search_spec.rb +++ b/spec/feature/queue/search_spec.rb @@ -612,7 +612,9 @@ def perform_search(docket_number = appeal.docket_number) context "when backend returns non-serialized error" do it "displays generic server error message" do - allow(LegacyAppeal).to receive(:fetch_appeals_by_file_number).and_raise(StandardError) + allow_any_instance_of(SearchQueryService).to( + receive(:search_by_veteran_file_number).and_raise(StandardError) + ) visit "/search" fill_in "searchBarEmptyList", with: appeal.sanitized_vbms_id click_on "Search" @@ -655,7 +657,7 @@ def perform_search it "shows 'Withdrawn' text on search results page" do policy = instance_double(WithdrawnDecisionReviewPolicy) - allow(WithdrawnDecisionReviewPolicy).to receive(:new).with(caseflow_appeal).and_return policy + allow(WithdrawnDecisionReviewPolicy).to receive(:new).and_return policy allow(policy).to receive(:satisfied?).and_return true perform_search diff --git a/spec/fixes/assigned_to_search_results_spec.rb b/spec/fixes/assigned_to_search_results_spec.rb index 75765fdddd3..1ef26f380a1 100644 --- a/spec/fixes/assigned_to_search_results_spec.rb +++ b/spec/fixes/assigned_to_search_results_spec.rb @@ -29,7 +29,7 @@ end it "creates tasks and other records associated with a dispatched appeal" do - expect(BVAAppealStatus.new(appeal: appeal).status).to eq :unknown # We will fix this + expect(BVAAppealStatus.new(tasks: appeal.tasks).status).to eq :unknown # We will fix this expect(appeal.root_task.status).to eq "on_hold" visit "/search?veteran_ids=#{appeal.veteran.id}" @@ -57,7 +57,7 @@ } visit "/search?veteran_ids=#{appeal.veteran.id}" expect(page).to have_content("Signed") # in the "Appellant Name" column - expect(BVAAppealStatus.new(appeal: appeal).status).to eq :signed + expect(BVAAppealStatus.new(tasks: appeal.tasks).status).to eq :signed bva_dispatcher = org_dispatch_task.children.first.assigned_to expect(page).to have_content(bva_dispatcher.css_id) # in the "Assigned To" column expect(appeal.assigned_to_location).to eq bva_dispatcher.css_id @@ -65,7 +65,7 @@ BvaDispatchTask.outcode(appeal, params, bva_dispatcher) visit "/search?veteran_ids=#{appeal.veteran.id}" expect(page).to have_content("Dispatched") # in the "Appellant Name" column - expect(BVAAppealStatus.new(appeal: appeal).status).to eq :dispatched + expect(BVAAppealStatus.new(tasks: appeal.tasks).status).to eq :dispatched expect(page).to have_content("Post-decision") # in the "Assigned To" column expect(appeal.assigned_to_location).to eq "Post-decision" expect(appeal.root_task.status).to eq "completed" diff --git a/spec/fixes/backfill_early_ama_appeal_spec.rb b/spec/fixes/backfill_early_ama_appeal_spec.rb index e1ed3774706..e26da3bfe4d 100644 --- a/spec/fixes/backfill_early_ama_appeal_spec.rb +++ b/spec/fixes/backfill_early_ama_appeal_spec.rb @@ -28,7 +28,7 @@ let(:atty_draft_date) { dispatch_date - 2.hours } it "creates tasks and other records associated with a dispatched appeal" do - expect(BVAAppealStatus.new(appeal: appeal).status).to eq :unknown # We will fix this + expect(BVAAppealStatus.new(tasks: appeal.tasks).status).to eq :unknown # We will fix this expect(appeal.root_task.status).to eq "completed" # 1. Create tasks to associate appeal with a judge and attorney @@ -134,7 +134,7 @@ decision_doc = DecisionDocument.create!(params) expect(appeal.decision_document).to eq decision_doc - expect(BVAAppealStatus.new(appeal: appeal).status).to eq :dispatched + expect(BVAAppealStatus.new(tasks: appeal.tasks).status).to eq :dispatched end end end diff --git a/spec/models/serializers/work_queue/appeal_search_serializer_spec.rb b/spec/models/serializers/work_queue/appeal_search_serializer_spec.rb new file mode 100644 index 00000000000..686d9e68074 --- /dev/null +++ b/spec/models/serializers/work_queue/appeal_search_serializer_spec.rb @@ -0,0 +1,183 @@ +# frozen_string_literal: true + +require "rails_helper" + +describe WorkQueue::AppealSearchSerializer, :all_dbs do + describe "#assigned_to_location" do + context "when appeal status is distributed to judge" do + let!(:judge_user) { create(:user, :with_vacols_judge_record, full_name: "Judge Judy", css_id: "JUDGE_J") } + let(:appeal) { create(:appeal, :assigned_to_judge, associated_judge: judge_user) } + + before do + User.authenticate!(user: judge_user) + end + + subject { described_class.new(appeal, params: { user: judge_user }) } + + context "and user is a board judge" do + it "shows CSS ID" do + expect(subject.serializable_hash[:data][:attributes][:assigned_to_location]) + .to eq(appeal.assigned_to_location) + end + end + + context "when appeal status is assigned to attorney" do + let(:appeal) { create(:appeal, :at_attorney_drafting) } + let!(:attorney_user) { create(:user) } + let!(:vacols_atty) { create(:staff, :attorney_role, sdomainid: attorney_user.css_id) } + + before do + User.authenticate!(user: attorney_user) + end + + subject { described_class.new(appeal, params: { user: attorney_user }) } + + context "and user is a board attorney" do + it "shows CSS ID" do + expect(subject.serializable_hash[:data][:attributes][:assigned_to_location]) + .to eq(appeal.assigned_to_location) + end + end + end + + context "when appeal status is ready for signature" do + let!(:judge_user) { create(:user, :with_vacols_judge_record, full_name: "Judge Judy", css_id: "JUDGE_J") } + let(:appeal) { create(:appeal, :at_judge_review, associated_judge: judge_user) } + let!(:hearings_coordinator_user) do + coordinator = create(:hearings_coordinator) + HearingsManagement.singleton.add_user(coordinator) + coordinator + end + + before do + User.authenticate!(user: hearings_coordinator_user) + end + + subject { described_class.new(appeal, params: { user: hearings_coordinator_user }) } + + context "and user is a hearings coordinator" do + it "shows CSS ID" do + expect(subject.serializable_hash[:data][:attributes][:assigned_to_location]) + .to eq(appeal.assigned_to_location) + end + end + end + + context "when status is distributed to judge" do + let!(:judge_user) { create(:user, :with_vacols_judge_record, full_name: "Judge Judy", css_id: "JUDGE_J") } + let(:appeal) { create(:appeal, :assigned_to_judge, associated_judge: judge_user) } + let(:vso_user) { create(:user, :vso_role) } + + before do + User.authenticate!(user: vso_user) + end + + subject { described_class.new(appeal, params: { user: vso_user }) } + + it "does not show CSS ID to VSO user" do + expect(subject.serializable_hash[:data][:attributes][:assigned_to_location]).to be_nil + end + end + + context "when status is ready for signature" do + let!(:judge_user) { create(:user, :with_vacols_judge_record, full_name: "Judge Judy", css_id: "JUDGE_J") } + let(:appeal) { create(:appeal, :at_judge_review, associated_judge: judge_user) } + let(:vso_user) { create(:user, :vso_role) } + + before do + User.authenticate!(user: vso_user) + end + + subject { described_class.new(appeal, params: { user: vso_user }) } + + it "does not show CSS ID to VSO user" do + expect(subject.serializable_hash[:data][:attributes][:assigned_to_location]).to be_nil + end + end + + context "when status is on hold" do + let(:appeal) do + create(:appeal).tap do |appeal| + create(:timed_hold_task, parent: create(:root_task, appeal: appeal)) + end + end + let(:vso_user) { create(:user, :vso_role) } + + before do + User.authenticate!(user: vso_user) + end + + subject { described_class.new(appeal, params: { user: vso_user }) } + + it "does not show CSS ID to VSO user" do + expect(subject.serializable_hash[:data][:attributes][:assigned_to_location]).to be_nil + end + end + + context "when status is misc" do + let(:appeal) do + create(:appeal).tap do |appeal| + create(:ama_judge_dispatch_return_task, parent: create(:root_task, appeal: appeal)) + end + end + let(:vso_user) { create(:user, :vso_role) } + + before do + User.authenticate!(user: vso_user) + end + + subject { described_class.new(appeal, params: { user: vso_user }) } + + it "does not show CSS ID to VSO user" do + expect(subject.serializable_hash[:data][:attributes][:assigned_to_location]).to be_nil + end + end + + context "when status is unknown" do + let(:appeal) { create(:appeal) } + let(:vso_user) { create(:user, :vso_role) } + + before do + User.authenticate!(user: vso_user) + end + + subject { described_class.new(appeal, params: { user: vso_user }) } + + it "does not show CSS ID to VSO user" do + expect(subject.serializable_hash[:data][:attributes][:assigned_to_location]).to be_nil + end + end + + context "when status is assigned to attorney" do + let(:appeal) { create(:appeal, :at_attorney_drafting) } + let(:vso_user) { create(:user, :vso_role) } + + before do + User.authenticate!(user: vso_user) + end + + subject { described_class.new(appeal, params: { user: vso_user }) } + + it "does not show CSS ID to VSO user" do + expect(subject.serializable_hash[:data][:attributes][:assigned_to_location]).to be_nil + end + end + end + + context "when appeal status is not restricted" do + let(:appeal) { create(:appeal, :with_pre_docket_task) } + let(:vso_user) { create(:user, :vso_role) } + + before do + User.authenticate!(user: vso_user) + end + + subject { described_class.new(appeal, params: { user: vso_user }) } + + it "shows CSS ID to VSO user" do + expect(subject.serializable_hash[:data][:attributes][:assigned_to_location]) + .to eq(appeal.assigned_to_location) + end + end + end +end diff --git a/spec/services/bva_appeal_status_spec.rb b/spec/services/bva_appeal_status_spec.rb index 5bdb70c2929..8effec2eb4a 100644 --- a/spec/services/bva_appeal_status_spec.rb +++ b/spec/services/bva_appeal_status_spec.rb @@ -18,7 +18,7 @@ status = pair.first sort_key = pair.last appeal = Appeal.find(appeal_id) - appeal_status = described_class.new(appeal: appeal) + appeal_status = described_class.new(tasks: appeal.tasks) expect(appeal_status.to_s).to eq(status) expect(appeal_status.to_i).to eq(sort_key.to_i + 1) # our sort keys are 1-based diff --git a/spec/services/search_query_service_spec.rb b/spec/services/search_query_service_spec.rb new file mode 100644 index 00000000000..66fcf4ec756 --- /dev/null +++ b/spec/services/search_query_service_spec.rb @@ -0,0 +1,311 @@ +# frozen_string_literal: true + +describe "SearchQueryService" do + let(:ssn) { "146600001" } + let(:dob) { Faker::Date.in_date_period(year: 1960) } + + let(:uuid) { SecureRandom.uuid } + let(:veteran_first_name) { Faker::Name.first_name } + let(:veteran_last_name) { Faker::Name.last_name } + let(:claimant_first_name) { Faker::Name.first_name } + let(:claimant_last_name) { Faker::Name.last_name } + let(:veteran_full_name) { FullName.new(veteran_first_name, "", veteran_last_name).to_s } + let(:claimant_full_name) { FullName.new(claimant_first_name, "", claimant_last_name).to_s } + let(:docket_type) { "hearing" } + let(:docket_number) { "240111-1111" } + + let(:descision_document_attrs) do + { + decision_date: Faker::Date.between(from: 2.years.ago, to: 1.year.ago) + } + end + + context "all data in caseflow" do + context "veteran is claimant" do + let(:veteran_attrs) do + { + ssn: ssn, + file_number: ssn, + date_of_birth: dob, + date_of_death: nil, + first_name: veteran_first_name, + middle_name: nil, + last_name: veteran_last_name + } + end + + let(:veteran) { FactoryBot.create(:veteran, veteran_attrs) } + + let(:appeal_attributes) do + { + aod_based_on_age: false, + stream_docket_number: docket_number, + veteran_file_number: ssn, + veteran: veteran, + stream_type: Constants.AMA_STREAM_TYPES.original, + uuid: uuid + } + end + + let(:judge) { create(:user, :judge) } + + let!(:appeal) do + FactoryBot.create( + :appeal, + # has hearing(s) + :hearing_docket, + :held_hearing, + :tied_to_judge, + # has decision document + :dispatched, + # has issue(s) + :with_request_issues, + :with_decision_issue, + { + associated_judge: judge, + tied_judge: judge + }.merge(appeal_attributes) + ).tap do |appeal| + appeal.decision_issues.first.update( + mst_status: true, + pact_status: true + ) + appeal.hearings.first.update(updated_by: judge) + # create work mode + appeal.overtime = true + AdvanceOnDocketMotion.create( + person: appeal.claimants.first.person, + appeal: appeal + ) + end.reload + end + + context "finds by docket number" do + subject { SearchQueryService.new(docket_number: appeal.stream_docket_number) } + + it "finds by docket number" do + expect(appeal).to be_persisted + + search_results = subject.search_by_docket_number + + expect(search_results.length).to eq(1) + + result = search_results.first.api_response + + expect(result.id).to be + expect(result.type).to eq "appeal" + + attributes = result.attributes + + expect(attributes.aod).to be_truthy + expect(attributes.appellant_full_name).to eq veteran_full_name + expect(attributes.assigned_to_location).to eq appeal.assigned_to_location + expect(attributes.caseflow_veteran_id).to eq veteran.id + expect(attributes.decision_date).to eq appeal.decision_document.decision_date + expect(attributes.docket_name).to eq appeal.docket_type + expect(attributes.docket_number).to eq appeal.stream_docket_number + expect(attributes.external_id).to eq appeal.uuid + expect(attributes.hearings.length).to eq appeal.hearings.length + expect(attributes.issues.length).to eq(appeal.request_issues.length) + expect(attributes.mst).to eq appeal.decision_issues.any?(&:mst_status) + expect(attributes.pact).to eq appeal.decision_issues.any?(&:pact_status) + expect(attributes.paper_case).to be_falsy + expect(attributes.status).to eq Appeal.find(appeal.id).status.status + expect(attributes.veteran_appellant_deceased).to be_falsy + expect(attributes.veteran_file_number).to eq ssn + expect(attributes.veteran_full_name).to eq veteran_full_name + expect(attributes.contested_claim).to be_falsy + expect(attributes.withdrawn).to eq(false) + end + end + + context "finds by file number" do + subject { SearchQueryService.new(file_number: ssn) } + + it "finds by veteran file number" do + expect(appeal).to be_persisted + + search_results = subject.search_by_veteran_file_number + + expect(search_results.length).to eq(1) + + result = search_results.first.api_response + + expect(result.id).to be + expect(result.type).to eq "appeal" + end + end + + context "finds by veteran ids" do + subject { SearchQueryService.new(veteran_ids: [veteran.id]) } + + it "finds by veteran ids" do + expect(appeal).to be_persisted + + search_results = subject.search_by_veteran_ids + + expect(search_results.length).to eq(1) + + result = search_results.first.api_response + + expect(result.id).to be + expect(result.type).to eq "appeal" + end + end + end + end + + let(:veteran_address) do + { + addrs_one_txt: nil, + addrs_two_txt: nil, + addrs_three_txt: nil, + city_nm: nil, + cntry_nm: nil, + postal_cd: nil, + zip_prefix_nbr: nil, + ptcpnt_addrs_type_nm: nil + } + end + + let(:legacy_appeal) do + create( + :legacy_appeal, + vbms_id: ssn, + vacols_case: vacols_case, + veteran_address: veteran_address + ) + end + + # must be created first for legacy_appeal factory to find it + let!(:veteran) do + create( + :veteran, + file_number: ssn, + first_name: veteran_first_name, + last_name: veteran_last_name + ) + end + + let(:vacols_decision_date) { 2.weeks.ago } + let(:vacols_case_attrs) do + { + bfkey: ssn, + bfcorkey: ssn, + bfac: "1", + bfcorlid: "100000099", + bfcurloc: "CASEFLOW", + bfddec: vacols_decision_date, + bfmpro: "ACT" + + # bfregoff: "RO18", + # bfdloout: "2024-03-26T11:13:32.000Z", + # bfcallup: "", + # bfhr: "2", + # bfdocind: "T", + } + end + + let(:issues_count) { 5 } + let(:vacols_case_issues) do + create_list( + :case_issue, + issues_count, + isspact: "Y", + issmst: "Y" + ) + end + + let(:hearings_count) { 5 } + let(:vacols_case_hearings) do + create_list( + :case_hearing, + hearings_count + ) + end + + let(:vacols_correspondent) do + create(:correspondent, vacols_correspondent_attrs) + end + + let(:vacols_folder) do + build(:folder) + end + + let(:vacols_case) do + create( + :case, + { + correspondent: vacols_correspondent, + case_issues: vacols_case_issues, + case_hearings: vacols_case_hearings, + folder: vacols_folder + }.merge(vacols_case_attrs) + ) + end + + context "when appeal is a legacy appeal with data in vacols and caseflow" do + context "when veteran is claimant" do + let(:vacols_correspondent_attrs) do + { + sspare2: veteran_first_name, + sspare1: veteran_last_name, + snamel: veteran_last_name, + snamef: veteran_first_name, + stafkey: ssn + } + end + + let!(:claimant) do + create( + :claimant, + type: "VeteranClaimant", + decision_review: legacy_appeal + ) + end + + subject { SearchQueryService.new(file_number: ssn) } + + it "finds by file number" do + search_results = subject.search_by_veteran_file_number + result = search_results.first.api_response + + expect(result.id).to be + expect(result.type).to eq "legacy_appeal" + + attributes = result.attributes + expect(attributes.docket_name).to eq "legacy" + expect(attributes.aod).to be_falsy + expect(attributes.appellant_full_name).to eq veteran_full_name + expect(attributes.assigned_to_location).to eq legacy_appeal.assigned_to_location + expect(attributes.caseflow_veteran_id).to eq veteran.id + expect(attributes.decision_date).to eq AppealRepository.normalize_vacols_date(vacols_decision_date) + expect(attributes.docket_name).to eq "legacy" + expect(attributes.docket_number).to eq vacols_folder.tinum + expect(attributes.external_id).to eq vacols_case.id + expect(attributes.hearings.length).to eq hearings_count + expect(attributes.issues.length).to eq issues_count + expect(attributes.mst).to be_truthy + expect(attributes.pact).to be_truthy + expect(attributes.paper_case).to eq "Paper" + expect(attributes.status).to eq "Active" + expect(attributes.veteran_appellant_deceased).to be_falsy + expect(attributes.veteran_file_number).to eq ssn + expect(attributes.veteran_full_name).to eq veteran_full_name + expect(attributes.withdrawn).to be_falsy + end + + context "finds by veteran ids" do + subject { SearchQueryService.new(veteran_ids: [veteran.id]) } + + it "finds by veteran ids" do + search_results = subject.search_by_veteran_ids + result = search_results.first.api_response + + expect(result.id).to be + expect(result.type).to eq "legacy_appeal" + end + end + end + end +end From 3f08895f8635d30ac281946dc85cf6ccf7226a56 Mon Sep 17 00:00:00 2001 From: Craig Reese <109101548+craigrva@users.noreply.github.com> Date: Mon, 16 Sep 2024 15:34:44 -0500 Subject: [PATCH 6/9] APPEALS-57167: Incorrect distribution of CAVC Appeals with Hearing held after Remand (#22698) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Calvin/APPEALS-43852-cavc-levers (#21441) * enabled cavc affinity levers in UI * updated rspec * [APPEALS-43849]Update CAVC Affinity Implementation for AMA Dockets toā€¦ (#21456) * [APPEALS-43849]Update CAVC Affinity Implementation for AMA Dockets to Account for Omit/Infinity * Affinity rules applied to non genpop * Addressed comments * Addressed comments * fixed rubocop issues + added clarity to where clause --------- Co-authored-by: Calvin * APPEALS-44956: Add AppealAffinity model and database table (#21526) * add migration for appeal_affinities * add AppealAffinity model and associations, update migration for new column * update index to be unique * add factory, add tests * add factory traits to appeal and case for appeal affinities * add combination trait to appeal factory * add appeal_affinity to skipped associations in ETL reporting * add a validation, test * Craig/appeals 44958 (#21564) * add new job, update affinity model validation and after save hook * add update from push job * fix job extending distribution scopes * add with appeal affinities to distribution scopes * typo * add error handling, add test file * add distributed case factory, refactor naming in job * fix factories, added tests * fix migration for null affinity start date column * fixes, added tests * more test updates * add return in job if no query results, tests for no query results * add test for after_save hook adding dist task instructions * set start dist job to queue affinity job after running * fix update job and start dist job spec * queue affinity update job from push job * code clarity * fix judge in seed file * remove comment, fix hearing factory, disable some seeds for testing * add more tests * test refactor * update appeals for dist query to add affinity start, add seed file, fix hearing factory, add stat to dist factory * disable new seed on reset * update seed file with vet names, add another seed category * fix distirbuted case factory? * actually fix GHA runs * lint, test fixes * change constants in new job * [Appeals 43850] Update Legacy Docket Queries to Account for the Previous Decision Judge and Type Action (#21556) * test changes for seans ticket * test changes * added joins to all required methods * fixed lint * fixed column ambiguity errors * cleaned up naming scheme * Documentation for JOIN_PREVIOUS_APPEALS constant --------- Co-authored-by: Calvin * APPEALS-44959: Modify affinity date checks to use appeal_affinity (#21611) * swap distribution queries from distribution_task to appeal_affinities * update seed files to use appeal affinities instead of distribution task * clean up seed file method names * add missing Timecop.return in ama affinity case seed * fix name of a method in a seed file * remove references to distribution task in distribution scopes * fix push priority job tests * fix naming of args in one of the seed files * fix user seed, fix date format in distribution task instructions * fix tests for date format update * APPEALS-44187: Factory Bot Additions (#21438) * AC1: values for bfddec and bfmpro * AC2: case issues updated to '3' * AC3-4: attorney and judge additions * ac 5: bfdpdcn addition * AC6: case type action addition * ac7: new folder match to original * ac8: case issues set to original * AC 3/4: added associations to original * ac3/4: updated logic to handle no args * ac3/4: return sattyid * ac7:updating folder assignment * ac7: added bfkey to except block * ac7/8: update to case issue list and validations dismissed * removed byebug * ac7: added 'ticknum' to except * lint fixes * lint fixes * lint fixes * lint fixes * nested trait into form_9 factory * new addtions * added .save to case issues * resolving correspondent and titrnum associations * fixed bfdc typo * factory additions * added ssn to associated corr. * removed transient and added .save * added after create to corr factory * veteran lookup check prior to create * committing missed 'end' * moved over veteran create to case fact. * move corr. association field to case fact. * lint issues + corres. save * Calvin/APPEALS-44957-rake-affinity (#21577) * grabbed receipt dates from distributed cases * refactored for functionality + added method to grab appeals that match * using receipt date, get all related appeals * added update/creation plus cleaned prior imple. * gets most recent distributed case receipt_date * skips if receipt_date is nil for performance * if appeal affinity is nil, it will now be updated * created spec file * fixed non ready appeals * updated query to match new AC * removing comment * testing for each docket * updated spec file * added new tests to rspec * updated start date to receipt date instead of Time.now * fixed date/time rspec errors * added rails logger to know when rake task has finished * added tag for rails log * removed nonpriority dockets for direct_review and evidence_submission * fixed lint issue * fixed flaky spec test * limits distributed cases query to within the last week * APPEALS-46016: Add Affinity Start Date to the Explain Page (#21660) * add affinity start date to explain page * add feature test to verify dates display * Sudhir/appeals 43851 (#21613) * Implement CAVC + AOD Affinity Lever for AMA Dockets * addressed comments * addressed comments * Addressed comments * added cavc_aod_affinity in case distribution lever model * addressed comments * addressed comments * updated specs * Updated specs * specs changes reverted * ama_aod and ama_non_aod queries updated * change the assertion in docket spec * Craig/appeals 46196 (#21689) * fix query, tested locally * add basic test to verify csv downloads aren't broken * APPEALS-43851: Add test to validate CAVC+AOD behavior on hearing docket appeals affinities (#21678) * add test to validate cavc+aod on hearing docket appeals * lint, test case_docket_spec fix * modify case_docket_spec again * more test fix testing * attempt to fix test again * test removing prev appeals from nonpriority queries * more test tests * feature toggle change in test * reorder new portions of query * remove unused portions of queries in case_docket * revert unneeded change to query order * revert unneeded change to query order * update rake task and spec (#21731) * APPEALS-46325: Add Seeds for AOD Appeals and Update Dates to Match CAVC (#21730) * add aod hearing cases to ama affinity cases seed * fix lever spec * APPEALS-45148: Hook to clear saved affinity date (#21623) * initial imp. idea * AC1: check for affinity_start_date on assignment * AC2/3: update affinity start date w/ instr. * updates to naming, instructions, and hook logic * updates after review * rspec coverage and addtional condition * removed unused identifier * removed reduntant 'self's * added update on actual AA record * updated to save aa record and addtional rspec * added change to assignment on no record test * check for assignment * addd update to 'on_hold' status * public method to handle legacy affinity appeals * added .reload to :with_affinity_appeal * added .reload to :ready_for_distribution * updates to pass explain_spec * switched boolean values * typo * readujsted order on :create for affinity appeal * removed after(:create) * testing rspec by readding after :create * reloading in assertation * addressing lint errors * fix seeds/users_spec * add case dist lever to new tests (#21776) * fix tests, add lever to factory, fix dist scopes (#21779) * fix rubocop warning * Acd/appeals 43853 43854 (#21971) * Calvin/appeals 43853 (#21723) * initial updates * removing unnecessary variable * focused in on priority * removing non priority stuff * added general comments * added BFAC and AOD to cavc aod lever query * adding judge vacols id to query * aod affinity_start_date filter initial changes * fixed sorting * fixed rubocop issues * updated filter method * error handling * added ineligibility to queries for PREV_DECIDING_JUDGE * fixed SQL query + added comments * added exclude from affinity check into the case docket queries * error handling + fixing sql queries * rejects appeals without affinity_start_dates and nonmatching judges * fixing rubocop offenses * fixed inconsistencies between methods * fixed conditions for rejecting appeals * refactored cavc aod affinity filter to make it much easier to read * refactored code to account for AC6 * error handling for empty exclude from affinity * reverted next if block to old logic to ensure it works * added PREV_DEC_JUDGE is not null * case.rb factory changes * added more options to legacy_cavc_appeal creation * cleaned up code for simpler reading * fix for aod legacy cavc creation * added tied to option to legacy cavc appeal factory * limits are now handled correctly in query * replaced return false to next if, as return false was causing unexpected behavior * fix rspecs + one edge case * added cavc aod lever creation to rspecs * removed bfac and aod from nonpriority query * cavc aod appeals w/excluded judges are now properly being filtered * refactored excluded judges check * added to old query to fix rspec errors * modified case factory bot * query now handles when prev_deciding_judge is nil * removed unnecessary condition * fixed case factory to now have tied_to attatched to orig appeal * fixed next if block within filter * handles omit scenarios + correctly rejects with next * working on rspec (still failing) * fix for ineligble VLJ when infinite * fixed rspec suite for cavc aod filters * fixed omit scenario in cavc aod affinity filter * consolidation & readability refactor * rubocop fixes * fixed spec error * Implement CAVC Affinity for Legacy Docket (#21706) * Implement CAVC Affinity for Legacy Docket * addressed comments * Added BFAC in the query * code changes for affinitty date * Added affinity code * code refactor and removed non priority code changes * fixed syntax change * Addressed comments * refactor cavc affinity filter * refactored code * code refactor * code refactor * Updated existing specs * code refactor * Added new rspecs * code refactor and added test cases * code refactor * added test cases * fixes push_priority_spec * fixed rubocop issues * rubocop issue fixed * refactored code to make it easier to understand * refactored + fixed rspec and lint errors --------- Co-authored-by: Calvin Co-authored-by: calvincostaBAH <108481161+calvincostaBAH@users.noreply.github.com> * basic creation of legacy affinity cases seed data * bug fixes, added bfcorlid with veterans, fully runs now * added new appeals for affinity_and_tied_to_judge * made data have realistic bfcorlids * changes document sequence to use less digits * added new file numbers for tied_to cases to make them easier to identify --------- Co-authored-by: samasudhirreddy <108430298+samasudhirreddy@users.noreply.github.com> * APPEALS-50692: Update Appeals Ready to Distribute CSV to include CAVC remand original judge (#22070) * CSV download functional * add tests, fix CSV query in CaseDocket * fix lint * Calvin/appeals 44313 (#22119) * initial seed data file * added legacy cavc and cavc aod affinity cases * update * fixed tied to for legacy appeals * added AOD versions of appeals * small lint fixes * ensured AOD cases for legacy hearings with exluded or ineligible judge * added vacols staff record creation for users without it * APPEALS-47741: Update the UpdateAppealAffinityDatesJob to add appeal_affinity records for Legacy Appeals (#22023) * AC1: changes and respective tests * adjustements after refactoring * identifier mismatch * name update * name update * added appeal affinity filter * updated comment * remove byebug * update rspec to handle hash input * added no start date test case * updated process method test * removed 'todo' comment * dried up query string * aligned conditions * update to hash quotations * update to hash quotations * added legacy to priority receipt dates from dist. * moved append to resulting list * added legacy receipt date to push job hash * uncomment call to legacy * updated dist.id to @dist_id * uncomment call to process legacy appeals * handling update to legacy docket type * current rspec status * fixed typo * fix rspec * legacy spec additions * legacy spec additions * added legacy dist. case factory * removed vacols_judge ref * updates for spec * final review * removed comment * rubocop fixes * fix rubocop warnings (#22225) * Fix rubocop and tests (#22231) * Calvin/appeals 52551 (#22293) * age_of_n_oldest_priority_appeals_available_to_judge time out changes * fixed timeout issue for distribute_priority_appeals * fixed lint errors * Craig/case docket optimization (#22294) * age_of_n_oldest_priority_appeals_available_to_judge time out changes * fixed timeout issue for distribute_priority_appeals * optimize case docket priority distribution methods * fix das deprecation distribution spec --------- Co-authored-by: Calvin * Update admin_ui_spec.rb for CAVC levers being enabled * APPEALS-53993: Update Implementation of Legacy Appeals with Hearing Held (#22473) * implement new hearing requirement and test * first pass at seed data for testing * add new case to test and seed, fix typo and comments * update for new 8.2.1.1 requirement * fix rubocop warning * fix error in seed file * add new affinity scenario to seed data * make some seed methods private * fix rubocop warnings * added test cases for infinite and omit levers with desired outcomes * add check for hearing date to levers being infinite * fix lint * restore Gemfile.lock platform to ruby --------- Co-authored-by: calvincostaBAH <108481161+calvincostaBAH@users.noreply.github.com> Co-authored-by: samasudhirreddy <108430298+samasudhirreddy@users.noreply.github.com> Co-authored-by: Calvin Co-authored-by: seanrpa <155660052+seanrpa@users.noreply.github.com> Co-authored-by: Isaiah Saucedo --- Gemfile.lock | 6 ++- app/models/vacols/case_docket.rb | 8 +++- spec/models/vacols/case_docket_spec.rb | 51 +++++++++++++++++++++++++- 3 files changed, 59 insertions(+), 6 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 3377b991095..2632deaf2e2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1710,6 +1710,7 @@ GEM mime-types-data (3.2019.1009) mini_histogram (0.3.1) mini_mime (1.1.2) + mini_portile2 (2.8.7) minitest (5.19.0) moment_timezone-rails (0.5.0) momentjs-rails (2.29.4.1) @@ -1729,7 +1730,8 @@ GEM net-protocol nio4r (2.5.9) no_proxy_fix (0.1.2) - nokogiri (1.15.5-x86_64-darwin) + nokogiri (1.15.5) + mini_portile2 (~> 2.8.2) racc (~> 1.4) nori (2.6.0) notiffany (0.1.1) @@ -2126,7 +2128,7 @@ GEM ziptz (2.1.6) PLATFORMS - x86_64-darwin-22 + ruby DEPENDENCIES aasm (= 4.11.0) diff --git a/app/models/vacols/case_docket.rb b/app/models/vacols/case_docket.rb index 7810dfbe51b..65c08422380 100644 --- a/app/models/vacols/case_docket.rb +++ b/app/models/vacols/case_docket.rb @@ -835,7 +835,7 @@ def self.cavc_affinity_filter(appeals, judge_sattyid, cavc_affinity_lever_value, reject_due_to_affinity?(appeal, cavc_affinity_lever_value) elsif cavc_affinity_lever_value == Constants.ACD_LEVERS.infinite - next if ineligible_judges_sattyids&.include?(appeal["vlj"]) + next if hearing_judge_ineligible_with_no_hearings_after_decision(appeal) appeal["prev_deciding_judge"] != judge_sattyid elsif cavc_affinity_lever_value == Constants.ACD_LEVERS.omit @@ -867,7 +867,7 @@ def self.cavc_aod_affinity_filter(appeals, judge_sattyid, cavc_aod_affinity_leve reject_due_to_affinity?(appeal, cavc_aod_affinity_lever_value) elsif cavc_aod_affinity_lever_value == Constants.ACD_LEVERS.infinite - next if ineligible_judges_sattyids&.include?(appeal["vlj"]) + next if hearing_judge_ineligible_with_no_hearings_after_decision(appeal) appeal["prev_deciding_judge"] != judge_sattyid elsif cavc_aod_affinity_lever_value == Constants.ACD_LEVERS.omit @@ -921,6 +921,10 @@ def self.reject_due_to_affinity?(appeal, lever) .affinity_start_date > lever.to_i.days.ago) end + def self.hearing_judge_ineligible_with_no_hearings_after_decision(appeal) + ineligible_judges_sattyids&.include?(appeal["vlj"]) && !appeal_has_hearing_after_previous_decision?(appeal) + end + def self.ineligible_judges_sattyids Rails.cache.fetch("case_distribution_ineligible_judges")&.pluck(:sattyid)&.reject(&:blank?) || [] end diff --git a/spec/models/vacols/case_docket_spec.rb b/spec/models/vacols/case_docket_spec.rb index 3af5052a634..073668e37f6 100644 --- a/spec/models/vacols/case_docket_spec.rb +++ b/spec/models/vacols/case_docket_spec.rb @@ -1110,12 +1110,13 @@ def create_case_hearing(original_case, hearing_judge) c end - it "considers cases tied to a judge if they held a hearing after the previous case was decided" do + it "cases are tied to the judge who held a hearing after the previous case was decided", :aggregate_failures do IneligibleJudgesJob.perform_now + # For case distribution levers set to a value + new_hearing_judge_cases = VACOLS::CaseDocket.distribute_priority_appeals(new_hearing_judge, "any", 100, true) tied_judge_cases = VACOLS::CaseDocket.distribute_priority_appeals(tied_judge_caseflow, "any", 100, true) other_judge_cases = VACOLS::CaseDocket.distribute_priority_appeals(other_judge_caseflow, "any", 100, true) - new_hearing_judge_cases = VACOLS::CaseDocket.distribute_priority_appeals(new_hearing_judge, "any", 100, true) expect(new_hearing_judge_cases.map { |c| c["bfkey"] }.sort) .to match_array([ @@ -1131,6 +1132,52 @@ def create_case_hearing(original_case, hearing_judge) .to match_array([ case_7, case_8, case_9, case_10, case_12, case_13 ].map { |c| (c["bfkey"].to_i + 1).to_s }.sort) + + # For case distribution levers set to infinite + CaseDistributionLever.find_by(item: "cavc_affinity_days").update!(value: "infinite") + CaseDistributionLever.find_by(item: "cavc_aod_affinity_days").update!(value: "infinite") + + new_hrng_judge_infinite = VACOLS::CaseDocket.distribute_priority_appeals(new_hearing_judge, "any", 100, true) + tied_judge_infinite = VACOLS::CaseDocket.distribute_priority_appeals(tied_judge_caseflow, "any", 100, true) + other_judge_infinite = VACOLS::CaseDocket.distribute_priority_appeals(other_judge_caseflow, "any", 100, true) + + expect(new_hrng_judge_infinite.map { |c| c["bfkey"] }.sort) + .to match_array([ + case_1, case_2, case_3, case_4, case_5, case_10, case_12 + ].map { |c| (c["bfkey"].to_i + 1).to_s }.sort) + + expect(tied_judge_infinite.map { |c| c["bfkey"] }.sort) + .to match_array([ + case_6, case_9, case_10, case_11, case_12, case_13 + ].map { |c| (c["bfkey"].to_i + 1).to_s }.sort) + + expect(other_judge_infinite.map { |c| c["bfkey"] }.sort) + .to match_array([ + case_7, case_8, case_10, case_12 + ].map { |c| (c["bfkey"].to_i + 1).to_s }.sort) + + # For case distribution levers set to omit + CaseDistributionLever.find_by(item: "cavc_affinity_days").update!(value: "omit") + CaseDistributionLever.find_by(item: "cavc_aod_affinity_days").update!(value: "omit") + + new_hearing_judge_omit = VACOLS::CaseDocket.distribute_priority_appeals(new_hearing_judge, "any", 100, true) + tied_judge_omit = VACOLS::CaseDocket.distribute_priority_appeals(tied_judge_caseflow, "any", 100, true) + other_judge_omit = VACOLS::CaseDocket.distribute_priority_appeals(other_judge_caseflow, "any", 100, true) + + expect(new_hearing_judge_omit.map { |c| c["bfkey"] }.sort) + .to match_array([ + case_1, case_2, case_3, case_4, case_5, case_7, case_8, case_9, case_10, case_11, case_12, case_13 + ].map { |c| (c["bfkey"].to_i + 1).to_s }.sort) + + expect(tied_judge_omit.map { |c| c["bfkey"] }.sort) + .to match_array([ + case_6, case_7, case_8, case_9, case_10, case_11, case_12, case_13 + ].map { |c| (c["bfkey"].to_i + 1).to_s }.sort) + + expect(other_judge_omit.map { |c| c["bfkey"] }.sort) + .to match_array([ + case_7, case_8, case_9, case_10, case_11, case_12, case_13 + ].map { |c| (c["bfkey"].to_i + 1).to_s }.sort) end end end From acd782803750bfd4ddb9d63badd81fa19f5a21ec Mon Sep 17 00:00:00 2001 From: Marc Steele <71673522+msteele96@users.noreply.github.com> Date: Mon, 16 Sep 2024 17:11:43 -0400 Subject: [PATCH 7/9] Feature/APPEALS-50887 (#22844) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Calvin/APPEALS-43852-cavc-levers (#21441) * enabled cavc affinity levers in UI * updated rspec * [APPEALS-43849]Update CAVC Affinity Implementation for AMA Dockets toā€¦ (#21456) * [APPEALS-43849]Update CAVC Affinity Implementation for AMA Dockets to Account for Omit/Infinity * Affinity rules applied to non genpop * Addressed comments * Addressed comments * fixed rubocop issues + added clarity to where clause --------- Co-authored-by: Calvin * APPEALS-44956: Add AppealAffinity model and database table (#21526) * add migration for appeal_affinities * add AppealAffinity model and associations, update migration for new column * update index to be unique * add factory, add tests * add factory traits to appeal and case for appeal affinities * add combination trait to appeal factory * add appeal_affinity to skipped associations in ETL reporting * add a validation, test * Craig/appeals 44958 (#21564) * add new job, update affinity model validation and after save hook * add update from push job * fix job extending distribution scopes * add with appeal affinities to distribution scopes * typo * add error handling, add test file * add distributed case factory, refactor naming in job * fix factories, added tests * fix migration for null affinity start date column * fixes, added tests * more test updates * add return in job if no query results, tests for no query results * add test for after_save hook adding dist task instructions * set start dist job to queue affinity job after running * fix update job and start dist job spec * queue affinity update job from push job * code clarity * fix judge in seed file * remove comment, fix hearing factory, disable some seeds for testing * add more tests * test refactor * update appeals for dist query to add affinity start, add seed file, fix hearing factory, add stat to dist factory * disable new seed on reset * update seed file with vet names, add another seed category * fix distirbuted case factory? * actually fix GHA runs * lint, test fixes * change constants in new job * [Appeals 43850] Update Legacy Docket Queries to Account for the Previous Decision Judge and Type Action (#21556) * test changes for seans ticket * test changes * added joins to all required methods * fixed lint * fixed column ambiguity errors * cleaned up naming scheme * Documentation for JOIN_PREVIOUS_APPEALS constant --------- Co-authored-by: Calvin * APPEALS-44959: Modify affinity date checks to use appeal_affinity (#21611) * swap distribution queries from distribution_task to appeal_affinities * update seed files to use appeal affinities instead of distribution task * clean up seed file method names * add missing Timecop.return in ama affinity case seed * fix name of a method in a seed file * remove references to distribution task in distribution scopes * fix push priority job tests * fix naming of args in one of the seed files * fix user seed, fix date format in distribution task instructions * fix tests for date format update * APPEALS-44187: Factory Bot Additions (#21438) * AC1: values for bfddec and bfmpro * AC2: case issues updated to '3' * AC3-4: attorney and judge additions * ac 5: bfdpdcn addition * AC6: case type action addition * ac7: new folder match to original * ac8: case issues set to original * AC 3/4: added associations to original * ac3/4: updated logic to handle no args * ac3/4: return sattyid * ac7:updating folder assignment * ac7: added bfkey to except block * ac7/8: update to case issue list and validations dismissed * removed byebug * ac7: added 'ticknum' to except * lint fixes * lint fixes * lint fixes * lint fixes * nested trait into form_9 factory * new addtions * added .save to case issues * resolving correspondent and titrnum associations * fixed bfdc typo * factory additions * added ssn to associated corr. * removed transient and added .save * added after create to corr factory * veteran lookup check prior to create * committing missed 'end' * moved over veteran create to case fact. * move corr. association field to case fact. * lint issues + corres. save * Calvin/APPEALS-44957-rake-affinity (#21577) * grabbed receipt dates from distributed cases * refactored for functionality + added method to grab appeals that match * using receipt date, get all related appeals * added update/creation plus cleaned prior imple. * gets most recent distributed case receipt_date * skips if receipt_date is nil for performance * if appeal affinity is nil, it will now be updated * created spec file * fixed non ready appeals * updated query to match new AC * removing comment * testing for each docket * updated spec file * added new tests to rspec * updated start date to receipt date instead of Time.now * fixed date/time rspec errors * added rails logger to know when rake task has finished * added tag for rails log * removed nonpriority dockets for direct_review and evidence_submission * fixed lint issue * fixed flaky spec test * limits distributed cases query to within the last week * APPEALS-46016: Add Affinity Start Date to the Explain Page (#21660) * add affinity start date to explain page * add feature test to verify dates display * Sudhir/appeals 43851 (#21613) * Implement CAVC + AOD Affinity Lever for AMA Dockets * addressed comments * addressed comments * Addressed comments * added cavc_aod_affinity in case distribution lever model * addressed comments * addressed comments * updated specs * Updated specs * specs changes reverted * ama_aod and ama_non_aod queries updated * change the assertion in docket spec * Craig/appeals 46196 (#21689) * fix query, tested locally * add basic test to verify csv downloads aren't broken * APPEALS-43851: Add test to validate CAVC+AOD behavior on hearing docket appeals affinities (#21678) * add test to validate cavc+aod on hearing docket appeals * lint, test case_docket_spec fix * modify case_docket_spec again * more test fix testing * attempt to fix test again * test removing prev appeals from nonpriority queries * more test tests * feature toggle change in test * reorder new portions of query * remove unused portions of queries in case_docket * revert unneeded change to query order * revert unneeded change to query order * update rake task and spec (#21731) * APPEALS-46325: Add Seeds for AOD Appeals and Update Dates to Match CAVC (#21730) * add aod hearing cases to ama affinity cases seed * fix lever spec * APPEALS-45148: Hook to clear saved affinity date (#21623) * initial imp. idea * AC1: check for affinity_start_date on assignment * AC2/3: update affinity start date w/ instr. * updates to naming, instructions, and hook logic * updates after review * rspec coverage and addtional condition * removed unused identifier * removed reduntant 'self's * added update on actual AA record * updated to save aa record and addtional rspec * added change to assignment on no record test * check for assignment * addd update to 'on_hold' status * public method to handle legacy affinity appeals * added .reload to :with_affinity_appeal * added .reload to :ready_for_distribution * updates to pass explain_spec * switched boolean values * typo * readujsted order on :create for affinity appeal * removed after(:create) * testing rspec by readding after :create * reloading in assertation * addressing lint errors * fix seeds/users_spec * add case dist lever to new tests (#21776) * fix tests, add lever to factory, fix dist scopes (#21779) * fix rubocop warning * Acd/appeals 43853 43854 (#21971) * Calvin/appeals 43853 (#21723) * initial updates * removing unnecessary variable * focused in on priority * removing non priority stuff * added general comments * added BFAC and AOD to cavc aod lever query * adding judge vacols id to query * aod affinity_start_date filter initial changes * fixed sorting * fixed rubocop issues * updated filter method * error handling * added ineligibility to queries for PREV_DECIDING_JUDGE * fixed SQL query + added comments * added exclude from affinity check into the case docket queries * error handling + fixing sql queries * rejects appeals without affinity_start_dates and nonmatching judges * fixing rubocop offenses * fixed inconsistencies between methods * fixed conditions for rejecting appeals * refactored cavc aod affinity filter to make it much easier to read * refactored code to account for AC6 * error handling for empty exclude from affinity * reverted next if block to old logic to ensure it works * added PREV_DEC_JUDGE is not null * case.rb factory changes * added more options to legacy_cavc_appeal creation * cleaned up code for simpler reading * fix for aod legacy cavc creation * added tied to option to legacy cavc appeal factory * limits are now handled correctly in query * replaced return false to next if, as return false was causing unexpected behavior * fix rspecs + one edge case * added cavc aod lever creation to rspecs * removed bfac and aod from nonpriority query * cavc aod appeals w/excluded judges are now properly being filtered * refactored excluded judges check * added to old query to fix rspec errors * modified case factory bot * query now handles when prev_deciding_judge is nil * removed unnecessary condition * fixed case factory to now have tied_to attatched to orig appeal * fixed next if block within filter * handles omit scenarios + correctly rejects with next * working on rspec (still failing) * fix for ineligble VLJ when infinite * fixed rspec suite for cavc aod filters * fixed omit scenario in cavc aod affinity filter * consolidation & readability refactor * rubocop fixes * fixed spec error * Implement CAVC Affinity for Legacy Docket (#21706) * Implement CAVC Affinity for Legacy Docket * addressed comments * Added BFAC in the query * code changes for affinitty date * Added affinity code * code refactor and removed non priority code changes * fixed syntax change * Addressed comments * refactor cavc affinity filter * refactored code * code refactor * code refactor * Updated existing specs * code refactor * Added new rspecs * code refactor and added test cases * code refactor * added test cases * fixes push_priority_spec * fixed rubocop issues * rubocop issue fixed * refactored code to make it easier to understand * refactored + fixed rspec and lint errors --------- Co-authored-by: Calvin Co-authored-by: calvincostaBAH <108481161+calvincostaBAH@users.noreply.github.com> * basic creation of legacy affinity cases seed data * bug fixes, added bfcorlid with veterans, fully runs now * added new appeals for affinity_and_tied_to_judge * made data have realistic bfcorlids * changes document sequence to use less digits * added new file numbers for tied_to cases to make them easier to identify --------- Co-authored-by: samasudhirreddy <108430298+samasudhirreddy@users.noreply.github.com> * APPEALS-50692: Update Appeals Ready to Distribute CSV to include CAVC remand original judge (#22070) * CSV download functional * add tests, fix CSV query in CaseDocket * fix lint * Remove transaction_wrapper * Remove extra newline * Calvin/appeals 44313 (#22119) * initial seed data file * added legacy cavc and cavc aod affinity cases * update * fixed tied to for legacy appeals * added AOD versions of appeals * small lint fixes * ensured AOD cases for legacy hearings with exluded or ineligible judge * added vacols staff record creation for users without it * APPEALS-51045: Remove ReceiveNotificationJob (#22207) Co-authored-by: nhansen3 * MattT/APPEALS-51115: Initialize FIFO SQS Queues Locally (#22182) * Set local env to utilize FIFO queues where appropriate FIFO queues are configured with the same attributes as our queues in higher environments. * Resolve failing test * Remove defunct test * Update make commands --------- Co-authored-by: Matthew Thornton * APPEALS-47741: Update the UpdateAppealAffinityDatesJob to add appeal_affinity records for Legacy Appeals (#22023) * AC1: changes and respective tests * adjustements after refactoring * identifier mismatch * name update * name update * added appeal affinity filter * updated comment * remove byebug * update rspec to handle hash input * added no start date test case * updated process method test * removed 'todo' comment * dried up query string * aligned conditions * update to hash quotations * update to hash quotations * added legacy to priority receipt dates from dist. * moved append to resulting list * added legacy receipt date to push job hash * uncomment call to legacy * updated dist.id to @dist_id * uncomment call to process legacy appeals * handling update to legacy docket type * current rspec status * fixed typo * fix rspec * legacy spec additions * legacy spec additions * added legacy dist. case factory * removed vacols_judge ref * updates for spec * final review * removed comment * rubocop fixes * fix rubocop warnings (#22225) * APPEALS-51847: Institute Database Migration for Establishing 'sms_status_reason' and 'email_status_reason' Columns (#22208) * adding and running migration * adding comments to migration and re running db:migrate * pushing up original scheam * committing schema * small edit to migration and deleting unnecessary columns from schema --------- Co-authored-by: nhansen3 Co-authored-by: Matthew Thornton <99351305+ThorntonMatthew@users.noreply.github.com> * Fix rubocop and tests (#22231) * removing job and all mentions of job (#22212) Co-authored-by: nhansen3 Co-authored-by: Matthew Thornton <99351305+ThorntonMatthew@users.noreply.github.com> * MattT/APPEALS-51059: Alter Select Quarterly Statuses (#22239) * Alter status messages * Save more info in fake response * Swap keys * Add missing param * Test fixes. Also, setting appeal_docketed to false whenever appeals are decided or cancelled * Fix fasterer issue --------- Co-authored-by: Matthew Thornton * MattT/APPEALS-51101: Rewrite ProcessNotificationStatusUpdateJob to Consume Messages from an SQS Queue (#22247) * Update fake response body * Try enabling localstack in CI * Init SqsService and alter most/all of the ProcessNotificationStatusUpdatesJob * Create SQS queues in test env * Change port formatting * Mount docker socket * Try localstack hostname * Wait for localstack in CI * Add more yarddoc comments * Log errors and number of messages processed * Add custom error classes * Add yarddoc comments to SqsService class * Create SqsService spec file * Redo much of ProcessNotificationStatusUpdatesJob's spec file * Disable line length checks for yarddoc comments --------- Co-authored-by: Matthew Thornton * Calvin/appeals 52551 (#22293) * age_of_n_oldest_priority_appeals_available_to_judge time out changes * fixed timeout issue for distribute_priority_appeals * fixed lint errors * Craig/case docket optimization (#22294) * age_of_n_oldest_priority_appeals_available_to_judge time out changes * fixed timeout issue for distribute_priority_appeals * optimize case docket priority distribution methods * fix das deprecation distribution spec --------- Co-authored-by: Calvin * APPEALS-51087: Rewrite Api::V1::VaNotifyController#notification_update Controller Action to No Longer Utilize Redis (#22295) * initial push * rubocop'n * fixing tests? * rubocop'n * addressing most of matt's comments * removing yard doc files and keeping comment * rubocop --------- Co-authored-by: nhansen3 * Update admin_ui_spec.rb for CAVC levers being enabled * Remove extra attribute * Fix incorrect log counts * Fix log statement * Fix typo * APPEALS-53603: Modify logic to always show recipient information if present (#22414) * initial commit * making rspec tests happy * linting * adding tests back and refactoring * small char error --------- Co-authored-by: nhansen3 * Sync decided appeals with states table (#22434) * APPEALS-52882 Sync decided appeals decision_mailed status * APPEALS-52882 Fix lint issues * APPEALS-52882 Fix codeclimate issues * APPEALS-52882 Address PR feedback * APPEALS-52882 Address PR feedback * Jcohen/APPEALS-52861 (#22450) * APPEALS-52861 Bug fix implemented. * APPEALS-52861 cleaned up some code through aliasing a common method name between appeal_types, eliminating a check that we do in the AppealDecisionMailed module. * APPEALS-52861 fixed method name error. * APPEALS-52861 fixed some tests. * APPEALS-52861 fixed More tests. * APPEALS-52861 fixed More tests. * APPEALS-52861 feature branch changes merged in with working branch and method name changes extracted. * APPEALS-52861 feature branch changes merged in with working branch and method name changes extracted, included case statement. * APPEALS-52861 moved case statement to another file to be the boolean check for constested status on an appeal. --------- Co-authored-by: Jonathan Cohen Co-authored-by: Marc Steele <71673522+msteele96@users.noreply.github.com> * APPEALS-52892: For all legacy appeal states/notifications check status in VACOLS (#22455) * initial commit * addressing comments * small change * making tests happy * fixing spec tests * rubocop'n * reverting --------- Co-authored-by: nhansen3 Co-authored-by: Marc Steele <71673522+msteele96@users.noreply.github.com> * APPEALS-53993: Update Implementation of Legacy Appeals with Hearing Held (#22473) * implement new hearing requirement and test * first pass at seed data for testing * add new case to test and seed, fix typo and comments * update for new 8.2.1.1 requirement * fix rubocop warning * fix error in seed file * add new affinity scenario to seed data * make some seed methods private * fix rubocop warnings * APPEALS-55159: Parameter Requirements Are Too Stringent Given What We Will Expect from VA Notify (#22561) * APPEALS-55159 * updating tests * APPEALS-55159 Refactor permitted and required params * APPEALS-55159 Fix linting issue --------- Co-authored-by: nhansen3 Co-authored-by: msteele Co-authored-by: Marc Steele <71673522+msteele96@users.noreply.github.com> * Adding rake task to sync decided appeals (#22492) * APPEALS-52872 Add rake task to sync decided appeals * APPEALS-52872 Remove byebug * APPEALS-52872 Fixing lint errors * APPEALS-52872 Addressing performance issues * APPEALS-52872 Addressing performance issues * APPEALS-52872 Fix ENV variable update * APPEALS-52872 Parallelizing AppealState updates * APPEALS-52872 Remove byebug --------- Co-authored-by: Marc Steele <71673522+msteele96@users.noreply.github.com> * Prevent Prodtest from contacting VA Notify Staging (#22629) * APPEALS-54918 Update VA Notify fake service and initializer * APPEALS-54918 Add CASEFLOW_BASE_URL env var in demo/dev * APPEALS-54918 Add seed file for API Key and added to main seed execution * APPEALS-54918 Add missing argument to method definition * APPEALS-54918 Update "to" params and fix linting issues * APPEALS-54918 Prevent post requests in test env * Rename ApiKeys seed file * Fix linting error on require statement * Add argument for call within sync_review_job.rb and minor refactor * Inherit seed from base, align with AMA for seed file placement * added test cases for infinite and omit levers with desired outcomes * add check for hearing date to levers being infinite * fix lint * feature/APPEALS-34124-43428-29105-28925-33581 - Rails 6.1 upgrade (release) (#22776) * šŸ”§ Assume defaults for `config.action_dispatch.use_cookies_with_metadata` and `config.action_mailer.delivery_job` The following config settings are not backwards compatible: - config.action_dispatch.use_cookies_with_metadata - config.action_mailer.delivery_job Now that Rails 6.0 is stable on production, we can assume their default values going forward. * āœ… Fix flakey spec * šŸ”§ Assume default for `config.action_dispatch.use_authenticated_cookie_encryption` Since we are making other cookie configuration changes in this PR for Rails 6.0, this is an opportune time to migrate this Rails 5.2 cookie setting to its default value as well. * āŖļø Restore overrides for `config.action_dispatch.use_authenticated_cookie_encryption` and `config.action_dispatch.use_cookies_with_metadata` While testing in PreProd, we discovered that, without these cookie config overrides, re-authentication was broken -- after logging out, a user could not log back in. Since the default settings are still optional going forward, we can restore these overrides and devise a solution to migrate cookies later. For more details, see Jira story APPEALS-54897: https://jira.devops.va.gov/browse/APPEALS-54897 * āœØ Add new utility module for adding DB indexes concurrently Introduces `Caseflow::Migrations::AddIndexConcurrently` as a replacement for `Caseflow::Migration` for migrations on ActiveRecord 6.0 and beyond, since `Caseflow::Migration` is forever coupled to ActiveRecord 5.1 due to its extensive use on legacy migrations and should be deprecated moving forward. * šŸ—‘ļø Deprecate `Caseflow::Migration` * šŸ”§ Add instructive error message for non-concurrent `add_index` migrations * šŸšØ Address linter / codeclimate complaints * āœØ Introduce `SslRedirectExclusionPolicy` To be used in the environment configuration settings for excluding exempt request paths from SSL redirects when `config. force_ssl = true` * ā™»ļø Replace deprecated controller-level `force_ssl` Replace deprecated controller-level `force_ssl` with equivalent configuration settings in preparation for the Rails 6.1 upgrade. * šŸ”„ Remove deprecated config setting `config.active_record.sqlite3.represent_boolean_as_integer` This will have no implications for Caseflow, since we are only using the `sqlite3` adapter nominally for the `demo_vacols` database, which is not actually being used in our demo environments (demo environments are deployed as `development` envs). * ā¬†ļø Update `caseflow-commons` to resolve sub-dependency conflicts Removes unneeded gems `bourbon` and `neat`, which had a sub-dependency conflict on `thor`. * ā¬†ļø Update rails and other gems as necessary * šŸ› Fix 'uninitialized constant' error when loading app * ā¬†ļø bin/rails app:update - Apply relevant changes * šŸ”§ Override default for `config.active_record.has_many_inversing` * šŸ”§ Assume default for `config.active_storage.track_variants` We're not currently using ActiveStorage in Caseflow, so it is safe to just assume the default here. * šŸ”§ Override default for `config.active_job.retry_jitter` The default jitter is probably safe, however, I'm not 100% sure that we don't have any jobs that need to be requeued with exact wait times. So we let's override this for now to stay on the safe side. * šŸ”§ Assume default for `config.active_job.skip_after_callbacks_if_terminated` We're not currently usingĀ `throw :abort`Ā within anyĀ `before_enqueue`/`before_perform`Ā  callbacks on existing Caseflow jobs, so the default should be fine here. For more background, see https://lilyreile.medium.com/rails-6-1-new-framework-defaults-what-they-do-and-how-to-safely-uncomment-them-c546b70f0c5e#4c60 * šŸ”§ Assume default for `config.action_dispatch.cookies_same_site_protection` This setting controls the `SameSite` optional attribute for the `Set-Cookie` header. `SameSite=Lax` means that the cookie is not sent on cross-site requests, such as on requests to load images or frames, but is sent when a user is navigating to the origin site from an external site (for example, when following a link). This is the default behavior if the SameSite attribute is not specified. `Lax` is currently the default assumed by both Chrome and Edge browsers when this attribute is left unspecified, so assuming this value should be sensible. It allows us to have our cake (blocking CSRF attacks) and eat it too (providing a logged-in experience when users navigate to Caseflow across origins). For more background, see - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value - https://lilyreile.medium.com/rails-6-1-new-framework-defaults-what-they-do-and-how-to-safely-uncomment-them-c546b70f0c5e#1f15 * šŸ”§ Assume default for `config.action_controller.urlsafe_csrf_tokens` * šŸ”§ Assume default for `ActiveSupport.utc_to_local_returns_utc_offset_times` We're not using `ActiveSupport::TimeZone.utc_to_local` anywhere, so the default is safe to assume here. * šŸ”§ Assume default for `config.action_dispatch.ssl_default_redirect_status` The default is safe to assume. For more background, see https://lilyreile.medium.com/rails-6-1-new-framework-defaults-what-they-do-and-how-to-safely-uncomment-them-c546b70f0c5e#4c3e * šŸ”§ Assume default for `config.active_record.legacy_connection_handling` The default should be safe to assume here, as we do not do any role or shard switching on database connections. For more background, see https://lilyreile.medium.com/rails-6-1-new-framework-defaults-what-they-do-and-how-to-safely-uncomment-them-c546b70f0c5e#8007 * šŸ”§ Assume default for `config.action_view.form_with_generates_remote_forms` We don't use the `form_with` helper anywhere, so this behavior change is inconsequential for us, and we can safely assume the new default. * šŸ”§ Assume default for `config.active_storage.queues.analysis` We do not use ActiveStorage, so the default is safe to assume here. * šŸ”§ Assume default for `config.active_storage.queues.purge` We do not use ActiveStorage, so the default is safe to assume here. * šŸ”§ Assume default for `config.action_mailbox.queues.incineration` We don't use ActionMailbox, so the new default is safe to assume here. * šŸ”§ Assume default for `config.action_mailbox.queues.routing` We do not use ActionMailbox, so the default is safe to assume here. * šŸ”§ Assume default for `config.action_mailer.deliver_later_queue_name` We're not using `ActionMailer::MessageDelivery #deliver_later` anywhere, so the default is safe to assume. * šŸ”§ Assume default for `config.action_view.preload_links_header` This flag can be safely uncommented. Browsers that support Link headers will get a performance boost. Browsers that donā€™t will ignore them. We override in `development` environments to avoid an edge case leading to an HTTP response header overflow. For more background, see https://lilyreile.medium.com/rails-6-1-new-framework-defaults-what-they-do-and-how-to-safely-uncomment-them-c546b70f0c5e#3679 * šŸ”„ Remove 'new_framework_defaults_6_1.rb' * šŸ”§ Load defaults for Rails 6.1 * ā™»ļø Extract constant * ā™»ļø Migrate to new Rails deprecation config where applicable * ā™»ļø Push members down now that there is only one subclass * šŸ©¹ Add forgotten disallowed deprecation warning This deprecation warning was addressed by the following PR, but we forgot to add it to the list of disallowed deprecation warnings: https://github.com/department-of-veterans-affairs/caseflow/pull/21614 * šŸ’” Update comment Task `rake routes` has been replaced with `rails routes` * āœ… Update test to account for change to `ActionDispatch::Response#content_type`Ā  `ActionDispatch::Response#content_type` now returns the full Content-Type header * šŸšØ Exclude 'config.ru' from Rubocop cops * šŸšš Move 'db/etl/migrate' to 'db/etl_migrate' * šŸšš Move 'db/etl/schema.rb' to 'db/etl_schema.rb' * ā™»ļø Arrange 'database.yml' configs by environment Group DB configs by environment in anticipation of reformatting for Rails 6+ multi-DB configuration. * šŸ”§ Reformat 'database.yml' to Rails 6+ multi-DB conventions * šŸ”§ Add etl migration paths to DB config * šŸ”§ Update DB connection names in 'database_cleaner' config * ā™»ļø Use new database-specific rake tasks After migrating to the Rails 6+ native multi-database configuration, the behavior of some DB management tasks, such as `rake db:migrate` changed such that they now act on ALL databases and not just the primary database. So we must replace the invocations of these tasks with their new, database-specific counterparts. * āž– Remove 'multiverse' gem Now that we have fiully transitioned to Rails-native multi-database support, we are no longer reliant on the 'multiverse' gem and can remove it. * šŸ—ƒļø Prohibit execution of vacols DB and non-DB-specific rake tasks After transitioning to Rails-native multi-DB support, the behavior of some DB tasks changed such that they will now act on ALL databases and not just the primary database (ex. `rake db:migrate` will now migrate ALL databases). To avoid accidents, we re-define these tasks here to no-op and output a helpful message to redirect developers toward using their new database-specific counterparts instead. * ā™»ļø Create new environment for GH workflow 'Make-docs-to-webpage' Instead of performing a bunch of hard-to-maintain `sed` gymnastics to modify the existing 'test' environment, let's create a new 'make_docs' environment (based off of 'test') and configure it appropriately for use by the 'Make-docs-to-webpage' GH workflow. * šŸ’š Remove redundant DB migrations from CI workflow Task `db:schema:load` already loads the checked in schema, so there should be no need to run `db:migrate` afterwards. * šŸ› Fix `spec/mailers/hearing_mailer_spec.rb` - `NoMethodError` Addresses the following error: NoMethodError: undefined method `build_lookup_context' for ActionView::Base:Class * šŸ› Fix `spec/workflows/post_decision_motion_updater_spec.rb` - `FrozenError` Addresses the following error: FrozenError: can't modify frozen Hash: {} * āœ… Add test for `RoSchedulePeriod` * šŸ› Fix `spec/models/schedule_period_spec.rb` - `ActiveRecord::RecordInvalid` Apparently, there were some changes to the inner workings of `ActiveModel::Errors` in Rails 6.1, causing a model to be considered invalid in the case that `errors[:base] == [[]]`. This makes sense, as `[[]]` is not considered "empty". Unfortunately, this was causing `RoSchedulePeriod #validate_spreadsheet` to inadvertently mark the model as invalid upon creation. `HearingSchedule::ValidateRoSpreadsheet #validate` returns an empty array (`[]`) when valid, which gets pushes onto the `RoSchedulePeriod` `errors[:base]` array, resulting in a non-empty array (`[[]]`) and an erroneously invalid disposition. Furthermore, calling `<<` to an `ActiveModel::Errors` message array in order to add an error is a deprecated, so we can take this opportunity to use the new `#add` API to hit two birds with one stone. The change implemented here is not a pure refactoring, however the end-user experience is unchanged in terms of how errors are presented when attempting to upload a spreadsheet with multiple non-conformities. Down the road, we may want to consider moving `HearingSchedule::ValidateRoSpreadsheet` toward using `ActiveModel::Validations` in order to leverage the full `ActiveModel::Errors` API and construct the errors object in the prescribed manner. For more details see - https://api.rubyonrails.org/v6.1.7.7/classes/ActiveModel/Validations.html - https://api.rubyonrails.org/v6.1.7.7/classes/ActiveModel/Errors.html * šŸ› Fix `spec/mailers/hearing_mailer_spec.rb` - `ActionView::Template::Error` * āœ… Fix `spec/models/veteran_spec.rb` * āœ… Fix `spec/sql/ama_cases_sql_spec.rb` Addresses failures such as the below: 0) AMA Cases Tableau data source expected report calculates age and AOD based on person.dob Failure/Error: expect(aod_case["aod_veteran.age"]).to eq("76") expected: "76" got: 0.76e2 * āœ… Fix multiple specs - `Minitest::UnexpectedError` Test helper method `#perform_enqueued_jobs` now wraps exceptions in an `Minitest::UnexpectedError`: https://github.com/rails/rails/blob/914caca2d31bd753f47f9168f2a375921d9e91cc/activejob/lib/active_job/test_helper.rb#L591 So, to assert that a specific exception is raised during execution of the `#perform_enqueued_jobs` block, we must rescue the `Minitest::UnexpectedError` and make the assertion on its error message instead. * āœ… Fix `spec/lib/helpers/association_wrapper_spec.rb` * āœ… Fix `spec/controllers/api/v1/jobs_controller_spec.rb` In Rails 6.1, `ActiveJob #perform_now` was changed to behave as it did once before (at the behest of GitHub), returning the value fo the job instead of true/false. See related GH issue: https://github.com/rails/rails/issues/38040 * šŸ› Fix `spec/controllers/appeals_controller_spec.rb` - `NoMethodError` Addresses error: NoMethodError: undefined method `workflow' for # 0) AppealsController GET appeals when current user is a System Admin when request header does not contain Veteran ID responds with an error Failure/Error: errors: errors.messages[:workflow], NoMethodError: undefined method `workflow' for # # ./app/workflows/case_search_results_base.rb:31:in `search_call' * šŸ› Fix `CaseSearchResultsBase` validations Addresses test failures in `spec/controllers/appeals_controller_spec.rb` similar to the below: AppealsController GET appeals when current user is a System Admin when request header does not contain Veteran ID responds with an error Failure/Error: expect(response_body["errors"][0]["title"]).to eq "Veteran file number missing" expected: "Veteran file number missing" got: nil Using `ActiveModel::Errors` to store error data in an arbitrary format may have been somewhat permissible in the past, but it is an abuse of the object's intended use and is also proving incompatible with the more formalized `ActiveModels::Errors` API in Rails 6.1. In order to preserve the existing response shape of the affected JSON endpoints, we need to move away from the `ActiveModel::Validations` implementation on `CaseSearchResultsBase` (and its descendent classes) to a more bespoke method of performing validations and aggregating errors, since Rails 6.1 `ActiveModel::Errors` is no longer appropriate for our needs here. * āœ… Fix `spec/controllers/application_controller_spec.rb` -- Cache-Control error Addresses the test failure below: ApplicationController no cache headers when toggle set sets Cache-Control etc Failure/Error: expect(response.headers["Cache-Control"]).to eq "no-cache, no-store" expected: "no-cache, no-store" got: "no-store" (compared using ==) # ./spec/controllers/application_controller_spec.rb:59:in `block (4 levels) in ' In Rails 6.1, the `no-store` directive is exclusive of any others that are set on the `Cache-Control` header, which makes sense given the specification https://datatracker.ietf.org/doc/html/rfc7234#section-3 This change was implemented in PR https://github.com/rails/rails/pull/39461 Since it no longer makese sense to set both `no-store` and `no-cache` directives, we will only set `no-store` here, as that is the stronger of the two. * šŸ› Fix multiple specs - `ActiveRecord::EagerLoadPolymorphicError` Addresses multiple test failures caused by the error below: QueueConfig.to_hash title when assigned to an org is formatted as expected Failure/Error: tasks.with_assignees.group("assignees.display_name").count(:all).each_pair.map do |option, count| label = self.class.format_option_label(option, count) self.class.filter_option_hash(option, label) end ActiveRecord::EagerLoadPolymorphicError: Cannot eagerly load the polymorphic association :appeal # ./app/models/queue_column.rb:110:in `assignee_options' * šŸ› Fix `spec/models/task_spec.rb` - `update_all` clears query cache In Rails 6.1.7.7, the method `ActiveRecord::Relation #update_all` will now clear any records cached by the calling relation. This was altering the behavior of `Task #cancel_task_and_child_subtasks` and causing the following test failure: Task#cancel_task_and_child_subtasks cancels all tasks and child subtasks Failure/Error: expect(second_level_tasks[0].versions.count).to eq(initial_versions + 2) expected: 3 got: 2 (compared using ==) # ./spec/models/task_spec.rb:368:in `block (3 levels) in ' To remedy, we will now cache the necessary Task records in an Array, which can be used for generating PaperTrail versions both before and after the `update_all`. * šŸ› Fix `spec/services/hearings/calendar_service_spec.rb` - template rendering error Addresses the following test failure: Hearings::CalendarService.confirmation_calendar_invite returns appropriate iCalendar event Failure/Error: expect(ical_event.description).to eq(expected_description) expected: "You're scheduled for a virtual hearing with a Veterans Law Judge of the Board of Veterans' Appeals.\...to reschedule or cancel your virtual hearing, contact us by email at bvahearingteamhotline@va.gov\n" got: # * šŸ› Fix YAML syntax error caused by whitespace in ENV var Address the following error, found during demo deployment: rake aborted! Cannot load database configuration: YAML syntax error occurred while parsing /caseflow/config/database.yml. Please note that YAML must be consistently indented using spaces. Tabs are not allowed. Error: (): could not find expected ':' while scanning a simple key at line 49 column 5 * ā¬†ļø Update `caseflow-commons` dependency to latest ref Removes `bourbon` and `neat` dependencies. * APPEALS-57351 Inactive Appeal Errors present when outcoding an appeal (#22721) * APPEALS-57351 add template name check to handle errors * APPEALS-57351 Add template_name argument to spec tests * Update app/models/prepend/va_notify/appellant_notification.rb Co-authored-by: Matthew Thornton <99351305+ThorntonMatthew@users.noreply.github.com> * APPEALS-57351 Partial rspec updates, pushed before handing off * APPEALS-57351 Add specs requested during review * Update spec/controllers/idt/api/v2/appeals_controller_spec.rb --------- Co-authored-by: Matthew Thornton <99351305+ThorntonMatthew@users.noreply.github.com> * feature/APPEALS-50882 - Open Telemetry configuration and implementation (#22787) * feature/appeals-45972 (#21950) * Remove UUID from attrs sent_to metrics. (#21630) * Remove UUID from attrs sent_to metrics. * pushing with some linting issues for visibility. * Change config implementation. * Remove UUID from spec * remove uuid from update_appellant job * diable rubocop for open_telemetry init * OTEL fixes * rescue, errors --------- Co-authored-by: mikefinneran <110622959+mikefinneran@users.noreply.github.com> * add otel reqs * Remove throw * comment out otel file loop * try vendor suggested config * update otel config to match vendor * update pg gem * otel include dt_host file * otel require bundler and rubygems * otel subset of instrumentation * Revert "update pg gem" This reverts commit fc1a45dfeedd3c2b9e299088cf10aea1394bf199. * update redis and minimize otel instrumentation to just rails, rack, and activerecord * otel add use_all except pg and redis * otel require instruments * otel fix typo * otel fix typo * otel comment out net_http * otel silence aws sdk internals * otel silence aws sdk internals * otel add net http * Individually use OTEL instruments (#22082) * Individually use OTEL instruments with options. * disable AwsSdk only. Checking Rack options. * re-require awssdk even while disabled. * disable awssdk * disable datadog for testing * change sequence factory to properly seed * updated opentelemetry and datadog configs * rack context getter initalizer * use one at a time * add curly braces * Revert change * revert change * Revert change * Revert change * Remove gemfile grouping * Remove datadog. * ActionPack and Actionview changes * APPEALS-44287: Excluding disposition held and select that appeal for distribution (#22277) * first run at an SQL query removing duplicate appeals from distribution * code refactor and excluding disposition held and select that appeal for distribution * automated test for the duplicate hearing bug * fix rubocop offense SpaceInsideBlockBraces --------- Co-authored-by: Sean Parker Co-authored-by: samasudhirreddy --------- Co-authored-by: Noelle Adkin <98478937+NoelleAd@users.noreply.github.com> Co-authored-by: Raymond Hughes <131811099+raymond-hughes@users.noreply.github.com> Co-authored-by: Dani Co-authored-by: raymond-hughes Co-authored-by: Craig Reese <109101548+craigrva@users.noreply.github.com> Co-authored-by: Sean Parker Co-authored-by: samasudhirreddy * Update bundler to 2.4.22 * Updating OTEL to use all and remove new relic gem * Remove calls to new relic * removed id attrs in metrics service calls that were causing dimension errors * Update config for otel * Reorder gems to fix linting issues * Updating config and refactoring verbose gems to add all * Reinstall gems * Adding all instruments * Update config to only include basic instruments * Update Gemfile.lock * Adding rake to gemfile * Adding instruments * Comment out PG and ActiveSupport * Suppress AWS logs * Remove redis and turn on actionview * Turn actionview off * Add Redis instrumentation back * Turn ActionPack back on * Disable Redis * Remove mentions of Datadog * Removed extra mentions of Datadog * removed newrelic references and yml file * Test updating workflow * Revert workflow change * Adding simplecov back * Fixing linting error * Removing by: attribute after removing keyword in metric service * Adding simplecov lcov gem and updating workflow * Update workflow * Updating simplecov * Revert simple cov * Adding featureenvy skip for reek * Update service name. (#22762) --------- Co-authored-by: mikefinneran <110622959+mikefinneran@users.noreply.github.com> Co-authored-by: Noelle Adkin <98478937+NoelleAd@users.noreply.github.com> Co-authored-by: Dani Co-authored-by: Craig Reese <109101548+craigrva@users.noreply.github.com> Co-authored-by: Sean Parker Co-authored-by: samasudhirreddy Co-authored-by: Andrew Hadley Co-authored-by: alex-guanipatin Co-authored-by: Drew Hadley <50673809+VandelayUtd@users.noreply.github.com> * Remove unused variable with lint error. Re-introduce cert fixes * Remove references to x86 * Revert "Remove references to x86" This reverts commit fda290d12e53b810bb95c4229fa27c9a548d620f. --------- Co-authored-by: calvincostaBAH <108481161+calvincostaBAH@users.noreply.github.com> Co-authored-by: samasudhirreddy <108430298+samasudhirreddy@users.noreply.github.com> Co-authored-by: Calvin Co-authored-by: Craig Reese <109101548+craigrva@users.noreply.github.com> Co-authored-by: seanrpa <155660052+seanrpa@users.noreply.github.com> Co-authored-by: Isaiah Saucedo Co-authored-by: Craig Reese Co-authored-by: Matthew Thornton Co-authored-by: noahhansen-gov <166541737+noahhansen-gov@users.noreply.github.com> Co-authored-by: nhansen3 Co-authored-by: Matthew Thornton <99351305+ThorntonMatthew@users.noreply.github.com> Co-authored-by: prernadevbah <132498915+prernadevbah@users.noreply.github.com> Co-authored-by: Jonathan Cohen <121630615+JCohDev@users.noreply.github.com> Co-authored-by: Jonathan Cohen Co-authored-by: Jeremy Croteau Co-authored-by: Raymond Hughes <131811099+raymond-hughes@users.noreply.github.com> Co-authored-by: mikefinneran <110622959+mikefinneran@users.noreply.github.com> Co-authored-by: Noelle Adkin <98478937+NoelleAd@users.noreply.github.com> Co-authored-by: Dani Co-authored-by: Sean Parker Co-authored-by: samasudhirreddy Co-authored-by: Andrew Hadley Co-authored-by: alex-guanipatin Co-authored-by: Drew Hadley <50673809+VandelayUtd@users.noreply.github.com> --- .github/workflows/workflow.yml | 12 + Makefile.example | 4 +- .../api/v1/va_notify_controller.rb | 67 +++-- app/helpers/sync_decided_appeals_helper.rb | 76 +++++ app/jobs/nightly_syncs_job.rb | 12 + app/jobs/process_decision_document_job.rb | 4 +- ...process_notification_status_updates_job.rb | 211 ++++++++++++-- app/jobs/receive_notification_job.rb | 73 ----- app/jobs/send_notification_job.rb | 24 +- app/jobs/sync_reviews_job.rb | 2 +- app/jobs/va_notify_status_update_job.rb | 187 ------------ app/models/appeal_state.rb | 7 +- app/models/decision_document.rb | 17 +- .../va_notify/appeal_decision_mailed.rb | 9 +- .../va_notify/appellant_notification.rb | 17 +- app/services/sqs_service.rb | 79 +++++ .../components/NotificationTableColumns.jsx | 2 +- client/constants/QUARTERLY_STATUSES.json | 4 +- client/constants/VA_NOTIFY_CONSTANTS.json | 3 + client/test/data/notifications.js | 4 +- config/environments/demo.rb | 2 + config/environments/development.rb | 9 +- config/environments/test.rb | 6 + config/initializers/message_queues.rb | 30 ++ config/initializers/scheduled_jobs.rb | 1 - config/initializers/shoryuken.rb | 19 +- config/initializers/va_notify.rb | 7 +- ...d_sm_sand_email_status_to_notifications.rb | 6 + db/schema.rb | 2 + db/seeds/api_keys.rb | 9 +- docker-compose-m1.yml | 5 +- docker-compose.yml | 5 +- lib/caseflow/error.rb | 4 + lib/fakes/va_notify_service.rb | 44 ++- lib/tasks/appeal_state_synchronizer.rake | 8 + .../api/v1/va_notify_controller_spec.rb | 222 +++++++++++++- .../idt/api/v2/appeals_controller_spec.rb | 31 ++ .../sync_decided_appeals_helper_spec.rb | 75 +++++ .../hearings/receive_notification_job_spec.rb | 182 ------------ spec/jobs/nightly_syncs_job_spec.rb | 77 +++++ .../notification_initialization_job_spec.rb | 4 + .../process_decision_document_job_spec.rb | 4 +- ...ss_notification_status_updates_job_spec.rb | 196 ++++++++----- spec/jobs/quarterly_notifications_job_spec.rb | 66 ++--- spec/jobs/send_notification_job_spec.rb | 12 +- spec/jobs/sync_reviews_job_spec.rb | 2 +- spec/jobs/va_notify_status_update_job_spec.rb | 271 ------------------ spec/models/appellant_notification_spec.rb | 56 ++-- spec/models/decision_document_spec.rb | 3 +- spec/models/tasks/bva_dispatch_task_spec.rb | 6 +- spec/services/sqs_service_spec.rb | 136 +++++++++ spec/workflows/ihp_tasks_factory_spec.rb | 2 +- 52 files changed, 1325 insertions(+), 991 deletions(-) create mode 100644 app/helpers/sync_decided_appeals_helper.rb delete mode 100644 app/jobs/receive_notification_job.rb delete mode 100644 app/jobs/va_notify_status_update_job.rb create mode 100644 app/services/sqs_service.rb create mode 100644 client/constants/VA_NOTIFY_CONSTANTS.json create mode 100644 config/initializers/message_queues.rb create mode 100644 db/migrate/20240717145856_add_sm_sand_email_status_to_notifications.rb create mode 100644 spec/helpers/sync_decided_appeals_helper_spec.rb delete mode 100644 spec/jobs/hearings/receive_notification_job_spec.rb delete mode 100644 spec/jobs/va_notify_status_update_job_spec.rb create mode 100644 spec/services/sqs_service_spec.rb diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index f2dec46dfae..d89fe2a19d5 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -45,6 +45,15 @@ jobs: ports: - 1521:1521 + localstack: + image: localstack/localstack:0.14.5 + ports: + - 4566:4566 + env: + SERVICES: "sqs" + volumes: + - /var/run/docker.sock:/var/run/docker.sock + strategy: fail-fast: false matrix: @@ -188,6 +197,9 @@ jobs: - name: "Wait for database" run: dockerize -wait tcp://postgres:5432 -timeout 1m + - name: "Wait for localstack" + run: dockerize -wait tcp://localstack:4566 -timeout 30s + - name: "Wait for FACOLS" run: ./ci-bin/capture-log "bundle exec rake local:vacols:wait_for_connection" diff --git a/Makefile.example b/Makefile.example index f37b02940ed..1305da089ec 100644 --- a/Makefile.example +++ b/Makefile.example @@ -289,7 +289,7 @@ one-test-headless: # run the rspec test headless. CI=1 bundle exec rspec $(RUN_ARGS) --format progress run-all-queues: ## start shoryuken with all queues - bundle exec shoryuken -q caseflow_development_send_notifications caseflow_development_high_priority caseflow_development_low_priority -R + bundle exec shoryuken -q caseflow_development_send_notifications.fifo caseflow_development_high_priority caseflow_development_low_priority -R run-low-priority: ## start shoryuken with just the low priority queue bundle exec shoryuken -q caseflow_development_low_priority -R @@ -298,7 +298,7 @@ run-high-priority: ## start shoryuken with just the high priority queue bundle exec shoryuken -q caseflow_development_high_priority -R run-send-notifications: ## start shoryuken with just the send_notification queue - bundle exec shoryuken -q caseflow_development_send_notifications -R + bundle exec shoryuken -q caseflow_development_send_notifications.fifo -R jest: ## Run jest tests cd client && yarn jest diff --git a/app/controllers/api/v1/va_notify_controller.rb b/app/controllers/api/v1/va_notify_controller.rb index d47a946fd06..9ddb119e58e 100644 --- a/app/controllers/api/v1/va_notify_controller.rb +++ b/app/controllers/api/v1/va_notify_controller.rb @@ -1,46 +1,63 @@ # frozen_string_literal: true class Api::V1::VaNotifyController < Api::ApplicationController - # Purpose: POST request to VA Notify API to update status for a Notification entry + # Purpose: POST request to VA Notify API to update status for a Notification entry. # # Params: Params content can be found at https://vajira.max.gov/browse/APPEALS-21021 # # Response: Update corresponding Notification status def notifications_update - send "#{required_params[:notification_type]}_update" + send_sqs_message + render json: { + message: "#{params['notification_type']} Notification successfully updated: ID #{params['id']}" + } + rescue StandardError => error + log_error(error, params["id"], params["notification_type"]) + render json: { error: error.message }, status: :bad_request end private - # Purpose: Finds and updates notification if type is email - # - # Params: Params content can be found at https://vajira.max.gov/browse/APPEALS-21021 - # - # Response: Update corresponding email Notification status - def email_update - redis.set("email_update:#{required_params[:id]}:#{required_params[:status]}", 0) - - render json: { message: "Email notification successfully updated: ID #{required_params[:id]}" } + def va_notify_params + params.permit(:id, :notification_type, :status, :status_reason, :to) end - # Purpose: Finds and updates notification if type is SMS - # - # Params: Params content can be found at https://vajira.max.gov/browse/APPEALS-21021 - # - # Response: Update corresponding SMS Notification status - def sms_update - redis.set("sms_update:#{required_params[:id]}:#{required_params[:status]}", 0) - - render json: { message: "SMS notification successfully updated: ID #{required_params[:id]}" } + def build_message_body + id_param, notification_type_param, status_param = va_notify_params.require([:id, :notification_type, :status]) + + { + external_id: id_param, + notification_type: notification_type_param, + recipient: va_notify_params[:to], + status: status_param, + status_reason: va_notify_params[:status_reason] + } + rescue StandardError => error + raise error end - def required_params - id_param, notification_type_param, status_param = params.require([:id, :notification_type, :status]) + def build_sqs_message + message_body = build_message_body.to_json + + { + queue_url: SqsService.find_queue_url_by_name(name: "receive_notifications"), + message_body: message_body, + message_deduplication_id: Digest::SHA256.hexdigest(message_body), + message_group_id: Constants.VA_NOTIFY_CONSTANTS.message_group_id + } + rescue StandardError => error + raise error + end - { id: id_param, notification_type: notification_type_param, status: status_param } + def send_sqs_message + sqs = SqsService.sqs_client + sqs.send_message(build_sqs_message) end - def redis - @redis ||= Redis.new(url: Rails.application.secrets.redis_url_cache) + def log_error(error, external_id, notification_type) + Rails.logger.error("#{error.message}\n#{error.backtrace.join("\n")}\n \ + external_id: #{external_id}\n \ + notification_type: #{notification_type}") + Raven.capture_exception(error) end end diff --git a/app/helpers/sync_decided_appeals_helper.rb b/app/helpers/sync_decided_appeals_helper.rb new file mode 100644 index 00000000000..45adcc0e36e --- /dev/null +++ b/app/helpers/sync_decided_appeals_helper.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +## +# Helper to sync the decided appeals and their decision_mailed status + +module SyncDecidedAppealsHelper + VACOLS_BATCH_PROCESS_LIMIT = ENV["VACOLS_QUERY_BATCH_SIZE"] || 800 + + # Syncs the decision_mailed status of Legacy Appeals with a decision made + def sync_decided_appeals + begin + # Join query to retrieve Legacy AppealState ids and corresponding vacols_id + appeal_state_ids = AppealState.legacy.where(decision_mailed: false) + .joins(:legacy_appeal).preload(:legacy_appeal) + .pluck(:id, :vacols_id) + + appeal_state_ids_hash = appeal_state_ids.to_h + + vacols_decision_dates = get_decision_dates(appeal_state_ids_hash.values).to_h + + ActiveSupport::Dependencies.interlock.permit_concurrent_loads do + Parallel.each(appeal_state_ids_hash, in_threads: 4) do |appeal_state_hash| + appeal_state_id = appeal_state_hash[0] + vacols_id = appeal_state_hash[1] + # If there is a decision date on the VACOLS record, + # update the decision_mailed status on the AppealState to true + if vacols_decision_dates[vacols_id].present? + AppealState.find(appeal_state_id).decision_mailed_appeal_state_update_action! + end + end + end + rescue StandardError => error + Rails.logger.error("#{error.class}: #{error.message}\n#{error.backtrace}") + + # Re-raising the error so it can be caught in the NightlySyncsJob report + raise error + end + end + + # Method to retrieve the decision dates from VACOLS in batches + # params: vacols_ids + # Returns: Hash containing the key, value pair of vacols_id, decision_date + def get_decision_dates(vacols_ids) + begin + decision_dates = {} + + # Query VACOLS in batches + vacols_ids.in_groups_of(VACOLS_BATCH_PROCESS_LIMIT.to_i) do |vacols_id| + VACOLS::Case.where(bfkey: vacols_id).each do |vacols_record| + decision_dates[vacols_record[:bfkey]] = vacols_record[:bfddec] + end + end + + decision_dates + rescue ActiveRecord::RecordNotFound + [] + end + end + + def get_vacols_ids(legacy_appeal_states) + begin + vacols_ids = {} + + legacy_appeal_states.each do |appeal_state| + legacy_appeal = LegacyAppeal.find(appeal_state.appeal_id) + + # Find the VACOLS record associated with the LegacyAppeal + vacols_ids << { appeal_state.id.to_s => (legacy_appeal[:vacols_id]).to_s } + end + + vacols_ids + rescue ActiveRecord::RecordNotFound + {} + end + end +end diff --git a/app/jobs/nightly_syncs_job.rb b/app/jobs/nightly_syncs_job.rb index ef835251d04..664a1fa0ce3 100644 --- a/app/jobs/nightly_syncs_job.rb +++ b/app/jobs/nightly_syncs_job.rb @@ -6,6 +6,7 @@ class NightlySyncsJob < CaseflowJob queue_with_priority :low_priority application_attr :queue # arbitrary + include SyncDecidedAppealsHelper def perform RequestStore.store[:current_user] = User.system_user @@ -16,6 +17,7 @@ def perform sync_vacols_users sync_decision_review_tasks sync_bgs_attorneys + sync_all_decided_appeals slack_service.send_notification(@slack_report.join("\n"), self.class.name) if @slack_report.any? end @@ -84,6 +86,14 @@ def sync_bgs_attorneys @slack_report << "*Fatal error in sync_bgs_attorneys:* #{error}" end + def sync_all_decided_appeals + begin + sync_decided_appeals + rescue StandardError => error + @slack_report << "*Fatal error in sync_decided_appeals* #{error}" + end + end + def dangling_legacy_appeals reporter = LegacyAppealsWithNoVacolsCase.new reporter.call @@ -105,5 +115,7 @@ def sync_hearing_states state.scheduled_in_error_appeal_state_update_action! end end + rescue StandardError => error + @slack_report << "*Fatal error in sync_hearing_states* #{error}" end end diff --git a/app/jobs/process_decision_document_job.rb b/app/jobs/process_decision_document_job.rb index e2d0e9f0cee..8f392bab152 100644 --- a/app/jobs/process_decision_document_job.rb +++ b/app/jobs/process_decision_document_job.rb @@ -4,10 +4,10 @@ class ProcessDecisionDocumentJob < CaseflowJob queue_with_priority :low_priority application_attr :intake - def perform(decision_document_id, mail_package = nil) + def perform(decision_document_id, contested, mail_package = nil) RequestStore.store[:application] = "idt" RequestStore.store[:current_user] = User.system_user - DecisionDocument.find(decision_document_id).process!(mail_package) + DecisionDocument.find(decision_document_id).process!(contested, mail_package) end end diff --git a/app/jobs/process_notification_status_updates_job.rb b/app/jobs/process_notification_status_updates_job.rb index 32bb0e443cb..7c8571c8c6a 100644 --- a/app/jobs/process_notification_status_updates_job.rb +++ b/app/jobs/process_notification_status_updates_job.rb @@ -1,45 +1,204 @@ # frozen_string_literal: true +# rubocop:disable Layout/LineLength +# A job that pulls messages from the 'receive_notifications' FIFO SQS queue +# that represent status updates for VA Notify notifications and persists +# the information in our notifications table. +# +# The messages are queued by {Api::V1::VaNotifyController#notifications_update} which is +# an endpoint where VA Notify sends information to us about notifications we've requested +# that they send via their +# {https://github.com/department-of-veterans-affairs/notification-api/blob/1b758dddf2d2c12d73415e4ee508cf6b0e101343/app/celery/service_callback_tasks.py#L29 send_delivery_status_to_service} callback. +# +# This information includes: +# - The latest status pertaining to the notification's delivery (ex: success or temporary-failure) +# - The status reason (extra context around the status, if available) +# - The recipient's email or phone number +# - Caseflow simply provides VA Notify with the intended recipient's participant ID with each initial notification request, and it does not know of the destination of a message until they inform us. +# +# @see https://github.com/department-of-veterans-affairs/caseflow/wiki/VA-Notify +# @see https://github.com/department-of-veterans-affairs/caseflow/wiki/Status-Webhook-API +# rubocop:enable Layout/LineLength class ProcessNotificationStatusUpdatesJob < CaseflowJob + include Hearings::EnsureCurrentUserIsSet + queue_with_priority :low_priority + MESSAGE_GROUP_ID = "VANotifyStatusUpdate" # Used to only process messages queued by the status update webhook + PROCESSING_LIMIT = 5000 # How many updates to perform per job execution + + # Consumes messages from the 'receive_notifications' FIFO SQS queue whose 'MessageGroupId' + # attribute matches MESSAGE_GROUP_ID, and then persists data contained within those messages + # about VA Notify notifications to our 'notifications' table. def perform - RequestStore[:current_user] = User.system_user + ensure_current_user_is_set - redis = Redis.new(url: Rails.application.secrets.redis_url_cache) + begin + number_of_messages_processed = 0 - processed_count = 0 + number_of_messages_processed += process_batch_of_messages while number_of_messages_processed < PROCESSING_LIMIT + rescue Caseflow::Error::SqsQueueExhaustionError + Rails.logger.info("ProcessNotificationStatusUpdatesJob is exiting early due to the queue being empty.") + rescue StandardError => error + log_error(error) + raise error + ensure + Rails.logger.info("#{number_of_messages_processed} messages have been processed by this execution.") + end + end - # prefer scan so we only load a single record into memory, - # dumping the whole list could cause performance issues when job runs - redis.scan_each(match: "*_update:*") do |key| - break if processed_count >= 1000 + private - begin - raw_notification_type, uuid, status = key.split(":") + # Returns the SQS URL of the 'receive_notifications' FIFO SQS queue for the + # current environment using a substring. + # + # @return [String] + # The URL of the queue that messages will be pulled from. + def recv_queue_url + @recv_queue_url ||= SqsService.find_queue_url_by_name(name: "receive_notifications", check_fifo: true) + end - notification_type = extract_notification_type(raw_notification_type) + # Pulls in up to 10 messages from the 'receive_notifications' FIFO SQS queue + # and consume the data in order to persist VA Notify status updates to the + # the notifications table. + # + # @see https://github.com/department-of-veterans-affairs/caseflow/blob/master/app/controllers/api/v1/va_notify_controller.rb + # + # @return [Integer] + # The number of messages that were attempted to be processed in a batch. + def process_batch_of_messages + response = SqsService.sqs_client.receive_message( + { + queue_url: recv_queue_url, + max_number_of_messages: 10, + attribute_names: ["MessageGroupId"] + } + ) - fail InvalidNotificationStatusFormat if [notification_type, uuid, status].any?(&:nil?) + # Exit loop early if there does not seem to be any more messages. + fail Caseflow::Error::SqsQueueExhaustionError if response.messages.empty? - rows_updated = Notification.select(Arel.star).where( - Notification.arel_table["#{notification_type}_notification_external_id".to_sym].eq(uuid) - ).update_all("#{notification_type}_notification_status" => status) + filtered_messages = filter_messages_by_group_id(response.messages) - fail StandardError, "No notification matches UUID #{uuid}" if rows_updated.zero? - rescue StandardError => error - log_error(error) - ensure - # cleanup keys - do first so we don't reporcess any failed keys - redis.del key - processed_count += 1 - end - end + batch_status_updates(filtered_messages) + SqsService.batch_delete_messages(queue_url: recv_queue_url, messages: filtered_messages) + + # Return the number of messages attempted to be processed + filtered_messages.size end - private + # Sorts pending status update messages by notification type and performs up to two + # separate UPDATE queries to persist data to the corresponding notifications + # table records. + # + # @param messages [Array] A collection of AWS SQS messages. + # + # @return [Boolean] + # True/False depending on if the final totals could be logged. + def batch_status_updates(messages) + parsed_bodies = messages.map { |msg| JSON.parse(msg.body) } + + email_rows_update_count = update_email_statuses(filter_body_by_notification_type(parsed_bodies, "email")) + sms_rows_update_count = update_sms_statuses(filter_body_by_notification_type(parsed_bodies, "sms")) + + Rails.logger.info( + "Email statuses updated: #{email_rows_update_count} - SMS statuses updated: #{sms_rows_update_count}" + ) + end + + # Filters messages bodies by notification_type. + # + # @param bodies [Array>] A collection of the bodies of messages that have been + # parsed into hashes. + # @param notification_type [String] The type of notification to filter for. 'email' and 'sms' + # are the two valid types at the time of writing this comment. + # + # @return [Array>] + # Messages bodies whose notification_type matches the desired one. + def filter_body_by_notification_type(bodies, notification_type) + bodies.filter { _1["notification_type"] == notification_type } + end + + # Performs updates to any email notifications in the current batch of messages + # being processed. Statuses, status reasons, and recipient informations are items that are updated. + # + # @param status_update_list [Array>] A collection of the bodies of messages that have been + # parsed into hashes. These represent VA Notify status updates. + # + # @return [Integer] + # The number of rows that have been updated. + def update_email_statuses(status_update_list) + return 0 if status_update_list.empty? + + query = <<-SQL + UPDATE notifications AS n SET + email_notification_status = new.n_status, + recipient_email = new.recipient, + email_status_reason = new.status_reason + FROM ( VALUES + #{build_values_mapping(status_update_list)} + ) AS new(external_id, n_status, status_reason, recipient) + WHERE new.external_id = n.email_notification_external_id + SQL + + ActiveRecord::Base.connection.update(query) + end + + # Performs updates to any SMS notifications in the current batch of messages + # being processed. Statuses, status reasons, and recipient informations are items that are updated. + # + # @param status_update_list [Array>] A collection of the bodies of messages that have been + # parsed into hashes. These represent VA Notify status updates. + # + # @return [Integer] + # The number of rows that have been updated. + def update_sms_statuses(status_update_list) + return 0 if status_update_list.empty? + + query = <<-SQL + UPDATE notifications AS n SET + sms_notification_status = new.n_status, + recipient_phone_number = new.recipient, + sms_status_reason = new.status_reason + FROM ( VALUES + #{build_values_mapping(status_update_list)} + ) AS new(external_id, n_status, status_reason, recipient) + WHERE new.external_id = n.sms_notification_external_id + SQL + + ActiveRecord::Base.connection.update(query) + end + + # Builds a comma-delimited list of VALUES expressions to represent the data to be used + # in updated notification statuses, status reasons, and recipient information. + # + # @param status_update_list [Array>] A collection of the bodies of messages that have been + # parsed into hashes. These represent VA Notify status updates. + # + # @return [String] + # A sanitized SQL string consisting of VALUE expressions. + def build_values_mapping(status_update_list) + values = status_update_list.map do |status_update| + external_id = status_update["external_id"] + status = status_update["status"] + status_reason = status_update["status_reason"] + recipient = status_update["recipient"] + + "('#{external_id}', '#{status}', '#{status_reason}', '#{recipient}')" + end + + ActiveRecord::Base.sanitize_sql(values.join(",")) + end - def extract_notification_type(raw_notification_type) - raw_notification_type.split("_").first + # Filters out SQS messages whose MessageGroupId isn't the one utilized by our VA Notify webhooks + # so that they're not accidentally processed. + # + # @param messages [Array] A collection of messages to be filtered. + # + # @return [Array] + # Messages whose MessageGroupId matches the one this job expect. Messages with + # a different MessageGroupId will be ignored. + def filter_messages_by_group_id(messages) + messages.filter { _1.attributes["MessageGroupId"] == MESSAGE_GROUP_ID } end end diff --git a/app/jobs/receive_notification_job.rb b/app/jobs/receive_notification_job.rb deleted file mode 100644 index 7294ce043af..00000000000 --- a/app/jobs/receive_notification_job.rb +++ /dev/null @@ -1,73 +0,0 @@ -# frozen_string_literal: true - -class ReceiveNotificationJob < CaseflowJob - queue_as ApplicationController.dependencies_faked? ? :receive_notifications : :"receive_notifications.fifo" - application_attr :hearing_schedule - - def perform(message) - if !message.nil? - message_attributes = message[:message_attributes] - if !message_attributes.nil? - # load reference value to obtain notification id for record lookup - notification_id = message_attributes[:reference][:string_value] - - # load intersecting fields that may change in our database - email_address = message_attributes[:email_address][:string_value] - phone_number = message_attributes[:phone_number][:string_value] - status = message_attributes[:status][:string_value] - type = message_attributes[:type][:string_value] - - # load record - audit_record = Notification.find_by(id: notification_id) - - compare_notification_audit_record(audit_record, email_address, phone_number, status, type) - - else - log_error("message_attributes was nil on the ReceiveNotificationListenerJob message. Exiting Job.") - end - else - log_error("There was no message passed into the ReceiveNotificationListener. Exiting job.") - end - end - - private - - # Purpose: Method to be called with an error need to be logged to the rails logger - # - # Params: error_message (Expecting a string) - Message to be logged to the logger - # - # Response: None - def log_error(error_message) - Rails.logger.error(error_message) - end - - # Purpose: Method to compare audit record from database with record in message - # - # Params: - # - audit_record - audit record to compare with message - # - email_address - email of recipient - # - phone_number = phone number of recipient - # - status - status of notification - # - type - sms or email, used to update email/text notification status - # - # Returns: Updated model from update_audit_record - def compare_notification_audit_record(audit_record, email_address, phone_number, status, type) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity - status = status.capitalize - - if !email_address.nil? && audit_record.recipient_email != email_address - audit_record.update!(recipient_email: email_address) - end - - if !phone_number.nil? && audit_record.recipient_phone_number != phone_number - audit_record.update!(recipient_phone_number: phone_number) - end - - if type == "email" && !status.nil? && status != audit_record.email_notification_status - audit_record.update!(email_notification_status: status) - elsif type == "sms" && !status.nil? && status != audit_record.sms_notification_status - audit_record.update!(sms_notification_status: status) - end - - audit_record - end -end diff --git a/app/jobs/send_notification_job.rb b/app/jobs/send_notification_job.rb index 4438014e8ec..9bce677e847 100644 --- a/app/jobs/send_notification_job.rb +++ b/app/jobs/send_notification_job.rb @@ -43,12 +43,11 @@ class SendNotificationJobError < StandardError; end class << self def queue_name_suffix - ApplicationController.dependencies_faked? ? :send_notifications : :"send_notifications.fifo" + :"send_notifications.fifo" end end # Must receive JSON string as argument - def perform(message_json) ensure_current_user_is_set @@ -57,18 +56,12 @@ def perform(message_json) @message = validate_message(JSON.parse(message_json, object_class: OpenStruct)) - transaction_wrapper do + ActiveRecord::Base.transaction do @notification_audit = find_or_create_notification_audit update_notification_statuses send_to_va_notify if message_status_valid? end rescue StandardError => error - if Rails.deploy_env?(:prodtest) && error.in?(DISCARD_ERRORS) - transaction_wrapper do - @notification_audit = find_or_create_notification_audit - end - end - log_error(error) raise error end @@ -76,19 +69,6 @@ def perform(message_json) private - # Conditionally wraps database operations in a transaction block depending on whether - # the current environment is ProdTest. The choice to not have ProdTest queries utilize - # a transction is due to how unlikely it will be for us to have an operation VA Notify - # integration in that environment due to this environment having production-replicated - # data and us not wanting to inadvertently transmit messages to actual recipients. - # - # The lack of a transaction block will prevent rollbacks on the records created in the - # notifications table and allow for observations around notification accuracy to be - # more easily obtained. - def transaction_wrapper - ActiveRecord::Base.transaction { yield } - end - def event_type message.template_name end diff --git a/app/jobs/sync_reviews_job.rb b/app/jobs/sync_reviews_job.rb index e7be4761efb..92a26c5aa81 100644 --- a/app/jobs/sync_reviews_job.rb +++ b/app/jobs/sync_reviews_job.rb @@ -58,7 +58,7 @@ def perform_decision_rating_issues_syncs(limit) def reprocess_decision_documents(limit) DecisionDocument.requires_processing.limit(limit).each do |decision_document| - ProcessDecisionDocumentJob.perform_later(decision_document.id) + ProcessDecisionDocumentJob.perform_later(decision_document.id, decision_document.for_contested_claim?) end end end diff --git a/app/jobs/va_notify_status_update_job.rb b/app/jobs/va_notify_status_update_job.rb deleted file mode 100644 index 0072204ab5d..00000000000 --- a/app/jobs/va_notify_status_update_job.rb +++ /dev/null @@ -1,187 +0,0 @@ -# frozen_string_literal: true - -class VANotifyStatusUpdateJob < CaseflowJob - queue_with_priority :low_priority - application_attr :hearing_schedule - - QUERY_LIMIT = ENV["VA_NOTIFY_STATUS_UPDATE_BATCH_LIMIT"] - VALID_NOTIFICATION_STATUSES = %w[Success temporary-failure technical-failure sending created].freeze - - # Description: Jobs main perform method that will find all notification records that do not have - # status updates from VA Notify and calls VA Notify API to get the latest status - # - # Params: None - # - # Retuns: None - def perform # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity - notifications_not_processed.each do |notification| - sms_external_id = notification.sms_notification_external_id - email_external_id = notification.email_notification_external_id - case notification.notification_type - when "Email" - if !email_external_id.nil? - update_attributes = get_current_status(email_external_id, "Email") - update_notification_audit_record(notification, update_attributes) - else - log_error("Notification Record " + notification.id.to_s + "With Email type does not have an external id.") - update_notification_audit_record(notification, "email_notification_status" => "No External Id") - end - when "SMS" - if !sms_external_id.nil? - update_attributes = get_current_status(sms_external_id, "SMS") - update_notification_audit_record(notification, update_attributes) - else - log_error("Notification Record " + notification.id.to_s + "With SMS type does not have an external id.") - update_notification_audit_record(notification, "sms_notification_status" => "No External Id") - end - when "Email and SMS" - if !email_external_id.nil? - update_attributes = get_current_status(email_external_id, "Email") - update_notification_audit_record(notification, update_attributes) - else - log_error("Notification Record " + notification.id.to_s + "With Email and SMS type does not have an \ - email external id.") - update_notification_audit_record(notification, "email_notification_status" => "No External Id") - end - if !sms_external_id.nil? - update_attributes = get_current_status(sms_external_id, "SMS") - update_notification_audit_record(notification, update_attributes) - else - log_error("Notification Record " + notification.id.to_s + "With Email and SMS type does not have a \ - SMS external id.") - update_notification_audit_record(notification, "sms_notification_status" => "No External Id") - end - end - notification.save! - end - end - - private - - # Description: Method that applies a query limit to the list of notification records that - # will get the status checked for. - # them from VA Notiufy - # - # Params: None - # - # Retuns: Lits of Notification records that has QUERY_LIMIT or less records - def notifications_not_processed - if !QUERY_LIMIT.nil? && QUERY_LIMIT.is_a?(String) - find_notifications_not_processed.first(QUERY_LIMIT.to_i) - else - log_info("VANotifyStatusJob can not read the VA_NOTIFY_STATUS_UPDATE_BATCH_LIMIT environment variable.\ - Defaulting to 650.") - find_notifications_not_processed.first(650) - end - end - - # Description: Method to query the Notification database for Notififcation - # records that have not been updated with a VA Notify Status - # - # Params: None - # - # Retuns: Lits of Notification Active Record associations meeting the where condition - def find_notifications_not_processed - Notification.select(Arel.star).where( - Arel::Nodes::Group.new( - email_status_check.or( - sms_status_check.or( - email_and_sms_status_check - ) - ) - ) - ) - .where(created_at: 4.days.ago..Time.zone.now) - .order(created_at: :desc) - end - - def email_status_check - Notification.arel_table[:notification_type].eq("Email").and( - generate_valid_status_check(:email_notification_status) - ) - end - - def sms_status_check - Notification.arel_table[:notification_type].eq("SMS").and( - generate_valid_status_check(:sms_notification_status) - ) - end - - def email_and_sms_status_check - Notification.arel_table[:notification_type].eq("Email and SMS").and( - generate_valid_status_check(:email_notification_status).or( - generate_valid_status_check(:sms_notification_status) - ) - ) - end - - def generate_valid_status_check(col_name_sym) - Notification.arel_table[col_name_sym].in(VALID_NOTIFICATION_STATUSES) - end - - # Description: Method to be called when an error message need to be logged - # - # Params: Error message to be logged - # - # Retuns: None - def log_error(message) - Rails.logger.error(message) - end - - # Description: Method to be called when an info message need to be logged - # - # Params: Info message to be logged - # - # Retuns: None - def log_info(message) - Rails.logger.info(message) - end - - # Description: Method that will get the VA Notify Status for the notification based on notification type - # - # - # Params: - # notification_id - The external id that VA Notify assigned to each notification. Can be for Email or SMS - # type - Type of notification to get status for - # values - Email, SMS or Email and SMS - # - # Retuns: Return a hash of attributes that need to be updated on the notification record - def get_current_status(notification_id, type) - begin - response = VANotifyService.get_status(notification_id) - if type == "Email" - { "email_notification_status" => response.body["status"], "recipient_email" => response.body["email_address"] } - elsif type == "SMS" - { "sms_notification_status" => response.body["status"], "recipient_phone_number" => - response.body["phone_number"] } - else - message = "Type neither email nor sms" - log_error("VA Notify API returned error for notificiation " + notification_id + " with type " + type) - Raven.capture_exception(type, extra: { error_uuid: error_uuid, message: message }) - end - rescue Caseflow::Error::VANotifyApiError => error - log_error( - "VA Notify API returned error for notification " + notification_id + " with error #{error}" - ) - Raven.capture_exception(error, extra: { error_uuid: error_uuid }) - nil - end - end - - # Description: Method that will update the notification record values - # - # Params: - # notification_audit_record - Notification Record to be updated - # to_update - Hash containing the column names and values to be updated - # - # Retuns: Lits of Notification records that has QUERY_LIMIT or less records - def update_notification_audit_record(notification_audit_record, to_update) - to_update&.each do |key, value| - notification_audit_record[key] = value - end - end -end - -def error_uuid - @error_uuid ||= SecureRandom.uuid -end diff --git a/app/models/appeal_state.rb b/app/models/appeal_state.rb index 6acd80c9c8f..c71700f39b0 100644 --- a/app/models/appeal_state.rb +++ b/app/models/appeal_state.rb @@ -443,9 +443,14 @@ def update_appeal_state_action!(status_to_update) if status_to_update == :appeal_cancelled existing_statuses.merge!({ privacy_act_complete: false, - privacy_act_pending: false + privacy_act_pending: false, + appeal_docketed: false }) end + + if status_to_update == :decision_mailed + existing_statuses[:appeal_docketed] = false + end end) end end diff --git a/app/models/decision_document.rb b/app/models/decision_document.rb index ec6496e8321..13d7a0ec93b 100644 --- a/app/models/decision_document.rb +++ b/app/models/decision_document.rb @@ -61,12 +61,16 @@ def submit_for_processing!(delay: processing_delay) super if not_processed_or_decision_date_not_in_the_future? - ProcessDecisionDocumentJob.perform_later(id, mail_package) + # Below we're grabbing the boolean value at this point in time. + # This will act as a point of truth that wont be affected by the + # async behavior of the outcode function due to triggering jobs. + + ProcessDecisionDocumentJob.perform_later(id, for_contested_claim?, mail_package) end end # rubocop:disable Metrics/CyclomaticComplexity - def process!(mail_package) + def process!(_contested, mail_package) return if processed? fail NotYetSubmitted unless submitted_and_ready? @@ -118,6 +122,15 @@ def all_contention_records(epe) contention_records(epe) end + def for_contested_claim? + case appeal_type + when "Appeal" + appeal.contested_claim? + when "LegacyAppeal" + appeal.contested_claim + end + end + private attr_reader :mail_package diff --git a/app/models/prepend/va_notify/appeal_decision_mailed.rb b/app/models/prepend/va_notify/appeal_decision_mailed.rb index 708af3e4955..9618ed5af0d 100644 --- a/app/models/prepend/va_notify/appeal_decision_mailed.rb +++ b/app/models/prepend/va_notify/appeal_decision_mailed.rb @@ -12,16 +12,11 @@ module AppealDecisionMailed # Params: none # # Response: returns true if successfully processed, returns false if not successfully processed (will not notify) - def process!(mail_package = nil) + def process!(contested, mail_package = nil) super_return_value = super if processed? appeal.appeal_state.decision_mailed_appeal_state_update_action! - case appeal_type - when "Appeal" - template = appeal.contested_claim? ? CONTESTED_CLAIM : NON_CONTESTED_CLAIM - when "LegacyAppeal" - template = appeal.contested_claim ? CONTESTED_CLAIM : NON_CONTESTED_CLAIM - end + template = contested ? CONTESTED_CLAIM : NON_CONTESTED_CLAIM AppellantNotification.notify_appellant(appeal, template) end super_return_value diff --git a/app/models/prepend/va_notify/appellant_notification.rb b/app/models/prepend/va_notify/appellant_notification.rb index 77e4df6ec8b..e13f36dbaf6 100644 --- a/app/models/prepend/va_notify/appellant_notification.rb +++ b/app/models/prepend/va_notify/appellant_notification.rb @@ -23,10 +23,23 @@ def status end end + class InactiveAppealError < StandardError + def initialize(appeal_id, message = "The appeal status is inactive") + super(message + " for appeal with id #{appeal_id}") + end + + def status + "Inactive" + end + end + class NoAppealError < StandardError; end - def self.handle_errors(appeal) + def self.handle_errors(appeal, template_name) fail NoAppealError if appeal.nil? + if template_name == Constants.EVENT_TYPE_FILTERS.quarterly_notification && !appeal.active? + fail InactiveAppealError, appeal.external_id + end message_attributes = {} message_attributes[:appeal_type] = appeal.class.to_s @@ -68,7 +81,7 @@ def self.notify_appellant( end def self.create_payload(appeal, template_name, appeal_status = nil) - message_attributes = AppellantNotification.handle_errors(appeal) + message_attributes = AppellantNotification.handle_errors(appeal, template_name) VANotifySendMessageTemplate.new(message_attributes, template_name, appeal_status) end diff --git a/app/services/sqs_service.rb b/app/services/sqs_service.rb new file mode 100644 index 00000000000..9f56544df56 --- /dev/null +++ b/app/services/sqs_service.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +# A service class to aid in interacting with Caseflow's SQS queues. +class SqsService + class << self + # Intializes an SQS client, or returns a cached version if one has already been initialized. + # + # @return [Aws::SQS::Client] + # An SQS Client + def sqs_client + @sqs_client ||= initialize_sqs_client + end + + # Locates the URL for a SQS queue based on a provided substring. + # + # @param name [String] A substring of the queue's name being searched for. + # @param check_fifo [Boolean] Whether or not the queue being searched for should be for a FIFO queue. + # + # @return [String] The full URL of the SQS queue whose name contains the substring provided. + def find_queue_url_by_name(name:, check_fifo: false) + url = sqs_client.list_queues.queue_urls.find { _1.include?(name) && _1.include?(ENV["DEPLOY_ENV"]) } + + fail Caseflow::Error::SqsQueueNotFoundError, "The #{name} SQS queue is missing in this environment." unless url + + # Optional validation check + if check_fifo && !url.include?(".fifo") + fail Caseflow::Error::SqsUnexpectedQueueTypeError, "No FIFO queue with name #{name} could be located." + end + + url + end + + # Removes the messages provided from a specified queue. + # + # @param queue_url [String] The URL of the SQS queue that the messages will be deleted from. + # @param messages [Array] Messages to be deleted. + def batch_delete_messages(queue_url:, messages:) + messages.in_groups_of(10, false).flat_map do |msg_batch| + sqs_client.delete_message_batch({ + queue_url: queue_url, + entries: process_entries_for_batch_delete(msg_batch) + }) + end + end + + private + + # Intializes an SQS client. Takes into account SQS endpoint overrides and applies them + # to the instantiated client object. + # + # @return [Aws::SQS::Client] + # An SQS Client + def initialize_sqs_client + sqs_client = Aws::SQS::Client.new + + # Allow for overriding the endpoint requests are sent to via the Rails config. + if Rails.application.config.sqs_endpoint + sqs_client.config[:endpoint] = URI(Rails.application.config.sqs_endpoint) + end + + sqs_client + end + + # Prepares a batch of messages to be in the format needed for the SQS SDK's delete_message_batch method. + # + # @param unprocessed_entries [Array] Messages to be deleted. + # + # @return [Array] An array where each entry is a hash that contains a unique (per batch) + # id and a message's receipt handle. + def process_entries_for_batch_delete(unprocessed_entries) + unprocessed_entries.map.with_index do |msg, index| + { + id: "message_#{index}", + receipt_handle: msg.receipt_handle + } + end + end + end +end diff --git a/client/app/queue/components/NotificationTableColumns.jsx b/client/app/queue/components/NotificationTableColumns.jsx index 592e8c775af..e23aa966832 100644 --- a/client/app/queue/components/NotificationTableColumns.jsx +++ b/client/app/queue/components/NotificationTableColumns.jsx @@ -81,7 +81,7 @@ export const recipientInformationColumn = (notifications) => { tableData: notifications, valueName: 'Recipient Information', // eslint-disable-next-line no-negated-condition - valueFunction: (notification) => notification.status !== 'delivered' ? 'ā€”' : notification.recipient_information + valueFunction: (notification) => notification.recipient_information ?? 'ā€”' }; }; diff --git a/client/constants/QUARTERLY_STATUSES.json b/client/constants/QUARTERLY_STATUSES.json index e6df95484e1..c6891b266e8 100644 --- a/client/constants/QUARTERLY_STATUSES.json +++ b/client/constants/QUARTERLY_STATUSES.json @@ -4,7 +4,7 @@ "hearing_scheduled": "Hearing Scheduled", "privacy_pending": "Privacy Act Pending", "ihp_pending": "VSO IHP Pending", - "hearing_to_be_rescheduled": "Hearing to be Rescheduled", - "hearing_to_be_rescheduled_privacy_pending": "Hearing to be Rescheduled / Privacy Act Pending", + "hearing_to_be_rescheduled": "docketed", + "hearing_to_be_rescheduled_privacy_pending": "Privacy Act Pending", "appeal_docketed": "docketed" } diff --git a/client/constants/VA_NOTIFY_CONSTANTS.json b/client/constants/VA_NOTIFY_CONSTANTS.json new file mode 100644 index 00000000000..5c30dead852 --- /dev/null +++ b/client/constants/VA_NOTIFY_CONSTANTS.json @@ -0,0 +1,3 @@ +{ + "message_group_id": "VANotifyStatusUpdate" +} diff --git a/client/test/data/notifications.js b/client/test/data/notifications.js index b31dd46ccf6..af53219e4c0 100644 --- a/client/test/data/notifications.js +++ b/client/test/data/notifications.js @@ -118,9 +118,9 @@ export const notifications = [ notification_type: 'Email and SMS', event_date: '2022-10-27', event_type: 'Appeal decision mailed (Non-contested claims)', - recipient_email: 'test@caseflow.com', + recipient_email: null, recipient_phone_number: '2468012345', - email_notification_status: 'sent', + email_notification_status: 'temporary-failure', sms_notification_status: 'delivered', notification_content: 'string' } diff --git a/config/environments/demo.rb b/config/environments/demo.rb index 2000da40c1b..474107fc1eb 100644 --- a/config/environments/demo.rb +++ b/config/environments/demo.rb @@ -85,6 +85,8 @@ # eFolder Express URL for demo environment used as a mock link ENV["EFOLDER_EXPRESS_URL"] ||= "http://localhost:4000" + ENV["CASEFLOW_BASE_URL"] ||= "https://www.demo.appeals.va.gov" + # BatchProcess ENVs # priority_ep_sync ENV["BATCH_PROCESS_JOB_DURATION"] ||= "1" # Number of hours the job will run for diff --git a/config/environments/development.rb b/config/environments/development.rb index c2c8b84f409..b9d9e2c9c33 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -116,10 +116,10 @@ # Set to true to get the documents from efolder running locally on port 4000. config.use_efolder_locally = false - # set to true to create queues and override the sqs endpiont + # set to true to create queues and override the sqs endpoint config.sqs_create_queues = true - config.sqs_endpoint = ENV.has_key?('DOCKERIZED') ? 'http://localstack:4576' : 'http://localhost:4576' + config.sqs_endpoint = ENV.has_key?('DOCKERIZED') ? 'http://localstack:4566' : 'http://localhost:4566' # since we mock aws using localstack, provide dummy creds to the aws gem ENV["AWS_ACCESS_KEY_ID"] ||= "dummykeyid" @@ -146,12 +146,17 @@ # One time Appeal States migration for Legacy & AMA Appeal Batch Sizes ENV["STATE_MIGRATION_JOB_BATCH_SIZE"] ||= "1000" + # Syncing decided appeals in select batch sizes + ENV["VACOLS_QUERY_BATCH_SIZE"] ||= "800" + # Travel Board Sync Batch Size ENV["TRAVEL_BOARD_HEARING_SYNC_BATCH_LIMIT"] ||= "250" # Time in seconds before the sync lock expires LOCK_TIMEOUT = ENV["SYNC_LOCK_MAX_DURATION"] ||= "60" + ENV["CASEFLOW_BASE_URL"] ||= "http://localhost:3000" + # Notifications page eFolder link ENV["CLAIM_EVIDENCE_EFOLDER_BASE_URL"] ||= "https://vefs-claimevidence-ui-uat.stage.bip.va.gov" diff --git a/config/environments/test.rb b/config/environments/test.rb index 49d5e11344f..3df218a1844 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -135,6 +135,9 @@ # One time Appeal States migration for Legacy & AMA Appeal Batch Sizes ENV["STATE_MIGRATION_JOB_BATCH_SIZE"] ||= "1000" + # Syncing decided appeals in select batch sizes + ENV["VACOLS_QUERY_BATCH_SIZE"] ||= "800" + # Travel Board Sync Batch Size ENV["TRAVEL_BOARD_HEARING_SYNC_BATCH_LIMIT"] ||= "250" @@ -156,4 +159,7 @@ # Dynatrace variables ENV["STATSD_ENV"] = "test" + + config.sqs_create_queues = true + config.sqs_endpoint = ENV["CI"] ? 'http://localstack:4566' : 'http://localhost:4566' end diff --git a/config/initializers/message_queues.rb b/config/initializers/message_queues.rb new file mode 100644 index 00000000000..c8832581647 --- /dev/null +++ b/config/initializers/message_queues.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +# Initializes SQS message queues not for intended for use with +# asynchronous jobs. +# +# This will primarily be utilized in our development and demo environments. + +QUEUE_PREFIX = "caseflow_#{ENV['DEPLOY_ENV']}_" + +MESSAGE_QUEUES = [ + { + name: "receive_notifications.fifo", + attributes: { + "FifoQueue" => "true", + "FifoThroughputLimit" => "perQueue" + } + } +].freeze + +if Rails.application.config.sqs_create_queues + sqs_client = Aws::SQS::Client.new + sqs_client.config[:endpoint] = URI(Rails.application.config.sqs_endpoint) + + MESSAGE_QUEUES.each do |queue_info| + sqs_client.create_queue({ + queue_name: "#{QUEUE_PREFIX}#{queue_info[:name]}".to_sym, + attributes: queue_info[:attributes] + }) + end +end diff --git a/config/initializers/scheduled_jobs.rb b/config/initializers/scheduled_jobs.rb index 1f706fa375a..73293371f1d 100644 --- a/config/initializers/scheduled_jobs.rb +++ b/config/initializers/scheduled_jobs.rb @@ -39,7 +39,6 @@ "update_appellant_representation_job" => UpdateAppellantRepresentationJob, "update_cached_appeals_attributes_job" => UpdateCachedAppealsAttributesJob, "warm_bgs_caches_job" => WarmBgsCachesJob, - "va_notify_status_update_job" => VANotifyStatusUpdateJob, "poll_docketed_legacy_appeals_job" => PollDocketedLegacyAppealsJob, "retrieve_and_cache_reader_documents_job" => RetrieveAndCacheReaderDocumentsJob, "travel_board_hearing_sync_job" => Hearings::TravelBoardHearingSyncJob, diff --git a/config/initializers/shoryuken.rb b/config/initializers/shoryuken.rb index 84400df8a42..bc2377beab6 100644 --- a/config/initializers/shoryuken.rb +++ b/config/initializers/shoryuken.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "#{Rails.root}/app/jobs/middleware/job_monitoring_middleware" require "#{Rails.root}/app/jobs/middleware/job_request_store_middleware" require "#{Rails.root}/app/jobs/middleware/job_sentry_scope_middleware" @@ -9,16 +11,23 @@ .shoryuken_options(retry_intervals: [3.seconds, 30.seconds, 5.minutes, 30.minutes, 2.hours, 5.hours]) if Rails.application.config.sqs_endpoint - # override the sqs_endpoint Shoryuken::Client.sqs.config[:endpoint] = URI(Rails.application.config.sqs_endpoint) end if Rails.application.config.sqs_create_queues # create the development queues - Shoryuken::Client.sqs.create_queue({ queue_name: ActiveJob::Base.queue_name_prefix + '_low_priority' }) - Shoryuken::Client.sqs.create_queue({ queue_name: ActiveJob::Base.queue_name_prefix + '_high_priority' }) - Shoryuken::Client.sqs.create_queue({ queue_name: ActiveJob::Base.queue_name_prefix + '_send_notifications' }) - Shoryuken::Client.sqs.create_queue({ queue_name: ActiveJob::Base.queue_name_prefix + '_receive_notifications' }) + Shoryuken::Client.sqs.create_queue({ queue_name: ActiveJob::Base.queue_name_prefix + "_low_priority" }) + Shoryuken::Client.sqs.create_queue({ queue_name: ActiveJob::Base.queue_name_prefix + "_high_priority" }) + Shoryuken::Client.sqs.create_queue({ + queue_name: ( + ActiveJob::Base.queue_name_prefix + "_send_notifications.fifo" + ).to_sym, + attributes: { + "FifoQueue" => "true", + "FifoThroughputLimit" => "perQueue", + "ContentBasedDeduplication" => "false" + } + }) end Shoryuken.configure_server do |config| diff --git a/config/initializers/va_notify.rb b/config/initializers/va_notify.rb index de650eac2fc..80d05092136 100644 --- a/config/initializers/va_notify.rb +++ b/config/initializers/va_notify.rb @@ -1 +1,6 @@ -VANotifyService = (ApplicationController.dependencies_faked? ? Fakes::VANotifyService : ExternalApi::VANotifyService) \ No newline at end of file +case Rails.deploy_env +when :uat, :prod + VANotifyService = ExternalApi::VANotifyService +else + VANotifyService = Fakes::VANotifyService +end diff --git a/db/migrate/20240717145856_add_sm_sand_email_status_to_notifications.rb b/db/migrate/20240717145856_add_sm_sand_email_status_to_notifications.rb new file mode 100644 index 00000000000..ee83c718534 --- /dev/null +++ b/db/migrate/20240717145856_add_sm_sand_email_status_to_notifications.rb @@ -0,0 +1,6 @@ +class AddSmSandEmailStatusToNotifications < ActiveRecord::Migration[6.0] + def change + add_column :notifications, :sms_status_reason, :string, comment: "Context around why this VA Notify notification is in the sms status" + add_column :notifications, :email_status_reason, :string, comment: "Context around why this VA Notify notification is in the email status" + end +end diff --git a/db/schema.rb b/db/schema.rb index a1fcc561b94..3afc92fdb92 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -1450,6 +1450,7 @@ t.string "email_notification_content", comment: "Full Email Text Content of Notification" t.string "email_notification_external_id", comment: "VA Notify Notification Id for the email notification send through their API " t.string "email_notification_status", comment: "Status of the Email Notification" + t.string "email_status_reason", comment: "Context around why this VA Notify notification is in the email status" t.date "event_date", null: false, comment: "Date of Event" t.string "event_type", null: false, comment: "Type of Event" t.bigint "notifiable_id" @@ -1465,6 +1466,7 @@ t.string "sms_notification_status", comment: "Status of SMS/Text Notification" t.string "sms_response_content", comment: "Message body of the sms notification response." t.datetime "sms_response_time", comment: "Date and Time of the sms notification response." + t.string "sms_status_reason", comment: "Context around why this VA Notify notification is in the sms status" t.datetime "updated_at", comment: "TImestamp of when Notification was Updated" t.index ["appeals_id", "appeals_type"], name: "index_appeals_notifications_on_appeals_id_and_appeals_type" t.index ["email_notification_external_id"], name: "index_notifications_on_email_notification_external_id" diff --git a/db/seeds/api_keys.rb b/db/seeds/api_keys.rb index 129c5fc8c86..0f27499b462 100644 --- a/db/seeds/api_keys.rb +++ b/db/seeds/api_keys.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -# create ApiKey seeds - module Seeds require "./app/models/api_key.rb" @@ -13,9 +11,10 @@ def seed! private def create_api_keys - ApiKey.create(consumer_name: "appeals_consumer", key_digest: "z1VxSVb2iae07+bYq8ZjQZs3ll4ZgSeVIUC9O5u+HfA=", - key_string: "5ecb5d7b440e429bb5fac331419c7e1a") + ApiKey.create!(consumer_name: "TestApiKey", key_string: "test") + ApiKey.create(consumer_name: "appeals_consumer", + key_digest: "z1VxSVb2iae07+bYq8ZjQZs3ll4ZgSeVIUC9O5u+HfA=", + key_string: "5ecb5d7b440e429bb5fac331419c7e1a") end end end - diff --git a/docker-compose-m1.yml b/docker-compose-m1.yml index 6d0f25a0f2b..7f8da3a3074 100644 --- a/docker-compose-m1.yml +++ b/docker-compose-m1.yml @@ -19,10 +19,9 @@ services: appeals-localstack-aws: platform: linux/amd64 container_name: localstack - image: localstack/localstack:0.11.4 + image: localstack/localstack:0.14.5 ports: - - "4567-4583:4567-4583" - - "8082:${PORT_WEB_UI-8080}" + - "4566:4566" environment: - SERVICES=sqs volumes: diff --git a/docker-compose.yml b/docker-compose.yml index 02ec69bc364..0ecd5d4fc06 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -24,10 +24,9 @@ services: appeals-localstack-aws: container_name: localstack - image: localstack/localstack:0.11.4 + image: localstack/localstack:0.14.5 ports: - - "4567-4583:4567-4583" - - "8082:${PORT_WEB_UI-8080}" + - "4566:4566" environment: - SERVICES=sqs volumes: diff --git a/lib/caseflow/error.rb b/lib/caseflow/error.rb index c60dc7bd5e9..69f46c0bdfb 100644 --- a/lib/caseflow/error.rb +++ b/lib/caseflow/error.rb @@ -510,4 +510,8 @@ def initialize(msg = "The batch size of jobs must not exceed 10") super(msg) end end + + class SqsUnexpectedQueueTypeError < StandardError; end + class SqsQueueNotFoundError < StandardError; end + class SqsQueueExhaustionError < StandardError; end end diff --git a/lib/fakes/va_notify_service.rb b/lib/fakes/va_notify_service.rb index 1dc5d00999a..e8b9cb98d7b 100644 --- a/lib/fakes/va_notify_service.rb +++ b/lib/fakes/va_notify_service.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Fakes::VANotifyService < ExternalApi::VANotifyService + VA_NOTIFY_ENDPOINT = "/api/v1/va_notify_update" + class << self # rubocop:disable Metrics/ParameterLists def send_email_notifications( @@ -11,7 +13,21 @@ def send_email_notifications( docket_number:, status: "" ) - fake_notification_response(email_template_id) + + external_id = SecureRandom.uuid + + unless Rails.deploy_env == :test + request = HTTPI::Request.new + request.url = "#{ENV['CASEFLOW_BASE_URL']}#{VA_NOTIFY_ENDPOINT}"\ + "?id=#{external_id}&status=delivered&to=test@example.com¬ification_type=email" + request.headers["Content-Type"] = "application/json" + request.headers["Authorization"] = "Bearer test" + request.auth.ssl.ca_cert_file = ENV["SSL_CERT_FILE"] + + HTTPI.post(request) + end + + fake_notification_response(email_template_id, status, external_id) end def send_sms_notifications( @@ -22,11 +38,25 @@ def send_sms_notifications( docket_number:, status: "" ) + + external_id = SecureRandom.uuid + + unless Rails.deploy_env == :test + request = HTTPI::Request.new + request.url = "#{ENV['CASEFLOW_BASE_URL']}#{VA_NOTIFY_ENDPOINT}"\ + "?id=#{external_id}&status=delivered&to=+15555555555¬ification_type=sms" + request.headers["Content-Type"] = "application/json" + request.headers["Authorization"] = "Bearer test" + request.auth.ssl.ca_cert_file = ENV["SSL_CERT_FILE"] + + HTTPI.post(request) + end + if participant_id.length.nil? return bad_participant_id_response end - fake_notification_response(sms_template_id) + fake_notification_response(sms_template_id, status, external_id) end # rubocop:enable Metrics/ParameterLists @@ -102,23 +132,23 @@ def bad_notification_response ) end - def fake_notification_response(email_template_id) + def fake_notification_response(template_id, status, external_id) HTTPI::Response.new( 200, {}, OpenStruct.new( - "id": SecureRandom.uuid, + "id": external_id, "reference": "string", "uri": "string", "template": { - "id" => email_template_id, + "id" => template_id, "version" => 0, "uri" => "string" }, "scheduled_for": "string", "content": { - "body" => "string", - "subject" => "string" + "body" => "Template: #{template_id} - Status: #{status}", + "subject" => "Test Subject" } ) ) diff --git a/lib/tasks/appeal_state_synchronizer.rake b/lib/tasks/appeal_state_synchronizer.rake index cd8e1f29149..ca1094a7c04 100644 --- a/lib/tasks/appeal_state_synchronizer.rake +++ b/lib/tasks/appeal_state_synchronizer.rake @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "#{Rails.root}/app/helpers/sync_decided_appeals_helper.rb" + namespace :appeal_state_synchronizer do desc "Used to synchronize appeal_states table using data from other sources." task sync_appeal_states: :environment do @@ -11,6 +13,12 @@ namespace :appeal_state_synchronizer do backfill_appeal_information end + task sync_legacy_appeal_decisions: :environment do + include SyncDecidedAppealsHelper + + sync_decided_appeals + end + def map_appeal_hearing_scheduled_state(appeal_state) if !appeal_state.appeal&.hearings&.empty? && appeal_state.appeal.hearings.max_by(&:scheduled_for).disposition.nil? return { hearing_scheduled: true } diff --git a/spec/controllers/api/v1/va_notify_controller_spec.rb b/spec/controllers/api/v1/va_notify_controller_spec.rb index 895b7bd04f9..a3d01684b6b 100644 --- a/spec/controllers/api/v1/va_notify_controller_spec.rb +++ b/spec/controllers/api/v1/va_notify_controller_spec.rb @@ -4,9 +4,13 @@ include ActiveJob::TestHelper before { Seeds::NotificationEvents.new.seed! } + before(:each) { wipe_queues } + after(:all) { wipe_queues } + let(:sqs_client) { SqsService.sqs_client } let(:api_key) { ApiKey.create!(consumer_name: "API Consumer").key_string } let!(:appeal) { create(:appeal) } + let!(:queue) { create_queue("receive_notifications", true) } let!(:notification_email) do create( :notification, @@ -27,7 +31,7 @@ appeals_type: "Appeal", event_date: "2023-02-27 13:11:51.91467", event_type: Constants.EVENT_TYPE_FILTERS.quarterly_notification, - notification_type: "Email", + notification_type: "Sms", notified_at: "2023-02-28 14:11:51.91467", sms_notification_external_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6", sms_notification_status: "Preferences Declined" @@ -36,6 +40,66 @@ let(:default_payload) do { id: "3fa85f64-5717-4562-b3fc-2c963f66afa6", + to: "to", + status_reason: "status_reason", + body: "string", + completed_at: "2023-04-17T12:38:48.699Z", + created_at: "2023-04-17T12:38:48.699Z", + created_by_name: "string", + email_address: "user@example.com", + line_1: "string", + line_2: "string", + line_3: "string", + line_4: "string", + line_5: "string", + line_6: "string", + phone_number: "+16502532222", + postage: "string", + postcode: "string", + reference: "string", + scheduled_for: "2023-04-17T12:38:48.699Z", + sent_at: "2023-04-17T12:38:48.699Z", + sent_by: "string", + status: "created", + subject: "string", + notification_type: "Email" + } + end + + let(:error_payload1) do + { + id: "3fa85f64-5717-4562-b3fc-2c963f66afa6", + to: "to", + status_reason: nil, + body: "string", + completed_at: "2023-04-17T12:38:48.699Z", + created_at: "2023-04-17T12:38:48.699Z", + created_by_name: "string", + email_address: "user@example.com", + line_1: "string", + line_2: "string", + line_3: "string", + line_4: "string", + line_5: "string", + line_6: "string", + phone_number: "+16502532222", + postage: "string", + postcode: "string", + reference: "string", + scheduled_for: "2023-04-17T12:38:48.699Z", + sent_at: "2023-04-17T12:38:48.699Z", + sent_by: "string", + status: "created", + subject: "string", + notification_type: "Email" + } + end + + let(:error_payload2) do + { + id: nil, + to: "to", + status_reason: "status_reason", body: "string", completed_at: "2023-04-17T12:38:48.699Z", created_at: "2023-04-17T12:38:48.699Z", @@ -56,11 +120,13 @@ sent_by: "string", status: "created", subject: "string", - notification_type: "" + notification_type: "Emailx" } end context "email notification status is changed" do + before { Seeds::NotificationEvents.new.seed! } + let(:payload_email) do default_payload.deep_dup.tap do |payload| payload[:notification_type] = "email" @@ -72,7 +138,6 @@ post :notifications_update, params: payload_email perform_enqueued_jobs { ProcessNotificationStatusUpdatesJob.perform_later } - expect(notification_email.reload.email_notification_status).to eq("created") end end @@ -89,7 +154,6 @@ post :notifications_update, params: payload_sms perform_enqueued_jobs { ProcessNotificationStatusUpdatesJob.perform_later } - expect(notification_sms.reload.sms_notification_status).to eq("created") end end @@ -98,6 +162,8 @@ let(:payload_fake) do { id: "fake", + to: "to", + status_reason: "status_reason", body: "string", completed_at: "2023-04-17T12:38:48.699Z", created_at: "2023-04-17T12:38:48.699Z", @@ -133,16 +199,156 @@ } end - it "Update job raises error if UUID is passed in for a non-existant notification" do - expect_any_instance_of(ProcessNotificationStatusUpdatesJob).to receive(:log_error) do |_job, error| - expect(error.message).to eq("No notification matches UUID #{payload_fake.dig(:id)}") + it "Update job runs cleanly when UUID is missing" do + request.headers["Authorization"] = "Bearer #{api_key}" + post :notifications_update, params: payload_fake + expect(response.status).to eq(200) + + perform_enqueued_jobs { ProcessNotificationStatusUpdatesJob.perform_later } + end + end + + context "payload missing required params" do + before { Seeds::NotificationEvents.new.seed! } + + let(:payload_email) do + error_payload1.deep_dup.tap do |payload| + payload[:notification_type] = "email" end + end + it "is missing the id and properly errors out" do request.headers["Authorization"] = "Bearer #{api_key}" - post :notifications_update, params: payload_fake + post :notifications_update, params: payload_email + + expect(response.status).to eq(200) + + perform_enqueued_jobs { ProcessNotificationStatusUpdatesJob.perform_later } + end + end + + context "payload status is delivered and status_reason and to are null" do + before { Seeds::NotificationEvents.new.seed! } + let(:payload) do + error_payload1.deep_dup.tap do |payload| + payload[:status] = "delivered" + payload[:status_reason] = nil + payload[:to] = nil + end + end + + it "updates status of notification" do + request.headers["Authorization"] = "Bearer #{api_key}" + post :notifications_update, params: payload + + perform_enqueued_jobs { ProcessNotificationStatusUpdatesJob.perform_later } + expect(response.status).to eq(200) + end + end + + context "payload status is delivered and status_reason is null" do + before { Seeds::NotificationEvents.new.seed! } + let(:payload) do + error_payload1.deep_dup.tap do |payload| + payload[:status] = "delivered" + payload[:status_reason] = nil + end + end + + it "updates status of notification" do + request.headers["Authorization"] = "Bearer #{api_key}" + post :notifications_update, params: payload + + perform_enqueued_jobs { ProcessNotificationStatusUpdatesJob.perform_later } + expect(response.status).to eq(200) + end + end + + context "payload status is delivered and to is null" do + before { Seeds::NotificationEvents.new.seed! } + let(:payload) do + error_payload1.deep_dup.tap do |payload| + payload[:status] = "delivered" + payload[:to] = nil + end + end + + it "updates status of notification" do + request.headers["Authorization"] = "Bearer #{api_key}" + post :notifications_update, params: payload + + perform_enqueued_jobs { ProcessNotificationStatusUpdatesJob.perform_later } + expect(response.status).to eq(200) + end + end + + context "payload status is NOT delivered and status reason and to are null" do + before { Seeds::NotificationEvents.new.seed! } + let(:payload) do + error_payload1.deep_dup.tap do |payload| + payload[:status] = "Pending Delivery" + payload[:to] = nil + payload[:status_reason] = nil + end + end + + it "updates status of notification" do + request.headers["Authorization"] = "Bearer #{api_key}" + post :notifications_update, params: payload + + perform_enqueued_jobs { ProcessNotificationStatusUpdatesJob.perform_later } expect(response.status).to eq(200) + end + end + + context "payload status is NOT delivered and status reason is null" do + before { Seeds::NotificationEvents.new.seed! } + let(:payload) do + error_payload1.deep_dup.tap do |payload| + payload[:status] = "Pending Delivery" + payload[:status_reason] = nil + end + end + + it "updates status of notification" do + request.headers["Authorization"] = "Bearer #{api_key}" + post :notifications_update, params: payload perform_enqueued_jobs { ProcessNotificationStatusUpdatesJob.perform_later } + expect(response.status).to eq(200) end end + + context "payload status is NOT delivered and to is null" do + before { Seeds::NotificationEvents.new.seed! } + let(:payload) do + error_payload1.deep_dup.tap do |payload| + payload[:status] = "Pending Delivery" + payload[:to] = nil + end + end + + it "updates status of notification" do + request.headers["Authorization"] = "Bearer #{api_key}" + post :notifications_update, params: payload + + perform_enqueued_jobs { ProcessNotificationStatusUpdatesJob.perform_later } + expect(response.status).to eq(200) + end + end + + def create_queue(name, fifo = false) + sqs_client.create_queue({ + queue_name: "caseflow_test_#{name}#{fifo ? '.fifo' : ''}".to_sym, + attributes: fifo ? { "FifoQueue" => "true" } : {} + }) + end + + def wipe_queues + client = SqsService.sqs_client + + queues_to_delete = client.list_queues.queue_urls.filter { |url| url.include?("caseflow_test") } + + queues_to_delete.each { |queue_url| client.delete_queue(queue_url: queue_url) } + end end diff --git a/spec/controllers/idt/api/v2/appeals_controller_spec.rb b/spec/controllers/idt/api/v2/appeals_controller_spec.rb index c74497409af..7135ba24eaf 100644 --- a/spec/controllers/idt/api/v2/appeals_controller_spec.rb +++ b/spec/controllers/idt/api/v2/appeals_controller_spec.rb @@ -585,6 +585,37 @@ expect(JSON.parse(response.body)["message"]).to eq("Successful dispatch!") end + context "when notifications are enabled" do + include ActiveJob::TestHelper + let(:veteran) { create(:veteran) } + let(:contested_appeal) do + create( + :legacy_appeal, + vacols_case: create(:case, bfcorlid: veteran.file_number), + vbms_id: "#{veteran.file_number}S" + ) + end + before do + FeatureToggle.enable!(:va_notify_sms) + FeatureToggle.enable!(:va_notify_email) + Seeds::NotificationEvents.new.seed! unless NotificationEvent.count > 0 + end + + it "should send the appeal decision mailed non contested claim notification" do + perform_enqueued_jobs { post :outcode, params: params, as: :json } + + expect(Notification.last.event_type).to eq("Appeal decision mailed (Non-contested claims)") + end + + it "should send the appeal decision mailed contested claim notification" do + VACOLS::Representative.create!(repkey: contested_appeal.vacols_id, reptype: "C") + params[:appeal_id] = contested_appeal.vacols_id + + perform_enqueued_jobs { post :outcode, params: params, as: :json } + expect(Notification.last.event_type).to eq("Appeal decision mailed (Contested claims)") + end + end + context "when dispatch is associated with a mail request" do include ActiveJob::TestHelper diff --git a/spec/helpers/sync_decided_appeals_helper_spec.rb b/spec/helpers/sync_decided_appeals_helper_spec.rb new file mode 100644 index 00000000000..b55dbe97ae7 --- /dev/null +++ b/spec/helpers/sync_decided_appeals_helper_spec.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +require_relative "../../app/helpers/sync_decided_appeals_helper" + +describe "SyncDecidedAppealsHelper" do + self.use_transactional_tests = false + + class Helper + include SyncDecidedAppealsHelper + end + + attr_reader :helper + + subject do + Helper.new + end + + context "#sync_decided_appeals" do + let(:decided_appeal_state) do + create_decided_appeal_state_with_case_record_and_hearing(true, true) + end + + let(:undecided_appeal_state) do + create_decided_appeal_state_with_case_record_and_hearing(false, true) + end + + let(:missing_vacols_case_appeal_state) do + create_decided_appeal_state_with_case_record_and_hearing(true, false) + end + + it "Job syncs decided appeals decision_mailed status", bypass_cleaner: true do + expect([decided_appeal_state, + undecided_appeal_state, + missing_vacols_case_appeal_state].all?(&:decision_mailed)).to eq false + + subject.sync_decided_appeals + + expect(decided_appeal_state.reload.decision_mailed).to eq true + expect(undecided_appeal_state.reload.decision_mailed).to eq false + expect(missing_vacols_case_appeal_state.reload.decision_mailed).to eq false + end + + it "catches standard errors", bypass_cleaner: true do + expect([decided_appeal_state, + undecided_appeal_state, + missing_vacols_case_appeal_state].all?(&:decision_mailed)).to eq false + + error_text = "Fatal error in sync_decided_appeals_helper" + allow(AppealState).to receive(:legacy).and_raise(StandardError.new(error_text)) + + expect(Rails.logger).to receive(:error) + + expect { subject.sync_decided_appeals }.to raise_error(StandardError) + end + + # Clean up parallel threads + after(:each) { clean_up_after_threads } + + # VACOLS record's decision date will be set to simulate a decided appeal + # decision_mailed will be set to false for the AppealState to verify the method + # functionality + def create_decided_appeal_state_with_case_record_and_hearing(decided_appeal, create_case) + case_hearing = create(:case_hearing) + decision_date = decided_appeal ? Time.current : nil + vacols_case = create_case ? create(:case, case_hearings: [case_hearing], bfddec: decision_date) : nil + appeal = create(:legacy_appeal, vacols_case: vacols_case) + + appeal.appeal_state.tap { _1.update!(decision_mailed: false) } + end + + def clean_up_after_threads + DatabaseCleaner.clean_with(:truncation, except: %w[vftypes issref notification_events]) + end + end +end diff --git a/spec/jobs/hearings/receive_notification_job_spec.rb b/spec/jobs/hearings/receive_notification_job_spec.rb deleted file mode 100644 index ab8a326a4ea..00000000000 --- a/spec/jobs/hearings/receive_notification_job_spec.rb +++ /dev/null @@ -1,182 +0,0 @@ -# frozen_string_literal: true - -# Testing plan: -# - 1. Create test records usiong factories and take note of notification ID and specific fields to compare -# - 2. Use custom message defined here to pass in perform method -# - 3. Test perform method by checking if field values in DB recored are equal to the field values in the message, -# - An update to record should only be called whenever there are differences between the message and the record in DB -# - 4. The updated record should be returned - -describe ReceiveNotificationJob, type: :job do - include ActiveJob::TestHelper - let(:current_user) { create(:user, roles: ["System Admin"]) } - # rubocop:disable Style/BlockDelimiters - let(:message) { - { - queue_url: "http://example_queue", - message_body: "Notification", - message_attributes: { - "id": { - data_type: "String", - string_value: "3fa85f64-5717-4562-b3fc-2c963f66afa6" - }, - "body": { - data_type: "String", - string_value: "AString" - }, - "created_at": { - data_type: "String", - string_value: "2022-09-02T20:40:11.184Z" - }, - "completed_at": { - data_type: "String", - string_value: "2022-09-02T20:40:11.184Z" - }, - "created_by_name": { - data_type: "String", - string_value: "John" - }, - "email_address": { - data_type: "String", - string_value: "user@example.com" - }, - "line_1": { - data_type: "String", - string_value: "address" - }, - "line_2": { - data_type: "String", - string_value: "address" - }, - "line_3": { - data_type: "String", - string_value: "address" - }, - "line_4": { - data_type: "String", - string_value: "address" - }, - "line_5": { - data_type: "String", - string_value: "address" - }, - "line_6": { - data_type: "String", - string_value: "address" - }, - "phone_number": { - data_type: "String", - string_value: nil - }, - "postage": { - data_type: "String", - string_value: "postage" - }, - "postcode": { - data_type: "String", - string_value: "postcode" - }, - "reference": { - data_type: "String", - string_value: "9" - }, - "scheduled_for": { - data_type: "String", - string_value: "2022-09-02T20:40:11.184Z" - }, - "sent_at": { - data_type: "String", - string_value: "2022-09-02T20:40:11.184Z" - }, - "sent_by": { - data_type: "String", - string_value: "sent-by" - }, - "status": { - data_type: "String", - string_value: "delivered" - }, - "subject": { - data_type: "String", - string_value: "subject" - }, - "type": { - string_value: "email", - data_type: "String" - } - - } - } - } - - # rubocop:enable Style/BlockDelimiters - let(:queue_name) { "caseflow_test_receive_notifications" } - - after do - clear_enqueued_jobs - clear_performed_jobs - end - - it "it is the correct queue" do - expect(ReceiveNotificationJob.new.queue_name).to eq(queue_name) - end - - context ".perform" do - # create notification event record - let(:hearing_scheduled_event) do - NotificationEvent.find_or_create_by(event_type: Constants.EVENT_TYPE_FILTERS.hearing_scheduled) do |event| - event.email_template_id = "27bf814b-f065-4fc8-89af-ae1292db894e" - event.sms_template_id = "c2798da3-4c7a-43ed-bc16-599329eaf7cc" - end - end - # create notification record - let(:notification) do - create(:notification, id: 9, appeals_id: 4, appeals_type: "Appeal", - event_type: Constants.EVENT_TYPE_FILTERS.hearing_scheduled, - participant_id: "123456789", notification_type: "Email", recipient_email: "", - event_date: Time.zone.now, email_notification_status: "Success") - end - - # add message to queue - subject(:job) { ReceiveNotificationJob.perform_later(message) } - - # make sure job count increases by 1 - describe "send message to queue" do - it "has one message in queue" do - expect { job }.to change(ActiveJob::Base.queue_adapter.enqueued_jobs, :size).by(1) - end - - # After receiving the notification (by notification id), check : if email is same, if number is still nil, - # if status changed form Success to delivered - it "updates notification" do - hearing_scheduled_event - notification - - # obtain record from compare_notification_audit_record function - record = ReceiveNotificationJob.perform_now(message) - - # run checks - expect(record.recipient_email).to eq(message[:message_attributes][:email_address][:string_value]) - expect(record.recipient_phone_number).to eq(nil) - expect(record.email_notification_status).to eq(message[:message_attributes][:status][:string_value].capitalize) - end - end - - describe "errors" do - it "logs error when message is nil" do - expect(Rails.logger).to receive(:error).with(/There was no message passed/) - perform_enqueued_jobs do - ReceiveNotificationJob.perform_later(nil) - end - end - - it "logs error when message_attributes is nil" do - message[:message_attributes] = nil - expect(Rails.logger).to receive(:error).with(/message_attributes was nil/) - perform_enqueued_jobs do - ReceiveNotificationJob.perform_later(message) - end - end - end - end -end diff --git a/spec/jobs/nightly_syncs_job_spec.rb b/spec/jobs/nightly_syncs_job_spec.rb index 700c5672e5b..2af498ffbfb 100644 --- a/spec/jobs/nightly_syncs_job_spec.rb +++ b/spec/jobs/nightly_syncs_job_spec.rb @@ -201,6 +201,23 @@ class FakeTask < Dispatch::Task expect(held_hearing_appeal_state.reload.hearing_scheduled).to eq false end + it "catches standard errors" do + expect([pending_hearing_appeal_state, + postponed_hearing_appeal_state, + withdrawn_hearing_appeal_state, + scheduled_in_error_hearing_appeal_state, + held_hearing_appeal_state].all?(&:hearing_scheduled)).to eq true + + allow(AppealState).to receive(:where).and_raise(StandardError) + slack_msg = "" + slack_msg_error_text = "Fatal error in sync_hearing_states" + allow_any_instance_of(SlackService).to receive(:send_notification) { |_, first_arg| slack_msg = first_arg } + + subject + + expect(slack_msg.include?(slack_msg_error_text)).to be true + end + # Hearing scheduled will be set to true to simulate Caseflow missing a # disposition update. def create_appeal_state_with_case_record_and_hearing(desired_disposition) @@ -213,6 +230,66 @@ def create_appeal_state_with_case_record_and_hearing(desired_disposition) end end + context "#sync_decided_appeals" do + let(:decided_appeal_state) do + create_decided_appeal_state_with_case_record_and_hearing(true, true) + end + + let(:undecided_appeal_state) do + create_decided_appeal_state_with_case_record_and_hearing(false, true) + end + + let(:missing_vacols_case_appeal_state) do + create_decided_appeal_state_with_case_record_and_hearing(true, false) + end + + it "Job syncs decided appeals decision_mailed status", bypass_cleaner: true do + expect([decided_appeal_state, + undecided_appeal_state, + missing_vacols_case_appeal_state].all?(&:decision_mailed)).to eq false + + subject + + expect(decided_appeal_state.reload.decision_mailed).to eq true + expect(undecided_appeal_state.reload.decision_mailed).to eq false + expect(missing_vacols_case_appeal_state.reload.decision_mailed).to eq false + end + + it "catches standard errors", bypass_cleaner: true do + expect([decided_appeal_state, + undecided_appeal_state, + missing_vacols_case_appeal_state].all?(&:decision_mailed)).to eq false + + allow(AppealState).to receive(:legacy).and_raise(StandardError) + slack_msg = "" + slack_msg_error_text = "Fatal error in sync_decided_appeals" + allow_any_instance_of(SlackService).to receive(:send_notification) { |_, first_arg| slack_msg = first_arg } + + subject + + expect(slack_msg.include?(slack_msg_error_text)).to be true + end + + # Clean up parallel threads + after(:each) { clean_up_after_threads } + + # VACOLS record's decision date will be set to simulate a decided appeal + # decision_mailed will be set to false for the AppealState to verify the method + # functionality + def create_decided_appeal_state_with_case_record_and_hearing(decided_appeal, create_case) + case_hearing = create(:case_hearing) + decision_date = decided_appeal ? Time.current : nil + vacols_case = create_case ? create(:case, case_hearings: [case_hearing], bfddec: decision_date) : nil + appeal = create(:legacy_appeal, vacols_case: vacols_case) + + appeal.appeal_state.tap { _1.update!(decision_mailed: false) } + end + + def clean_up_after_threads + DatabaseCleaner.clean_with(:truncation, except: %w[vftypes issref notification_events]) + end + end + context "when errors occur" do context "in the sync_vacols_cases step" do context "due to existing FK associations" do diff --git a/spec/jobs/notification_initialization_job_spec.rb b/spec/jobs/notification_initialization_job_spec.rb index 3b86fb7e74d..bf1e2d3fb15 100644 --- a/spec/jobs/notification_initialization_job_spec.rb +++ b/spec/jobs/notification_initialization_job_spec.rb @@ -49,6 +49,10 @@ ) end + before do + InitialTasksFactory.new(appeal_state.appeal).create_root_and_sub_tasks! + end + it "enqueues an SendNotificationJob" do expect { subject }.to have_enqueued_job(SendNotificationJob) end diff --git a/spec/jobs/process_decision_document_job_spec.rb b/spec/jobs/process_decision_document_job_spec.rb index f7295efe73a..ca8210c3ed5 100644 --- a/spec/jobs/process_decision_document_job_spec.rb +++ b/spec/jobs/process_decision_document_job_spec.rb @@ -1,8 +1,10 @@ # frozen_string_literal: true describe ProcessDecisionDocumentJob do + let(:contested) { true } + context ".perform" do - subject { ProcessDecisionDocumentJob.perform_now(decision_document.id) } + subject { ProcessDecisionDocumentJob.perform_now(decision_document.id, contested) } let(:decision_document) { build_stubbed(:decision_document) } diff --git a/spec/jobs/process_notification_status_updates_job_spec.rb b/spec/jobs/process_notification_status_updates_job_spec.rb index a5f94379961..e40699a0e6f 100644 --- a/spec/jobs/process_notification_status_updates_job_spec.rb +++ b/spec/jobs/process_notification_status_updates_job_spec.rb @@ -3,107 +3,163 @@ describe ProcessNotificationStatusUpdatesJob, type: :job do include ActiveJob::TestHelper - let(:redis) do - # Creates a fresh Redis connection before each test and deletes all keys in the store - Redis.new(url: Rails.application.secrets.redis_url_cache).tap(&:flushall) - end + before(:each) { wipe_queues } + after(:all) { wipe_queues } + + let(:sqs_client) { SqsService.sqs_client } context ".perform" do before { Seeds::NotificationEvents.new.seed! } subject(:job) { ProcessNotificationStatusUpdatesJob.perform_later } - let(:new_status) { "test_status" } let(:appeal) { create(:appeal, veteran_file_number: "500000102", receipt_date: 6.months.ago.to_date.mdY) } + + let(:email_external_id) { SecureRandom.uuid } let(:email_notification) do create(:notification, appeals_id: appeal.uuid, appeals_type: "Appeal", event_date: 6.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.quarterly_notification, notification_type: "Email", - email_notification_external_id: SecureRandom.uuid) + email_notification_external_id: email_external_id) end + + let(:sms_external_id) { SecureRandom.uuid } let(:sms_notification) do create(:notification, appeals_id: appeal.uuid, appeals_type: "Appeal", event_date: 6.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.hearing_scheduled, - sms_notification_external_id: SecureRandom.uuid, + sms_notification_external_id: sms_external_id, notification_type: "SMS") end - it "has one message in queue" do - expect { job }.to change(ActiveJob::Base.queue_adapter.enqueued_jobs, :size).by(1) - end - - it "processes email notifications from redis cache" do - expect(email_notification.email_notification_status).to_not eq(new_status) - - create_cache_entries(email_notification) - - expect(redis.keys.grep(/email_update:/).count).to eq(1) - - perform_enqueued_jobs { ProcessNotificationStatusUpdatesJob.perform_later } - - expect(redis.keys.grep(/email_update:/).count).to eq(0) - expect(email_notification.reload.email_notification_status).to eq(new_status) + let(:sms_notification_2) do + create(:notification, appeals_id: appeal.uuid, + appeals_type: "Appeal", + event_date: 6.days.ago, + event_type: Constants.EVENT_TYPE_FILTERS.postponement_of_hearing, + sms_notification_external_id: "1234", + notification_type: "SMS") end - it "processes sms notifications from redis cache" do - expect(sms_notification.sms_notification_status).to_not eq(new_status) - - create_cache_entries(sms_notification) - - expect(redis.keys.grep(/sms_update:/).count).to eq(1) - - perform_enqueued_jobs { ProcessNotificationStatusUpdatesJob.perform_later } - - expect(redis.keys.grep(/sms_update:/).count).to eq(0) - expect(sms_notification.reload.sms_notification_status).to eq(new_status) + it "has one message in queue" do + expect { job }.to change(ActiveJob::Base.queue_adapter.enqueued_jobs, :size).by(1) end - it "processes a mix of email and sms notifications from redis cache" do - create_cache_entries(sms_notification, email_notification) - - expect(redis.keys.grep(/(sms|email)_update:/).count).to eq(2) - - perform_enqueued_jobs { ProcessNotificationStatusUpdatesJob.perform_later } - - expect(redis.keys.grep(/(sms|email)_update:/).count).to eq(0) - expect(email_notification.reload.email_notification_status).to eq(new_status) - expect(sms_notification.reload.sms_notification_status).to eq(new_status) + context "Updates are pulled from the SQS queue and applied to the datebase" do + let(:recipient_email) { "test@test.com" } + let(:email_status) { "delivered" } + let(:email_status_reason) { "Email delivery was succesful" } + + let(:recipient_phone) { "123-456-7890" } + let(:sms_status) { "temporary-failure" } + let(:sms_status_reason) { "Provider is retrying." } + + let(:test_queue) do + sqs_client.create_queue({ + queue_name: "caseflow_test_receive_notifications.fifo".to_sym, + attributes: { + "FifoQueue" => "true" + } + }) + end + let(:queue_url) { test_queue.queue_url } + let!(:sms_sqs_message) do + sqs_client.send_message( + queue_url: queue_url, + message_body: { + notification_type: "sms", + external_id: sms_external_id, + status: sms_status, + status_reason: sms_status_reason, + recipient: recipient_phone + }.to_json, + message_deduplication_id: "1", + message_group_id: ProcessNotificationStatusUpdatesJob::MESSAGE_GROUP_ID + ) + end + + let!(:sms_sqs_message_wrong_group_id) do + sqs_client.send_message( + queue_url: queue_url, + message_body: { + notification_type: "sms", + external_id: "1234", + status: sms_status, + status_reason: sms_status_reason, + recipient: recipient_phone + }.to_json, + message_deduplication_id: "2", + message_group_id: "SomethingElse" + ) + end + + let!(:email_sqs_message) do + sqs_client.send_message( + queue_url: queue_url, + message_body: { + notification_type: "email", + external_id: email_external_id, + status: email_status, + status_reason: email_status_reason, + recipient: recipient_email + }.to_json, + message_deduplication_id: "3", + message_group_id: ProcessNotificationStatusUpdatesJob::MESSAGE_GROUP_ID + ) + end + + it "Status update info from messages with correct group ID is persisted correctly" do + expect(all_message_info_empty?).to eq true + + perform_enqueued_jobs { job } + + # Reload records + [email_notification, sms_notification, sms_notification_2].each(&:reload) + + expect(email_notification.email_notification_status).to eq email_status + expect(email_notification.email_status_reason).to eq email_status_reason + expect(email_notification.recipient_email).to eq recipient_email + + expect(sms_notification.sms_notification_status).to eq sms_status + expect(sms_notification.sms_status_reason).to eq sms_status_reason + expect(sms_notification.recipient_phone_number).to eq recipient_phone + + # Update with the wrong message_group_id should have been skipped. + expect([ + sms_notification_2.sms_notification_status, + sms_notification_2.sms_status_reason, + sms_notification_2.recipient_phone_number + ].all?(&:nil?)).to eq true + end end + end - it "an error is raised if a UUID doesn't match with a notification record, but the job isn't halted" do - expect_any_instance_of(ProcessNotificationStatusUpdatesJob).to receive(:log_error) do |_job, error| - expect(error.message).to eq("No notification matches UUID not-going-to-match") - end.exactly(:once) - - # This notification update will cause an error - redis.set("sms_update:not-going-to-match:#{new_status}", 0) - - # This notification update should be fine - create_cache_entries(email_notification) - - expect(redis.keys.grep(/(sms|email)_update:/).count).to eq(2) - - perform_enqueued_jobs { ProcessNotificationStatusUpdatesJob.perform_later } - - expect(sms_notification.reload.sms_notification_status).to be_nil - expect(email_notification.reload.email_notification_status).to eq(new_status) - - expect(redis.keys.grep(/(sms|email)_update:/).count).to eq(0) - end + def all_message_info_empty? + [ + email_notification.email_notification_status, + email_notification.email_status_reason, + email_notification.recipient_email + ].all?(&:nil?) && + [ + sms_notification.sms_notification_status, + sms_notification.sms_status_reason, + sms_notification.recipient_phone_number + ].all?(&:nil?) && + [ + sms_notification_2.sms_notification_status, + sms_notification_2.sms_status_reason, + sms_notification_2.recipient_phone_number + ].all?(&:nil?) end - private + def wipe_queues + client = SqsService.sqs_client - def create_cache_entries(*keys) - keys.each do |key| - notification_type = key.notification_type.downcase - external_id = key.send("#{notification_type}_notification_external_id".to_sym) + queues_to_delete = client.list_queues.queue_urls.filter { |url| url.include?("caseflow_test") } - redis.set("#{notification_type}_update:#{external_id}:#{new_status}", 0) - end + queues_to_delete.each { |queue_url| client.delete_queue(queue_url: queue_url) } end end diff --git a/spec/jobs/quarterly_notifications_job_spec.rb b/spec/jobs/quarterly_notifications_job_spec.rb index f7ba2c0a12b..55a368e53c7 100644 --- a/spec/jobs/quarterly_notifications_job_spec.rb +++ b/spec/jobs/quarterly_notifications_job_spec.rb @@ -2,6 +2,7 @@ describe QuarterlyNotificationsJob, type: :job do include ActiveJob::TestHelper + let(:appeal) { create(:appeal, :active) } let(:legacy_appeal) { create(:legacy_appeal, vacols_case: vacols_case) } let(:vacols_case) { create(:case) } @@ -48,6 +49,7 @@ it "pushes a new message" do expect_message_to_be_queued + expect_message_to_have_status("docketed") subject end @@ -68,6 +70,7 @@ it "pushes a new message" do expect_message_to_be_queued + expect_message_to_have_status("docketed") subject end @@ -89,6 +92,7 @@ it "pushes a new message" do expect_message_to_be_queued + expect_message_to_have_status("Privacy Act Pending") subject end @@ -109,6 +113,7 @@ it "pushes a new message" do expect_message_to_be_queued + expect_message_to_have_status("docketed") subject end @@ -123,13 +128,14 @@ created_by_id: user.id, updated_by_id: user.id, appeal_docketed: true, - hearing_withdrawn: true, - scheduled_in_error: true + scheduled_in_error: true, + privacy_act_pending: true ) end it "pushes a new message" do expect_message_to_be_queued + expect_message_to_have_status("Privacy Act Pending") subject end @@ -150,26 +156,7 @@ it "pushes a new message" do expect_message_to_be_queued - - subject - end - end - - context "Hearing Scheduled / Privacy Act Pending with ihp task" do - let(:hearing) { create(:hearing, :with_tasks) } - let!(:appeal_state) do - hearing.appeal.appeal_state.tap do - _1.update!( - appeal_docketed: true, - hearing_scheduled: true, - privacy_act_pending: true, - vso_ihp_pending: true - ) - end - end - - it "pushes a new message" do - expect_message_to_be_queued + expect_message_to_have_status("docketed") subject end @@ -191,25 +178,7 @@ it "pushes a new message" do expect_message_to_be_queued - - subject - end - end - - context "Hearing Scheduled with ihp task pending" do - let(:hearing) { create(:hearing, :with_tasks) } - let!(:appeal_state) do - hearing.appeal.appeal_state.tap do - _1.update!( - appeal_docketed: true, - hearing_scheduled: true, - vso_ihp_pending: true - ) - end - end - - it "pushes a new message" do - expect_message_to_be_queued + expect_message_to_have_status("VSO IHP Pending / Privacy Act Pending") subject end @@ -229,6 +198,7 @@ it "pushes a new message" do expect_message_to_be_queued + expect_message_to_have_status("Hearing Scheduled / Privacy Act Pending") subject end @@ -249,6 +219,7 @@ it "pushes a new message" do expect_message_to_be_queued + expect_message_to_have_status("Privacy Act Pending") subject end @@ -269,6 +240,7 @@ it "pushes a new message" do expect_message_to_be_queued + expect_message_to_have_status("VSO IHP Pending") subject end @@ -286,6 +258,7 @@ it "pushes a new message" do expect_message_to_be_queued + expect_message_to_have_status("Hearing Scheduled") subject end @@ -339,6 +312,17 @@ def expect_message_to_be_queued ) end + def expect_message_to_have_status(status) + expect_any_instance_of(NotificationInitializationJob) + .to receive(:initialize) + .with({ + appeal_id: appeal_state.appeal_id, + appeal_type: appeal_state.appeal_type, + template_name: Constants.EVENT_TYPE_FILTERS.quarterly_notification, + appeal_status: status + }) + end + def expect_message_to_not_be_enqueued expect_any_instance_of(QuarterlyNotificationsJob) .to_not receive(:enqueue_init_jobs) diff --git a/spec/jobs/send_notification_job_spec.rb b/spec/jobs/send_notification_job_spec.rb index 32ebb4b8296..cde49138139 100644 --- a/spec/jobs/send_notification_job_spec.rb +++ b/spec/jobs/send_notification_job_spec.rb @@ -178,21 +178,11 @@ context "#queue_name_suffix" do subject { described_class.queue_name_suffix } - it "returns non-FIFO name in development environment" do - is_expected.to eq :send_notifications - end - - it "returns FIFO name in non-development environment" do - allow(ApplicationController).to receive(:dependencies_faked?).and_return(false) - + it "returns FIFO name" do is_expected.to eq :"send_notifications.fifo" end end - it "it is the correct queue" do - expect(SendNotificationJob.new.queue_name).to eq(queue_name) - end - context ".perform" do subject(:job) { SendNotificationJob.perform_later(good_message.to_json) } diff --git a/spec/jobs/sync_reviews_job_spec.rb b/spec/jobs/sync_reviews_job_spec.rb index a285d531439..369c93b2e8d 100644 --- a/spec/jobs/sync_reviews_job_spec.rb +++ b/spec/jobs/sync_reviews_job_spec.rb @@ -143,7 +143,7 @@ SyncReviewsJob.perform_now end.to have_enqueued_job( ProcessDecisionDocumentJob - ).with(decision_document_needs_reprocessing.id).exactly(:once) + ).with(decision_document_needs_reprocessing.id, false).exactly(:once) end end end diff --git a/spec/jobs/va_notify_status_update_job_spec.rb b/spec/jobs/va_notify_status_update_job_spec.rb deleted file mode 100644 index 4c6f2c65842..00000000000 --- a/spec/jobs/va_notify_status_update_job_spec.rb +++ /dev/null @@ -1,271 +0,0 @@ -# frozen_string_literal: true - -describe VANotifyStatusUpdateJob, type: :job do - include ActiveJob::TestHelper - let(:current_user) { create(:user, roles: ["System Admin"]) } - let(:notifications_email_only) do - FactoryBot.create_list :notification_email_only, 10 - end - let(:notifications_sms_only) do - FactoryBot.create_list :notification_sms_only, 10 - end - let(:notifications_email_and_sms) do - FactoryBot.create_list :notification_email_and_sms, 10 - end - let(:email_only) do - create(:notification, - appeals_id: "5d70058f-8641-4155-bae8-5af4b61b1576", - appeals_type: "Appeal", - event_type: Constants.EVENT_TYPE_FILTERS.hearing_scheduled, - event_date: Time.zone.today, - notification_type: "Email", - email_notification_status: "Success") - end - let(:sms_only) do - create(:notification, - appeals_id: "5d70058f-8641-4155-bae8-5af4b61b1576", - appeals_type: "Appeal", - event_type: Constants.EVENT_TYPE_FILTERS.hearing_scheduled, - event_date: Time.zone.today, - notification_type: "SMS", - sms_notification_status: "Success") - end - let(:email_and_sms) do - create(:notification, - appeals_id: "5d70058f-8641-4155-bae8-5af4b61b1576", - appeals_type: "Appeal", - event_type: Constants.EVENT_TYPE_FILTERS.hearing_scheduled, - event_date: Time.zone.today, - notification_type: "Email and SMS", - email_notification_status: "Success", - sms_notification_status: "Success") - end - let(:notification_collection) do - create(:notification, - appeals_id: "5d70058f-8641-4155-bae8-5af4b61b1576", - appeals_type: "Appeal", - event_type: Constants.EVENT_TYPE_FILTERS.hearing_scheduled, - event_date: Time.zone.today, - notification_type: "Email", - email_notification_external_id: "0", - sms_notification_external_id: nil, - email_notification_status: "Success", - created_at: Time.zone.now) - create(:notification, - appeals_id: "5d70058f-8641-4155-bae8-5af4b61b1576", - appeals_type: "Appeal", - event_type: Constants.EVENT_TYPE_FILTERS.hearing_scheduled, - event_date: Time.zone.today, - notification_type: "SMS", - email_notification_external_id: nil, - sms_notification_external_id: "0", - sms_notification_status: "temporary-failure", - created_at: Time.zone.now) - create(:notification, - appeals_id: "5d70058f-8641-4155-bae8-5af4b61b1576", - appeals_type: "Appeal", - event_type: Constants.EVENT_TYPE_FILTERS.hearing_scheduled, - event_date: Time.zone.today, - notification_type: "SMS", - email_notification_external_id: nil, - sms_notification_external_id: "1", - sms_notification_status: "created", - created_at: Time.zone.now) - create(:notification, - appeals_id: "5d70058f-8641-4155-bae8-5af4b61b1576", - appeals_type: "Appeal", - event_type: Constants.EVENT_TYPE_FILTERS.hearing_scheduled, - event_date: Time.zone.today, - notification_type: "Email", - email_notification_external_id: "1", - sms_notification_external_id: nil, - email_notification_status: "technical-failure", - created_at: Time.zone.now) - create(:notification, - appeals_id: "5d70058f-8641-4155-bae8-5af4b61b1576", - appeals_type: "Appeal", - event_type: Constants.EVENT_TYPE_FILTERS.hearing_scheduled, - event_date: Time.zone.today, - notification_type: "Email and SMS", - email_notification_external_id: "2", - sms_notification_external_id: "2", - email_notification_status: "temporary-failure", - sms_notification_status: "temporary-failure", - created_at: Time.zone.now - 5.days) - create(:notification, - appeals_id: "5d70058f-8641-4155-bae8-5af4b61b1576", - appeals_type: "Appeal", - event_type: Constants.EVENT_TYPE_FILTERS.hearing_scheduled, - event_date: Time.zone.today, - notification_type: "Email and SMS", - email_notification_external_id: "3", - sms_notification_external_id: "3", - email_notification_status: "delivered", - sms_notification_status: "delivered", - created_at: Time.zone.now - 5.days) - create(:notification, - appeals_id: "5d70058f-8641-4155-bae8-5af4b61b1577", - appeals_type: "Appeal", - event_type: Constants.EVENT_TYPE_FILTERS.hearing_scheduled, - event_date: Time.zone.today, - notification_type: "Email and SMS", - email_notification_external_id: "4", - sms_notification_external_id: "4", - email_notification_status: "delivered", - sms_notification_status: "delivered", - created_at: Time.zone.now - 5.days) - end - - let(:collect) { Notification.where(id: [1, 2, 3, 4, 5]) } - - let(:queue_name) { "caseflow_test_low_priority" } - - before do - Seeds::NotificationEvents.new.seed! - end - - after(:each) do - clear_enqueued_jobs - clear_performed_jobs - end - - it "it is the correct queue" do - expect(VANotifyStatusUpdateJob.new.queue_name).to eq(queue_name) - end - - context ".perform" do - subject(:job) { VANotifyStatusUpdateJob.perform_later } - describe "send message to queue" do - it "has one message in queue" do - expect { job }.to change(ActiveJob::Base.queue_adapter.enqueued_jobs, :size).by(1) - end - - it "processes message" do - perform_enqueued_jobs do - result = VANotifyStatusUpdateJob.perform_later - expect(result.arguments[0]).to eq(nil) - end - end - - it "sends to VA Notify when no errors are present" do - expect(Rails.logger).not_to receive(:error) - expect { VANotifyStatusUpdateJob.perform_now.to receive(:send_to_va_notify) } - end - - it "defaults to 650 for the query limit if environment variable not found or invalid" do - stub_const("VANotifyStatusUpdateJob::QUERY_LIMIT", nil) - expect(Rails.logger).to receive(:info) - .with("VANotifyStatusJob can not read the VA_NOTIFY_STATUS_UPDATE_BATCH_LIMIT environment variable.\ - Defaulting to 650.") - VANotifyStatusUpdateJob.perform_now - end - - it "logs out an error to Raven when email type that is not Email or SMS is found" do - external_id = SecureRandom.uuid - email_only.update!(email_notification_external_id: external_id) - job_instance = VANotifyStatusUpdateJob.new - external_id = SecureRandom.uuid - result = job_instance.send(:get_current_status, external_id, "None") - expect(result).to eq(false) - end - end - - describe "feature flags" do - describe "Email" do - it "updates the Notification when successful" do - email_only.email_notification_external_id = SecureRandom.uuid - allow(job).to receive(:notifications_not_processed).and_return([email_only]) - job.perform_now - expect(email_only.email_notification_status).to eq("created") - end - it "logs when external id is not present" do - allow(job).to receive(:notifications_not_processed).and_return([email_only]) - job.perform_now - expect(email_only.email_notification_status).to eq("No External Id") - end - end - - describe "SMS" do - it "updates the Notification when successful" do - sms_only.sms_notification_external_id = SecureRandom.uuid - allow(job).to receive(:notifications_not_processed).and_return([sms_only]) - job.perform_now - expect(sms_only.sms_notification_status).to eq("created") - end - it "logs when external id is not present" do - allow(job).to receive(:notifications_not_processed).and_return([sms_only]) - job.perform_now - expect(sms_only.sms_notification_status).to eq("No External Id") - end - end - - describe "Email and SMS" do - it "updates the Notification when successful" do - email_and_sms.sms_notification_external_id = SecureRandom.uuid - email_and_sms.email_notification_external_id = SecureRandom.uuid - allow(job).to receive(:notifications_not_processed).and_return([email_and_sms]) - job.perform_now - expect(email_and_sms.sms_notification_status && email_and_sms.email_notification_status).to eq("created") - end - it "logs when external id is not present" do - allow(job).to receive(:notifications_not_processed).and_return([email_and_sms]) - job.perform_now - expect(email_and_sms.sms_notification_status && - email_and_sms.email_notification_status).to eq("No External Id") - end - - it "updates the email and sms notification status if an external id is found" do - email_and_sms.update!(sms_notification_external_id: SecureRandom.uuid, - email_notification_external_id: SecureRandom.uuid) - job.perform_now - notification = Notification.first - expect(notification.email_notification_status && notification.sms_notification_status).to eq("created") - end - end - end - end - - context "#get_current_status" do - subject(:job) { VANotifyStatusUpdateJob.perform_later } - it "handles VA Notify errors" do - email_and_sms.sms_notification_external_id = SecureRandom.uuid - email_and_sms.email_notification_external_id = SecureRandom.uuid - allow(job).to receive(:notifications_not_processed).and_return([email_and_sms]) - allow(VANotifyService).to receive(:get_status).and_raise(Caseflow::Error::VANotifyNotFoundError) - expect(job).to receive(:log_error).with(/VA Notify API returned error/).twice - job.perform_now - end - end - - context "#notifications_not_processed" do - subject(:job) { VANotifyStatusUpdateJob.perform_later } - it "queries the notification table using activerecord" do - allow(job).to receive(:find_notifications_not_processed).and_return([]) - expect(job.send(:find_notifications_not_processed)) - job.perform_now - end - end - - context "#find_notif_not_processed" do - subject(:job) { VANotifyStatusUpdateJob.perform_later } - it "returns a collection of notifications from the DB that hold the qualifying statuses" do - notification_collection - expect(job.send(:find_notifications_not_processed)).not_to include(Notification.where(id: [6, 7])) - end - end - - context "#default_to_650" do - before do - VANotifyStatusUpdateJob::QUERY_LIMIT = nil - end - - subject(:job) { VANotifyStatusUpdateJob.perform_later } - it "defaults to 650" do - expect(Rails.logger).to receive(:info).with( - "VANotifyStatusJob can not read the VA_NOTIFY_STATUS_UPDATE_BATCH_LIMIT environment variable.\ - Defaulting to 650." - ) - job.perform - end - end -end diff --git a/spec/models/appellant_notification_spec.rb b/spec/models/appellant_notification_spec.rb index 9b09924d134..4c97d740f4d 100644 --- a/spec/models/appellant_notification_spec.rb +++ b/spec/models/appellant_notification_spec.rb @@ -3,21 +3,22 @@ describe AppellantNotification do describe "class methods" do describe "self.handle_errors" do - let(:appeal) { create(:appeal, number_of_claimants: 1) } + let(:template_name) { "Quarterly Notification" } + let(:appeal) { create(:appeal, :active, number_of_claimants: 1) } let(:current_user) { User.system_user } context "if appeal is nil" do let(:empty_appeal) {} it "reports the error" do - expect { AppellantNotification.handle_errors(empty_appeal) }.to raise_error( + expect { AppellantNotification.handle_errors(empty_appeal, template_name) }.to raise_error( AppellantNotification::NoAppealError ) end end context "with no claimant listed" do - let(:appeal) { create(:appeal, number_of_claimants: 0) } + let(:appeal) { create(:appeal, :active, number_of_claimants: 0) } it "returns error message" do - expect(AppellantNotification.handle_errors(appeal)[:status]).to eq( + expect(AppellantNotification.handle_errors(appeal, template_name)[:status]).to eq( AppellantNotification::NoClaimantError.new(appeal.id).status ) end @@ -25,31 +26,42 @@ context "with no participant_id listed" do let(:claimant) { create(:claimant, participant_id: "") } - let(:appeal) { create(:appeal) } + let(:appeal) { create(:appeal, :active) } before do appeal.claimants = [claimant] end it "returns error message" do - expect(AppellantNotification.handle_errors(appeal)[:status]).to eq( + expect(AppellantNotification.handle_errors(appeal, template_name)[:status]).to eq( AppellantNotification::NoParticipantIdError.new(appeal.id).status ) end end + context "with an inactive appeal" do + let(:appeal) { create(:appeal, :active, number_of_claimants: 1) } + it "returns error message" do + appeal.root_task.completed! + expect { AppellantNotification.handle_errors(appeal, template_name) }.to raise_error( + AppellantNotification::InactiveAppealError + ) + end + end + context "with no errors" do it "doesn't raise" do - expect(AppellantNotification.handle_errors(appeal)[:status]).to eq "Success" + expect(AppellantNotification.handle_errors(appeal, template_name)[:status]).to eq "Success" end end end describe "veteran is deceased" do - let(:appeal) { create(:appeal, number_of_claimants: 1) } + let(:appeal) { create(:appeal, :active, number_of_claimants: 1) } let(:substitute_appellant) { create(:appellant_substitution) } + let(:template_name) { "test" } it "with no substitute appellant" do appeal.veteran.update!(date_of_death: Time.zone.today) - expect(AppellantNotification.handle_errors(appeal)[:status]).to eq "Failure Due to Deceased" + expect(AppellantNotification.handle_errors(appeal, template_name)[:status]).to eq "Failure Due to Deceased" end it "with substitute appellant" do @@ -57,13 +69,13 @@ substitute_appellant.update!(source_appeal_id: appeal.id) substitute_appellant.send(:establish_substitution_on_same_appeal) appeal.update!(veteran_is_not_claimant: true) - expect(AppellantNotification.handle_errors(appeal)[:status]).to eq "Success" + expect(AppellantNotification.handle_errors(appeal, template_name)[:status]).to eq "Success" end end describe "self.create_payload" do - let(:good_appeal) { create(:appeal, number_of_claimants: 1) } - let(:bad_appeal) { create(:appeal) } + let(:good_appeal) { create(:appeal, :active, number_of_claimants: 1) } + let(:bad_appeal) { create(:appeal, :active) } let(:bad_claimant) { create(:claimant, participant_id: "") } let(:template_name) { "test" } @@ -148,14 +160,14 @@ it "Will notify appellant that the legacy appeal decision has been mailed (Non Contested)" do expect(AppellantNotification).to receive(:notify_appellant).with(legacy_appeal, non_contested) decision_document = dispatch.send dispatch_func, params - decision_document.process! + decision_document.process!(false) end it "Will notify appellant that the legacy appeal decision has been mailed (Contested)" do expect(AppellantNotification).to receive(:notify_appellant).with(legacy_appeal, contested) allow(legacy_appeal).to receive(:contested_claim).and_return(true) legacy_appeal.contested_claim decision_document = dispatch.send dispatch_func, params - decision_document.process! + decision_document.process!(true) end end @@ -205,7 +217,7 @@ it "Will notify appellant that the AMA appeal decision has been mailed (Non Contested)" do expect(AppellantNotification).to receive(:notify_appellant).with(appeal, non_contested) decision_document = dispatch.send dispatch_func, params - decision_document.process! + decision_document.process!(false) end it "Will notify appellant that the AMA appeal decision has been mailed (Contested)" do expect(AppellantNotification).to receive(:notify_appellant).with(contested_appeal, contested) @@ -213,7 +225,7 @@ contested_appeal.contested_claim? contested_decision_document = contested_dispatch .send dispatch_func, contested_params - contested_decision_document.process! + contested_decision_document.process!(true) end end end @@ -570,7 +582,7 @@ # Note: only privacyactrequestmailtask is tested because the process is the same as foiarequestmailtask describe "mail task" do - let(:appeal) { create(:appeal) } + let(:appeal) { create(:appeal, :active) } let(:appeal_state) { create(:appeal_state, appeal_id: appeal.id, appeal_type: appeal.class.to_s) } let(:current_user) { create(:user) } let(:priv_org) { PrivacyTeam.singleton } @@ -640,7 +652,7 @@ end context "Foia Colocated Tasks" do - let(:appeal) { create(:appeal) } + let(:appeal) { create(:appeal, :active) } let(:appeal_state) { create(:appeal_state, appeal_id: appeal.id, appeal_type: appeal.class.to_s) } let!(:attorney) { create(:user) } let!(:attorney_task) { create(:ama_attorney_task, appeal: appeal, assigned_to: attorney) } @@ -691,7 +703,7 @@ end context "Privacy Act Tasks" do - let(:appeal) { create(:appeal) } + let(:appeal) { create(:appeal, :active) } let(:appeal_state) { create(:appeal_state, appeal_id: appeal.id, appeal_type: appeal.class.to_s) } let(:attorney) { create(:user) } let(:current_user) { create(:user) } @@ -929,6 +941,12 @@ let(:task) { create(:informal_hearing_presentation_task, :in_progress, assigned_to: org) } let(:appeal_state) { create(:appeal_state, appeal_id: task.appeal.id, appeal_type: task.appeal.class.to_s) } let(:template_name) { Constants.EVENT_TYPE_FILTERS.vso_ihp_complete } + let(:appeal) { task.appeal } + + before do + InitialTasksFactory.new(appeal).create_root_and_sub_tasks! + end + it "will notify the appellant of the 'IhpTaskComplete' status" do allow(task).to receive(:verify_user_can_update!).with(user).and_return(true) expect(AppellantNotification).to receive(:notify_appellant).with(task.appeal, template_name) diff --git a/spec/models/decision_document_spec.rb b/spec/models/decision_document_spec.rb index a90aba992cb..9896879c68d 100644 --- a/spec/models/decision_document_spec.rb +++ b/spec/models/decision_document_spec.rb @@ -133,7 +133,7 @@ end context "#process!" do - subject { decision_document.process! } + subject { decision_document.process!(false) } before do allow(decision_document).to receive(:submitted_and_ready?).and_return(true) @@ -141,6 +141,7 @@ allow(VBMSService).to receive(:establish_claim!).and_call_original allow(VBMSService).to receive(:create_contentions!).and_call_original FeatureToggle.enable!(:send_email_for_dispatched_appeals) + InitialTasksFactory.new(decision_document.appeal).create_root_and_sub_tasks! end after { FeatureToggle.disable!(:send_email_for_dispatched_appeals) } diff --git a/spec/models/tasks/bva_dispatch_task_spec.rb b/spec/models/tasks/bva_dispatch_task_spec.rb index 9124d11eaf8..7e612ef0e51 100644 --- a/spec/models/tasks/bva_dispatch_task_spec.rb +++ b/spec/models/tasks/bva_dispatch_task_spec.rb @@ -113,7 +113,7 @@ decision_document = DecisionDocument.find_by(appeal_id: root_task.appeal.id) expect(ProcessDecisionDocumentJob).to have_received(:perform_later) - .with(decision_document.id, nil).exactly(:once) + .with(decision_document.id, false, nil).exactly(:once) expect(decision_document).to_not eq nil expect(decision_document.document_type).to eq "BVA Decision" expect(decision_document.source).to eq "BVA" @@ -144,7 +144,7 @@ decision_document = DecisionDocument.find_by(appeal_id: legacy_appeal.id) expect(ProcessDecisionDocumentJob).to have_received(:perform_later) - .with(decision_document.id, nil).exactly(:once) + .with(decision_document.id, false, nil).exactly(:once) expect(decision_document).to_not eq nil expect(decision_document.document_type).to eq "BVA Decision" expect(decision_document.source).to eq "BVA" @@ -248,7 +248,7 @@ decision_document = DecisionDocument.find_by(appeal_id: root_task.appeal.id) expect(ProcessDecisionDocumentJob).to have_received(:perform_later) - .with(decision_document.id, nil).exactly(:once) + .with(decision_document.id, false, nil).exactly(:once) expect(decision_document).to_not eq nil expect(decision_document.document_type).to eq "BVA Decision" expect(decision_document.source).to eq "BVA" diff --git a/spec/services/sqs_service_spec.rb b/spec/services/sqs_service_spec.rb new file mode 100644 index 00000000000..9322989ff54 --- /dev/null +++ b/spec/services/sqs_service_spec.rb @@ -0,0 +1,136 @@ +# frozen_string_literal: true + +describe SqsService do + let(:sqs_client) { SqsService.sqs_client } + + before(:each) { wipe_queues } + after(:all) { wipe_queues } + + context "#find_queue_url_by_name" do + let!(:queue) { create_queue(queue_name, fifo) } + + subject { SqsService.find_queue_url_by_name(name: queue_name, check_fifo: false) } + + context "FIFO" do + let(:fifo) { true } + let(:queue_name) { "my_fifo_queue" } + + it "the queue is found and is validated to be a FIFO queue" do + expect(subject { SqsService.find_queue_url_by_name(name: queue_name, check_fifo: true) }) + .to include("caseflow_test_my_fifo_queue.fifo") + end + + it "the queue is found while validation is opted out" do + is_expected.to include("caseflow_test_my_fifo_queue.fifo") + end + + it "a non-existent queue cannot be found" do + expect { SqsService.find_queue_url_by_name(name: "fake", check_fifo: false) }.to raise_error do |error| + expect(error).to be_a(Caseflow::Error::SqsQueueNotFoundError) + expect(error.to_s).to include("The fake SQS queue is missing in this environment.") + end + end + end + + context "non-FIFO" do + let(:fifo) { false } + let(:queue_name) { "my_normal_queue" } + + it "the queue is found" do + is_expected.to include("caseflow_test_my_normal_queue") + is_expected.to_not include(".fifo") + end + + it "the queue found fails the FIFO check" do + expect { SqsService.find_queue_url_by_name(name: queue_name, check_fifo: true) }.to raise_error do |error| + expect(error).to be_a(Caseflow::Error::SqsUnexpectedQueueTypeError) + expect(error.to_s).to include("No FIFO queue with name my_normal_queue could be located.") + end + end + end + end + + context "#batch_delete_messages" do + let!(:queue) { create_queue("batch_delete_test", false) } + let(:queue_url) { queue.queue_url } + + context "ten or fewer messages are deleted" do + let!(:initial_messages) { queue_messages(queue_url) } + let(:received_messages) do + SqsService.sqs_client.receive_message({ + queue_url: queue_url, + max_number_of_messages: 10 + }).messages + end + + it "the messages are deleted properly" do + expect(approximate_number_of_messages_in_queue(queue_url)).to eq 10 + + SqsService.batch_delete_messages(queue_url: queue_url, messages: received_messages) + + expect(approximate_number_of_messages_in_queue(queue_url)).to eq 0 + end + end + + context "more than ten messages are deleted" + let!(:initial_messages) { queue_messages(queue_url, 20) } + + let(:received_messages) do + Array.new(2).flat_map do + SqsService.sqs_client.receive_message( + { + queue_url: queue_url, + max_number_of_messages: 10 + } + ).messages + end + end + + it "the messages are deleted properly" do + expect(approximate_number_of_messages_in_queue(queue_url)).to eq 20 + + SqsService.batch_delete_messages(queue_url: queue.queue_url, messages: received_messages) + + expect(approximate_number_of_messages_in_queue(queue_url)).to eq 0 + end + end + + def create_queue(name, fifo = false) + sqs_client.create_queue({ + queue_name: "caseflow_test_#{name}#{fifo ? '.fifo' : ''}".to_sym, + attributes: fifo ? { "FifoQueue" => "true" } : {} + }) + end + + def queue_messages(queue_url, num_to_queue = 10) + bodies = Array.new(num_to_queue).map.with_index do |_val, idx| + { test: idx }.to_json + end + + bodies.each do |body| + sqs_client.send_message({ + queue_url: queue_url, + message_body: body + }) + end + end + + def approximate_number_of_messages_in_queue(queue_url) + resp = sqs_client.get_queue_attributes({ + queue_url: queue_url, + attribute_names: ["ApproximateNumberOfMessages"] + }) + + resp.attributes["ApproximateNumberOfMessages"].to_i + end + + def wipe_queues + client = SqsService.sqs_client + + queues_to_delete = client.list_queues.queue_urls.filter { _1.include?("caseflow_test") } + + queues_to_delete.each do |queue_url| + client.delete_queue(queue_url: queue_url) + end + end +end diff --git a/spec/workflows/ihp_tasks_factory_spec.rb b/spec/workflows/ihp_tasks_factory_spec.rb index 29f185ee4eb..7c80b37f763 100644 --- a/spec/workflows/ihp_tasks_factory_spec.rb +++ b/spec/workflows/ihp_tasks_factory_spec.rb @@ -3,7 +3,7 @@ require "rails_helper" describe IhpTasksFactory, :postgres do - let(:appeal) { create(:appeal) } + let(:appeal) { create(:appeal, :active) } let(:parent_task) { create(:task, appeal: appeal) } let(:ihp_tasks_factory) { IhpTasksFactory.new(parent_task) } From 5486121e58902af24080cff18355d8140f080298 Mon Sep 17 00:00:00 2001 From: Ron Wabukenda <130374706+ronwabVa@users.noreply.github.com> Date: Tue, 17 Sep 2024 16:03:18 -0400 Subject: [PATCH 8/9] Revert "hotfix APPEALS-23420 and APPEALS-57844 (#22848)" (#22867) This reverts commit 8d7b9da94a17b9a06d6081bb108a12ae7657f920. --- .../idt/api/v2/appeals_controller.rb | 4 +- app/decorators/appeal_status_api_decorator.rb | 12 +- app/models/appeal.rb | 16 +- .../work_queue/appeal_search_serializer.rb | 15 +- app/models/task.rb | 6 +- .../tasks/evidence_submission_window_task.rb | 2 +- app/services/bva_appeal_status.rb | 75 ++-- app/services/search_query_service.rb | 96 ----- .../search_query_service/api_response.rb | 3 - .../search_query_service/appeal_row.rb | 167 -------- .../search_query_service/attributes.rb | 30 -- .../search_query_service/legacy_appeal_row.rb | 142 ------- .../search_query_service/legacy_attributes.rb | 25 -- .../search_query_service/queried_appeal.rb | 133 ------ .../search_query_service/queried_hearing.rb | 33 -- .../queried_legacy_appeal.rb | 41 -- app/services/search_query_service/query.rb | 390 ------------------ .../search_query_service/search_response.rb | 9 - .../vso_user_search_results.rb | 68 --- app/workflows/case_search_results_base.rb | 118 ++---- ..._search_results_for_caseflow_veteran_id.rb | 22 - .../case_search_results_for_docket_number.rb | 6 +- ..._search_results_for_veteran_file_number.rb | 27 -- spec/feature/queue/search_spec.rb | 6 +- spec/fixes/assigned_to_search_results_spec.rb | 6 +- spec/fixes/backfill_early_ama_appeal_spec.rb | 4 +- .../appeal_search_serializer_spec.rb | 183 -------- spec/services/bva_appeal_status_spec.rb | 2 +- spec/services/search_query_service_spec.rb | 311 -------------- 29 files changed, 102 insertions(+), 1850 deletions(-) delete mode 100644 app/services/search_query_service.rb delete mode 100644 app/services/search_query_service/api_response.rb delete mode 100644 app/services/search_query_service/appeal_row.rb delete mode 100644 app/services/search_query_service/attributes.rb delete mode 100644 app/services/search_query_service/legacy_appeal_row.rb delete mode 100644 app/services/search_query_service/legacy_attributes.rb delete mode 100644 app/services/search_query_service/queried_appeal.rb delete mode 100644 app/services/search_query_service/queried_hearing.rb delete mode 100644 app/services/search_query_service/queried_legacy_appeal.rb delete mode 100644 app/services/search_query_service/query.rb delete mode 100644 app/services/search_query_service/search_response.rb delete mode 100644 app/services/search_query_service/vso_user_search_results.rb delete mode 100644 spec/models/serializers/work_queue/appeal_search_serializer_spec.rb delete mode 100644 spec/services/search_query_service_spec.rb diff --git a/app/controllers/idt/api/v2/appeals_controller.rb b/app/controllers/idt/api/v2/appeals_controller.rb index b93cf8179dc..74884231964 100644 --- a/app/controllers/idt/api/v2/appeals_controller.rb +++ b/app/controllers/idt/api/v2/appeals_controller.rb @@ -14,11 +14,11 @@ def details result = if docket_number?(case_search) CaseSearchResultsForDocketNumber.new( docket_number: case_search, user: current_user - ).api_call + ).call else CaseSearchResultsForVeteranFileNumber.new( file_number_or_ssn: case_search, user: current_user - ).api_call + ).call end render_search_results_as_json(result) diff --git a/app/decorators/appeal_status_api_decorator.rb b/app/decorators/appeal_status_api_decorator.rb index 7be8e8e75aa..a93ed1b9bbf 100644 --- a/app/decorators/appeal_status_api_decorator.rb +++ b/app/decorators/appeal_status_api_decorator.rb @@ -3,12 +3,6 @@ # Extends the Appeal model with methods for the Appeals Status API class AppealStatusApiDecorator < ApplicationDecorator - def initialize(appeal, scheduled_hearing = nil) - super(appeal) - - @scheduled_hearing = scheduled_hearing - end - def appeal_status_id "A#{id}" end @@ -168,11 +162,11 @@ def remanded_sc_decision_issues end def open_pre_docket_task? - open_tasks.any? { |task| task.is_a?(PreDocketTask) } + tasks.open.any? { |task| task.is_a?(PreDocketTask) } end def pending_schedule_hearing_task? - pending_schedule_hearing_tasks.any? + tasks.open.where(type: ScheduleHearingTask.name).any? end def hearing_pending? @@ -180,7 +174,7 @@ def hearing_pending? end def evidence_submission_hold_pending? - evidence_submission_hold_pending_tasks.any? + tasks.open.where(type: EvidenceSubmissionWindowTask.name).any? end def at_vso? diff --git a/app/models/appeal.rb b/app/models/appeal.rb index 59367fa3a36..4691f672563 100644 --- a/app/models/appeal.rb +++ b/app/models/appeal.rb @@ -308,18 +308,6 @@ def decorated_with_status AppealStatusApiDecorator.new(self) end - def open_tasks - tasks.open - end - - def pending_schedule_hearing_tasks - tasks.open.where(type: ScheduleHearingTask.name) - end - - def evidence_submission_hold_pending_tasks - tasks.open.where(type: EvidenceSubmissionWindowTask.name) - end - # :reek:RepeatedConditionals def active_request_issues_or_decision_issues decision_issues.empty? ? active_request_issues : fetch_all_decision_issues @@ -645,7 +633,7 @@ def direct_review_docket? end def active? - open_tasks.of_type(:RootTask).any? + tasks.open.of_type(:RootTask).any? end def ready_for_distribution? @@ -760,7 +748,7 @@ def substitutions end def status - @status ||= BVAAppealStatus.new(tasks: tasks) + @status ||= BVAAppealStatus.new(appeal: self) end def previously_selected_for_quality_review diff --git a/app/models/serializers/work_queue/appeal_search_serializer.rb b/app/models/serializers/work_queue/appeal_search_serializer.rb index 320c5bdb075..90be5a6f498 100644 --- a/app/models/serializers/work_queue/appeal_search_serializer.rb +++ b/app/models/serializers/work_queue/appeal_search_serializer.rb @@ -6,16 +6,6 @@ class WorkQueue::AppealSearchSerializer set_type :appeal - RESTRICTED_STATUSES = - [ - :distributed_to_judge, - :ready_for_signature, - :on_hold, - :misc, - :unknown, - :assigned_to_attorney - ].freeze - attribute :contested_claim, &:contested_claim? attribute :mst, &:mst? @@ -83,11 +73,10 @@ class WorkQueue::AppealSearchSerializer attribute :veteran_appellant_deceased, &:veteran_appellant_deceased? attribute :assigned_to_location do |object, params| - if RESTRICTED_STATUSES.include?(object&.status&.status) - unless params[:user]&.vso_employee? + if object&.status&.status == :distributed_to_judge + if params[:user]&.judge? || params[:user]&.attorney? || User.list_hearing_coordinators.include?(params[:user]) object.assigned_to_location end - # if not in a restricted status, show assigned location to all users else object.assigned_to_location end diff --git a/app/models/task.rb b/app/models/task.rb index 4c0b7066290..b2244e0f1b0 100644 --- a/app/models/task.rb +++ b/app/models/task.rb @@ -101,7 +101,7 @@ class << self; undef_method :open; end # Equivalent to .reject(&:hide_from_queue_table_view) but offloads that to the database. scope :visible_in_queue_table_view, lambda { where.not( - type: hidden_task_classes + type: Task.descendants.select(&:hide_from_queue_table_view).map(&:name) ) } @@ -138,10 +138,6 @@ class << self # With taks that are likely to need Reader to complete READER_PRIORITY_TASK_TYPES = [JudgeAssignTask.name, JudgeDecisionReviewTask.name].freeze - def hidden_task_classes - Task.descendants.select(&:hide_from_queue_table_view).map(&:name) - end - def reader_priority_task_types READER_PRIORITY_TASK_TYPES end diff --git a/app/models/tasks/evidence_submission_window_task.rb b/app/models/tasks/evidence_submission_window_task.rb index 0028fc54366..7c9cdb5f8fa 100644 --- a/app/models/tasks/evidence_submission_window_task.rb +++ b/app/models/tasks/evidence_submission_window_task.rb @@ -11,7 +11,7 @@ class EvidenceSubmissionWindowTask < Task before_validation :set_assignee - def initialize(args = {}) + def initialize(args) @end_date = args&.fetch(:end_date, nil) super(args&.except(:end_date)) end diff --git a/app/services/bva_appeal_status.rb b/app/services/bva_appeal_status.rb index c25886c15a3..1577a7ea7c6 100644 --- a/app/services/bva_appeal_status.rb +++ b/app/services/bva_appeal_status.rb @@ -3,7 +3,7 @@ # Determine the BVA workflow status of an Appeal (symbol and string) based on its Tasks. class BVAAppealStatus - attr_reader :status, :tasks + attr_reader :status SORT_KEYS = { not_distributed: 1, @@ -69,18 +69,8 @@ def attorney_task_names end end - Tasks = Struct.new( - :open, - :active, - :in_progress, - :cancelled, - :completed, - :assigned, - keyword_init: true - ) - - def initialize(tasks:) - @tasks = tasks + def initialize(appeal:) + @appeal = appeal @status = compute end @@ -96,12 +86,15 @@ def to_i SORT_KEYS[status] end - def as_json(_args = nil) + def as_json(_args) to_sym end private + attr_reader :appeal + + delegate :tasks, to: :appeal # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength def compute if open_pre_docket_task? @@ -120,7 +113,7 @@ def compute :ready_for_signature elsif active_sign_task? :signed - elsif completed_dispatch_task? && tasks.open.empty? + elsif completed_dispatch_task? && open_tasks.empty? :dispatched elsif completed_dispatch_task? :post_dispatch @@ -140,60 +133,84 @@ def compute end # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength + def open_tasks + @open_tasks ||= tasks.open + end + + def active_tasks + @active_tasks ||= tasks.active + end + + def assigned_tasks + @assigned_tasks ||= tasks.assigned + end + + def in_progress_tasks + @in_progress_tasks ||= tasks.in_progress + end + + def cancelled_tasks + @cancelled_tasks ||= tasks.cancelled + end + + def completed_tasks + @completed_tasks ||= tasks.completed + end + def open_pre_docket_task? - tasks.open.any? { |task| task.type == "PreDocketTask" } + open_tasks.any? { |task| task.is_a?(PreDocketTask) } end def open_distribution_task? - tasks.open.any? { |task| task.type == "DistributionTask" } + open_tasks.any? { |task| task.is_a?(DistributionTask) } end def open_timed_hold_task? - tasks.open.any? { |task| task.type == "TimedHoldTask" } + open_tasks.any? { |task| task.is_a?(TimedHoldTask) } end def active_judge_assign_task? - tasks.active.any? { |task| task.type == "JudgeAssignTask" } + active_tasks.any? { |task| task.is_a?(JudgeAssignTask) } end def assigned_attorney_task? - tasks.assigned.any? { |task| self.class.attorney_task_names.include?(task.type) } + assigned_tasks.any? { |task| self.class.attorney_task_names.include?(task.type) } end def active_colocated_task? - tasks.active.any? { |task| self.class.colocated_task_names.include?(task.type) } + active_tasks.any? { |task| self.class.colocated_task_names.include?(task.type) } end def attorney_task_in_progress? - tasks.in_progress.any? { |task| self.class.attorney_task_names.include?(task.type) } + in_progress_tasks.any? { |task| self.class.attorney_task_names.include?(task.type) } end def active_judge_decision_review_task? - tasks.active.any? { |task| task.type == "JudgeDecisionReviewTask" } + active_tasks.any? { |task| task.is_a?(JudgeDecisionReviewTask) } end def active_sign_task? - tasks.active.any? { |task| %w[BvaDispatchTask QualityReviewTask].include?(task.type) } + active_tasks.any? { |task| %w[BvaDispatchTask QualityReviewTask].include?(task.type) } end def completed_dispatch_task? - tasks.completed.any? { |task| task.type == "BvaDispatchTask" } + completed_tasks.any? { |task| task.is_a?(BvaDispatchTask) } end def docket_switched? # TODO: this should be updated to check that there are no active tasks once the task handling is implemented - tasks.completed.any? { |task| task.type == "DocketSwitchGrantedTask" } + completed_tasks.any? { |task| task.is_a?(DocketSwitchGrantedTask) } end def cancelled_root_task? - tasks.cancelled.any? { |task| task.type == "RootTask" } + cancelled_tasks.any? { |task| task.is_a?(RootTask) } end def misc_task? - tasks.active.any? { |task| self.class.misc_task_names.include?(task.type) } + active_tasks.any? { |task| self.class.misc_task_names.include?(task.type) } end def active_specialty_case_team_assign_task? - tasks.active.any? { |task| task.type == "SpecialtyCaseTeamAssignTask" } + active_tasks.any? { |task| task.is_a?(SpecialtyCaseTeamAssignTask) } end end diff --git a/app/services/search_query_service.rb b/app/services/search_query_service.rb deleted file mode 100644 index 11286329925..00000000000 --- a/app/services/search_query_service.rb +++ /dev/null @@ -1,96 +0,0 @@ -# frozen_string_literal: true - -class SearchQueryService - def initialize(file_number: nil, docket_number: nil, veteran_ids: nil) - @docket_number = docket_number - @file_number = file_number - @veteran_ids = veteran_ids - @queries = SearchQueryService::Query.new - end - - def search_by_veteran_file_number - combined_results - end - - def search_by_docket_number - results = ActiveRecord::Base.connection.exec_query( - sanitize([queries.docket_number_query, docket_number]) - ) - - results.map do |row| - AppealRow.new(row).search_response - end - end - - def search_by_veteran_ids - combined_results - end - - private - - attr_reader :docket_number, :file_number, :queries, :veteran_ids - - def combined_results - search_results.map do |row| - if row["type"] != "legacy_appeal" - AppealRow.new(row).search_response - else - vacols_row = vacols_results.find { |result| result["vacols_id"] == row["external_id"] } - LegacyAppealRow.new(row, vacols_row).search_response - end - end - end - - def vacols_ids - legacy_results.map { |result| result["external_id"] } - end - - def legacy_results - search_results.select { |result| result["type"] == "legacy_appeal" } - end - - def search_results - @search_results ||= - if file_number.present? - file_number_search_results - else - veteran_ids_search_results - end - end - - def veteran_ids_search_results - ActiveRecord::Base - .connection - .exec_query( - sanitize([queries.veteran_ids_query, veteran_ids, veteran_ids]) - ) - .uniq { |result| result["external_id"] } - end - - def file_number_search_results - ActiveRecord::Base - .connection - .exec_query(file_number_or_ssn_query) - .uniq { |result| result["external_id"] } - end - - def file_number_or_ssn_query - sanitize( - [ - queries.veteran_file_number_query, - *[file_number].cycle(queries.veteran_file_number_num_params).to_a - ] - ) - end - - def vacols_results - @vacols_results ||= begin - vacols_query = VACOLS::Record.sanitize_sql_array([queries.vacols_query, vacols_ids]) - VACOLS::Record.connection.exec_query(vacols_query) - end - end - - def sanitize(values) - ActiveRecord::Base.sanitize_sql_array(values) - end -end diff --git a/app/services/search_query_service/api_response.rb b/app/services/search_query_service/api_response.rb deleted file mode 100644 index 76a216b678e..00000000000 --- a/app/services/search_query_service/api_response.rb +++ /dev/null @@ -1,3 +0,0 @@ -# frozen_string_literal: true - -SearchQueryService::ApiResponse = Struct.new(:id, :type, :attributes, keyword_init: true) diff --git a/app/services/search_query_service/appeal_row.rb b/app/services/search_query_service/appeal_row.rb deleted file mode 100644 index e47be978e31..00000000000 --- a/app/services/search_query_service/appeal_row.rb +++ /dev/null @@ -1,167 +0,0 @@ -# frozen_string_literal: true - -class SearchQueryService::AppealRow - def initialize(query_row) - @query_row = query_row - end - - def search_response - SearchQueryService::SearchResponse.new( - queried_appeal, - :appeal, - SearchQueryService::ApiResponse.new( - id: query_row["id"], - type: "appeal", - attributes: attributes - ) - ) - end - - private - - attr_reader :query_row - - # rubocop:disable Metrics/MethodLength - def attributes - SearchQueryService::Attributes.new( - aod: aod, - appellant_full_name: appellant_full_name, - assigned_to_location: queried_appeal.assigned_to_location, - assigned_attorney: assigned_attorney, - caseflow_veteran_id: query_row["veteran_id"], - contested_claim: contested_claim, - decision_date: decision_date, - decision_issues: decision_issues, - docket_name: query_row["docket_type"], - docket_number: docket_number, - external_id: query_row["external_id"], - hearings: hearings, - issues: issues, - mst: mst_status, - overtime: query_row["overtime"], - pact: pact_status, - paper_case: false, - status: queried_appeal.status, - type: stream_type, - veteran_appellant_deceased: veteran_appellant_deceased, - veteran_file_number: veteran_file_number, - veteran_full_name: veteran_full_name, - withdrawn: withdrawn - ) - end - # rubocop:enable Metrics/MethodLength - - def aod - query_row["aod_granted_for_person"].present? - end - - def decision_issues - json_array("decision_issues") - end - - def docket_number - attrs, = JSON.parse query_row["appeal"] - attrs["stream_docket_number"] - end - - def decision_date - Date.parse(query_row["decision_date"]) - rescue TypeError, Date::Error - nil - end - - def appellant_full_name - FullName.new(query_row["person_first_name"], "", query_row["person_last_name"]).to_s - end - - def veteran_full_name - FullName.new(query_row["veteran_first_name"], "", query_row["veteran_last_name"]).to_s - end - - def veteran_file_number - attrs, = JSON.parse(query_row["appeal"]) - attrs["veteran_file_number"] - end - - def issue(attributes) - unless FeatureToggle.enabled?(:pact_identification) - attributes.delete("pact_status") - end - unless FeatureToggle.enabled?(:mst_identification) - attributes.delete("mst_status") - end - end - - def issues - json_array("request_issues").map do |attributes| - attributes.tap do |attrs| - issue(attrs) - end - end - end - - def hearings - json_array("hearings") - end - - def withdrawn - WithdrawnDecisionReviewPolicy.new( - Struct.new( - :active_request_issues, - :withdrawn_request_issues - ).new( - json_array("active_request_issues"), - json_array("active_request_issues") - ) - ).satisfied? - end - - def stream_type - (query_row["stream_type"] || "Original").titleize - end - - def contested_claim - json_array("active_request_issues").any? do |issue| - %w(Contested Apportionment).any? do |code| - category = issue["nonrating_issue_category"] || "" - category.include?(code) - end - end - end - - def veteran_appellant_deceased - !!query_row["date_of_death"] && !json_array("appeal").first["veteran_is_not_claimant"] - end - - def pact_status - json_array("decision_issues").any? do |issue| - issue["pact_status"] - end - end - - def mst_status - json_array("decision_issues").any? do |issue| - issue["mst_status"] - end - end - - def queried_appeal - @queried_appeal ||= begin - appeal_attrs, = JSON.parse query_row["appeal"] - - SearchQueryService::QueriedAppeal.new( - attributes: appeal_attrs, - tasks_attributes: json_array("tasks"), - hearings_attributes: json_array("hearings") - ) - end - end - - def assigned_attorney - json_array("assigned_attorney").first - end - - def json_array(key) - JSON.parse(query_row[key] || "[]") - end -end diff --git a/app/services/search_query_service/attributes.rb b/app/services/search_query_service/attributes.rb deleted file mode 100644 index 6c4487a9b1e..00000000000 --- a/app/services/search_query_service/attributes.rb +++ /dev/null @@ -1,30 +0,0 @@ -# frozen_string_literal: true - -SearchQueryService::Attributes = Struct.new( - :aod, - :power_of_attorney, - :appellant_full_name, - :assigned_attorney, - :assigned_judge, - :assigned_to_location, - :caseflow_veteran_id, - :decision_date, - :decision_issues, - :docket_name, - :docket_number, - :external_id, - :hearings, - :issues, - :mst, - :overtime, - :pact, - :paper_case, - :status, - :type, - :veteran_appellant_deceased, - :veteran_file_number, - :veteran_full_name, - :contested_claim, - :withdrawn, - keyword_init: true -) diff --git a/app/services/search_query_service/legacy_appeal_row.rb b/app/services/search_query_service/legacy_appeal_row.rb deleted file mode 100644 index 22009feb574..00000000000 --- a/app/services/search_query_service/legacy_appeal_row.rb +++ /dev/null @@ -1,142 +0,0 @@ -# frozen_string_literal: true - -class SearchQueryService::LegacyAppealRow - def initialize(search_row, vacols_row) - @search_row = search_row - @vacols_row = vacols_row - end - - def search_response - SearchQueryService::SearchResponse.new( - legacy_appeal, - :legacy_appeal, - SearchQueryService::ApiResponse.new( - id: search_row["id"], - type: "legacy_appeal", - attributes: attributes - ) - ) - end - - private - - attr_reader :search_row, :vacols_row - - # rubocop:disable Metrics/MethodLength - def attributes - SearchQueryService::LegacyAttributes.new( - aod: aod, - appellant_full_name: appellant_full_name, - assigned_to_location: vacols_row["bfcurloc"], - caseflow_veteran_id: search_row["veteran_id"], - decision_date: decision_date, - docket_name: "legacy", - docket_number: vacols_row["tinum"], - external_id: vacols_row["vacols_id"], - hearings: hearings, - issues: [{}] * vacols_row["issues_count"], - mst: mst, - overtime: search_row["overtime"], - pact: pact, - paper_case: paper_case, - status: status, - type: stream_type, - veteran_appellant_deceased: veteran_appellant_deceased, - veteran_file_number: search_row["veteran_file_number"], - veteran_full_name: veteran_full_name - ) - end - # rubocop:enable Metrics/MethodLength - - def hearings - vacols_json_array("hearings").map do |attrs| - HearingAttributes.new(attrs).call - end - end - - class HearingAttributes - def initialize(attributes) - @attributes = attributes - end - - def call - { - disposition: VACOLS::CaseHearing::HEARING_DISPOSITIONS[attributes["disposition"].try(:to_sym)], - request_type: attributes["type"], - appeal_type: VACOLS::Case::TYPES[attributes["bfac"]], - external_id: attributes["external_id"], - date: HearingMapper.datetime_based_on_type( - datetime: attributes["date"], - regional_office: regional_office(attributes["venue"]), - type: attributes["type"] - ) - } - end - - private - - attr_reader :attributes - - def regional_office(ro_key) - RegionalOffice.find!(ro_key) - rescue NotFoundError - nil - end - end - - def vacols_json_array(key) - JSON.parse(vacols_row[key] || "[]") - end - - def veteran_appellant_deceased - search_row["date_of_death"].present? && - search_row["person_first_name"].present? - end - - def stream_type - VACOLS::Case::TYPES[vacols_row["bfac"]] - end - - def status - VACOLS::Case::STATUS[vacols_row["bfmpro"]] - end - - def paper_case - folder = Struct.new(:tivbms, :tisubj2).new( - vacols_row["tivbms"], - vacols_row["tisubj2"] - ) - AppealRepository.folder_type_from(folder) - end - - def mst - vacols_row["issues_mst_count"] > 0 - end - - def pact - vacols_row["issues_pact_count"] > 0 - end - - def appellant_full_name - FullName.new(vacols_row["sspare2"], "", vacols_row["sspare1"]).to_s - end - - def veteran_full_name - FullName.new(vacols_row["snamef"], "", vacols_row["snamel"]).to_s - end - - def aod - vacols_row["aod"] == 1 - end - - def decision_date - AppealRepository.normalize_vacols_date(vacols_row["bfddec"]) - end - - def legacy_appeal - @legacy_appeal ||= begin - appeal_attrs, = JSON.parse search_row["appeal"] - SearchQueryService::QueriedLegacyAppeal.new(attributes: appeal_attrs) - end - end -end diff --git a/app/services/search_query_service/legacy_attributes.rb b/app/services/search_query_service/legacy_attributes.rb deleted file mode 100644 index c4cf999b5f3..00000000000 --- a/app/services/search_query_service/legacy_attributes.rb +++ /dev/null @@ -1,25 +0,0 @@ -# frozen_string_literal: true - -SearchQueryService::LegacyAttributes = Struct.new( - :aod, - :appellant_full_name, - :assigned_to_location, - :caseflow_veteran_id, - :decision_date, - :docket_name, - :docket_number, - :external_id, - :hearings, - :issues, - :mst, - :overtime, - :pact, - :paper_case, - :status, - :type, - :veteran_appellant_deceased, - :veteran_file_number, - :veteran_full_name, - :withdrawn, - keyword_init: true -) diff --git a/app/services/search_query_service/queried_appeal.rb b/app/services/search_query_service/queried_appeal.rb deleted file mode 100644 index 297ad46cdcf..00000000000 --- a/app/services/search_query_service/queried_appeal.rb +++ /dev/null @@ -1,133 +0,0 @@ -# frozen_string_literal: true - -class SearchQueryService::QueriedAppeal < SimpleDelegator - def initialize(attributes:, tasks_attributes:, hearings_attributes:) - @attributes = OpenStruct.new( - appeal: attributes, - tasks: tasks_attributes, - hearings: hearings_attributes, - root_task: attributes.delete("root_task") || {}, - claimants: attributes.delete("claimants") || [] - ) - - super(appeal) - end - - def assigned_to_location - return COPY::CASE_LIST_TABLE_POST_DECISION_LABEL if root_task&.status == Constants.TASK_STATUSES.completed - - return most_recently_updated_visible_task.assigned_to_label if most_recently_updated_visible_task - - # this condition is no longer needed since we only want active or on hold tasks - return most_recently_updated_task&.assigned_to_label if most_recently_updated_task.present? - - fetch_api_status - end - - def claimant_participant_ids - claimants.map(&:participant_id) - end - - def claimants - @claimants ||= begin - attributes.claimants.map do |attrs| - Struct.new(:participant_id).new(attrs["participant_id"]) - end - end - end - - def root_task - @root_task ||= begin - if attributes.root_task.present? - Task.new.tap do |task| - task.assign_attributes attributes.root_task - end - end - end - end - - def open_tasks - @open_tasks ||= tasks.select do |task| - Task.open_statuses.include?(task.status) - end - end - - def active? - Task.active_statuses.include?(attributes.root_task["status"]) - end - - def pending_schedule_hearing_tasks - open_tasks.select { |task| task.type == "ScheduleHearingTask" } - end - - def evidence_submission_hold_pending_tasks - open_tasks.select { |task| task.type == "EvidenceSubmissionWindowTask" } - end - - def status - BVAAppealStatus.new( - tasks: BVAAppealStatus::Tasks.new( - open: tasks.select(&:open?), - active: tasks.select(&:active?), - in_progress: tasks.select(&:in_progress?), - cancelled: tasks.select(&:cancelled?), - completed: tasks.select(&:completed?), - assigned: tasks.select(&:assigned?) - ) - ).status - end - - private - - attr_reader :attributes - - def appeal - @appeal ||= Appeal.new.tap do |appeal| - appeal.assign_attributes(attributes.appeal) - end - end - - def most_recently_updated_visible_task - visible_tasks.select { |task| Task.active_statuses.include?(task.status) }.max_by(&:updated_at) || - visible_tasks.select { |task| task.status == "on_hold" }.max_by(&:updated_at) - end - - def visible_tasks - @visible_tasks ||= tasks.reject do |task| - Task.hidden_task_classes.include?(task.type) - end - end - - def most_recently_updated_task - tasks.max_by(&:updated_at) - end - - def tasks - @tasks ||= begin - attributes.tasks.map do |attrs| - attrs["type"].constantize.new.tap do |task| - task.assign_attributes attrs - end - end - end - end - - def fetch_api_status - AppealStatusApiDecorator.new( - self, - scheduled_hearing - ).fetch_status.to_s.titleize.to_sym - end - - def scheduled_hearing - @scheduled_hearing ||= begin - hearings = attributes.hearings.map do |attrs| - SearchQueryService::QueriedHearing.new(attrs) - end - - hearings.reject(&:disposition).find do |hearing| - hearing.scheduled_for >= Time.zone.today - end - end - end -end diff --git a/app/services/search_query_service/queried_hearing.rb b/app/services/search_query_service/queried_hearing.rb deleted file mode 100644 index efd16b8d187..00000000000 --- a/app/services/search_query_service/queried_hearing.rb +++ /dev/null @@ -1,33 +0,0 @@ -# frozen_string_literal: true - -class SearchQueryService::QueriedHearing < SimpleDelegator - def initialize(attributes) - @attributes = attributes - manage_attributes - - super(hearing) - end - - def hearing_day - OpenStruct.new(hearing_day_attributes) - end - - def updated_by - OpenStruct.new(updated_by_attributes) - end - - private - - attr_reader :attributes, :hearing_day_attributes, :updated_by_attributes - - def manage_attributes - @hearing_day_attributes = attributes.delete("hearing_day") - @updated_by_attributes = attributes.delete("updated_by") - end - - def hearing - Hearing.new.tap do |hearing| - hearing.assign_attributes attributes - end - end -end diff --git a/app/services/search_query_service/queried_legacy_appeal.rb b/app/services/search_query_service/queried_legacy_appeal.rb deleted file mode 100644 index 50aaab31a7f..00000000000 --- a/app/services/search_query_service/queried_legacy_appeal.rb +++ /dev/null @@ -1,41 +0,0 @@ -# frozen_string_literal: true - -class SearchQueryService::QueriedLegacyAppeal < SimpleDelegator - def initialize(attributes:) - @attributes = attributes - @root_task_attributes = attributes.delete("root_task") - @veteran_attributes = attributes.delete("veteran") - - super(legacy_appeal) - end - - def veteran - @veteran ||= Veteran.new.tap do |veteran| - veteran.assign_attributes veteran_attributes - end - end - - def root_task - @root_task ||= begin - if root_task_attributes - RootTask.new.tap do |root_task| - root_task.assign_attributes root_task_attributes - end - end - end - end - - def claimant_participant_ids - veteran.participant_id - end - - private - - attr_reader :attributes, :root_task_attributes, :veteran_attributes - - def legacy_appeal - @legacy_appeal ||= LegacyAppeal.new.tap do |appeal| - appeal.assign_attributes(attributes) - end - end -end diff --git a/app/services/search_query_service/query.rb b/app/services/search_query_service/query.rb deleted file mode 100644 index c089ac2750e..00000000000 --- a/app/services/search_query_service/query.rb +++ /dev/null @@ -1,390 +0,0 @@ -# frozen_string_literal: true - -class SearchQueryService::Query - def docket_number_query - <<-SQL - #{appeals_internal_query} - where a.stream_docket_number=?; - SQL - end - - def veteran_file_number_num_params - 4 - end - - def veteran_file_number_query - <<-SQL - ( - #{appeals_internal_query} - where v.ssn=? or v.file_number=? - ) - UNION - ( - #{legacy_appeals_internal_query} - where v.ssn=? or v.file_number=? - ) - SQL - end - - def veteran_ids_query - <<-SQL - ( - #{appeals_internal_query} - where v.id in (?) - ) - UNION - ( - #{legacy_appeals_internal_query} - where v.id in (?) - ) - SQL - end - - def vacols_query - <<-SQL - select - aod, - "cases".bfkey vacols_id, - "cases".bfcurloc, - "cases".bfddec, - "cases".bfmpro, - "cases".bfac, - "cases".bfcorlid, - "correspondents".snamef, - "correspondents".snamel, - "correspondents".sspare1, - "correspondents".sspare2, - "correspondents".slogid, - "folders".tinum, - "folders".tivbms, - "folders".tisubj2, - (select - JSON_ARRAYAGG(JSON_OBJECT( - 'venue' value #{case_hearing_venue_select}, - 'external_id' value "h".hearing_pkseq, - 'type' value "h".hearing_type, - 'disposition' value "h".hearing_disp, - 'date' value "h".hearing_date - )) - from hearsched "h" - where "h".folder_nr="cases".bfkey - ) hearings, - (select count("issues".isskey) from issues "issues" where "issues".isskey="cases".bfkey) issues_count, - (select count("hearings".hearing_pkseq) from hearsched "hearings" where "hearings".folder_nr="cases".bfkey) hearing_count, - (select count("issues".isskey) from issues "issues" where "issues".isskey="cases".bfkey and "issues".issmst='Y') issues_mst_count, - (select count("issues".isskey) from issues "issues" where "issues".isskey="cases".bfkey and "issues".isspact='Y') issues_pact_count - from - brieff "cases" - left join folder "folders" - on "cases".bfkey="folders".ticknum - left join corres "correspondents" - on "cases".bfcorkey="correspondents".stafkey - #{VACOLS::Case::JOIN_AOD} - where - "cases".bfkey in (?) - SQL - end - - private - - def case_hearing_venue_select - <<-SQL - case - when "h".hearing_type='#{VACOLS::CaseHearing::HEARING_TYPE_LOOKUP[:video]}' AND - "h".hearing_date < '#{VACOLS::CaseHearing::VACOLS_VIDEO_HEARINGS_END_DATE}' - then #{Rails.application.config.vacols_db_name}.HEARING_VENUE("h".vdkey) - else "cases".bfregoff - end - SQL - end - - def legacy_appeals_internal_query - <<-SQL - select - a.id, - a.vacols_id external_id, - 'legacy_appeal' type, - null aod_granted_for_person, - 'legacy' docket_type, - ( - select - jsonb_agg(la2) - from - ( - select - la.*, - ( - select - row_to_json(t.*) - from tasks t - where - t.appeal_id=a.id and - t.type='RootTask' and - t.appeal_type='Appeal' - order by t.updated_at desc - limit 1 - ) root_task, - (select row_to_json(v.*)) veteran - from legacy_appeals la - where la.id=a.id - ) la2 - ) appeal, - dd.decision_date, - wm.overtime, - pp.first_name person_first_name, - pp.last_name person_last_name, - v.id veteran_id, - v.first_name veteran_first_name, - v.last_name veteran_last_name, - v.file_number as veteran_file_number, - v.date_of_death, - ( - select jsonb_agg(u2) from - ( - select - u.* - from tasks t - left join users u on - t.assigned_to_id=u.id and - t.assigned_to_type='User' - where - t.appeal_type = 'LegacyAppeal' and - t.appeal_id=a.id and - t.type='AttorneyTask' and - t.status != '#{Constants.TASK_STATUSES.cancelled}' - order by t.created_at desc - limit 1 - ) u2 - ) assigned_attorney, - null request_issues, - null active_request_issues, - null withdrawn_request_issues, - null decision_issues, - null hearings_count, - '[]' hearings, - ( - select jsonb_agg(t2) from - ( - select - t.* - from tasks t - left join organizations o on o.id=t.assigned_to_id - left join users u on u.id=t.assigned_to_id - where - t.appeal_id=a.id and - t.appeal_type='LegacyAppeal' - order by updated_at desc - ) t2 - ) tasks - from legacy_appeals a - left join claimants cl on cl.decision_review_id=a.id and cl.decision_review_type='LegacyAppeal' - left join people pp on cl.participant_id=pp.participant_id - left join work_modes wm on wm.appeal_id=a.id and wm.appeal_type='LegacyAppeal' - left join decision_documents dd on dd.appeal_id=a.id and dd.appeal_type='LegacyAppeal' - left join veterans v on v.file_number=( - select - case - when right(a.vbms_id, 1) = 'C' then lpad(regexp_replace(a.vbms_id, '[^0-9]+', '', 'g'), 8, '0') - else regexp_replace(a.vbms_id, '[^0-9]+', '', 'g') - end - ) or v.ssn=( - select - case - when right(a.vbms_id, 1) = 'C' then lpad(regexp_replace(a.vbms_id, '[^0-9]+', '', 'g'), 8, '0') - else regexp_replace(a.vbms_id, '[^0-9]+', '', 'g') - end - ) - SQL - end - - def appeals_internal_query - <<-SQL - select - a.id, - a.uuid::varchar external_id, - a.stream_type type, - aod.id aod_granted_for_person, - a.docket_type, - ( - select jsonb_agg(a2) - from - ( - select - appeals.*, - ( - select - row_to_json(t.*) - from tasks t - where - t.appeal_id=a.id and - t.type='RootTask' and - t.appeal_type='Appeal' - order by updated_at desc - limit 1 - ) root_task, - ( - select jsonb_agg(c2) from - ( - select - c.id, - c.participant_id - from claimants c - where - c.decision_review_type = 'Appeal' and - c.decision_review_id=a.id - ) c2 - ) claimants - from appeals - where id=a.id - ) a2 - ) appeal, - dd.decision_date, - wm.overtime, - pp.first_name person_first_name, - pp.last_name person_last_name, - v.id veteran_id, - v.first_name veteran_first_name, - v.last_name veteran_last_name, - v.file_number as veteran_file_number, - v.date_of_death, - ( - select jsonb_agg(u2) from - ( - select - u.* - from tasks t - left join users u on - t.assigned_to_id=u.id and - t.assigned_to_type='User' - where - t.appeal_type = 'Appeal' and - t.appeal_id=a.id and - t.type='AttorneyTask' and - t.status != '#{Constants.TASK_STATUSES.cancelled}' - order by t.created_at desc - limit 1 - ) u2 - ) assigned_attorney, - ( - select jsonb_agg(ri2) from - ( - select - ri.id, - ri.benefit_type program, - ri.notes, - ri.decision_date, - ri.nonrating_issue_category, - ri.mst_status, - ri.pact_status, - ri.mst_status_update_reason_notes mst_justification, - ri.pact_status_update_reason_notes pact_justification - from request_issues ri - where - ri.decision_review_type='Appeal' and - ri.decision_review_id=a.id - ) ri2 - ) request_issues, - ( - select jsonb_agg(ri2) from - ( - select - nonrating_issue_category - from request_issues ri - where - ri.ineligible_reason is null and - ri.closed_at is null and - (ri.split_issue_status is null or ri.split_issue_status = 'in_progress') and - ri.decision_review_type='Appeal' and - ri.decision_review_id=a.id - ) ri2 - ) active_request_issues, - ( - select jsonb_agg(ri2) from - ( - select - nonrating_issue_category - from request_issues ri - where - ri.ineligible_reason is null and - ri.closed_status = 'widthrawn' and - ri.decision_review_type='Appeal' and - ri.decision_review_id=a.id - ) ri2 - ) withdrawn_request_issues, - ( - select jsonb_agg(di2) from - ( - select - di.id, - di.disposition, - di.description, - di.benefit_type, - di.diagnostic_code, - di.mst_status, - di.pact_status, - array( - select rdi.id - from request_decision_issues rdi - where rdi.decision_issue_id=di.id - ) request_issue_ids, - array( - select rr2 from - ( - select - rr.id, - rr.code, - rr.post_aoj - from remand_reasons rr - where rr.decision_issue_id=di.id - ) rr2 - ) remand_reasons - from decision_issues di - where - di.decision_review_type='Appeal' and - di.decision_review_id = a.id - ) di2 - ) decision_issues, - (select count(id) from hearings h where h.appeal_id=a.id) hearings_count, - ( - select jsonb_agg(h2) from - ( - select - h.*, - ( - select row_to_json(ub) - from (select * from users u where u.id=h.updated_by_id limit 1) ub - ) updated_by, - ( - select row_to_json(hd2) - from (select * from hearing_days hd where hd.id=h.hearing_day_id limit 1) hd2 - ) hearing_day - from - hearings h - where - h.appeal_id=a.id - ) h2 - ) hearings, - ( - select jsonb_agg(t2) from - ( - select - t.* - from tasks t - left join organizations o on o.id=t.assigned_to_id - left join users u on u.id=t.assigned_to_id - where - t.appeal_id=a.id and - t.appeal_type='Appeal' - order by updated_at desc - ) t2 - ) tasks - from appeals a - left join claimants cl on cl.decision_review_id=a.id and cl.decision_review_type='Appeal' - left join people pp on cl.participant_id=pp.participant_id - left join work_modes wm on wm.appeal_id=a.id and wm.appeal_type='Appeal' - left join decision_documents dd on dd.appeal_id=a.id and dd.appeal_type='Appeal' - left join advance_on_docket_motions aod on aod.appeal_id=a.id and aod.person_id=pp.id and aod.appeal_type='Appeal' - left join veterans v on a.veteran_file_number=v.file_number or a.veteran_file_number=v.ssn - SQL - end -end diff --git a/app/services/search_query_service/search_response.rb b/app/services/search_query_service/search_response.rb deleted file mode 100644 index 09ca32c022e..00000000000 --- a/app/services/search_query_service/search_response.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -SearchQueryService::SearchResponse = Struct.new(:appeal, :type, :api_response) do - def filter_restricted_info!(statuses) - if statuses.include?(api_response.attributes.status) - api_response.attributes.assigned_to_location = nil - end - end -end diff --git a/app/services/search_query_service/vso_user_search_results.rb b/app/services/search_query_service/vso_user_search_results.rb deleted file mode 100644 index 7b7e313b29e..00000000000 --- a/app/services/search_query_service/vso_user_search_results.rb +++ /dev/null @@ -1,68 +0,0 @@ -# frozen_string_literal: true - -class SearchQueryService::VsoUserSearchResults - def initialize(search_results:, user:) - @user = user - @search_results = search_results - - filter_restricted_results! - end - - def call - established_results.select do |result| - if result.type == :appeal - result.appeal.claimants.any? do |claimant| - vso_participant_ids.include?(poas.dig(claimant.participant_id, :participant_id)) - end - else - vso_participant_ids.include?(poas.dig(result.appeal.veteran.participant_id, :participant_id)) - end - end - end - - private - - attr_reader :search_results, :user - - RESTRICTED_STATUSES = - [ - :distributed_to_judge, - :ready_for_signature, - :on_hold, - :misc, - :unknown, - :assigned_to_attorney - ].freeze - - def filter_restricted_results! - search_results.map do |result| - result.filter_restricted_info!(RESTRICTED_STATUSES) - end - end - - def vso_participant_ids - @vso_participant_ids ||= user.vsos_user_represents.map { |poa| poa[:participant_id] } - end - - def established_results - @established_results ||= search_results.select do |result| - result.type == :legacy_appeal || result.appeal.established_at.present? - end - end - - def claimant_participant_ids - @claimant_participant_ids ||= established_results.flat_map do |result| - result.appeal.claimant_participant_ids - end.uniq - end - - def poas - Rails.logger.info "BGS Called `fetch_poas_by_participant_ids` with \"#{claimant_participant_ids.join('"')}\"" - - @poas ||= bgs.fetch_poas_by_participant_ids(claimant_participant_ids) - end - - def bgs - @bgs ||= BGSService.new - end -end diff --git a/app/workflows/case_search_results_base.rb b/app/workflows/case_search_results_base.rb index 56c7d870c1e..f8e4e1a0528 100644 --- a/app/workflows/case_search_results_base.rb +++ b/app/workflows/case_search_results_base.rb @@ -56,54 +56,18 @@ def search_call ) end - def api_call - @success = valid? - - api_search_results if success - - FormResponse.new( - success: success, - errors: errors.messages[:workflow], - extra: error_status_or_api_search_results - ) - end - protected attr_reader :status, :user - def search_page_json_appeals(appeals) - ama_appeals, legacy_appeals = appeals.partition { |appeal| appeal.is_a?(Appeal) } - - ama_hash = WorkQueue::AppealSearchSerializer.new( - ama_appeals, is_collection: true, params: { user: user } - ).serializable_hash - - legacy_hash = WorkQueue::LegacyAppealSearchSerializer.new( - legacy_appeals, is_collection: true, params: { user: user } - ).serializable_hash - - ama_hash[:data].concat(legacy_hash[:data]) - end - - def json_appeals(appeals) - ama_appeals, legacy_appeals = appeals.partition { |appeal| appeal.is_a?(Appeal) } - - ama_hash = WorkQueue::AppealSerializer.new( - ama_appeals, is_collection: true, params: { user: user } - ).serializable_hash - - legacy_hash = WorkQueue::LegacyAppealSerializer.new( - legacy_appeals, is_collection: true, params: { user: user } - ).serializable_hash - - ama_hash[:data].concat(legacy_hash[:data]) - end - def current_user_is_vso_employee? user.vso_employee? end + def appeals + AppealFinder.new(user: user).find_appeals_for_veterans(veterans_user_can_access) + end + def claim_reviews ClaimReview.find_all_visible_by_file_number(veterans_user_can_access.map(&:file_number)) end @@ -113,14 +77,10 @@ def veterans [] end - def appeals - [] - end - # Users may also view appeals with appellants whom they represent. # We use this to add these appeals back into results when the user is not on the veteran's poa. def additional_appeals_user_can_access - appeals.map(&:appeal).filter do |appeal| + appeals.filter do |appeal| appeal.veteran_is_not_claimant && user.organizations.any? do |uo| appeal.representatives.include?(uo) @@ -132,6 +92,34 @@ def veterans_user_can_access @veterans_user_can_access ||= veterans.select { |veteran| access?(veteran.file_number) } end + def json_appeals(appeals) + ama_appeals, legacy_appeals = appeals.partition { |appeal| appeal.is_a?(Appeal) } + + ama_hash = WorkQueue::AppealSerializer.new( + ama_appeals, is_collection: true, params: { user: user } + ).serializable_hash + + legacy_hash = WorkQueue::LegacyAppealSerializer.new( + legacy_appeals, is_collection: true, params: { user: user } + ).serializable_hash + + ama_hash[:data].concat(legacy_hash[:data]) + end + + def search_page_json_appeals(appeals) + ama_appeals, legacy_appeals = appeals.partition { |appeal| appeal.is_a?(Appeal) } + + ama_hash = WorkQueue::AppealSearchSerializer.new( + ama_appeals, is_collection: true, params: { user: user } + ).serializable_hash + + legacy_hash = WorkQueue::LegacyAppealSearchSerializer.new( + legacy_appeals, is_collection: true, params: { user: user } + ).serializable_hash + + ama_hash[:data].concat(legacy_hash[:data]) + end + private attr_accessor :errors @@ -154,11 +142,7 @@ def valid? def validation_hook; end def access?(file_number) - return true if !current_user_is_vso_employee? - - Rails.logger.info "BGS Called `can_access?` with \"#{file_number}\"" - - bgs.can_access?(file_number) + !current_user_is_vso_employee? || bgs.can_access?(file_number) end def bgs @@ -184,40 +168,10 @@ def error_status_or_case_search_results case_search_results end - def error_status_or_api_search_results - return { status: status } unless success - - api_search_results - end - - def error_status_or_api_case_search_results - return { status: status } unless success - - api_case_search_results - end - - def api_search_results - @api_search_results ||= { - search_results: { - appeals: json_appeals(appeal_finder_appeals), - claim_reviews: claim_reviews.map(&:search_table_ui_hash) - } - } - end - - def api_case_search_results - @api_case_search_results ||= { - case_search_results: { - appeals: search_page_json_appeals(appeal_finder_appeals), - claim_reviews: claim_reviews.map(&:search_table_ui_hash) - } - } - end - def search_results @search_results ||= { search_results: { - appeals: appeals.map(&:api_response), + appeals: json_appeals(appeals), claim_reviews: claim_reviews.map(&:search_table_ui_hash) } } @@ -226,7 +180,7 @@ def search_results def case_search_results @case_search_results ||= { case_search_results: { - appeals: appeals.map(&:api_response), + appeals: search_page_json_appeals(appeals), claim_reviews: claim_reviews.map(&:search_table_ui_hash) } } diff --git a/app/workflows/case_search_results_for_caseflow_veteran_id.rb b/app/workflows/case_search_results_for_caseflow_veteran_id.rb index a471b76f598..3fe2b5604df 100644 --- a/app/workflows/case_search_results_for_caseflow_veteran_id.rb +++ b/app/workflows/case_search_results_for_caseflow_veteran_id.rb @@ -16,28 +16,6 @@ def veterans attr_reader :caseflow_veteran_ids - def appeal_finder_appeals - AppealFinder.new(user: user).find_appeals_for_veterans(veterans_user_can_access) - end - - def search_results - @search_results ||= SearchQueryService.new( - veteran_ids: veterans_user_can_access.map(&:id) - ).search_by_veteran_ids - end - - def vso_user_search_results - SearchQueryService::VsoUserSearchResults.new(user: user, search_results: search_results).call - end - - def appeals - if user.vso_employee? - vso_user_search_results - else - search_results - end - end - def validation_hook validate_veterans_exist end diff --git a/app/workflows/case_search_results_for_docket_number.rb b/app/workflows/case_search_results_for_docket_number.rb index 78b9dc49656..6c16a50a46d 100644 --- a/app/workflows/case_search_results_for_docket_number.rb +++ b/app/workflows/case_search_results_for_docket_number.rb @@ -13,10 +13,6 @@ def claim_reviews end def appeals - SearchQueryService.new(docket_number: docket_number).search_by_docket_number - end - - def appeal_finder_appeals AppealFinder.find_appeals_by_docket_number(docket_number) end @@ -37,7 +33,7 @@ def not_found_error def veterans # Determine vet that corresponds to docket number so we can validate user can access - @file_numbers_for_appeals ||= appeals.map(&:api_response).map(&:attributes).map(&:veteran_file_number) + @file_numbers_for_appeals ||= appeals.map(&:veteran_file_number) @veterans ||= VeteranFinder.find_or_create_all(@file_numbers_for_appeals) end end diff --git a/app/workflows/case_search_results_for_veteran_file_number.rb b/app/workflows/case_search_results_for_veteran_file_number.rb index ba03eae1bce..3f0f97fe466 100644 --- a/app/workflows/case_search_results_for_veteran_file_number.rb +++ b/app/workflows/case_search_results_for_veteran_file_number.rb @@ -23,33 +23,6 @@ def validate_file_number_or_ssn_presence @status = :bad_request end - def appeals - if user.vso_employee? - vso_user_search_results - else - search_results - end - end - - def vso_user_search_results - SearchQueryService::VsoUserSearchResults.new(user: user, search_results: search_results).call - end - - def search_results - @search_results ||= SearchQueryService.new(file_number: file_number_or_ssn).search_by_veteran_file_number - end - - def appeal_finder_appeals - AppealFinder.new(user: user).find_appeals_for_veterans(veterans_user_can_access) - end - - def file_number_or_ssn_presence - return if file_number_or_ssn - - errors.add(:workflow, missing_veteran_file_number_or_ssn_error) - @status = :bad_request - end - def missing_veteran_file_number_or_ssn_error { "title": "Veteran file number missing", diff --git a/spec/feature/queue/search_spec.rb b/spec/feature/queue/search_spec.rb index 02e393a7350..7be69b901dd 100644 --- a/spec/feature/queue/search_spec.rb +++ b/spec/feature/queue/search_spec.rb @@ -612,9 +612,7 @@ def perform_search(docket_number = appeal.docket_number) context "when backend returns non-serialized error" do it "displays generic server error message" do - allow_any_instance_of(SearchQueryService).to( - receive(:search_by_veteran_file_number).and_raise(StandardError) - ) + allow(LegacyAppeal).to receive(:fetch_appeals_by_file_number).and_raise(StandardError) visit "/search" fill_in "searchBarEmptyList", with: appeal.sanitized_vbms_id click_on "Search" @@ -657,7 +655,7 @@ def perform_search it "shows 'Withdrawn' text on search results page" do policy = instance_double(WithdrawnDecisionReviewPolicy) - allow(WithdrawnDecisionReviewPolicy).to receive(:new).and_return policy + allow(WithdrawnDecisionReviewPolicy).to receive(:new).with(caseflow_appeal).and_return policy allow(policy).to receive(:satisfied?).and_return true perform_search diff --git a/spec/fixes/assigned_to_search_results_spec.rb b/spec/fixes/assigned_to_search_results_spec.rb index 1ef26f380a1..75765fdddd3 100644 --- a/spec/fixes/assigned_to_search_results_spec.rb +++ b/spec/fixes/assigned_to_search_results_spec.rb @@ -29,7 +29,7 @@ end it "creates tasks and other records associated with a dispatched appeal" do - expect(BVAAppealStatus.new(tasks: appeal.tasks).status).to eq :unknown # We will fix this + expect(BVAAppealStatus.new(appeal: appeal).status).to eq :unknown # We will fix this expect(appeal.root_task.status).to eq "on_hold" visit "/search?veteran_ids=#{appeal.veteran.id}" @@ -57,7 +57,7 @@ } visit "/search?veteran_ids=#{appeal.veteran.id}" expect(page).to have_content("Signed") # in the "Appellant Name" column - expect(BVAAppealStatus.new(tasks: appeal.tasks).status).to eq :signed + expect(BVAAppealStatus.new(appeal: appeal).status).to eq :signed bva_dispatcher = org_dispatch_task.children.first.assigned_to expect(page).to have_content(bva_dispatcher.css_id) # in the "Assigned To" column expect(appeal.assigned_to_location).to eq bva_dispatcher.css_id @@ -65,7 +65,7 @@ BvaDispatchTask.outcode(appeal, params, bva_dispatcher) visit "/search?veteran_ids=#{appeal.veteran.id}" expect(page).to have_content("Dispatched") # in the "Appellant Name" column - expect(BVAAppealStatus.new(tasks: appeal.tasks).status).to eq :dispatched + expect(BVAAppealStatus.new(appeal: appeal).status).to eq :dispatched expect(page).to have_content("Post-decision") # in the "Assigned To" column expect(appeal.assigned_to_location).to eq "Post-decision" expect(appeal.root_task.status).to eq "completed" diff --git a/spec/fixes/backfill_early_ama_appeal_spec.rb b/spec/fixes/backfill_early_ama_appeal_spec.rb index e26da3bfe4d..e1ed3774706 100644 --- a/spec/fixes/backfill_early_ama_appeal_spec.rb +++ b/spec/fixes/backfill_early_ama_appeal_spec.rb @@ -28,7 +28,7 @@ let(:atty_draft_date) { dispatch_date - 2.hours } it "creates tasks and other records associated with a dispatched appeal" do - expect(BVAAppealStatus.new(tasks: appeal.tasks).status).to eq :unknown # We will fix this + expect(BVAAppealStatus.new(appeal: appeal).status).to eq :unknown # We will fix this expect(appeal.root_task.status).to eq "completed" # 1. Create tasks to associate appeal with a judge and attorney @@ -134,7 +134,7 @@ decision_doc = DecisionDocument.create!(params) expect(appeal.decision_document).to eq decision_doc - expect(BVAAppealStatus.new(tasks: appeal.tasks).status).to eq :dispatched + expect(BVAAppealStatus.new(appeal: appeal).status).to eq :dispatched end end end diff --git a/spec/models/serializers/work_queue/appeal_search_serializer_spec.rb b/spec/models/serializers/work_queue/appeal_search_serializer_spec.rb deleted file mode 100644 index 686d9e68074..00000000000 --- a/spec/models/serializers/work_queue/appeal_search_serializer_spec.rb +++ /dev/null @@ -1,183 +0,0 @@ -# frozen_string_literal: true - -require "rails_helper" - -describe WorkQueue::AppealSearchSerializer, :all_dbs do - describe "#assigned_to_location" do - context "when appeal status is distributed to judge" do - let!(:judge_user) { create(:user, :with_vacols_judge_record, full_name: "Judge Judy", css_id: "JUDGE_J") } - let(:appeal) { create(:appeal, :assigned_to_judge, associated_judge: judge_user) } - - before do - User.authenticate!(user: judge_user) - end - - subject { described_class.new(appeal, params: { user: judge_user }) } - - context "and user is a board judge" do - it "shows CSS ID" do - expect(subject.serializable_hash[:data][:attributes][:assigned_to_location]) - .to eq(appeal.assigned_to_location) - end - end - - context "when appeal status is assigned to attorney" do - let(:appeal) { create(:appeal, :at_attorney_drafting) } - let!(:attorney_user) { create(:user) } - let!(:vacols_atty) { create(:staff, :attorney_role, sdomainid: attorney_user.css_id) } - - before do - User.authenticate!(user: attorney_user) - end - - subject { described_class.new(appeal, params: { user: attorney_user }) } - - context "and user is a board attorney" do - it "shows CSS ID" do - expect(subject.serializable_hash[:data][:attributes][:assigned_to_location]) - .to eq(appeal.assigned_to_location) - end - end - end - - context "when appeal status is ready for signature" do - let!(:judge_user) { create(:user, :with_vacols_judge_record, full_name: "Judge Judy", css_id: "JUDGE_J") } - let(:appeal) { create(:appeal, :at_judge_review, associated_judge: judge_user) } - let!(:hearings_coordinator_user) do - coordinator = create(:hearings_coordinator) - HearingsManagement.singleton.add_user(coordinator) - coordinator - end - - before do - User.authenticate!(user: hearings_coordinator_user) - end - - subject { described_class.new(appeal, params: { user: hearings_coordinator_user }) } - - context "and user is a hearings coordinator" do - it "shows CSS ID" do - expect(subject.serializable_hash[:data][:attributes][:assigned_to_location]) - .to eq(appeal.assigned_to_location) - end - end - end - - context "when status is distributed to judge" do - let!(:judge_user) { create(:user, :with_vacols_judge_record, full_name: "Judge Judy", css_id: "JUDGE_J") } - let(:appeal) { create(:appeal, :assigned_to_judge, associated_judge: judge_user) } - let(:vso_user) { create(:user, :vso_role) } - - before do - User.authenticate!(user: vso_user) - end - - subject { described_class.new(appeal, params: { user: vso_user }) } - - it "does not show CSS ID to VSO user" do - expect(subject.serializable_hash[:data][:attributes][:assigned_to_location]).to be_nil - end - end - - context "when status is ready for signature" do - let!(:judge_user) { create(:user, :with_vacols_judge_record, full_name: "Judge Judy", css_id: "JUDGE_J") } - let(:appeal) { create(:appeal, :at_judge_review, associated_judge: judge_user) } - let(:vso_user) { create(:user, :vso_role) } - - before do - User.authenticate!(user: vso_user) - end - - subject { described_class.new(appeal, params: { user: vso_user }) } - - it "does not show CSS ID to VSO user" do - expect(subject.serializable_hash[:data][:attributes][:assigned_to_location]).to be_nil - end - end - - context "when status is on hold" do - let(:appeal) do - create(:appeal).tap do |appeal| - create(:timed_hold_task, parent: create(:root_task, appeal: appeal)) - end - end - let(:vso_user) { create(:user, :vso_role) } - - before do - User.authenticate!(user: vso_user) - end - - subject { described_class.new(appeal, params: { user: vso_user }) } - - it "does not show CSS ID to VSO user" do - expect(subject.serializable_hash[:data][:attributes][:assigned_to_location]).to be_nil - end - end - - context "when status is misc" do - let(:appeal) do - create(:appeal).tap do |appeal| - create(:ama_judge_dispatch_return_task, parent: create(:root_task, appeal: appeal)) - end - end - let(:vso_user) { create(:user, :vso_role) } - - before do - User.authenticate!(user: vso_user) - end - - subject { described_class.new(appeal, params: { user: vso_user }) } - - it "does not show CSS ID to VSO user" do - expect(subject.serializable_hash[:data][:attributes][:assigned_to_location]).to be_nil - end - end - - context "when status is unknown" do - let(:appeal) { create(:appeal) } - let(:vso_user) { create(:user, :vso_role) } - - before do - User.authenticate!(user: vso_user) - end - - subject { described_class.new(appeal, params: { user: vso_user }) } - - it "does not show CSS ID to VSO user" do - expect(subject.serializable_hash[:data][:attributes][:assigned_to_location]).to be_nil - end - end - - context "when status is assigned to attorney" do - let(:appeal) { create(:appeal, :at_attorney_drafting) } - let(:vso_user) { create(:user, :vso_role) } - - before do - User.authenticate!(user: vso_user) - end - - subject { described_class.new(appeal, params: { user: vso_user }) } - - it "does not show CSS ID to VSO user" do - expect(subject.serializable_hash[:data][:attributes][:assigned_to_location]).to be_nil - end - end - end - - context "when appeal status is not restricted" do - let(:appeal) { create(:appeal, :with_pre_docket_task) } - let(:vso_user) { create(:user, :vso_role) } - - before do - User.authenticate!(user: vso_user) - end - - subject { described_class.new(appeal, params: { user: vso_user }) } - - it "shows CSS ID to VSO user" do - expect(subject.serializable_hash[:data][:attributes][:assigned_to_location]) - .to eq(appeal.assigned_to_location) - end - end - end -end diff --git a/spec/services/bva_appeal_status_spec.rb b/spec/services/bva_appeal_status_spec.rb index 8effec2eb4a..5bdb70c2929 100644 --- a/spec/services/bva_appeal_status_spec.rb +++ b/spec/services/bva_appeal_status_spec.rb @@ -18,7 +18,7 @@ status = pair.first sort_key = pair.last appeal = Appeal.find(appeal_id) - appeal_status = described_class.new(tasks: appeal.tasks) + appeal_status = described_class.new(appeal: appeal) expect(appeal_status.to_s).to eq(status) expect(appeal_status.to_i).to eq(sort_key.to_i + 1) # our sort keys are 1-based diff --git a/spec/services/search_query_service_spec.rb b/spec/services/search_query_service_spec.rb deleted file mode 100644 index 66fcf4ec756..00000000000 --- a/spec/services/search_query_service_spec.rb +++ /dev/null @@ -1,311 +0,0 @@ -# frozen_string_literal: true - -describe "SearchQueryService" do - let(:ssn) { "146600001" } - let(:dob) { Faker::Date.in_date_period(year: 1960) } - - let(:uuid) { SecureRandom.uuid } - let(:veteran_first_name) { Faker::Name.first_name } - let(:veteran_last_name) { Faker::Name.last_name } - let(:claimant_first_name) { Faker::Name.first_name } - let(:claimant_last_name) { Faker::Name.last_name } - let(:veteran_full_name) { FullName.new(veteran_first_name, "", veteran_last_name).to_s } - let(:claimant_full_name) { FullName.new(claimant_first_name, "", claimant_last_name).to_s } - let(:docket_type) { "hearing" } - let(:docket_number) { "240111-1111" } - - let(:descision_document_attrs) do - { - decision_date: Faker::Date.between(from: 2.years.ago, to: 1.year.ago) - } - end - - context "all data in caseflow" do - context "veteran is claimant" do - let(:veteran_attrs) do - { - ssn: ssn, - file_number: ssn, - date_of_birth: dob, - date_of_death: nil, - first_name: veteran_first_name, - middle_name: nil, - last_name: veteran_last_name - } - end - - let(:veteran) { FactoryBot.create(:veteran, veteran_attrs) } - - let(:appeal_attributes) do - { - aod_based_on_age: false, - stream_docket_number: docket_number, - veteran_file_number: ssn, - veteran: veteran, - stream_type: Constants.AMA_STREAM_TYPES.original, - uuid: uuid - } - end - - let(:judge) { create(:user, :judge) } - - let!(:appeal) do - FactoryBot.create( - :appeal, - # has hearing(s) - :hearing_docket, - :held_hearing, - :tied_to_judge, - # has decision document - :dispatched, - # has issue(s) - :with_request_issues, - :with_decision_issue, - { - associated_judge: judge, - tied_judge: judge - }.merge(appeal_attributes) - ).tap do |appeal| - appeal.decision_issues.first.update( - mst_status: true, - pact_status: true - ) - appeal.hearings.first.update(updated_by: judge) - # create work mode - appeal.overtime = true - AdvanceOnDocketMotion.create( - person: appeal.claimants.first.person, - appeal: appeal - ) - end.reload - end - - context "finds by docket number" do - subject { SearchQueryService.new(docket_number: appeal.stream_docket_number) } - - it "finds by docket number" do - expect(appeal).to be_persisted - - search_results = subject.search_by_docket_number - - expect(search_results.length).to eq(1) - - result = search_results.first.api_response - - expect(result.id).to be - expect(result.type).to eq "appeal" - - attributes = result.attributes - - expect(attributes.aod).to be_truthy - expect(attributes.appellant_full_name).to eq veteran_full_name - expect(attributes.assigned_to_location).to eq appeal.assigned_to_location - expect(attributes.caseflow_veteran_id).to eq veteran.id - expect(attributes.decision_date).to eq appeal.decision_document.decision_date - expect(attributes.docket_name).to eq appeal.docket_type - expect(attributes.docket_number).to eq appeal.stream_docket_number - expect(attributes.external_id).to eq appeal.uuid - expect(attributes.hearings.length).to eq appeal.hearings.length - expect(attributes.issues.length).to eq(appeal.request_issues.length) - expect(attributes.mst).to eq appeal.decision_issues.any?(&:mst_status) - expect(attributes.pact).to eq appeal.decision_issues.any?(&:pact_status) - expect(attributes.paper_case).to be_falsy - expect(attributes.status).to eq Appeal.find(appeal.id).status.status - expect(attributes.veteran_appellant_deceased).to be_falsy - expect(attributes.veteran_file_number).to eq ssn - expect(attributes.veteran_full_name).to eq veteran_full_name - expect(attributes.contested_claim).to be_falsy - expect(attributes.withdrawn).to eq(false) - end - end - - context "finds by file number" do - subject { SearchQueryService.new(file_number: ssn) } - - it "finds by veteran file number" do - expect(appeal).to be_persisted - - search_results = subject.search_by_veteran_file_number - - expect(search_results.length).to eq(1) - - result = search_results.first.api_response - - expect(result.id).to be - expect(result.type).to eq "appeal" - end - end - - context "finds by veteran ids" do - subject { SearchQueryService.new(veteran_ids: [veteran.id]) } - - it "finds by veteran ids" do - expect(appeal).to be_persisted - - search_results = subject.search_by_veteran_ids - - expect(search_results.length).to eq(1) - - result = search_results.first.api_response - - expect(result.id).to be - expect(result.type).to eq "appeal" - end - end - end - end - - let(:veteran_address) do - { - addrs_one_txt: nil, - addrs_two_txt: nil, - addrs_three_txt: nil, - city_nm: nil, - cntry_nm: nil, - postal_cd: nil, - zip_prefix_nbr: nil, - ptcpnt_addrs_type_nm: nil - } - end - - let(:legacy_appeal) do - create( - :legacy_appeal, - vbms_id: ssn, - vacols_case: vacols_case, - veteran_address: veteran_address - ) - end - - # must be created first for legacy_appeal factory to find it - let!(:veteran) do - create( - :veteran, - file_number: ssn, - first_name: veteran_first_name, - last_name: veteran_last_name - ) - end - - let(:vacols_decision_date) { 2.weeks.ago } - let(:vacols_case_attrs) do - { - bfkey: ssn, - bfcorkey: ssn, - bfac: "1", - bfcorlid: "100000099", - bfcurloc: "CASEFLOW", - bfddec: vacols_decision_date, - bfmpro: "ACT" - - # bfregoff: "RO18", - # bfdloout: "2024-03-26T11:13:32.000Z", - # bfcallup: "", - # bfhr: "2", - # bfdocind: "T", - } - end - - let(:issues_count) { 5 } - let(:vacols_case_issues) do - create_list( - :case_issue, - issues_count, - isspact: "Y", - issmst: "Y" - ) - end - - let(:hearings_count) { 5 } - let(:vacols_case_hearings) do - create_list( - :case_hearing, - hearings_count - ) - end - - let(:vacols_correspondent) do - create(:correspondent, vacols_correspondent_attrs) - end - - let(:vacols_folder) do - build(:folder) - end - - let(:vacols_case) do - create( - :case, - { - correspondent: vacols_correspondent, - case_issues: vacols_case_issues, - case_hearings: vacols_case_hearings, - folder: vacols_folder - }.merge(vacols_case_attrs) - ) - end - - context "when appeal is a legacy appeal with data in vacols and caseflow" do - context "when veteran is claimant" do - let(:vacols_correspondent_attrs) do - { - sspare2: veteran_first_name, - sspare1: veteran_last_name, - snamel: veteran_last_name, - snamef: veteran_first_name, - stafkey: ssn - } - end - - let!(:claimant) do - create( - :claimant, - type: "VeteranClaimant", - decision_review: legacy_appeal - ) - end - - subject { SearchQueryService.new(file_number: ssn) } - - it "finds by file number" do - search_results = subject.search_by_veteran_file_number - result = search_results.first.api_response - - expect(result.id).to be - expect(result.type).to eq "legacy_appeal" - - attributes = result.attributes - expect(attributes.docket_name).to eq "legacy" - expect(attributes.aod).to be_falsy - expect(attributes.appellant_full_name).to eq veteran_full_name - expect(attributes.assigned_to_location).to eq legacy_appeal.assigned_to_location - expect(attributes.caseflow_veteran_id).to eq veteran.id - expect(attributes.decision_date).to eq AppealRepository.normalize_vacols_date(vacols_decision_date) - expect(attributes.docket_name).to eq "legacy" - expect(attributes.docket_number).to eq vacols_folder.tinum - expect(attributes.external_id).to eq vacols_case.id - expect(attributes.hearings.length).to eq hearings_count - expect(attributes.issues.length).to eq issues_count - expect(attributes.mst).to be_truthy - expect(attributes.pact).to be_truthy - expect(attributes.paper_case).to eq "Paper" - expect(attributes.status).to eq "Active" - expect(attributes.veteran_appellant_deceased).to be_falsy - expect(attributes.veteran_file_number).to eq ssn - expect(attributes.veteran_full_name).to eq veteran_full_name - expect(attributes.withdrawn).to be_falsy - end - - context "finds by veteran ids" do - subject { SearchQueryService.new(veteran_ids: [veteran.id]) } - - it "finds by veteran ids" do - search_results = subject.search_by_veteran_ids - result = search_results.first.api_response - - expect(result.id).to be - expect(result.type).to eq "legacy_appeal" - end - end - end - end -end From bb83d03ea046fca6cce83be02b14da095a55ce39 Mon Sep 17 00:00:00 2001 From: ryanpmessner <163380175+ryanpmessner@users.noreply.github.com> Date: Tue, 17 Sep 2024 17:04:32 -0400 Subject: [PATCH 9/9] APPEALS-50829: (#22868) Add restricted_statuses solution and initial RSpec test file Fix positioning of restricted_statuses array to under set_type Make modification that only prevents non-board users from viewing the assigned_to_location CSS IDs Only prevent vso employees/Private attorneys from viewing CSS IDs in restricted status state Refactor comment to better explain what is happening, and add proper staging to RSpec file Make restricted statuses array immutable Add more extensive test coverage (every restricted status type) Co-authored-by: AimanK --- .../work_queue/appeal_search_serializer.rb | 15 +- .../appeal_search_serializer_spec.rb | 183 ++++++++++++++++++ 2 files changed, 196 insertions(+), 2 deletions(-) create mode 100644 spec/models/serializers/work_queue/appeal_search_serializer_spec.rb diff --git a/app/models/serializers/work_queue/appeal_search_serializer.rb b/app/models/serializers/work_queue/appeal_search_serializer.rb index 90be5a6f498..320c5bdb075 100644 --- a/app/models/serializers/work_queue/appeal_search_serializer.rb +++ b/app/models/serializers/work_queue/appeal_search_serializer.rb @@ -6,6 +6,16 @@ class WorkQueue::AppealSearchSerializer set_type :appeal + RESTRICTED_STATUSES = + [ + :distributed_to_judge, + :ready_for_signature, + :on_hold, + :misc, + :unknown, + :assigned_to_attorney + ].freeze + attribute :contested_claim, &:contested_claim? attribute :mst, &:mst? @@ -73,10 +83,11 @@ class WorkQueue::AppealSearchSerializer attribute :veteran_appellant_deceased, &:veteran_appellant_deceased? attribute :assigned_to_location do |object, params| - if object&.status&.status == :distributed_to_judge - if params[:user]&.judge? || params[:user]&.attorney? || User.list_hearing_coordinators.include?(params[:user]) + if RESTRICTED_STATUSES.include?(object&.status&.status) + unless params[:user]&.vso_employee? object.assigned_to_location end + # if not in a restricted status, show assigned location to all users else object.assigned_to_location end diff --git a/spec/models/serializers/work_queue/appeal_search_serializer_spec.rb b/spec/models/serializers/work_queue/appeal_search_serializer_spec.rb new file mode 100644 index 00000000000..686d9e68074 --- /dev/null +++ b/spec/models/serializers/work_queue/appeal_search_serializer_spec.rb @@ -0,0 +1,183 @@ +# frozen_string_literal: true + +require "rails_helper" + +describe WorkQueue::AppealSearchSerializer, :all_dbs do + describe "#assigned_to_location" do + context "when appeal status is distributed to judge" do + let!(:judge_user) { create(:user, :with_vacols_judge_record, full_name: "Judge Judy", css_id: "JUDGE_J") } + let(:appeal) { create(:appeal, :assigned_to_judge, associated_judge: judge_user) } + + before do + User.authenticate!(user: judge_user) + end + + subject { described_class.new(appeal, params: { user: judge_user }) } + + context "and user is a board judge" do + it "shows CSS ID" do + expect(subject.serializable_hash[:data][:attributes][:assigned_to_location]) + .to eq(appeal.assigned_to_location) + end + end + + context "when appeal status is assigned to attorney" do + let(:appeal) { create(:appeal, :at_attorney_drafting) } + let!(:attorney_user) { create(:user) } + let!(:vacols_atty) { create(:staff, :attorney_role, sdomainid: attorney_user.css_id) } + + before do + User.authenticate!(user: attorney_user) + end + + subject { described_class.new(appeal, params: { user: attorney_user }) } + + context "and user is a board attorney" do + it "shows CSS ID" do + expect(subject.serializable_hash[:data][:attributes][:assigned_to_location]) + .to eq(appeal.assigned_to_location) + end + end + end + + context "when appeal status is ready for signature" do + let!(:judge_user) { create(:user, :with_vacols_judge_record, full_name: "Judge Judy", css_id: "JUDGE_J") } + let(:appeal) { create(:appeal, :at_judge_review, associated_judge: judge_user) } + let!(:hearings_coordinator_user) do + coordinator = create(:hearings_coordinator) + HearingsManagement.singleton.add_user(coordinator) + coordinator + end + + before do + User.authenticate!(user: hearings_coordinator_user) + end + + subject { described_class.new(appeal, params: { user: hearings_coordinator_user }) } + + context "and user is a hearings coordinator" do + it "shows CSS ID" do + expect(subject.serializable_hash[:data][:attributes][:assigned_to_location]) + .to eq(appeal.assigned_to_location) + end + end + end + + context "when status is distributed to judge" do + let!(:judge_user) { create(:user, :with_vacols_judge_record, full_name: "Judge Judy", css_id: "JUDGE_J") } + let(:appeal) { create(:appeal, :assigned_to_judge, associated_judge: judge_user) } + let(:vso_user) { create(:user, :vso_role) } + + before do + User.authenticate!(user: vso_user) + end + + subject { described_class.new(appeal, params: { user: vso_user }) } + + it "does not show CSS ID to VSO user" do + expect(subject.serializable_hash[:data][:attributes][:assigned_to_location]).to be_nil + end + end + + context "when status is ready for signature" do + let!(:judge_user) { create(:user, :with_vacols_judge_record, full_name: "Judge Judy", css_id: "JUDGE_J") } + let(:appeal) { create(:appeal, :at_judge_review, associated_judge: judge_user) } + let(:vso_user) { create(:user, :vso_role) } + + before do + User.authenticate!(user: vso_user) + end + + subject { described_class.new(appeal, params: { user: vso_user }) } + + it "does not show CSS ID to VSO user" do + expect(subject.serializable_hash[:data][:attributes][:assigned_to_location]).to be_nil + end + end + + context "when status is on hold" do + let(:appeal) do + create(:appeal).tap do |appeal| + create(:timed_hold_task, parent: create(:root_task, appeal: appeal)) + end + end + let(:vso_user) { create(:user, :vso_role) } + + before do + User.authenticate!(user: vso_user) + end + + subject { described_class.new(appeal, params: { user: vso_user }) } + + it "does not show CSS ID to VSO user" do + expect(subject.serializable_hash[:data][:attributes][:assigned_to_location]).to be_nil + end + end + + context "when status is misc" do + let(:appeal) do + create(:appeal).tap do |appeal| + create(:ama_judge_dispatch_return_task, parent: create(:root_task, appeal: appeal)) + end + end + let(:vso_user) { create(:user, :vso_role) } + + before do + User.authenticate!(user: vso_user) + end + + subject { described_class.new(appeal, params: { user: vso_user }) } + + it "does not show CSS ID to VSO user" do + expect(subject.serializable_hash[:data][:attributes][:assigned_to_location]).to be_nil + end + end + + context "when status is unknown" do + let(:appeal) { create(:appeal) } + let(:vso_user) { create(:user, :vso_role) } + + before do + User.authenticate!(user: vso_user) + end + + subject { described_class.new(appeal, params: { user: vso_user }) } + + it "does not show CSS ID to VSO user" do + expect(subject.serializable_hash[:data][:attributes][:assigned_to_location]).to be_nil + end + end + + context "when status is assigned to attorney" do + let(:appeal) { create(:appeal, :at_attorney_drafting) } + let(:vso_user) { create(:user, :vso_role) } + + before do + User.authenticate!(user: vso_user) + end + + subject { described_class.new(appeal, params: { user: vso_user }) } + + it "does not show CSS ID to VSO user" do + expect(subject.serializable_hash[:data][:attributes][:assigned_to_location]).to be_nil + end + end + end + + context "when appeal status is not restricted" do + let(:appeal) { create(:appeal, :with_pre_docket_task) } + let(:vso_user) { create(:user, :vso_role) } + + before do + User.authenticate!(user: vso_user) + end + + subject { described_class.new(appeal, params: { user: vso_user }) } + + it "shows CSS ID to VSO user" do + expect(subject.serializable_hash[:data][:attributes][:assigned_to_location]) + .to eq(appeal.assigned_to_location) + end + end + end +end