diff --git a/Gemfile b/Gemfile index fa331b09d18..0b6822d2c27 100644 --- a/Gemfile +++ b/Gemfile @@ -10,14 +10,14 @@ gem "acts_as_tree" # amoeba gem for cloning appeals gem "amoeba" +gem "aws-sdk" # BGS - gem "bgs", git: "https://github.com/department-of-veterans-affairs/ruby-bgs.git", ref: "a2e055b5a52bd1e2bb8c2b3b8d5820b1a404cd3d" # Bootsnap speeds up app boot (and started to be a default gem in 5.2). gem "bootsnap", require: false gem "browser" gem "business_time", "~> 0.9.3" -gem "caseflow", git: "https://github.com/department-of-veterans-affairs/caseflow-commons", ref: "871f7034c502f8d7101bde74e58606716b601c70" +gem "caseflow", git: "https://github.com/department-of-veterans-affairs/caseflow-commons", ref: "716b58caf2116da5fca21c3b3aeea6c9712f3b9d" gem "connect_mpi", git: "https://github.com/department-of-veterans-affairs/connect-mpi.git", ref: "a3a58c64f85b980a8b5ea6347430dd73a99ea74c" gem "connect_vbms", git: "https://github.com/department-of-veterans-affairs/connect_vbms.git", ref: "9807d9c9f0f3e3494a60b6693dc4f455c1e3e922" gem "console_tree_renderer", git: "https://github.com/department-of-veterans-affairs/console-tree-renderer.git", tag: "v0.1.1" diff --git a/Gemfile.lock b/Gemfile.lock index b06ef627e05..82df5ebe5a5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -9,11 +9,11 @@ GIT GIT remote: https://github.com/department-of-veterans-affairs/caseflow-commons - revision: 871f7034c502f8d7101bde74e58606716b601c70 - ref: 871f7034c502f8d7101bde74e58606716b601c70 + revision: 716b58caf2116da5fca21c3b3aeea6c9712f3b9d + ref: 716b58caf2116da5fca21c3b3aeea6c9712f3b9d specs: caseflow (0.4.8) - aws-sdk (~> 3.2) + aws-sdk-s3 bourbon (= 4.2.7) d3-rails jquery-rails @@ -2035,6 +2035,7 @@ DEPENDENCIES acts_as_tree amoeba anbt-sql-formatter + aws-sdk bgs! bootsnap brakeman diff --git a/app/jobs/ama_notification_efolder_sync_job.rb b/app/jobs/ama_notification_efolder_sync_job.rb index a015cdbcb3d..bbf9b6db122 100644 --- a/app/jobs/ama_notification_efolder_sync_job.rb +++ b/app/jobs/ama_notification_efolder_sync_job.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class AmaNotificationEfolderSyncJob < CaseflowJob + include MessageConfigurations::DeleteMessageBeforeStart + queue_with_priority :low_priority BATCH_LIMIT = ENV["AMA_NOTIFICATION_REPORT_SYNC_LIMIT"] || 500 diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb index 7b06c649ed4..f72a18f3a4a 100644 --- a/app/jobs/application_job.rb +++ b/app/jobs/application_job.rb @@ -5,6 +5,17 @@ class ApplicationJob < ActiveJob::Base class InvalidJobPriority < StandardError; end + # Override in job classes if you anticipate that the job will take longer than the SQS visibility + # timeout value (ex: currently 5 hours for our low priority queue at the time of writing this) + # to prevent multiple instances of the job from being executed. + # + # See https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-visibility-timeout.html + DELETE_SQS_MESSAGE_BEFORE_START = false + + # For jobs that run multiple times in a short time span, we do not want to continually update + # the JobsExecutionTime table. This boolean will help us ignore those jobs + IGNORE_JOB_EXECUTION_TIME = false + class << self def queue_with_priority(priority) unless [:low_priority, :high_priority].include? priority @@ -48,5 +59,15 @@ def capture_exception(error:, extra: {}) if self.class.app_name.present? RequestStore.store[:application] = "#{self.class.app_name}_job" end + + # Check whether Job execution time should be tracked + unless self.class::IGNORE_JOB_EXECUTION_TIME + # Add Record to JobExecutionTimes to track the current job execution time + JobExecutionTime.upsert( + { job_name: self.class.to_s, + last_executed_at: Time.now.utc }, + unique_by: :job_name + ) + end end end diff --git a/app/jobs/caseflow_job.rb b/app/jobs/caseflow_job.rb index e1ce79195f7..b9717f403b0 100644 --- a/app/jobs/caseflow_job.rb +++ b/app/jobs/caseflow_job.rb @@ -14,6 +14,35 @@ class CaseflowJob < ApplicationJob metrics_service_report_runtime(metric_group_name: job.class.name.underscore) unless @reported_to_metrics_service end + class << self + # Serializes and formats a job object so that it can be placed into an SQS message queue + # + # @param job [ActiveJob::Base] The job to be serialized + # + # @return [Hash] + # A hash representation of the job object that is compatible with SQS. + def serialize_job_for_enqueueing(job) + ActiveJob::QueueAdapters::ShoryukenAdapter.instance.send(:message, job) + end + + # Allows for enqueueing up to 10 async jobs at a time via the SendMessageBatch endpoint in the + # SQS API. This is to allow for reducing the number of round trips to the API when enqueueing a large + # number of jobs for delayed execution. + # + # @param jobs_to_enqueue [Array] The jobs to enqueue for later execution. + # @param name_of_queue [String] The name of the SQS queue to place the messages onto. + # + # @return [Aws::SQS::Types::SendMessageBatchResult] + # A struct containing the messages that were successfully enqueued and those that failed. + def enqueue_batch_of_jobs(jobs_to_enqueue:, name_of_queue:) + fail Caseflow::Error::MaximumBatchSizeViolationError if jobs_to_enqueue.size > 10 + + Shoryuken::Client.queues(name_of_queue).send_messages( + jobs_to_enqueue.map { serialize_job_for_enqueueing(_1) } + ) + end + end + def metrics_service_report_runtime(metric_group_name:) MetricsService.record_runtime( app_name: "caseflow_job", diff --git a/app/jobs/hearings/geomatch_and_cache_appeal_job.rb b/app/jobs/hearings/geomatch_and_cache_appeal_job.rb index 16e11ed1c1b..5e6a8f647d4 100644 --- a/app/jobs/hearings/geomatch_and_cache_appeal_job.rb +++ b/app/jobs/hearings/geomatch_and_cache_appeal_job.rb @@ -5,7 +5,7 @@ class Hearings::GeomatchAndCacheAppealJob < ApplicationJob application_attr :hearing_schedule # :nocov: - retry_on(StandardError, wait: 10.seconds, attempts: 10) do |job, exception| + retry_on(StandardError, wait: 10.seconds, attempts: 5) do |job, exception| Rails.logger.error("#{job.class.name} (#{job.job_id}) failed with error: #{exception}") if job.executions == 10 diff --git a/app/jobs/ignore_job_execution_time.rb b/app/jobs/ignore_job_execution_time.rb new file mode 100644 index 00000000000..f22d22029f4 --- /dev/null +++ b/app/jobs/ignore_job_execution_time.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module IgnoreJobExecutionTime + IGNORE_JOB_EXECUTION_TIME = true +end diff --git a/app/jobs/legacy_notification_efolder_sync_job.rb b/app/jobs/legacy_notification_efolder_sync_job.rb index 1aa41950469..4107d2a9a45 100644 --- a/app/jobs/legacy_notification_efolder_sync_job.rb +++ b/app/jobs/legacy_notification_efolder_sync_job.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class LegacyNotificationEfolderSyncJob < CaseflowJob + include MessageConfigurations::DeleteMessageBeforeStart + queue_with_priority :low_priority BATCH_LIMIT = ENV["LEGACY_NOTIFICATION_REPORT_SYNC_LIMIT"] || 500 diff --git a/app/jobs/message_configurations/delete_message_before_start.rb b/app/jobs/message_configurations/delete_message_before_start.rb new file mode 100644 index 00000000000..233298b9480 --- /dev/null +++ b/app/jobs/message_configurations/delete_message_before_start.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module MessageConfigurations::DeleteMessageBeforeStart + DELETE_SQS_MESSAGE_BEFORE_START = true +end diff --git a/app/jobs/middleware/job_message_deletion_middleware.rb b/app/jobs/middleware/job_message_deletion_middleware.rb new file mode 100644 index 00000000000..c4b0c760637 --- /dev/null +++ b/app/jobs/middleware/job_message_deletion_middleware.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +# Deletes the message from the associated SQS queue if the job class +# specifies that this operation should take place PRIOR to the job being initiated. +# +# This will occur if the job will take longer than the SQS queue's visibility timeout +# which would potentially allow multiple instances of the same job to be executed. +class JobMessageDeletionMiddleware + # :reek:LongParameterList + def call(_worker, _queue, msg, body) + if body["job_class"].constantize::DELETE_SQS_MESSAGE_BEFORE_START + msg.client.delete_message(queue_url: msg.queue_url, receipt_handle: msg.data.receipt_handle) + end + + yield + end +end diff --git a/app/jobs/nightly_syncs_job.rb b/app/jobs/nightly_syncs_job.rb index 1dbe22bc95f..ef835251d04 100644 --- a/app/jobs/nightly_syncs_job.rb +++ b/app/jobs/nightly_syncs_job.rb @@ -11,6 +11,7 @@ def perform RequestStore.store[:current_user] = User.system_user @slack_report = [] + sync_hearing_states sync_vacols_cases sync_vacols_users sync_decision_review_tasks @@ -88,4 +89,21 @@ def dangling_legacy_appeals reporter.call reporter.buffer.map { |vacols_id| LegacyAppeal.find_by(vacols_id: vacols_id) } end + + # Adjusts any appeal states appropriately if it is found that a seemingly pending + # hearing has been marked with a disposition in VACOLS without Caseflow's knowledge. + def sync_hearing_states + AppealState.where(appeal_type: "LegacyAppeal", hearing_scheduled: true).each do |state| + case state.appeal&.hearings&.max_by(&:scheduled_for)&.disposition + when Constants.HEARING_DISPOSITION_TYPES.held + state.hearing_held_appeal_state_update_action! + when Constants.HEARING_DISPOSITION_TYPES.cancelled + state.hearing_withdrawn_appeal_state_update_action! + when Constants.HEARING_DISPOSITION_TYPES.postponed + state.hearing_postponed_appeal_state_update_action! + when Constants.HEARING_DISPOSITION_TYPES.scheduled_in_error + state.scheduled_in_error_appeal_state_update_action! + end + end + end end diff --git a/app/jobs/notification_initialization_job.rb b/app/jobs/notification_initialization_job.rb new file mode 100644 index 00000000000..41a2f0ae0ba --- /dev/null +++ b/app/jobs/notification_initialization_job.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +# == Overview +# +# The NotificationInitializationJob encapsulates the instantiation of templates +# for messages to be sent by VANotify. +# +# Information such as whether or not the claimant is deceased and if the veteran is still the +# primary claimant (or if an appellant substitution has taken place) is gathered and factored +# into what is then sent to the SendNotificationJob. +# +# This job was created in order to extract logic that causes calls to external services so that +# large batch notification queueing jobs, like the QuarterlyNotificationsJob, can run much more quickly. +class NotificationInitializationJob < CaseflowJob + include Hearings::EnsureCurrentUserIsSet + include IgnoreJobExecutionTime + + queue_as SendNotificationJob.queue_name_suffix + application_attr :va_notify + + # ... + # + # @param appeal_id [Integer] Foreign key ID of the appeal to be associated with the notification. + # @param appeal_type [String] Class name of appeal to be associated with the notification. Appeal or LegacyAppeal. + # @param template_name [String] VANotify template name to be requested transmission of. + # Must be present in the configuration for our VANotify account, and must be a template represented in our + # notification_events table. + # @param appeal_status [String] An optional status that is used to fill in a blank in the quarterly notification + # template to let the claimant know what the status of their appeal is. + # + # @return [SendNotificationJob, nil] + # A SendNotificationJob job object representing the job that was enqueued, or nil if a notification + # wasn't ultimately attempted to be sent. + # :reek:LongParameterList + def perform(appeal_id:, appeal_type:, template_name:, appeal_status: nil) + begin + ensure_current_user_is_set + + appeal = appeal_type.constantize.find_by(id: appeal_id) + + fail Caseflow::Error::AppealNotFound, "#{appeal_type} with ID #{appeal_id} could not be found." unless appeal + + AppellantNotification.notify_appellant( + appeal, + template_name, + appeal_status + ) + rescue StandardError => error + log_error(error) + end + end +end diff --git a/app/jobs/poll_docketed_legacy_appeals_job.rb b/app/jobs/poll_docketed_legacy_appeals_job.rb index ae5f0443226..e9f11051a5f 100644 --- a/app/jobs/poll_docketed_legacy_appeals_job.rb +++ b/app/jobs/poll_docketed_legacy_appeals_job.rb @@ -18,6 +18,7 @@ def perform RequestStore.store[:current_user] = User.system_user vacols_ids = most_recent_docketed_appeals(LEGACY_DOCKETED) filtered_vacols_ids = filter_duplicate_legacy_notifications(vacols_ids) + create_corresponding_appeal_states(filtered_vacols_ids.uniq) send_legacy_notifications(filtered_vacols_ids) end @@ -38,6 +39,25 @@ def filter_duplicate_legacy_notifications(vacols_ids) vacols_ids.reject { |id| duplicate_ids.include?(id) } end + # Purpose: To create an AppealState record for the docketed legacy appeals + # Params: An array of vacols_ids for docketed legacy appeals + # Return: None + def create_corresponding_appeal_states(vacols_ids) + vacols_ids.each do |vacols_id| + appeal = LegacyAppeal.find_by_vacols_id(vacols_id) + appeal_state = AppealState.find_by(appeal: appeal) + if appeal_state + appeal_state.appeal_docketed = true + appeal_state.save! + else + AppealState.new(appeal: appeal, + created_by_id: User.system_user.id, + appeal_docketed: true) + .save! + end + end + end + # rubocop:disable all # Purpose: To send the 'appeal docketed' notification for the legacy appeals # Params: vacols_ids - An array of filtered vacols ids for legacy appeals that didnt already have notifications sent @@ -46,7 +66,10 @@ def send_legacy_notifications(vacols_ids) Rails.logger.info("Found #{vacols_ids.count} legacy appeals that have been recently docketed and have not gotten docketed notifications") vacols_ids.each do |vacols_id| begin - AppellantNotification.notify_appellant(LegacyAppeal.find_by_vacols_id(vacols_id), "Appeal docketed") + AppellantNotification.notify_appellant( + LegacyAppeal.find_by_vacols_id(vacols_id), + Constants.EVENT_TYPE_FILTERS.appeal_docketed + ) rescue Exception => ex Rails.logger.error("#{ex.class}: #{ex.message} for vacols id:#{vacols_id} on #{JOB_ATTR.class} of ID:#{JOB_ATTR.job_id}\n #{ex.backtrace.join("\n")}") next diff --git a/app/jobs/quarterly_notifications_job.rb b/app/jobs/quarterly_notifications_job.rb index 70444b312bf..5356b03b678 100644 --- a/app/jobs/quarterly_notifications_job.rb +++ b/app/jobs/quarterly_notifications_job.rb @@ -1,140 +1,55 @@ # frozen_string_literal: true class QuarterlyNotificationsJob < CaseflowJob + include MessageConfigurations::DeleteMessageBeforeStart + include Hearings::EnsureCurrentUserIsSet queue_with_priority :low_priority - application_attr :hearing_schedule + application_attr :va_notify - QUERY_LIMIT = ENV["QUARTERLY_NOTIFICATIONS_JOB_BATCH_SIZE"] + NOTIFICATION_TYPES = Constants.QUARTERLY_STATUSES.to_h.freeze - # Purpose: Loop through all open appeals quarterly and sends statuses for VA Notify - # - # Params: none + # Locates appeals eligible for quarterly notifications and queues a NotificationInitializationJob + # for each for further processing, and eventual (maybe) transmission of correspondence to an appellant. # - # Response: None - def perform # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity + # @return [Hash] + # Returns the hash of NOTIFICATION_TYPES that were iterated over, though this value isn't designed + # to be utilized by a caller due to the async nature of this job. + def perform ensure_current_user_is_set - 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" - appeal = Appeal.find_by(id: appeal_state.appeal_id) - elsif appeal_state.appeal_type == "LegacyAppeal" - appeal = LegacyAppeal.find_by(id: appeal_state.appeal_id) - end - if appeal.nil? - begin - fail Caseflow::Error::AppealNotFound, "Standard Error ID: " + SecureRandom.uuid + " The appeal was unable "\ - "to be found." - rescue Caseflow::Error::AppealNotFound => error - Rails.logger.error("QuarterlyNotificationsJob::Error - Unable to send a notification for "\ - "#{appeal_state&.appeal_type} ID #{appeal_state&.appeal_id} because of #{error}") - end - else - begin - MetricsService.record("Creating Quarterly Notification for #{appeal.class} ID #{appeal.id}", - name: "send_quarterly_notifications(appeal_state, appeal)") do - send_quarterly_notifications(appeal_state, appeal) - end - rescue StandardError => error - Rails.logger.error("QuarterlyNotificationsJob::Error - Unable to send a notification for "\ - "#{appeal_state&.appeal_type} ID #{appeal_state&.appeal_id} because of #{error}") - end + begin + NOTIFICATION_TYPES.each_key do |notification_type| + jobs = AppealState.eligible_for_quarterly.send(notification_type).pluck(:appeal_id, :appeal_type) + .map do |related_appeal_info| + NotificationInitializationJob.new( + appeal_id: related_appeal_info.first, + appeal_type: related_appeal_info.last, + template_name: Constants.EVENT_TYPE_FILTERS.quarterly_notification, + appeal_status: notification_type.to_s + ) end + + Parallel.each(jobs.each_slice(10).to_a, in_threads: 5) { |jobs_to_enqueue| enqueue_init_jobs(jobs_to_enqueue) } end + rescue StandardError => error + log_error(error) end end private - # Purpose: Method to check appeal state for statuses and send out a notification based on - # which statuses are turned on in the appeal state + # Batches enqueueing of the NotificationInitializationJobs in order to reduce round-trips to the SQS API # - # Params: appeal state, appeal + # @param jobs [Array] An array of NotificationInitializationJob objects to enqueue. # - # Response: SendNotificationJob queued to send_notification SQS queue - def send_quarterly_notifications(appeal_state, appeal) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity - # if either there's a hearing postponed or a hearing scheduled in error - if appeal_state.hearing_postponed || appeal_state.scheduled_in_error - # appeal status is Hearing to be Rescheduled / Privacy Act Pending - if appeal_state.privacy_act_pending - AppellantNotification.notify_appellant( - appeal, - "Quarterly Notification", - Constants.QUARTERLY_STATUSES.hearing_to_be_rescheduled_privacy_pending - ) - # appeal status is Hearing to be Rescheduled - else - AppellantNotification.notify_appellant( - appeal, - "Quarterly Notification", - Constants.QUARTERLY_STATUSES.hearing_to_be_rescheduled - ) - end - # if there's a hearing scheduled - elsif appeal_state.hearing_scheduled - # if there's privacy act tasks pending - # appeal status is Hearing Scheduled / Privacy Act Pending - if appeal_state.privacy_act_pending - AppellantNotification.notify_appellant( - appeal, - "Quarterly Notification", - Constants.QUARTERLY_STATUSES.hearing_scheduled_privacy_pending - ) - # if there's no privacy act tasks pending - # appeal status is Hearing Scheduled - elsif !appeal_state.privacy_act_pending - AppellantNotification.notify_appellant( - appeal, - "Quarterly Notification", - Constants.QUARTERLY_STATUSES.hearing_scheduled - ) - end - # if there's no hearing scheduled and no hearing withdrawn - elsif !appeal_state.hearing_withdrawn - # if there's ihp tasks pending and privacy act tasks pending - # appeal status is VSO IHP Pending / Privacy Act Pending - if appeal_state.vso_ihp_pending && appeal_state.privacy_act_pending - AppellantNotification.notify_appellant( - appeal, - "Quarterly Notification", - Constants.QUARTERLY_STATUSES.ihp_pending_privacy_pending - ) - # if there's no ihp tasks pending and there are privacy act tasks pending - # appeal status is Privacy Act Pending - elsif !appeal_state.vso_ihp_pending && appeal_state.privacy_act_pending - AppellantNotification.notify_appellant( - appeal, - "Quarterly Notification", - Constants.QUARTERLY_STATUSES.privacy_pending - ) - # if there's no privacy acts pending and there are ihp tasks pending - # appeal status is VSO IHP Pending - elsif appeal_state.vso_ihp_pending && !appeal_state.privacy_act_pending - AppellantNotification.notify_appellant( - appeal, - "Quarterly Notification", - Constants.QUARTERLY_STATUSES.ihp_pending - ) - # if there's no privacy acts pending or ihp tasks pending - # appeal status is Appeal Docketed - elsif !appeal_state.vso_ihp_pending && !appeal_state.privacy_act_pending && appeal_state.appeal_docketed - AppellantNotification.notify_appellant( - appeal, - "Quarterly Notification", - Constants.QUARTERLY_STATUSES.appeal_docketed - ) - end - # appeal status is Appeal Docketed - elsif appeal_state.appeal_docketed && appeal_state.hearing_withdrawn - AppellantNotification.notify_appellant( - appeal, - "Quarterly Notification", - Constants.QUARTERLY_STATUSES.appeal_docketed - ) - end + # @return [Aws::SQS::Types::SendMessageBatchResult] + # A struct containing the messages that were successfully enqueued and those that failed. + def enqueue_init_jobs(jobs) + CaseflowJob.enqueue_batch_of_jobs( + jobs_to_enqueue: jobs, + name_of_queue: NotificationInitializationJob.queue_name + ) end end diff --git a/app/jobs/send_notification_job.rb b/app/jobs/send_notification_job.rb index 1a145882481..4438014e8ec 100644 --- a/app/jobs/send_notification_job.rb +++ b/app/jobs/send_notification_job.rb @@ -3,226 +3,292 @@ # Purpose: Active Job that handles the processing of VA Notifcation event trigger. # This job saves the data to an audit table and If the corresponding feature flag is enabled will send # an email or SMS request to VA Notify API +# :reek:RepeatedConditional class SendNotificationJob < CaseflowJob include Hearings::EnsureCurrentUserIsSet + include IgnoreJobExecutionTime - queue_as ApplicationController.dependencies_faked? ? :send_notifications : :"send_notifications.fifo" - application_attr :hearing_schedule + queue_as { self.class.queue_name_suffix } + application_attr :va_notify + attr_accessor :notification_audit, :message - retry_on(Caseflow::Error::VANotifyNotFoundError, attempts: 5, wait: :exponentially_longer) do |job, exception| - Rails.logger.error("Retrying #{job.class.name} (#{job.job_id}) because failed with error: #{exception}") - end + class SendNotificationJobError < StandardError; end - retry_on(Caseflow::Error::VANotifyInternalServerError, attempts: 5, wait: :exponentially_longer) do |job, exception| - Rails.logger.error("Retrying #{job.class.name} (#{job.job_id}) because failed with error: #{exception}") - end + RETRY_ERRORS = [ + Caseflow::Error::VANotifyNotFoundError, + Caseflow::Error::VANotifyInternalServerError, + Caseflow::Error::VANotifyRateLimitError, + HTTPClient::ReceiveTimeoutError + ].freeze + + DISCARD_ERRORS = [ + Caseflow::Error::VANotifyUnauthorizedError, + Caseflow::Error::VANotifyForbiddenError, + SendNotificationJobError + ].freeze - retry_on(Caseflow::Error::VANotifyRateLimitError, attempts: 5, wait: :exponentially_longer) do |job, exception| - Rails.logger.error("Retrying #{job.class.name} (#{job.job_id}) because failed with error: #{exception}") + RETRY_ERRORS.each do |err| + retry_on(err, attempts: 5, wait: :exponentially_longer) do |job, exception| + Rails.logger.error("Retrying #{job.class.name} (#{job.job_id}) because failed with error: #{exception}") + end end - discard_on(Caseflow::Error::VANotifyUnauthorizedError) do |job, exception| - Rails.logger.warn("Discarding #{job.class.name} (#{job.job_id}) because failed with error: #{exception}") + DISCARD_ERRORS.each do |err| + discard_on(err) do |job, exception| + error_message = "Discarding #{job.class.name} (#{job.job_id}) because failed with error: #{exception}" + err_level = exception.instance_of?(SendNotificationJobError) ? :error : :warn + Rails.logger.send(err_level, error_message) + end end - discard_on(Caseflow::Error::VANotifyForbiddenError) do |job, exception| - Rails.logger.warn("Discarding #{job.class.name} (#{job.job_id}) because failed with error: #{exception}") + class << self + def queue_name_suffix + ApplicationController.dependencies_faked? ? :send_notifications : :"send_notifications.fifo" + end end # Must receive JSON string as argument + def perform(message_json) ensure_current_user_is_set begin - unless message_json - fail Caseflow::Error::NotificationInitializationError, - message: "There was no message passed into the " \ - "SendNotificationJob.perform_later function. Exiting job." - end + fail SendNotificationJobError, "Message argument of value nil supplied to job" if message_json.nil? + + @message = validate_message(JSON.parse(message_json, object_class: OpenStruct)) - handle_message_json(message_json) + transaction_wrapper do + @notification_audit = find_or_create_notification_audit + update_notification_statuses + send_to_va_notify if message_status_valid? + end rescue StandardError => error + if Rails.deploy_env?(:prodtest) && error.in?(DISCARD_ERRORS) + transaction_wrapper do + @notification_audit = find_or_create_notification_audit + end + end + log_error(error) + raise error end end private - # Purpose: Conditionally handles a JSON outline of a notification and maybe sends it to - # to a veteran via VANotify. - # - # Params: A JSON object containing notification data - # - # Response: nil - # rubocop:disable Layout/LineLength, Metrics/BlockNesting - def handle_message_json(message_json) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity - @va_notify_email = FeatureToggle.enabled?(:va_notify_email) - @va_notify_sms = FeatureToggle.enabled?(:va_notify_sms) - @va_notify_quarterly_sms = FeatureToggle.enabled?(:va_notify_quarterly_sms) - message = JSON.parse(message_json, object_class: OpenStruct) - if message.appeal_id && message.appeal_type && message.template_name - notification_audit_record = create_notification_audit_record( - message.appeal_id, - message.appeal_type, - message.template_name, - message.participant_id - ) - if notification_audit_record - if message.status != "No participant_id" && message.status != "No claimant" - to_update = {} - if @va_notify_email - to_update[:email_notification_status] = message.status - end - if @va_notify_sms && message.template_name != "Quarterly Notification" || - @va_notify_quarterly_sms && message.template_name == "Quarterly Notification" - to_update[:sms_notification_status] = message.status - end - update_notification_audit_record(notification_audit_record, to_update) - if message.template_name == "Appeal docketed" && message.appeal_type == "LegacyAppeal" && !FeatureToggle.enabled?(:appeal_docketed_notification) - notification_audit_record.update!(email_enabled: false) - else - send_to_va_notify(message, notification_audit_record) - end - else - status = (message.status == "No participant_id") ? "No Participant Id Found" : "No Claimant Found" - to_update = {} - if @va_notify_email - to_update[:email_notification_status] = status - end - if @va_notify_sms && message.template_name != "Quarterly Notification" || - @va_notify_quarterly_sms && message.template_name == "Quarterly Notification" - to_update[:sms_notification_status] = status - end - update_notification_audit_record(notification_audit_record, to_update) - end - notification_audit_record.save! - else - fail Caseflow::Error::NotificationInitializationError, - message: "Audit record was unable to be found or created in SendNotificationJob. Exiting Job." - end - else - fail Caseflow::Error::NotificationInitializationError, - message: "appeals_id or appeal_type or event_type was nil in the SendNotificationJob. Exiting job." + # Conditionally wraps database operations in a transaction block depending on whether + # the current environment is ProdTest. The choice to not have ProdTest queries utilize + # a transction is due to how unlikely it will be for us to have an operation VA Notify + # integration in that environment due to this environment having production-replicated + # data and us not wanting to inadvertently transmit messages to actual recipients. + # + # The lack of a transaction block will prevent rollbacks on the records created in the + # notifications table and allow for observations around notification accuracy to be + # more easily obtained. + def transaction_wrapper + ActiveRecord::Base.transaction { yield } + end + + def event_type + message.template_name + end + + def event + @event ||= NotificationEvent.find_by(event_type: event_type) + end + + def appeal + @appeal ||= find_appeal_by_external_id + end + + # Purpose: Find appeal by external ID + # + # Returns: Appeal object + def find_appeal_by_external_id + appeal = Appeal.find_appeal_by_uuid_or_find_or_create_legacy_appeal_by_vacols_id(message.appeal_id) + + return appeal unless appeal.nil? + + fail SendNotificationJobError, "Associated appeal cannot be found for external ID #{message.appeal_id}" + end + + # Purpose: Determine if either a quarterly sms notification or non-quarterly sms notification + # + # Returns: Boolean + def sms_enabled? + @sms_enabled ||= va_notify_sms_enabled? || va_notify_quarterly_sms_enabled? + end + + def va_notify_sms_enabled? + FeatureToggle.enabled?(:va_notify_sms) && !quarterly_notification? + end + + def va_notify_quarterly_sms_enabled? + FeatureToggle.enabled?(:va_notify_quarterly_sms) && quarterly_notification? + end + + def quarterly_notification? + event_type == Constants.EVENT_TYPE_FILTERS.quarterly_notification + end + + # Purpose: Ensure necessary message attributes present to send notification + # + # Params: message: object containing details from appeal for notification + # + # Returns: message + # :reek:FeatureEnvy + def validate_message(message_to_validate) + nil_attributes = [:appeal_id, :appeal_type, :template_name].filter { |attr| message_to_validate.send(attr).nil? } + + return message_to_validate unless nil_attributes.any? + + fail SendNotificationJobError, "Nil message attribute(s): #{nil_attributes.map(&:to_s).join(', ')}" + end + + # Purpose: Find or create a new notification table row for the appeal + # + # Returns: Notification active model or nil + def find_or_create_notification_audit + params = { + appeals_id: message.appeal_id, + appeals_type: message.appeal_type, + event_type: event_type, + event_date: Time.zone.today, + notification_type: notification_type, + notifiable: appeal + } + + if legacy_appeal_docketed_event? && FeatureToggle.enabled?(:appeal_docketed_event) + notification = Notification.where(params).last + + return notification unless notification.nil? end + + create_notification(params.merge(participant_id: message.participant_id, notified_at: Time.zone.now)) end - # rubocop:enable Layout/LineLength, Metrics/BlockNesting - # Purpose: Updates and saves notification status for notification_audit_record + # Purpose: Determine if the notification event is for a legacy appeal that has been docketed # - # Params: notification_audit_record: object, - # to_update: hash. key corresponds to notification_events column and value corresponds to new value + # Returns: Boolean + def legacy_appeal_docketed_event? + event_type == Constants.EVENT_TYPE_FILTERS.appeal_docketed && appeal.is_a?(LegacyAppeal) + end + + # Purpose: Create notification audit record + # + # Params: params: Payload of attributes with which to create notification object # - # Response: Updated notification_audit_record - def update_notification_audit_record(notification_audit_record, to_update) - to_update.each do |key, value| - notification_audit_record[key] = value + # Returns: Notification object + def create_notification(params) + notification = Notification.create(params) + + return notification unless notification.nil? + + fail SendNotificationJobError, "Notification audit record was unable to be found or created" + end + + # Purpose: Updates and saves notification status for notification object + # + # Response: Updated notification object + def update_notification_statuses + status = format_message_status + params = {} + params[:email_notification_status] = status + params[:sms_notification_status] = status if sms_enabled? + + notification_audit.update(params) + end + + # Purpose: Reformat message status if status belongs to invalid category + # + # Response: Message string + def format_message_status + return message.status if message_status_valid? + + case message.status + when "No participant_id" then "No Participant Id Found" + when "No claimant" then "No Claimant Found" + when "Failure Due to Deceased" then "Failure Due to Deceased" + else + fail StandardError, "Message status #{message.status} is not recognized." end end + # Purpose: Determine if message status belongs to invalid + # + # Response: Boolean + def message_status_valid? + ["No participant_id", "No claimant", "Failure Due to Deceased"].exclude?(message.status) + end + # Purpose: Send message to VA Notify to send notification # - # Params: message (object containing participant_id, template_name, and others) Details from appeal for notification - # notification_id: ID of the notification_audit record (must be converted to string to work with API) - # - # Response: Updated Notification object (still not saved) - def send_to_va_notify(message, notification_audit_record) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity - event = NotificationEvent.find_by(event_type: message.template_name) - email_template_id = event.email_template_id - sms_template_id = event.sms_template_id - quarterly_sms_template_id = NotificationEvent.find_by(event_type: "Quarterly Notification").sms_template_id - appeal = Appeal.find_by_uuid(message.appeal_id) || LegacyAppeal.find_by(vacols_id: message.appeal_id) - first_name = appeal&.appellant_or_veteran_name&.split(" ")&.first || "Appellant" - status = message.appeal_status || "" - docket_number = appeal.docket_number - - if @va_notify_email - response = VANotifyService.send_email_notifications( - message.participant_id, - notification_audit_record.id.to_s, - email_template_id, - first_name, - docket_number, - status - ) - if !response.nil? && response != "" - to_update = { notification_content: response.body["content"]["body"], - email_notification_content: response.body["content"]["body"], - email_notification_external_id: response.body["id"] } - update_notification_audit_record(notification_audit_record, to_update) - end - end + # Response: Updated Notification object + def send_to_va_notify + send_va_notify_email + send_va_notify_sms if sms_enabled? + end - if @va_notify_sms && sms_template_id != quarterly_sms_template_id || - @va_notify_quarterly_sms && sms_template_id == quarterly_sms_template_id - response = VANotifyService.send_sms_notifications( - message.participant_id, - notification_audit_record.id.to_s, - sms_template_id, - first_name, - docket_number, - status + # Purpose: Build payload for VA Notify request body + # + # Response: Payload object + def va_notify_payload + { + participant_id: message.participant_id, + notification_id: notification_audit.id.to_s, + first_name: first_name || "Appellant", + docket_number: appeal.docket_number, + status: message.appeal_status || "" + } + end + + # Purpose: Send payload to VA Notify to send email notification + # + # Response: Updated notification object + def send_va_notify_email + email_response = VANotifyService.send_email_notifications( + va_notify_payload.merge(email_template_id: event.email_template_id) + ) + + if email_response.present? + body = email_response.body + notification_audit.update( + notification_content: body["content"]["body"], + email_notification_content: body["content"]["body"], + email_notification_external_id: body["id"] ) - if !response.nil? && response != "" - to_update = { - sms_notification_content: response.body["content"]["body"], sms_notification_external_id: response.body["id"] - } - update_notification_audit_record(notification_audit_record, to_update) - end end end - # Purpose: Method to create a new notification table row for the appeal - # - # Params: - # - appeals_id - UUID or vacols_id of the appeals the event triggered - # - appeals_type - Polymorphic column to identify the type of appeal - # - - Appeal - # - - LegacyAppeal - # - event_type: Name of the event that has transpired. Event names can be found in the notification_events table + # Purpose: Send payload to VA Notify to send sms notification # - # Returns: Notification active model or nil + # Response: Updated notification object + def send_va_notify_sms + response = VANotifyService.send_sms_notifications(va_notify_payload.merge(sms_template_id: event.sms_template_id)) - # rubocop:disable all - def create_notification_audit_record(appeals_id, appeals_type, event_type, participant_id) - notification_type = - if @va_notify_email && @va_notify_sms && event_type != "Quarterly Notification" || - @va_notify_email && @va_notify_quarterly_sms && event_type == "Quarterly Notification" - "Email and SMS" - elsif @va_notify_email - "Email" - elsif @va_notify_sms && event_type != "Quarterly Notification" || - @va_notify_quarterly_sms && event_type == "Quarterly Notification" - "SMS" - else - "None" - end + if response.present? + notification_audit.update( + sms_notification_content: response.body["content"]["body"], + sms_notification_external_id: response.body["id"] + ) + end + end - if event_type == "Appeal docketed" && appeals_type == "LegacyAppeal" && FeatureToggle.enabled?(:appeal_docketed_event) - notification = Notification.where(appeals_id: appeals_id, event_type: event_type, notification_type: notification_type, appeals_type: appeals_type, event_date: Time.zone.today).last - if !notification.nil? - notification - else - Notification.new( - appeals_id: appeals_id, - appeals_type: appeals_type, - event_type: event_type, - notification_type: notification_type, - participant_id: participant_id, - notified_at: Time.zone.now, - event_date: Time.zone.today - ) - end + # Purpose: Determine notification type depending on enabled feature toggles and event type + # + # Response: String + def notification_type + if sms_enabled? + "Email and SMS" else - Notification.new( - appeals_id: appeals_id, - appeals_type: appeals_type, - event_type: event_type, - notification_type: notification_type, - participant_id: participant_id, - notified_at: Time.zone.now, - event_date: Time.zone.today - ) + "Email" end end - # rubocop:enable all + + # Purpose: Parse first name of veteran or appellant from appeal + # + # Response: String + def first_name + appeal&.appellant_or_veteran_name&.split(" ")&.first + end end diff --git a/app/jobs/virtual_hearings/create_conference_job.rb b/app/jobs/virtual_hearings/create_conference_job.rb index 0da4a21227f..e88973d77c0 100644 --- a/app/jobs/virtual_hearings/create_conference_job.rb +++ b/app/jobs/virtual_hearings/create_conference_job.rb @@ -31,16 +31,16 @@ class VirtualHearingLinkGenerationFailed < StandardError; end ) end - retry_on(IncompleteError, attempts: 10, wait: :exponentially_longer) do |job, exception| + retry_on(IncompleteError, attempts: 5, wait: :exponentially_longer) do |job, exception| Rails.logger.error("#{job.class.name} (#{job.job_id}) failed with error: #{exception}") end - retry_on(VirtualHearingNotCreatedError, attempts: 10, wait: :exponentially_longer) do |job, exception| + retry_on(VirtualHearingNotCreatedError, attempts: 5, wait: :exponentially_longer) do |job, exception| Rails.logger.error("#{job.class.name} (#{job.job_id}) failed with error: #{exception}") end # Retry if Pexip returns an invalid response. - retry_on(Caseflow::Error::PexipApiError, attempts: 10, wait: :exponentially_longer) do |job, exception| + retry_on(Caseflow::Error::PexipApiError, attempts: 5, wait: :exponentially_longer) do |job, exception| Rails.logger.error("#{job.class.name} (#{job.job_id}) failed with error: #{exception}") kwargs = job.arguments.first diff --git a/app/jobs/warm_bgs_caches_job.rb b/app/jobs/warm_bgs_caches_job.rb index ab72151c89c..f821720b1de 100644 --- a/app/jobs/warm_bgs_caches_job.rb +++ b/app/jobs/warm_bgs_caches_job.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class WarmBgsCachesJob < CaseflowJob + include MessageConfigurations::DeleteMessageBeforeStart + queue_with_priority :low_priority application_attr :hearing_schedule diff --git a/app/models/appeal.rb b/app/models/appeal.rb index 7fc54269674..51ba29ecbcf 100644 --- a/app/models/appeal.rb +++ b/app/models/appeal.rb @@ -26,6 +26,7 @@ class Appeal < DecisionReview has_many :email_recipients, class_name: "HearingEmailRecipient" has_many :available_hearing_locations, as: :appeal, class_name: "AvailableHearingLocations" has_many :vbms_uploaded_documents, as: :appeal + has_many :notifications, as: :notifiable # decision_documents is effectively a has_one until post decisional motions are supported has_many :decision_documents, as: :appeal @@ -958,6 +959,10 @@ def is_legacy? false end + def appeal_state + super || AppealState.find_or_create_by(appeal: self) + end + private def business_lines_needing_assignment diff --git a/app/models/appeal_state.rb b/app/models/appeal_state.rb index 32322fc338a..6acd80c9c8f 100644 --- a/app/models/appeal_state.rb +++ b/app/models/appeal_state.rb @@ -1,7 +1,451 @@ # frozen_string_literal: true +# == Overview +# +# AppealState records are utilized to facilitate state machine-like behavior in order +# to track which status each appeal (AMA and Legacy) being processed in Caseflow is in. +# +# These states are most prominently used in the determination of which status to place in the +# 'Quarterly Notification' template whenever sending the quarterly correspondence to appellants. +# This is performed by the QuarterlyNotificationsJob. class AppealState < CaseflowRecord include HasAppealUpdatedSince include CreatedAndUpdatedByUserConcern include AppealStateBelongsToPolymorphicAppealConcern + + # Purpose: Default state of a hash of attributes for an appeal_state, all set to false. + # This will be used in the `update_appeal_state` method. + DEFAULT_STATE = ActiveSupport::HashWithIndifferentAccess.new(decision_mailed: false, + hearing_postponed: false, + hearing_withdrawn: false, + hearing_scheduled: false, + vso_ihp_pending: false, + vso_ihp_complete: false, + scheduled_in_error: false, + appeal_cancelled: false).freeze + + # Locates appeal states that are related to appeals eligible to potentially receive quarterly notifications. + # These appeals must not have been cancelled and their decisions must not have already been mailed. + # + # @return [ActiveRecord::Relation] + # Appeals eligible (potentially, assuming claimant is listed and claimant doesn't have an NOD on file) + # to receive a quarterly notification. + scope :eligible_for_quarterly, lambda { + where( + appeal_cancelled: false, + decision_mailed: false + ) + } + + # Locates appeal states related to appeals whose hearings have been cancelled + # (postponed or marked as "scheduled in error") and have yet to be rescheduled. + # + # @return [ActiveRecord::Relation] + # Appeal states for appeals with hearings awaiting reschedulement. + scope :hearing_to_be_rescheduled, lambda { + where( + <<~SQL + hearing_scheduled IS FALSE AND + privacy_act_pending IS FALSE AND + ( + hearing_postponed IS TRUE OR + scheduled_in_error IS TRUE + ) + SQL + ) + } + + # Locates appeal states related to appeals whose hearings have been cancelled + # (postponed or marked as "scheduled in error") and have yet to be rescheduled. + # In addition, these appeals have an active Privacy Act/FOIA request in their trees. + # + # @return [ActiveRecord::Relation] + # Appeal states for appeals with hearings awaiting reschedulement with Privacy Act/FOIA tasks. + scope :hearing_to_be_rescheduled_privacy_pending, lambda { + where( + <<~SQL + hearing_scheduled IS FALSE AND + privacy_act_pending IS TRUE AND + ( + hearing_postponed IS TRUE OR + scheduled_in_error IS TRUE + ) + SQL + ) + } + + # Locates appeal states related to appeals whose hearings have been scheduled and waiting to be held. + # + # @return [Array] + # Appeal states for appeals with scheduled hearings without dispositions + scope :hearing_scheduled, lambda { + hearing_scheduled_ama.where( + privacy_act_pending: false + ) + validated_hearing_scheduled_legacy_states.where( + privacy_act_pending: false + ) + } + + # Locates appeal states related to appeals whose hearings have been scheduled and waiting to be held. + # In addition, these appeals have an active Privacy Act/FOIA request in their trees. + # + # @return [Array] + # Appeal states for appeals with scheduled hearings without dispositions, and those appeals + # have open Privacy Act/FOIA-related tasks in their task trees. + scope :hearing_scheduled_privacy_pending, lambda { + hearing_scheduled_ama.where( + privacy_act_pending: true + ) + validated_hearing_scheduled_legacy_states.where( + privacy_act_pending: true + ) + } + + # Locates appeal states related to appeals with open InformalHearingPresentationTasks. + # In addition, these appeals have an open Privacy Act/FOIA request-related tasks in their trees. + # + # @return [ActiveRecord::Relation] + # Appeal states for appeals with open InformalHearingPresentationTasks. + # have open Privacy Act/FOIA-related tasks in their task trees. + scope :ihp_pending_privacy_pending, lambda { + where( + vso_ihp_pending: true, + privacy_act_pending: true, + hearing_scheduled: false, + hearing_postponed: false, + scheduled_in_error: false, + hearing_withdrawn: false + ) + } + + # Locates appeal states related to appeals with open InformalHearingPresentationTasks. + # + # @return [ActiveRecord::Relation] + # Appeal states for appeals with open InformalHearingPresentationTasks. + scope :ihp_pending, lambda { + where( + vso_ihp_pending: true, + privacy_act_pending: false, + hearing_scheduled: false, + hearing_postponed: false, + scheduled_in_error: false, + hearing_withdrawn: false + ) + } + + # Locates appeal states related to appeals with open Privacy Act/FOIA request-related tasks in their trees, + # however no other actions have taken place on the appeal (aside from perhaps docketing). + # + # @return [ActiveRecord::Relation] + # Appeal states for appeals with open Privacy Act/FOIA request-related tasks + scope :privacy_pending, lambda { + where( + vso_ihp_pending: false, + privacy_act_pending: true, + hearing_scheduled: false, + hearing_postponed: false, + scheduled_in_error: false, + hearing_withdrawn: false + ) + } + + # Locates appeal states related to appeals that have either just been docketed, or have had their hearing withdrawn + # causing them to return to their initial state. + # + # @return [ActiveRecord::Relation] + # Appeal states for appeals that have been docketed and are awaiting further action. + scope :appeal_docketed, lambda { + where( + <<~SQL + appeal_docketed IS TRUE AND + hearing_postponed IS FALSE AND + scheduled_in_error IS FALSE AND + hearing_scheduled IS FALSE AND + ( + ( + hearing_withdrawn IS FALSE AND + vso_ihp_pending IS FALSE AND + privacy_act_pending IS FALSE + ) OR ( + hearing_withdrawn IS TRUE + ) + ) + SQL + ) + } + + # Purpose: Method to update appeal_state in the case of + # a mailed decision. + # + # Params: appeal_state + # + # Response: None + def decision_mailed_appeal_state_update_action! + update_appeal_state_action!(:decision_mailed) + end + + # Purpose: Method to update appeal_state in the case of + # a cancelled appeal. + # + # Params: appeal_state + # + # Response: None + def appeal_cancelled_appeal_state_update_action! + update_appeal_state_action!(:appeal_cancelled) + end + + # Purpose: Method to update appeal_state in the case of + # a completed informal hearing presentaiton(IHP). + # + # Params: appeal + # Params: None + # + # Response: None + def vso_ihp_complete_appeal_state_update_action! + if !appeal.active_vso_ihp_task? + update_appeal_state_action!(:vso_ihp_complete) + end + end + + # Purpose: Method to update appeal_state in the case of + # a privacy related tasks marked as complete. + # + # Params: appeal + # Params: None + # + # Response: None + def privacy_act_complete_appeal_state_update_action! + unless appeal.active_foia_task? + update!(privacy_act_pending: false, privacy_act_complete: true) + end + end + + # Purpose: Method to update appeal_state in the case of + # privacy related tasks being cancelled. + # + # Params: appeal + # Params: None + # + # Response: None + def privacy_act_cancelled_appeal_state_update_action! + unless appeal.active_foia_task? + update!(privacy_act_pending: false) + end + end + + # Purpose: Method to update appeal_state in the case of + # a docketed appeal. + # + # Params: None + # + # Response: None + def appeal_docketed_appeal_state_update_action! + update_appeal_state_action!(:appeal_docketed) + end + + # Purpose: Method to update appeal_state in the case of + # a hearing being postponed. + # + # Params: None + # + # Response: None + def hearing_postponed_appeal_state_update_action! + update_appeal_state_action!(:hearing_postponed) + end + + # Purpose: Method to update appeal_state in the case of + # a hearing being marked as having been held. + # + # Params: None + # + # Response: None + def hearing_held_appeal_state_update_action! + update!(hearing_scheduled: false) + end + + # Purpose: Method to update appeal_state in the case of + # a hearing being withdrawn. + # + # Params: None + # + # Response: None + def hearing_withdrawn_appeal_state_update_action! + update_appeal_state_action!(:hearing_withdrawn) + end + + # Purpose: Method to update appeal_state in the case of + # a hearing being scheduled. + # + # Params: None + # + # Response: None + def hearing_scheduled_appeal_state_update_action! + update_appeal_state_action!(:hearing_scheduled) + end + + # Purpose: Method to update appeal_state in the case of + # a hearing being scheduled in error. + # + # Params: None + # + # Response: None + def scheduled_in_error_appeal_state_update_action! + update_appeal_state_action!(:scheduled_in_error) + end + + # Purpose: Method to update appeal_state in the case of + # the most recent VSO IHP Organizational task in the task + # tree being in an opened state. + # + # Params: None + # + # Response: None + def vso_ihp_pending_appeal_state_update_action! + update_appeal_state_action!(:vso_ihp_pending) + end + + # Purpose: Method to update appeal_state in the case of + # the most recent VSO IHP Organizational task in the task + # tree being cancelled. + # + # Params: None + # + # Response: None + def vso_ihp_cancelled_appeal_state_update_action! + update!(vso_ihp_pending: false, vso_ihp_complete: false) + end + + # Purpose: Method to update appeal_state in the case of + # there being at least one of the privacy act related + # tasks is still in an opened status. + # + # Params: None + # + # Response: None + def privacy_act_pending_appeal_state_update_action! + update!(privacy_act_pending: true, privacy_act_complete: false) + end + + private + + # A base set of conditions for identifying if an appeal's most recent milestone was its + # hearing being scheduled. This scope can be utilized for any variation on this + # status, such as whether or not certain mail or FOIA tasks also exist for the appeal. + # + # @return [AppealState::ActiveRecord_Relation] + # An ActiveRecord_Relation that can be changes with other AR scopes, clauses, methods, etc.. + # in order to construct a SQL query. + scope :hearing_scheduled_base, lambda { + where( + hearing_scheduled: true, + hearing_postponed: false, + scheduled_in_error: false + ) + } + + # @return [AppealState::ActiveRecord_Relation] + # The base hearing scheduled status query scoped only to AMA appeals. + scope :hearing_scheduled_ama, lambda { + task_join.hearing_scheduled_base.where(appeal_type: "Appeal").with_assigned_assign_hearing_disposition_task + } + + # @return [AppealState::ActiveRecord_Relation] + # The base hearing scheduled status query scoped only to legacy appeals. + scope :hearing_scheduled_legacy_base, lambda { + hearing_scheduled_base.where(appeal_type: "LegacyAppeal") + } + + # Represents an inner join between the appeal_states and tasks tables. This allows for utilizing + # tasks to further inform us of where an appeal is in its lifecycle. + # + # @return [AppealState::ActiveRecord_Relation] + # An ActiveRecord_Relation that can be changes with other AR scopes, clauses, methods, etc.. + # in order to construct a SQL query. + scope :task_join, lambda { + joins( + "join tasks on tasks.appeal_id = appeal_states.appeal_id and tasks.appeal_type = appeal_states.appeal_type" + ) + } + + # Represents an inner join between the appeal_states and legacy_appeals tables. + # This association is used to then pull relevant data from VACOLS to validate an appeal's state. + # + # @return [AppealState::ActiveRecord_Relation] + # An ActiveRecord_Relation that can be changes with other AR scopes, clauses, methods, etc.. + # in order to construct a SQL query. + scope :legacy_appeals_join, lambda { + joins( + "join legacy_appeals on legacy_appeals.id = appeal_states.appeal_id " \ + "and appeal_states.appeal_type = 'LegacyAppeal'" + ) + } + + # A clause to enforce the need for an assigned AssignHearingDispositionTask to be + # associated with the same appeal as an appeal state record. + # + # If constraints are met, then it should mean that there is a pending hearing for the appeal + # that is waiting for a disposition, and therefore has not been held. This is key + # for us in determining which appeals are in a state of hearing_scheduled. + # + # At this time this task is not possible to be placed into a status of in_progress, and on_hold + # often means that it has a child EvidenceSubmissionWindowTask (as long as the evidence submission wasn't waived) + # and/or TranscriptionTask. This occurs after a hearing is held. + # + # @return [AppealState::ActiveRecord_Relation] + # An ActiveRecord_Relation that can be changes with other AR scopes, clauses, methods, etc.. + # in order to construct a SQL query. + scope :with_assigned_assign_hearing_disposition_task, lambda { + where( + "tasks.type = ? and tasks.status = ?", + AssignHearingDispositionTask.name, + Constants.TASK_STATUSES.assigned + ) + } + + class << self + # Utilizes the appeal states that we have recorded as being hearing_scheduled = true + # and then reaches out to VACOLS to validate that the related hearings do not yet have + # a disposition. + # + # This is to combat instances where VACOLS is updated without Caseflow's knowledge and other + # difficulties around synchronizing the appeals states table for legacy appeals and hearings. + # + # @note The in_groups_of size must not exceed 1k due to Oracle database limitations. + # + # @return [AppealState::ActiveRecord_Relation] + # Either an AR relation signifying appeal states where the hearing has been confirmed to have a pending + # disposition in VACOLS, or simply nothing (none). Regardless, the relation returned can be safely chained to + # other ActiveRecord query building methods. + def validated_hearing_scheduled_legacy_states + ids_to_validate = hearing_scheduled_legacy_base.legacy_appeals_join.pluck(:vacols_id) + validated_vacols_ids = [] + + ids_to_validate.in_groups_of(500, false) do |ids_to_examine| + validated_vacols_ids.concat(VACOLS::CaseHearing.where( + folder_nr: ids_to_examine, + hearing_disp: nil + ).pluck(:folder_nr)) + end + + return none if validated_vacols_ids.empty? + + where( + appeal_type: "LegacyAppeal", + appeal_id: LegacyAppeal.where(vacols_id: validated_vacols_ids).pluck(:id) + ) + end + end + + # :reek:FeatureEnvy + def update_appeal_state_action!(status_to_update) + update!({}.merge(DEFAULT_STATE).tap do |existing_statuses| + existing_statuses[status_to_update] = true + + if status_to_update == :appeal_cancelled + existing_statuses.merge!({ + privacy_act_complete: false, + privacy_act_pending: false + }) + end + end) + end end diff --git a/app/models/concerns/appeal_concern.rb b/app/models/concerns/appeal_concern.rb index 767c63be2e1..d7e28da2689 100644 --- a/app/models/concerns/appeal_concern.rb +++ b/app/models/concerns/appeal_concern.rb @@ -90,6 +90,25 @@ def representative_tz timezone_identifier_for_address(representative_address) end + # Checks for any active foia tasks on an appeal. + def active_foia_task? + tasks.open.where(type: [ + FoiaColocatedTask.name, + PrivacyActTask.name, + HearingAdminActionFoiaPrivacyRequestTask.name, + FoiaRequestMailTask.name, + PrivacyActRequestMailTask.name + ]).any? + end + + # Checks for any active vso ihp tasks on an appeal. + def active_vso_ihp_task? + tasks.open.where(type: [ + IhpColocatedTask.name, + InformalHearingPresentationTask.name + ]).any? + end + def accessible? # this is used for calling BGSService.can_access? to fix VSO access that is being blocked # by BGS returning false for veteran.accessible? when they should indeed have access to the appeal. diff --git a/app/models/hearing.rb b/app/models/hearing.rb index 82ab53e311b..cafe866bba5 100644 --- a/app/models/hearing.rb +++ b/app/models/hearing.rb @@ -32,10 +32,12 @@ class Hearing < CaseflowRecord include HearingConcern include HasHearingEmailRecipientsConcern + # VA Notify Hooks prepend HearingScheduled prepend HearingPostponed prepend HearingWithdrawn prepend HearingScheduledInError + prepend HearingHeld belongs_to :hearing_day belongs_to :appeal @@ -291,6 +293,7 @@ def update_appeal_states_on_hearing_update update_appeal_states_on_hearing_scheduled_in_error update_appeal_states_on_hearing_postponed update_appeal_states_on_hearing_withdrawn + update_appeal_states_on_hearing_held end def assign_created_by_user diff --git a/app/models/hearings/forms/hearing_update_form.rb b/app/models/hearings/forms/hearing_update_form.rb index a55149196b9..fda1377fbee 100644 --- a/app/models/hearings/forms/hearing_update_form.rb +++ b/app/models/hearings/forms/hearing_update_form.rb @@ -1,8 +1,11 @@ # frozen_string_literal: true class HearingUpdateForm < BaseHearingUpdateForm + # VA Notify Hooks prepend DocketHearingPostponed prepend DocketHearingWithdrawn + prepend DocketHearingHeld + attr_accessor :advance_on_docket_motion_attributes, :evidence_window_waived, :hearing_issue_notes_attributes, :transcript_sent_date, :transcription_attributes diff --git a/app/models/job_execution_time.rb b/app/models/job_execution_time.rb new file mode 100644 index 00000000000..12d8ddbb964 --- /dev/null +++ b/app/models/job_execution_time.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +class JobExecutionTime < CaseflowRecord +end diff --git a/app/models/legacy_appeal.rb b/app/models/legacy_appeal.rb index 51eff1ec6c9..be859405409 100644 --- a/app/models/legacy_appeal.rb +++ b/app/models/legacy_appeal.rb @@ -42,6 +42,7 @@ class LegacyAppeal < CaseflowRecord accepts_nested_attributes_for :worksheet_issues, allow_destroy: true has_one :appeal_state, as: :appeal has_many :vbms_uploaded_documents, as: :appeal + has_many :notifications, as: :notifiable class UnknownLocationError < StandardError; end @@ -962,6 +963,10 @@ def is_legacy? end # rubocop:enable Naming/PredicateName + def appeal_state + super || AppealState.find_or_create_by(appeal: self) + end + private def soc_eligible_for_opt_in?(receipt_date:, covid_flag: false) diff --git a/app/models/legacy_hearing.rb b/app/models/legacy_hearing.rb index 370ce6aa900..b1c32759852 100644 --- a/app/models/legacy_hearing.rb +++ b/app/models/legacy_hearing.rb @@ -35,10 +35,13 @@ class LegacyHearing < CaseflowRecord include UpdatedByUserConcern include HearingConcern include HasHearingEmailRecipientsConcern + + # VA Notify Hooks prepend HearingScheduled prepend HearingWithdrawn prepend HearingPostponed prepend HearingScheduledInError + prepend HearingHeld # When these instance variable getters are called, first check if we've # fetched the values from VACOLS. If not, first fetch all values and save them @@ -397,6 +400,7 @@ def update_appeal_states_on_hearing_update update_appeal_states_on_hearing_scheduled_in_error update_appeal_states_on_hearing_postponed update_appeal_states_on_hearing_withdrawn + update_appeal_states_on_hearing_held end def assign_created_by_user diff --git a/app/models/prepend/va_notify/appeal_cancelled.rb b/app/models/prepend/va_notify/appeal_cancelled.rb index 408c8c289b5..886d55f2823 100644 --- a/app/models/prepend/va_notify/appeal_cancelled.rb +++ b/app/models/prepend/va_notify/appeal_cancelled.rb @@ -8,9 +8,6 @@ module AppealCancelled extend AppellantNotification - # rubocop:disable all - @@template_name = "Appeal Cancelled" - # rubocop:enable all # Original Method in app/models/task.rb # Purpose: Update Record in Appeal States Table @@ -21,12 +18,7 @@ module AppealCancelled def update_appeal_state_when_appeal_cancelled if ["RootTask"].include?(type) && status == Constants.TASK_STATUSES.cancelled - MetricsService.record("Updating APPEAL_CANCELLED column in Appeal States Table to TRUE "\ - "for #{appeal.class} ID #{appeal.id}", - service: :queue, - name: "AppellantNotification.appeal_mapper") do - AppellantNotification.appeal_mapper(appeal.id, appeal.class.to_s, "appeal_cancelled") - end + appeal.appeal_state.appeal_cancelled_appeal_state_update_action! end end end diff --git a/app/models/prepend/va_notify/appeal_decision_mailed.rb b/app/models/prepend/va_notify/appeal_decision_mailed.rb index baaa752660c..708af3e4955 100644 --- a/app/models/prepend/va_notify/appeal_decision_mailed.rb +++ b/app/models/prepend/va_notify/appeal_decision_mailed.rb @@ -3,12 +3,9 @@ # Module to notify appellant if an Appeal Decision is Mailed module AppealDecisionMailed extend AppellantNotification - # rubocop:disable all - @@template_name = "Appeal decision mailed" - CONTESTED_CLAIM = "#{@@template_name} (Contested claims)" - NON_CONTESTED_CLAIM = "#{@@template_name} (Non-contested claims)" - # rubocop:enable all + CONTESTED_CLAIM = Constants.EVENT_TYPE_FILTERS.appeal_decision_mailed_contested_claims + NON_CONTESTED_CLAIM = Constants.EVENT_TYPE_FILTERS.appeal_decision_mailed_non_contested_claims # Purpose: Adds VA Notify integration to the original method defined in app/models/decision_document.rb # @@ -18,7 +15,7 @@ module AppealDecisionMailed def process!(mail_package = nil) super_return_value = super if processed? - AppellantNotification.appeal_mapper(appeal.id, appeal.class.to_s, "decision_mailed") + appeal.appeal_state.decision_mailed_appeal_state_update_action! case appeal_type when "Appeal" template = appeal.contested_claim? ? CONTESTED_CLAIM : NON_CONTESTED_CLAIM diff --git a/app/models/prepend/va_notify/appeal_docketed.rb b/app/models/prepend/va_notify/appeal_docketed.rb index 36e98bca385..a9df03b8972 100644 --- a/app/models/prepend/va_notify/appeal_docketed.rb +++ b/app/models/prepend/va_notify/appeal_docketed.rb @@ -19,9 +19,6 @@ # Module to notify appellant when an appeal gets docketed module AppealDocketed extend AppellantNotification - # rubocop:disable all - @@template_name = "Appeal docketed" - # rubocop:enable all # original method defined in app/models/appeal.rb @@ -39,7 +36,7 @@ def create_tasks_on_intake_success! "for #{self.class} ID #{self.id}", service: nil, name: "AppellantNotification.notify_appellant") do - AppellantNotification.notify_appellant(self, @@template_name) + AppellantNotification.notify_appellant(self, Constants.EVENT_TYPE_FILTERS.appeal_docketed) end end super_return_value @@ -59,7 +56,7 @@ def docket_appeal "for #{appeal.class} ID #{appeal.id}", service: nil, name: "AppellantNotification.notify_appellant") do - AppellantNotification.notify_appellant(appeal, @@template_name) + AppellantNotification.notify_appellant(appeal, Constants.EVENT_TYPE_FILTERS.appeal_docketed) end super_return_value end @@ -73,12 +70,7 @@ def docket_appeal # Response: Update 'appeal_docketed' column to True def update_appeal_state_when_appeal_docketed if type == "DistributionTask" - MetricsService.record("Updating APPEAL_DOCKETED column in Appeal States Table to TRUE for #{appeal.class} "\ - "ID #{appeal.id}", - service: nil, - name: "AppellantNotification.appeal_mapper") do - AppellantNotification.appeal_mapper(appeal.id, appeal.class.to_s, "appeal_docketed") - end + appeal.appeal_state.appeal_docketed_appeal_state_update_action! end end end diff --git a/app/models/prepend/va_notify/appellant_notification.rb b/app/models/prepend/va_notify/appellant_notification.rb index 12988bd712e..77e4df6ec8b 100644 --- a/app/models/prepend/va_notify/appellant_notification.rb +++ b/app/models/prepend/va_notify/appellant_notification.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true # Module containing Aspect Overrides to Classes used to Track Statuses for Appellant Notification -# rubocop:disable Metrics/ModuleLength module AppellantNotification extend ActiveSupport::Concern class NoParticipantIdError < StandardError @@ -25,143 +24,19 @@ def status end class NoAppealError < StandardError; end - # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity + def self.handle_errors(appeal) fail NoAppealError if appeal.nil? message_attributes = {} message_attributes[:appeal_type] = appeal.class.to_s - message_attributes[:appeal_id] = (appeal.class.to_s == "Appeal") ? appeal.uuid : appeal.vacols_id + message_attributes[:appeal_id] = appeal.external_id message_attributes[:participant_id] = appeal.claimant_participant_id claimant = get_claimant(appeal) - begin - if claimant.nil? - fail NoClaimantError, message_attributes[:appeal_id] - elsif message_attributes[:participant_id] == "" || message_attributes[:participant_id].nil? - fail NoParticipantIdError, message_attributes[:appeal_id] - elsif appeal.veteran_appellant_deceased? - message_attributes[:status] = "Failure Due to Deceased" - else - message_attributes[:status] = "Success" - end - rescue StandardError => error - Rails.logger.error("#{error.message}\n#{error.backtrace.join("\n")}") - message_attributes[:status] = error.status - end - message_attributes + AppellantNotification.error_handling_messages_and_attributes(appeal, claimant, message_attributes) end - # Public: Updates/creates appeal state based on event type - # - # appeal - appeal that was found in appeal_mapper - # event - The module that is being triggered to send a notification - # - # Examples - # - # AppellantNotification.update_appeal_state(appeal, "hearing_postponed") - # # => A new appeal state is created if it doesn't exist - # or the existing appeal state is updated, then appeal_state.hearing_postponed becomes true - - # rubocop:disable Metrics/AbcSize - def self.update_appeal_state(appeal, event) - appeal_type = appeal.class.to_s - appeal_state = AppealState.find_by(appeal_id: appeal.id, appeal_type: appeal_type) || - AppealState.create!(appeal_id: appeal.id, appeal_type: appeal_type) - case event - when "decision_mailed" - appeal_state.update!( - decision_mailed: true, - appeal_docketed: false, - hearing_postponed: false, - hearing_withdrawn: false, - hearing_scheduled: false, - vso_ihp_pending: false, - vso_ihp_complete: false, - privacy_act_pending: false, - privacy_act_complete: false - ) - when "appeal_docketed" - appeal_state.update!(appeal_docketed: true) - when "appeal_cancelled" - appeal_state.update!( - decision_mailed: false, - appeal_docketed: false, - hearing_postponed: false, - hearing_withdrawn: false, - hearing_scheduled: false, - vso_ihp_pending: false, - vso_ihp_complete: false, - privacy_act_pending: false, - privacy_act_complete: false, - scheduled_in_error: false, - appeal_cancelled: true - ) - when "hearing_postponed" - appeal_state.update!(hearing_postponed: true, hearing_scheduled: false) - when "hearing_withdrawn" - appeal_state.update!(hearing_withdrawn: true, hearing_postponed: false, hearing_scheduled: false) - when "hearing_scheduled" - appeal_state.update!(hearing_scheduled: true, hearing_postponed: false, scheduled_in_error: false) - when "scheduled_in_error" - appeal_state.update!(scheduled_in_error: true, hearing_scheduled: false) - when "vso_ihp_pending" - appeal_state.update!(vso_ihp_pending: true, vso_ihp_complete: false) - when "vso_ihp_cancelled" - appeal_state.update!(vso_ihp_pending: false, vso_ihp_complete: false) - when "vso_ihp_complete" - # Only updates appeal state if ALL ihp tasks are completed - if appeal.tasks.open.where(type: IhpColocatedTask.name).empty? && - appeal.tasks.open.where(type: InformalHearingPresentationTask.name).empty? - appeal_state.update!(vso_ihp_complete: true, vso_ihp_pending: false) - end - when "privacy_act_pending" - appeal_state.update!(privacy_act_pending: true, privacy_act_complete: false) - when "privacy_act_complete" - # Only updates appeal state if ALL privacy act tasks are completed - open_tasks = appeal.tasks.open - if open_tasks.where(type: FoiaColocatedTask.name).empty? && - open_tasks.where(type: PrivacyActTask.name).empty? && - open_tasks.where(type: HearingAdminActionFoiaPrivacyRequestTask.name).empty? && - open_tasks.where(type: FoiaRequestMailTask.name).empty? && - open_tasks.where(type: PrivacyActRequestMailTask.name).empty? - appeal_state.update!(privacy_act_complete: true, privacy_act_pending: false) - end - when "privacy_act_cancelled" - # Only updates appeal state if ALL privacy act tasks are completed - open_tasks = appeal.tasks.open - if open_tasks.where(type: FoiaColocatedTask.name).empty? && open_tasks.where(type: PrivacyActTask.name).empty? && - open_tasks.where(type: HearingAdminActionFoiaPrivacyRequestTask.name).empty? && - open_tasks.where(type: FoiaRequestMailTask.name).empty? && - open_tasks.where(type: PrivacyActRequestMailTask.name).empty? - appeal_state.update!(privacy_act_pending: false) - end - end - end - # rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/AbcSize - # Public: Finds the appeal based on the id and type, then calls update_appeal_state to create/update appeal state - # - # appeal_id - id of appeal - # appeal_type - string of appeal object's class (e.g. "LegacyAppeal") - # event - The module that is being triggered to send a notification - # - # Examples - # - # AppellantNotification.appeal_mapper(1, "Appeal", "hearing_postponed") - # # => A new appeal state is created if it doesn't exist - # or the existing appeal state is updated, then appeal_state.hearing_postponed becomes true - - def self.appeal_mapper(appeal_id, appeal_type, event) - if appeal_type == "Appeal" - appeal = Appeal.find_by(id: appeal_id) - AppellantNotification.update_appeal_state(appeal, event) - elsif appeal_type == "LegacyAppeal" - appeal = LegacyAppeal.find_by(id: appeal_id) - AppellantNotification.update_appeal_state(appeal, event) - else - Rails.logger.error("Appeal type not supported for " + event) - end - end # Purpose: Method to check appeal state for statuses and send out a notification based on # which statuses are turned on in the appeal state # @@ -170,7 +45,6 @@ def self.appeal_mapper(appeal_id, appeal_type, event) # appeal_status (only used for quarterly notifications) # # Response: Create notification and return it to SendNotificationJob - # rubocop:disable Metrics/CyclomaticComplexity def self.notify_appellant( appeal, @@ -178,57 +52,49 @@ def self.notify_appellant( appeal_status = nil ) msg_bdy = create_payload(appeal, template_name, appeal_status) - appeal_docketed_event_enabled = FeatureToggle.enabled?(:appeal_docketed_event) - - return nil if template_name == "Appeal docketed" && - !appeal_docketed_event_enabled && - msg_bdy.appeal_type == "LegacyAppeal" - if template_name == "Appeal docketed" && appeal_docketed_event_enabled && msg_bdy.appeal_type == "LegacyAppeal" + if template_name == "Appeal docketed" && msg_bdy.appeal_type == "LegacyAppeal" Notification.create!( appeals_id: msg_bdy.appeal_id, appeals_type: msg_bdy.appeal_type, event_type: template_name, - notification_type: notification_type, + notification_type: "Email and SMS", participant_id: msg_bdy.participant_id, - event_date: Time.zone.today + event_date: Time.zone.today, + notifiable: appeal ) end SendNotificationJob.perform_later(msg_bdy.to_json) end - # rubocop:enable Metrics/CyclomaticComplexity def self.create_payload(appeal, template_name, appeal_status = nil) message_attributes = AppellantNotification.handle_errors(appeal) VANotifySendMessageTemplate.new(message_attributes, template_name, appeal_status) end - def self.notification_type - notification_type = - if FeatureToggle.enabled?(:va_notify_email) && FeatureToggle.enabled?(:va_notify_sms) - "Email and SMS" - elsif FeatureToggle.enabled?(:va_notify_email) - "Email" - elsif FeatureToggle.enabled?(:va_notify_sms) - "SMS" - else - "None" - end - notification_type + def self.get_claimant(appeal) + if appeal.is_a?(Appeal) + appeal.claimant + elsif appeal.is_a?(LegacyAppeal) + appeal.appellant_is_not_veteran ? appeal.person_for_appellant : appeal.veteran + end end - def self.get_claimant(appeal) - appeal_type = appeal.class.to_s - participant_id = appeal.claimant_participant_id - claimant = - if appeal_type == "Appeal" - appeal.claimant - elsif appeal_type == "LegacyAppeal" - veteran = Veteran.find_by(participant_id: participant_id) - person = Person.find_by(participant_id: participant_id) - appeal.appellant_is_not_veteran ? person : veteran + def self.error_handling_messages_and_attributes(appeal, claimant, message_attributes) + begin + if claimant.nil? + fail NoClaimantError, message_attributes[:appeal_id] + elsif message_attributes[:participant_id].blank? + fail NoParticipantIdError, message_attributes[:appeal_id] + elsif appeal.veteran_appellant_deceased? + message_attributes[:status] = "Failure Due to Deceased" + else + message_attributes[:status] = "Success" end - claimant + rescue StandardError => error + Rails.logger.error("#{error.message}\n#{error.backtrace.join("\n")}") + message_attributes[:status] = error.status + end + message_attributes end end -# rubocop:enable Metrics/ModuleLength diff --git a/app/models/prepend/va_notify/docket_hearing_held.rb b/app/models/prepend/va_notify/docket_hearing_held.rb new file mode 100644 index 00000000000..eeca47f8c7b --- /dev/null +++ b/app/models/prepend/va_notify/docket_hearing_held.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +# == Overview +# +# This module is used to intercept events triggered via the Daily Docket page to place a 'Held' +# disposition onto a hearing. +module DocketHearingHeld + # If a hearing is being assigned a disposition of 'Held' then this method will ensure that the + # hearing's appeal's appeal_states record has its hearing_scheduled attribute set back to false, + # as the appeal's latest milestone is no longer that its hearing has been scheduled, but rather that it's + # awaiting a decision. + # + # @return [AdvanceOnDocketMotion] + # If advance_on_docket_motion_attributes is non-nil. + # @see HearingsController#advance_on_docket_motion_params for information on what these params/attributes are. + # @return [nil] + # If advance_on_docket_motion_attributes is nil/blank. + def update_hearing + super_return_value = super + + if hearing_updates[:disposition] == Constants.HEARING_DISPOSITION_TYPES.held + hearing.appeal.appeal_state.hearing_held_appeal_state_update_action! + end + + super_return_value + end +end diff --git a/app/models/prepend/va_notify/docket_hearing_postponed.rb b/app/models/prepend/va_notify/docket_hearing_postponed.rb index ca792c7cb6f..c691869b0dd 100644 --- a/app/models/prepend/va_notify/docket_hearing_postponed.rb +++ b/app/models/prepend/va_notify/docket_hearing_postponed.rb @@ -4,16 +4,13 @@ # For postponed hearings from daily docket page for AMA appeals module DocketHearingPostponed extend AppellantNotification - # rubocop:disable all - @@template_name = "Postponement of hearing" - # rubocop:enable all # AMA Hearing Postponed from the Daily Docket # original method defined in app/models/hearings/forms/hearing_update_form.rb def update_hearing super_return_value = super if hearing_updates[:disposition] == Constants.HEARING_DISPOSITION_TYPES.postponed - AppellantNotification.notify_appellant(hearing.appeal, @@template_name) + AppellantNotification.notify_appellant(hearing.appeal, Constants.EVENT_TYPE_FILTERS.postponement_of_hearing) end super_return_value end diff --git a/app/models/prepend/va_notify/docket_hearing_withdrawn.rb b/app/models/prepend/va_notify/docket_hearing_withdrawn.rb index fbc74bf29f7..096e894ad3c 100644 --- a/app/models/prepend/va_notify/docket_hearing_withdrawn.rb +++ b/app/models/prepend/va_notify/docket_hearing_withdrawn.rb @@ -4,16 +4,13 @@ # For withdrawn hearings from daily docket page for AMA appeals module DocketHearingWithdrawn extend AppellantNotification - # rubocop:disable all - @@template_name = "Withdrawal of hearing" - # rubocop:enable all # AMA Hearing Withdrawn from the Daily Docket # original method defined in app/models/hearings/forms/hearing_update_form.rb def update_hearing super_return_value = super if hearing_updates[:disposition] == Constants.HEARING_DISPOSITION_TYPES.cancelled - AppellantNotification.notify_appellant(hearing.appeal, @@template_name) + AppellantNotification.notify_appellant(hearing.appeal, Constants.EVENT_TYPE_FILTERS.withdrawal_of_hearing) end super_return_value end diff --git a/app/models/prepend/va_notify/hearing_held.rb b/app/models/prepend/va_notify/hearing_held.rb new file mode 100644 index 00000000000..e49a7a8455e --- /dev/null +++ b/app/models/prepend/va_notify/hearing_held.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +# == Overview +# +# This module is used to intercept events triggered either on the Daily Docket or Case Details pages +# that cause a hearing's state to shift (and by extension the appeal's state also). It is particularly +# set up to look for instances where a hearing is being marked with a "Held" disposition. +module HearingHeld + # Legacy Hearing Postponed from the Daily Docket + # original method defined in app/models/legacy_hearing.rb + def update_caseflow_and_vacols(hearing_hash) + original_disposition = vacols_record.hearing_disp + super_return_value = super + new_disposition = vacols_record.hearing_disp + if new_disposition == "H" && original_disposition != new_disposition + appeal = LegacyAppeal.find(appeal_id) + + appeal&.appeal_state&.hearing_held_appeal_state_update_action! + end + super_return_value + end + + # Legacy OR AMA Hearing Marked as "Held" from Queue + # original method defined in app/models/tasks/assign_hearing_disposition_task.rb + def update_hearing(hearing_hash) + super_return_value = super + + if hearing_hash[:disposition] == Constants.HEARING_DISPOSITION_TYPES.held + appeal.appeal_state.hearing_held_appeal_state_update_action! + end + + super_return_value + end + + # Purpose: Callback method when a hearing updates to also update appeal_states table + # + # Params: none + # + # Response: none + def update_appeal_states_on_hearing_held + appeal.appeal_state.hearing_held_appeal_state_update_action! if ama_hearing_held? || legacy_hearing_held? + end + + private + + # Checks to see if the current object is an instance of a Hearing, and if its disposition has + # been set to "Held". + # + # @return [Boolean] + # True if self is an AMA hearing and its disposition is set to "Held". False otherwise. + def ama_hearing_held? + is_a?(Hearing) && disposition == Constants.HEARING_DISPOSITION_TYPES.held + end + + # Checks to see if the current object is an instance of a LegacyHearing, and if its disposition has + # been set to "Held". + # + # @note The dispotion for a legacy hearing is located in the HEARSCHED table's hearing_disp column in + # in the VACOLS database. + # + # @return [Boolean] + # True if self is an AMA hearing and its disposition is set to "Held". False otherwise. + def legacy_hearing_held? + is_a?(LegacyHearing) && VACOLS::CaseHearing.find_by(hearing_pkseq: vacols_id)&.hearing_disp == "H" + end +end diff --git a/app/models/prepend/va_notify/hearing_postponed.rb b/app/models/prepend/va_notify/hearing_postponed.rb index f6544033a5a..24beea63048 100644 --- a/app/models/prepend/va_notify/hearing_postponed.rb +++ b/app/models/prepend/va_notify/hearing_postponed.rb @@ -3,9 +3,6 @@ # Module to notify appellant if Hearing is Postponed module HearingPostponed extend AppellantNotification - # rubocop:disable all - @@template_name = "Postponement of hearing" - # rubocop:enable all # Legacy Hearing Postponed from the Daily Docket # original method defined in app/models/legacy_hearing.rb @@ -15,7 +12,7 @@ def update_caseflow_and_vacols(hearing_hash) new_disposition = vacols_record.hearing_disp if postponed? && original_disposition != new_disposition appeal = LegacyAppeal.find(appeal_id) - AppellantNotification.notify_appellant(appeal, @@template_name) + AppellantNotification.notify_appellant(appeal, Constants.EVENT_TYPE_FILTERS.postponement_of_hearing) end super_return_value end @@ -25,7 +22,7 @@ def update_caseflow_and_vacols(hearing_hash) def update_hearing(hearing_hash) super_return_value = super if hearing_hash[:disposition] == Constants.HEARING_DISPOSITION_TYPES.postponed && appeal.class.to_s == "Appeal" - AppellantNotification.notify_appellant(appeal, @@template_name) + AppellantNotification.notify_appellant(appeal, Constants.EVENT_TYPE_FILTERS.postponement_of_hearing) end super_return_value end @@ -35,23 +32,15 @@ def update_hearing(hearing_hash) # Params: none # # Response: none - # rubocop:disable Metrics/AbcSize def update_appeal_states_on_hearing_postponed if is_a?(LegacyHearing) if VACOLS::CaseHearing.find_by(hearing_pkseq: vacols_id)&.hearing_disp == "P" - MetricsService.record("Updating HEARING_POSTPONED in Appeal States Table for #{appeal.class} ID #{appeal.id}", - name: "AppellantNotification.appeal_mapper") do - AppellantNotification.appeal_mapper(appeal.id, appeal.class.to_s, "hearing_postponed") - end + appeal.appeal_state.hearing_postponed_appeal_state_update_action! end elsif is_a?(Hearing) if disposition == Constants.HEARING_DISPOSITION_TYPES.postponed - MetricsService.record("Updating HEARING_POSTPONED in Appeal States Table for #{appeal.class} ID #{appeal.id}", - name: "AppellantNotification.appeal_mapper") do - AppellantNotification.appeal_mapper(appeal.id, appeal.class.to_s, "hearing_postponed") - end + appeal.appeal_state.hearing_postponed_appeal_state_update_action! end end end - # rubocop:enable Metrics/AbcSize end diff --git a/app/models/prepend/va_notify/hearing_scheduled.rb b/app/models/prepend/va_notify/hearing_scheduled.rb index c82855ae9a0..8d08bb89b6a 100644 --- a/app/models/prepend/va_notify/hearing_scheduled.rb +++ b/app/models/prepend/va_notify/hearing_scheduled.rb @@ -3,14 +3,11 @@ # Module to notify appellant if Hearing is Scheduled module HearingScheduled extend AppellantNotification - # rubocop:disable all - @@template_name = "Hearing scheduled" - # rubocop:enable all def create_hearing(task_values) # original method defined in app/models/tasks/schedule_hearing_task.rb super_return_value = super - AppellantNotification.notify_appellant(appeal, @@template_name) + AppellantNotification.notify_appellant(appeal, Constants.EVENT_TYPE_FILTERS.hearing_scheduled) super_return_value end @@ -20,9 +17,6 @@ def create_hearing(task_values) # # Response: none def update_appeal_states_on_hearing_scheduled - MetricsService.record("Updating HEARING_SCHEDULED in Appeal States Table for #{appeal.class} ID #{appeal.id}", - name: "AppellantNotification.appeal_mapper") do - AppellantNotification.appeal_mapper(appeal.id, appeal.class.to_s, "hearing_scheduled") - end + appeal.appeal_state.hearing_scheduled_appeal_state_update_action! end end diff --git a/app/models/prepend/va_notify/hearing_scheduled_in_error.rb b/app/models/prepend/va_notify/hearing_scheduled_in_error.rb index d7f7f91f536..3bfb3e2d7cc 100644 --- a/app/models/prepend/va_notify/hearing_scheduled_in_error.rb +++ b/app/models/prepend/va_notify/hearing_scheduled_in_error.rb @@ -11,23 +11,15 @@ module HearingScheduledInError # Params: none # # Response: none - # rubocop:disable Metrics/AbcSize def update_appeal_states_on_hearing_scheduled_in_error if is_a?(LegacyHearing) if VACOLS::CaseHearing.find_by(hearing_pkseq: vacols_id)&.hearing_disp == "E" - MetricsService.record("Updating SCHEDULED_IN_ERROR in Appeal States Table for #{appeal.class} ID #{appeal.id}", - name: "AppellantNotification.appeal_mapper") do - AppellantNotification.appeal_mapper(appeal.id, appeal.class.to_s, "scheduled_in_error") - end + appeal.appeal_state.scheduled_in_error_appeal_state_update_action! end elsif is_a?(Hearing) if disposition == Constants.HEARING_DISPOSITION_TYPES.scheduled_in_error - MetricsService.record("Updating SCHEDULED_IN_ERROR in Appeal States Table for #{appeal.class} ID #{appeal.id}", - name: "AppellantNotification.appeal_mapper") do - AppellantNotification.appeal_mapper(appeal.id, appeal.class.to_s, "scheduled_in_error") - end + appeal.appeal_state.scheduled_in_error_appeal_state_update_action! end end end - # rubocop:enable Metrics/AbcSize end diff --git a/app/models/prepend/va_notify/hearing_withdrawn.rb b/app/models/prepend/va_notify/hearing_withdrawn.rb index 82269ad8f03..489eae64dbb 100644 --- a/app/models/prepend/va_notify/hearing_withdrawn.rb +++ b/app/models/prepend/va_notify/hearing_withdrawn.rb @@ -3,16 +3,13 @@ # Module to notify appellant if Hearing is Withdrawn module HearingWithdrawn extend AppellantNotification - # rubocop:disable all - @@template_name = "Withdrawal of hearing" - # rubocop:enable all # Legacy OR AMA Hearing Withdrawn from Queue # original method defined in app/models/tasks/assign_hearing_disposition_task.rb def update_hearing(hearing_hash) super_return_value = super if hearing_hash[:disposition] == Constants.HEARING_DISPOSITION_TYPES.cancelled && appeal.class.to_s == "Appeal" - AppellantNotification.notify_appellant(appeal, @@template_name) + AppellantNotification.notify_appellant(appeal, Constants.EVENT_TYPE_FILTERS.withdrawal_of_hearing) end super_return_value end @@ -25,7 +22,7 @@ def update_caseflow_and_vacols(hearing_hash) new_disposition = vacols_record.hearing_disp if cancelled? && original_disposition != new_disposition appeal = LegacyAppeal.find(appeal_id) - AppellantNotification.notify_appellant(appeal, @@template_name) + AppellantNotification.notify_appellant(appeal, Constants.EVENT_TYPE_FILTERS.withdrawal_of_hearing) end super_return_value end @@ -35,23 +32,15 @@ def update_caseflow_and_vacols(hearing_hash) # Params: none # # Response: none - # rubocop:disable Metrics/AbcSize def update_appeal_states_on_hearing_withdrawn if is_a?(LegacyHearing) if VACOLS::CaseHearing.find_by(hearing_pkseq: vacols_id)&.hearing_disp == "C" - MetricsService.record("Updating HEARING_WITHDRAWN in Appeal States Table for #{appeal.class} ID #{appeal.id}", - name: "AppellantNotification.appeal_mapper") do - AppellantNotification.appeal_mapper(appeal.id, appeal.class.to_s, "hearing_withdrawn") - end + appeal.appeal_state.hearing_withdrawn_appeal_state_update_action! end elsif is_a?(Hearing) if disposition == Constants.HEARING_DISPOSITION_TYPES.cancelled - MetricsService.record("Updating HEARING_WITHDRAWN in Appeal States Table for #{appeal.class} ID #{appeal.id}", - name: "AppellantNotification.appeal_mapper") do - AppellantNotification.appeal_mapper(appeal.id, appeal.class.to_s, "hearing_withdrawn") - end + appeal.appeal_state.hearing_withdrawn_appeal_state_update_action! end end end - # rubocop:enable Metrics/AbcSize end diff --git a/app/models/prepend/va_notify/ihp_task_cancelled.rb b/app/models/prepend/va_notify/ihp_task_cancelled.rb index 908cb8084c5..ae1d3f3722b 100644 --- a/app/models/prepend/va_notify/ihp_task_cancelled.rb +++ b/app/models/prepend/va_notify/ihp_task_cancelled.rb @@ -25,12 +25,7 @@ def update_appeal_state_when_ihp_cancelled if IHP_TYPE_TASKS.include?(type) && !IHP_TYPE_TASKS.include?(parent&.type) && status == Constants.TASK_STATUSES.cancelled - MetricsService.record("Updating VSO_IHP_PENDING column to FALSE & VSO_IHP_COMPLETE column to FALSE in"\ - " Appeal States Table for #{appeal.class} ID #{appeal.id}", - service: nil, - name: "AppellantNotification.appeal_mapper") do - AppellantNotification.appeal_mapper(appeal.id, appeal.class.to_s, "vso_ihp_cancelled") - end + appeal.appeal_state.vso_ihp_cancelled_appeal_state_update_action! end end end diff --git a/app/models/prepend/va_notify/ihp_task_complete.rb b/app/models/prepend/va_notify/ihp_task_complete.rb index 98f17167f1b..92f2935a964 100644 --- a/app/models/prepend/va_notify/ihp_task_complete.rb +++ b/app/models/prepend/va_notify/ihp_task_complete.rb @@ -12,9 +12,6 @@ # within the Appeal States table updated to be TRUE. module IhpTaskComplete extend AppellantNotification - # rubocop:disable all - @@template_name = "VSO IHP complete" - # rubocop:enable all # All variants of IHP Tasks IHP_TYPE_TASKS = %w[IhpColocatedTask InformalHearingPresentationTask].freeze @@ -34,7 +31,7 @@ def update_from_params(params, user) "ID #{appeal.id}", service: nil, name: "AppellantNotification.notify_appellant") do - AppellantNotification.notify_appellant(appeal, @@template_name) + AppellantNotification.notify_appellant(appeal, Constants.EVENT_TYPE_FILTERS.vso_ihp_complete) end end super_return_value @@ -51,12 +48,7 @@ def update_appeal_state_when_ihp_completed if IHP_TYPE_TASKS.include?(type) && !IHP_TYPE_TASKS.include?(parent&.type) && status == Constants.TASK_STATUSES.completed - MetricsService.record("Updating VSO_IHP_COMPLETED column to TRUE & VSO_IHP_PENDING column to FALSE in Appeal"\ - " States Table for #{appeal.class} ID #{appeal.id}", - service: nil, - name: "AppellantNotification.appeal_mapper") do - AppellantNotification.appeal_mapper(appeal.id, appeal.class.to_s, "vso_ihp_complete") - end + appeal.appeal_state.vso_ihp_complete_appeal_state_update_action! end end end diff --git a/app/models/prepend/va_notify/ihp_task_pending.rb b/app/models/prepend/va_notify/ihp_task_pending.rb index 5d6ea9d83eb..6271c77ca4e 100644 --- a/app/models/prepend/va_notify/ihp_task_pending.rb +++ b/app/models/prepend/va_notify/ihp_task_pending.rb @@ -16,9 +16,6 @@ module IhpTaskPending extend AppellantNotification - # rubocop:disable all - @@template_name = "VSO IHP pending" - # rubocop:enable all # All variants of IHP Tasks IHP_TYPE_TASKS = %w[IhpColocatedTask InformalHearingPresentationTask].freeze @@ -38,7 +35,7 @@ def create_ihp_tasks! "for #{@parent.appeal.class} ID #{@parent.appeal.id}", service: nil, name: "AppellantNotification.notify_appellant") do - AppellantNotification.notify_appellant(@parent.appeal, @@template_name) + AppellantNotification.notify_appellant(@parent.appeal, Constants.EVENT_TYPE_FILTERS.vso_ihp_pending) end end super_return_value @@ -61,7 +58,7 @@ def create_from_params(params, user) "ID #{appeal.id}", service: nil, name: "AppellantNotification.notify_appellant") do - AppellantNotification.notify_appellant(appeal, @@template_name) + AppellantNotification.notify_appellant(appeal, Constants.EVENT_TYPE_FILTERS.vso_ihp_pending) end end super_return_value @@ -76,12 +73,7 @@ def create_from_params(params, user) # Response: Update 'vso_ihp_pending' column to True def update_appeal_state_when_ihp_created if IHP_TYPE_TASKS.include?(type) - MetricsService.record("Updating VSO_IHP_PENDING column to TRUE & VSO_IHP_COMPLETE column to FALSE in"\ - " Appeal States Table for #{appeal.class} ID #{appeal.id}", - service: nil, - name: "AppellantNotification.appeal_mapper") do - AppellantNotification.appeal_mapper(appeal.id, appeal.class.to_s, "vso_ihp_pending") - end + appeal.appeal_state.vso_ihp_pending_appeal_state_update_action! end end end diff --git a/app/models/prepend/va_notify/privacy_act_cancelled.rb b/app/models/prepend/va_notify/privacy_act_cancelled.rb index 68735c1509b..1e0d9ba6119 100644 --- a/app/models/prepend/va_notify/privacy_act_cancelled.rb +++ b/app/models/prepend/va_notify/privacy_act_cancelled.rb @@ -18,12 +18,7 @@ def update_appeal_state_when_privacy_act_cancelled if PRIVACY_ACT_TASKS.include?(type) && !PRIVACY_ACT_TASKS.include?(parent&.type) && status == Constants.TASK_STATUSES.cancelled - MetricsService.record("Updating PRIVACY_ACT_PENDING column in Appeal States Table to FALSE "\ - "for #{appeal.class} ID #{appeal.id}", - service: nil, - name: "AppellantNotification.appeal_mapper") do - AppellantNotification.appeal_mapper(appeal.id, appeal.class.to_s, "privacy_act_cancelled") - end + appeal.appeal_state.privacy_act_cancelled_appeal_state_update_action! end end end diff --git a/app/models/prepend/va_notify/privacy_act_complete.rb b/app/models/prepend/va_notify/privacy_act_complete.rb index 16ceabbaa59..f07e0f432e4 100644 --- a/app/models/prepend/va_notify/privacy_act_complete.rb +++ b/app/models/prepend/va_notify/privacy_act_complete.rb @@ -3,9 +3,7 @@ # Module to notify appellant if Privacy Act Request is Completed module PrivacyActComplete extend AppellantNotification - # rubocop:disable all - @@template_name = "Privacy Act request complete" - # rubocop:enable all + PRIVACY_ACT_TASKS = %w[FoiaColocatedTask PrivacyActTask HearingAdminActionFoiaPrivacyRequestTask PrivacyActRequestMailTask FoiaRequestMailTask].freeze @@ -16,7 +14,7 @@ def update_status_if_children_tasks_are_closed(child_task) (type.to_s.include?("PrivacyAct") && !parent&.type.to_s.include?("PrivacyAct"))) && status == Constants.TASK_STATUSES.completed # appellant notification call - AppellantNotification.notify_appellant(appeal, @@template_name) + AppellantNotification.notify_appellant(appeal, Constants.EVENT_TYPE_FILTERS.privacy_act_request_complete) end super_return_value end @@ -27,7 +25,7 @@ def update_with_instructions(params) super_return_value = super if type.to_s == "PrivacyActTask" && assigned_to_type == "Organization" && status == Constants.TASK_STATUSES.completed - AppellantNotification.notify_appellant(appeal, @@template_name) + AppellantNotification.notify_appellant(appeal, Constants.EVENT_TYPE_FILTERS.privacy_act_request_complete) end super_return_value end @@ -36,13 +34,7 @@ def update_appeal_state_when_privacy_act_complete if PRIVACY_ACT_TASKS.include?(type) && !PRIVACY_ACT_TASKS.include?(parent&.type) && status == Constants.TASK_STATUSES.completed - MetricsService.record("updating PRIVACY_ACT_COMPLETE column to true and - PRIVACY_ACT_PENDING column in Appeal States Table to FALSE "\ - "for #{appeal.class} ID #{appeal.id} if no pending Privacy Acts exist", - service: nil, - name: "AppellantNotification.appeal_mapper") do - AppellantNotification.appeal_mapper(appeal.id, appeal.class.to_s, "privacy_act_complete") - end + appeal.appeal_state.privacy_act_complete_appeal_state_update_action! end end end diff --git a/app/models/prepend/va_notify/privacy_act_pending.rb b/app/models/prepend/va_notify/privacy_act_pending.rb index 1e5b34b4e67..81e95cdf55f 100644 --- a/app/models/prepend/va_notify/privacy_act_pending.rb +++ b/app/models/prepend/va_notify/privacy_act_pending.rb @@ -3,9 +3,7 @@ # Module to notify appellant if Privacy Act Request is Pending module PrivacyActPending extend AppellantNotification - # rubocop:disable all - @@template_name = "Privacy Act request pending" - # rubocop:enable all + PRIVACY_ACT_TASKS = %w[FoiaColocatedTask PrivacyActTask HearingAdminActionFoiaPrivacyRequestTask PrivacyActRequestMailTask FoiaRequestMailTask].freeze @@ -13,17 +11,7 @@ module PrivacyActPending def create_privacy_act_task # original method defined in app/models/tasks/foia_colocated_task.rb super_return_value = super - AppellantNotification.notify_appellant(appeal, @@template_name) - super_return_value - end - - # for foia/privacy act mail tasks - # original method defined in app/models/mail_task.rb - def create_twin_of_type(params) - super_return_value = super - if params[:type] == "PrivacyActRequestMailTask" || params[:type] == "FoiaRequestMailTask" - AppellantNotification.notify_appellant(appeal, @@template_name) - end + AppellantNotification.notify_appellant(appeal, Constants.EVENT_TYPE_FILTERS.privacy_act_request_pending) super_return_value end @@ -31,22 +19,32 @@ def create_twin_of_type(params) # original method defined in app/models/task.rb def create_child_task(parent, current_user, params) super_return_value = super - if (params[:type] == "PrivacyActTask" && params[:assigned_to_type].include?("Organization")) || - (params[:type] == "HearingAdminActionFoiaPrivacyRequestTask" && parent.type == "ScheduleHearingTask") - AppellantNotification.notify_appellant(parent.appeal, @@template_name) + if organization_assigned_privacy_task?(params) || + privacy_act_mail_task?(params) || + valid_hearing_admin_foia_privacy_request?(params, parent) + AppellantNotification.notify_appellant(parent.appeal, + Constants.EVENT_TYPE_FILTERS.privacy_act_request_pending) end super_return_value end def update_appeal_state_when_privacy_act_created if PRIVACY_ACT_TASKS.include?(type) && !PRIVACY_ACT_TASKS.include?(parent&.type) - MetricsService.record("Updating PRIVACY_ACT_PENDING column in Appeal States Table to TRUE and - PRIVACY_ACT_COMPLETE to FALSE "\ - "for #{appeal.class} ID #{appeal.id}", - service: nil, - name: "AppellantNotification.appeal_mapper") do - AppellantNotification.appeal_mapper(appeal.id, appeal.class.to_s, "privacy_act_pending") - end + appeal.appeal_state.privacy_act_pending_appeal_state_update_action! end end + + private + + def privacy_act_mail_task?(params) + params[:type] == "PrivacyActRequestMailTask" || params[:type] == "FoiaRequestMailTask" + end + + def organization_assigned_privacy_task?(params) + params[:type] == "PrivacyActTask" && params[:assigned_to_type].include?("Organization") + end + + def valid_hearing_admin_foia_privacy_request?(params, parent) + params[:type] == "HearingAdminActionFoiaPrivacyRequestTask" && parent.type == "ScheduleHearingTask" + end end diff --git a/app/models/tasks/assign_hearing_disposition_task.rb b/app/models/tasks/assign_hearing_disposition_task.rb index d29e63cbbc9..a501e32e7d9 100644 --- a/app/models/tasks/assign_hearing_disposition_task.rb +++ b/app/models/tasks/assign_hearing_disposition_task.rb @@ -23,6 +23,7 @@ class AssignHearingDispositionTask < Task prepend HearingWithdrawn prepend HearingPostponed prepend HearingScheduledInError + prepend HearingHeld validates :parent, presence: true, parentTask: { task_type: HearingTask }, on: :create delegate :hearing, to: :hearing_task, allow_nil: true diff --git a/app/models/tasks/hearing_mail_tasks/hearing_postponement_request_mail_task.rb b/app/models/tasks/hearing_mail_tasks/hearing_postponement_request_mail_task.rb index 9a81594a77c..1d898787a6c 100644 --- a/app/models/tasks/hearing_mail_tasks/hearing_postponement_request_mail_task.rb +++ b/app/models/tasks/hearing_mail_tasks/hearing_postponement_request_mail_task.rb @@ -143,7 +143,7 @@ def reschedule( disposition_task = AssignHearingDispositionTask .create_assign_hearing_disposition_task!(appeal, new_hearing_task, new_hearing) - AppellantNotification.notify_appellant(appeal, "Hearing scheduled") + AppellantNotification.notify_appellant(appeal, Constants.EVENT_TYPE_FILTERS.hearing_scheduled) [new_hearing_task, disposition_task] end diff --git a/app/models/vanotify/notification.rb b/app/models/vanotify/notification.rb index 9607ae597cb..7e40f347af9 100644 --- a/app/models/vanotify/notification.rb +++ b/app/models/vanotify/notification.rb @@ -1,5 +1,9 @@ # frozen_string_literal: true class Notification < CaseflowRecord + belongs_to :notifiable, polymorphic: true + + alias appeal notifiable + self.ignored_columns = ["notification_events_id"] end diff --git a/app/queries/appeals_updated_since_query.rb b/app/queries/appeals_updated_since_query.rb index 3b17101fff8..afe3819b5b3 100644 --- a/app/queries/appeals_updated_since_query.rb +++ b/app/queries/appeals_updated_since_query.rb @@ -23,6 +23,7 @@ def call claims_folder_searches job_notes nod_date_updates + notifications record_synced_by_job request_decision_issues request_issues_updates diff --git a/app/services/external_api/va_notify_service.rb b/app/services/external_api/va_notify_service.rb index 4bd3d1a3e10..5b621763b1d 100644 --- a/app/services/external_api/va_notify_service.rb +++ b/app/services/external_api/va_notify_service.rb @@ -33,12 +33,12 @@ class << self # Return: email_response: JSON response from VA Notify API # rubocop:disable Metrics/ParameterLists def send_email_notifications( - participant_id, - notification_id, - email_template_id, - first_name, - docket_number, - status = "" + participant_id:, + notification_id:, + email_template_id:, + first_name:, + docket_number:, + status: "" ) email_response = send_va_notify_request( email_request(participant_id, notification_id, email_template_id, first_name, docket_number, status) @@ -46,7 +46,7 @@ def send_email_notifications( log_info(email_response) email_response end - # rubocop:enable Metrics/ParameterLists + # Purpose: Send the sms notifications # # Params: Details from appeal for notification @@ -58,8 +58,14 @@ def send_email_notifications( # status: appeal status for quarterly notification (not necessary for other notifications) # Return: sms_response: JSON response from VA Notify API - # rubocop:disable Metrics/ParameterLists - def send_sms_notifications(participant_id, notification_id, sms_template_id, first_name, docket_number, status = "") + def send_sms_notifications( + participant_id:, + notification_id:, + sms_template_id:, + first_name:, + docket_number:, + status: "" + ) sms_response = send_va_notify_request( sms_request(participant_id, notification_id, sms_template_id, first_name, docket_number, status) ) diff --git a/app/workflows/concerns/vbms_document_transaction_concern.rb b/app/workflows/concerns/vbms_document_transaction_concern.rb index e807d377401..564bce04734 100644 --- a/app/workflows/concerns/vbms_document_transaction_concern.rb +++ b/app/workflows/concerns/vbms_document_transaction_concern.rb @@ -4,6 +4,25 @@ module VbmsDocumentTransactionConcern extend ActiveSupport::Concern + # We have to always download the file from s3 to make sure it exists locally + # instead of storing it on the server and relying that it will be there + def pdf_location + S3Service.fetch_file(s3_location, output_location) + output_location + end + + def source + "BVA" + end + + def document_type_id + Document.type_id(document_type) + end + + def cache_file + S3Service.store_file(s3_location, Base64.decode64(document.file)) + end + # :reek:FeatureEnvy def persist_efolder_version_info(response, response_key) document.update!( @@ -30,4 +49,54 @@ def throw_error_if_file_number_not_match_bgs veteran_file_number end end + + private + + def set_processed_at_to_current_time + document.update!(processed_at: Time.zone.now) + end + + def save_rescued_error!(error) + document.update!(error: error, document_version_reference_id: nil) + end + + def s3_location + "#{s3_bucket_by_doc_type}/#{pdf_name}" + end + + def output_location + File.join(Rails.root, "tmp", "pdfs", pdf_name) + end + + def pdf_name + "veteran-#{file_number}-doc-#{document.id}.pdf" + end + + def file_number + document.veteran_file_number + end + + def cleanup_up_file + File.delete(output_location) if File.exist?(output_location) + end + + def log_info(info_message) + uuid = SecureRandom.uuid + Rails.logger.info("#{info_message} ID: #{uuid}") + end + + # Purpose: Get the s3_sub_bucket based on the document type + # S3_SUB_BUCKET was previously a constant defined for this class. + # + # Params: None + # + # Return: string for the sub-bucket + def s3_bucket_by_doc_type + case document_type + when "BVA Case Notifications" + "notification-reports" + else + "idt-uploaded-documents" + end + end end diff --git a/app/workflows/update_document_in_vbms.rb b/app/workflows/update_document_in_vbms.rb index 0100aad5a42..cc96a40bf2b 100644 --- a/app/workflows/update_document_in_vbms.rb +++ b/app/workflows/update_document_in_vbms.rb @@ -18,25 +18,8 @@ def call rescue StandardError => error save_rescued_error!(error.to_s) raise error - end - - # We have to always download the file from s3 to make sure it exists locally - # instead of storing it on the server and relying that it will be there - def pdf_location - S3Service.fetch_file(s3_location, output_location) - output_location - end - - def source - "BVA" - end - - def document_type_id - Document.type_id(document_type) - end - - def cache_file - S3Service.store_file(s3_location, Base64.decode64(document.file)) + ensure + cleanup_up_file end private @@ -63,43 +46,4 @@ def update_in_vbms! document.update!(uploaded_to_vbms_at: Time.zone.now) end - - def set_processed_at_to_current_time - document.update!(processed_at: Time.zone.now) - end - - def save_rescued_error!(error) - document.update!(error: error, document_version_reference_id: nil) - end - - def s3_location - "#{s3_bucket_by_doc_type}/#{pdf_name}" - end - - def output_location - File.join(Rails.root, "tmp", "pdfs", pdf_name) - end - - def pdf_name - "veteran-#{file_number}-doc-#{document.id}.pdf" - end - - def file_number - document.veteran_file_number - end - - # Purpose: Get the s3_sub_bucket based on the document type - # S3_SUB_BUCKET was previously a constant defined for this class. - # - # Params: None - # - # Return: string for the sub-bucket - def s3_bucket_by_doc_type - case document_type - when "BVA Case Notifications" - "notification-reports" - else - "idt-uploaded-documents" - end - end end diff --git a/app/workflows/upload_document_to_vbms.rb b/app/workflows/upload_document_to_vbms.rb index c4cadde87ed..02aee5035f9 100644 --- a/app/workflows/upload_document_to_vbms.rb +++ b/app/workflows/upload_document_to_vbms.rb @@ -19,25 +19,8 @@ def call rescue StandardError => error save_rescued_error!(error.to_s) raise error - end - - # We have to always download the file from s3 to make sure it exists locally - # instead of storing it on the server and relying that it will be there - def pdf_location - S3Service.fetch_file(s3_location, output_location) - output_location - end - - def source - "BVA" - end - - def document_type_id - Document.type_id(document_type) - end - - def cache_file - S3Service.store_file(s3_location, Base64.decode64(document.file)) + ensure + cleanup_up_file end private @@ -64,48 +47,4 @@ def upload_to_vbms! document.update!(uploaded_to_vbms_at: Time.zone.now) end - - def set_processed_at_to_current_time - document.update!(processed_at: Time.zone.now) - end - - def save_rescued_error!(error) - document.update!(error: error) - end - - def s3_location - "#{s3_bucket_by_doc_type}/#{pdf_name}" - end - - def output_location - File.join(Rails.root, "tmp", "pdfs", pdf_name) - end - - def pdf_name - "veteran-#{file_number}-doc-#{document.id}.pdf" - end - - def file_number - document.veteran_file_number - end - - def log_info(info_message) - uuid = SecureRandom.uuid - Rails.logger.info("#{info_message} ID: #{uuid}") - end - - # Purpose: Get the s3_sub_bucket based on the document type - # S3_SUB_BUCKET was previously a constant defined for this class. - # - # Params: None - # - # Return: string for the sub-bucket - def s3_bucket_by_doc_type - case document_type - when "BVA Case Notifications" - "notification-reports" - else - "idt-uploaded-documents" - end - end end diff --git a/config/environments/development.rb b/config/environments/development.rb index 3d5d43787e7..e27e16487b1 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -128,9 +128,6 @@ # One time Appeal States migration for Legacy & AMA Appeal Batch Sizes ENV["STATE_MIGRATION_JOB_BATCH_SIZE"] ||= "1000" - # Quarterly Notifications Batch Sizes - ENV["QUARTERLY_NOTIFICATIONS_JOB_BATCH_SIZE"] ||= "1000" - # Travel Board Sync Batch Size ENV["TRAVEL_BOARD_HEARING_SYNC_BATCH_LIMIT"] ||= "250" diff --git a/config/environments/test.rb b/config/environments/test.rb index df91ac33a6f..83e3a687dd0 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -128,9 +128,6 @@ # One time Appeal States migration for Legacy & AMA Appeal Batch Sizes ENV["STATE_MIGRATION_JOB_BATCH_SIZE"] ||= "1000" - # Quarterly Notifications Batch Sizes - ENV["QUARTERLY_NOTIFICATIONS_JOB_BATCH_SIZE"] ||= "1000" - # Travel Board Sync Batch Size ENV["TRAVEL_BOARD_HEARING_SYNC_BATCH_LIMIT"] ||= "250" diff --git a/config/initializers/shoryuken.rb b/config/initializers/shoryuken.rb index 759c3d8d35c..4d1061d0e96 100644 --- a/config/initializers/shoryuken.rb +++ b/config/initializers/shoryuken.rb @@ -1,6 +1,7 @@ require "#{Rails.root}/app/jobs/middleware/job_monitoring_middleware" require "#{Rails.root}/app/jobs/middleware/job_request_store_middleware" require "#{Rails.root}/app/jobs/middleware/job_sentry_scope_middleware" +require "#{Rails.root}/app/jobs/middleware/job_message_deletion_middleware" # set up default exponential backoff parameters ActiveJob::QueueAdapters::ShoryukenAdapter::JobWrapper @@ -32,5 +33,6 @@ chain.add JobMonitoringMiddleware chain.add JobRequestStoreMiddleware chain.add JobSentryScopeMiddleware + chain.add JobMessageDeletionMiddleware end end diff --git a/db/migrate/20231218144043_add_sms_response_content_and_sms_response_time_to_notifications.rb b/db/migrate/20231218144043_add_sms_response_content_and_sms_response_time_to_notifications.rb new file mode 100644 index 00000000000..d4f6eba36ba --- /dev/null +++ b/db/migrate/20231218144043_add_sms_response_content_and_sms_response_time_to_notifications.rb @@ -0,0 +1,6 @@ +class AddSmsResponseContentAndSmsResponseTimeToNotifications < ActiveRecord::Migration[5.2] + def change + add_column :notifications, :sms_response_content, :string, comment: "Message body of the sms notification response." + add_column :notifications, :sms_response_time, :datetime, comment: "Date and Time of the sms notification response." + end +end diff --git a/db/migrate/20240423172438_add_polymorphic_appeal_association_to_notifications_table.rb b/db/migrate/20240423172438_add_polymorphic_appeal_association_to_notifications_table.rb new file mode 100644 index 00000000000..796023d693e --- /dev/null +++ b/db/migrate/20240423172438_add_polymorphic_appeal_association_to_notifications_table.rb @@ -0,0 +1,7 @@ +class AddPolymorphicAppealAssociationToNotificationsTable < Caseflow::Migration + disable_ddl_transaction! + + def change + add_reference :notifications, :notifiable, polymorphic: true, index: false + end +end diff --git a/db/migrate/20240423180432_add_index_to_polymorphic_appeal_association_in_notification_table.rb b/db/migrate/20240423180432_add_index_to_polymorphic_appeal_association_in_notification_table.rb new file mode 100644 index 00000000000..e39aef226bc --- /dev/null +++ b/db/migrate/20240423180432_add_index_to_polymorphic_appeal_association_in_notification_table.rb @@ -0,0 +1,10 @@ +class AddIndexToPolymorphicAppealAssociationInNotificationTable < ActiveRecord::Migration[6.0] + disable_ddl_transaction! + + def change + add_index :notifications, + [:notifiable_type, :notifiable_id], + name: "index_notifications_on_notifiable_type_and_notifiable_id", + algorithm: :concurrently + end +end diff --git a/db/migrate/20240525120005_create_job_execution_times.rb b/db/migrate/20240525120005_create_job_execution_times.rb new file mode 100644 index 00000000000..3d5b98ab817 --- /dev/null +++ b/db/migrate/20240525120005_create_job_execution_times.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +# Add table to track the Time a Job was Last Executed +class CreateJobExecutionTimes < Caseflow::Migration + def change + create_table :job_execution_times, id: :serial do |t| + t.string "job_name", comment: "Name of the Job whose Last Execution Time is being tracked", index: {unique: true} + t.datetime "last_executed_at", comment: "DateTime value when the Job was Last Executed" + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 01ad93865ba..59400b85aea 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2024_05_01_180157) do +ActiveRecord::Schema.define(version: 2024_05_25_120005) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -1146,6 +1146,12 @@ t.index ["veteran_id"], name: "index_intakes_on_veteran_id" end + create_table "job_execution_times", id: :serial, force: :cascade do |t| + t.string "job_name", comment: "Name of the Job whose Last Execution Time is being tracked" + t.datetime "last_executed_at", comment: "DateTime value when the Job was Last Executed" + t.index ["job_name"], name: "index_job_execution_times_on_job_name", unique: true + end + create_table "job_notes", force: :cascade do |t| t.datetime "created_at", null: false, comment: "Default created_at/updated_at" t.bigint "job_id", null: false, comment: "The job to which the note applies" @@ -1387,6 +1393,8 @@ t.string "email_notification_status", comment: "Status of the Email Notification" t.date "event_date", null: false, comment: "Date of Event" t.string "event_type", null: false, comment: "Type of Event" + t.bigint "notifiable_id" + t.string "notifiable_type" t.text "notification_content", comment: "Full Text Content of Notification" t.string "notification_type", null: false, comment: "Type of Notification that was created" t.datetime "notified_at", comment: "Time Notification was created" @@ -1396,10 +1404,13 @@ t.string "sms_notification_content", comment: "Full SMS Text Content of Notification" t.string "sms_notification_external_id", comment: "VA Notify Notification Id for the sms notification send through their API " t.string "sms_notification_status", comment: "Status of SMS/Text Notification" + t.string "sms_response_content", comment: "Message body of the sms notification response." + t.datetime "sms_response_time", comment: "Date and Time of the sms notification response." t.datetime "updated_at", comment: "TImestamp of when Notification was Updated" t.index ["appeals_id", "appeals_type"], name: "index_appeals_notifications_on_appeals_id_and_appeals_type" t.index ["email_notification_external_id"], name: "index_notifications_on_email_notification_external_id" t.index ["email_notification_status"], name: "index_notifications_on_email_notification_status" + t.index ["notifiable_type", "notifiable_id"], name: "index_notifications_on_notifiable_type_and_notifiable_id" t.index ["participant_id"], name: "index_participant_id" t.index ["sms_notification_external_id"], name: "index_notifications_on_sms_notification_external_id" t.index ["sms_notification_status"], name: "index_notifications_on_sms_notification_status" diff --git a/db/seeds/case_distribution_levers.rb b/db/seeds/case_distribution_levers.rb index e74bc653d66..c1d4bc94f61 100644 --- a/db/seeds/case_distribution_levers.rb +++ b/db/seeds/case_distribution_levers.rb @@ -22,7 +22,7 @@ def seed! validate_levers_creation updated_levers.compact! - puts "#{updated_levers.count} levers updated: #{updated_levers}" if updated_levers.count > 0 + Rails.logger.info("#{updated_levers.count} levers updated: #{updated_levers}") if updated_levers.count > 0 end private @@ -46,9 +46,11 @@ def create_lever(lever) lever_group_order: lever[:lever_group_order] ) - puts "*********************************************" unless lever.valid? - puts lever.errors.full_messages unless lever.valid? - puts "*********************************************" unless lever.valid? + unless lever.valid? + Rails.logger.error( "*********************************************") + Rails.logger.error(lever.errors.full_messages) + Rails.logger.error( "*********************************************") + end end # For properties missing those were intentionally ignored so that they would not @@ -101,8 +103,8 @@ def validate_levers_creation levers = CaseDistributionLevers.levers.map { |lever| lever[:item] } existing_levers = CaseDistributionLever.all.map(&:item) - puts "#{CaseDistributionLever.count} levers exist" - puts "Levers not created #{levers - existing_levers}" if levers.length != existing_levers.length + Rails.logger.info("#{CaseDistributionLever.count} levers exist") + Rails.logger.info("Levers not created #{levers - existing_levers}") if levers.length != existing_levers.length end class << self @@ -689,7 +691,7 @@ def full_update(item) full_update_lever(lever) end - puts "Levers updated: #{levers_to_update.map { |lever| lever[:item] }}" + Rails.logger.info("Levers updated: #{levers_to_update.map { |lever| lever[:item] }}") end private @@ -723,9 +725,11 @@ def full_update_lever(lever) lever_group_order: lever[:lever_group_order] ) - puts "*********************************************" unless existing_lever.valid? - puts existing_lever.errors.full_messages unless existing_lever.valid? - puts "*********************************************" unless existing_lever.valid? + unless lever.valid? + Rails.logger.error( "*********************************************") + Rails.logger.error(lever.errors.full_messages) + Rails.logger.error( "*********************************************") + end end end end diff --git a/db/seeds/notification_events.rb b/db/seeds/notification_events.rb index 3b3ec85c49c..ff50a4e760c 100644 --- a/db/seeds/notification_events.rb +++ b/db/seeds/notification_events.rb @@ -11,17 +11,17 @@ def seed! private def create_notification_events - NotificationEvent.find_or_create_by(event_type: "Quarterly Notification", email_template_id: "d9cf3926-d6b7-4ec7-ba06-a430741db68c", sms_template_id: "44ac639e-e90b-4423-8d7b-acfa8e5131d8") - NotificationEvent.find_or_create_by(event_type: "Appeal docketed", email_template_id: "ae2f0d17-247f-47ee-8f1a-b83a71e0f050", sms_template_id: "9953f7e8-80cb-4fe4-aaef-0309410c84e3") - NotificationEvent.find_or_create_by(event_type: "Appeal decision mailed (Non-contested claims)", email_template_id: "8124f1e1-975b-41f5-ad07-af078f783106", sms_template_id: "78b50f00-6707-464b-b3f9-c87b3f8ed790") - NotificationEvent.find_or_create_by(event_type: "Appeal decision mailed (Contested claims)", email_template_id: "dc4a0400-ee8f-4486-86d8-3b25ec7a43f3", sms_template_id: "ef418229-0c50-4fb1-8a3a-e134acc57bfc") - NotificationEvent.find_or_create_by(event_type: "Hearing scheduled", email_template_id: "27bf814b-f065-4fc8-89af-ae1292db894e", sms_template_id: "c2798da3-4c7a-43ed-bc16-599329eaf7cc") - NotificationEvent.find_or_create_by(event_type: "Withdrawal of hearing", email_template_id: "14b0022f-0431-485b-a188-15f104766ef4", sms_template_id: "ec310973-b013-4b71-ac12-2ac86fb5738a") - NotificationEvent.find_or_create_by(event_type: "Postponement of hearing", email_template_id: "e36fe052-258f-42aa-8b3e-a9aca1cd1c2e", sms_template_id: "27f3aa08-91e2-4e77-9636-5f6cb6bc7574") - NotificationEvent.find_or_create_by(event_type: "Privacy Act request pending", email_template_id: "079ad556-ed04-4491-8661-19cd8b1c537d", sms_template_id: "69047f23-b161-441e-a155-0aeab62a886e") - NotificationEvent.find_or_create_by(event_type: "Privacy Act request complete", email_template_id: "5b7a4450-2d9d-44ad-8691-cc195e3aa5e4", sms_template_id: "48ec08e3-bf86-4329-af2c-943415396699") - NotificationEvent.find_or_create_by(event_type: "VSO IHP pending", email_template_id: "33f1f441-325e-4825-adb3-3bde3393d79d", sms_template_id: "3adcbf09-827d-4d02-af28-864ab2e56b6f") - NotificationEvent.find_or_create_by(event_type: "VSO IHP complete", email_template_id: "33496907-3292-48cb-8543-949023941b4a", sms_template_id: "02bc8052-1a8c-4e55-bb33-66bb2b50ad67") + NotificationEvent.find_or_create_by(event_type: Constants.EVENT_TYPE_FILTERS.quarterly_notification, email_template_id: "d9cf3926-d6b7-4ec7-ba06-a430741db68c", sms_template_id: "44ac639e-e90b-4423-8d7b-acfa8e5131d8") + NotificationEvent.find_or_create_by(event_type: Constants.EVENT_TYPE_FILTERS.appeal_docketed, email_template_id: "ae2f0d17-247f-47ee-8f1a-b83a71e0f050", sms_template_id: "9953f7e8-80cb-4fe4-aaef-0309410c84e3") + NotificationEvent.find_or_create_by(event_type: Constants.EVENT_TYPE_FILTERS.appeal_decision_mailed_non_contested_claims, email_template_id: "8124f1e1-975b-41f5-ad07-af078f783106", sms_template_id: "78b50f00-6707-464b-b3f9-c87b3f8ed790") + NotificationEvent.find_or_create_by(event_type: Constants.EVENT_TYPE_FILTERS.appeal_decision_mailed_contested_claims, email_template_id: "dc4a0400-ee8f-4486-86d8-3b25ec7a43f3", sms_template_id: "ef418229-0c50-4fb1-8a3a-e134acc57bfc") + NotificationEvent.find_or_create_by(event_type: Constants.EVENT_TYPE_FILTERS.hearing_scheduled, email_template_id: "27bf814b-f065-4fc8-89af-ae1292db894e", sms_template_id: "c2798da3-4c7a-43ed-bc16-599329eaf7cc") + NotificationEvent.find_or_create_by(event_type: Constants.EVENT_TYPE_FILTERS.withdrawal_of_hearing, email_template_id: "14b0022f-0431-485b-a188-15f104766ef4", sms_template_id: "ec310973-b013-4b71-ac12-2ac86fb5738a") + NotificationEvent.find_or_create_by(event_type: Constants.EVENT_TYPE_FILTERS.postponement_of_hearing, email_template_id: "e36fe052-258f-42aa-8b3e-a9aca1cd1c2e", sms_template_id: "27f3aa08-91e2-4e77-9636-5f6cb6bc7574") + NotificationEvent.find_or_create_by(event_type: Constants.EVENT_TYPE_FILTERS.privacy_act_request_pending, email_template_id: "079ad556-ed04-4491-8661-19cd8b1c537d", sms_template_id: "69047f23-b161-441e-a155-0aeab62a886e") + NotificationEvent.find_or_create_by(event_type: Constants.EVENT_TYPE_FILTERS.privacy_act_request_complete, email_template_id: "5b7a4450-2d9d-44ad-8691-cc195e3aa5e4", sms_template_id: "48ec08e3-bf86-4329-af2c-943415396699") + NotificationEvent.find_or_create_by(event_type: Constants.EVENT_TYPE_FILTERS.vso_ihp_pending, email_template_id: "33f1f441-325e-4825-adb3-3bde3393d79d", sms_template_id: "3adcbf09-827d-4d02-af28-864ab2e56b6f") + NotificationEvent.find_or_create_by(event_type: Constants.EVENT_TYPE_FILTERS.vso_ihp_complete, email_template_id: "33496907-3292-48cb-8543-949023941b4a", sms_template_id: "02bc8052-1a8c-4e55-bb33-66bb2b50ad67") # Following lines are fake uuids with no template NotificationEvent.find_or_create_by(event_type: "No Participant Id Found", email_template_id: "f54a9779-24b0-46a3-b2c1-494d42db0614", sms_template_id: "663c2b42-3381-46e4-9d48-f336d79901bc") NotificationEvent.find_or_create_by(event_type: "No Claimant Found", email_template_id: "ff871007-1f40-455d-beb3-5f2c71d065fc", sms_template_id: "364dd348-d577-44e8-82de-9fa000d6cd74") diff --git a/db/seeds/notifications.rb b/db/seeds/notifications.rb index 2b9a057ebf3..cd74ebb7c12 100644 --- a/db/seeds/notifications.rb +++ b/db/seeds/notifications.rb @@ -275,213 +275,213 @@ def notification_content def create_notifications # Multiple Notifications for Legacy Appeal 2226048 - Notification.create(appeals_id: "2226048", appeals_type: "LegacyAppeal", event_date: 8.days.ago, event_type: "Appeal docketed", notification_type: "Email and SMS", + Notification.create(appeals_id: "2226048", appeals_type: "LegacyAppeal", event_date: 8.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.appeal_docketed, notification_type: "Email and SMS", recipient_email: "example@example.com", recipient_phone_number: "555-555-5555", email_notification_status: "delivered", sms_notification_status: "delivered", notification_content: notification_content[:appeal_docketed]) - Notification.create(appeals_id: "2226048", appeals_type: "LegacyAppeal", event_date: 7.days.ago, event_type: "Hearing scheduled", notification_type: "Email and SMS", + Notification.create(appeals_id: "2226048", appeals_type: "LegacyAppeal", event_date: 7.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.hearing_scheduled, notification_type: "Email and SMS", recipient_email: "example@example.com", recipient_phone_number: nil, email_notification_status: "delivered", sms_notification_status: "temporary-failure", notification_content: notification_content[:hearing_scheduled]) - Notification.create(appeals_id: "2226048", appeals_type: "LegacyAppeal", event_date: 6.days.ago, event_type: "Privacy Act request pending", notification_type: "Email and SMS", + Notification.create(appeals_id: "2226048", appeals_type: "LegacyAppeal", event_date: 6.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.privacy_act_request_pending, notification_type: "Email and SMS", recipient_email: "example@example.com", recipient_phone_number: nil, email_notification_status: "delivered", sms_notification_status: "temporary-failure", notification_content: notification_content[:privacy_act_pending]) - Notification.create(appeals_id: "2226048", appeals_type: "LegacyAppeal", event_date: 5.days.ago, event_type: "Privacy Act request complete", notification_type: "Email and SMS", + Notification.create(appeals_id: "2226048", appeals_type: "LegacyAppeal", event_date: 5.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.privacy_act_request_complete, notification_type: "Email and SMS", recipient_email: "example@example.com", recipient_phone_number: nil, email_notification_status: "delivered", sms_notification_status: "temporary-failure", notification_content: notification_content[:privacy_act_complete]) - Notification.create(appeals_id: "2226048", appeals_type: "LegacyAppeal", event_date: 4.days.ago, event_type: "Withdrawal of hearing", notification_type: "Email and SMS", + Notification.create(appeals_id: "2226048", appeals_type: "LegacyAppeal", event_date: 4.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.withdrawal_of_hearing, notification_type: "Email and SMS", recipient_email: nil, recipient_phone_number: nil, email_notification_status: "Success", sms_notification_status: "temporary-failure", notification_content: notification_content[:hearing_withdrawn]) - Notification.create(appeals_id: "2226048", appeals_type: "LegacyAppeal", event_date: 3.days.ago, event_type: "VSO IHP pending", notification_type: "Email and SMS", + Notification.create(appeals_id: "2226048", appeals_type: "LegacyAppeal", event_date: 3.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.vso_ihp_pending, notification_type: "Email and SMS", recipient_email: nil, recipient_phone_number: nil, email_notification_status: "Success", sms_notification_status: "Success", notification_content: notification_content[:vso_ihp_pending]) - Notification.create(appeals_id: "2226048", appeals_type: "LegacyAppeal", event_date: 2.days.ago, event_type: "VSO IHP complete", notification_type: "Email and SMS", + Notification.create(appeals_id: "2226048", appeals_type: "LegacyAppeal", event_date: 2.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.vso_ihp_complete, notification_type: "Email and SMS", recipient_email: nil, recipient_phone_number: nil, email_notification_status: "Success", sms_notification_status: "Success", notification_content: notification_content[:vso_ihp_complete]) - Notification.create(appeals_id: "2226048", appeals_type: "LegacyAppeal", event_date: 1.days.ago, event_type: "Appeal decision mailed (Non-contested claims)", + Notification.create(appeals_id: "2226048", appeals_type: "LegacyAppeal", event_date: 1.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.appeal_decision_mailed_non_contested_claims, notification_type: "Email and SMS", recipient_email: nil, recipient_phone_number: nil, email_notification_status: "Success", notification_content: notification_content[:appeal_decision_mailed_non_contested], sms_notification_status: "permanent-failure") # Multiple Notifications for Legacy Appeal 2309289 - Notification.create(appeals_id: "2309289", appeals_type: "LegacyAppeal", event_date: 8.days.ago, event_type: "Appeal docketed", notification_type: "Email and SMS", + Notification.create(appeals_id: "2309289", appeals_type: "LegacyAppeal", event_date: 8.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.appeal_docketed, notification_type: "Email and SMS", recipient_email: "example@example.com", recipient_phone_number: "555-555-5555", email_notification_status: "delivered", sms_notification_status: "delivered", notification_content: notification_content[:appeal_docketed]) - Notification.create(appeals_id: "2309289", appeals_type: "LegacyAppeal", event_date: 7.days.ago, event_type: "Hearing scheduled", notification_type: "Email and SMS", + Notification.create(appeals_id: "2309289", appeals_type: "LegacyAppeal", event_date: 7.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.hearing_scheduled, notification_type: "Email and SMS", recipient_email: "example@example.com", recipient_phone_number: nil, email_notification_status: "delivered", sms_notification_status: "temporary-failure", notification_content: notification_content[:hearing_scheduled]) - Notification.create(appeals_id: "2309289", appeals_type: "LegacyAppeal", event_date: 6.days.ago, event_type: "Privacy Act request pending", notification_type: "Email and SMS", + Notification.create(appeals_id: "2309289", appeals_type: "LegacyAppeal", event_date: 6.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.privacy_act_request_pending, notification_type: "Email and SMS", recipient_email: "example@example.com", recipient_phone_number: nil, email_notification_status: "delivered", sms_notification_status: "temporary-failure", notification_content: notification_content[:privacy_act_pending]) - Notification.create(appeals_id: "2309289", appeals_type: "LegacyAppeal", event_date: 5.days.ago, event_type: "Privacy Act request complete", notification_type: "Email and SMS", + Notification.create(appeals_id: "2309289", appeals_type: "LegacyAppeal", event_date: 5.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.privacy_act_request_complete, notification_type: "Email and SMS", recipient_email: "example@example.com", recipient_phone_number: nil, email_notification_status: "delivered", sms_notification_status: "temporary-failure", notification_content: notification_content[:privacy_act_complete]) - Notification.create(appeals_id: "2309289", appeals_type: "LegacyAppeal", event_date: 4.days.ago, event_type: "Withdrawal of hearing", notification_type: "Email and SMS", + Notification.create(appeals_id: "2309289", appeals_type: "LegacyAppeal", event_date: 4.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.withdrawal_of_hearing, notification_type: "Email and SMS", recipient_email: nil, recipient_phone_number: nil, email_notification_status: "Success", sms_notification_status: "temporary-failure", notification_content: notification_content[:hearing_withdrawn]) - Notification.create(appeals_id: "2309289", appeals_type: "LegacyAppeal", event_date: 3.days.ago, event_type: "VSO IHP pending", notification_type: "Email and SMS", + Notification.create(appeals_id: "2309289", appeals_type: "LegacyAppeal", event_date: 3.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.vso_ihp_pending, notification_type: "Email and SMS", recipient_email: nil, recipient_phone_number: nil, email_notification_status: "Success", sms_notification_status: "Success", notification_content: notification_content[:vso_ihp_pending]) - Notification.create(appeals_id: "2309289", appeals_type: "LegacyAppeal", event_date: 2.days.ago, event_type: "VSO IHP complete", notification_type: "Email and SMS", + Notification.create(appeals_id: "2309289", appeals_type: "LegacyAppeal", event_date: 2.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.vso_ihp_complete, notification_type: "Email and SMS", recipient_email: nil, recipient_phone_number: nil, email_notification_status: "Success", sms_notification_status: "Success", notification_content: notification_content[:vso_ihp_complete]) - Notification.create(appeals_id: "2309289", appeals_type: "LegacyAppeal", event_date: 1.days.ago, event_type: "Appeal decision mailed (Non-contested claims)", + Notification.create(appeals_id: "2309289", appeals_type: "LegacyAppeal", event_date: 1.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.appeal_decision_mailed_non_contested_claims, notification_type: "Email and SMS", recipient_email: nil, recipient_phone_number: nil, email_notification_status: "Success", notification_content: notification_content[:appeal_decision_mailed_non_contested], sms_notification_status: "permanent-failure") # Multiple Notifications for Legacy Appeal 2362049 - Notification.create(appeals_id: "2362049", appeals_type: "LegacyAppeal", event_date: 8.days.ago, event_type: "Appeal docketed", notification_type: "Email and SMS", + Notification.create(appeals_id: "2362049", appeals_type: "LegacyAppeal", event_date: 8.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.appeal_docketed, notification_type: "Email and SMS", recipient_email: "example@example.com", recipient_phone_number: "555-555-5555", email_notification_status: "delivered", sms_notification_status: "delivered", notification_content: notification_content[:appeal_docketed]) - Notification.create(appeals_id: "2362049", appeals_type: "LegacyAppeal", event_date: 7.days.ago, event_type: "Hearing scheduled", notification_type: "Email and SMS", + Notification.create(appeals_id: "2362049", appeals_type: "LegacyAppeal", event_date: 7.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.hearing_scheduled, notification_type: "Email and SMS", recipient_email: "example@example.com", recipient_phone_number: nil, email_notification_status: "delivered", sms_notification_status: "temporary-failure", notification_content: notification_content[:hearing_scheduled]) - Notification.create(appeals_id: "2362049", appeals_type: "LegacyAppeal", event_date: 6.days.ago, event_type: "Privacy Act request pending", notification_type: "Email and SMS", + Notification.create(appeals_id: "2362049", appeals_type: "LegacyAppeal", event_date: 6.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.privacy_act_request_pending, notification_type: "Email and SMS", recipient_email: "example@example.com", recipient_phone_number: nil, email_notification_status: "delivered", sms_notification_status: "temporary-failure", notification_content: notification_content[:privacy_act_pending]) - Notification.create(appeals_id: "2362049", appeals_type: "LegacyAppeal", event_date: 5.days.ago, event_type: "Privacy Act request complete", notification_type: "Email and SMS", + Notification.create(appeals_id: "2362049", appeals_type: "LegacyAppeal", event_date: 5.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.privacy_act_request_complete, notification_type: "Email and SMS", recipient_email: "example@example.com", recipient_phone_number: nil, email_notification_status: "delivered", sms_notification_status: "temporary-failure", notification_content: notification_content[:privacy_act_complete]) - Notification.create(appeals_id: "2362049", appeals_type: "LegacyAppeal", event_date: 4.days.ago, event_type: "Withdrawal of hearing", notification_type: "Email and SMS", + Notification.create(appeals_id: "2362049", appeals_type: "LegacyAppeal", event_date: 4.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.withdrawal_of_hearing, notification_type: "Email and SMS", recipient_email: nil, recipient_phone_number: nil, email_notification_status: "Success", sms_notification_status: "temporary-failure", notification_content: notification_content[:hearing_withdrawn]) - Notification.create(appeals_id: "2362049", appeals_type: "LegacyAppeal", event_date: 3.days.ago, event_type: "VSO IHP pending", notification_type: "Email and SMS", + Notification.create(appeals_id: "2362049", appeals_type: "LegacyAppeal", event_date: 3.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.vso_ihp_pending, notification_type: "Email and SMS", recipient_email: nil, recipient_phone_number: nil, email_notification_status: "Success", sms_notification_status: "Success", notification_content: notification_content[:vso_ihp_pending]) - Notification.create(appeals_id: "2362049", appeals_type: "LegacyAppeal", event_date: 2.days.ago, event_type: "VSO IHP complete", notification_type: "Email and SMS", + Notification.create(appeals_id: "2362049", appeals_type: "LegacyAppeal", event_date: 2.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.vso_ihp_complete, notification_type: "Email and SMS", recipient_email: nil, recipient_phone_number: nil, email_notification_status: "Success", sms_notification_status: "Success", notification_content: notification_content[:vso_ihp_complete]) - Notification.create(appeals_id: "2362049", appeals_type: "LegacyAppeal", event_date: 1.days.ago, event_type: "Appeal decision mailed (Non-contested claims)", + Notification.create(appeals_id: "2362049", appeals_type: "LegacyAppeal", event_date: 1.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.appeal_decision_mailed_non_contested_claims, notification_type: "Email and SMS", recipient_email: nil, recipient_phone_number: nil, email_notification_status: "Success", notification_content: notification_content[:appeal_decision_mailed_non_contested], sms_notification_status: "permanent-failure") # Single Notification for Legacy Appeal 2591483 - Notification.create(appeals_id: "2591483", appeals_type: "LegacyAppeal", event_date: 1.days.ago, event_type: "Appeal docketed", notification_type: "Email and SMS", + Notification.create(appeals_id: "2591483", appeals_type: "LegacyAppeal", event_date: 1.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.appeal_docketed, notification_type: "Email and SMS", recipient_email: nil, recipient_phone_number: nil, email_notification_status: "Success", sms_notification_status: "Success", notification_content: notification_content[:appeal_docketed]) # Single Notification for Legacy Appeal 2687879 - Notification.create(appeals_id: "2687879", appeals_type: "LegacyAppeal", event_date: 1.days.ago, event_type: "Appeal docketed", notification_type: "Email and SMS", + Notification.create(appeals_id: "2687879", appeals_type: "LegacyAppeal", event_date: 1.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.appeal_docketed, notification_type: "Email and SMS", recipient_email: nil, recipient_phone_number: nil, email_notification_status: "Success", sms_notification_status: "Success", notification_content: notification_content[:appeal_docketed]) # Single Notification for Legacy Appeal 2727431 - Notification.create(appeals_id: "2727431", appeals_type: "LegacyAppeal", event_date: 1.days.ago, event_type: "Appeal docketed", notification_type: "Email and SMS", + Notification.create(appeals_id: "2727431", appeals_type: "LegacyAppeal", event_date: 1.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.appeal_docketed, notification_type: "Email and SMS", recipient_email: nil, recipient_phone_number: nil, email_notification_status: "Success", sms_notification_status: "Success", notification_content: notification_content[:appeal_docketed]) # Multiple Notifications for AMA Appeal d31d7f91-91a0-46f8-b4bc-c57e139cee72 - Notification.create(appeals_id: "d31d7f91-91a0-46f8-b4bc-c57e139cee72", appeals_type: "Appeal", event_date: 8.days.ago, event_type: "Appeal docketed", notification_type: "Email and SMS", + Notification.create(appeals_id: "d31d7f91-91a0-46f8-b4bc-c57e139cee72", appeals_type: "Appeal", event_date: 8.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.appeal_docketed, notification_type: "Email and SMS", recipient_email: "example@example.com", recipient_phone_number: "555-555-5555", email_notification_status: "delivered", sms_notification_status: "delivered", notification_content: notification_content[:appeal_docketed]) - Notification.create(appeals_id: "d31d7f91-91a0-46f8-b4bc-c57e139cee72", appeals_type: "Appeal", event_date: 7.days.ago, event_type: "Hearing scheduled", notification_type: "Email and SMS", + Notification.create(appeals_id: "d31d7f91-91a0-46f8-b4bc-c57e139cee72", appeals_type: "Appeal", event_date: 7.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.hearing_scheduled, notification_type: "Email and SMS", recipient_email: "example@example.com", recipient_phone_number: nil, email_notification_status: "delivered", sms_notification_status: "temporary-failure", notification_content: notification_content[:hearing_scheduled]) - Notification.create(appeals_id: "d31d7f91-91a0-46f8-b4bc-c57e139cee72", appeals_type: "Appeal", event_date: 6.days.ago, event_type: "Privacy Act request pending", notification_type: "Email and SMS", + Notification.create(appeals_id: "d31d7f91-91a0-46f8-b4bc-c57e139cee72", appeals_type: "Appeal", event_date: 6.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.privacy_act_request_pending, notification_type: "Email and SMS", recipient_email: "example@example.com", recipient_phone_number: nil, email_notification_status: "delivered", sms_notification_status: "temporary-failure", notification_content: notification_content[:privacy_act_pending]) - Notification.create(appeals_id: "d31d7f91-91a0-46f8-b4bc-c57e139cee72", appeals_type: "Appeal", event_date: 5.days.ago, event_type: "Privacy Act request complete", notification_type: "Email and SMS", + Notification.create(appeals_id: "d31d7f91-91a0-46f8-b4bc-c57e139cee72", appeals_type: "Appeal", event_date: 5.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.privacy_act_request_complete, notification_type: "Email and SMS", recipient_email: "example@example.com", recipient_phone_number: nil, email_notification_status: "delivered", sms_notification_status: "temporary-failure", notification_content: notification_content[:privacy_act_complete]) - Notification.create(appeals_id: "d31d7f91-91a0-46f8-b4bc-c57e139cee72", appeals_type: "Appeal", event_date: 4.days.ago, event_type: "Withdrawal of hearing", notification_type: "Email and SMS", + Notification.create(appeals_id: "d31d7f91-91a0-46f8-b4bc-c57e139cee72", appeals_type: "Appeal", event_date: 4.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.withdrawal_of_hearing, notification_type: "Email and SMS", recipient_email: nil, recipient_phone_number: nil, email_notification_status: "Success", sms_notification_status: "temporary-failure", notification_content: notification_content[:hearing_withdrawn]) - Notification.create(appeals_id: "d31d7f91-91a0-46f8-b4bc-c57e139cee72", appeals_type: "Appeal", event_date: 3.days.ago, event_type: "VSO IHP pending", notification_type: "Email and SMS", + Notification.create(appeals_id: "d31d7f91-91a0-46f8-b4bc-c57e139cee72", appeals_type: "Appeal", event_date: 3.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.vso_ihp_pending, notification_type: "Email and SMS", recipient_email: nil, recipient_phone_number: nil, email_notification_status: "Success", sms_notification_status: "Success", notification_content: notification_content[:vso_ihp_pending]) - Notification.create(appeals_id: "d31d7f91-91a0-46f8-b4bc-c57e139cee72", appeals_type: "Appeal", event_date: 2.days.ago, event_type: "VSO IHP complete", notification_type: "Email and SMS", + Notification.create(appeals_id: "d31d7f91-91a0-46f8-b4bc-c57e139cee72", appeals_type: "Appeal", event_date: 2.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.vso_ihp_complete, notification_type: "Email and SMS", recipient_email: nil, recipient_phone_number: nil, email_notification_status: "Success", sms_notification_status: "Success", notification_content: notification_content[:vso_ihp_complete]) - Notification.create(appeals_id: "d31d7f91-91a0-46f8-b4bc-c57e139cee72", appeals_type: "Appeal", event_date: 1.days.ago, event_type: "Appeal decision mailed (Non-contested claims)", + Notification.create(appeals_id: "d31d7f91-91a0-46f8-b4bc-c57e139cee72", appeals_type: "Appeal", event_date: 1.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.appeal_decision_mailed_non_contested_claims, notification_type: "Email and SMS", recipient_email: nil, recipient_phone_number: nil, email_notification_status: "Success", notification_content: notification_content[:appeal_decision_mailed_non_contested], sms_notification_status: "permanent-failure") # Multiple Notifications for AMA Appeal 25c4857b-3cc5-4497-a066-25be73aa4b6b - Notification.create(appeals_id: "25c4857b-3cc5-4497-a066-25be73aa4b6b", appeals_type: "Appeal", event_date: 8.days.ago, event_type: "Appeal docketed", notification_type: "Email and SMS", + Notification.create(appeals_id: "25c4857b-3cc5-4497-a066-25be73aa4b6b", appeals_type: "Appeal", event_date: 8.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.appeal_docketed, notification_type: "Email and SMS", recipient_email: "example@example.com", recipient_phone_number: "555-555-5555", email_notification_status: "delivered", sms_notification_status: "delivered", notification_content: notification_content[:appeal_docketed]) - Notification.create(appeals_id: "25c4857b-3cc5-4497-a066-25be73aa4b6b", appeals_type: "Appeal", event_date: 7.days.ago, event_type: "Hearing scheduled", notification_type: "Email and SMS", + Notification.create(appeals_id: "25c4857b-3cc5-4497-a066-25be73aa4b6b", appeals_type: "Appeal", event_date: 7.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.hearing_scheduled, notification_type: "Email and SMS", recipient_email: "example@example.com", recipient_phone_number: nil, email_notification_status: "delivered", sms_notification_status: "temporary-failure", notification_content: notification_content[:hearing_scheduled]) - Notification.create(appeals_id: "25c4857b-3cc5-4497-a066-25be73aa4b6b", appeals_type: "Appeal", event_date: 6.days.ago, event_type: "Privacy Act request pending", notification_type: "Email and SMS", + Notification.create(appeals_id: "25c4857b-3cc5-4497-a066-25be73aa4b6b", appeals_type: "Appeal", event_date: 6.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.privacy_act_request_pending, notification_type: "Email and SMS", recipient_email: "example@example.com", recipient_phone_number: nil, email_notification_status: "delivered", sms_notification_status: "temporary-failure", notification_content: notification_content[:privacy_act_pending]) - Notification.create(appeals_id: "25c4857b-3cc5-4497-a066-25be73aa4b6b", appeals_type: "Appeal", event_date: 5.days.ago, event_type: "Privacy Act request complete", notification_type: "Email and SMS", + Notification.create(appeals_id: "25c4857b-3cc5-4497-a066-25be73aa4b6b", appeals_type: "Appeal", event_date: 5.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.privacy_act_request_complete, notification_type: "Email and SMS", recipient_email: "example@example.com", recipient_phone_number: nil, email_notification_status: "delivered", sms_notification_status: "temporary-failure", notification_content: notification_content[:privacy_act_complete]) - Notification.create(appeals_id: "25c4857b-3cc5-4497-a066-25be73aa4b6b", appeals_type: "Appeal", event_date: 4.days.ago, event_type: "Withdrawal of hearing", notification_type: "Email and SMS", + Notification.create(appeals_id: "25c4857b-3cc5-4497-a066-25be73aa4b6b", appeals_type: "Appeal", event_date: 4.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.withdrawal_of_hearing, notification_type: "Email and SMS", recipient_email: nil, recipient_phone_number: nil, email_notification_status: "Success", sms_notification_status: "temporary-failure", notification_content: notification_content[:hearing_withdrawn]) - Notification.create(appeals_id: "25c4857b-3cc5-4497-a066-25be73aa4b6b", appeals_type: "Appeal", event_date: 3.days.ago, event_type: "VSO IHP pending", notification_type: "Email and SMS", + Notification.create(appeals_id: "25c4857b-3cc5-4497-a066-25be73aa4b6b", appeals_type: "Appeal", event_date: 3.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.vso_ihp_pending, notification_type: "Email and SMS", recipient_email: nil, recipient_phone_number: nil, email_notification_status: "Success", sms_notification_status: "Success", notification_content: notification_content[:vso_ihp_pending]) - Notification.create(appeals_id: "25c4857b-3cc5-4497-a066-25be73aa4b6b", appeals_type: "Appeal", event_date: 2.days.ago, event_type: "VSO IHP complete", notification_type: "Email and SMS", + Notification.create(appeals_id: "25c4857b-3cc5-4497-a066-25be73aa4b6b", appeals_type: "Appeal", event_date: 2.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.vso_ihp_complete, notification_type: "Email and SMS", recipient_email: nil, recipient_phone_number: nil, email_notification_status: "Success", sms_notification_status: "Success", notification_content: notification_content[:vso_ihp_complete]) - Notification.create(appeals_id: "25c4857b-3cc5-4497-a066-25be73aa4b6b", appeals_type: "Appeal", event_date: 1.days.ago, event_type: "Appeal decision mailed (Non-contested claims)", + Notification.create(appeals_id: "25c4857b-3cc5-4497-a066-25be73aa4b6b", appeals_type: "Appeal", event_date: 1.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.appeal_decision_mailed_non_contested_claims, notification_type: "Email and SMS", recipient_email: nil, recipient_phone_number: nil, email_notification_status: "Success", notification_content: notification_content[:appeal_decision_mailed_non_contested], sms_notification_status: "permanent-failure") # Multiple Notifications for AMA Appeal 7a060e04-1143-4e42-9ede-bdc42877f4f8 - Notification.create(appeals_id: "7a060e04-1143-4e42-9ede-bdc42877f4f8", appeals_type: "Appeal", event_date: 8.days.ago, event_type: "Appeal docketed", notification_type: "Email and SMS", + Notification.create(appeals_id: "7a060e04-1143-4e42-9ede-bdc42877f4f8", appeals_type: "Appeal", event_date: 8.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.appeal_docketed, notification_type: "Email and SMS", recipient_email: "example@example.com", recipient_phone_number: "555-555-5555", email_notification_status: "delivered", sms_notification_status: "delivered", notification_content: notification_content[:appeal_docketed]) - Notification.create(appeals_id: "7a060e04-1143-4e42-9ede-bdc42877f4f8", appeals_type: "Appeal", event_date: 7.days.ago, event_type: "Hearing scheduled", notification_type: "Email and SMS", + Notification.create(appeals_id: "7a060e04-1143-4e42-9ede-bdc42877f4f8", appeals_type: "Appeal", event_date: 7.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.hearing_scheduled, notification_type: "Email and SMS", recipient_email: "example@example.com", recipient_phone_number: nil, email_notification_status: "delivered", sms_notification_status: "temporary-failure", notification_content: notification_content[:hearing_scheduled]) - Notification.create(appeals_id: "7a060e04-1143-4e42-9ede-bdc42877f4f8", appeals_type: "Appeal", event_date: 6.days.ago, event_type: "Privacy Act request pending", notification_type: "Email and SMS", + Notification.create(appeals_id: "7a060e04-1143-4e42-9ede-bdc42877f4f8", appeals_type: "Appeal", event_date: 6.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.privacy_act_request_pending, notification_type: "Email and SMS", recipient_email: "example@example.com", recipient_phone_number: nil, email_notification_status: "delivered", sms_notification_status: "temporary-failure", notification_content: notification_content[:privacy_act_pending]) - Notification.create(appeals_id: "7a060e04-1143-4e42-9ede-bdc42877f4f8", appeals_type: "Appeal", event_date: 5.days.ago, event_type: "Privacy Act request complete", notification_type: "Email and SMS", + Notification.create(appeals_id: "7a060e04-1143-4e42-9ede-bdc42877f4f8", appeals_type: "Appeal", event_date: 5.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.privacy_act_request_complete, notification_type: "Email and SMS", recipient_email: "example@example.com", recipient_phone_number: nil, email_notification_status: "delivered", sms_notification_status: "temporary-failure", notification_content: notification_content[:privacy_act_complete]) - Notification.create(appeals_id: "7a060e04-1143-4e42-9ede-bdc42877f4f8", appeals_type: "Appeal", event_date: 4.days.ago, event_type: "Withdrawal of hearing", notification_type: "Email and SMS", + Notification.create(appeals_id: "7a060e04-1143-4e42-9ede-bdc42877f4f8", appeals_type: "Appeal", event_date: 4.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.withdrawal_of_hearing, notification_type: "Email and SMS", recipient_email: nil, recipient_phone_number: nil, email_notification_status: "Success", sms_notification_status: "temporary-failure", notification_content: notification_content[:hearing_withdrawn]) - Notification.create(appeals_id: "7a060e04-1143-4e42-9ede-bdc42877f4f8", appeals_type: "Appeal", event_date: 3.days.ago, event_type: "VSO IHP pending", notification_type: "Email and SMS", + Notification.create(appeals_id: "7a060e04-1143-4e42-9ede-bdc42877f4f8", appeals_type: "Appeal", event_date: 3.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.vso_ihp_pending, notification_type: "Email and SMS", recipient_email: nil, recipient_phone_number: nil, email_notification_status: "Success", sms_notification_status: "Success", notification_content: notification_content[:vso_ihp_pending]) - Notification.create(appeals_id: "7a060e04-1143-4e42-9ede-bdc42877f4f8", appeals_type: "Appeal", event_date: 2.days.ago, event_type: "VSO IHP complete", notification_type: "Email and SMS", + Notification.create(appeals_id: "7a060e04-1143-4e42-9ede-bdc42877f4f8", appeals_type: "Appeal", event_date: 2.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.vso_ihp_complete, notification_type: "Email and SMS", recipient_email: nil, recipient_phone_number: nil, email_notification_status: "Success", sms_notification_status: "Success", notification_content: notification_content[:vso_ihp_complete]) - Notification.create(appeals_id: "7a060e04-1143-4e42-9ede-bdc42877f4f8", appeals_type: "Appeal", event_date: 1.days.ago, event_type: "Appeal decision mailed (Non-contested claims)", + Notification.create(appeals_id: "7a060e04-1143-4e42-9ede-bdc42877f4f8", appeals_type: "Appeal", event_date: 1.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.appeal_decision_mailed_non_contested_claims, notification_type: "Email and SMS", recipient_email: nil, recipient_phone_number: nil, email_notification_status: "Success", notification_content: notification_content[:appeal_decision_mailed_non_contested], sms_notification_status: "permanent-failure") # Single Notification for AMA Appeal 952b6490-a10a-484b-a29b-31489e9a6e5a - Notification.create(appeals_id: "952b6490-a10a-484b-a29b-31489e9a6e5a", appeals_type: "Appeal", event_date: 8.days.ago, event_type: "Appeal docketed", notification_type: "Email and SMS", + Notification.create(appeals_id: "952b6490-a10a-484b-a29b-31489e9a6e5a", appeals_type: "Appeal", event_date: 8.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.appeal_docketed, notification_type: "Email and SMS", recipient_email: "example@example.com", recipient_phone_number: nil, email_notification_status: "delivered", sms_notification_status: "permanent-failure", notification_content: notification_content[:appeal_docketed]) # Single Notification for AMA Appeal fb3b029f-f07e-45bf-9277-809b44f7451a - Notification.create(appeals_id: "fb3b029f-f07e-45bf-9277-809b44f7451a", appeals_type: "Appeal", event_date: 8.days.ago, event_type: "Appeal docketed", notification_type: "Email and SMS", + Notification.create(appeals_id: "fb3b029f-f07e-45bf-9277-809b44f7451a", appeals_type: "Appeal", event_date: 8.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.appeal_docketed, notification_type: "Email and SMS", recipient_email: "example@example.com", recipient_phone_number: nil, email_notification_status: "delivered", sms_notification_status: "permanent-failure", notification_content: notification_content[:appeal_docketed]) # Single Notification for AMA Appeal 2b3afced-f698-4abe-84f9-6d44f26d20d4 - Notification.create(appeals_id: "2b3afced-f698-4abe-84f9-6d44f26d20d4", appeals_type: "Appeal", event_date: 8.days.ago, event_type: "Appeal docketed", notification_type: "Email and SMS", + Notification.create(appeals_id: "2b3afced-f698-4abe-84f9-6d44f26d20d4", appeals_type: "Appeal", event_date: 8.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.appeal_docketed, notification_type: "Email and SMS", recipient_email: "example@example.com", recipient_phone_number: nil, email_notification_status: "delivered", sms_notification_status: "permanent-failure", notification_content: notification_content[:appeal_docketed]) # Notifications of No Participant Id Found, No Claimant Found, and No External Id for Legacy Appeal 3565723 3565723 - Notification.create(appeals_id: "3565723", appeals_type: "LegacyAppeal", event_date: 8.days.ago, event_type: "Appeal docketed", notification_type: "Email and SMS", + Notification.create(appeals_id: "3565723", appeals_type: "LegacyAppeal", event_date: 8.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.appeal_docketed, notification_type: "Email and SMS", recipient_email: "example@example.com", recipient_phone_number: "555-555-5555", email_notification_status: "No Participant Id Found", sms_notification_status: "No Participant Id Found") - Notification.create(appeals_id: "3565723", appeals_type: "LegacyAppeal", event_date: 7.days.ago, event_type: "Hearing scheduled", notification_type: "Email and SMS", + Notification.create(appeals_id: "3565723", appeals_type: "LegacyAppeal", event_date: 7.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.hearing_scheduled, notification_type: "Email and SMS", recipient_email: "example@example.com", recipient_phone_number: "555-555-5555", email_notification_status: "No Claimant Found", sms_notification_status: "No Claimant Found") - Notification.create(appeals_id: "3565723", appeals_type: "LegacyAppeal", event_date: 6.days.ago, event_type: "Privacy Act request pending", notification_type: "Email and SMS", + Notification.create(appeals_id: "3565723", appeals_type: "LegacyAppeal", event_date: 6.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.privacy_act_request_pending, notification_type: "Email and SMS", recipient_email: "example@example.com", recipient_phone_number: "555-555-5555", email_notification_status: "No External Id", sms_notification_status: "No External Id") # Notifications of No Participant Id Found, No Claimant Found, and No External Id for AMA Appeal ea2303e9-2bab-472b-a653-94b71bca8ca3 3565723 - Notification.create(appeals_id: "ea2303e9-2bab-472b-a653-94b71bca8ca3", appeals_type: "Appeal", event_date: 8.days.ago, event_type: "Appeal docketed", notification_type: "Email and SMS", + Notification.create(appeals_id: "ea2303e9-2bab-472b-a653-94b71bca8ca3", appeals_type: "Appeal", event_date: 8.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.appeal_docketed, notification_type: "Email and SMS", recipient_email: "example@example.com", recipient_phone_number: "555-555-5555", email_notification_status: "No Participant Id Found", sms_notification_status: "No Participant Id Found") - Notification.create(appeals_id: "ea2303e9-2bab-472b-a653-94b71bca8ca3", appeals_type: "Appeal", event_date: 7.days.ago, event_type: "Hearing scheduled", notification_type: "Email and SMS", + Notification.create(appeals_id: "ea2303e9-2bab-472b-a653-94b71bca8ca3", appeals_type: "Appeal", event_date: 7.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.hearing_scheduled, notification_type: "Email and SMS", recipient_email: "example@example.com", recipient_phone_number: "555-555-5555", email_notification_status: "No Claimant Found", sms_notification_status: "No Claimant Found") - Notification.create(appeals_id: "ea2303e9-2bab-472b-a653-94b71bca8ca3", appeals_type: "Appeal", event_date: 6.days.ago, event_type: "Privacy Act request pending", notification_type: "Email and SMS", + Notification.create(appeals_id: "ea2303e9-2bab-472b-a653-94b71bca8ca3", appeals_type: "Appeal", event_date: 6.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.privacy_act_request_pending, notification_type: "Email and SMS", recipient_email: "example@example.com", recipient_phone_number: "555-555-5555", email_notification_status: "No External Id", sms_notification_status: "No External Id") end end diff --git a/lib/caseflow/error.rb b/lib/caseflow/error.rb index b0e23c065bb..08329f63cd6 100644 --- a/lib/caseflow/error.rb +++ b/lib/caseflow/error.rb @@ -486,4 +486,10 @@ def ignorable? true end end + + class MaximumBatchSizeViolationError < StandardError + def initialize(msg = "The batch size of jobs must not exceed 10") + super(msg) + end + end end diff --git a/lib/fakes/va_notify_service.rb b/lib/fakes/va_notify_service.rb index 5574ee91e3a..1dc5d00999a 100644 --- a/lib/fakes/va_notify_service.rb +++ b/lib/fakes/va_notify_service.rb @@ -4,23 +4,23 @@ class Fakes::VANotifyService < ExternalApi::VANotifyService class << self # rubocop:disable Metrics/ParameterLists def send_email_notifications( - participant_id, - notification_id, - email_template_id, - first_name, - docket_number, - status = "" + participant_id:, + notification_id:, + email_template_id:, + first_name:, + docket_number:, + status: "" ) fake_notification_response(email_template_id) end def send_sms_notifications( - participant_id, - notification_id, - sms_template_id, - first_name, - docket_number, - status = "" + participant_id:, + notification_id:, + sms_template_id:, + first_name:, + docket_number:, + status: "" ) if participant_id.length.nil? return bad_participant_id_response diff --git a/lib/tasks/appeal_state_synchronizer.rake b/lib/tasks/appeal_state_synchronizer.rake new file mode 100644 index 00000000000..c38299913f6 --- /dev/null +++ b/lib/tasks/appeal_state_synchronizer.rake @@ -0,0 +1,143 @@ +# frozen_string_literal: true + +namespace :appeal_state_synchronizer do + desc "Used to synchronize appeal_states table using data from other sources." + task sync_appeal_states: :environment do + Rails.application.eager_load! + + adjust_legacy_hearing_statuses + adjust_ama_hearing_statuses + locate_unrecorded_docketed_states + backfill_appeal_information + end + + def map_appeal_hearing_scheduled_state(appeal_state) + if !appeal_state.appeal&.hearings&.empty? && appeal_state.appeal.hearings.max_by(&:scheduled_for).disposition.nil? + return { hearing_scheduled: true } + end + + { hearing_scheduled: false } + end + + def map_appeal_hearing_postponed_state(appeal_state) + if appeal_state.appeal.hearings&.max_by(&:scheduled_for)&.disposition == + Constants.HEARING_DISPOSITION_TYPES.postponed + { hearing_postponed: true } + else + { hearing_postponed: false } + end + end + + def map_appeal_hearing_scheduled_in_error_state(appeal_state) + if appeal_state.appeal.hearings&.max_by(&:scheduled_for)&.disposition == + Constants.HEARING_DISPOSITION_TYPES.scheduled_in_error + { scheduled_in_error: true } + else + { scheduled_in_error: false } + end + end + + def map_appeal_hearing_withdrawn_state(appeal_state) + if appeal_state.appeal.hearings&.max_by(&:scheduled_for)&.disposition == + Constants.HEARING_DISPOSITION_TYPES.cancelled + { hearing_withdrawn: true } + else + { hearing_withdrawn: false } + end + end + + # Looks at the latest legacy hearings (in the VACOLS HEARSCHED table via the HearingRepository class) + # to see if a disposition was placed onto a hearing without Caseflow having registered that event. + def adjust_legacy_hearing_statuses + relations = [ + AppealState.eligible_for_quarterly.where(hearing_scheduled: true, appeal_type: "LegacyAppeal"), + AppealState.eligible_for_quarterly.hearing_to_be_rescheduled.where(appeal_type: "LegacyAppeal") + ] + + Parallel.each(relations, in_processes: 2) do |relation| + Parallel.each(relation, in_threads: 10) do |appeal_state| + RequestStore[:current_user] = User.system_user + + hs_state = map_appeal_hearing_scheduled_state(appeal_state) + hp_state = map_appeal_hearing_postponed_state(appeal_state) + sie_state = map_appeal_hearing_scheduled_in_error_state(appeal_state) + w_state = map_appeal_hearing_withdrawn_state(appeal_state) + + appeal_state.update!([hs_state, hp_state, sie_state, w_state].inject(&:merge)) + end + end + end + + def locate_unrecorded_docketed_states + appeals_missing_states = LegacyAppeal.find_by_sql( + <<-SQL + SELECT DISTINCT la.* + FROM legacy_appeals la + JOIN notifications n ON la.vacols_id = n.appeals_id AND event_type = 'Appeal docketed' + LEFT JOIN appeal_states states ON la.id = states.appeal_id AND states.appeal_type = 'LegacyAppeal' + WHERE states.id IS NULL + SQL + ) + + Parallel.each(appeals_missing_states, in_threads: 10) do |appeal| + # It's necessary to have a current user set whenever creating appeal_states records + # as created_by_id is a required field, AND it's derived from RequestStore[:current_user] + # in some higher environments. This must be done in each thread since a RequestStore instance's + # contents are scoped to each thread. + RequestStore[:current_user] = User.system_user + + appeal.appeal_state.appeal_docketed_appeal_state_update_action! + end + end + + def incorrect_hearing_scheduled_appeal_states_query + <<-SQL + SELECT DISTINCT * + FROM appeal_states + WHERE hearing_scheduled IS TRUE + AND appeal_type = 'Appeal' + AND id NOT IN ( + SELECT s.id + FROM appeals + INNER JOIN tasks ON appeals.id = tasks.appeal_id + INNER JOIN hearings h ON appeals.id = h.appeal_id + INNER JOIN appeal_states s ON s.appeal_id = appeals.id AND s.appeal_type = 'Appeal' + WHERE tasks.appeal_type = 'Appeal' + AND tasks.type = 'AssignHearingDispositionTask' + AND tasks.status = 'assigned' + AND appeals.docket_type = 'hearing' + AND h.disposition IS NULL + ) + SQL + end + + def adjust_ama_hearing_statuses + incorrect_appeal_states = AppealState.find_by_sql(incorrect_hearing_scheduled_appeal_states_query) + + Parallel.each(incorrect_appeal_states, in_threads: 10) do |state_to_correct| + RequestStore[:current_user] = User.system_user + + state_to_correct.update!(hearing_scheduled: false) + end + end + + def backfill_appeal_information + updates_to_make = {} + + Notification.where(notifiable_id: nil) + .or(Notification.where(notifiable_type: nil)) + .find_in_batches(batch_size: 10_000) do |notification_batch| + notification_batch.index_by(&:id).each do |id, notification_row| + appeal = Appeal.find_appeal_by_uuid_or_find_or_create_legacy_appeal_by_vacols_id(notification_row.appeals_id) + updates_to_make[id] = { id: id, notifiable: appeal } + end + + Notification.update(updates_to_make.keys, updates_to_make.values) + + rescue StandardError => error + Rails.logger.error("#{notification.id} couldn't have its appeal set because of #{error.message}") + ensure + updates_to_make = {} + end + end +end diff --git a/spec/controllers/api/v1/va_notify_controller_spec.rb b/spec/controllers/api/v1/va_notify_controller_spec.rb index d20084033ba..895b7bd04f9 100644 --- a/spec/controllers/api/v1/va_notify_controller_spec.rb +++ b/spec/controllers/api/v1/va_notify_controller_spec.rb @@ -13,7 +13,7 @@ appeals_id: appeal.uuid, appeals_type: "Appeal", event_date: "2023-02-27 13:11:51.91467", - event_type: "Quarterly Notification", + event_type: Constants.EVENT_TYPE_FILTERS.quarterly_notification, notification_type: "Email", notified_at: "2023-02-28 14:11:51.91467", email_notification_external_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6", @@ -26,7 +26,7 @@ appeals_id: appeal.uuid, appeals_type: "Appeal", event_date: "2023-02-27 13:11:51.91467", - event_type: "Quarterly Notification", + event_type: Constants.EVENT_TYPE_FILTERS.quarterly_notification, notification_type: "Email", notified_at: "2023-02-28 14:11:51.91467", sms_notification_external_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6", diff --git a/spec/controllers/appeals_controller_spec.rb b/spec/controllers/appeals_controller_spec.rb index e81086e86ad..83e6de31ff1 100644 --- a/spec/controllers/appeals_controller_spec.rb +++ b/spec/controllers/appeals_controller_spec.rb @@ -759,23 +759,29 @@ def allow_vbms_to_return_empty_array let!(:notifications) do [ create(:notification, appeals_id: legacy_appeal.vacols_id, appeals_type: legacy_appeals_type, - event_date: 6.days.ago, event_type: "Appeal docketed", notification_type: "SMS", + event_date: 6.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.appeal_docketed, + notification_type: "SMS", email_notification_status: nil, sms_notification_status: "Success"), create(:notification, appeals_id: ama_appeal.uuid, appeals_type: ama_appeals_type, - event_date: 6.days.ago, event_type: "Hearing scheduled", notification_type: "Email", + event_date: 6.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.hearing_scheduled, + notification_type: "Email", email_notification_status: "Success", sms_notification_status: nil), create(:notification, appeals_id: legacy_appeal_without_claimant.vacols_id, appeals_type: legacy_appeals_type, - event_date: 6.days.ago, event_type: "Hearing scheduled", notification_type: "SMS", + event_date: 6.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.hearing_scheduled, + notification_type: "SMS", email_notification_status: nil, sms_notification_status: "No Claimant Found"), create(:notification, appeals_id: legacy_appeal_without_participant_id.vacols_id, appeals_type: legacy_appeals_type, - event_date: 6.days.ago, event_type: "Hearing scheduled", notification_type: "SMS", + event_date: 6.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.hearing_scheduled, + notification_type: "SMS", email_notification_status: nil, sms_notification_status: "No Participant Id Found"), create(:notification, appeals_id: ama_appeal_without_claimant.uuid, appeals_type: ama_appeals_type, - event_date: 6.days.ago, event_type: "Hearing scheduled", notification_type: "Email", + event_date: 6.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.hearing_scheduled, + notification_type: "Email", email_notification_status: "No Claimant Found", sms_notification_status: nil), create(:notification, appeals_id: ama_appeal_without_participant_id.uuid, appeals_type: ama_appeals_type, - event_date: 6.days.ago, event_type: "Hearing scheduled", notification_type: "SMS", + event_date: 6.days.ago, event_type: Constants.EVENT_TYPE_FILTERS.hearing_scheduled, + notification_type: "SMS", email_notification_status: nil, sms_notification_status: "No Participant Id Found") ] end @@ -795,7 +801,7 @@ def allow_vbms_to_return_empty_array it "should have the event type of 'Appeal docketed'" do subject response_body = JSON.parse(subject.body) - expect(response_body.first["attributes"]["event_type"]).to eq "Appeal docketed" + expect(response_body.first["attributes"]["event_type"]).to eq Constants.EVENT_TYPE_FILTERS.appeal_docketed end it "should return a successful response" do subject @@ -857,7 +863,7 @@ def allow_vbms_to_return_empty_array it "should have the event type of 'Hearing scheduled'" do subject response_body = JSON.parse(subject.body) - expect(response_body.first["attributes"]["event_type"]).to eq "Hearing scheduled" + expect(response_body.first["attributes"]["event_type"]).to eq Constants.EVENT_TYPE_FILTERS.hearing_scheduled end it "should return a succesful response" do subject diff --git a/spec/controllers/hearings_controller_spec.rb b/spec/controllers/hearings_controller_spec.rb index aa3546a0646..c844441cdd8 100644 --- a/spec/controllers/hearings_controller_spec.rb +++ b/spec/controllers/hearings_controller_spec.rb @@ -13,6 +13,8 @@ let!(:vso_participant_id) { "12345" } describe "PATCH update" do + let(:legacy_appeal_state) { legacy_hearing.appeal.appeal_state.tap { _1.update!(hearing_scheduled: true) } } + it "should be successful", :aggregate_failures do params = { notes: "Test", @@ -25,6 +27,9 @@ }, prepped: true } + + expect(legacy_appeal_state.hearing_scheduled).to eq true + patch :update, as: :json, params: { id: legacy_hearing.external_id, hearing: params } expect(response.status).to eq 200 response_body = JSON.parse(response.body)["data"] @@ -35,10 +40,12 @@ expect(response_body["disposition"]).to eq "held" expect(response_body["location"]["facility_id"]).to eq "vba_301" expect(response_body["prepped"]).to eq true + expect(legacy_appeal_state.reload.hearing_scheduled).to eq false end context "when updating an ama hearing" do let!(:hearing) { create(:hearing, :with_tasks) } + let(:appeal_state) { hearing.appeal.appeal_state.tap { _1.update!(hearing_scheduled: true) } } it "should update an ama hearing", :aggregate_failures do params = { @@ -51,6 +58,9 @@ prepped: true, evidence_window_waived: true } + + expect(appeal_state.hearing_scheduled).to eq true + patch :update, as: :json, params: { id: hearing.external_id, hearing: params } expect(response.status).to eq 200 response_body = JSON.parse(response.body)["data"] @@ -60,6 +70,7 @@ expect(response_body["prepped"]).to eq true expect(response_body["location"]["facility_id"]).to eq "vba_301" expect(response_body["evidence_window_waived"]).to eq true + expect(appeal_state.reload.hearing_scheduled).to eq false end end diff --git a/spec/factories/appeal_state.rb b/spec/factories/appeal_state.rb index bb2f194546c..2590b52f762 100644 --- a/spec/factories/appeal_state.rb +++ b/spec/factories/appeal_state.rb @@ -25,7 +25,7 @@ end trait :legacy do - appeal { create(:legacy_appeal, vacols_case: create(:case)) } + appeal { create(:legacy_appeal, :with_veteran, vacols_case: create(:case)) } end end end diff --git a/spec/factories/notification.rb b/spec/factories/notification.rb index 7eb8d375b0a..d19d87a5a4e 100644 --- a/spec/factories/notification.rb +++ b/spec/factories/notification.rb @@ -18,6 +18,7 @@ updated_at { Time.zone.now } email_notification_external_id { nil } sms_notification_external_id { nil } + notifiable { nil } end factory :notification_email_only do @@ -37,6 +38,7 @@ updated_at { Time.zone.now } email_notification_external_id { md5(uniqid(time)) } sms_notification_external_id { nil } + notifiable { appeal } end factory :notification_sms_only do @@ -56,6 +58,7 @@ updated_at { Time.zone.now } email_notification_external_id { nil } sms_notification_external_id { md5(uniqid(time)) } + notifiable { appeal } end factory :notification_email_and_sms do @@ -75,5 +78,6 @@ updated_at { Time.zone.now } email_notification_external_id { md5(uniqid(time)) } sms_notification_external_id { md5(uniqid(time)) } + notifiable { appeal } end end diff --git a/spec/feature/queue/appeal_notifications_page_spec.rb b/spec/feature/queue/appeal_notifications_page_spec.rb index ecfef5030f9..44dd8eca6ff 100644 --- a/spec/feature/queue/appeal_notifications_page_spec.rb +++ b/spec/feature/queue/appeal_notifications_page_spec.rb @@ -16,7 +16,8 @@ shared_examples "with notifications" do let(:seed_notifications) do create(:notification, appeals_id: appeals_id, appeals_type: appeal.class.name, event_date: "2022-11-01", - event_type: "Appeal docketed", notification_type: "Email and SMS", + event_type: Constants.EVENT_TYPE_FILTERS.appeal_docketed, + notification_type: "Email and SMS", recipient_email: "example@example.com", recipient_phone_number: "555-555-5555", email_notification_status: "delivered", sms_notification_status: "delivered", notification_content: "Your appeal at the Board of Veteran's Appeals has been docketed. "\ @@ -25,19 +26,22 @@ "any questions please reach out to your Veterans Service Organization or representative "\ "or log onto VA.gov for additional information.") create(:notification, appeals_id: appeals_id, appeals_type: appeal.class.name, event_date: "2022-11-02", - event_type: "Hearing scheduled", notification_type: "Email and SMS", + event_type: Constants.EVENT_TYPE_FILTERS.hearing_scheduled, + notification_type: "Email and SMS", recipient_email: "example@example.com", recipient_phone_number: nil, email_notification_status: "delivered", sms_notification_status: "temporary-failure", notification_content: "Your hearing has been scheduled with a Veterans Law Judge at the "\ "Board of Veterans' Appeals. You will be notified of the details in writing shortly.") create(:notification, appeals_id: appeals_id, appeals_type: appeal.class.name, event_date: "2022-11-03", - event_type: "Privacy Act request pending", notification_type: "Email and SMS", + event_type: Constants.EVENT_TYPE_FILTERS.privacy_act_request_pending, + notification_type: "Email and SMS", recipient_email: "example@example.com", recipient_phone_number: nil, email_notification_status: "delivered", sms_notification_status: "temporary-failure", notification_content: "You or your representative filed a Privacy Act request. The Board "\ "placed your appeal on hold until this request is satisfied.") create(:notification, appeals_id: appeals_id, appeals_type: appeal.class.name, event_date: "2022-11-04", - event_type: "Privacy Act request complete", notification_type: "Email and SMS", + event_type: Constants.EVENT_TYPE_FILTERS.privacy_act_request_complete, + notification_type: "Email and SMS", recipient_email: "example@example.com", recipient_phone_number: nil, email_notification_status: "delivered", sms_notification_status: "temporary-failure", notification_content: "The Privacy Act request has been satisfied and the Board will "\ @@ -46,7 +50,8 @@ "Organization or representative, if you have one, or log onto VA.gov for additional "\ "information") create(:notification, appeals_id: appeals_id, appeals_type: appeal.class.name, event_date: "2022-11-05", - event_type: "Withdrawal of hearing", notification_type: "Email and SMS", + event_type: Constants.EVENT_TYPE_FILTERS.withdrawal_of_hearing, + notification_type: "Email and SMS", recipient_email: nil, recipient_phone_number: nil, email_notification_status: "Success", sms_notification_status: "temporary-failure", notification_content: "You or your representative have requested to withdraw your hearing "\ @@ -56,7 +61,8 @@ "hearing coordinator for your region. For a list of hearing coordinators by region "\ "with contact information, please visit https://www.bva.va.gov.") create(:notification, appeals_id: appeals_id, appeals_type: appeal.class.name, event_date: "2022-11-06", - event_type: "VSO IHP pending", notification_type: "Email and SMS", recipient_email: nil, + event_type: Constants.EVENT_TYPE_FILTERS.vso_ihp_pending, + notification_type: "Email and SMS", recipient_email: nil, recipient_phone_number: nil, email_notification_status: "Success", sms_notification_status: "Success", notification_content: "You filed an appeal with the Board of Veterans' Appeals. Your case "\ @@ -64,7 +70,8 @@ "Once the argument has been received, the Board of Veterans' Appeals will resume "\ "processing of your appeal.") create(:notification, appeals_id: appeals_id, appeals_type: appeal.class.name, event_date: "2022-11-07", - event_type: "VSO IHP complete", notification_type: "Email and SMS", + event_type: Constants.EVENT_TYPE_FILTERS.vso_ihp_complete, + notification_type: "Email and SMS", recipient_email: nil, recipient_phone_number: nil, email_notification_status: "Success", sms_notification_status: "Success", notification_content: "The Board of Veterans' Appeals received the written argument from "\ @@ -73,7 +80,7 @@ "please reach out to your Veterans Service Organization or log onto VA.gov for additional "\ "information.") create(:notification, appeals_id: appeals_id, appeals_type: appeal.class.name, event_date: "2022-11-08", - event_type: "Appeal decision mailed (Non-contested claims)", + event_type: Constants.EVENT_TYPE_FILTERS.appeal_decision_mailed_non_contested_claims, notification_type: "Email and SMS", recipient_email: nil, recipient_phone_number: nil, email_notification_status: "Success", sms_notification_status: "permanent-failure", notification_content: "The Board of Veterans' Appeals issued a decision on your appeal "\ @@ -100,7 +107,7 @@ # correct event type event_type_cell = page.find("td", match: :first) - expect(event_type_cell).to have_content("Appeal docketed") + expect(event_type_cell).to have_content(Constants.EVENT_TYPE_FILTERS.appeal_docketed) # correct notification date date_cell = page.all("td", minimum: 1)[1] @@ -138,13 +145,14 @@ # by event type filter = page.find("path", class: "unselected-filter-icon-inner-1", match: :first) filter.click - filter_option = page.find("li", class: "cf-filter-option-row", text: "Appeal docketed") + filter_option = page.find("li", class: "cf-filter-option-row", + text: Constants.EVENT_TYPE_FILTERS.appeal_docketed) filter_option.click table = page.find("tbody") cells = table.all("td", minimum: 1) expect(table).to have_selector("tr", count: 2) - expect(cells[0]).to have_content("Appeal docketed") - expect(cells[5]).to have_content("Appeal docketed") + expect(cells[0]).to have_content(Constants.EVENT_TYPE_FILTERS.appeal_docketed) + expect(cells[5]).to have_content(Constants.EVENT_TYPE_FILTERS.appeal_docketed) # clear filter filter.click @@ -198,13 +206,13 @@ # by multiple columns at once filters = page.all("path", class: "unselected-filter-icon-inner-1", minimum: 1) filters[0].click - page.find("li", class: "cf-filter-option-row", text: "Hearing scheduled").click + page.find("li", class: "cf-filter-option-row", text: Constants.EVENT_TYPE_FILTERS.hearing_scheduled).click filters[1].click page.find("li", class: "cf-filter-option-row", text: "Text").click table = page.find("tbody") cells = table.all("td", minimum: 1) expect(table).to have_selector("tr", count: 1) - expect(cells[0]).to have_content("Hearing scheduled") + expect(cells[0]).to have_content(Constants.EVENT_TYPE_FILTERS.hearing_scheduled) expect(cells[2]).to have_content("Text") end end @@ -229,7 +237,7 @@ # prev button moves to previous page click_on("Prev", match: :first) event_type_cell = page.find("td", match: :first) - expect(event_type_cell).to have_content("Appeal docketed") + expect(event_type_cell).to have_content(Constants.EVENT_TYPE_FILTERS.appeal_docketed) # prev button disabled on the first page expect(page).to have_button("Prev", disabled: true) diff --git a/spec/feature/queue/mail_task_spec.rb b/spec/feature/queue/mail_task_spec.rb index d575c40a4bf..358bfc8d1fd 100644 --- a/spec/feature/queue/mail_task_spec.rb +++ b/spec/feature/queue/mail_task_spec.rb @@ -281,9 +281,11 @@ end it "sends proper notifications", skip: "test is failing in local env and github actions" do - scheduled_payload = AppellantNotification.create_payload(appeal, "Hearing scheduled").to_json + scheduled_payload = AppellantNotification.create_payload(appeal, + Constants.EVENT_TYPE_FILTERS.hearing_scheduled).to_json if appeal.hearings.any? - postpone_payload = AppellantNotification.create_payload(appeal, "Postponement of hearing") + postpone_payload = AppellantNotification.create_payload(appeal, + Constants.EVENT_TYPE_FILTERS.postponement_of_hearing) .to_json expect(SendNotificationJob).to receive(:perform_later).with(postpone_payload) end @@ -745,7 +747,9 @@ shared_context "async actions" do context "async actions" do it "sends withdrawal of hearing notification" do - withdrawal_payload = AppellantNotification.create_payload(appeal, "Withdrawal of hearing").to_json + withdrawal_payload = + AppellantNotification.create_payload(appeal, + Constants.EVENT_TYPE_FILTERS.withdrawal_of_hearing).to_json expect(SendNotificationJob).to receive(:perform_later).with(withdrawal_payload) perform_enqueued_jobs do diff --git a/spec/jobs/ama_notification_efolder_sync_job_spec.rb b/spec/jobs/ama_notification_efolder_sync_job_spec.rb index c6016c9e877..66823d9b460 100644 --- a/spec/jobs/ama_notification_efolder_sync_job_spec.rb +++ b/spec/jobs/ama_notification_efolder_sync_job_spec.rb @@ -21,7 +21,7 @@ appeals_id: appeal.uuid, appeals_type: "Appeal", event_date: today, - event_type: "Appeal docketed", + event_type: Constants.EVENT_TYPE_FILTERS.appeal_docketed, notification_type: "Email", notified_at: Time.zone.now - (10 - index).minutes, email_notification_status: "delivered") @@ -39,6 +39,8 @@ before(:all) { Seeds::NotificationEvents.new.seed! } before(:each) { stub_const("AmaNotificationEfolderSyncJob::BATCH_LIMIT", BATCH_LIMIT_SIZE) } + after(:all) { DatabaseCleaner.clean_with(:truncation, except: %w[vftypes issref]) } + context "first run" do after(:all) { clean_up_after_threads } @@ -59,7 +61,7 @@ appeals_id: appeals[6].uuid, appeals_type: "Appeal", event_date: today, - event_type: "Appeal docketed", + event_type: Constants.EVENT_TYPE_FILTERS.appeal_docketed, notification_type: "Email", notified_at: Time.zone.now, email_notification_status: "delivered") @@ -85,7 +87,7 @@ appeals_id: appeals[6].uuid, appeals_type: appeals[6].class.name, event_date: today, - event_type: "Appeal decision mailed (Non-contested claims)", + event_type: Constants.EVENT_TYPE_FILTERS.appeal_decision_mailed_non_contested_claims, notification_type: "Email", notified_at: Time.zone.now, email_notification_status: "delivered") @@ -121,7 +123,7 @@ appeals_id: appeals[6].uuid, appeals_type: "Appeal", event_date: today, - event_type: "Appeal docketed", + event_type: Constants.EVENT_TYPE_FILTERS.appeal_docketed, notification_type: "Email", notified_at: Time.zone.now, email_notification_status: "Failure Due to Deceased") @@ -151,7 +153,7 @@ appeals_id: appeals[4].uuid, appeals_type: "Appeal", event_date: today, - event_type: "Appeal docketed", + event_type: Constants.EVENT_TYPE_FILTERS.appeal_docketed, notification_type: "Email", notified_at: Time.zone.now, email_notification_status: "delivered") @@ -213,7 +215,7 @@ appeals_id: appeals[4].uuid, appeals_type: "Appeal", event_date: today, - event_type: "Appeal docketed", + event_type: Constants.EVENT_TYPE_FILTERS.appeal_docketed, notification_type: "Email", notified_at: 2.minutes.ago, email_notification_status: "delivered") @@ -229,7 +231,7 @@ appeals_id: appeals[4].uuid, appeals_type: "Appeal", event_date: today, - event_type: "Appeal docketed", + event_type: Constants.EVENT_TYPE_FILTERS.appeal_docketed, notification_type: "Email", notified_at: 1.minute.ago, email_notification_status: "Failure Due to Deceased") @@ -244,7 +246,7 @@ appeals_id: appeals[4].uuid, appeals_type: "Appeal", event_date: today, - event_type: "Appeal docketed", + event_type: Constants.EVENT_TYPE_FILTERS.appeal_docketed, notification_type: "Email", notified_at: Time.zone.now, email_notification_status: "delivered") diff --git a/spec/jobs/application_job_spec.rb b/spec/jobs/application_job_spec.rb index e145256a2d8..c2e8861c1a5 100644 --- a/spec/jobs/application_job_spec.rb +++ b/spec/jobs/application_job_spec.rb @@ -10,26 +10,50 @@ def perform(target = nil) end describe "ApplicationJob" do + let(:freeze_time_first_run) { Time.zone.local(2024, 8, 30, 19, 0, 20) } + let(:freeze_time_second_run) { Time.zone.local(2024, 8, 30, 20, 0, 20) } + context ".application_attr" do it "sets application request store" do JobThatIsGood.perform_now expect(RequestStore[:application]).to eq("fake_job") end + end + + context "JobExecutionTime" do + def job_execution_record_checks + expect(JobExecutionTime.count).to eq(1) + execution_time_record = JobExecutionTime.first + expect(execution_time_record.job_name).to eq("JobThatIsGood") + expect(execution_time_record.last_executed_at).to eq(Time.now.utc) + end + + it "adds record to JobExecutionTime if the IGNORE_JOB_EXECUTION_TIME constant is false" do + Timecop.freeze(freeze_time_first_run) do + expect(JobExecutionTime.count).to eq(0) + JobThatIsGood.perform_now + + job_execution_record_checks + end + end - it "sets extra context in middleware" do - allow(Raven).to receive(:extra_context) + it "update existing record in JobExecutionTime table when job is run multiple times" do + JobExecutionTime.create(job_name: JobThatIsGood.name, last_executed_at: 2.days.ago) - sqs_msg = double("sqs_msg") - allow(sqs_msg).to receive(:message_id).and_return("msgid") + Timecop.freeze(freeze_time_first_run) do + expect(JobExecutionTime.count).to eq(1) + JobThatIsGood.perform_now - JobSentryScopeMiddleware.new.call( - double("worker"), - "high_priority", - sqs_msg, - ActiveSupport::HashWithIndifferentAccess.new(job_class: "JobThatIsGood", job_id: "jobid") - ) {} + job_execution_record_checks + end - expect(Raven).to have_received(:extra_context).with(hash_including(application: :fake)) + Timecop.freeze(freeze_time_second_run) do + JobThatIsGood.perform_now + expect(JobExecutionTime.count).to eq(1) + execution_time_record = JobExecutionTime.first + expect(execution_time_record.job_name).to eq("JobThatIsGood") + expect(execution_time_record.last_executed_at).to eq(Time.now.utc) + end end end diff --git a/spec/jobs/caseflow_job_spec.rb b/spec/jobs/caseflow_job_spec.rb index 632808dda93..d0d29c8c4a0 100644 --- a/spec/jobs/caseflow_job_spec.rb +++ b/spec/jobs/caseflow_job_spec.rb @@ -25,4 +25,47 @@ def perform subject end end + + context "#serialize_job_for_enqueueing" do + subject do + CaseflowJob.serialize_job_for_enqueueing(SomeCaseflowJob.new) + end + + it "serializes the job into a hash" do + result = subject + + expect(result.dig(:message_body, "job_class")).to eq "SomeCaseflowJob" + expect(result.dig(:message_body, "queue_name")).to eq "caseflow_test_low_priority" + expect( + result.dig(:message_attributes, "shoryuken_class", :string_value) + ).to eq "ActiveJob::QueueAdapters::ShoryukenAdapter::JobWrapper" + end + end + + context "#enqueue_batch_of_jobs" do + let(:queue_name) { "fake_queue" } + + subject do + CaseflowJob.enqueue_batch_of_jobs( + jobs_to_enqueue: jobs, + name_of_queue: queue_name + ) + end + + context "when the number of jobs exceeds 10" do + let(:jobs) { Array.new(11).map { SomeCaseflowJob.new } } + + it "raises a MaximumBatchSizeViolationError" do + expect { subject }.to raise_error(Caseflow::Error::MaximumBatchSizeViolationError) + end + end + + context "when the number of jobs doesn't exceed 10" do + let(:jobs) { Array.new(2).map { SomeCaseflowJob.new } } + + it "does not raise a MaximumBatchSizeViolationError" do + expect { subject }.to_not raise_error(Caseflow::Error::MaximumBatchSizeViolationError) + end + end + end end diff --git a/spec/jobs/fetch_all_active_ama_appeals_job_spec.rb b/spec/jobs/fetch_all_active_ama_appeals_job_spec.rb index cdff7c1ce3b..31ab918362d 100644 --- a/spec/jobs/fetch_all_active_ama_appeals_job_spec.rb +++ b/spec/jobs/fetch_all_active_ama_appeals_job_spec.rb @@ -57,7 +57,7 @@ end it "only OPEN Legacy Appeal records will be added to the Appeal States table" do subject.perform - expect(AppealState.all.map(&:appeal_id)).to eq(open_ama_appeals.map(&:id)) + expect(AppealState.all.map(&:appeal_id)).to match_array(open_ama_appeals.map(&:id)) expect(AppealState.all.count).to eq(5) end end diff --git a/spec/jobs/hearings/receive_notification_job_spec.rb b/spec/jobs/hearings/receive_notification_job_spec.rb index bbad6b47a9d..0ed8049697a 100644 --- a/spec/jobs/hearings/receive_notification_job_spec.rb +++ b/spec/jobs/hearings/receive_notification_job_spec.rb @@ -124,13 +124,14 @@ context ".perform" do # create notification event record let(:hearing_scheduled_event) do - create(:notification_event, event_type: "Hearing scheduled", + create(:notification_event, event_type: Constants.EVENT_TYPE_FILTERS.hearing_scheduled, email_template_id: "27bf814b-f065-4fc8-89af-ae1292db894e", sms_template_id: "c2798da3-4c7a-43ed-bc16-599329eaf7cc") end # create notification record let(:notification) do - create(:notification, id: 9, appeals_id: 4, appeals_type: "Appeal", event_type: "Hearing scheduled", + create(:notification, id: 9, appeals_id: 4, appeals_type: "Appeal", + event_type: Constants.EVENT_TYPE_FILTERS.hearing_scheduled, participant_id: "123456789", notification_type: "Email", recipient_email: "", event_date: Time.zone.now, email_notification_status: "Success") end diff --git a/spec/jobs/legacy_notification_efolder_sync_job_spec.rb b/spec/jobs/legacy_notification_efolder_sync_job_spec.rb index 7cb01075fb8..d43ce3e5ec6 100644 --- a/spec/jobs/legacy_notification_efolder_sync_job_spec.rb +++ b/spec/jobs/legacy_notification_efolder_sync_job_spec.rb @@ -33,7 +33,7 @@ appeals_id: appeal.vacols_id, appeals_type: "LegacyAppeal", event_date: Time.zone.today, - event_type: "Appeal docketed", + event_type: Constants.EVENT_TYPE_FILTERS.appeal_docketed, notification_type: "Email", notified_at: Time.zone.now - (10 - index).minutes, email_notification_status: "delivered") @@ -55,6 +55,8 @@ before(:all) { ensure_notification_events_exist } before(:each) { stub_const("LegacyNotificationEfolderSyncJob::BATCH_LIMIT", BATCH_LIMIT_SIZE) } + after(:all) { DatabaseCleaner.clean_with(:truncation, except: %w[vftypes issref]) } + context "first run" do after(:all) { clean_up_after_threads } @@ -89,7 +91,7 @@ appeals_id: appeals[6].vacols_id, appeals_type: "LegacyAppeal", event_date: today, - event_type: "Appeal docketed", + event_type: Constants.EVENT_TYPE_FILTERS.appeal_docketed, notification_type: "Email", notified_at: Time.zone.now, email_notification_status: "Failure Due to Deceased") @@ -119,7 +121,7 @@ appeals_id: appeals[4].vacols_id, appeals_type: "LegacyAppeal", event_date: today, - event_type: "Appeal docketed", + event_type: Constants.EVENT_TYPE_FILTERS.appeal_docketed, notification_type: "Email", notified_at: Time.zone.now, email_notification_status: "delivered") @@ -131,7 +133,7 @@ appeals_id: appeals[6].vacols_id, appeals_type: "LegacyAppeal", event_date: today, - event_type: "Appeal docketed", + event_type: Constants.EVENT_TYPE_FILTERS.appeal_docketed, notification_type: "Email", notified_at: Time.zone.now, email_notification_status: "delivered") @@ -155,7 +157,7 @@ appeals_id: appeals[6].vacols_id, appeals_type: appeals[6].class.name, event_date: today, - event_type: "Appeal decision mailed (Non-contested claims)", + event_type: Constants.EVENT_TYPE_FILTERS.appeal_decision_mailed_non_contested_claims, notification_type: "Email", notified_at: Time.zone.now, email_notification_status: "delivered") @@ -217,7 +219,7 @@ appeals_id: appeals[4].vacols_id, appeals_type: "LegacyAppeal", event_date: today, - event_type: "Appeal docketed", + event_type: Constants.EVENT_TYPE_FILTERS.appeal_docketed, notification_type: "Email", notified_at: 2.minutes.ago, email_notification_status: "delivered") @@ -233,7 +235,7 @@ appeals_id: appeals[4].vacols_id, appeals_type: "LegacyAppeal", event_date: today, - event_type: "Appeal docketed", + event_type: Constants.EVENT_TYPE_FILTERS.appeal_docketed, notification_type: "Email", notified_at: 1.minute.ago, email_notification_status: "Failure Due to Deceased") @@ -249,7 +251,7 @@ appeals_id: appeals[4].vacols_id, appeals_type: "LegacyAppeal", event_date: today, - event_type: "Appeal docketed", + event_type: Constants.EVENT_TYPE_FILTERS.appeal_docketed, notification_type: "Email", notified_at: Time.zone.now, email_notification_status: "delivered") diff --git a/spec/jobs/middleware/job_message_deletion_middleware_spec.rb b/spec/jobs/middleware/job_message_deletion_middleware_spec.rb new file mode 100644 index 00000000000..f91e99e4ec8 --- /dev/null +++ b/spec/jobs/middleware/job_message_deletion_middleware_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +describe JobMessageDeletionMiddleware do + let(:sqs) { Aws::SQS::Client.new(stub_responses: true) } + let(:msg) do + OpenStruct.new( + queue_url: "http://localhost:4576/000000000000/caseflow_development_low_priority", + data: OpenStruct.new(receipt_handle: "123456"), + client: sqs + ) + end + let(:quarterly_notification_job_body) { { "job_class" => "QuarterlyNotificationsJob" } } + let(:ama_notification_job_body) { { "job_class" => "AmaNotificationEfolderSyncJob" } } + let(:legacy_notification_job_body) { { "job_class" => "LegacyNotificationEfolderSyncJob" } } + let(:warm_bgs_cache_job_body) { { "job_class" => "WarmBgsCachesJob" } } + let(:caseflow_job_body) { { "job_class" => "CaseflowJob" } } + let(:subject) { JobMessageDeletionMiddleware.new } + + it "deletes SQS message for QuarterlyNotificationsJob" do + expect(sqs).to receive(:delete_message).with(queue_url: msg.queue_url, receipt_handle: msg.data.receipt_handle) + test_yield_statement = subject.call(nil, nil, msg, quarterly_notification_job_body) { "Executes middleware block" } + expect(test_yield_statement).to eql("Executes middleware block") + end + + it "deletes SQS message for AmaNotificationEfolderSyncJob" do + expect(sqs).to receive(:delete_message).with(queue_url: msg.queue_url, receipt_handle: msg.data.receipt_handle) + test_yield_statement = subject.call(nil, nil, msg, ama_notification_job_body) { "Executes middleware block" } + expect(test_yield_statement).to eql("Executes middleware block") + end + + it "deletes SQS message for LegacyNotificationEfolderSyncJob" do + expect(sqs).to receive(:delete_message).with(queue_url: msg.queue_url, receipt_handle: msg.data.receipt_handle) + test_yield_statement = subject.call(nil, nil, msg, legacy_notification_job_body) { "Executes middleware block" } + expect(test_yield_statement).to eql("Executes middleware block") + end + + it "deletes SQS message for WarmBgsCacheJob" do + expect(sqs).to receive(:delete_message).with(queue_url: msg.queue_url, receipt_handle: msg.data.receipt_handle) + test_yield_statement = subject.call(nil, nil, msg, warm_bgs_cache_job_body) { "Executes middleware block" } + expect(test_yield_statement).to eql("Executes middleware block") + end + + it "does not delete SQS message for all jobs" do + expect(sqs).not_to receive(:delete_message).with(queue_url: msg.queue_url, receipt_handle: msg.data.receipt_handle) + test_yield_statement = subject.call(nil, nil, msg, caseflow_job_body) { "Executes middleware block" } + expect(test_yield_statement).to eql("Executes middleware block") + end +end diff --git a/spec/jobs/nightly_syncs_job_spec.rb b/spec/jobs/nightly_syncs_job_spec.rb index 0422aad1ac9..700c5672e5b 100644 --- a/spec/jobs/nightly_syncs_job_spec.rb +++ b/spec/jobs/nightly_syncs_job_spec.rb @@ -156,6 +156,61 @@ class FakeTask < Dispatch::Task expect(BgsAttorney.count).to eq 5 end end + + context "#sync_hearing_states" do + let(:pending_hearing_appeal_state) do + create_appeal_state_with_case_record_and_hearing(nil) + end + + let(:postponed_hearing_appeal_state) do + create_appeal_state_with_case_record_and_hearing("P") + end + + let(:withdrawn_hearing_appeal_state) do + create_appeal_state_with_case_record_and_hearing("C") + end + + let(:scheduled_in_error_hearing_appeal_state) do + create_appeal_state_with_case_record_and_hearing("E") + end + + let(:held_hearing_appeal_state) do + create_appeal_state_with_case_record_and_hearing("H") + end + + it "Job synchronizes hearing statuses" do + expect([pending_hearing_appeal_state, + postponed_hearing_appeal_state, + withdrawn_hearing_appeal_state, + scheduled_in_error_hearing_appeal_state, + held_hearing_appeal_state].all?(&:hearing_scheduled)).to eq true + + subject + + expect(pending_hearing_appeal_state.hearing_scheduled).to eq true + + expect(postponed_hearing_appeal_state.reload.hearing_scheduled).to eq false + expect(postponed_hearing_appeal_state.hearing_postponed).to eq true + + expect(withdrawn_hearing_appeal_state.reload.hearing_scheduled).to eq false + expect(withdrawn_hearing_appeal_state.hearing_withdrawn).to eq true + + expect(scheduled_in_error_hearing_appeal_state.reload.hearing_scheduled).to eq false + expect(scheduled_in_error_hearing_appeal_state.scheduled_in_error).to eq true + + expect(held_hearing_appeal_state.reload.hearing_scheduled).to eq false + end + + # Hearing scheduled will be set to true to simulate Caseflow missing a + # disposition update. + def create_appeal_state_with_case_record_and_hearing(desired_disposition) + case_hearing = create(:case_hearing, hearing_disp: desired_disposition) + vacols_case = create(:case, case_hearings: [case_hearing]) + appeal = create(:legacy_appeal, vacols_case: vacols_case) + + appeal.appeal_state.tap { _1.update!(hearing_scheduled: true) } + end + end end context "when errors occur" do diff --git a/spec/jobs/notification_initialization_job_spec.rb b/spec/jobs/notification_initialization_job_spec.rb new file mode 100644 index 00000000000..3b86fb7e74d --- /dev/null +++ b/spec/jobs/notification_initialization_job_spec.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +describe NotificationInitializationJob, type: :job do + include ActiveJob::TestHelper + + before { Seeds::NotificationEvents.new.seed! } + + let(:user) { create(:user) } + + subject do + NotificationInitializationJob.perform_now( + appeal_id: appeal_state.appeal_id, + appeal_type: appeal_state.appeal_type, + template_name: Constants.EVENT_TYPE_FILTERS.appeal_docketed, + appeal_status: nil + ) + end + + context "When an appeal does not exist for an appeal state" do + let(:appeal_state) do + create( + :appeal_state, + appeal_id: 99_999, + appeal_type: "Appeal", + created_by_id: user.id, + updated_by_id: user.id, + appeal_docketed: true + ) + end + + it "An AppealNotError exception is raised" do + expect_any_instance_of(NotificationInitializationJob).to receive(:log_error).with( + instance_of(Caseflow::Error::AppealNotFound) + ) + + subject + end + end + + context "When an appeal exists for an appeal state" do + context "The appeal is an AMA appeal" do + let(:appeal_state) do + create( + :appeal_state, + :ama, + created_by_id: user.id, + updated_by_id: user.id, + appeal_docketed: true + ) + end + + it "enqueues an SendNotificationJob" do + expect { subject }.to have_enqueued_job(SendNotificationJob) + end + end + + context "The appeal is a legacy appeal" do + before { FeatureToggle.enable!(:appeal_docketed_event) } + after { FeatureToggle.disable!(:appeal_docketed_event) } + + let(:appeal_state) do + create( + :appeal_state, + :legacy, + created_by_id: user.id, + updated_by_id: user.id, + appeal_docketed: true + ) + end + + it "enqueues an SendNotificationJob" do + expect { subject }.to have_enqueued_job(SendNotificationJob) + end + end + end +end diff --git a/spec/jobs/poll_docketed_legacy_appeals_job_spec.rb b/spec/jobs/poll_docketed_legacy_appeals_job_spec.rb index bb6ffc006a4..ba8bd65667a 100644 --- a/spec/jobs/poll_docketed_legacy_appeals_job_spec.rb +++ b/spec/jobs/poll_docketed_legacy_appeals_job_spec.rb @@ -39,7 +39,7 @@ appeals_id: "12342", appeals_type: "LegacyAppeal", event_date: today, - event_type: "Appeal docketed", + event_type: Constants.EVENT_TYPE_FILTERS.appeal_docketed, notification_type: "Email", notified_at: today) } @@ -71,6 +71,13 @@ expect(PollDocketedLegacyAppealsJob.new.most_recent_docketed_appeals(query)).to eq(recent_docketed_appeal_ids) end + it "should create AppealState records if they do not exist" do + expect(AppealState.where(appeal_id: recent_docketed_appeal_ids)[0]).to be(nil) + appeal_state_count_before = AppealState.count + PollDocketedLegacyAppealsJob.new.create_corresponding_appeal_states(recent_docketed_appeal_ids) + expect(AppealState.count).not_to eq(appeal_state_count_before) + end + it "should filter for all legacy appeals that havent already gotten a notification yet" do expect(PollDocketedLegacyAppealsJob.perform_now).to eq(filtered_docketed_appeal_ids) end diff --git a/spec/jobs/process_notification_status_updates_job_spec.rb b/spec/jobs/process_notification_status_updates_job_spec.rb index 58592c4eaa3..a5f94379961 100644 --- a/spec/jobs/process_notification_status_updates_job_spec.rb +++ b/spec/jobs/process_notification_status_updates_job_spec.rb @@ -19,7 +19,7 @@ create(:notification, appeals_id: appeal.uuid, appeals_type: "Appeal", event_date: 6.days.ago, - event_type: "Quarterly Notification", + event_type: Constants.EVENT_TYPE_FILTERS.quarterly_notification, notification_type: "Email", email_notification_external_id: SecureRandom.uuid) end @@ -27,7 +27,7 @@ create(:notification, appeals_id: appeal.uuid, appeals_type: "Appeal", event_date: 6.days.ago, - event_type: "Hearing scheduled", + event_type: Constants.EVENT_TYPE_FILTERS.hearing_scheduled, sms_notification_external_id: SecureRandom.uuid, notification_type: "SMS") end diff --git a/spec/jobs/quarterly_notifications_job_spec.rb b/spec/jobs/quarterly_notifications_job_spec.rb index 4675e128f82..f7ba2c0a12b 100644 --- a/spec/jobs/quarterly_notifications_job_spec.rb +++ b/spec/jobs/quarterly_notifications_job_spec.rb @@ -8,24 +8,6 @@ let(:user) { create(:user) } subject { QuarterlyNotificationsJob.perform_now } describe "#perform" do - context "appeal is nil" do - let!(:appeal_state) do - create( - :appeal_state, - appeal_id: 2, - appeal_type: "Appeal", - created_by_id: user.id, - updated_by_id: user.id - ) - end - it "does not push a new message" do - expect { subject }.not_to have_enqueued_job(SendNotificationJob) - end - it "rescues and logs error" do - expect(Rails.logger).to receive(:error) - subject - end - end context "Appeal Decision Mailed" do let!(:appeal_state) do create( @@ -37,16 +19,21 @@ decision_mailed: true ) end + it "does not push a new message" do - expect { subject }.not_to have_enqueued_job(SendNotificationJob) + expect_message_to_not_be_enqueued + + subject end end + context "job setup" do it "sets the current user for BGS calls" do subject expect(RequestStore[:current_user]).to eq(User.system_user) end end + context "Appeal Docketed" do let!(:appeal_state) do create( @@ -58,11 +45,14 @@ appeal_docketed: true ) end + it "pushes a new message" do + expect_message_to_be_queued + subject - expect(SendNotificationJob).to have_been_enqueued.exactly(:once) end end + context "Appeal Docketed with withdrawn hearing" do let!(:appeal_state) do create( @@ -75,11 +65,14 @@ hearing_withdrawn: true ) end + it "pushes a new message" do + expect_message_to_be_queued + subject - expect(SendNotificationJob).to have_been_enqueued.exactly(:once) end end + context "Hearing to be Rescheduled / Privacy Act Pending for hearing postponed" do let!(:appeal_state) do create( @@ -93,11 +86,14 @@ privacy_act_pending: true ) end + it "pushes a new message" do + expect_message_to_be_queued + subject - expect(SendNotificationJob).to have_been_enqueued.exactly(:once) end end + context "Hearing to be Rescheduled for hearing postponed" do let!(:appeal_state) do create( @@ -110,11 +106,14 @@ hearing_postponed: true ) end + it "pushes a new message" do + expect_message_to_be_queued + subject - expect(SendNotificationJob).to have_been_enqueued.exactly(:once) end end + context "Hearing to be Rescheduled / Privacy Act Pending for scheduled in error" do let!(:appeal_state) do create( @@ -128,11 +127,14 @@ scheduled_in_error: true ) end + it "pushes a new message" do + expect_message_to_be_queued + subject - expect(SendNotificationJob).to have_been_enqueued.exactly(:once) end end + context "Hearing to be Rescheduled for scheduled in error" do let!(:appeal_state) do create( @@ -145,30 +147,34 @@ scheduled_in_error: true ) end + it "pushes a new message" do + expect_message_to_be_queued + subject - expect(SendNotificationJob).to have_been_enqueued.exactly(:once) end end + context "Hearing Scheduled / Privacy Act Pending with ihp task" do + let(:hearing) { create(:hearing, :with_tasks) } let!(:appeal_state) do - create( - :appeal_state, - appeal_id: appeal.id, - appeal_type: "Appeal", - created_by_id: user.id, - updated_by_id: user.id, - appeal_docketed: true, - hearing_scheduled: true, - privacy_act_pending: true, - vso_ihp_pending: true - ) + hearing.appeal.appeal_state.tap do + _1.update!( + appeal_docketed: true, + hearing_scheduled: true, + privacy_act_pending: true, + vso_ihp_pending: true + ) + end end + it "pushes a new message" do + expect_message_to_be_queued + subject - expect(SendNotificationJob).to have_been_enqueued.exactly(:once) end end + context "VSO IHP Pending / Privacy Act Pending" do let!(:appeal_state) do create( @@ -182,47 +188,52 @@ vso_ihp_pending: true ) end + it "pushes a new message" do + expect_message_to_be_queued + subject - expect(SendNotificationJob).to have_been_enqueued.exactly(:once) end end + context "Hearing Scheduled with ihp task pending" do + let(:hearing) { create(:hearing, :with_tasks) } let!(:appeal_state) do - create( - :appeal_state, - appeal_id: appeal.id, - appeal_type: "Appeal", - created_by_id: user.id, - updated_by_id: user.id, - appeal_docketed: true, - hearing_scheduled: true, - vso_ihp_pending: true - ) + hearing.appeal.appeal_state.tap do + _1.update!( + appeal_docketed: true, + hearing_scheduled: true, + vso_ihp_pending: true + ) + end end + it "pushes a new message" do + expect_message_to_be_queued + subject - expect(SendNotificationJob).to have_been_enqueued.exactly(:once) end end + context "Hearing Scheduled / Privacy Act Pending" do + let(:hearing) { create(:hearing, :with_tasks) } let!(:appeal_state) do - create( - :appeal_state, - appeal_id: appeal.id, - appeal_type: "Appeal", - created_by_id: user.id, - updated_by_id: user.id, - appeal_docketed: true, - hearing_scheduled: true, - privacy_act_pending: true - ) + hearing.appeal.appeal_state.tap do + _1.update!( + appeal_docketed: true, + hearing_scheduled: true, + privacy_act_pending: true + ) + end end + it "pushes a new message" do + expect_message_to_be_queued + subject - expect(SendNotificationJob).to have_been_enqueued.exactly(:once) end end + context "Privacy Act Pending" do let!(:appeal_state) do create( @@ -235,11 +246,14 @@ privacy_act_pending: true ) end + it "pushes a new message" do + expect_message_to_be_queued + subject - expect(SendNotificationJob).to have_been_enqueued.exactly(:once) end end + context "VSO IHP Pending" do let!(:appeal_state) do create( @@ -252,28 +266,32 @@ vso_ihp_pending: true ) end + it "pushes a new message" do + expect_message_to_be_queued + subject - expect(SendNotificationJob).to have_been_enqueued.exactly(:once) end end + context "Hearing Scheduled" do + let(:legacy_hearing) { create(:legacy_hearing) } let!(:appeal_state) do - create( - :appeal_state, - appeal_id: legacy_appeal.id, - appeal_type: "LegacyAppeal", - created_by_id: user.id, - updated_by_id: user.id, - appeal_docketed: true, - hearing_scheduled: true - ) - end - it "pushes a new message" do - subject - expect(SendNotificationJob).to have_been_enqueued.exactly(:once) + legacy_hearing.appeal.appeal_state.tap do + _1.update!( + appeal_docketed: true, + hearing_scheduled: true + ) + end + + it "pushes a new message" do + expect_message_to_be_queued + + subject + end end end + context "cancelled appeal" do let!(:appeal_state) do create( @@ -285,11 +303,13 @@ appeal_cancelled: true ) end + it "does not push a new message" do subject expect { subject }.not_to have_enqueued_job(SendNotificationJob) end end + context "decision mailed" do let!(:appeal_state) do create( @@ -302,9 +322,25 @@ ) end it "does not push a new message" do + expect_message_to_not_be_enqueued + subject - expect { subject }.not_to have_enqueued_job(SendNotificationJob) end end end + + def expect_message_to_be_queued + expect_any_instance_of(QuarterlyNotificationsJob) + .to receive(:enqueue_init_jobs) + .with( + array_including( + instance_of(NotificationInitializationJob) + ) + ) + end + + def expect_message_to_not_be_enqueued + expect_any_instance_of(QuarterlyNotificationsJob) + .to_not receive(:enqueue_init_jobs) + end end diff --git a/spec/jobs/send_notification_job_spec.rb b/spec/jobs/send_notification_job_spec.rb index 9ad5ffb21de..32ebb4b8296 100644 --- a/spec/jobs/send_notification_job_spec.rb +++ b/spec/jobs/send_notification_job_spec.rb @@ -7,17 +7,19 @@ create(:notification, appeals_id: "5d70058f-8641-4155-bae8-5af4b61b1576", appeals_type: "Appeal", - event_type: "Hearing scheduled", + event_type: Constants.EVENT_TYPE_FILTERS.hearing_scheduled, event_date: Time.zone.today, - notification_type: "Email") + notification_type: "Email", + notifiable: appeal) end let(:legacy_appeal_notification) do create(:notification, appeals_id: "123456", appeals_type: "LegacyAppeal", - event_type: "Appeal docketed", + event_type: Constants.EVENT_TYPE_FILTERS.appeal_docketed, event_date: Time.zone.today, - notification_type: "SMS") + notification_type: "Email and SMS", + notifiable: appeal) end let(:appeal) do create(:appeal, @@ -26,6 +28,7 @@ homelessness: false, veteran_file_number: "123456789") end + let(:legacy_appeal) { create(:legacy_appeal) } let!(:no_name_appeal) do create(:appeal, docket_type: "Appeal", @@ -40,24 +43,33 @@ last_name: nil) end # rubocop:disable Style/BlockDelimiters - let(:good_template_name) { "Appeal docketed" } + let(:good_template_name) { Constants.EVENT_TYPE_FILTERS.appeal_docketed } let(:error_template_name) { "No Participant Id Found" } + let(:deceased_status) { "Failure Due to Deceased" } let(:success_status) { "Success" } let(:error_status) { "No participant_id" } let(:success_message_attributes) { { participant_id: "123456789", status: success_status, - appeal_id: "5d70058f-8641-4155-bae8-5af4b61b1576", - appeal_type: "Appeal" + appeal_id: appeal.external_id, + appeal_type: appeal.class.name } } let(:success_legacy_message_attributes) { { participant_id: "123456789", status: success_status, - appeal_id: "123456", - appeal_type: "LegacyAppeal" + appeal_id: legacy_appeal.external_id, + appeal_type: legacy_appeal.class.name + } + } + let(:deceased_legacy_message_attributes) { + { + participant_id: "123456789", + status: deceased_status, + appeal_id: target_appeal.external_id, + appeal_type: target_appeal.class.name } } let(:no_name_message_attributes) { @@ -65,15 +77,23 @@ participant_id: "246813579", status: success_status, appeal_id: no_name_appeal.uuid, - appeal_type: "Appeal" + appeal_type: appeal.class.name } } let(:error_message_attributes) { { participant_id: nil, status: error_status, - appeal_id: "5d70058f-8641-4155-bae8-5af4b61b1578", - appeal_type: "Appeal" + appeal_id: appeal.external_id, + appeal_type: appeal.class.name + } + } + let(:deceased_message_attributes) { + { + participant_id: "123456789", + status: deceased_status, + appeal_id: target_appeal.external_id, + appeal_type: target_appeal.class.name } } let(:fail_create_message_attributes) { @@ -86,10 +106,17 @@ } let(:good_message) { VANotifySendMessageTemplate.new(success_message_attributes, good_template_name) } let(:legacy_message) { VANotifySendMessageTemplate.new(success_legacy_message_attributes, good_template_name) } + let(:legacy_deceased_message) do + AppellantNotification.create_payload(target_appeal, good_template_name).to_json + end let(:no_name_message) { VANotifySendMessageTemplate.new(no_name_message_attributes, good_template_name) } let(:bad_message) { VANotifySendMessageTemplate.new(error_message_attributes, error_template_name) } + let(:deceased_message) { VANotifySendMessageTemplate.new(deceased_message_attributes, good_template_name).to_json } let(:fail_create_message) { VANotifySendMessageTemplate.new(fail_create_message_attributes, error_template_name) } - let(:quarterly_message) { VANotifySendMessageTemplate.new(success_message_attributes, "Quarterly Notification") } + let(:quarterly_message) { + VANotifySendMessageTemplate.new(success_message_attributes, + Constants.EVENT_TYPE_FILTERS.quarterly_notification) + } let(:participant_id) { success_message_attributes[:participant_id] } let(:no_name_participant_id) { no_name_message_attributes[:participant_id] } let(:bad_participant_id) { "123" } @@ -126,9 +153,17 @@ ) ) } - let(:notification_events_id) { "VSO IHP complete" } - let(:notification_type) { "VSO IHP complete" } + let(:notification_events_id) { Constants.EVENT_TYPE_FILTERS.vso_ihp_complete } + let(:notification_type) { Constants.EVENT_TYPE_FILTERS.vso_ihp_complete } let(:queue_name) { "caseflow_test_send_notifications" } + let(:appeal_to_notify_about) { create(:appeal, :with_deceased_veteran) } + let(:cob_user) do + create(:user).tap do |new_user| + OrganizationsUser.make_user_admin(new_user, ClerkOfTheBoard.singleton) + end + end + + let(:substitution) { AppellantSubstitution.new(created_by_id: cob_user.id, source_appeal_id: appeal.id) } # rubocop:enable Style/BlockDelimiters before do @@ -140,6 +175,20 @@ clear_performed_jobs end + context "#queue_name_suffix" do + subject { described_class.queue_name_suffix } + + it "returns non-FIFO name in development environment" do + is_expected.to eq :send_notifications + end + + it "returns FIFO name in non-development environment" do + allow(ApplicationController).to receive(:dependencies_faked?).and_return(false) + + is_expected.to eq :"send_notifications.fifo" + end + end + it "it is the correct queue" do expect(SendNotificationJob.new.queue_name).to eq(queue_name) end @@ -161,6 +210,7 @@ end it "logs error when message is nil" do + expect(Rails.logger).to receive(:send).with(:error, /Message argument of value nil supplied to job/) perform_enqueued_jobs do expect_any_instance_of(SendNotificationJob).to receive(:log_error) do |_recipient, error_received| expect(error_received.message).to eq "There was no message passed into the " \ @@ -172,6 +222,7 @@ end it "logs error when appeals_id, appeals_type, or event_type is nil" do + expect(Rails.logger).to receive(:send).with(:error, /Nil message attribute\(s\): appeal_type/) perform_enqueued_jobs do expect_any_instance_of(SendNotificationJob).to receive(:log_error) do |_recipient, error_received| expect(error_received.message).to eq "appeals_id or appeal_type or event_type was nil " \ @@ -183,8 +234,10 @@ end it "logs error when audit record is nil" do - allow_any_instance_of(SendNotificationJob).to receive(:create_notification_audit_record).and_return(nil) + allow_any_instance_of(Notification).to receive(:nil?).and_return(true) + expect(Rails.logger).to receive(:send) + .with(:error, /Notification audit record was unable to be found or created/) perform_enqueued_jobs do expect_any_instance_of(SendNotificationJob).to receive(:log_error) do |_recipient, error_received| expect(error_received.message).to eq "Audit record was unable to be found or created " \ @@ -195,6 +248,17 @@ end end + it "notification audit record is recreated when error is in DISCARD ERRORS" do + expect(Rails.logger).to receive(:send).with(:error, /Message argument of value nil supplied to job/) + perform_enqueued_jobs do + expect_any_instance_of(SendNotificationJob).to receive(:log_error) do |_recipient| + expect(SendNotificationJob).to receive(:find_or_create_notification_audit) + end + + SendNotificationJob.perform_later(nil) + end + end + it "sends to VA Notify when no errors are present" do expect(Rails.logger).not_to receive(:error) expect { SendNotificationJob.perform_now(good_message.to_json).to receive(:send_to_va_notify) } @@ -269,10 +333,9 @@ end end - describe "#create_notification_audit_record" do + describe "#create_notification" do it "makes a new notification object" do - expect(Notification).to receive(:new) - SendNotificationJob.perform_now(good_message.to_json) + expect { SendNotificationJob.perform_now(good_message.to_json) }.to change(Notification, :count).by(1) end end @@ -283,45 +346,35 @@ context "va_notify FeatureToggles" do describe "email" do it "is expected to send when the feature toggle is on" do - FeatureToggle.enable!(:va_notify_email) expect(VANotifyService).to receive(:send_email_notifications) SendNotificationJob.perform_now(good_message.to_json) end it "updates the notification_content field with content" do - FeatureToggle.enable!(:va_notify_email) SendNotificationJob.perform_now(good_message.to_json) expect(Notification.last.notification_content).not_to eq(nil) end it "updates the email_notification_content field with content" do - FeatureToggle.enable!(:va_notify_email) SendNotificationJob.perform_now(good_message.to_json) expect(Notification.last.email_notification_content).not_to eq(nil) end it "updates the notification_audit_record with email_notification_external_id" do - FeatureToggle.enable!(:va_notify_email) SendNotificationJob.perform_now(good_message.to_json) expect(Notification.last.email_notification_external_id).not_to eq(nil) end - it "is expected to not send when the feature toggle is off" do - FeatureToggle.disable!(:va_notify_email) - expect(VANotifyService).not_to receive(:send_email_notifications) - SendNotificationJob.perform_now(good_message.to_json) - end end describe "sms" do + before { FeatureToggle.enable!(:va_notify_sms) } + after { FeatureToggle.disable!(:va_notify_sms) } it "is expected to send when the feature toggle is on" do - FeatureToggle.enable!(:va_notify_sms) expect(VANotifyService).to receive(:send_sms_notifications) SendNotificationJob.perform_now(good_message.to_json) end it "updates the sms_notification_content field with content" do - FeatureToggle.enable!(:va_notify_sms) SendNotificationJob.perform_now(good_message.to_json) expect(Notification.last.sms_notification_content).not_to eq(nil) end it "updates the notification_audit_record with sms_notification_external_id" do - FeatureToggle.enable!(:va_notify_sms) SendNotificationJob.perform_now(good_message.to_json) expect(Notification.last.sms_notification_external_id).not_to eq(nil) end @@ -334,22 +387,14 @@ end context "appeal first name not found" do - let(:notification_event) { NotificationEvent.find_by(event_type: "Appeal docketed") } + let(:notification_event) { NotificationEvent.find_by(event_type: Constants.EVENT_TYPE_FILTERS.appeal_docketed) } describe "email" do before { FeatureToggle.enable!(:va_notify_email) } after { FeatureToggle.disable!(:va_notify_email) } it "is expected to send a generic saluation instead of a name" do - expect(VANotifyService).to receive(:send_email_notifications).with( - no_name_participant_id, - "", - notification_event.email_template_id, - "Appellant", - no_name_appeal.docket_number, - "" - ) - + expect(VANotifyService).to receive(:send_email_notifications).with(hash_including(first_name: "Appellant")) SendNotificationJob.perform_now(no_name_message.to_json) end end @@ -359,106 +404,78 @@ after { FeatureToggle.disable!(:va_notify_sms) } it "is expected to send a generic saluation instead of a name" do - expect(VANotifyService).to receive(:send_sms_notifications).with( - no_name_participant_id, - "", - notification_event.sms_template_id, - "Appellant", - no_name_appeal.docket_number, - "" - ) - + expect(VANotifyService).to receive(:send_sms_notifications).with(hash_including(first_name: "Appellant")) SendNotificationJob.perform_now(no_name_message.to_json) end end end context "on retry" do + before { FeatureToggle.enable!(:va_notify_email) } + after { FeatureToggle.disable!(:va_notify_email) } describe "notification object" do it "does not create multiple notification objects" do - FeatureToggle.enable!(:va_notify_email) job = SendNotificationJob.new(good_message.to_json) - job.instance_variable_set(:@notification_audit_record, notification) - expect(Notification).not_to receive(:create) - job.perform_now + allow(job).to receive(:find_or_create_notification_audit).and_return(notification) + expect { job.perform_now }.not_to change(Notification, :count) end end end context "feature flags for setting notification type" do it "notification type should be email if only email flag is on" do + FeatureToggle.enable!(:va_notify_email) job = SendNotificationJob.new(good_message.to_json) - job.instance_variable_set(:@va_notify_email, true) - record = job.send(:create_notification_audit_record, - notification.appeals_id, - notification.appeals_type, - notification.event_type, - "123456789") + job.instance_variable_set(:@message, JSON.parse(job.arguments[0], object_class: OpenStruct)) + record = job.send(:find_or_create_notification_audit) expect(record.notification_type).to eq("Email") + FeatureToggle.disable!(:va_notify_email) end it "notification type should be sms if only sms flag is on" do + FeatureToggle.enable!(:va_notify_sms) job = SendNotificationJob.new(good_message.to_json) - job.instance_variable_set(:@va_notify_sms, true) - record = job.send(:create_notification_audit_record, - notification.appeals_id, - notification.appeals_type, - notification.event_type, - "123456789") - expect(record.notification_type).to eq("SMS") + job.instance_variable_set(:@message, JSON.parse(job.arguments[0], object_class: OpenStruct)) + record = job.send(:find_or_create_notification_audit) + expect(record.notification_type).to eq("Email and SMS") + FeatureToggle.disable!(:va_notify_sms) end it "notification type should be email and sms if both of those flags are on" do + FeatureToggle.enable!(:va_notify_email) + FeatureToggle.enable!(:va_notify_sms) job = SendNotificationJob.new(good_message.to_json) - job.instance_variable_set(:@va_notify_email, true) - job.instance_variable_set(:@va_notify_sms, true) - record = job.send(:create_notification_audit_record, - notification.appeals_id, - notification.appeals_type, - notification.event_type, - "123456789") + job.instance_variable_set(:@message, JSON.parse(job.arguments[0], object_class: OpenStruct)) + record = job.send(:find_or_create_notification_audit) expect(record.notification_type).to eq("Email and SMS") + FeatureToggle.disable!(:va_notify_email) + FeatureToggle.disable!(:va_notify_sms) end end - context "feature flags for sending legacy notifications" do - it "should only send notifications when feature flag is turned on" do - FeatureToggle.enable!(:appeal_docketed_notification) - job = SendNotificationJob.new(legacy_message.to_json) - job.instance_variable_set(:@notification_audit_record, notification) - expect(job).to receive(:send_to_va_notify) - job.perform_now - end + context "feature flag testing for creating legacy appeal notification records" do + let(:legacy_appeal) { create(:legacy_appeal) } + let!(:case) { create(:case, bfkey: legacy_appeal.vacols_id) } - it "should not send notifications when feature flag is turned off" do - FeatureToggle.disable!(:appeal_docketed_notification) + it "creates an instance of a notification" do + FeatureToggle.enable!(:appeal_docketed_event) job = SendNotificationJob.new(legacy_message.to_json) - job.instance_variable_set(:@notification_audit_record, notification) - expect(job).not_to receive(:send_to_va_notify) + allow(job).to receive(:find_appeal_by_external_id).and_return(legacy_appeal) + expect(Notification).to receive(:create) job.perform_now + FeatureToggle.disable!(:appeal_docketed_event) end end - context "feature flag testing for creating legacy appeal notification records" do - it "should only create an instance of a notification before saving if a notification was found" do - FeatureToggle.enable!(:appeal_docketed_event) - job = SendNotificationJob.new(legacy_message.to_json) - expect(Notification).to receive(:new) - job.perform_now + context "feature flag for quarterly notifications" do + before do + FeatureToggle.enable!(:va_notify_quarterly_sms) end - it "should return the notification record if one is found and not try to create one" do - legacy_appeal_notification - FeatureToggle.enable!(:appeal_docketed_event) - FeatureToggle.enable!(:va_notify_sms) - job = SendNotificationJob.new(legacy_message.to_json) - job.instance_variable_set(:@va_notify_sms, true) - expect(Notification).not_to receive(:new) - job.perform_now + after do + FeatureToggle.disable!(:va_notify_quarterly_sms) end - end - context "feature flag for quarterly notifications" do it "should send an sms for quarterly notifications when the flag is on" do FeatureToggle.enable!(:va_notify_quarterly_sms) expect(VANotifyService).to receive(:send_sms_notifications) @@ -473,16 +490,102 @@ end context "no participant or claimant found" do - it "the email status should be updated to say no participant id if that is the message" do + before do FeatureToggle.enable!(:va_notify_email) + FeatureToggle.enable!(:va_notify_sms) + end + + after do + FeatureToggle.disable!(:va_notify_email) + FeatureToggle.disable!(:va_notify_sms) + end + it "the email status should be updated to say no participant id if that is the message" do SendNotificationJob.new(bad_message.to_json).perform_now expect(Notification.first.email_notification_status).to eq("No Participant Id Found") end it "the sms status should be updated to say no participant id if that is the message" do - FeatureToggle.enable!(:va_notify_sms) SendNotificationJob.new(bad_message.to_json).perform_now expect(Notification.first.sms_notification_status).to eq("No Participant Id Found") end end + + context "Deceased veteran checks" do + before do + FeatureToggle.enable!(:va_notify_email) + FeatureToggle.enable!(:va_notify_sms) + FeatureToggle.enable!(:appeal_docketed_notification) + end + after do + FeatureToggle.disable!(:va_notify_email) + FeatureToggle.disable!(:va_notify_sms) + FeatureToggle.disable!(:appeal_docketed_notification) + end + + context "Appeal is the subject of the notification" do + let!(:target_appeal) { create(:appeal, :with_deceased_veteran) } + + it "The veteran being the claimant and is alive" do + expect(VANotifyService).to receive(:send_email_notifications) + expect(VANotifyService).to receive(:send_sms_notifications) + + SendNotificationJob.new(good_message.to_json).perform_now + + expect(Notification.first.email_notification_status).to eq("Success") + end + + it "The veteran being the claimant and is deceased" do + expect(VANotifyService).to_not receive(:send_email_notifications) + expect(VANotifyService).to_not receive(:send_sms_notifications) + + SendNotificationJob.perform_now(deceased_message) + + expect(Notification.first.email_notification_status).to eq("Failure Due to Deceased") + end + + it "The veteran being deceased and there being an AppellantSubstitution on the appeal to swap the claimant" do + substitution + expect(VANotifyService).to_not receive(:send_email_notifications) + expect(VANotifyService).to_not receive(:send_sms_notifications) + + SendNotificationJob.new(deceased_message).perform_now + + expect(Notification.first.email_notification_status).to eq("Failure Due to Deceased") + end + end + + context "Legacy Appeal is the subject of the notification" do + let(:target_appeal) do + create( + :legacy_appeal, + :with_veteran, + vacols_case: create(:case_with_form_9) + ) + end + + it "The veteran being the claimant and is alive" do + job = SendNotificationJob.new(legacy_message.to_json) + job.instance_variable_set(:@notification_audit, notification) + allow(job).to receive(:find_appeal_by_external_id).and_return(target_appeal) + expect(job).to receive(:send_to_va_notify) + + job.perform_now + + expect(Notification.last.email_notification_status).to eq("Success") + end + + it "The veteran being the claimant and is deceased" do + target_appeal.veteran.update!(date_of_death: 2.weeks.ago) + + job = SendNotificationJob.new(legacy_deceased_message) + job.instance_variable_set(:@notification_audit, notification) + allow(job).to receive(:find_appeal_by_external_id).and_return(target_appeal) + expect(job).to_not receive(:send_to_va_notify) + + job.perform_now + + expect(Notification.last.email_notification_status).to eq("Failure Due to Deceased") + end + end + end end diff --git a/spec/jobs/update_appellant_representation_job_spec.rb b/spec/jobs/update_appellant_representation_job_spec.rb index 35219a6cb01..77c899236d0 100644 --- a/spec/jobs/update_appellant_representation_job_spec.rb +++ b/spec/jobs/update_appellant_representation_job_spec.rb @@ -41,14 +41,6 @@ metric_name: "runtime", metric_value: anything ) - expect(MetricsService).to receive(:emit_gauge).with( - app_name: "queue_job", - attrs: { endpoint: "AppellantNotification.appeal_mapper", service: "queue_job" }, - metric_group: "service", - metric_name: "request_latency", - metric_value: anything - ).exactly(new_task_count).times - UpdateAppellantRepresentationJob.perform_now end @@ -80,13 +72,6 @@ end end end - - it "sends the correct number of messages to DataDog and not send a message to Slack" do - expect(MetricsService).to receive(:increment_counter).exactly(7).times - expect_any_instance_of(SlackService).to_not receive(:send_notification) - - UpdateAppellantRepresentationJob.perform_now - end end context "when individual appeals throw errors" do diff --git a/spec/jobs/va_notify_status_update_job_spec.rb b/spec/jobs/va_notify_status_update_job_spec.rb index c58d7546a2e..4c6f2c65842 100644 --- a/spec/jobs/va_notify_status_update_job_spec.rb +++ b/spec/jobs/va_notify_status_update_job_spec.rb @@ -16,7 +16,7 @@ create(:notification, appeals_id: "5d70058f-8641-4155-bae8-5af4b61b1576", appeals_type: "Appeal", - event_type: "Hearing scheduled", + event_type: Constants.EVENT_TYPE_FILTERS.hearing_scheduled, event_date: Time.zone.today, notification_type: "Email", email_notification_status: "Success") @@ -25,7 +25,7 @@ create(:notification, appeals_id: "5d70058f-8641-4155-bae8-5af4b61b1576", appeals_type: "Appeal", - event_type: "Hearing scheduled", + event_type: Constants.EVENT_TYPE_FILTERS.hearing_scheduled, event_date: Time.zone.today, notification_type: "SMS", sms_notification_status: "Success") @@ -34,7 +34,7 @@ create(:notification, appeals_id: "5d70058f-8641-4155-bae8-5af4b61b1576", appeals_type: "Appeal", - event_type: "Hearing scheduled", + event_type: Constants.EVENT_TYPE_FILTERS.hearing_scheduled, event_date: Time.zone.today, notification_type: "Email and SMS", email_notification_status: "Success", @@ -44,7 +44,7 @@ create(:notification, appeals_id: "5d70058f-8641-4155-bae8-5af4b61b1576", appeals_type: "Appeal", - event_type: "Hearing scheduled", + event_type: Constants.EVENT_TYPE_FILTERS.hearing_scheduled, event_date: Time.zone.today, notification_type: "Email", email_notification_external_id: "0", @@ -54,7 +54,7 @@ create(:notification, appeals_id: "5d70058f-8641-4155-bae8-5af4b61b1576", appeals_type: "Appeal", - event_type: "Hearing scheduled", + event_type: Constants.EVENT_TYPE_FILTERS.hearing_scheduled, event_date: Time.zone.today, notification_type: "SMS", email_notification_external_id: nil, @@ -64,7 +64,7 @@ create(:notification, appeals_id: "5d70058f-8641-4155-bae8-5af4b61b1576", appeals_type: "Appeal", - event_type: "Hearing scheduled", + event_type: Constants.EVENT_TYPE_FILTERS.hearing_scheduled, event_date: Time.zone.today, notification_type: "SMS", email_notification_external_id: nil, @@ -74,7 +74,7 @@ create(:notification, appeals_id: "5d70058f-8641-4155-bae8-5af4b61b1576", appeals_type: "Appeal", - event_type: "Hearing scheduled", + event_type: Constants.EVENT_TYPE_FILTERS.hearing_scheduled, event_date: Time.zone.today, notification_type: "Email", email_notification_external_id: "1", @@ -84,7 +84,7 @@ create(:notification, appeals_id: "5d70058f-8641-4155-bae8-5af4b61b1576", appeals_type: "Appeal", - event_type: "Hearing scheduled", + event_type: Constants.EVENT_TYPE_FILTERS.hearing_scheduled, event_date: Time.zone.today, notification_type: "Email and SMS", email_notification_external_id: "2", @@ -95,7 +95,7 @@ create(:notification, appeals_id: "5d70058f-8641-4155-bae8-5af4b61b1576", appeals_type: "Appeal", - event_type: "Hearing scheduled", + event_type: Constants.EVENT_TYPE_FILTERS.hearing_scheduled, event_date: Time.zone.today, notification_type: "Email and SMS", email_notification_external_id: "3", @@ -106,7 +106,7 @@ create(:notification, appeals_id: "5d70058f-8641-4155-bae8-5af4b61b1577", appeals_type: "Appeal", - event_type: "Hearing scheduled", + event_type: Constants.EVENT_TYPE_FILTERS.hearing_scheduled, event_date: Time.zone.today, notification_type: "Email and SMS", email_notification_external_id: "4", diff --git a/spec/jobs/virtual_hearings/create_conference_job_spec.rb b/spec/jobs/virtual_hearings/create_conference_job_spec.rb index 9828792b5b2..343e4e07fa9 100644 --- a/spec/jobs/virtual_hearings/create_conference_job_spec.rb +++ b/spec/jobs/virtual_hearings/create_conference_job_spec.rb @@ -58,7 +58,7 @@ end end.to( have_performed_job(VirtualHearings::CreateConferenceJob) - .exactly(10) + .exactly(5) .times ) end @@ -170,7 +170,7 @@ end it "job goes back on queue and logs if error", :aggregate_failures do - expect(Rails.logger).to receive(:error).exactly(11).times + expect(Rails.logger).to receive(:error).exactly(6).times expect do perform_enqueued_jobs do @@ -178,7 +178,7 @@ end end.to( have_performed_job(VirtualHearings::CreateConferenceJob) - .exactly(10) + .exactly(5) .times ) diff --git a/spec/models/appeal_state_spec.rb b/spec/models/appeal_state_spec.rb index e9d85d775dc..3684e193fcb 100644 --- a/spec/models/appeal_state_spec.rb +++ b/spec/models/appeal_state_spec.rb @@ -2,6 +2,681 @@ describe AppealState do it_behaves_like "AppealState belongs_to polymorphic appeal" do - let!(:_user) { create(:user) } # A User needs to exist for `appeal_state` factories + let!(:user) { create(:user) } # A User needs to exist for `appeal_state` factories + end + + let!(:user) { create(:user) } + + context "State scopes" do + let!(:appeal_docketed_state) do + create( + :appeal_state, + :ama, + created_by_id: user.id, + updated_by_id: user.id, + appeal_docketed: true + ) + end + + let!(:hearing_withdrawn_docketed_appeal_state) do + create( + :appeal_state, + :ama, + created_by_id: user.id, + updated_by_id: user.id, + hearing_withdrawn: true, + appeal_docketed: true + ) + end + + let!(:privacy_pending_state) do + create( + :appeal_state, + :ama, + created_by_id: user.id, + updated_by_id: user.id, + hearing_withdrawn: false, + vso_ihp_pending: false, + privacy_act_pending: true + ) + end + + let!(:ihp_pending_state) do + create( + :appeal_state, + :ama, + created_by_id: user.id, + updated_by_id: user.id, + hearing_withdrawn: false, + vso_ihp_pending: true, + privacy_act_pending: false + ) + end + + let!(:ihp_pending_privacy_pending_state) do + create( + :appeal_state, + :ama, + created_by_id: user.id, + updated_by_id: user.id, + hearing_withdrawn: false, + vso_ihp_pending: true, + privacy_act_pending: true + ) + end + + let!(:hearing_to_be_rescheduled_postponed_state) do + create( + :appeal_state, + :ama, + created_by_id: user.id, + updated_by_id: user.id, + hearing_postponed: true + ) + end + + let!(:hearing_to_be_rescheduled_scheduled_in_error_state) do + create( + :appeal_state, + :ama, + created_by_id: user.id, + updated_by_id: user.id, + scheduled_in_error: true + ) + end + + let!(:hearing_to_be_rescheduled_privacy_pending_state) do + create( + :appeal_state, + :ama, + created_by_id: user.id, + updated_by_id: user.id, + hearing_postponed: true, + privacy_act_pending: true + ) + end + + let!(:appeal_decided_state) do + create( + :appeal_state, + :ama, + created_by_id: user.id, + updated_by_id: user.id, + decision_mailed: true + ) + end + + let!(:appeal_cancelled_state) do + create( + :appeal_state, + :ama, + created_by_id: user.id, + updated_by_id: user.id, + appeal_cancelled: true + ) + end + + shared_context "staged hearing task tree" do + let(:appeal) { create(:appeal, :active) } + let(:distribution_task) { DistributionTask.create!(appeal: appeal, parent: appeal.root_task) } + let(:hearing_task) { HearingTask.create!(appeal: appeal, parent: distribution_task) } + let!(:assign_disp_task) do + AssignHearingDispositionTask.create!(appeal: appeal, parent: hearing_task, assigned_to: Bva.singleton) + end + end + + shared_context "vacols case with case hearing" do + let(:case_hearing) { create(:case_hearing) } + let(:vacols_case) { create(:case, case_hearings: [case_hearing]) } + let!(:legacy_appeal) { create(:legacy_appeal, vacols_case: vacols_case) } + end + + context "#eligible_for_quarterly" do + subject { described_class.eligible_for_quarterly.pluck(:id) } + + it "Decided and cancelled appeals are excluded" do + is_expected.to_not contain_exactly( + appeal_decided_state.id, appeal_cancelled_state.id + ) + end + end + + context "#appeal_docketed" do + subject { described_class.appeal_docketed.pluck(:id) } + + it "Only appeals in docketed state are included" do + is_expected.to match_array( + [appeal_docketed_state.id, hearing_withdrawn_docketed_appeal_state.id] + ) + end + end + + context "#hearing_scheduled" do + subject { described_class.hearing_scheduled.pluck(:id) } + + context "ama" do + subject do + AppealState.find_by_appeal_id(appeal.id).update!(hearing_scheduled: true) + + described_class.hearing_scheduled.pluck(:id) + end + + let!(:appeal_state) { appeal.appeal_state } + + context "Whenever the expected task is absent" do + let(:appeal) { create(:appeal, :active) } + + it "The appeal state isn't retrieved by the query" do + is_expected.to be_empty + end + end + + context "Whenever the hearing has been held and the evidence submission window is open" do + include_context "staged hearing task tree" + + let!(:evidence_task) do + EvidenceSubmissionWindowTask.create!( + appeal: appeal, + parent: assign_disp_task, + assigned_to: MailTeam.singleton + ) + end + + it "The appeal state isn't retrieved by the query" do + is_expected.to be_empty + end + end + + context "Whenever the hearing has not been held" do + include_context "staged hearing task tree" + + it "The appeal state is returned" do + is_expected.to match_array([appeal_state.id]) + end + end + end + + context "legacy" do + include_context "vacols case with case hearing" + + let!(:appeal_state) { legacy_appeal.appeal_state.tap { _1.update!(hearing_scheduled: true) } } + + context "hearing has not been held" do + it "the appeal state is returned" do + is_expected.to match_array([appeal_state.id]) + end + end + + context "hearing has been held" do + it "the appeal state is not returned" do + case_hearing.update!(hearing_disp: "H") + + is_expected.to be_empty + end + end + end + + context "ama and legacy" do + include_context "vacols case with case hearing" + include_context "staged hearing task tree" + + let!(:ama_state) { appeal.appeal_state.tap { _1.update!(hearing_scheduled: true) } } + let!(:legacy_state) { legacy_appeal.appeal_state.tap { _1.update!(hearing_scheduled: true) } } + + context "An AMA and legacy hearings are both pending" do + it "both appeal states are returned by the query" do + is_expected.to match_array([ama_state.id, legacy_state.id]) + end + end + + context "An AMA and legacy hearings have been held" do + let!(:evidence_task) do + EvidenceSubmissionWindowTask.create!( + parent: assign_disp_task, + appeal: appeal, + assigned_to: MailTeam.singleton + ) + end + + it "neither appeal states are returned by the query" do + case_hearing.update!(hearing_disp: "H") + + is_expected.to be_empty + end + end + + context "An AMA hearing is pending and the legacy hearing has been held" do + it "only the AMA appeal state is returned by the query" do + case_hearing.update!(hearing_disp: "H") + + is_expected.to match_array([ama_state.id]) + end + end + + context "A legacy hearing is pending and an AMA hearing has been held" do + let!(:evidence_task) do + EvidenceSubmissionWindowTask.create!( + assigned_to: MailTeam.singleton, + parent: assign_disp_task, + appeal: appeal + ) + end + + it "only the legacy appeal state is returned by the query" do + is_expected.to match_array([legacy_state.id]) + end + end + end + end + + context "#hearing_scheduled_privacy_pending" do + include_context "staged hearing task tree" + + subject { described_class.hearing_scheduled_privacy_pending.pluck(:id) } + + let!(:hearing_scheduled_privacy_pending_state) do + appeal.appeal_state.tap do + _1.update!(hearing_scheduled: true, + privacy_act_pending: true) + end + end + + it "Only appeals in hearing scheduled and privacy act state are included" do + is_expected.to match_array([hearing_scheduled_privacy_pending_state.id]) + end + end + + context "#hearing_to_be_rescheduled" do + subject { described_class.hearing_to_be_rescheduled.pluck(:id) } + + it "Only appeals in hearing to be rescheduled state are included" do + is_expected.to match_array( + [ + hearing_to_be_rescheduled_postponed_state.id, + hearing_to_be_rescheduled_scheduled_in_error_state.id + ] + ) + end + end + + context "#hearing_to_be_rescheduled_privacy_pending" do + subject { described_class.hearing_to_be_rescheduled_privacy_pending.pluck(:id) } + + it "Only appeals in hearing to be rescheduled and privacy act state are included" do + is_expected.to match_array([hearing_to_be_rescheduled_privacy_pending_state.id]) + end + end + + context "#ihp_pending" do + subject { described_class.ihp_pending.pluck(:id) } + + it "Only appeals in VSO IHP pending state are included" do + is_expected.to match_array([ihp_pending_state.id]) + end + end + + context "#ihp_pending_privacy_pending" do + subject { described_class.ihp_pending_privacy_pending.pluck(:id) } + + it "Only appeals in VSO IHP Pending and privacy act state are included" do + is_expected.to match_array([ihp_pending_privacy_pending_state.id]) + end + end + + context "#privacy_pending" do + subject { described_class.privacy_pending.pluck(:id) } + + it "Only appeals in hearing to be rescheduled and privacy act state are included" do + is_expected.to match_array([privacy_pending_state.id]) + end + end + end + + shared_examples "privacy_act_pending status remains active upon update" do + before { appeal_state.update!(privacy_act_pending: true) } + + it "privacy_act_pending remains true" do + subject + + expect(appeal_state.privacy_act_pending).to eq true + end + end + + context "#appeal_docketed_appeal_state_update!" do + subject { appeal_state.appeal_docketed_appeal_state_update_action! } + + context "updates the appeal_docketed attribute" do + include_examples "privacy_act_pending status remains active upon update" + + let(:appeal_state) do + create( + :appeal_state, + :ama, + created_by_id: user.id, + updated_by_id: user.id, + appeal_docketed: false + ) + end + + it "sets appeal_docketed to true and all others false" do + subject + + expect(appeal_state.appeal_docketed).to eq true + end + end + end + + context "#vso_ihp_pending_appeal_state_update!" do + subject { appeal_state.vso_ihp_pending_appeal_state_update_action! } + + context "updates the vso_ihp_pending attribute" do + include_examples "privacy_act_pending status remains active upon update" + + let(:appeal_state) do + create( + :appeal_state, + :ama, + created_by_id: user.id, + updated_by_id: user.id, + appeal_docketed: true + ) + end + + it "sets vso_ihp_pending to true" do + subject + + expect(appeal_state.vso_ihp_pending).to eq true + end + end + end + + context "#vso_ihp_cancelled_appeal_state_update!" do + subject { appeal_state.vso_ihp_cancelled_appeal_state_update_action! } + + context "updates the vso_ihp_pending attribute" do + include_examples "privacy_act_pending status remains active upon update" + + let(:appeal_state) do + create( + :appeal_state, + :ama, + created_by_id: user.id, + updated_by_id: user.id, + vso_ihp_pending: true + ) + end + + it "sets vso_ihp_pending to false and all others false" do + subject + + expect(appeal_state.vso_ihp_pending).to eq false + end + end + end + + context "#vso_ihp_complete_appeal_state_update_action!" do + subject { appeal_state.vso_ihp_complete_appeal_state_update_action! } + + context "updates the vso_ihp_complete attribute" do + include_examples "privacy_act_pending status remains active upon update" + + let(:appeal_state) do + create( + :appeal_state, + :ama, + created_by_id: user.id, + updated_by_id: user.id, + vso_ihp_complete: true + ) + end + + it "sets vso_ihp_complete to true and all others false" do + subject + + expect(appeal_state.vso_ihp_complete).to eq true + end + end + end + + context "#privacy_act_pending_appeal_state_update!" do + subject { appeal_state.privacy_act_pending_appeal_state_update_action! } + + context "updates the privacy_act_pending attribute" do + let(:appeal_state) do + create( + :appeal_state, + :ama, + created_by_id: user.id, + updated_by_id: user.id, + privacy_act_pending: false + ) + end + + it "sets privacy_act_pending to true and all others false" do + subject + + expect(appeal_state.privacy_act_pending).to eq true + end + end + end + + context "#privacy_act_cancelled_appeal_state_update_action!" do + subject { appeal_state.privacy_act_cancelled_appeal_state_update_action! } + + context "updates the privacy_act_pending attribute" do + let(:appeal_state) do + create( + :appeal_state, + :ama, + created_by_id: user.id, + updated_by_id: user.id, + privacy_act_pending: true + ) + end + + it "sets privacy_act_cancelled to true and all others false" do + subject + + expect(appeal_state.privacy_act_pending).to eq false + end + end + end + + context "#privacy_act_complete_appeal_state_update_action!" do + subject { appeal_state.privacy_act_complete_appeal_state_update_action! } + + context "updates the privacy_act_complete attribute" do + let(:appeal_state) do + create( + :appeal_state, + :ama, + created_by_id: user.id, + updated_by_id: user.id, + privacy_act_pending: true, + hearing_scheduled: true + ) + end + + it "sets privacy_act_complete to true and leaves others intact" do + subject + + expect(appeal_state.privacy_act_pending).to eq false + expect(appeal_state.privacy_act_complete).to eq true + expect(appeal_state.hearing_scheduled).to eq true + end + end + end + + context "#decision_mailed_appeal_state_update_action!" do + subject { appeal_state.decision_mailed_appeal_state_update_action! } + + context "updates the decision_mailed attribute" do + include_examples "privacy_act_pending status remains active upon update" + + let(:appeal_state) do + create( + :appeal_state, + :ama, + created_by_id: user.id, + updated_by_id: user.id, + decision_mailed: false + ) + end + + it "sets decision_mailed to true and all others false" do + subject + + expect(appeal_state.decision_mailed).to eq true + end + end + end + + context "#appeal_cancelled_appeal_state_update_action!" do + subject { appeal_state.appeal_cancelled_appeal_state_update_action! } + + context "updates the appeal_cancelled attribute" do + let(:appeal_state) do + create( + :appeal_state, + :ama, + created_by_id: user.id, + updated_by_id: user.id, + hearing_scheduled: true, + privacy_act_complete: true + ) + end + + it "sets appeal_cancelled to true and all others false" do + subject + + expect(appeal_state.hearing_scheduled).to eq false + expect(appeal_state.privacy_act_complete).to eq false + expect(appeal_state.appeal_cancelled).to eq true + end + end + end + + context "#hearing_postponed_appeal_state_update!" do + subject { appeal_state.hearing_postponed_appeal_state_update_action! } + + context "updates the hearing_postponed attribute" do + include_examples "privacy_act_pending status remains active upon update" + + let(:appeal_state) do + create( + :appeal_state, + :ama, + created_by_id: user.id, + updated_by_id: user.id, + hearing_scheduled: true + ) + end + + it "sets hearing_postponed to true and all others false" do + subject + + expect(appeal_state.hearing_scheduled).to eq false + expect(appeal_state.hearing_postponed).to eq true + end + end + end + + context "#hearing_withdrawn_appeal_state_update!" do + subject { appeal_state.hearing_withdrawn_appeal_state_update_action! } + + context "updates the hearing_withdrawn attribute" do + include_examples "privacy_act_pending status remains active upon update" + + let(:appeal_state) do + create( + :appeal_state, + :ama, + created_by_id: user.id, + updated_by_id: user.id, + hearing_scheduled: true + ) + end + + it "sets hearing_withdrawn to true and all others false" do + subject + + expect(appeal_state.appeal_docketed).to eq false + expect(appeal_state.hearing_withdrawn).to eq true + end + end + end + context "#hearing_scheduled_appeal_state_update!" do + subject { appeal_state.hearing_scheduled_appeal_state_update_action! } + + context "updates the hearing_scheduled attribute" do + include_examples "privacy_act_pending status remains active upon update" + + let(:appeal_state) do + create( + :appeal_state, + :ama, + created_by_id: user.id, + updated_by_id: user.id, + appeal_docketed: true + ) + end + + it "sets hearing_scheduled to true" do + subject + + expect(appeal_state.hearing_scheduled).to eq true + end + end + end + + context "hearing_held_appeal_state_update_action!" do + subject { appeal_state.hearing_held_appeal_state_update_action! } + + context "updates the hearing_scheduled attribute" do + include_examples "privacy_act_pending status remains active upon update" + + let(:appeal_state) do + create( + :appeal_state, + :ama, + created_by_id: user.id, + updated_by_id: user.id, + hearing_scheduled: true + ) + end + + it "sets hearing_scheduled to true and all others false" do + expect(appeal_state.hearing_scheduled).to eq true + + subject + + expect(appeal_state.hearing_scheduled).to eq false + end + end + end + + context "#scheduled_in_error_appeal_state_update!" do + subject { appeal_state.scheduled_in_error_appeal_state_update_action! } + + context "updates the scheduled_in_error attribute" do + include_examples "privacy_act_pending status remains active upon update" + + let(:appeal_state) do + create( + :appeal_state, + :ama, + created_by_id: user.id, + updated_by_id: user.id, + hearing_scheduled: true + ) + end + + it "sets scheduled_in_error to true and all others false" do + subject + + expect(appeal_state.hearing_scheduled).to eq false + expect(appeal_state.scheduled_in_error).to eq true + end + end end end diff --git a/spec/models/appellant_notification_spec.rb b/spec/models/appellant_notification_spec.rb index d4d0289a4e5..a0438ec0ced 100644 --- a/spec/models/appellant_notification_spec.rb +++ b/spec/models/appellant_notification_spec.rb @@ -94,7 +94,7 @@ describe "docket_appeal" do let(:appeal) { create(:appeal, :with_pre_docket_task) } let(:appeal_state) { create(:appeal_state, appeal_id: appeal.id, appeal_type: appeal.class.to_s) } - let(:template_name) { "Appeal docketed" } + let(:template_name) { Constants.EVENT_TYPE_FILTERS.appeal_docketed } let!(:pre_docket_task) { PreDocketTask.find_by(appeal: appeal) } it "will update the appeal state after docketing the Predocketed Appeal" do pre_docket_task.docket_appeal @@ -107,16 +107,12 @@ appeal_state_record = AppealState.find_by(appeal_id: appeal.id, appeal_type: appeal.class.to_s) expect(appeal_state_record.appeal_docketed).to eq(true) end - it "will update the appeal state after docketing the Predocketed Appeal" do - expect(AppellantNotification).to receive(:appeal_mapper).with(appeal.id, appeal.class.to_s, "appeal_docketed") - pre_docket_task.docket_appeal - end end describe "create_tasks_on_intake_success!" do let(:appeal) { create(:appeal) } let(:appeal_state) { create(:appeal_state, appeal_id: appeal.id, appeal_type: appeal.class.to_s) } - let(:template_name) { "Appeal docketed" } + let(:template_name) { Constants.EVENT_TYPE_FILTERS.appeal_docketed } it "will notify appellant that appeal is docketed on successful intake" do appeal.create_tasks_on_intake_success! appeal_state_record = AppealState.find_by(appeal_id: appeal.id, appeal_type: appeal.class.to_s) @@ -127,10 +123,6 @@ appeal_state_record = AppealState.find_by(appeal_id: appeal.id, appeal_type: appeal.class.to_s) expect(appeal_state_record.appeal_docketed).to eq(true) end - it "will update appeal state after appeal is docketed on successful intake" do - expect(AppellantNotification).to receive(:appeal_mapper).with(appeal.id, appeal.class.to_s, "appeal_docketed") - appeal.create_tasks_on_intake_success! - end end end @@ -149,8 +141,8 @@ file: "some file" } end - let(:contested) { "Appeal decision mailed (Contested claims)" } - let(:non_contested) { "Appeal decision mailed (Non-contested claims)" } + let(:contested) { Constants.EVENT_TYPE_FILTERS.appeal_decision_mailed_contested_claims } + let(:non_contested) { Constants.EVENT_TYPE_FILTERS.appeal_decision_mailed_non_contested_claims } let(:dispatch) { LegacyAppealDispatch.new(appeal: legacy_appeal, params: params) } let(:dispatch_func) { "create_decision_document_and_submit_for_processing!" } it "Will notify appellant that the legacy appeal decision has been mailed (Non Contested)" do @@ -158,12 +150,6 @@ decision_document = dispatch.send dispatch_func, params decision_document.process! end - it "Will update appeal state after legacy appeal decision has been mailed (Non Contested)" do - expect(AppellantNotification).to receive(:appeal_mapper) - .with(legacy_appeal.id, legacy_appeal.class.to_s, "decision_mailed") - decision_document = dispatch.send dispatch_func, params - decision_document.process! - end it "Will notify appellant that the legacy appeal decision has been mailed (Contested)" do expect(AppellantNotification).to receive(:notify_appellant).with(legacy_appeal, contested) allow(legacy_appeal).to receive(:contested_claim).and_return(true) @@ -171,14 +157,6 @@ decision_document = dispatch.send dispatch_func, params decision_document.process! end - it "Will update appeal state after legacy appeal decision has been mailed (Contested)" do - expect(AppellantNotification).to receive(:appeal_mapper) - .with(legacy_appeal.id, legacy_appeal.class.to_s, "decision_mailed") - allow(legacy_appeal).to receive(:contested_claim).and_return(true) - legacy_appeal.contested_claim - decision_document = dispatch.send dispatch_func, params - decision_document.process! - end end describe "AMA Appeal Decision Mailed" do @@ -207,8 +185,8 @@ file: "some file" } end - let(:contested) { "Appeal decision mailed (Contested claims)" } - let(:non_contested) { "Appeal decision mailed (Non-contested claims)" } + let(:contested) { Constants.EVENT_TYPE_FILTERS.appeal_decision_mailed_contested_claims } + let(:non_contested) { Constants.EVENT_TYPE_FILTERS.appeal_decision_mailed_non_contested_claims } let(:dispatch) do AmaAppealDispatch.new( appeal: appeal, @@ -229,11 +207,6 @@ decision_document = dispatch.send dispatch_func, params decision_document.process! end - it "Will update appeal state after AMA appeal decision has been mailed (Non Contested)" do - expect(AppellantNotification).to receive(:appeal_mapper).with(appeal.id, appeal.class.to_s, "decision_mailed") - decision_document = dispatch.send dispatch_func, params - decision_document.process! - end it "Will notify appellant that the AMA appeal decision has been mailed (Contested)" do expect(AppellantNotification).to receive(:notify_appellant).with(contested_appeal, contested) allow(contested_appeal).to receive(:contested_claim?).and_return(true) @@ -242,14 +215,6 @@ .send dispatch_func, contested_params contested_decision_document.process! end - it "Will update appeal state after AMA appeal decision has been mailed (Contested)" do - expect(AppellantNotification).to receive(:appeal_mapper) - .with(contested_appeal.id, contested_appeal.class.to_s, "decision_mailed") - allow(contested_appeal).to receive(:contested_claim?).and_return(true) - contested_appeal.contested_claim? - contested_decision_document = contested_dispatch.send dispatch_func, contested_params - contested_decision_document.process! - end end end @@ -263,7 +228,7 @@ appeal_type: appeal_hearing.class.to_s, created_by_id: user.id, updated_by_id: user.id) end - let(:template_name) { "Hearing scheduled" } + let(:template_name) { Constants.EVENT_TYPE_FILTERS.hearing_scheduled } let(:hearing) { create(:hearing, appeal: appeal) } let(:schedule_hearing_task) { ScheduleHearingTask.find_by(appeal: appeal_hearing) } let(:task_values) do @@ -319,7 +284,7 @@ describe HearingPostponed do describe "#postpone!" do - let(:template_name) { "Postponement of hearing" } + let(:template_name) { Constants.EVENT_TYPE_FILTERS.postponement_of_hearing } let(:postponed_hearing) { create(:hearing, :postponed, :with_tasks) } let(:appeal_state) do create(:appeal_state, @@ -354,7 +319,7 @@ let!(:hearing) { create(:hearing, hearing_day: hearing_day) } let(:appeal_state) { create(:appeal_state, appeal_id: hearing.appeal.id, appeal_type: hearing.appeal.class.to_s) } context "when a hearing coordinator selects 'postponed' on the daily docket page for an AMA Appeal" do - let(:template_name) { "Postponement of hearing" } + let(:template_name) { Constants.EVENT_TYPE_FILTERS.postponement_of_hearing } let(:params) do { hearing: hearing.reload, @@ -387,7 +352,7 @@ let!(:hearing) { create(:hearing, hearing_day: hearing_day) } let(:appeal_state) { create(:appeal_state, appeal_id: hearing.appeal.id, appeal_type: hearing.appeal.class.to_s) } context "when a hearing coordinator selects 'cancelled' on the daily docket page for an AMA Appeal" do - let(:template_name) { "Withdrawal of hearing" } + let(:template_name) { Constants.EVENT_TYPE_FILTERS.withdrawal_of_hearing } let(:params) do { hearing: hearing.reload, @@ -415,7 +380,7 @@ end describe DocketHearingPostponed do - let!(:template_name) { "Postponement of hearing" } + let!(:template_name) { Constants.EVENT_TYPE_FILTERS.postponement_of_hearing } let(:bva) { Bva.singleton } let!(:hearing_coord) { create(:user, roles: ["Edit HearSched", "Build HearSched"]) } describe ".update_hearing" do @@ -477,7 +442,7 @@ end describe DocketHearingWithdrawn do - let!(:template_name) { "Withdrawal of hearing" } + let!(:template_name) { Constants.EVENT_TYPE_FILTERS.withdrawal_of_hearing } let(:bva) { Bva.singleton } let!(:hearing_coord) { create(:user, roles: ["Edit HearSched", "Build HearSched"]) } describe ".update_hearing" do @@ -543,8 +508,8 @@ end describe "FOIA/Privacy Act tasks" do - let(:template_pending) { "Privacy Act request pending" } - let(:template_closed) { "Privacy Act request complete" } + let(:template_pending) { Constants.EVENT_TYPE_FILTERS.privacy_act_request_pending } + let(:template_closed) { Constants.EVENT_TYPE_FILTERS.privacy_act_request_complete } context "HearingAdminFoiaPrivacyRequestTask" do let(:appeal) { create(:appeal) } @@ -584,8 +549,7 @@ task_params_org) end it "updates appeal state when task is created" do - expect(AppellantNotification).to receive(:appeal_mapper) - .with(appeal.id, appeal.class.to_s, "privacy_act_pending") + expect_any_instance_of(AppealState).to receive(:privacy_act_pending_appeal_state_update_action!) HearingAdminActionFoiaPrivacyRequestTask.create_child_task(parent_task, hearings_management_user, task_params_org) @@ -595,13 +559,11 @@ hafpr_child.update!(status: "completed") end it "updates appeal state when task is completed" do - expect(AppellantNotification).to receive(:appeal_mapper) - .with(appeal.id, appeal.class.to_s, "privacy_act_complete") + expect_any_instance_of(AppealState).to receive(:privacy_act_complete_appeal_state_update_action!) hafpr_child.update!(status: "completed") end it "updates appeal state when task is cancelled" do - expect(AppellantNotification).to receive(:appeal_mapper) - .with(appeal.id, appeal.class.to_s, "privacy_act_cancelled") + expect_any_instance_of(AppealState).to receive(:privacy_act_cancelled_appeal_state_update_action!) hafpr_child.update!(status: "cancelled") end end @@ -623,30 +585,44 @@ before do priv_org.add_user(current_user) end + context "PrivacyActRequestMailTask" do - let(:task_params) do - { - type: "PrivacyActRequestMailTask", - instructions: "fjdkfjwpie" - } - end - it "sends a notification when PrivacyActRequestMailTask is created" do - expect(AppellantNotification).to receive(:notify_appellant).with(appeal, template_pending) - mail_task.create_twin_of_type(task_params) - end - it "updates appeal state when PrivacyActRequestMailTask is created" do - expect(AppellantNotification).to receive(:appeal_mapper) - .with(appeal.id, appeal.class.to_s, "privacy_act_pending") - mail_task.create_twin_of_type(task_params) + context "being created" do + let(:appeal) { create(:appeal) } + let(:root_task) { RootTask.create!(appeal: appeal) } + let!(:distribution_task) { DistributionTask.create!(appeal: appeal, parent: root_task) } + let(:current_user) do + create(:user).tap do |user| + MailTeam.singleton.add_user(user) + end + end + + let(:task_params) do + { + type: "PrivacyActRequestMailTask", + instructions: "fjdkfjwpie", + parent_id: root_task.id + } + end + + it "sends a notification when PrivacyActRequestMailTask is created" do + expect(AppellantNotification).to receive(:notify_appellant).with(appeal, template_pending) + PrivacyActRequestMailTask.create_from_params(task_params, current_user) + end + + it "updates appeal state when PrivacyActRequestMailTask is created" do + expect_any_instance_of(AppealState).to receive(:privacy_act_pending_appeal_state_update_action!) + PrivacyActRequestMailTask.create_from_params(task_params, current_user) + end end + it "sends a notification when PrivacyActRequestMailTask is completed" do expect(AppellantNotification).to receive(:notify_appellant).with(appeal, template_closed) foia_child.update!(status: "completed") foia_task.update_status_if_children_tasks_are_closed(foia_child) end it "updates appeal state when PrivacyActRequestMailTask is completed" do - expect(AppellantNotification).to receive(:appeal_mapper) - .with(appeal.id, appeal.class.to_s, "privacy_act_complete") + expect_any_instance_of(AppealState).to receive(:privacy_act_complete_appeal_state_update_action!) foia_child.update!(status: "completed") foia_task.update_status_if_children_tasks_are_closed(foia_child) end @@ -656,8 +632,7 @@ foia_task.update_status_if_children_tasks_are_closed(foia_child) end it "does updates appeal state when PrivacyActRequestMailTask is cancelled" do - expect(AppellantNotification).to receive(:appeal_mapper) - .with(appeal.id, appeal.class.to_s, "privacy_act_cancelled") + expect_any_instance_of(AppealState).to receive(:privacy_act_cancelled_appeal_state_update_action!) foia_child.update!(status: "cancelled") foia_task.update_status_if_children_tasks_are_closed(foia_child) end @@ -690,8 +665,7 @@ ColocatedTask.create_from_params(foia_colocated_task, attorney) end it "updates appeal state when creating a FoiaColocatedTask" do - expect(AppellantNotification).to receive(:appeal_mapper) - .with(appeal.id, appeal.class.to_s, "privacy_act_pending") + expect_any_instance_of(AppealState).to receive(:privacy_act_pending_appeal_state_update_action!) ColocatedTask.create_from_params(foia_colocated_task, attorney) end it "sends notification when completing a FoiaColocatedTask" do @@ -701,8 +675,7 @@ end it "updates appeal state when completing a FoiaColocatedTask" do foia_c_task = ColocatedTask.create_from_params(foia_colocated_task, attorney) - expect(AppellantNotification).to receive(:appeal_mapper) - .with(appeal.id, appeal.class.to_s, "privacy_act_complete") + expect_any_instance_of(AppealState).to receive(:privacy_act_complete_appeal_state_update_action!) foia_c_task.children.first.update!(status: "completed") end it "does not send a notification when cancelling a FoiaColocatedTask" do @@ -712,8 +685,7 @@ end it "updates the appeal state when cancelling a FoiaColocatedTask" do foia_c_task = ColocatedTask.create_from_params(foia_colocated_task, attorney) - expect(AppellantNotification).to receive(:appeal_mapper) - .with(appeal.id, appeal.class.to_s, "privacy_act_cancelled") + expect_any_instance_of(AppealState).to receive(:privacy_act_cancelled_appeal_state_update_action!) foia_c_task.children.first.update!(status: "cancelled") end end @@ -756,8 +728,7 @@ PrivacyActTask.create_child_task(colocated_task, attorney, privacy_params_org) end it "updates appeal state when creating a PrivacyActTask" do - expect(AppellantNotification).to receive(:appeal_mapper) - .with(appeal.id, appeal.class.to_s, "privacy_act_pending") + expect_any_instance_of(AppealState).to receive(:privacy_act_pending_appeal_state_update_action!) PrivacyActTask.create_child_task(colocated_task, attorney, privacy_params_org) end it "sends notification when completing a PrivacyActTask assigned to user" do @@ -765,10 +736,8 @@ privacy_child.update!(status: "completed") end it "updates appeal state when completing a PrivacyActTask assigned to user" do - expect(AppellantNotification).to receive(:appeal_mapper) - .with(appeal.id, appeal.class.to_s, "privacy_act_pending") - expect(AppellantNotification).to receive(:appeal_mapper) - .with(appeal.id, appeal.class.to_s, "privacy_act_complete") + expect_any_instance_of(AppealState).to receive(:privacy_act_pending_appeal_state_update_action!) + expect_any_instance_of(AppealState).to receive(:privacy_act_complete_appeal_state_update_action!) privacy_child.update!(status: "completed") end it "sends notification when completing a PrivacyActTask assigned to organization" do @@ -776,10 +745,8 @@ privacy_parent.update_with_instructions(status: "completed") end it "updates appeal state when cancelling a PrivacyActTask assigned to organization" do - expect(AppellantNotification).to receive(:appeal_mapper) - .with(appeal.id, appeal.class.to_s, "privacy_act_pending") - expect(AppellantNotification).to receive(:appeal_mapper) - .with(appeal.id, appeal.class.to_s, "privacy_act_cancelled") + expect_any_instance_of(AppealState).to receive(:privacy_act_pending_appeal_state_update_action!) + expect_any_instance_of(AppealState).to receive(:privacy_act_cancelled_appeal_state_update_action!) privacy_parent.update_with_instructions(status: "cancelled") end end @@ -802,7 +769,7 @@ ) end let(:task_factory) { IhpTasksFactory.new(root_task) } - let(:template_name) { "VSO IHP pending" } + let(:template_name) { Constants.EVENT_TYPE_FILTERS.vso_ihp_pending } before do allow_any_instance_of(BGSService).to receive(:fetch_poas_by_participant_ids) .with([participant_id_with_pva]) do @@ -841,7 +808,7 @@ end let(:root_task) { RootTask.find_by(appeal: appeal) } let(:task_factory) { IhpTasksFactory.new(root_task) } - let(:template_name) { "VSO IHP pending" } + let(:template_name) { Constants.EVENT_TYPE_FILTERS.vso_ihp_pending } it "The appellant will NOT recieve an 'IhpTaskPending' notification" do expect(AppellantNotification).not_to receive(:notify_appellant).with(appeal, template_name) task_factory.create_ihp_tasks! @@ -860,7 +827,7 @@ let(:colocated_task) do ColocatedTask.create!(appeal: appeal, parent_id: root_task.id, assigned_by: attorney, assigned_to: org) end - let(:template_name) { "VSO IHP pending" } + let(:template_name) { Constants.EVENT_TYPE_FILTERS.vso_ihp_pending } let(:params) do { instructions: "test", @@ -940,7 +907,7 @@ let(:org) { create(:organization) } let(:task) { create(:colocated_task, :ihp, :in_progress, assigned_to: org) } let(:appeal_state) { create(:appeal_state, appeal_id: task.appeal.id, appeal_type: task.appeal.class.to_s) } - let(:template_name) { "VSO IHP complete" } + let(:template_name) { Constants.EVENT_TYPE_FILTERS.vso_ihp_complete } it "will notify the appellant of the 'IhpTaskComplete' status" do allow(task).to receive(:verify_user_can_update!).with(user).and_return(true) expect(AppellantNotification).to receive(:notify_appellant).with(task.appeal, template_name) @@ -961,7 +928,7 @@ let(:org) { create(:organization) } let(:task) { create(:informal_hearing_presentation_task, :in_progress, assigned_to: org) } let(:appeal_state) { create(:appeal_state, appeal_id: task.appeal.id, appeal_type: task.appeal.class.to_s) } - let(:template_name) { "VSO IHP complete" } + let(:template_name) { Constants.EVENT_TYPE_FILTERS.vso_ihp_complete } it "will notify the appellant of the 'IhpTaskComplete' status" do allow(task).to receive(:verify_user_can_update!).with(user).and_return(true) expect(AppellantNotification).to receive(:notify_appellant).with(task.appeal, template_name) @@ -994,7 +961,7 @@ let(:user) { create(:user) } let(:org) { create(:organization) } let(:task) { create(:colocated_task, :ihp, :in_progress, assigned_to: org) } - let(:template_name) { "VSO IHP complete" } + let(:template_name) { Constants.EVENT_TYPE_FILTERS.vso_ihp_complete } it "will update the 'vso_ihp_complete' column in the Appeal State table to TRUE" do allow(task).to receive(:verify_user_can_update!).with(user).and_return(true) task.update!(status: "completed") @@ -1031,7 +998,7 @@ describe SendNotificationJob do let(:appeal) { create(:appeal, :active) } - let(:template) { "Hearing scheduled" } + let(:template) { Constants.EVENT_TYPE_FILTERS.hearing_scheduled } let(:payload) { AppellantNotification.create_payload(appeal, template_name) } describe "#perform" do it "pushes a new message" do diff --git a/spec/models/tasks/assign_hearing_disposition_task_spec.rb b/spec/models/tasks/assign_hearing_disposition_task_spec.rb index 188901dbfee..133ef7f8546 100644 --- a/spec/models/tasks/assign_hearing_disposition_task_spec.rb +++ b/spec/models/tasks/assign_hearing_disposition_task_spec.rb @@ -221,9 +221,13 @@ it "sets the hearing disposition and calls hold!", :aggregate_failures do expect(disposition_task).to receive(:hold!).exactly(1).times.and_call_original + state = appeal.appeal_state.tap { _1.update!(hearing_scheduled: true) } + expect(state.hearing_scheduled).to eq true + subject expect(hearing.disposition).to eq Constants.HEARING_DISPOSITION_TYPES.held + expect(state.reload.hearing_scheduled).to eq false if appeal.is_a? Appeal expect(Hearing.count).to eq 1 diff --git a/spec/services/external_api/va_notify_service_spec.rb b/spec/services/external_api/va_notify_service_spec.rb index 21b385dccce..53cadcd0d45 100644 --- a/spec/services/external_api/va_notify_service_spec.rb +++ b/spec/services/external_api/va_notify_service_spec.rb @@ -17,6 +17,13 @@ let(:status) { "in-progress" } let(:first_name) { "Bob" } let(:docket_number) { "1234567" } + let(:payload) do + { participant_id: participant_id, + notification_id: notification_id, + first_name: first_name, + docket_number: docket_number, + status: status } + end let(:success_response) do HTTPI::Response.new(200, {}, notification_response_body) end @@ -48,9 +55,7 @@ context "notifications sent" do describe "email" do subject do - ExternalApi::VANotifyService.send_email_notifications( - participant_id, notification_id, email_template_id, first_name, docket_number, status - ) + ExternalApi::VANotifyService.send_email_notifications(**payload, email_template_id: email_template_id) end it "email sent successfully" do allow(HTTPI).to receive(:post).and_return(success_response) @@ -67,9 +72,7 @@ describe "sms" do subject do - ExternalApi::VANotifyService.send_sms_notifications( - participant_id, notification_id, sms_template_id, first_name, docket_number, status - ) + ExternalApi::VANotifyService.send_sms_notifications(**payload, sms_template_id: sms_template_id) end it "sms sent successfully" do allow(HTTPI).to receive(:post).and_return(sms_success_response) diff --git a/spec/support/shared_examples/workflows/vbms_document_transactions.rb b/spec/support/shared_examples/workflows/vbms_document_transactions.rb index bd0233ad6e3..c65441c4dbf 100644 --- a/spec/support/shared_examples/workflows/vbms_document_transactions.rb +++ b/spec/support/shared_examples/workflows/vbms_document_transactions.rb @@ -5,7 +5,7 @@ it "fetches file from s3 and returns temporary location" do pdf_name = "veteran-#{document.veteran_file_number}-doc-#{document.id}.pdf" expect(Caseflow::Fakes::S3Service).to receive(:fetch_file) - expect(doc_to_upload.pdf_location) + expect(document_transaction.pdf_location) .to eq File.join(Rails.root, "tmp", "pdfs", pdf_name) end end @@ -14,16 +14,27 @@ context "fetches a bucket name based on document type" do it "changes based on specific document types" do document.document_type = "BVA Case Notifications" - expect(doc_to_upload.send(:s3_location)).to include("notification-reports") + expect(document_transaction.send(:s3_location)).to include("notification-reports") end it "defaults to idt-uploaded-documents" do - expect(doc_to_upload.send(:s3_location)).to include("idt-uploaded-documents") + expect(document_transaction.send(:s3_location)).to include("idt-uploaded-documents") end end end context "#call" do - subject { doc_to_upload.call } + let(:file_name) do + "veteran-#{document.veteran_file_number}-doc-#{document.id}.pdf" + end + + subject do + # Pulls document from S3 + document_transaction.pdf_location + + expect(File).to exist(File.join(Rails.root, "tmp", "pdfs", file_name)) + + document_transaction.call + end before do allow(VBMSService).to receive(transaction_method).and_call_original @@ -43,11 +54,14 @@ subject expect(VBMSService).to have_received(transaction_method).with( - upload_arg, doc_to_upload + upload_arg, document_transaction ) expect(document.uploaded_to_vbms_at).to eq(Time.zone.now) expect(document.processed_at).to_not be_nil expect(document.submitted_at).to eq(Time.zone.now) + + # Ensure that the file is cleaned up + expect(File).not_to exist(File.join(Rails.root, "tmp", "pdfs", file_name)) end end @@ -62,6 +76,9 @@ expect(document.attempted_at).to eq(Time.zone.now) expect(document.processed_at).to be_nil expect(document.error).to eq("Some VBMS error") + + # Ensure that the file is cleaned up + expect(File).not_to exist(File.join(Rails.root, "tmp", "pdfs", file_name)) end end @@ -86,7 +103,7 @@ expect(S3Service).to receive(:store_file).with(expected_path, /PDF/) - doc_to_upload.cache_file + document_transaction.cache_file end end end diff --git a/spec/workflows/bulk_task_reassignment_spec.rb b/spec/workflows/bulk_task_reassignment_spec.rb index 06bcbc0ffac..9f442c14808 100644 --- a/spec/workflows/bulk_task_reassignment_spec.rb +++ b/spec/workflows/bulk_task_reassignment_spec.rb @@ -93,11 +93,8 @@ expect { subject }.to raise_error(BulkTaskReassignment::InvalidTaskParent).with_message(expected_output) end end - context "with no children" do it "describes what changes will be made and makes them" do - expect(Rails.logger).to receive(:info).exactly(9).times - subject tasks.each do |task| expect(task.reload.cancelled?).to eq true diff --git a/spec/workflows/update_document_in_vbms_spec.rb b/spec/workflows/update_document_in_vbms_spec.rb index 90ad5a86540..8d36f0e0249 100644 --- a/spec/workflows/update_document_in_vbms_spec.rb +++ b/spec/workflows/update_document_in_vbms_spec.rb @@ -21,7 +21,7 @@ end let(:uploaded_to_vbms_at) { nil } let(:processed_at) { nil } - let!(:doc_to_upload) { UpdateDocumentInVbms.new(document: document) } + let!(:document_transaction) { UpdateDocumentInVbms.new(document: document) } let(:transaction_method) { :update_document_in_vbms } let(:upload_arg) { document.appeal } @@ -29,19 +29,19 @@ describe "#source" do it "is hardcoded to BVA" do - expect(doc_to_upload.source).to eq "BVA" + expect(document_transaction.source).to eq "BVA" end end describe "#document_type_id" do it "fetches the ID corresponding to the document type string" do - expect(doc_to_upload.document_type_id).to eq 482 + expect(document_transaction.document_type_id).to eq 482 end end describe "#document_type" do it "reads it from the document instance" do - expect(doc_to_upload.document_type).to eq document.document_type + expect(document_transaction.document_type).to eq document.document_type end end end diff --git a/spec/workflows/upload_document_to_vbms_spec.rb b/spec/workflows/upload_document_to_vbms_spec.rb index 045d40be31a..b69067b2978 100644 --- a/spec/workflows/upload_document_to_vbms_spec.rb +++ b/spec/workflows/upload_document_to_vbms_spec.rb @@ -20,7 +20,7 @@ end let(:uploaded_to_vbms_at) { nil } let(:processed_at) { nil } - let!(:doc_to_upload) { UploadDocumentToVbms.new(document: document) } + let!(:document_transaction) { UploadDocumentToVbms.new(document: document) } let(:transaction_method) { :upload_document_to_vbms_veteran } let(:upload_arg) { document.veteran_file_number } @@ -28,19 +28,19 @@ describe "#source" do it "is hardcoded to BVA" do - expect(doc_to_upload.source).to eq "BVA" + expect(document_transaction.source).to eq "BVA" end end describe "#document_type_id" do it "fetches the ID corresponding to the document type string" do - expect(doc_to_upload.document_type_id).to eq 482 + expect(document_transaction.document_type_id).to eq 482 end end describe "#document_type" do it "reads it from the document instance" do - expect(doc_to_upload.document_type).to eq document.document_type + expect(document_transaction.document_type).to eq document.document_type end end end