From ba45953e7e12fae18cf56138cd9ed284c9e635c2 Mon Sep 17 00:00:00 2001 From: Adam Ducker Date: Mon, 20 May 2024 12:47:18 -0500 Subject: [PATCH] Merging lastest changes from outside APPEALS-31792 (#21680) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * added saftey opperator and removed outdated date check * removed leftover method that was breaking and unused. * removed legacy appeal date check for MST on special issue list * updated mst and pact badges to check decisions in flight (if any) then appeal model. * removed bypass for special issue list for pact toggle for AMA. * updated rake and seed comments * allowed special issues page tracking for AMA MST issues when pact is enabled * remove code changes causing cancel to progress forward * Update scheduled jobs key and move job to different folder * Update job to set user manually and pass in updated_by * moved method definition validate_claim_code into ClaimLabelChange class * fixed spacing issue * msteele/APPEALS-45182 Fix transcription_files relationships on Hearings/LegacyHearings (#21492) * APPEALS-45182 Reinforce has_many relationships * APPEALS-45182 Add missing comma * Resolve lint issue --------- Co-authored-by: Matthew Thornton * fixed linting error * fixed linting issue * draft pr * Hotfix/APPEALS-45218 (#21503) * APPEALS-45218 reverted webex config arguments * APPEALS-45218 reverted webex config arguments * APPEALS-25218 Update WebexService to remove config in favor of individual arguments * APPEALS-45218 Format comment --------- Co-authored-by: msteele * Wmedders21/appeals 44873 (#21469) * add rubyzip gem * Test: Hearings::ZipAndUploadTranscriptionFilesJob * Feature: Hearings::ZipAndUploadTranscriptionFilesJob * Add zip subfolder to tmp/transcription_files * Move new gem to proper alphabetical position in list * revert gem file changes * update gem file with feature branch * refactor #perform method parameters * update parameter name * Piedram/appeals 44897 (#21519) * Create new Workflow transcription_packages: Step One * add some changes * Modified the parameters for ZipAndUploadTranscriptionFilesJob * Modified spec test * Update transcription_packages.rb * remove extra blank line --------- Co-authored-by: piedram * Wmedders21/appeals 44874 (#21512) * add rubyzip gem * Test: Hearings::ZipAndUploadTranscriptionFilesJob * Feature: Hearings::ZipAndUploadTranscriptionFilesJob * Add zip subfolder to tmp/transcription_files * Move new gem to proper alphabetical position in list * revert gem file changes * update gem file with feature branch * refactor #perform method parameters * update parameter name * add zip extension to valid file types * Test: transcription file creation and upload to s3 * Hearings::ZipAndUploadTranscriptionFilesJob updates db and uploads to s3 * resolve merge conflict * APPEALS-44876 Work order job file (#21504) * APPEALS-44876 Work order job file * Updated filename with work order name * Refactored create table method and updated spec --------- Co-authored-by: Jim Foley * Completing AC for ticketing for styling and error portion of page. Now doing feature test * B_reed/hotfix_APPEALS-45285 (#21531) * APPEALS-45285 Bug Fix * APPEALS-45285 update pexip_service_spec * APPEALS-45285 remove white space * Hotfix/APPEALS-45218-v2 (#21522) * APPEALS-45218 reverted webex config arguments * APPEALS-45218 reverted webex config arguments * APPEALS-45218 added query argument to webex conference link * APPEALS-45218: webex recordings endpoint correction * APPEALS-45218 adjusted arguments during inititalizing of refresh tokens * APPEALS-45218 fixed rspec * APPEALS-45218: added error catching on webex service response * APPEALS-45218: remove tested code * APPEALS-45218 Go back to leveraging HTTPI response inherited methods --------- Co-authored-by: Ariana Konhilas Co-authored-by: msteele * added ability for user to search by css id * added feature tests for using search bar * reverted changes in package.json * removed added file * removed linting error * msteele/APPEALS-45349 (#21539) * APPEALS-45349 Check for error on HTTPI object, then create Webex response object and fail with specific error * APPEALS-45349 Flip conditionals * APPEALS-45349 Fix linting errors * searchable by 2 characters instead of 1 * hotfix/APPEALS-45399-45401-45472 (#21555) * APPEALS-45399-45401 Update fetch webex list and details to retrieve apikey from cred stash * APPEALS-45399-45401 Remove CGI escape from fetch webex list job * APPEALS-445399-45401 Update spec files for webex list and details job * APPEALS-45399-45401 Include error handling for transcription issue mailer failed delivery * Revert "APPEALS-45399-45401 Include error handling for transcription issue mailer failed delivery" This reverts commit d9ce585c59444f9793e681e58060d71872057e4c. * ccc * gggg * gggg * Create new Workflow transcription_packages: Step Two (#21570) * Add call function to the new job, and modified spec files * call the jobs * feature/APPEALS-44188-44282 - Deprecation fixes for Rails 6.1 (release) (#21614) * 🔀 Squash merge jcroteau/APPEALS-44188-fix-deprecation-warning-class-level-methods-will-no-longer-inherit-scoping * 🔀 Squash merge jcroteau/APPEALS-44282-fix-deprecation-not-conditions-will-no-longer-behave-as-nor * Work order file upload to AWS s3 (#21582) * Work order file upload to AWS s3 * refactor code for upload to AWS * refactor code for instance variables * Revert "refactor code for instance variables" This reverts commit b61c72a3f2e30e4003e9d4a4dc57a01dbb105443. * code refactor for work order file * APPEALS-44877 refactor for work order * APPEALS-44310: add notification for unexpected task trees and update tests (#21474) * add notification for unexpected task trees and update tests * add slack notification to distribution errors, set default slack url in SlackService * add tests for distributions to validate handling of errors in the ama_statistics method * fix bad check for allowable tasks * fix tests which were expecting a url as arg to new slack service * fix codeclimate warning * APPEALS-45199: Update Quality Review selection probabilities #21518 * APPEALS-44423: Remove therubyracer and jshint gems (#21443) * APPEALS-42941 copied over Matt's work (#21624) * APPEALS-43190 (#21448) * APPEALS-43190 initial migration file * APPEALS-43190 add seed data * APPEALS-43190 add magic comment * APPEALS-43190 move seed command * APPEALS-43190 add bang! * APPEALS-43190 update migration file * APPEALS-43190 attribute name fix and updates * APPEALS-43190 fix seed attribute * APPEALS-43190 stubbed more seed data * APPEALS-43190 remove seed comment * APPEALS-43190 remove comment * APPEALS-43190 add magic comment * APPEALS-43190 update comments * Wmedders21/appeals 44875 (#21605) * Add retry logic for upload failure * fix indentation rubocop warning * Refactor zip job * remove blank line --------- Co-authored-by: Jim Foley * akonhilas/APPEALS-43193 (#21581) * APPEALS-43193: Add new columns and indexes to transcriptions table * APPEALS-43193: Add new columns and indexes to transcriptions table * APPEALS-43193: move index to own file * APPEALS-43193: Update transcription contractor id to bigint * APPEALS-43193: bigint * APPEALS-43193: add fk after trancription_contractors table added * lint error resolution (#21631) * hotfix/APPEALS-45472 (#21562) * APPEALS-45472 Refactor error handling for transcription * APPEALS-45472 Include missing provider in error details for fetch recording list job and update comment for mailer * Hotfix/APPEALS-45818 (#21632) * APPEALS-45818: parsing fix, remove topic param, fix response * APPEALS-45818: revert list and details job back to original state, update tests --------- Co-authored-by: Marc Steele <71673522+msteele96@users.noreply.github.com> * APPEALS-44878 refactor for retry and upon success kickoff zip file job (#21652) * APPEALS-44878 refactor for retry and upon success kickoff zip file job * APPEALS-44878 refactor code * APPEALS-44878 refactor xls path * lint error fix * msteele/APPEALS-45285-v2 Fix delete_conference in PexipService (#21634) * APPEALS-45285 update ExternalApi::PexipService#delete_conference to accept virtual_hearing as the only argument * APPEALS-45285 Update spec to use virtual_hearing * APPEALS-45285 Linting fixes * Piedram/appeals 45696 (#21655) * Create new Workflow Error handling transcription_packages: Step Three related to appeals-31793 * MOdified spec file * fix lint error * APPEALS-45696 refactor for work_order_params --------- Co-authored-by: Kamala Madamanchi Co-authored-by: Jim Foley * hotfix/APPEALS-45828 (#21625) * APPEALS-45828 Include call to create webex conference links in base hearing update form * APPEALS-45828 Update details feature spec to expect webex link creation on conversion from virtual * Fix needing to reload to display co host link on details page * Fix incorrect method name * Remove temporary rendering of default pexip link for webex hearings after conversion * APPEALS-45828 Fix hearing mailer spec context issues and null check for BAD_VIRTUAL_LINK_TEXT * APPEALS-45828 Remove file name argument from fetch details job spec * APPEALS-45828 Fail on bad hearing link if link nil or legacy link * APPEALS-45828 Sanitize url string * APPEALS-45828 Undo url sanitize * Remove unused styles after merge * Fix lint warnings --------- Co-authored-by: HunJerBAH Co-authored-by: HunJerBAH <99915461+HunJerBAH@users.noreply.github.com> Co-authored-by: msteele Co-authored-by: Andrew Hadley Co-authored-by: Marc Steele <71673522+msteele96@users.noreply.github.com> Co-authored-by: Matthew Thornton Co-authored-by: IsaiahBar Co-authored-by: minhazur9 <65432922+minhazur9@users.noreply.github.com> Co-authored-by: Will Medders <93014155+wmedders21@users.noreply.github.com> Co-authored-by: Jim Foley Co-authored-by: piedram Co-authored-by: Kamala Madamanchi <110078646+kamala-07@users.noreply.github.com> Co-authored-by: breedbah <123968373+breedbah@users.noreply.github.com> Co-authored-by: Ariana Konhilas Co-authored-by: raymond-hughes Co-authored-by: Jeff Marks <106996298+jefftmarks@users.noreply.github.com> Co-authored-by: piedram <110848569+piedram@users.noreply.github.com> Co-authored-by: Jeremy Croteau Co-authored-by: Craig Reese <109101548+craigrva@users.noreply.github.com> Co-authored-by: cacevesva <109166981+cacevesva@users.noreply.github.com> Co-authored-by: Amy Detwiler <133032208+amybids@users.noreply.github.com> Co-authored-by: Ariana Konhilas <109693628+konhilas-ariana@users.noreply.github.com> Co-authored-by: mikefinneran <110622959+mikefinneran@users.noreply.github.com> Co-authored-by: Kamala Madamanchi --- Gemfile | 6 +- Gemfile.lock | 18 +-- app/controllers/hearings_controller.rb | 1 + app/jobs/caseflow_job.rb | 6 +- .../create_non_virtual_conference_job.rb | 11 +- .../download_transcription_file_job.rb | 24 +-- .../fetch_webex_recordings_details_job.rb | 23 +-- .../fetch_webex_recordings_list_job.rb | 18 +-- .../refresh_webex_access_token_job.rb | 8 +- .../send_transcription_issues_email.rb | 10 ++ app/jobs/hearings/work_order_file_job.rb | 138 ++++++++++++++++++ .../zip_and_upload_transcription_files_job.rb | 102 +++++++++++++ app/jobs/out_of_service_reminder_job.rb | 6 +- app/jobs/prepare_establish_claim_tasks_job.rb | 6 +- app/jobs/quarterly_notifications_job.rb | 5 +- .../virtual_hearings/conference_client.rb | 70 +++++---- app/mailers/hearing_mailer.rb | 2 +- .../transcription_file_issues_mailer.rb | 25 +++- app/mailers/work_order_file_issues_mailer.rb | 30 ++++ app/models/appeal.rb | 4 +- app/models/bgs_power_of_attorney.rb | 2 +- app/models/concerns/distribution_concern.rb | 50 ++++--- app/models/dispatch/task.rb | 2 +- app/models/distribution.rb | 3 + app/models/hearing.rb | 2 +- .../forms/base_hearing_update_form.rb | 14 +- app/models/hearings/transcription_file.rb | 2 +- app/models/hearings/virtual_hearing.rb | 16 +- app/models/hearings/webex_conference_link.rb | 9 +- app/models/judge_case_review.rb | 2 +- app/models/legacy_appeal.rb | 5 +- app/models/legacy_hearing.rb | 2 +- app/models/quality_review_case_selector.rb | 2 +- .../tasks/judge_decision_review_task.rb | 4 +- .../tasks/judge_dispatch_return_task.rb | 4 +- app/models/team_quota.rb | 2 +- app/models/vbms_uploaded_document.rb | 5 +- ...n_status_with_completed_root_task_query.rb | 3 +- ...nknown_status_with_open_root_task_query.rb | 3 +- app/repositories/task_action_repository.rb | 4 +- .../disallowed_deprecations.rb | 3 +- .../production_handler.rb | 4 +- app/services/etl/syncer.rb | 7 +- app/services/external_api/pexip_service.rb | 6 +- .../external_api/pexip_service/response.rb | 6 + app/services/external_api/webex_service.rb | 58 +++++--- .../recording_details_response.rb | 2 +- .../webex_service/recordings_list_response.rb | 6 +- .../external_api/webex_service/response.rb | 2 +- app/services/slack_service.rb | 2 +- .../work_order_file_issues_mailer.html.erb | 16 ++ app/workflows/transcription_file_upload.rb | 3 +- app/workflows/transcription_packages.rb | 35 +++++ .../reducers/levers/leversActions.js | 19 ++- .../reducers/levers/leversSelector.js | 14 +- .../components/badges/MstBadge/MstBadge.jsx | 11 +- .../components/badges/PactBadge/PactBadge.jsx | 11 +- client/app/queue/OrganizationUsers.jsx | 112 ++++++++++---- client/app/queue/SelectDispositionsView.jsx | 1 - client/app/queue/components/QueueFlowPage.jsx | 6 +- client/app/styles/_team_management.scss | 35 +++++ .../reducers/levers/LeversReducer.test.js | 10 +- .../reducers/levers/leversActions.test.js | 20 +-- .../create_transcription_files_folder.rb | 2 +- config/initializers/scheduled_jobs.rb | 2 +- ...507145931_add_columns_to_transcriptions.rb | 13 ++ ...507203310_add_indexes_to_transcriptions.rb | 6 + db/schema.rb | 48 ++++++ db/seeds/mst_pact_legacy_case_appeals.rb | 1 + lib/caseflow/error.rb | 1 + lib/fakes/webex_service.rb | 4 +- lib/helpers/claim_label_change.rb | 10 +- .../additional_legacy_remanded_appeals.rake | 1 - lib/tasks/seed_legacy_appeals.rake | 5 - spec/feature/hearings/hearing_details_spec.rb | 13 +- spec/feature/queue/user_organization_spec.rb | 23 ++- .../batch_process_rescue_job_spec.rb | 2 +- ...priority_ep_sync_batch_process_job_spec.rb | 2 +- .../download_transcription_file_job_spec.rb | 30 ++-- ...fetch_webex_recordings_details_job_spec.rb | 48 +++++- .../fetch_webex_recordings_list_job_spec.rb | 77 ++++++++-- .../refresh_webex_access_token_job_spec.rb | 2 +- .../jobs/hearings/work_order_file_job_spec.rb | 95 ++++++++++++ ...and_upload_transcription_files_job_spec.rb | 91 ++++++++++++ spec/mailers/hearing_mailer_spec.rb | 13 +- .../by_docket_date_distribution_spec.rb | 22 +++ .../concerns/distribution_concern_spec.rb | 69 ++++----- spec/models/distribution_spec.rb | 3 +- .../transcription_contractors_model_spec.rb | 55 ------- .../quality_review_case_selector_spec.rb | 19 ++- .../production_handler_spec.rb | 2 +- .../external_api/pexip_service_spec.rb | 2 +- .../external_api/webex_service_spec.rb | 18 ++- spec/workflows/transcription_packages_spec.rb | 42 ++++++ 94 files changed, 1315 insertions(+), 444 deletions(-) rename app/jobs/{virtual_hearings => hearings}/refresh_webex_access_token_job.rb (89%) create mode 100644 app/jobs/hearings/send_transcription_issues_email.rb create mode 100644 app/jobs/hearings/work_order_file_job.rb create mode 100644 app/jobs/hearings/zip_and_upload_transcription_files_job.rb create mode 100644 app/mailers/work_order_file_issues_mailer.rb create mode 100644 app/views/layouts/work_order_file_issues_mailer.html.erb create mode 100644 app/workflows/transcription_packages.rb create mode 100644 db/migrate/20240507145931_add_columns_to_transcriptions.rb create mode 100644 db/migrate/20240507203310_add_indexes_to_transcriptions.rb rename spec/jobs/{virtual_hearings => hearings}/refresh_webex_access_token_job_spec.rb (94%) create mode 100644 spec/jobs/hearings/work_order_file_job_spec.rb create mode 100644 spec/jobs/hearings/zip_and_upload_transcription_files_job_spec.rb delete mode 100644 spec/models/hearings/transcription_contractors_model_spec.rb create mode 100644 spec/workflows/transcription_packages_spec.rb diff --git a/Gemfile b/Gemfile index 177e8a21bd7..928c750c4a2 100644 --- a/Gemfile +++ b/Gemfile @@ -72,12 +72,11 @@ gem "sass-rails", "~> 5.0" # Error reporting to Sentry gem "sentry-raven" gem "shoryuken", "3.1.11" +gem "spreadsheet", "~> 1.3" gem "statsd-instrument" gem "stringex", require: false # catch problematic migrations at development/test time gem "strong_migrations" -# execjs runtime -gem "therubyracer", platforms: :ruby # print trees gem "tty-tree" gem "tzinfo", "1.2.10" @@ -108,7 +107,6 @@ group :test, :development, :demo do gem "guard-rspec" gem "immigrant" # Linters - gem "jshint", platforms: :ruby gem "pluck_to_hash" gem "pry", "~> 0.13.0" # Call 'byebug' anywhere in the code to stop execution and get a debugger console @@ -116,8 +114,8 @@ group :test, :development, :demo do gem "rails-erd" gem "rb-readline" gem "rspec" - gem "rspec-rails" # For CircleCI test metadata analysis + gem "rspec-rails" gem "rspec_junit_formatter" gem "rubocop", "= 0.83", require: false gem "rubocop-performance" diff --git a/Gemfile.lock b/Gemfile.lock index 84571920b40..feb78706664 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1596,6 +1596,7 @@ GEM aws-eventstream (~> 1, >= 1.0.2) backport (1.2.0) benchmark-ips (2.7.2) + bigdecimal (3.1.7) bootsnap (1.7.5) msgpack (~> 1.0) bourbon (4.2.7) @@ -1796,10 +1797,6 @@ GEM rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) - jshint (1.5.0) - execjs (>= 1.4.0) - multi_json (~> 1.0) - therubyracer (~> 0.12.1) json (2.3.0) json_schemer (0.2.16) ecma-re-validator (~> 0.2) @@ -1860,7 +1857,6 @@ GEM momentjs-rails (2.29.4.1) railties (>= 3.1) msgpack (1.4.2) - multi_json (1.12.2) multipart-post (2.1.1) multiverse (0.2.2) activerecord (>= 4.2) @@ -1998,7 +1994,6 @@ GEM redis-store (>= 1.2, < 2) redis-store (1.4.1) redis (>= 2.2, < 5) - ref (2.0.0) regexp_parser (1.6.0) request_store (1.4.1) rack (>= 1.4) @@ -2053,6 +2048,7 @@ GEM rake (>= 0.8.1) ruby-graphviz (1.2.4) ruby-oci8 (2.2.7) + ruby-ole (1.2.13.1) ruby-plsql (0.8.0) ruby-prof (1.4.1) ruby-progressbar (1.13.0) @@ -2116,6 +2112,9 @@ GEM thor (~> 0.19, >= 0.19.4) tilt (~> 2.0) yard (~> 0.9) + spreadsheet (1.3.1) + bigdecimal + ruby-ole sprockets (3.7.2) concurrent-ruby (~> 1.0) rack (> 1, < 3) @@ -2131,9 +2130,6 @@ GEM terminal-table (1.8.0) unicode-display_width (~> 1.1, >= 1.1.1) test-prof (0.10.1) - therubyracer (0.12.3) - libv8 (~> 3.16.14.15) - ref thor (0.20.3) thread_safe (0.3.6) tilt (2.0.8) @@ -2185,6 +2181,7 @@ GEM ziptz (2.1.6) PLATFORMS + x86_64-darwin-21 x86_64-darwin-22 x86_64-darwin-23 x86_64-linux @@ -2230,7 +2227,6 @@ DEPENDENCIES holidays (~> 6.4) icalendar immigrant - jshint json_schemer (~> 0.2.16) kaminari knapsack_pro (~> 3.8) @@ -2284,12 +2280,12 @@ DEPENDENCIES single_cov sniffybara! solargraph + spreadsheet (~> 1.3) sql_tracker statsd-instrument stringex strong_migrations test-prof - therubyracer timecop tty-tree tzinfo (= 1.2.10) diff --git a/app/controllers/hearings_controller.rb b/app/controllers/hearings_controller.rb index 59095f24635..bfa4089daa3 100644 --- a/app/controllers/hearings_controller.rb +++ b/app/controllers/hearings_controller.rb @@ -82,6 +82,7 @@ def virtual_hearing_job_status alias_with_host: hearing.virtual_hearing&.formatted_alias_or_alias_with_host, guest_link: hearing.virtual_hearing&.guest_link, host_link: hearing.virtual_hearing&.host_link, + co_host_link: hearing.virtual_hearing&.co_host_hearing_link, guest_pin: hearing.virtual_hearing&.guest_pin, host_pin: hearing.virtual_hearing&.host_pin } diff --git a/app/jobs/caseflow_job.rb b/app/jobs/caseflow_job.rb index ab4fac2d639..e1ce79195f7 100644 --- a/app/jobs/caseflow_job.rb +++ b/app/jobs/caseflow_job.rb @@ -34,12 +34,8 @@ def metrics_service_report_time_segment(segment:, start_time:) ) end - def slack_url - ENV["SLACK_DISPATCH_ALERT_URL"] - end - def slack_service - @slack_service ||= SlackService.new(url: slack_url) + @slack_service ||= SlackService.new end def log_error(error, extra: {}) diff --git a/app/jobs/hearings/create_non_virtual_conference_job.rb b/app/jobs/hearings/create_non_virtual_conference_job.rb index 1d414a4c62d..78b1ca678e3 100644 --- a/app/jobs/hearings/create_non_virtual_conference_job.rb +++ b/app/jobs/hearings/create_non_virtual_conference_job.rb @@ -3,10 +3,12 @@ # This job creates a Webex conference & link for a non virtual hearing class Hearings::CreateNonVirtualConferenceJob < CaseflowJob - include Hearings::EnsureCurrentUserIsSet + # We are not using ensure_current_user_is_set because of some + # potential for rollbacks if the set user is not the system user queue_with_priority :high_priority application_attr :hearing_schedule + attr_reader :hearing # Retry if Webex returns an invalid response. @@ -15,12 +17,11 @@ class Hearings::CreateNonVirtualConferenceJob < CaseflowJob end def perform(hearing:) - ensure_current_user_is_set + RequestStore.store[:current_user] = User.system_user WebexConferenceLink.find_or_create_by!( - hearing_id: hearing.id, - hearing_type: hearing.readable_request_type, hearing: hearing, - created_by: hearing.created_by + created_by: hearing.created_by, + updated_by: hearing.created_by ) end end diff --git a/app/jobs/hearings/download_transcription_file_job.rb b/app/jobs/hearings/download_transcription_file_job.rb index 7211c4edfbd..ea1a49e9429 100644 --- a/app/jobs/hearings/download_transcription_file_job.rb +++ b/app/jobs/hearings/download_transcription_file_job.rb @@ -8,6 +8,7 @@ class Hearings::DownloadTranscriptionFileJob < CaseflowJob include Hearings::EnsureCurrentUserIsSet + include Hearings::SendTranscriptionIssuesEmail queue_with_priority :low_priority application_attr :hearing_schedule @@ -25,25 +26,24 @@ class HearingAssociationError < StandardError; end provider: "webex" } error_details = job.build_error_details(exception, details_hash) - TranscriptionFileIssuesMailer.issue_notification(error_details) - job.log_error(exception) + job.log_download_error(exception) + job.send_transcription_issues_email(error_details) end retry_on(TranscriptionFileUpload::FileUploadError, wait: :exponentially_longer) do |job, exception| details_hash = { error: { type: "upload" }, provider: "S3" } error_details = job.build_error_details(exception, details_hash) - TranscriptionFileIssuesMailer.issue_notification(error_details) job.transcription_file.clean_up_tmp_location - job.log_error(exception) + job.log_download_error(exception) + job.send_transcription_issues_email(error_details) end retry_on(TranscriptionTransformer::FileConversionError, wait: 10.seconds) do |job, exception| job.transcription_file.clean_up_tmp_location details_hash = { error: { type: "conversion" }, conversion_type: "rtf" } error_details = job.build_error_details(exception, details_hash) - - TranscriptionFileIssuesMailer.issue_notification(error_details) - job.log_error(exception) + job.log_download_error(exception) + job.send_transcription_issues_email(error_details) end discard_on(FileNameError) do |job, exception| @@ -54,8 +54,8 @@ class HearingAssociationError < StandardError; end expected_file_name_format: "[docket_number]_[internal_id]_[hearing_type].[file_type]" } error_details = job.build_error_details(exception, details_hash) - TranscriptionFileIssuesMailer.issue_notification(error_details) - Rails.logger.error("#{job.class.name} (#{job.job_id}) discarded with error: #{exception}") + job.log_download_error(exception) + job.send_transcription_issues_email(error_details) end # Purpose: Downloads audio (mp3), video (mp4), or transcript (vtt) file from Webex temporary download link and @@ -96,14 +96,14 @@ def build_error_details(error, details_hash) # Note: Public method to provide access during job retry # # Params: error - Error object - def log_error(error) + def log_download_error(error) extra = { application: self.class.name, - hearing_id: hearing.id, + hearing_id: !error.is_a?(FileNameError) ? hearing.id : nil, file_name: file_name, job_id: job_id } - super(error, extra: extra) + log_error(error, extra: extra) end private diff --git a/app/jobs/hearings/fetch_webex_recordings_details_job.rb b/app/jobs/hearings/fetch_webex_recordings_details_job.rb index 31367754536..ea1946743c5 100644 --- a/app/jobs/hearings/fetch_webex_recordings_details_job.rb +++ b/app/jobs/hearings/fetch_webex_recordings_details_job.rb @@ -5,6 +5,7 @@ class Hearings::FetchWebexRecordingsDetailsJob < CaseflowJob include Hearings::EnsureCurrentUserIsSet + include Hearings::SendTranscriptionIssuesEmail queue_with_priority :low_priority application_attr :hearing_schedule @@ -20,13 +21,12 @@ class Hearings::FetchWebexRecordingsDetailsJob < CaseflowJob response: { status: exception.code, message: exception.message }.to_json, docket_number: nil } - TranscriptionFileIssuesMailer.issue_notification(error_details) job.log_error(exception) + job.send_transcription_issues_email(error_details) end - def perform(id:, file_name:) + def perform(id:) ensure_current_user_is_set - @file_name ||= file_name data = fetch_recording_details(id) topic = data.topic @@ -51,21 +51,24 @@ def log_error(error) private def fetch_recording_details(id) - config = { + WebexService.new( host: ENV["WEBEX_HOST_MAIN"], port: ENV["WEBEX_PORT"], aud: ENV["WEBEX_ORGANIZATION"], - apikey: ENV["WEBEX_BOTTOKEN"], + apikey: CredStash.get("webex_#{Rails.deploy_env}_access_token"), domain: ENV["WEBEX_DOMAIN_MAIN"], api_endpoint: ENV["WEBEX_API_MAIN"], - query: { "id": id } - } - - WebexService.new(config: config).fetch_recording_details + query: nil + ).fetch_recording_details(id) end def create_file_name(topic, extension) - subject = topic.split("-").second.lstrip + type = topic.scan(/[A-Za-z]+?(?=-)/).first + subject = if type == "Hearing" + topic.scan(/\d*-\d*_\d*_[A-Za-z]+?(?=-)/).first + else + topic.split("-").second.lstrip + end counter = topic.split("-").last "#{subject}-#{counter}.#{extension}" end diff --git a/app/jobs/hearings/fetch_webex_recordings_list_job.rb b/app/jobs/hearings/fetch_webex_recordings_list_job.rb index b798cd11334..fe7f5801275 100644 --- a/app/jobs/hearings/fetch_webex_recordings_list_job.rb +++ b/app/jobs/hearings/fetch_webex_recordings_list_job.rb @@ -4,6 +4,7 @@ class Hearings::FetchWebexRecordingsListJob < CaseflowJob include Hearings::EnsureCurrentUserIsSet + include Hearings::SendTranscriptionIssuesEmail queue_with_priority :low_priority application_attr :hearing_schedule @@ -15,23 +16,20 @@ class Hearings::FetchWebexRecordingsListJob < CaseflowJob query = "?max=#{max}?from=#{CGI.escape(from.iso8601)}?to=#{CGI.escape(to.iso8601)}" error_details = { error: { type: "retrieval", explanation: "retrieve a list of recordings from Webex" }, + provider: "webex", api_call: "GET #{ENV['WEBEX_HOST_MAIN']}#{ENV['WEBEX_DOMAIN_MAIN']}#{ENV['WEBEX_API_MAIN']}#{query}", response: { status: exception.code, message: exception.message }.to_json, times: { from: from, to: to }, docket_number: nil } - TranscriptionFileIssuesMailer.issue_notification(error_details) job.log_error(exception) + job.send_transcription_issues_email(error_details) end def perform ensure_current_user_is_set - response = fetch_recordings_list - topics = response.topics - topic_num = 0 - response.ids.each do |id| - Hearings::FetchWebexRecordingsDetailsJob.perform_later(id: id, topic: topics[topic_num]) - topic_num += 1 + fetch_recordings_list.ids.each do |n| + Hearings::FetchWebexRecordingsDetailsJob.perform_later(id: n) end end @@ -42,8 +40,8 @@ def log_error(error) private def fetch_recordings_list - from = CGI.escape(2.hours.ago.in_time_zone("America/New_York").beginning_of_hour.iso8601) - to = CGI.escape(1.hour.ago.in_time_zone("America/New_York").beginning_of_hour.iso8601) + from = 2.hours.ago.in_time_zone("America/New_York").beginning_of_hour.iso8601 + to = 1.hour.ago.in_time_zone("America/New_York").beginning_of_hour.iso8601 max = 100 query = { "from": from, "to": to, "max": max } @@ -51,7 +49,7 @@ def fetch_recordings_list host: ENV["WEBEX_HOST_MAIN"], port: ENV["WEBEX_PORT"], aud: ENV["WEBEX_ORGANIZATION"], - apikey: ENV["WEBEX_BOTTOKEN"], + apikey: CredStash.get("webex_#{Rails.deploy_env}_access_token"), domain: ENV["WEBEX_DOMAIN_MAIN"], api_endpoint: ENV["WEBEX_API_MAIN"], query: query diff --git a/app/jobs/virtual_hearings/refresh_webex_access_token_job.rb b/app/jobs/hearings/refresh_webex_access_token_job.rb similarity index 89% rename from app/jobs/virtual_hearings/refresh_webex_access_token_job.rb rename to app/jobs/hearings/refresh_webex_access_token_job.rb index d2a8251da4d..c2721771675 100644 --- a/app/jobs/virtual_hearings/refresh_webex_access_token_job.rb +++ b/app/jobs/hearings/refresh_webex_access_token_job.rb @@ -14,17 +14,17 @@ # This job is queued with low priority, indicating that it does not need to be run immediately # and can wait until the system is less busy. -class VirtualHearings::RefreshWebexAccessTokenJob < CaseflowJob +class Hearings::RefreshWebexAccessTokenJob < CaseflowJob queue_with_priority :low_priority def perform webex_service = WebexService.new( - host: nil, + host: ENV["WEBEX_HOST_MAIN"], port: nil, aud: nil, apikey: nil, - domain: nil, - api_endpoint: nil, + domain: ENV["WEBEX_DOMAIN_MAIN"], + api_endpoint: ENV["WEBEX_API_MAIN"], query: nil ) response = webex_service.refresh_access_token diff --git a/app/jobs/hearings/send_transcription_issues_email.rb b/app/jobs/hearings/send_transcription_issues_email.rb new file mode 100644 index 00000000000..af1eb627dbe --- /dev/null +++ b/app/jobs/hearings/send_transcription_issues_email.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Hearings::SendTranscriptionIssuesEmail + def send_transcription_issues_email(error_details) + TranscriptionFileIssuesMailer.issue_notification(error_details).deliver_now! + rescue StandardError, Savon::Error, BGS::ShareError => error + # Savon::Error and BGS::ShareError are sometimes thrown when making requests to BGS endpoints + log_error(error) + end +end diff --git a/app/jobs/hearings/work_order_file_job.rb b/app/jobs/hearings/work_order_file_job.rb new file mode 100644 index 00000000000..7cbef96aa0c --- /dev/null +++ b/app/jobs/hearings/work_order_file_job.rb @@ -0,0 +1,138 @@ +# frozen_string_literal: true + +class Hearings::WorkOrderFileJob < CaseflowJob + queue_with_priority :low_priority + + S3_BUCKET = "vaec-appeals-caseflow" + TMP_FOLDER = Rails.root.join("tmp", "transcription_files", "xls") + + attr_reader :file_name, :file_path + + class WorkOrderFileUploadError < StandardError; end + + retry_on WorkOrderFileUploadError, wait: :exponentially_longer do |job, _exception| + job.send_failure_notification + false + end + + def initialize(*args) + super(*args) + @file_name = nil + @file_path = nil + end + + def perform(work_order) + work_book = create_spreadsheet(work_order) + write_to_workbook(work_book, work_order[:work_order_name]) + upload_to_s3(work_order[:work_order_name]) + true + end + + def send_failure_notification + WorkOrderFileIssuesMailer.send_notification + end + + private + + def create_spreadsheet(work_order) + workbook = Spreadsheet::Workbook.new + worksheet = workbook.create_worksheet + + worksheet.row(0).concat ["Work Order", work_order[:work_order_name]] + worksheet.row(2).concat ["Return Date", work_order[:return_date]] + worksheet.row(4).concat ["Contractor Name", work_order[:contractor]] + + create_table(work_order[:hearings], worksheet) + workbook + end + + def write_to_workbook(workbook, work_order_name) + @file_name = "#{work_order_name}.xls" + @file_path = TMP_FOLDER.join(@file_name) + workbook.write(@file_path) + end + + def create_table(hearings_data, worksheet) + setup_worksheet_header(worksheet) + hearings = fetch_hearings(hearings_data) + populate_table_data(hearings, worksheet) + end + + def setup_worksheet_header(worksheet) + header_format = Spreadsheet::Format.new weight: :bold, border: :thin + columns = ["DOCKET NUMBER", "FIRST NAME", "LAST NAME", "TYPES", "HEARING DATE", "RO", "VLJ", "APPEAL TYPE"] + set_border_format(worksheet.row(6), header_format) + worksheet.row(6).concat(columns) + end + + def fetch_hearings(hearings_data) + Hearing.includes(:appeal).where(id: hearings_data.pluck(:hearing_id)) + end + + def populate_table_data(hearings, worksheet) + table_data = hearings.map { |hearing| format_hearing_data(hearing) } + append_table_data_to_worksheet(table_data, worksheet) + end + + def format_hearing_data(hearing) + begin + appeal = hearing.appeal + rescue StandardError + Rails.logger.error "Work Order File Job failed to fetch appeal from hearing #{hearing.id}" + return default_hearing_data + end + + hearing_date = format_hearing_date(appeal) + [ + appeal.docket_number, + hearing.appellant_first_name, + hearing.appellant_last_name, + appeal.type, + hearing_date, + hearing.regional_office.name, + hearing.judge.full_name, + appeal_type(appeal) + ] + end + + def default_hearing_data + ["N/A", "N/A", "N/A", "N/A", "N/A", "N/A", "N/A", "N/A"] + end + + def format_hearing_date(appeal) + appeal.hearing_day_if_schedueled&.strftime("%m/%d/%Y") || "" + end + + def appeal_type(appeal) + appeal.is_a?(LegacyAppeal) ? "Legacy" : "AMA" + end + + def append_table_data_to_worksheet(table_data, worksheet) + table_data.each_with_index do |row, index| + worksheet.row(7 + index).replace(row) + end + end + + def set_border_format(row, row_format) + (0..7).each { |col_index| row.set_format(col_index, row_format) } + end + + def upload_to_s3(work_order_name) + begin + S3Service.store_file(s3_location, @file_path, :filepath) + rescue StandardError => error + Rails.logger.error "Work Order File Job failed to upload Work Order #{work_order_name} to S3: #{error.message}" + cleanup_tmp_file + raise WorkOrderFileUploadError + end + end + + def s3_location + folder_name = (Rails.deploy_env == :prod) ? S3_BUCKET : "#{S3_BUCKET}-#{Rails.deploy_env}" + "#{folder_name}/transcript_text/#{@file_name}" + end + + def cleanup_tmp_file + File.delete(@file_path) if File.exist?(@file_path) + end +end diff --git a/app/jobs/hearings/zip_and_upload_transcription_files_job.rb b/app/jobs/hearings/zip_and_upload_transcription_files_job.rb new file mode 100644 index 00000000000..01ad2b1437e --- /dev/null +++ b/app/jobs/hearings/zip_and_upload_transcription_files_job.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true + +module Hearings + class ZipAndUploadTranscriptionFilesJob < CaseflowJob + include EnsureCurrentUserIsSet + + queue_as :low_priority + attr_reader :tmp_files_to_cleanup + + class ZipFileUploadError < StandardError; end + + retry_on TranscriptionFileUpload::FileUploadError, wait: :exponentially_longer do |job, _exception| + job.cleanup_tmp_files + fail ZipFileUploadError + end + + def perform(hearing_lookup_hashes) + @tmp_files_to_cleanup = [] + ensure_current_user_is_set + hearing_lookup_hashes.each do |hearing_lookup_hash| + process_hearing(hearing_lookup_hash) + end + cleanup_tmp_files + end + + def cleanup_tmp_files + @tmp_files_to_cleanup&.each { |path| File.delete(path) if File.exist?(path) } + Rails.logger.info("Cleaned up the following files from tmp: #{@tmp_files_to_cleanup}") + end + + private + + ALLOWED_HEARING_CLASSES = { + "Hearing" => Hearing, + "LegacyHearing" => LegacyHearing + }.freeze + + TRANSCRIPTION_FILE_TYPES = %w[mp3 rtf].freeze + + def process_hearing(hearing_lookup_hash) + hearing = fetch_hearing(hearing_lookup_hash) + tmp_file_paths = fetch_transcription_files(hearing) + zip_file_path = create_zip_file(tmp_file_paths, hearing) + formatted_zip_path = rename_before_upload(zip_file_path) + @tmp_files_to_cleanup += tmp_file_paths + [formatted_zip_path] + create_and_upload_transcription_file(hearing, formatted_zip_path) + end + + def fetch_hearing(hearing_lookup_hash) + hearing_class = ALLOWED_HEARING_CLASSES[hearing_lookup_hash[:hearing_type]] + hearing_class.find(hearing_lookup_hash[:hearing_id]) + end + + def fetch_transcription_files(hearing) + hearing.transcription_files.where(file_type: TRANSCRIPTION_FILE_TYPES).map(&:fetch_file_from_s3!) + end + + def create_zip_file(file_paths, hearing) + zip_file_name = generate_zip_file_name(hearing) + Zip::File.open(zip_file_name, create: true) do |zip_file| + file_paths.each { |path| zip_file.add(File.basename(path), path) } + end + zip_file_name + end + + def generate_zip_file_name(hearing) + File.join( + Rails.root, "tmp", "transcription_files", "zip", "#{hearing.docket_number}_#{hearing.id}_#{hearing.class}.zip" + ) + end + + def rename_before_upload(zip_file_path) + checksum = xor_checksum(zip_file_path) + creation_date = format_creation_date(zip_file_path) + new_path = zip_file_path.sub(".", "-#{checksum}-#{creation_date}.") + File.rename(zip_file_path, new_path) + new_path + end + + def xor_checksum(file_path) + checksum = File.open(file_path, "rb").each_byte.reduce(0, :^) + checksum.to_s(16) + end + + def format_creation_date(file_path) + File.ctime(file_path).strftime("%Y%m%d") + end + + def create_and_upload_transcription_file(hearing, file_path) + TranscriptionFile.create!( + file_name: File.basename(file_path), + hearing_id: hearing.id, + hearing_type: hearing.class.name, + docket_number: hearing.docket_number, + file_type: "zip", + created_by_id: RequestStore[:current_user].id + ).upload_to_s3! + rescue ActiveRecord::RecordInvalid => error + Rails.logger.error "Failed to create transcription file: #{error.message}" + end + end +end diff --git a/app/jobs/out_of_service_reminder_job.rb b/app/jobs/out_of_service_reminder_job.rb index 1477126c6c1..8698d68f4c9 100644 --- a/app/jobs/out_of_service_reminder_job.rb +++ b/app/jobs/out_of_service_reminder_job.rb @@ -13,7 +13,7 @@ def perform out_of_service_apps.push(app.humanize) if Rails.cache.read(app + "_out_of_service") end - SlackService.new(url: url).send_notification(message(out_of_service_apps)) unless out_of_service_apps.empty? + SlackService.new.send_notification(message(out_of_service_apps)) unless out_of_service_apps.empty? end def message(apps) @@ -23,8 +23,4 @@ def message(apps) "Reminder: #{apps.to_sentence} are out of service." end end - - def url - ENV["SLACK_DISPATCH_ALERT_URL"] - end end diff --git a/app/jobs/prepare_establish_claim_tasks_job.rb b/app/jobs/prepare_establish_claim_tasks_job.rb index 4ffbd1fb96d..e77de7a4118 100644 --- a/app/jobs/prepare_establish_claim_tasks_job.rb +++ b/app/jobs/prepare_establish_claim_tasks_job.rb @@ -28,10 +28,6 @@ def log_info(count) msg = "PrepareEstablishClaimTasksJob successfully ran: #{count[:success]} tasks " \ "prepared and #{count[:fail]} tasks failed" Rails.logger.info msg - SlackService.new(url: url).send_notification(msg) - end - - def url - ENV["SLACK_DISPATCH_ALERT_URL"] + SlackService.new.send_notification(msg) end end diff --git a/app/jobs/quarterly_notifications_job.rb b/app/jobs/quarterly_notifications_job.rb index 0c7a686f2bb..70444b312bf 100644 --- a/app/jobs/quarterly_notifications_job.rb +++ b/app/jobs/quarterly_notifications_job.rb @@ -16,9 +16,8 @@ class QuarterlyNotificationsJob < CaseflowJob def perform # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity ensure_current_user_is_set - AppealState.where.not( - decision_mailed: true, appeal_cancelled: true - ).find_in_batches(batch_size: QUERY_LIMIT.to_i) do |batched_appeal_states| + AppealState.where.not(decision_mailed: true).where.not(appeal_cancelled: true) + .find_in_batches(batch_size: QUERY_LIMIT.to_i) do |batched_appeal_states| batched_appeal_states.each do |appeal_state| # add_record_to_appeal_states_table(appeal_state.appeal) if appeal_state.appeal_type == "Appeal" diff --git a/app/jobs/virtual_hearings/conference_client.rb b/app/jobs/virtual_hearings/conference_client.rb index cc7aec4577f..fbab92c5905 100644 --- a/app/jobs/virtual_hearings/conference_client.rb +++ b/app/jobs/virtual_hearings/conference_client.rb @@ -1,32 +1,50 @@ # frozen_string_literal: true module VirtualHearings::ConferenceClient - # rubocop:disable Metrics/MethodLength def client(virtual_hearing) - case virtual_hearing.conference_provider - when "pexip" - @client ||= PexipService.new( - host: ENV["PEXIP_MANAGEMENT_NODE_HOST"], - port: ENV["PEXIP_MANAGEMENT_NODE_PORT"], - user_name: ENV["PEXIP_USERNAME"], - password: ENV["PEXIP_PASSWORD"], - client_host: ENV["PEXIP_CLIENT_HOST"] - ) - when "webex" - config = { - host: ENV["WEBEX_HOST_IC"], - port: ENV["WEBEX_PORT"], - aud: ENV["WEBEX_ORGANIZATION"], - apikey: ENV["WEBEX_BOTTOKEN"], - domain: ENV["WEBEX_DOMAIN_IC"], - api_endpoint: ENV["WEBEX_API_IC"], - query: nil - } - @client ||= WebexService.new(config: config) - else - msg = "Conference Provider for the Virtual Hearing Not Found" - fail Caseflow::Error::MeetingTypeNotFoundError, message: msg - end + @client ||= case virtual_hearing.conference_provider + when "pexip" then create_pexip_client + when "webex" then create_webex_client + when nil + virtual_hearing.set_default_meeting_type + + return create_pexip_client if virtual_hearing.conference_provider == "pexip" + + return create_webex_client if virtual_hearing.conference_provider == "webex" + + raise_not_found_error + else + raise_not_found_error + end + end + + private + + def raise_not_found_error + msg = "Conference Provider for the Virtual Hearing Not Found" + + fail Caseflow::Error::MeetingTypeNotFoundError, message: msg + end + + def create_webex_client + WebexService.new( + host: ENV["WEBEX_HOST_IC"], + port: ENV["WEBEX_PORT"], + aud: ENV["WEBEX_ORGANIZATION"], + apikey: ENV["WEBEX_BOTTOKEN"], + domain: ENV["WEBEX_DOMAIN_IC"], + api_endpoint: ENV["WEBEX_API_IC"], + query: nil + ) + end + + def create_pexip_client + PexipService.new( + host: ENV["PEXIP_MANAGEMENT_NODE_HOST"], + port: ENV["PEXIP_MANAGEMENT_NODE_PORT"], + user_name: ENV["PEXIP_USERNAME"], + password: ENV["PEXIP_PASSWORD"], + client_host: ENV["PEXIP_CLIENT_HOST"] + ) end - # rubocop:enable Metrics/MethodLength end diff --git a/app/mailers/hearing_mailer.rb b/app/mailers/hearing_mailer.rb index c6f352f66c0..be13a99ad96 100644 --- a/app/mailers/hearing_mailer.rb +++ b/app/mailers/hearing_mailer.rb @@ -165,7 +165,7 @@ def link end # Raise an error if the link contains the old virtual hearing link 2021-11-10 - if hearing_link.include?(BAD_VIRTUAL_LINK_TEXT) + if hearing_link.nil? || hearing_link.include?(BAD_VIRTUAL_LINK_TEXT) fail BadVirtualLinkError, virtual_hearing_id: virtual_hearing&.id end diff --git a/app/mailers/transcription_file_issues_mailer.rb b/app/mailers/transcription_file_issues_mailer.rb index 25f64f33fb5..324515b6646 100644 --- a/app/mailers/transcription_file_issues_mailer.rb +++ b/app/mailers/transcription_file_issues_mailer.rb @@ -2,14 +2,35 @@ # rubocop:disable Rails/ApplicationMailer ## -# TranscriptionFileIssuesMailerr will: +# TranscriptionFileIssuesMailer: # - Generate emails from the templates in app/views/transcription_file_issues ## class TranscriptionFileIssuesMailer < ActionMailer::Base default from: "Board of Veterans' Appeals " layout "transcription_file_issues_mailer" - # Builds email from view in app/views/transcription_file_issues_mailer/issue_notification + # Purpose: Builds email from view in app/views/transcription_file_issues_mailer/issue_notification + # + # Params: details - Hash of key-value pairs required to populate email template: + # - error: { type: string, explanation: string } + # - type: to render subject in #build_subject + # - explanation: "Caseflow attempted to #{explanation} and received a fatal error." + # - provider: string, to build subject and closing statement in #build_outro + # - docket_number: string, optional, but if present renders in subject + # - appeal_id: string, optional, but if present renders Case Details link + # + # - Optionally, any additional key-value pairs are iterated over and included in body as bullets + # according to following formats: + # - key: value =>
  • key.to_s: value
  • + # - key: { link: value } =>
  • key.to_s
  • + # - key: { nested_key_1: value_1, nested_key_2: value_2 } => + #
  • key: + #
      + #
    • nested_key_1.to_s: value_1
    • + #
    • nested_key_2.to_us: value 2
    • + #
    + #
  • + # def issue_notification(details) @details = details build_mailer_params diff --git a/app/mailers/work_order_file_issues_mailer.rb b/app/mailers/work_order_file_issues_mailer.rb new file mode 100644 index 00000000000..458faa0ce07 --- /dev/null +++ b/app/mailers/work_order_file_issues_mailer.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +# rubocop:disable Rails/ApplicationMailer +class WorkOrderFileIssuesMailer < ActionMailer::Base + default from: "Board of Veterans' Appeals " + layout "work_order_file_issues_mailer" + + MAIL_ADDRESSES = { + development: "Caseflow@test.com", + test: "Caseflow@test.com", + uat: "BID_Appeals_UAT@bah.com", + prodtest: "VHACHABID_Appeals_ProdTest@va.gov", + prod: "BVAHearingTeam@VA.gov" + }.freeze + + def send_notification + mail(subject: subject, to: to_mail) + end + + private + + def subject + "Caseflow unable to upload to AWS S3 bucket" + end + + def to_mail + MAIL_ADDRESSES[Rails.deploy_env] + end +end +# rubocop:enable Rails/ApplicationMailer diff --git a/app/models/appeal.rb b/app/models/appeal.rb index 7004353d377..d296655fc4d 100644 --- a/app/models/appeal.rb +++ b/app/models/appeal.rb @@ -263,9 +263,7 @@ def mst? return decision_issues.any?(&:mst_status) unless decision_issues.empty? request_issues.active.any?(&:mst_status) || - (special_issue_list && - special_issue_list.created_at < "2023-06-01".to_date && - special_issue_list.military_sexual_trauma) + special_issue_list&.military_sexual_trauma end # :reek:RepeatedConditionals diff --git a/app/models/bgs_power_of_attorney.rb b/app/models/bgs_power_of_attorney.rb index 95b88cd238a..91750270316 100644 --- a/app/models/bgs_power_of_attorney.rb +++ b/app/models/bgs_power_of_attorney.rb @@ -49,7 +49,7 @@ def find_or_create_by_file_number(file_number) end def find_or_create_by_claimant_participant_id(claimant_participant_id) - poa = find_or_create_by!(claimant_participant_id: claimant_participant_id) + poa = default_scoped.find_or_create_by!(claimant_participant_id: claimant_participant_id) if FeatureToggle.enabled?(:poa_auto_refresh, user: RequestStore.store[:current_user]) poa.save_with_updated_bgs_record! if poa&.expired? end diff --git a/app/models/concerns/distribution_concern.rb b/app/models/concerns/distribution_concern.rb index 564064b07f1..982b14d5fb9 100644 --- a/app/models/concerns/distribution_concern.rb +++ b/app/models/concerns/distribution_concern.rb @@ -5,27 +5,47 @@ module DistributionConcern private + # A list of tasks which are expected or allowed to be open at time of distribution + ALLOWABLE_TASKS = [ + RootTask.name, + DistributionTask.name, + JudgeAssignTask.name, + TrackVeteranTask.name, + VeteranRecordRequest.name, + *MailTask.subclasses.reject(&:blocking?).map(&:name) + ].freeze + def assign_judge_tasks_for_appeals(appeals, judge) appeals.map do |appeal| + check_for_unexpected_tasks(appeal) + # If an appeal does not have an open DistributionTask, then it has already been distributed by automatic # case distribution and a new JudgeAssignTask should not be created. This should only occur if two users # request a distribution simultaneously. - if appeal.tasks.open.of_type(:DistributionTask).any? - distribution_task_assignee_id = appeal.tasks.of_type(:DistributionTask).first.assigned_to_id - Rails.logger.info("Calling JudgeAssignTaskCreator for appeal #{appeal.id} with judge #{judge.css_id}") - JudgeAssignTaskCreator.new(appeal: appeal, - judge: judge, - assigned_by_id: distribution_task_assignee_id).call - else - msg = "Appeal ID #{appeal.id} cannot be distributed. Check its task tree and manually remediate if necessary" - title = "Appeal unable to be distributed" - SlackService.new(url: slack_url).send_notification(msg, title) - - nil - end + next nil unless appeal.tasks.open.of_type(:DistributionTask).any? + + distribution_task_assignee_id = appeal.tasks.of_type(:DistributionTask).first.assigned_to_id + Rails.logger.info("Calling JudgeAssignTaskCreator for appeal #{appeal.id} with judge #{judge.css_id}") + JudgeAssignTaskCreator.new(appeal: appeal, + judge: judge, + assigned_by_id: distribution_task_assignee_id).call end end + # Check for tasks which are open that we would not expect to see at the time of distribution. Send a slack + # message for notification of a potential bug in part of the application, but do not stop the distribution + def check_for_unexpected_tasks(appeal) + unless appeal.tasks.open.reject { |task| ALLOWABLE_TASKS.include?(task.class.name) }.empty? + send_slack_notification(appeal) + end + end + + def send_slack_notification(appeal) + msg = "Appeal #{appeal.id}. Check its task tree for a potential bug or tasks which need to be manually remediated" + title = "Appeal with unexpected open tasks during distribution" + SlackService.new.send_notification(msg, title) + end + def assign_sct_tasks_for_appeals(appeals) appeals.map do |appeal| next nil unless appeal.tasks.open.of_type(:DistributionTask).any? @@ -41,10 +61,6 @@ def cancel_previous_judge_assign_task(appeal, judge_id) appeal.tasks.of_type(:JudgeAssignTask).where.not(assigned_to_id: judge_id).update(status: :cancelled) end - def slack_url - ENV["SLACK_DISPATCH_ALERT_URL"] - end - # rubocop:disable Metrics/MethodLength # :reek:FeatureEnvy def create_sct_appeals(appeals_args, limit) diff --git a/app/models/dispatch/task.rb b/app/models/dispatch/task.rb index b4aa97079a4..bbe2b877072 100644 --- a/app/models/dispatch/task.rb +++ b/app/models/dispatch/task.rb @@ -273,7 +273,7 @@ def should_invalidate? end def no_open_tasks_for_appeal - if self.class.to_complete_task_for_appeal(appeal).any? + if self.class.default_scoped.to_complete_task_for_appeal(appeal).any? errors.add(:appeal, "Uncompleted task already exists for this appeal") end end diff --git a/app/models/distribution.rb b/app/models/distribution.rb index 3066975042b..6b5479f377e 100644 --- a/app/models/distribution.rb +++ b/app/models/distribution.rb @@ -52,6 +52,9 @@ def distribute!(limit = nil) end rescue StandardError => error process_error(error) + title = "Distribution Failed" + msg = "Distribution #{id} failed: #{error.message}}" + SlackService.new.send_notification(msg, title) raise error end diff --git a/app/models/hearing.rb b/app/models/hearing.rb index 8f6e4f305ba..8c758bc5932 100644 --- a/app/models/hearing.rb +++ b/app/models/hearing.rb @@ -48,7 +48,7 @@ class Hearing < CaseflowRecord has_many :hearing_issue_notes has_many :email_events, class_name: "SentHearingEmailEvent" has_many :email_recipients, class_name: "HearingEmailRecipient" - has_many :transcription_files + has_many :transcription_files, as: :hearing class HearingDayFull < StandardError; end diff --git a/app/models/hearings/forms/base_hearing_update_form.rb b/app/models/hearings/forms/base_hearing_update_form.rb index 259b4acf547..83887aa39ed 100644 --- a/app/models/hearings/forms/base_hearing_update_form.rb +++ b/app/models/hearings/forms/base_hearing_update_form.rb @@ -128,14 +128,24 @@ def start_async_job? end def start_async_job + # If converting hearing from virtual to non-virtual if start_async_job? && virtual_hearing_cancelled? perform_later_or_now(VirtualHearings::DeleteConferencesJob) + maybe_start_activate_non_virtual_job + # If converting hearing from non-virtual to virtual elsif start_async_job? - start_activate_job + start_activate_virtual_job end end - def start_activate_job + # If a Webex hearing, activate new Webex conference links when converting from virtual to non-virtual + def maybe_start_activate_non_virtual_job + return unless hearing.conference_provider == "webex" + + perform_later_or_now(Hearings::CreateNonVirtualConferenceJob, hearing: hearing) + end + + def start_activate_virtual_job hearing.virtual_hearing.establishment.submit_for_processing! job_args = { diff --git a/app/models/hearings/transcription_file.rb b/app/models/hearings/transcription_file.rb index 6342e0f8fc6..5da2eab8b7d 100644 --- a/app/models/hearings/transcription_file.rb +++ b/app/models/hearings/transcription_file.rb @@ -6,7 +6,7 @@ class TranscriptionFile < CaseflowRecord belongs_to :transcription belongs_to :docket - VALID_FILE_TYPES = %w[mp3 mp4 vtt rtf xls csv].freeze + VALID_FILE_TYPES = %w[mp3 mp4 vtt rtf xls csv zip].freeze validates :file_type, inclusion: { in: VALID_FILE_TYPES, message: "'%s' is not valid" } diff --git a/app/models/hearings/virtual_hearing.rb b/app/models/hearings/virtual_hearing.rb index 19cc0b23964..d58d2346284 100644 --- a/app/models/hearings/virtual_hearing.rb +++ b/app/models/hearings/virtual_hearing.rb @@ -165,9 +165,11 @@ def host_pin def guest_link return guest_hearing_link if guest_hearing_link.present? - "#{VirtualHearing.base_url}?join=1&media=&escalate=1&" \ - "conference=#{formatted_alias_or_alias_with_host}&" \ - "pin=#{guest_pin}&role=guest" + if conference_provider == "pexip" + "#{VirtualHearing.base_url}?join=1&media=&escalate=1&" \ + "conference=#{formatted_alias_or_alias_with_host}&" \ + "pin=#{guest_pin}&role=guest" + end end def co_host_hearing_link @@ -177,9 +179,11 @@ def co_host_hearing_link def host_link return host_hearing_link if host_hearing_link.present? - "#{VirtualHearing.base_url}?join=1&media=&escalate=1&" \ - "conference=#{formatted_alias_or_alias_with_host}&" \ - "pin=#{host_pin}&role=host" + if conference_provider == "pexip" + "#{VirtualHearing.base_url}?join=1&media=&escalate=1&" \ + "conference=#{formatted_alias_or_alias_with_host}&" \ + "pin=#{host_pin}&role=host" + end end def test_link(title) diff --git a/app/models/hearings/webex_conference_link.rb b/app/models/hearings/webex_conference_link.rb index f9ad73a037b..fccc0a0a534 100644 --- a/app/models/hearings/webex_conference_link.rb +++ b/app/models/hearings/webex_conference_link.rb @@ -14,16 +14,15 @@ def guest_link def generate_conference_information meeting_type.update!(service_name: "webex") - config = { + conference_response = WebexService.new( host: ENV["WEBEX_HOST_IC"], port: ENV["WEBEX_PORT"], aud: ENV["WEBEX_ORGANIZATION"], apikey: ENV["WEBEX_BOTTOKEN"], domain: ENV["WEBEX_DOMAIN_IC"], - api_endpoint: ENV["WEBEX_API_IC"] - } - - conference_response = WebexService.new(config: config).create_conference(hearing) + api_endpoint: ENV["WEBEX_API_IC"], + query: nil + ).create_conference(hearing) update!( host_link: conference_response.host_link, diff --git a/app/models/judge_case_review.rb b/app/models/judge_case_review.rb index 86309e110fa..98a31b981a9 100644 --- a/app/models/judge_case_review.rb +++ b/app/models/judge_case_review.rb @@ -30,7 +30,7 @@ class JudgeCaseReview < CaseflowRecord # As of Dec 2019, we want AMA and Legacy to use the same cap. The percentages may differ. The # goal is to get to the cap as steadily across the month as possible MONTHLY_LIMIT_OF_QUALITY_REVIEWS = 137 - QUALITY_REVIEW_SELECTION_PROBABILITY = 0.032 + QUALITY_REVIEW_SELECTION_PROBABILITY = 0.100 def update_in_vacols! MetricsService.record("VACOLS: judge_case_review #{task_id}", diff --git a/app/models/legacy_appeal.rb b/app/models/legacy_appeal.rb index caa6c0c57c9..8ac8befc495 100644 --- a/app/models/legacy_appeal.rb +++ b/app/models/legacy_appeal.rb @@ -636,10 +636,7 @@ def mst? return false unless FeatureToggle.enabled?(:mst_identification, user: RequestStore[:current_user]) && FeatureToggle.enabled?(:legacy_mst_pact_identification, user: RequestStore[:current_user]) - issues.any?(&:mst_status) || - (special_issue_list && - special_issue_list.created_at < "2023-06-01".to_date && - special_issue_list.military_sexual_trauma) + issues.any?(&:mst_status) || special_issue_list&.military_sexual_trauma end def pact? diff --git a/app/models/legacy_hearing.rb b/app/models/legacy_hearing.rb index e94b4dd67d9..c22cde01167 100644 --- a/app/models/legacy_hearing.rb +++ b/app/models/legacy_hearing.rb @@ -74,7 +74,7 @@ class LegacyHearing < CaseflowRecord has_one :hearing_location, as: :hearing has_many :email_events, class_name: "SentHearingEmailEvent", foreign_key: :hearing_id has_many :email_recipients, class_name: "HearingEmailRecipient", foreign_key: :hearing_id - has_many :transcription_files, foreign_key: :hearing_id + has_many :transcription_files, as: :hearing alias_attribute :location, :hearing_location accepts_nested_attributes_for :hearing_location, reject_if: proc { |attributes| attributes.blank? } diff --git a/app/models/quality_review_case_selector.rb b/app/models/quality_review_case_selector.rb index f80603b9d2f..e319781bf97 100644 --- a/app/models/quality_review_case_selector.rb +++ b/app/models/quality_review_case_selector.rb @@ -4,7 +4,7 @@ class QualityReviewCaseSelector # AMA Limit and Probability. See Legacy numbers at app/models/judge_case_review.rb:28 # This probability was selected by Derek Brown, QR Director at BVA MONTHLY_LIMIT_OF_QUALITY_REVIEWS = 164 - QUALITY_REVIEW_SELECTION_PROBABILITY = 0.188 + QUALITY_REVIEW_SELECTION_PROBABILITY = 0.057 class << self def select_case_for_quality_review? diff --git a/app/models/tasks/judge_decision_review_task.rb b/app/models/tasks/judge_decision_review_task.rb index e0ae4a8b86f..9860f36affc 100644 --- a/app/models/tasks/judge_decision_review_task.rb +++ b/app/models/tasks/judge_decision_review_task.rb @@ -35,9 +35,9 @@ def self.label private def ama_judge_actions - # bypass special issues page if mst/pact enabled + # bypass special issues page if mst issue tracking is enabled return Constants.TASK_ACTIONS.JUDGE_AMA_CHECKOUT.to_h if - FeatureToggle.enabled?(:mst_identification) || FeatureToggle.enabled?(:pact_identification) + FeatureToggle.enabled?(:mst_identification) Constants.TASK_ACTIONS.JUDGE_AMA_CHECKOUT_SPECIAL_ISSUES.to_h end diff --git a/app/models/tasks/judge_dispatch_return_task.rb b/app/models/tasks/judge_dispatch_return_task.rb index fe0a6209656..676e366a567 100644 --- a/app/models/tasks/judge_dispatch_return_task.rb +++ b/app/models/tasks/judge_dispatch_return_task.rb @@ -18,9 +18,9 @@ def self.label # :reek:UtilityFunction def ama_issue_checkout - # bypass special issues page if mst/pact enabled + # bypass special issues page if mst issue tracking is enabled return Constants.TASK_ACTIONS.JUDGE_AMA_CHECKOUT.to_h if - FeatureToggle.enabled?(:mst_identification) || FeatureToggle.enabled?(:pact_identification) + FeatureToggle.enabled?(:mst_identification) Constants.TASK_ACTIONS.JUDGE_AMA_CHECKOUT_SPECIAL_ISSUES.to_h end diff --git a/app/models/team_quota.rb b/app/models/team_quota.rb index e084ecc28d7..5b5f012e990 100644 --- a/app/models/team_quota.rb +++ b/app/models/team_quota.rb @@ -94,6 +94,6 @@ def most_recent_user_count # Cap the search to the last month to avoid an infinite loop def most_recent_user_counts - self.class.where(task_type: task_type).order(:date).limit(31).lazy.map(&:user_count) + self.class.default_scoped.where(task_type: task_type).order(:date).limit(31).lazy.map(&:user_count) end end diff --git a/app/models/vbms_uploaded_document.rb b/app/models/vbms_uploaded_document.rb index 4614cf6afff..a715d927cd5 100644 --- a/app/models/vbms_uploaded_document.rb +++ b/app/models/vbms_uploaded_document.rb @@ -10,7 +10,10 @@ class VbmsUploadedDocument < CaseflowRecord attribute :file, :string scope :successfully_uploaded, lambda { - where(error: nil).where.not(uploaded_to_vbms_at: nil, attempted_at: nil, processed_at: nil) + where(error: nil) + .where.not(uploaded_to_vbms_at: nil) + .where.not(attempted_at: nil) + .where.not(processed_at: nil) } def cache_file diff --git a/app/queries/etl/unknown_status_with_completed_root_task_query.rb b/app/queries/etl/unknown_status_with_completed_root_task_query.rb index d24ceb9eb22..1b827f647d0 100644 --- a/app/queries/etl/unknown_status_with_completed_root_task_query.rb +++ b/app/queries/etl/unknown_status_with_completed_root_task_query.rb @@ -21,6 +21,7 @@ def appeal_ids_for_completed_root_tasks def appeal_ids_for_open_child_tasks ETL::Task.select(:appeal_id).distinct .where(appeal_type: "Appeal") - .where.not(task_type: "RootTask", task_status: Task.closed_statuses) + .where.not(task_type: "RootTask") + .where.not(task_status: Task.closed_statuses) end end diff --git a/app/queries/etl/unknown_status_with_open_root_task_query.rb b/app/queries/etl/unknown_status_with_open_root_task_query.rb index 3159a1368e4..1d2979de146 100644 --- a/app/queries/etl/unknown_status_with_open_root_task_query.rb +++ b/app/queries/etl/unknown_status_with_open_root_task_query.rb @@ -21,6 +21,7 @@ def appeal_ids_for_open_root_tasks def appeal_ids_for_open_child_tasks ETL::Task.select(:appeal_id).distinct .where(appeal_type: "Appeal") - .where.not(task_type: "RootTask", task_status: Task.closed_statuses) + .where.not(task_type: "RootTask") + .where.not(task_status: Task.closed_statuses) end end diff --git a/app/repositories/task_action_repository.rb b/app/repositories/task_action_repository.rb index 5f9683719f7..e5171aaf9f0 100644 --- a/app/repositories/task_action_repository.rb +++ b/app/repositories/task_action_repository.rb @@ -902,9 +902,9 @@ def cancel_task_and_return_to_sct_action(task, _) def select_ama_review_decision_action(task) return Constants.TASK_ACTIONS.REVIEW_VACATE_DECISION.to_h if task.appeal.vacate? - # route to decision if mst/pact toggles are enabled. + # bypass special issues page if MST is enabled return Constants.TASK_ACTIONS.REVIEW_AMA_DECISION.to_h if - FeatureToggle.enabled?(:mst_identification) || FeatureToggle.enabled?(:pact_identification) + FeatureToggle.enabled?(:mst_identification) Constants.TASK_ACTIONS.REVIEW_AMA_DECISION_SP_ISSUES.to_h end diff --git a/app/services/deprecation_warnings/disallowed_deprecations.rb b/app/services/deprecation_warnings/disallowed_deprecations.rb index c8abde11879..df4f14a24b1 100644 --- a/app/services/deprecation_warnings/disallowed_deprecations.rb +++ b/app/services/deprecation_warnings/disallowed_deprecations.rb @@ -18,7 +18,8 @@ class ::DisallowedDeprecationError < StandardError; end /ActionView::Base instances should be constructed with a lookup context, assignments, and a controller./, /ActionView::Base instances must implement `compiled_method_container`/, /render file: should be given the absolute path to a file/, - /`ActiveRecord::Result#to_hash` has been renamed to `to_a`/ + /`ActiveRecord::Result#to_hash` has been renamed to `to_a`/, + /Class level methods will no longer inherit scoping/ ].freeze # Regular expressions for deprecation warnings that should raise an exception on detection diff --git a/app/services/deprecation_warnings/production_handler.rb b/app/services/deprecation_warnings/production_handler.rb index 70a4b7fb44b..fe1c1310d78 100644 --- a/app/services/deprecation_warnings/production_handler.rb +++ b/app/services/deprecation_warnings/production_handler.rb @@ -46,9 +46,7 @@ def emit_warning_to_sentry(message, callstack, deprecation_horizon, gem_name) def emit_warning_to_slack_alerts_channel(message) slack_alert_title = "Deprecation Warning - #{APP_NAME} (#{ENV['DEPLOY_ENV']})" - SlackService - .new(url: ENV["SLACK_DISPATCH_ALERT_URL"]) - .send_notification(message, slack_alert_title, SLACK_ALERT_CHANNEL) + SlackService.new.send_notification(message, slack_alert_title, SLACK_ALERT_CHANNEL) end end end diff --git a/app/services/etl/syncer.rb b/app/services/etl/syncer.rb index 93b15e67411..3fafe869903 100644 --- a/app/services/etl/syncer.rb +++ b/app/services/etl/syncer.rb @@ -24,13 +24,8 @@ def initialize(since: nil, etl_build:, id_offset: 0) @id_offset = id_offset end - # :reek:UtilityFunction - def slack_url - ENV["SLACK_DISPATCH_ALERT_URL"] - end - def slack_service - @slack_service ||= SlackService.new(url: slack_url) + @slack_service ||= SlackService.new end # :reek:FeatureEnvy diff --git a/app/services/external_api/pexip_service.rb b/app/services/external_api/pexip_service.rb index 96188d407fd..cefa1c5ce1b 100644 --- a/app/services/external_api/pexip_service.rb +++ b/app/services/external_api/pexip_service.rb @@ -45,13 +45,17 @@ def delete_conference(virtual_hearing) delete_endpoint = "#{CONFERENCES_ENDPOINT}#{virtual_hearing.conference_id}/" resp = send_pexip_request(delete_endpoint, :delete) - return if resp.nil? + return lack_of_connectivity_response if resp.nil? ExternalApi::PexipService::DeleteResponse.new(resp) end private + def lack_of_connectivity_response + HTTPI::Response.new(503, {}, {}) + end + attr_reader :host, :port, :user_name, :password, :client_host # :nocov: diff --git a/app/services/external_api/pexip_service/response.rb b/app/services/external_api/pexip_service/response.rb index bcb91028f96..bb987aecf97 100644 --- a/app/services/external_api/pexip_service/response.rb +++ b/app/services/external_api/pexip_service/response.rb @@ -21,6 +21,7 @@ def success? private # :nocov: + # rubocop:disable Metrics/CyclomaticComplexity def check_for_error return if success? @@ -34,10 +35,15 @@ def check_for_error Caseflow::Error::PexipNotFoundError.new(code: code, message: msg) when 405 Caseflow::Error::PexipMethodNotAllowedError.new(code: code, message: msg) + when 503 + Caseflow::Error::PexipServiceNotReachableError.new(code: code, message: "Pexip Service is currently not + available") else + Caseflow::Error::PexipApiError.new(code: code, message: msg) end end + # rubocop:enable Metrics/CyclomaticComplexity def error_message return "No error message from Pexip" if resp.raw_body.empty? diff --git a/app/services/external_api/webex_service.rb b/app/services/external_api/webex_service.rb index f070fe23918..eceb0220e4f 100644 --- a/app/services/external_api/webex_service.rb +++ b/app/services/external_api/webex_service.rb @@ -19,9 +19,17 @@ # # All requests to the Webex API are recorded using the MetricsService. class ExternalApi::WebexService - def initialize(config:) - @config = config + # rubocop:disable Metrics/ParameterLists + def initialize(host:, port:, aud:, apikey:, domain:, api_endpoint:, query: nil) + @host = host + @port = port + @aud = aud + @apikey = apikey + @domain = domain + @api_endpoint = api_endpoint + @query = query end + # rubocop:enable Metrics/ParameterLists def create_conference(conferenced_item) body = { @@ -30,14 +38,13 @@ def create_conference(conferenced_item) "nbf": conferenced_item.nbf, "exp": conferenced_item.exp }, - "aud": @config[:aud], + "aud": @aud, "numHost": 2, "provideShortUrls": true, "verticalType": "gen" } method = "POST" - resp = send_webex_request(body, method) - ExternalApi::WebexService::CreateResponse.new(resp) if !resp.nil? + ExternalApi::WebexService::CreateResponse.new(send_webex_request(body, method)) end def delete_conference(conferenced_item) @@ -47,20 +54,19 @@ def delete_conference(conferenced_item) "nbf": 0, "exp": 0 }, - "aud": @config[:aud], + "aud": @aud, "numHost": 2, "provideShortUrls": true, "verticalType": "gen" } method = "POST" - resp = send_webex_request(body, method) - ExternalApi::WebexService::DeleteResponse.new(resp) if !resp.nil? + ExternalApi::WebexService::DeleteResponse.new(send_webex_request(body, method)) end # Purpose: Refreshing the access token to access the API # Return: The response body def refresh_access_token - url = URI::DEFAULT_PARSER.escape("#{BASE_URL}/v1/access_token") + url = URI::DEFAULT_PARSER.escape("https://#{@host}#{@domain}#{@api_endpoint}access_token") body = { grant_type: "refresh_token", @@ -88,41 +94,51 @@ def refresh_access_token def fetch_recordings_list body = nil method = "GET" - resp = send_webex_request(body, method) - ExternalApi::WebexService::RecordingsListResponse.new(resp) if !resp.nil? + @api_endpoint += "admin/recordings" + ExternalApi::WebexService::RecordingsListResponse.new(send_webex_request(body, method)) end - def fetch_recording_details + def fetch_recording_details(recording_id) body = nil method = "GET" - resp = send_webex_request(body, method) - ExternalApi::WebexService::RecordingDetailsResponse.new(resp) if !resp.nil? + @api_endpoint += "recordings/#{recording_id}" + ExternalApi::WebexService::RecordingDetailsResponse.new(send_webex_request(body, method)) end private # :nocov: + # rubocop:disable Metrics/MethodLength def send_webex_request(body, method) - url = "https://#{@config[:host]}#{@config[:domain]}#{@config[:api_endpoint]}" + url = "https://#{@host}#{@domain}#{@api_endpoint}" request = HTTPI::Request.new(url) request.open_timeout = 300 request.read_timeout = 300 request.body = body.to_json unless body.nil? - request.query = @config[:query] - request.headers = { "Authorization": "Bearer #{@config[:apikey]}", "Content-Type": "application/json" } + request.query = @query + request.headers = { "Authorization": "Bearer #{@apikey}", "Content-Type": "application/json" } MetricsService.record( - "#{@config[:host]} #{method} request to #{url}", + "#{@host} #{method} request to #{url}", service: :webex, - name: @config[:api_endpoint] + name: @api_endpoint ) do case method when "POST" - HTTPI.post(request) + response = HTTPI.post(request) + fail ExternalApi::WebexService::Response.new(response).error if response.error? + + response when "GET" - HTTPI.get(request) + response = HTTPI.get(request) + fail ExternalApi::WebexService::Response.new(response).error if response.error? + + response + else + fail NotImplementedError end end end + # rubocop:enable Metrics/MethodLength # :nocov: end diff --git a/app/services/external_api/webex_service/recording_details_response.rb b/app/services/external_api/webex_service/recording_details_response.rb index c24e5a04ff4..e8ab5afb393 100644 --- a/app/services/external_api/webex_service/recording_details_response.rb +++ b/app/services/external_api/webex_service/recording_details_response.rb @@ -6,7 +6,7 @@ def mp4_link end def vtt_link - data["temporaryDirectDownloadLinks"]["transcriptionDownloadLink"] + data["temporaryDirectDownloadLinks"]["transcriptDownloadLink"] end def mp3_link diff --git a/app/services/external_api/webex_service/recordings_list_response.rb b/app/services/external_api/webex_service/recordings_list_response.rb index 752a050282c..0e4c0b46432 100644 --- a/app/services/external_api/webex_service/recordings_list_response.rb +++ b/app/services/external_api/webex_service/recordings_list_response.rb @@ -2,10 +2,6 @@ class ExternalApi::WebexService::RecordingsListResponse < ExternalApi::WebexService::Response def ids - data["items"].pluck("id") - end - - def topics - data["items"].pluck("topic") + data.nil? ? [] : data["items"].pluck("id") end end diff --git a/app/services/external_api/webex_service/response.rb b/app/services/external_api/webex_service/response.rb index ea430b806e0..5bd77e23247 100644 --- a/app/services/external_api/webex_service/response.rb +++ b/app/services/external_api/webex_service/response.rb @@ -23,7 +23,7 @@ def success? private def check_for_errors - return if success? + return false if success? msg = error_message case code diff --git a/app/services/slack_service.rb b/app/services/slack_service.rb index f6cd64f3dc1..565452e328e 100644 --- a/app/services/slack_service.rb +++ b/app/services/slack_service.rb @@ -16,7 +16,7 @@ class SlackService warn: "#ffff00" }.freeze - def initialize(url:) + def initialize(url: ENV["SLACK_DISPATCH_ALERT_URL"]) @url = url end diff --git a/app/views/layouts/work_order_file_issues_mailer.html.erb b/app/views/layouts/work_order_file_issues_mailer.html.erb new file mode 100644 index 00000000000..c83fd898a0b --- /dev/null +++ b/app/views/layouts/work_order_file_issues_mailer.html.erb @@ -0,0 +1,16 @@ + + + + + + + + + <%= yield %> + <%= yield :intro %> + <%= yield :content %> + <%= yield :signature %> + + diff --git a/app/workflows/transcription_file_upload.rb b/app/workflows/transcription_file_upload.rb index 19db1c44742..56f2b338521 100644 --- a/app/workflows/transcription_file_upload.rb +++ b/app/workflows/transcription_file_upload.rb @@ -11,7 +11,8 @@ class TranscriptionFileUpload vtt: "transcript_raw", rtf: "transcript_text", xls: "transcript_text", - csv: "transcript_text" + csv: "transcript_text", + zip: "transcript_text" }.freeze class FileUploadError < StandardError; end diff --git a/app/workflows/transcription_packages.rb b/app/workflows/transcription_packages.rb new file mode 100644 index 00000000000..6a55154031b --- /dev/null +++ b/app/workflows/transcription_packages.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +class TranscriptionPackages + include ActiveModel::Model + include ActiveModel::Validations + + include MailRequestValidator::Distribution + include MailRequestValidator::DistributionDestination + + attr_reader :work_order_params + + def initialize(work_order_params) + @work_order_params = work_order_params + end + + def call + Hearings::WorkOrderFileJob.perform_now(work_order_params) ? create_zip_file : return + end + + def create_zip_file + Hearings::ZipAndUploadTranscriptionFilesJob.perform_now(work_order_params.hearings) ? create_bom_file : return + end + + def create_bom_file + Hearings::CreateBomFileJob.perform_now(work_order_params) ? create_transcription_package : return + end + + def create_transcription_package + Hearings::CreateTranscriptionPackageJob.perform_now(work_order_params) ? upload_transcription_package : return + end + + def upload_transcription_package + Hearings::UploadTranscriptionPackageJob.perform_now(work_order_params) + end +end diff --git a/client/app/caseDistribution/reducers/levers/leversActions.js b/client/app/caseDistribution/reducers/levers/leversActions.js index 5b9fb446f02..e416a735755 100644 --- a/client/app/caseDistribution/reducers/levers/leversActions.js +++ b/client/app/caseDistribution/reducers/levers/leversActions.js @@ -54,14 +54,29 @@ export const resetLevers = () => async (dispatch) => { }); }; -export const updateRadioLever = (leverGroup, leverItem, value, optionValue = null) => +/** + * Used when updating a radio lever + * Pass in the selected option and a value if the selected option is value + * + * This will break if a Radio lever has more than one option that has an input + * + * @param {*} leverGroup is the group the lever is in: + * affinity, batch, docket_distribution_prior, docket_time_goal, docket_levers + * @param {*} leverItem is the name of the lever: + * see DISTRIBUTION.json for valid names + * @param {*} optionItem is the option that was selected: + * value, omit, infinite + * @param {*} optionValue if value option is the selected the value of the input + * @returns + */ +export const updateRadioLever = (leverGroup, leverItem, optionItem, optionValue = null) => (dispatch) => { dispatch({ type: ACTIONS.UPDATE_RADIO_LEVER, payload: { leverGroup, leverItem, - value, + optionItem, optionValue } }); diff --git a/client/app/caseDistribution/reducers/levers/leversSelector.js b/client/app/caseDistribution/reducers/levers/leversSelector.js index c7fa5e99eab..a21efb8fe59 100644 --- a/client/app/caseDistribution/reducers/levers/leversSelector.js +++ b/client/app/caseDistribution/reducers/levers/leversSelector.js @@ -1,7 +1,7 @@ import { createSelector } from 'reselect'; import ACD_LEVERS from '../../../../constants/ACD_LEVERS'; import { - findSelectedOption, + findOption, hasCombinationLeverChanged, radioValueOptionSelected, findValueOption, @@ -176,7 +176,7 @@ export const hasNoLeverErrors = createSelector( ); /** - * Used when updating the a radio lever + * Used when updating a radio lever * Pass in the selected option and a value if the selected option is value * * This will break if a Radio lever has more than one option that has an input @@ -190,13 +190,13 @@ export const hasNoLeverErrors = createSelector( * Set valueOptionValue to value in value's option */ export const updateLeverGroupForRadioLever = (state, action) => { - const { leverGroup, leverItem, value, optionValue } = action.payload; + const { leverGroup, leverItem, optionItem, optionValue } = action.payload; const updateLeverValue = (lever) => { - const selectedOption = findSelectedOption(lever); - const isValueOption = radioValueOptionSelected(value); + const selectedOption = findOption(lever, optionItem); + const isValueOption = radioValueOptionSelected(optionItem); const valueOptionValue = isValueOption ? optionValue : findValueOption(lever).value; - const leverValue = isValueOption ? optionValue : value; + const leverValue = isValueOption ? optionValue : optionItem; // Set all options to not selected lever.options.forEach((option) => option.selected = false); @@ -207,7 +207,7 @@ export const updateLeverGroupForRadioLever = (state, action) => { return { ...lever, value: leverValue, - selectedOption: value, + selectedOption: optionItem, valueOptionValue }; }; diff --git a/client/app/components/badges/MstBadge/MstBadge.jsx b/client/app/components/badges/MstBadge/MstBadge.jsx index 8726dd68f61..ed847389642 100644 --- a/client/app/components/badges/MstBadge/MstBadge.jsx +++ b/client/app/components/badges/MstBadge/MstBadge.jsx @@ -12,16 +12,15 @@ const MstBadge = (props) => { const { appeal } = props; // During decision review workflow, saved/staged changes made are updated to appeal.decisionIssues - // if legacy check issues for changes, if ama check decision for changes - const issues = (appeal.isLegacyAppeal || appeal.type === 'LegacyAppeal') ? appeal.issues : appeal.decisionIssues; + const decisionIssues = appeal?.decisionIssues; - // check the issues/decisions for mst/pact changes in flight - if (issues && issues?.length > 0) { - if (!issues.some((issue) => issue.mst_status === true)) { + // check the decisions for mst changes in flight + if (decisionIssues?.length > 0) { + if (!decisionIssues.some((issue) => issue.mst_status === true)) { return null; } } else if (!appeal?.mst) { - // if issues are empty/undefined, use appeal model mst check + // if decisions are empty/undefined, use appeal model mst check return null; } diff --git a/client/app/components/badges/PactBadge/PactBadge.jsx b/client/app/components/badges/PactBadge/PactBadge.jsx index e8e58362fe0..590b854a97e 100644 --- a/client/app/components/badges/PactBadge/PactBadge.jsx +++ b/client/app/components/badges/PactBadge/PactBadge.jsx @@ -12,16 +12,15 @@ const PactBadge = (props) => { const { appeal } = props; // During decision review workflow, saved/staged changes made are updated to appeal.decisionIssues - // if legacy check issues for changes, if ama check decision for changes - const issues = (appeal.isLegacyAppeal || appeal.type === 'LegacyAppeal') ? appeal.issues : appeal.decisionIssues; + const decisionIssues = appeal?.decisionIssues; - // check the issues/decisions for mst/pact changes in flight - if (issues && issues?.length > 0) { - if (!issues.some((issue) => issue.pact_status === true)) { + // check the decisions for pact changes in flight + if (decisionIssues && decisionIssues?.length > 0) { + if (!decisionIssues.some((issue) => issue.pact_status === true)) { return null; } } else if (!appeal?.pact) { - // if issues are empty/undefined, use appeal model mst check + // if decisions are empty/undefined, use appeal model mst check return null; } diff --git a/client/app/queue/OrganizationUsers.jsx b/client/app/queue/OrganizationUsers.jsx index a245599450d..71274059578 100644 --- a/client/app/queue/OrganizationUsers.jsx +++ b/client/app/queue/OrganizationUsers.jsx @@ -1,3 +1,4 @@ +/* eslint-disable max-lines */ /* eslint-disable no-nested-ternary */ /* eslint-disable max-len */ import React from 'react'; @@ -10,6 +11,7 @@ import ApiUtil from '../util/ApiUtil'; import Alert from '../components/Alert'; import Button from '../components/Button'; import SearchableDropdown from '../components/SearchableDropdown'; +import SearchBar from 'app/components/SearchBar'; import { LOGO_COLORS } from '../constants/AppConstants'; import COPY from '../../COPY'; @@ -44,6 +46,10 @@ const userListItemStyle = css({ } }); +const topUserBorder = css({ + borderBottom: '.1rem solid gray' +}); + const titleButtonsStyle = css({ width: '60rem' }); @@ -72,6 +78,7 @@ export default class OrganizationUsers extends React.PureComponent { membershipRequests: [], loading: true, error: null, + searchValue: '', success: null, addingUser: null, changingAdminRights: {}, @@ -256,37 +263,45 @@ export default class OrganizationUsers extends React.PureComponent { loading={this.state.removingUser[user.id]} onClick={this.removeUser(user)} /> +getFilteredUsers = () => { + if (this.state.searchValue.length > 1) { + + // return name or css id if match + return this.state.organizationUsers.filter((user) => + user.attributes.full_name.toLowerCase().includes(this.state.searchValue.toLowerCase()) || + user.attributes.css_id.toLowerCase().includes(this.state.searchValue.toLowerCase()) + ); + } + + return this.state.organizationUsers; +}; + mainContent = () => { const judgeTeam = this.state.judgeTeam; const dvcTeam = this.state.dvcTeam; - const listOfUsers = this.state.organizationUsers.map((user) => { + const listOfUsers = this.getFilteredUsers().map((user) => { const { dvc, admin } = user.attributes; const { conferenceSelectionVisibility } = this.props; - let altLabel = ''; - - if (judgeTeam && admin) { - altLabel = COPY.USER_MANAGEMENT_JUDGE_LABEL; - } - if (dvcTeam && dvc) { - altLabel = COPY.USER_MANAGEMENT_DVC_LABEL; - } - if (judgeTeam && !admin) { - altLabel = COPY.USER_MANAGEMENT_ATTORNEY_LABEL; - } - if ((judgeTeam || dvcTeam) && admin) { - altLabel = COPY.USER_MANAGEMENT_ADMIN_LABEL; - } return ( - +
  • - {this.formatName(user)} - {altLabel !== '' && (
    ( {altLabel} )
    )} -
    - {judgeTeam || dvcTeam ? '' : this.adminButton(user, admin)} - {this.removeUserButton(user)} -
    + { this.formatName(user) } + { judgeTeam && admin && ( {COPY.USER_MANAGEMENT_JUDGE_LABEL} ) } + { dvcTeam && dvc && ( {COPY.USER_MANAGEMENT_DVC_LABEL} ) } + { judgeTeam && !admin && ( {COPY.USER_MANAGEMENT_ATTORNEY_LABEL} ) } + { (judgeTeam || dvcTeam) && admin && ( {COPY.USER_MANAGEMENT_ADMIN_LABEL} ) } + + { + (judgeTeam || dvcTeam) && admin ? +
    : +
    + { (judgeTeam || dvcTeam) ? '' : this.adminButton(user, admin) } + { this.removeUserButton(user) } +
    + } +
    {this.state.organizationName === 'Hearings Management' && @@ -309,6 +324,18 @@ export default class OrganizationUsers extends React.PureComponent { ); }); + const handleSearchChange = (value) => { + this.setState({ + searchValue: value + }); + }; + + const handleClearSearch = () => { + this.setState({ + searchValue: '' + }); + }; + return

    {COPY.USER_MANAGEMENT_ADD_USER_TO_ORG_DROPDOWN_LABEL}

    @@ -329,14 +356,39 @@ export default class OrganizationUsers extends React.PureComponent { async={this.asyncLoadUser} />
    -

    {COPY.USER_MANAGEMENT_EDIT_USER_IN_ORG_LABEL}

    -
      - { (judgeTeam || dvcTeam) ? '' :
    • {COPY.USER_MANAGEMENT_ADMIN_RIGHTS_HEADING}{COPY.USER_MANAGEMENT_ADMIN_RIGHTS_DESCRIPTION}
    • } -
    • {COPY.USER_MANAGEMENT_REMOVE_USER_HEADING}{ judgeTeam ? - COPY.USER_MANAGEMENT_JUDGE_TEAM_REMOVE_USER_DESCRIPTION : - COPY.USER_MANAGEMENT_REMOVE_USER_DESCRIPTION }
    • -
    -
      {listOfUsers}
    +
    +

    {COPY.USER_MANAGEMENT_EDIT_USER_IN_ORG_LABEL}

    +
      + { (judgeTeam || dvcTeam) ? '' :
    • {COPY.USER_MANAGEMENT_ADMIN_RIGHTS_HEADING}{COPY.USER_MANAGEMENT_ADMIN_RIGHTS_DESCRIPTION}
    • } +
    • {COPY.USER_MANAGEMENT_REMOVE_USER_HEADING}{ judgeTeam ? + COPY.USER_MANAGEMENT_JUDGE_TEAM_REMOVE_USER_DESCRIPTION : + COPY.USER_MANAGEMENT_REMOVE_USER_DESCRIPTION }
    • +
    +
    +

    + Filter by username or CSS ID

    +
    + handleSearchChange(value)} + onClearSearch={handleClearSearch} + value= {this.state.searchValue} + /> +
    +
    +
    + { listOfUsers.length > 0 ? ( +
      {listOfUsers}
    + ) : ( + <> +

    No results found

    +

    Please enter a valid username or CSS ID and try again.

    + + ) + }
    ; } diff --git a/client/app/queue/SelectDispositionsView.jsx b/client/app/queue/SelectDispositionsView.jsx index 6831556225c..8006ac6209e 100644 --- a/client/app/queue/SelectDispositionsView.jsx +++ b/client/app/queue/SelectDispositionsView.jsx @@ -92,7 +92,6 @@ class SelectDispositionsView extends React.PureComponent { (response) => { const { ...specialIssues } = response.body; - this.editStagedAppeal({ specialIssues }); this.setState({ specialIssues }); } ); diff --git a/client/app/queue/components/QueueFlowPage.jsx b/client/app/queue/components/QueueFlowPage.jsx index 213163e8508..ce079af1624 100644 --- a/client/app/queue/components/QueueFlowPage.jsx +++ b/client/app/queue/components/QueueFlowPage.jsx @@ -88,11 +88,7 @@ class QueueFlowPage extends React.PureComponent { } = this.props; this.props.hideModal('cancelCheckout'); - if (location.href.includes('dispositions')) { - this.props.resetDecisionOptions(); - } else { - this.props.goToNextStep(); - } + this.props.resetDecisionOptions(); _.each(stagedAppeals, this.props.checkoutStagedAppeal); this.withUnblockedTransition( diff --git a/client/app/styles/_team_management.scss b/client/app/styles/_team_management.scss index db46f830835..d5b01198ced 100644 --- a/client/app/styles/_team_management.scss +++ b/client/app/styles/_team_management.scss @@ -66,3 +66,38 @@ transform: translateY(0%); } } + +.search-bar-styling-for-filter { + padding-left: 80px; + margin-top: 20px; + margin-bottom: 15px; + justify-self: right; + display: block; +} + +.text-styling-for-filter-search-bar { + padding-left: 80px; + justify-self: flex-end; + display: flex; + margin-bottom: 5px; +} + +.no-results-found-styling { + font-size: 24px; + text-align: center; + font-weight: bold; +} + +.reenter-valid-username-styling { + padding-bottom: 60rem; + text-align: center; +} + +.make-content-centered { + justify-content: end; + width: 100%; + display: flex; + flex-direction: column; + padding-left: 55vw; + margin-right: 15vw; +} diff --git a/client/test/app/caseDistribution/reducers/levers/LeversReducer.test.js b/client/test/app/caseDistribution/reducers/levers/LeversReducer.test.js index d7c6f01cb08..ddabfc370b6 100644 --- a/client/test/app/caseDistribution/reducers/levers/LeversReducer.test.js +++ b/client/test/app/caseDistribution/reducers/levers/LeversReducer.test.js @@ -11,6 +11,7 @@ import { mockTextLeverReturn, mockDocketDistributionPriorLeversReturn, } from 'test/data/adminCaseDistributionLevers'; +import ACD_LEVERS from '../../../../../constants/ACD_LEVERS'; let mockInitialLevers = { static: mockStaticLevers, @@ -37,7 +38,6 @@ describe('Lever reducer', () => { backendLevers: [{ item: 'item1' }, { item: 'item2' }], // Sample backendLevers state leversErrors: [], isUserAcdAdmin: false, - acdExcludeFromAffinity: false }; afterEach(() => { @@ -182,9 +182,9 @@ describe('Lever reducer', () => { const action = { type: ACTIONS.UPDATE_RADIO_LEVER, payload: { - leverGroup: 'affinity', + leverGroup: ACD_LEVERS.lever_groups.affinity, leverItem: 'ama_hearing_case_affinity_days', - value: '0', + optionItem: ACD_LEVERS.value, optionValue: 0 } }; @@ -194,7 +194,9 @@ describe('Lever reducer', () => { if (lever.item === 'ama_hearing_case_affinity_days') { return { ...lever, - currentValue: 80 + value: 80, + selectedOption: ACD_LEVERS.value, + valueOptionValue: 80 }; } diff --git a/client/test/app/caseDistribution/reducers/levers/leversActions.test.js b/client/test/app/caseDistribution/reducers/levers/leversActions.test.js index 891b5a4adc6..e1989302d59 100644 --- a/client/test/app/caseDistribution/reducers/levers/leversActions.test.js +++ b/client/test/app/caseDistribution/reducers/levers/leversActions.test.js @@ -23,20 +23,6 @@ describe('levers actions', () => { expect(dispatch).toHaveBeenCalledWith(expectedAction); }); - it('should create an action to set exclude from affinity', () => { - const acdExcludeFromAffinity = true; - const expectedAction = { - type: ACTIONS.SET_ACD_EXCLUDE_FROM_AFFINITY, - payload: { acdExcludeFromAffinity } - }; - - const dispatch = jest.fn(); - - actions.loadAcdExcludeFromAffinity(acdExcludeFromAffinity)(dispatch); - - expect(dispatch).toHaveBeenCalledWith(expectedAction); - }); - it('should create an action to load levers', () => { const expectedAction = { type: ACTIONS.LOAD_LEVERS, @@ -91,14 +77,14 @@ describe('levers actions', () => { payload: { leverGroup: lever.lever_group, leverItem: lever.item, - value: option.value, - optionValue: option.text + optionItem: option.item, + optionValue: option.value } }; const dispatch = jest.fn(); - actions.updateRadioLever(lever.lever_group, lever.item, option.value, option.text)(dispatch); + actions.updateRadioLever(lever.lever_group, lever.item, option.item, option.value)(dispatch); expect(dispatch).toHaveBeenCalledWith(expectedAction); }); diff --git a/config/initializers/create_transcription_files_folder.rb b/config/initializers/create_transcription_files_folder.rb index 525d4adcd4a..934117af903 100644 --- a/config/initializers/create_transcription_files_folder.rb +++ b/config/initializers/create_transcription_files_folder.rb @@ -1,6 +1,6 @@ require 'fileutils' -FILE_TYPES = %w[mp4 mp3 vtt rtf xls csv].freeze +FILE_TYPES = %w[mp4 mp3 vtt rtf xls csv zip].freeze # Create the tmp folder with subdirectory for each file type to store transcription files FILE_TYPES.each do |file_type| diff --git a/config/initializers/scheduled_jobs.rb b/config/initializers/scheduled_jobs.rb index d378f77b62f..fe0194ae1ee 100644 --- a/config/initializers/scheduled_jobs.rb +++ b/config/initializers/scheduled_jobs.rb @@ -52,6 +52,6 @@ "process_notification_status_updates_job" => ProcessNotificationStatusUpdatesJob, "stuck_job_scheduler_job" => StuckJobSchedulerJob, "fetch_webex_recordings_list_job" => Hearings::FetchWebexRecordingsListJob, - "refresh_webex_tokens_job" => VirtualHearings::RefreshWebexAccessTokenJob, + "refresh_webex_access_tokens_job" => Hearings::RefreshWebexAccessTokenJob, "ineligible_judges_job" => IneligibleJudgesJob }.freeze diff --git a/db/migrate/20240507145931_add_columns_to_transcriptions.rb b/db/migrate/20240507145931_add_columns_to_transcriptions.rb new file mode 100644 index 00000000000..21af1359c55 --- /dev/null +++ b/db/migrate/20240507145931_add_columns_to_transcriptions.rb @@ -0,0 +1,13 @@ +class AddColumnsToTranscriptions < Caseflow::Migration + def change + add_column :transcriptions, :transcription_contractor_id, :bigint + add_column :transcriptions, :created_by_id, :bigint + add_column :transcriptions, :transcription_status, :string, comment: "Possible values: 'unassigned', 'in_transcription', 'completed', 'completed_overdue'" + add_column :transcriptions, :updated_by_id, :bigint + add_column :transcriptions, :deleted_at, :datetime, comment: "acts_as_paranoid in the model" + + add_foreign_key :transcriptions, :transcription_contractors, column: "transcription_contractor_id", validate: false + add_foreign_key :transcriptions, :users, column: "created_by_id", validate: false + add_foreign_key :transcriptions, :users, column: "updated_by_id", validate: false + end +end diff --git a/db/migrate/20240507203310_add_indexes_to_transcriptions.rb b/db/migrate/20240507203310_add_indexes_to_transcriptions.rb new file mode 100644 index 00000000000..309d0b908a4 --- /dev/null +++ b/db/migrate/20240507203310_add_indexes_to_transcriptions.rb @@ -0,0 +1,6 @@ +class AddIndexesToTranscriptions < Caseflow::Migration + def change + add_safe_index :transcriptions, [:transcription_contractor_id], name: "index_transcriptions_on_transcription_contractor_id" + add_safe_index :transcriptions, [:deleted_at], name: "index_transcriptions_on_deleted_at" + end +end diff --git a/db/schema.rb b/db/schema.rb index ace650ae92a..0d879bdcaaa 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -1920,6 +1920,8 @@ create_table "transcriptions", force: :cascade do |t| t.datetime "created_at", comment: "Automatic timestamp of when transcription was created" + t.bigint "created_by_id" + t.datetime "deleted_at", comment: "acts_as_paranoid in the model" t.date "expected_return_date", comment: "Expected date when transcription would be returned by the transcriber" t.bigint "hearing_id", comment: "Hearing ID; use as FK to hearings" t.date "problem_notice_sent_date", comment: "Date when notice of problem with recording was sent to appellant" @@ -1929,8 +1931,11 @@ t.string "task_number", comment: "Number associated with transcription" t.string "transcriber", comment: "Contractor who will transcribe the recording; i.e, 'Genesis Government Solutions, Inc.', 'Jamison Professional Services', etc" t.bigint "transcription_contractor_id" + t.string "transcription_status", comment: "Possible values: 'unassigned', 'in_transcription', 'completed', 'completed_overdue'" t.datetime "updated_at", comment: "Automatic timestamp of when transcription was updated" + t.bigint "updated_by_id" t.date "uploaded_to_vbms_date", comment: "Date when the hearing transcription was uploaded to VBMS" + t.index ["deleted_at"], name: "index_transcriptions_on_deleted_at" t.index ["hearing_id"], name: "index_transcriptions_on_hearing_id" t.index ["transcription_contractor_id"], name: "index_transcriptions_on_transcription_contractor_id" t.index ["updated_at"], name: "index_transcriptions_on_updated_at" @@ -2065,6 +2070,46 @@ t.index ["vbms_communication_package_id"], name: "index_vbms_distributions_on_vbms_communication_package_id" end + create_table "vbms_ext_claim", primary_key: "CLAIM_ID", id: :decimal, precision: 38, force: :cascade do |t| + t.string "ALLOW_POA_ACCESS", limit: 5 + t.decimal "CLAIMANT_PERSON_ID", precision: 38 + t.datetime "CLAIM_DATE" + t.string "CLAIM_SOJ", limit: 25 + t.integer "CONTENTION_COUNT" + t.datetime "CREATEDDT", null: false + t.string "EP_CODE", limit: 25 + t.datetime "ESTABLISHMENT_DATE" + t.datetime "EXPIRATIONDT" + t.string "INTAKE_SITE", limit: 25 + t.datetime "LASTUPDATEDT", null: false + t.string "LEVEL_STATUS_CODE", limit: 25 + t.datetime "LIFECYCLE_STATUS_CHANGE_DATE" + t.string "LIFECYCLE_STATUS_NAME", limit: 50 + t.string "ORGANIZATION_NAME", limit: 100 + t.string "ORGANIZATION_SOJ", limit: 25 + t.string "PAYEE_CODE", limit: 25 + t.string "POA_CODE", limit: 25 + t.integer "PREVENT_AUDIT_TRIG", limit: 2, default: 0, null: false + t.string "PRE_DISCHARGE_IND", limit: 5 + t.string "PRE_DISCHARGE_TYPE_CODE", limit: 10 + t.string "PRIORITY", limit: 10 + t.string "PROGRAM_TYPE_CODE", limit: 10 + t.string "RATING_SOJ", limit: 25 + t.string "SERVICE_TYPE_CODE", limit: 10 + t.string "SUBMITTER_APPLICATION_CODE", limit: 25 + t.string "SUBMITTER_ROLE_CODE", limit: 25 + t.datetime "SUSPENSE_DATE" + t.string "SUSPENSE_REASON_CODE", limit: 25 + t.string "SUSPENSE_REASON_COMMENTS", limit: 1000 + t.decimal "SYNC_ID", precision: 38, null: false + t.string "TEMPORARY_CLAIM_SOJ", limit: 25 + t.string "TYPE_CODE", limit: 25 + t.decimal "VERSION", precision: 38, null: false + t.decimal "VETERAN_PERSON_ID", precision: 15 + t.index ["CLAIM_ID"], name: "claim_id_index" + t.index ["LEVEL_STATUS_CODE"], name: "level_status_code_index" + end + create_table "vbms_uploaded_documents", force: :cascade do |t| t.bigint "appeal_id", comment: "Appeal/LegacyAppeal ID; use as FK to appeals/legacy_appeals" t.string "appeal_type", comment: "'Appeal' or 'LegacyAppeal'" @@ -2346,6 +2391,9 @@ add_foreign_key "tasks", "users", column: "assigned_by_id" add_foreign_key "tasks", "users", column: "cancelled_by_id" add_foreign_key "transcriptions", "hearings" + add_foreign_key "transcriptions", "transcription_contractors" + add_foreign_key "transcriptions", "users", column: "created_by_id" + add_foreign_key "transcriptions", "users", column: "updated_by_id" add_foreign_key "unrecognized_appellants", "claimants" add_foreign_key "unrecognized_appellants", "not_listed_power_of_attorneys" add_foreign_key "unrecognized_appellants", "unrecognized_appellants", column: "current_version_id" diff --git a/db/seeds/mst_pact_legacy_case_appeals.rb b/db/seeds/mst_pact_legacy_case_appeals.rb index 081a1dc4738..af3e41a3afe 100644 --- a/db/seeds/mst_pact_legacy_case_appeals.rb +++ b/db/seeds/mst_pact_legacy_case_appeals.rb @@ -18,6 +18,7 @@ def seed! end # confirms the user CSS IDS are for valid users or skip + # seeds come from seed_legacy_appeals.rake # :reek:UtilityFunction def generate_legacy_appeals USER_CSS_IDS.each do |id| diff --git a/lib/caseflow/error.rb b/lib/caseflow/error.rb index 60f455b6a93..bcee413e964 100644 --- a/lib/caseflow/error.rb +++ b/lib/caseflow/error.rb @@ -426,6 +426,7 @@ class PexipApiError < ConferenceCreationError; end class PexipNotFoundError < PexipApiError; end class PexipBadRequestError < PexipApiError; end class PexipMethodNotAllowedError < PexipApiError; end + class PexipServiceNotReachableError < PexipApiError; end class WebexApiError < ConferenceCreationError; end class WebexNotFoundError < WebexApiError; end diff --git a/lib/fakes/webex_service.rb b/lib/fakes/webex_service.rb index 3c0c0ca8ea1..10651555d24 100644 --- a/lib/fakes/webex_service.rb +++ b/lib/fakes/webex_service.rb @@ -69,7 +69,7 @@ def fetch_recordings_list ) end - def fetch_recording_details + def fetch_recording_details(recording_id) if error? return ExternalApi::WebexService::RecordingDetailsResponse.new( HTTPI::Response.new(@status_code, {}, error_response) @@ -205,7 +205,7 @@ def fake_recording_details_data "temporaryDirectDownloadLinks": { "recordingDownloadLink": "https://www.learningcontainer.com/mp4-sample-video-files-download/#", "audioDownloadLink": "https://freetestdata.com/audio-files/mp3/", - "transcriptionDownloadLink": "https://www.capsubservices.com/assets/downloads/web/WebVTT.vtt", + "transcriptDownloadLink": "https://www.capsubservices.com/assets/downloads/web/WebVTT.vtt", "expiration": "2022-05-01T10:30:25Z" }, "format": "ARF", diff --git a/lib/helpers/claim_label_change.rb b/lib/helpers/claim_label_change.rb index 6f3f535a303..cd9fefbbdb2 100644 --- a/lib/helpers/claim_label_change.rb +++ b/lib/helpers/claim_label_change.rb @@ -93,12 +93,12 @@ def claim_label_updater(reference_id, original_code, new_code) update_vbms(epe, original_code, new_code) end end - end - def validate_claim_code(claim_code, error_message) - unless claim_code_check(claim_code) - puts(error_message) - fail Interrupt + def validate_claim_code(claim_code, error_message) + unless claim_code_check(claim_code) + puts(error_message) + fail Interrupt + end end end end diff --git a/lib/tasks/additional_legacy_remanded_appeals.rake b/lib/tasks/additional_legacy_remanded_appeals.rake index 77d3271cbef..ed00c4237d6 100644 --- a/lib/tasks/additional_legacy_remanded_appeals.rake +++ b/lib/tasks/additional_legacy_remanded_appeals.rake @@ -228,7 +228,6 @@ namespace :additional_legacy_remand_reasons do else veterans_with_like_45_appeals = %w[011899917 011899918] # UAT option for veterans - end # set task to ATTORNEYTASK diff --git a/lib/tasks/seed_legacy_appeals.rake b/lib/tasks/seed_legacy_appeals.rake index 7b7805f5417..d0d3510cbbd 100644 --- a/lib/tasks/seed_legacy_appeals.rake +++ b/lib/tasks/seed_legacy_appeals.rake @@ -124,8 +124,6 @@ namespace :db do else veterans_with_like_45_appeals = %w[011899917 011899918] - - # veterans_with_250_appeals = %w[011899906 011899999] end # request CSS ID for task assignment if not given @@ -147,9 +145,6 @@ namespace :db do docket_number += 1 LegacyAppealFactory.stamp_out_legacy_appeals(5, file_number, user, docket_number) end - # veterans_with_250_appeals.each do |file_number| - # LegacyAppealFactory.stamp_out_legacy_appeals(250, file_number, user) - # end end end end diff --git a/spec/feature/hearings/hearing_details_spec.rb b/spec/feature/hearings/hearing_details_spec.rb index 62b63d1a6a1..44eeb97686f 100644 --- a/spec/feature/hearings/hearing_details_spec.rb +++ b/spec/feature/hearings/hearing_details_spec.rb @@ -118,17 +118,17 @@ def check_pexip_hearings_links(link, link_alias, disable_link = false) def check_webex_hearings_links(link, disable_link = false) # Confirm that the host hearing link details exist within "#vlj-hearings-link" do - find("div", text: link.host_link) + find("div").should have_content(link.host_link) ensure_link_present(link.host_link, disable_link) end # Confirm that the co-host hearing link details exist within "#hc-hearings-link" do - find("div", text: link.co_host_link) + find("div").should have_content(link.co_host_link) ensure_link_present(link.co_host_link, disable_link) end # Confirm that the guest hearing link details exist within "#guest-hearings-link" do - find("div", text: link.guest_link) + find("div").should have_content(link.guest_link) ensure_link_present(link.guest_link, disable_link) end end @@ -372,7 +372,7 @@ def wait_for_download(file_location) ) end - # include_examples "always updatable fields" + include_examples "always updatable fields" context "User switches hearing type from Virtual back to original type" do let!(:virtual_hearing) do @@ -476,14 +476,9 @@ def wait_for_download(file_location) end context "when hearing conference type is webex" do - let!(:hearing) { create(:hearing, :with_webex_non_virtual_conference_link) } - before { hearing.meeting_type.update(service_name: "webex") } scenario "links display correctly" do - FeatureToggle.enable!(:pexip_conference_service) - hearing.update(hearing_day_id: hearing_day.id) - visit "hearings/" + hearing.external_id.to_s + "/details" click_dropdown(name: "hearingType", index: 0) diff --git a/spec/feature/queue/user_organization_spec.rb b/spec/feature/queue/user_organization_spec.rb index 14f0a41d17e..d144870e093 100644 --- a/spec/feature/queue/user_organization_spec.rb +++ b/spec/feature/queue/user_organization_spec.rb @@ -66,7 +66,7 @@ end context "when there are many users in the organization" do - let(:other_org_user) { create(:user) } + let(:other_org_user) { create(:user, full_name: "Inego Montoya") } before do organization.add_user(other_org_user) end @@ -87,6 +87,27 @@ page.assert_selector("button", text: COPY::USER_MANAGEMENT_GIVE_USER_ADMIN_RIGHTS_BUTTON_TEXT, count: 1) expect(organization.user_is_admin?(other_org_user)).to eq(false) end + + it "allows the admin to search for users in the organization by their names" do + visit(organization.user_admin_path) + fill_in("searchBar", with: other_org_user.full_name) + expect(page).to have_content(other_org_user.full_name) + expect(page).to_not have_content(user_with_role.full_name) + end + + it "allows the admin to search for users in the organization by their css id" do + visit(organization.user_admin_path) + fill_in("searchBar", with: other_org_user.css_id) + expect(page).to have_content(other_org_user.css_id) + expect(page).to_not have_content(user_with_role.css_id) + end + + it "displays a message if no users are found" do + visit(organization.user_admin_path) + fill_in("searchBar", with: "you killed my father, prepare to die") + expect(page).to have_content("No results found") + expect(page).to have_content("Please enter a valid username or CSS ID and try again.") + end end context "the user is in a judge team" do diff --git a/spec/jobs/batch_processes/batch_process_rescue_job_spec.rb b/spec/jobs/batch_processes/batch_process_rescue_job_spec.rb index 3469ce51e2d..ec142467d75 100644 --- a/spec/jobs/batch_processes/batch_process_rescue_job_spec.rb +++ b/spec/jobs/batch_processes/batch_process_rescue_job_spec.rb @@ -7,7 +7,7 @@ before do Timecop.freeze(Time.utc(2022, 1, 1, 12, 0, 0)) - allow(SlackService).to receive(:new).with(url: anything).and_return(slack_service) + allow(SlackService).to receive(:new).and_return(slack_service) allow(slack_service).to receive(:send_notification) { |_, first_arg| @slack_msg = first_arg } end diff --git a/spec/jobs/batch_processes/priority_ep_sync_batch_process_job_spec.rb b/spec/jobs/batch_processes/priority_ep_sync_batch_process_job_spec.rb index 8d00d31b9d2..c633c195e92 100644 --- a/spec/jobs/batch_processes/priority_ep_sync_batch_process_job_spec.rb +++ b/spec/jobs/batch_processes/priority_ep_sync_batch_process_job_spec.rb @@ -9,7 +9,7 @@ let(:slack_service) { SlackService.new(url: "http://www.example.com") } before do - allow(SlackService).to receive(:new).with(url: anything).and_return(slack_service) + allow(SlackService).to receive(:new).and_return(slack_service) allow(slack_service).to receive(:send_notification) { |_, first_arg| @slack_msg = first_arg } end diff --git a/spec/jobs/hearings/download_transcription_file_job_spec.rb b/spec/jobs/hearings/download_transcription_file_job_spec.rb index 93da55ad60c..a2e39223b9b 100644 --- a/spec/jobs/hearings/download_transcription_file_job_spec.rb +++ b/spec/jobs/hearings/download_transcription_file_job_spec.rb @@ -233,11 +233,23 @@ allow_any_instance_of(TranscriptionFile).to receive(:clean_up_tmp_location).and_return(nil) end - shared_examples "sends correct email template" do - it "mailer receives correct params" do - expect(TranscriptionFileIssuesMailer).to receive(:issue_notification) - .with(error_details) - perform_enqueued_jobs { described_class.perform_later(download_link: link, file_name: file_name) } + shared_examples "sends email template" do + context "email delivery succeeds" do + it "mailer receives correct params" do + allow(TranscriptionFileIssuesMailer).to receive(:issue_notification).and_call_original + expect(TranscriptionFileIssuesMailer).to receive(:issue_notification).with(error_details) + expect_any_instance_of(described_class).to receive(:log_error).once + perform_enqueued_jobs { described_class.perform_later(download_link: link, file_name: file_name) } + end + end + + context "email delivery fails" do + it "captures external delivery error" do + allow(TranscriptionFileIssuesMailer).to receive(:issue_notification).with(error_details) + .and_raise(GovDelivery::TMS::Request::Error.new(500)) + expect_any_instance_of(described_class).to receive(:log_error).twice + perform_enqueued_jobs { described_class.perform_later(download_link: link, file_name: file_name) } + end end end @@ -256,7 +268,7 @@ .and_raise(upload_error) end - include_examples "sends correct email template" + include_examples "sends email template" end context "failed download" do @@ -275,7 +287,7 @@ .and_raise(download_error) end - include_examples "sends correct email template" + include_examples "sends email template" end context "failed conversion" do @@ -292,7 +304,7 @@ .and_raise(conversion_error) end - include_examples "sends correct email template" + include_examples "sends email template" end context "failed to parse filename" do @@ -313,7 +325,7 @@ .and_raise(file_name_error) end - include_examples "sends correct email template" + include_examples "sends email template" end end end diff --git a/spec/jobs/hearings/fetch_webex_recordings_details_job_spec.rb b/spec/jobs/hearings/fetch_webex_recordings_details_job_spec.rb index 1826e6d5743..5b2f9c27b95 100644 --- a/spec/jobs/hearings/fetch_webex_recordings_details_job_spec.rb +++ b/spec/jobs/hearings/fetch_webex_recordings_details_job_spec.rb @@ -10,10 +10,13 @@ let(:mp4_file_name) { "180000304_1_LegacyHearing-1.mp4" } let(:vtt_file_name) { "180000304_1_LegacyHearing-1.vtt" } let(:mp3_file_name) { "180000304_1_LegacyHearing-1.mp3" } - let(:hearing) { create(:hearing) } - let(:file_name) { "#{hearing.docket_number}_#{hearing.id}_#{hearing.class}" } + let(:access_token) { "sample_#{Rails.deploy_env}_token" } - subject { described_class.perform_now(id: id, file_name: file_name) } + subject { described_class.perform_now(id: id) } + + before do + allow(CredStash).to receive(:get).with("webex_#{Rails.deploy_env}_access_token").and_return(access_token) + end context "method testing" do before do @@ -22,6 +25,12 @@ .and_return(nil) end + it "Uses correct api key for correct environment" do + allow(WebexService).to receive(:new).and_call_original + expect(WebexService).to receive(:new).with(hash_including(apikey: access_token)) + subject + end + it "hits the webex API and returns recording details" do get_details = Hearings::FetchWebexRecordingsDetailsJob.new run = get_details.send(:fetch_recording_details, id) @@ -45,10 +54,22 @@ end context "job errors" do + let(:exception) { Caseflow::Error::WebexApiError.new(code: 400, message: "Fake Error") } + let(:error_details) do + { + error: { type: "retrieval", explanation: "retrieve recording details from Webex" }, + provider: "webex", + recording_id: id, + api_call: "GET #{ENV['WEBEX_HOST_MAIN']}#{ENV['WEBEX_DOMAIN_MAIN']}#{ENV['WEBEX_API_MAIN']}/#{id}", + response: { status: exception.code, message: exception.message }.to_json, + docket_number: nil + } + end + before do allow_any_instance_of(WebexService) .to receive(:fetch_recording_details) - .and_raise(Caseflow::Error::WebexApiError.new(code: 400, message: "Fake Error")) + .and_raise(exception) end it "Successfully catches errors and adds to retry queue" do @@ -59,7 +80,24 @@ it "retries and logs errors" do subject expect(Rails.logger).to receive(:error).at_least(:once) - perform_enqueued_jobs { described_class.perform_later(id: id, file_name: file_name) } + perform_enqueued_jobs { described_class.perform_later(id: id) } + end + + it "mailer receives correct params" do + allow(TranscriptionFileIssuesMailer).to receive(:issue_notification).and_call_original + expect(TranscriptionFileIssuesMailer).to receive(:issue_notification) + .with(error_details) + expect_any_instance_of(described_class).to receive(:log_error).once + perform_enqueued_jobs { described_class.perform_later(id: id) } + end + + context "mailer fails to send email" do + it "captures external delivery error" do + allow(TranscriptionFileIssuesMailer).to receive(:issue_notification).with(error_details) + .and_raise(GovDelivery::TMS::Request::Error.new(500)) + expect_any_instance_of(described_class).to receive(:log_error).twice + perform_enqueued_jobs { described_class.perform_later(id: id) } + end end end end diff --git a/spec/jobs/hearings/fetch_webex_recordings_list_job_spec.rb b/spec/jobs/hearings/fetch_webex_recordings_list_job_spec.rb index e8970091297..c989a319868 100644 --- a/spec/jobs/hearings/fetch_webex_recordings_list_job_spec.rb +++ b/spec/jobs/hearings/fetch_webex_recordings_list_job_spec.rb @@ -2,25 +2,61 @@ describe Hearings::FetchWebexRecordingsListJob, type: :job do include ActiveJob::TestHelper + let(:access_token) { "sample_#{Rails.deploy_env}_token" } + let(:from_param) { 2.hours.ago.in_time_zone("America/New_York").beginning_of_hour } + let(:to_param) { 1.hour.ago.in_time_zone("America/New_York").beginning_of_hour } subject { described_class.perform_now } - it "Returns the correct array of ids" do - allow_any_instance_of(Hearings::DownloadTranscriptionFileJob) - .to receive(:perform) - .and_return(nil) - expect(subject).to eq(%w[ - 4f914b1dfe3c4d11a61730f18c0f5387 - 3324fb76946249cfa07fc30b3ccbf580 - 42b80117a2a74dcf9863bf06264f8075 - ]) + before do + allow(CredStash).to receive(:get).with("webex_#{Rails.deploy_env}_access_token").and_return(access_token) + end + + context "job success" do + before do + allow_any_instance_of(Hearings::DownloadTranscriptionFileJob) + .to receive(:perform) + .and_return(nil) + end + + it "Returns the correct array of ids" do + expect(subject).to eq(%w[ + 4f914b1dfe3c4d11a61730f18c0f5387 + 3324fb76946249cfa07fc30b3ccbf580 + 42b80117a2a74dcf9863bf06264f8075 + ]) + end + + it "Uses correct api key for correct environment" do + allow(WebexService).to receive(:new).and_call_original + expect(WebexService).to receive(:new).with(hash_including(apikey: access_token)) + subject + end + + it "Uses correctly formatted to and from query parameters" do + allow(WebexService).to receive(:new).and_call_original + expect(WebexService).to receive(:new) + .with(hash_including(query: { to: to_param.iso8601, from: from_param.iso8601, max: 100 })) + subject + end end context "job errors" do + let(:exception) { Caseflow::Error::WebexApiError.new(code: 400, message: "Fake Error") } + let(:query) { "?max=100?from=#{CGI.escape(from_param.iso8601)}?to=#{CGI.escape(to_param.iso8601)}" } + let(:error_details) do + { + error: { type: "retrieval", explanation: "retrieve a list of recordings from Webex" }, + provider: "webex", + api_call: "GET #{ENV['WEBEX_HOST_MAIN']}#{ENV['WEBEX_DOMAIN_MAIN']}#{ENV['WEBEX_API_MAIN']}#{query}", + response: { status: exception.code, message: exception.message }.to_json, + times: { from: from_param, to: to_param }, + docket_number: nil + } + end + before do - allow_any_instance_of(WebexService) - .to receive(:fetch_recordings_list) - .and_raise(Caseflow::Error::WebexApiError.new(code: 400, message: "Fake Error")) + allow_any_instance_of(WebexService).to receive(:fetch_recordings_list).and_raise(exception) end it "Successfully catches errors and adds retry to queue" do @@ -33,5 +69,22 @@ expect(Rails.logger).to receive(:error).at_least(:once) perform_enqueued_jobs { described_class.perform_later } end + + it "mailer receives correct params" do + allow(TranscriptionFileIssuesMailer).to receive(:issue_notification).and_call_original + expect(TranscriptionFileIssuesMailer).to receive(:issue_notification) + .with(error_details) + expect_any_instance_of(described_class).to receive(:log_error).once + perform_enqueued_jobs { described_class.perform_later } + end + + context "mailer fails to send email" do + it "captures external delivery error" do + allow(TranscriptionFileIssuesMailer).to receive(:issue_notification).with(error_details) + .and_raise(GovDelivery::TMS::Request::Error.new(500)) + expect_any_instance_of(described_class).to receive(:log_error).twice + perform_enqueued_jobs { described_class.perform_later } + end + end end end diff --git a/spec/jobs/virtual_hearings/refresh_webex_access_token_job_spec.rb b/spec/jobs/hearings/refresh_webex_access_token_job_spec.rb similarity index 94% rename from spec/jobs/virtual_hearings/refresh_webex_access_token_job_spec.rb rename to spec/jobs/hearings/refresh_webex_access_token_job_spec.rb index 9a580793de7..415f987feed 100644 --- a/spec/jobs/virtual_hearings/refresh_webex_access_token_job_spec.rb +++ b/spec/jobs/hearings/refresh_webex_access_token_job_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -RSpec.describe VirtualHearings::RefreshWebexAccessTokenJob, type: :job do +RSpec.describe Hearings::RefreshWebexAccessTokenJob, type: :job do let(:new_access_token) { "token1" } let(:new_refresh_token) { "token2" } diff --git a/spec/jobs/hearings/work_order_file_job_spec.rb b/spec/jobs/hearings/work_order_file_job_spec.rb new file mode 100644 index 00000000000..0a362d69900 --- /dev/null +++ b/spec/jobs/hearings/work_order_file_job_spec.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +describe Hearings::WorkOrderFileJob, type: :job do + include ActiveJob::TestHelper + + let(:hearing) { create(:hearing) } + let(:work_order) do + { + work_order_name: "#12347767", + return_date: "02/12/2024", + contractor: "Contractor Name", + hearings: [{ hearing_id: hearing.id, hearing_type: hearing.class.to_s }] + } + end + let(:file_path) { Rails.root.join("tmp/transcription_files/xls/#{work_order[:work_order_name]}.xls") } + + subject { described_class.perform_now(work_order) } + + def cleanup_tmp_file + File.delete(file_path) if File.exist?(file_path) + end + + it "temporarily saves a xls file in the work order" do + allow_any_instance_of(described_class).to receive(:cleanup_tmp_file).and_return(nil) + expect(File.exist?(file_path)).to eq false + subject + expect(File.exist?(file_path)).to eq true + cleanup_tmp_file + end + + describe "Excel file content" do + let(:work_order_file) { Spreadsheet.open(file_path) } + before do + allow_any_instance_of(described_class).to receive(:cleanup_tmp_file).and_return(nil) + subject + work_order_file + end + + after do + cleanup_tmp_file + end + + it "reads the data correctly" do + expect(work_order_file.worksheet(0).row(0)).to eq(["Work Order", work_order[:work_order_name]]) + expect(work_order_file.worksheet(0).row(2)).to eq(["Return Date", work_order[:return_date]]) + expect(work_order_file.worksheet(0).row(4)).to eq(["Contractor Name", work_order[:contractor]]) + end + + it "reads the table header correctly" do + expect(work_order_file.worksheet(0).row(6)).to eq([ + "DOCKET NUMBER", + "FIRST NAME", + "LAST NAME", + "TYPES", + "HEARING DATE", + "RO", + "VLJ", + "APPEAL TYPE" + ]) + end + + it "reads the table data rows correctly" do + expect(work_order_file.worksheet(0).row(7)).to eq([ + hearing.appeal.docket_number, + hearing.appellant_first_name, + hearing.appellant_last_name, + hearing.appeal.type, + hearing.appeal.hearing_day_if_schedueled.strftime("%m/%d/%Y"), + hearing.regional_office.name, + hearing.judge.full_name, + "AMA" + ]) + end + end + + describe "Upload to S3" do + before do + hearing + end + it "should upload the file to S3 bucket" do + expect(S3Service).to receive(:store_file).with( + "vaec-appeals-caseflow-test/transcript_text/#{work_order[:work_order_name]}.xls", + file_path, + :filepath + ) + expect(subject).to eq true + end + + it "should retry and send notification on S3 upload failure" do + expect(S3Service).to receive(:store_file).exactly(5).times.and_raise(StandardError) + expect(WorkOrderFileIssuesMailer).to receive(:send_notification).once + perform_enqueued_jobs { subject } + end + end +end diff --git a/spec/jobs/hearings/zip_and_upload_transcription_files_job_spec.rb b/spec/jobs/hearings/zip_and_upload_transcription_files_job_spec.rb new file mode 100644 index 00000000000..97be544e9b7 --- /dev/null +++ b/spec/jobs/hearings/zip_and_upload_transcription_files_job_spec.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +RSpec.describe Hearings::ZipAndUploadTranscriptionFilesJob do + include ActiveJob::TestHelper + + def hearings_in_work_order(hearings) + hearings.map { |hearing| { hearing_id: hearing.id, hearing_type: hearing.class.to_s } } + end + + def cleanup_tmp_directories + %w(mp3 rtf zip).each do |directory| + dir = Dir.new("tmp/transcription_files/#{directory}") + dir.each_child { |file_name| File.delete(dir.path + "/" + file_name) } + end + end + + let(:hearings) { (1..5).map { create(:hearing, :with_transcription_files) } } + let(:legacy_hearings) { (1..5).map { create(:legacy_hearing, :with_transcription_files) } } + + subject { described_class.perform_now(hearings_in_work_order(hearings + legacy_hearings)) } + + before { User.authenticate!(user: create(:user)) } + + after { cleanup_tmp_directories } + + it "temporarily downloads mp3s and rtfs for all the hearings in a work order" do + allow_any_instance_of(described_class).to receive(:cleanup_tmp_files).and_return(nil) + + %w(mp3 rtf).each do |directory| + expect(Dir.empty?("tmp/transcription_files/#{directory}")).to eq(true) + end + + subject + + %w(mp3 rtf).each do |directory| + dir = Dir.new("tmp/transcription_files/#{directory}") + expect(Dir.empty?(dir.path)).to eq(false) + dir.each_child do |file_name| + expect(File.exist?(dir.path + "/" + file_name)).to eq true + end + end + end + + it "temporarily saves a zip file for each hearing in the work order" do + allow_any_instance_of(described_class).to receive(:cleanup_tmp_files).and_return(nil) + + expect(Dir.empty?("tmp/transcription_files/zip")).to eq true + + subject + + dir = Dir.new("tmp/transcription_files/zip") + expect(dir.children.count).to eq(hearings.count + legacy_hearings.count) + dir.each_child do |file_name| + full_path = dir.path + "/" + file_name + expect(File.exist?(full_path)).to eq true + expect(File.extname(full_path)).to eq ".zip" + end + end + + it "creates a transcription file record for each zip file" do + expect(TranscriptionFile.where(file_type: "zip").empty?).to eq true + + subject + + TranscriptionFile.where(file_type: "zip").each do |file| + expect(file.file_name).to be_a String + expect(file.date_upload_aws).to be_a Time + expect(file.created_by_id).to be_a Integer + expect(file.aws_link).to be_a String + end + end + + it "uploads the zip file to s3 bucket" do + subject + + TranscriptionFile.where(file_type: "zip").pluck("aws_link").each do |link| + expect(link.include?("vaec-appeals-caseflow-test/transcript_text")).to eq true + end + end + + context "fails to upload to s3" do + it "retries on failure to upload to s3" do + allow_any_instance_of(described_class).to receive(:perform) + .and_raise(TranscriptionFileUpload::FileUploadError) + + expect { perform_enqueued_jobs { subject } }.to raise_error( + Hearings::ZipAndUploadTranscriptionFilesJob::ZipFileUploadError + ) + end + end +end diff --git a/spec/mailers/hearing_mailer_spec.rb b/spec/mailers/hearing_mailer_spec.rb index eba5c3f9feb..12fc6e8af0c 100644 --- a/spec/mailers/hearing_mailer_spec.rb +++ b/spec/mailers/hearing_mailer_spec.rb @@ -195,6 +195,7 @@ end it "Webex test link appears" do + allow(virtual_hearing).to receive(:guest_link).and_return("linK") expect(subject.html_part.body).to include("https://instant-usgov.webex.com/mediatest") end end @@ -660,7 +661,9 @@ ) end - include_context "test link for a webex conference" + context "webex conference" do + include_context "test link for a webex conference" + end end context "regional office is in eastern timezone" do @@ -741,7 +744,9 @@ ) end - include_context "test link for a webex conference" + context "webex conference" do + include_context "test link for a webex conference" + end end context "regional office is in eastern timezone" do @@ -1223,7 +1228,9 @@ ) end - include_context "test link for a webex conference" + context "webex conference" do + include_context "test link for a webex conference" + end end context "regional office is in eastern timezone" do diff --git a/spec/models/concerns/by_docket_date_distribution_spec.rb b/spec/models/concerns/by_docket_date_distribution_spec.rb index b56f7da8e86..fb7c9f8a1ca 100644 --- a/spec/models/concerns/by_docket_date_distribution_spec.rb +++ b/spec/models/concerns/by_docket_date_distribution_spec.rb @@ -211,5 +211,27 @@ def add_dates_to_date_array(num) expect(nonpriority_stats).to include(sym) end end + + context "handles errors without stopping a distribution" do + let(:appeal) { create(:appeal) } + + before do + @new_acd.instance_variable_set(:@appeals, [appeal, nil]) + Rails.cache.fetch("case_distribution_ineligible_judges") { [{ sattyid: "1", id: "1" }] } + end + + it "#ama_distributed_cases_tied_to_ineligible_judges raises an error if passed nil in array" do + expect { @new_acd.send(:ama_distributed_cases_tied_to_ineligible_judges) }.to raise_error(NoMethodError) + end + + it "#distributed_cases_tied_to_ineligible_judges raises an error if passed nil in array" do + expect { @new_acd.send(:distributed_cases_tied_to_ineligible_judges) }.to raise_error(NoMethodError) + end + + it "ama_statistics handles the errors from #ama_distributed_cases_tied_to_ineligible_judges + and #distributed_cases_tied_to_ineligible_judges" do + expect { @new_acd.send(:ama_statistics) }.not_to raise_error + end + end end end diff --git a/spec/models/concerns/distribution_concern_spec.rb b/spec/models/concerns/distribution_concern_spec.rb index 1c8aceb5f00..819f0976309 100644 --- a/spec/models/concerns/distribution_concern_spec.rb +++ b/spec/models/concerns/distribution_concern_spec.rb @@ -30,62 +30,55 @@ class DistributionConcernTestClass subject { @concern_test_class } - context "for appeals with an open distribution task" do - context "if can_redistribute_appeal? is true" do - let!(:appeals) { [appeal_open_dist_task, appeal_open_dist_and_non_blocking_task] } + context "for appeals with no open distribution task" do + let!(:appeals) { [appeal_no_open_dist_task] } - it "a JudgeAssignTask is created" do - result = subject.send :assign_judge_tasks_for_appeals, appeals, judge + it "appeals are skipped and return nil " do + result = subject.send :assign_judge_tasks_for_appeals, appeals, judge - expect(result[0].is_a?(JudgeAssignTask)).to be true - expect(result[1].is_a?(JudgeAssignTask)).to be true - end + expect(result.first).to be nil end end - context "for appeals with no open distribution task" do - context "if can_redistribute_appeal? is true" do - let!(:appeals) { [appeal_no_open_dist_task] } + context "for appeals with an open distribution task" do + context "when an appeal has another open task in allowable list" do + let!(:appeals) do + appeal = create(:appeal, :direct_review_docket, :ready_for_distribution) - it "appeals are skipped and return nil " do - result = subject.send :assign_judge_tasks_for_appeals, appeals, judge + VeteranRecordRequest.create!(appeal: appeal, parent: appeal.root_task, + status: Constants.TASK_STATUSES.assigned, assigned_to: create(:field_vso)) - expect(result.first).to be nil + [appeal.reload] end - end - end - context "when an appeal has an assigned DistributionTask and open VeteranRecordRequest task" do - let!(:appeals) do - appeal = create(:appeal, :direct_review_docket, :ready_for_distribution) + it "a JudgeAssignTask is created and no slack notification is sent" do + expect_any_instance_of(SlackService).not_to receive(:send_notification) - VeteranRecordRequest.create!(appeal: appeal, parent: appeal.root_task, - status: Constants.TASK_STATUSES.assigned, assigned_to: create(:field_vso)) + result = subject.send :assign_judge_tasks_for_appeals, appeals, judge - [appeal.reload] + expect(result[0].is_a?(JudgeAssignTask)).to be true + end end - it "a JudgeAssignTask is created" do - result = subject.send :assign_judge_tasks_for_appeals, appeals, judge - - expect(result[0].is_a?(JudgeAssignTask)).to be true - end - end + context "when an appeal has another open task not in allowable list" do + let!(:appeals) do + appeal = create(:appeal, :direct_review_docket, :ready_for_distribution) - context "when an appeal has an assigned DistributionTask and open QualityReviewTask task" do - let!(:appeals) do - appeal = create(:appeal, :direct_review_docket, :ready_for_distribution) + VeteranRecordRequest.create!(appeal: appeal, parent: appeal.root_task, + status: Constants.TASK_STATUSES.assigned, assigned_to: create(:field_vso)) + QualityReviewTask.create!(appeal: appeal, parent: appeal.root_task, + status: Constants.TASK_STATUSES.assigned, assigned_to: QualityReview.singleton) - QualityReviewTask.create!(appeal: appeal, parent: appeal.root_task, - status: Constants.TASK_STATUSES.assigned, assigned_to: QualityReview.singleton) + [appeal.reload] + end - [appeal.reload] - end + it "a JudgeAssignTask is created and slack notification is sent" do + expect_any_instance_of(SlackService).to receive(:send_notification).exactly(1).times - it "a JudgeAssignTask is created" do - result = subject.send :assign_judge_tasks_for_appeals, appeals, judge + result = subject.send :assign_judge_tasks_for_appeals, appeals, judge - expect(result[0].is_a?(JudgeAssignTask)).to be true + expect(result[0].is_a?(JudgeAssignTask)).to be true + end end end end diff --git a/spec/models/distribution_spec.rb b/spec/models/distribution_spec.rb index 5fc1856c57c..6711f7653a8 100644 --- a/spec/models/distribution_spec.rb +++ b/spec/models/distribution_spec.rb @@ -163,8 +163,9 @@ new_distribution.distribute! end - it "updates status to error if an error is thrown" do + it "updates status to error if an error is thrown and sends slack notification" do allow_any_instance_of(LegacyDocket).to receive(:distribute_appeals).and_raise(StandardError) + expect_any_instance_of(SlackService).to receive(:send_notification).exactly(1).times expect { new_distribution.distribute! }.to raise_error(StandardError) diff --git a/spec/models/hearings/transcription_contractors_model_spec.rb b/spec/models/hearings/transcription_contractors_model_spec.rb deleted file mode 100644 index 334836c8737..00000000000 --- a/spec/models/hearings/transcription_contractors_model_spec.rb +++ /dev/null @@ -1,55 +0,0 @@ -# frozen_string_literal: true - -require "rails_helper" - -RSpec.describe TranscriptionContractor, type: :model do - subject(:transcription_contractor) do - described_class.new( - name: "Genesis Government Solutions, Inc.", - directory: "BVA Hearing Transcripts/Genesis Government Solutions, Inc.", - poc: "Example POC", - phone: "888-888-8888", - email: "test_email@bah.com" - ) - end - - describe "validations" do - it { is_expected.to validate_presence_of(:name) } - it { is_expected.to validate_presence_of(:directory) } - end - - describe "default values" do - it "has correct default values" do - expect(transcription_contractor.is_available_for_work).to eq(false) - expect(transcription_contractor.previous_goal).to eq(0) - expect(transcription_contractor.current_goal).to eq(0) - expect(transcription_contractor.inactive).to eq(false) - end - end - - describe ".all_contractors" do - let!(:contractors) do - [ - { name: "Genesis Government Solutions, Inc.", - directory: "BVA Hearing Transcripts/Genesis Government Solutions, Inc.", - email: "email_1@test.com", - phone: "888-888-8888", - poc: "Example POC" }, - { name: "Jamison Professional Services", - directory: "BVA Hearing Transcripts/Jamison Professional Services", - email: "email_2@test.com", - phone: "888-888-8888", - poc: "Example POC" }, - { name: "The Ravens Group, Inc.", - directory: "BVA Hearing Transcripts/The Ravens Group, Inc.", - email: "email_3@test.com", - phone: "888-888-8888", - poc: "Example POC" } - ].map { |attrs| described_class.create!(attrs) } - end - - it "returns all contractors" do - expect(described_class.all_contractors).to contain_exactly(*contractors) - end - end -end diff --git a/spec/models/quality_review_case_selector_spec.rb b/spec/models/quality_review_case_selector_spec.rb index 1e0f1841118..ac06f9d7a23 100644 --- a/spec/models/quality_review_case_selector_spec.rb +++ b/spec/models/quality_review_case_selector_spec.rb @@ -4,22 +4,27 @@ describe ".reached_monthly_limit_in_quality_reviews?" do subject { QualityReviewCaseSelector.reached_monthly_limit_in_quality_reviews? } context "when a realistic number of cases are completed" do - # Pulled from prod with: + # Originally pulled from prod with: # Task.where( # appeal_type: Appeal.name, # assigned_to_type: Organization.name, # type: [QualityReviewTask.name, BvaDispatchTask.name], # created_at: 2.month.ago.beginning_of_month..2.month.ago.end_of_month # ).count - let(:complete_cases_count) { 1600 } - let!(:qr_tasks) do - complete_cases_count.times do - create(:qr_task) if QualityReviewCaseSelector.select_case_for_quality_review? - end + # Updated to use the constants provided in the file with margin to not be a seemingly arbitrary number. + # As of 4/2024, there were over 4000 cases being completed each month. + let(:complete_cases_count) do + limit = QualityReviewCaseSelector::MONTHLY_LIMIT_OF_QUALITY_REVIEWS + probability = QualityReviewCaseSelector::QUALITY_REVIEW_SELECTION_PROBABILITY + + ((limit / probability) * 1.2).to_i end it "should hit at least the monthly minimum of QR tasks" do - expect(QualityReviewTask.count).to be >= 164 + count = 0 + complete_cases_count.times { count += 1 if QualityReviewCaseSelector.select_case_for_quality_review? } + + expect(count).to be >= QualityReviewCaseSelector::MONTHLY_LIMIT_OF_QUALITY_REVIEWS end end diff --git a/spec/services/deprecation_warnings/production_handler_spec.rb b/spec/services/deprecation_warnings/production_handler_spec.rb index 2c4893ee8f3..e28cf27f0e1 100644 --- a/spec/services/deprecation_warnings/production_handler_spec.rb +++ b/spec/services/deprecation_warnings/production_handler_spec.rb @@ -21,7 +21,7 @@ module DeprecationWarnings allow(Raven).to receive(:capture_message) allow(Raven).to receive(:capture_exception) - allow(SlackService).to receive(:new).with(url: anything).and_return(slack_service) + allow(SlackService).to receive(:new).and_return(slack_service) allow(slack_service).to receive(:send_notification) end diff --git a/spec/services/external_api/pexip_service_spec.rb b/spec/services/external_api/pexip_service_spec.rb index 21bd17b8d57..43c9cacf4c7 100644 --- a/spec/services/external_api/pexip_service_spec.rb +++ b/spec/services/external_api/pexip_service_spec.rb @@ -96,8 +96,8 @@ let(:virtual_hearing) do create(:virtual_hearing, conference_id: "123") end - subject { pexip_service.delete_conference(virtual_hearing) } + subject { pexip_service.delete_conference(virtual_hearing) } let(:success_del_resp) { HTTPI::Response.new(204, {}, {}) } let(:error_del_resp) { HTTPI::Response.new(404, {}, {}) } diff --git a/spec/services/external_api/webex_service_spec.rb b/spec/services/external_api/webex_service_spec.rb index 7737a5d2cfb..a54f56ccae9 100644 --- a/spec/services/external_api/webex_service_spec.rb +++ b/spec/services/external_api/webex_service_spec.rb @@ -8,8 +8,8 @@ let(:domain) { "gov.fake.com" } let(:api_endpoint) { "/api/v2/fake" } let(:query) { nil } - let(:config) do - { + let(:webex_service) do + ExternalApi::WebexService.new( host: host, domain: domain, api_endpoint: api_endpoint, @@ -17,11 +17,7 @@ apikey: apikey, port: port, query: query - } - end - - let(:webex_service) do - ExternalApi::WebexService.new(config: config) + ) end describe "webex requests" do @@ -57,6 +53,7 @@ subject { webex_service.create_conference(virtual_hearing) } it "calls send_webex_request and passes the correct body" do + allow(webex_service).to receive(:send_webex_request).with(body, method).and_return(success_create_resp) expect(webex_service).to receive(:send_webex_request).with(body, method) subject end @@ -109,6 +106,7 @@ subject { webex_service.delete_conference(virtual_hearing) } it "calls send_webex_request and passes correct body" do + allow(webex_service).to receive(:send_webex_request).with(body, method).and_return(success_create_resp) expect(webex_service).to receive(:send_webex_request).with(body, method) subject end @@ -164,6 +162,7 @@ subject { webex_service.fetch_recordings_list } it "it calls send webex request with nil body and GET method" do + allow(webex_service).to receive(:send_webex_request).with(body, method).and_return(success_create_resp) expect(webex_service).to receive(:send_webex_request).with(body, method) subject end @@ -219,9 +218,12 @@ let(:method) { "GET" } - subject { webex_service.fetch_recording_details } + let(:recording_id) { "fake_id" } + + subject { webex_service.fetch_recording_details(recording_id) } it "it calls send webex request with nil body and GET method" do + allow(webex_service).to receive(:send_webex_request).with(body, method).and_return(success_create_resp) expect(webex_service).to receive(:send_webex_request).with(body, method) subject end diff --git a/spec/workflows/transcription_packages_spec.rb b/spec/workflows/transcription_packages_spec.rb new file mode 100644 index 00000000000..4cffe572070 --- /dev/null +++ b/spec/workflows/transcription_packages_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +describe TranscriptionPackages do + describe "#call" do + context "start to execute all jobs" do + let(:hearings) { (1..5).map { create(:hearing, :with_transcription_files) } } + let(:legacy_hearings) { (1..5).map { create(:hearing, :with_transcription_files) } } + + def hearings_in_work_order(all_hearings) + all_hearings.map { |hearing| { hearing_id: hearing.id, hearing_type: hearing.class.to_s } } + end + + let(:work_order_params) do + { + work_order_name: "#1234567", + return_date: "05/07/2024", + contractor: "Contractor A", + hearings: hearings_in_work_order(hearings + legacy_hearings) + } + end + + subject { TranscriptionPackages.new(work_order_params) } + + it "Call to initialize method" do + expect(subject.instance_variable_get(:@work_order_params)[:work_order_name]).to eq("#1234567") + expect(subject.instance_variable_get(:@work_order_params)[:return_date]).to eq("05/07/2024") + expect(subject.instance_variable_get(:@work_order_params)[:contractor]).to eq("Contractor A") + expect(subject.instance_variable_get(:@work_order_params)[:hearings]).to eq( + hearings_in_work_order(hearings + legacy_hearings) + ) + end + + it "Call to Call method " do + allow_any_instance_of(TranscriptionPackages).to receive(:create_zip_file).and_return(true) + allow_any_instance_of(TranscriptionPackages).to receive(:create_bom_file).and_return(true) + allow_any_instance_of(TranscriptionPackages).to receive(:create_transcription_package).and_return(true) + allow_any_instance_of(TranscriptionPackages).to receive(:upload_transcription_package).and_return(true) + expect { subject.call }.not_to raise_error + end + end + end +end