Skip to content

Commit

Permalink
Decision Reviews Engine: Duplicate controllers and routes (#19789)
Browse files Browse the repository at this point in the history
* Duplicate main form endpoints
- also contestable issues endpoints
-v1 and v2

* Fix linting errors

* Duplicate final endpoints
- evidence
- notification callbacks

* Fix shared example constant collision

---------

Co-authored-by: dfong-adh <[email protected]>
  • Loading branch information
kayline and dfong-adh authored Dec 19, 2024
1 parent ef379d4 commit ad7406c
Show file tree
Hide file tree
Showing 22 changed files with 2,085 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# frozen_string_literal: true

require 'caseflow/service'
require 'decision_reviews/v1/service'

module DecisionReviews
module V1
class AppealsBaseController < ApplicationController
include FailedRequestLoggable
before_action { authorize :appeals, :access? }

private

def decision_review_service
DecisionReviews::V1::Service.new
end

def request_body_hash
@request_body_hash ||= get_hash_from_request_body
end

def get_hash_from_request_body
# rubocop:disable Style/ClassEqualityComparison
# testing string b/c NullIO class doesn't always exist
raise request_body_is_not_a_hash_error if request.body.class.name == 'Puma::NullIO'
# rubocop:enable Style/ClassEqualityComparison

body = JSON.parse request.body.string
raise request_body_is_not_a_hash_error unless body.is_a?(Hash)

body
rescue JSON::ParserError
raise request_body_is_not_a_hash_error
end

def request_body_is_not_a_hash_error
DecisionReviewV1::ServiceException.new key: 'DR_REQUEST_BODY_IS_NOT_A_HASH'
end

def request_body_debug_data
{
request_body_class_name: request.try(:body).class.name,
request_body_string: request.try(:body).try(:string)
}
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# frozen_string_literal: true

require 'decision_reviews/v1/logging_utils'
require 'common/pdf_helpers'

# Notice of Disagreement evidence submissions
module DecisionReviews
module V1
class DecisionReviewEvidencesController < ApplicationController
include FormAttachmentCreate
include DecisionReviews::V1::LoggingUtils
service_tag 'evidence-upload'

FORM_ATTACHMENT_MODEL = DecisionReviewEvidenceAttachment

private

def serializer_klass
DecisionReviewEvidenceAttachmentSerializer
end

# This method, declared in `FormAttachmentCreate`, is responsible for uploading file data to S3.
def save_attachment_to_cloud!
# `form_attachment` is declared in `FormAttachmentCreate`, included above.
form_attachment_guid = form_attachment&.guid
password = filtered_params[:password]

log_params = {
form_attachment_guid:,
encrypted: password.present?
}

# Unlock pdf with hexapdf instead of using pdftk
if password.present?
unlocked_pdf = unlock_pdf(filtered_params[:file_data], password)
form_attachment.set_file_data!(unlocked_pdf)
else
super
end

log_formatted(**common_log_params.merge(params: log_params, is_success: true))
rescue => e
log_formatted(**common_log_params.merge(params: log_params, is_success: false, response_error: e))
raise e
end

def common_log_params
{
key: :evidence_upload_to_s3,
form_id: get_form_id_from_request_headers,
user_uuid: current_user.uuid,
downstream_system: 'AWS S3'
}
end

def unlock_pdf(file, password)
tmpf = Tempfile.new(['decrypted_form_attachment', '.pdf'])
::Common::PdfHelpers.unlock_pdf(file.tempfile.path, password, tmpf)
tmpf.rewind

file.tempfile.unlink
file.tempfile = tmpf
file
end

def get_form_id_from_request_headers
# 'Source-App-Name', which specifies the form from which evidence was submitted, is taken from `window.appName`,
# which is taken from the `entryName` in the manifest.json files for each form. See:
# - vets-website/src/platform/utilities/api/index.js (apiRequest)
# - vets-website/src/platform/startup/setup.js (setUpCommonFunctionality)
# - vets-website/src/platform/startup/index.js (startApp)
# - vets-api/lib/source_app_middleware.rb
source_app_name = request.env['SOURCE_APP']
# The higher-level review form (996) is not included in this list because it does not permit evidence uploads.
form_id = {
'10182-board-appeal' => '10182',
'995-supplemental-claim' => '995'
}[source_app_name]

if form_id.present?
form_id
else
# If, for some odd reason, the `entryName`s are changed in these manifest.json files (or if the HLR form
# begins accepting additional evidence), we will trigger a DataDog alert hinging on the StatsD metric below.
# Upon receiving this alert, we can update the form_id hash above.
StatsD.increment('decision_review.evidence_upload_to_s3.unexpected_form_id')
# In this situation, there is no good reason to block the Veteran from uploading their evidence to S3,
# so we return the unexpected `source_app_name` to be logged by `log_formatted` above.
source_app_name
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# frozen_string_literal: true

require 'decision_reviews/v1/logging_utils'

module DecisionReviews
module V1
class DecisionReviewNotificationCallbacksController < ApplicationController
include ActionController::HttpAuthentication::Token::ControllerMethods
include DecisionReviews::V1::LoggingUtils

service_tag 'appeal-application'

skip_before_action :verify_authenticity_token, only: [:create]
skip_before_action :authenticate, only: [:create]
skip_after_action :set_csrf_header, only: [:create]
before_action :authenticate_header, only: [:create]

STATSD_KEY_PREFIX = 'api.decision_review.notification_callback'

DELIVERED_STATUS = 'delivered'

APPEAL_TYPE_TO_SERVICE_MAP = {
'HLR' => 'higher-level-review',
'NOD' => 'board-appeal',
'SC' => 'supplemental-claims'
}.freeze

VALID_FUNCTION_TYPES = %w[form evidence secondary_form].freeze

def create
return render json: nil, status: :not_found unless enabled?

payload = JSON.parse(request.body.string)
status = payload['status']&.downcase
reference = payload['reference']

StatsD.increment("#{STATSD_KEY_PREFIX}.received", tags: { status: })
send_silent_failure_avoided_metric(reference) if status == DELIVERED_STATUS

DecisionReviewNotificationAuditLog.create!(notification_id: payload['id'], reference:, status:, payload:)

log_formatted(**log_params(payload, true))
render json: { message: 'success' }
rescue => e
log_formatted(**log_params(payload, false), params: { exception_message: e.message })
render json: { message: 'failed' }
end

private

def log_params(payload, is_success)
{
key: :decision_review_notification_callback,
form_id: '995',
user_uuid: nil,
upstream_system: 'VANotify',
body: payload.merge('to' => '<FILTERED>'), # scrub PII from logs
is_success:,
params: {
notification_id: payload['id'],
status: payload['status']
}
}
end

def send_silent_failure_avoided_metric(reference)
service_name, function_type = parse_reference_value(reference)
tags = ["service:#{service_name}", "function: #{function_type} submission to Lighthouse"]
StatsD.increment('silent_failure_avoided', tags:)
rescue => e
Rails.logger.error('Failed to send silent_failure_avoided metric', params: { reference:, message: e.message })
end

def parse_reference_value(reference)
appeal_type, function_type = reference.split('-')
raise 'Invalid function_type' unless VALID_FUNCTION_TYPES.include? function_type

[APPEAL_TYPE_TO_SERVICE_MAP.fetch(appeal_type.upcase), function_type]
end

def authenticate_header
authenticate_user_with_token || authenticity_error
end

def authenticate_user_with_token
authenticate_with_http_token do |token|
is_authenticated = token == bearer_token_secret
Rails.logger.info('DecisionReviewNotificationCallbacksController callback received', is_authenticated:)

is_authenticated
end
end

def authenticity_error
render json: { message: 'Invalid credentials' }, status: :unauthorized
end

def bearer_token_secret
Settings.nod_vanotify_status_callback.bearer_token
end

def enabled?
Flipper.enabled? :nod_callbacks_endpoint
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# frozen_string_literal: true

module DecisionReviews
module V1
module HigherLevelReviews
class ContestableIssuesController < AppealsBaseController
service_tag 'higher-level-review'

def index
ci = decision_review_service
.get_higher_level_review_contestable_issues(user: current_user, benefit_type: params[:benefit_type])
.body
render json: merge_legacy_appeals(ci)
rescue => e
log_exception_to_personal_information_log(
e,
error_class: "#{self.class.name}#index exception #{e.class} (HLR_V1)",
benefit_type: params[:benefit_type]
)
raise
end

private

def merge_legacy_appeals(contestable_issues)
# Fetch Legacy Appels and combine with CIs
ci_la = nil
begin
la = decision_review_service
.get_legacy_appeals(user: current_user)
.body
# punch in an empty LA section if no LAs for user to distinguish no LAs from a LA call fail
la['data'] = [{ type: 'legacyAppeal', attributes: { issues: [] } }] if la['data'].empty?
ci_la = { data: contestable_issues['data'] + la['data'] }
rescue => e
# If LA fails keep going Legacy Appeals are not critical, return original contestable_issues
log_exception_to_personal_information_log(
e,
error_class: "#{self.class.name}#index exception #{e.class} (HLR_V1_LEGACY_APPEALS)",
benefit_type: params[:benefit_type]
)
contestable_issues
else
ci_la
end
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# frozen_string_literal: true

require 'decision_reviews/saved_claim/service'

module DecisionReviews
module V1
class HigherLevelReviewsController < AppealsBaseController
include DecisionReviews::SavedClaim::Service
service_tag 'higher-level-review'

def show
render json: decision_review_service.get_higher_level_review(params[:id]).body
rescue => e
log_exception_to_personal_information_log(
e, error_class: error_class(method: 'show', exception_class: e.class), id: params[:id]
)
raise
end

def create
hlr_response_body = decision_review_service
.create_higher_level_review(request_body: request_body_hash, user: @current_user)
.body
submitted_appeal_uuid = hlr_response_body.dig('data', 'id')
ActiveRecord::Base.transaction do
AppealSubmission.create!(user_uuid: @current_user.uuid, user_account: @current_user.user_account,
type_of_appeal: 'HLR', submitted_appeal_uuid:)

store_saved_claim(claim_class: ::SavedClaim::HigherLevelReview, form: request_body_hash.to_json,
guid: submitted_appeal_uuid)

# Clear in-progress form since submit was successful
InProgressForm.form_for_user('20-0996', current_user)&.destroy!
end
render json: hlr_response_body
rescue => e
::Rails.logger.error(
message: "Exception occurred while submitting Higher Level Review: #{e.message}",
backtrace: e.backtrace
)

handle_personal_info_error(e)
end

private

def error_class(method:, exception_class:)
"#{self.class.name}##{method} exception #{exception_class} (HLR_V1)"
end

def handle_personal_info_error(e)
request = begin
{ body: request_body_hash }
rescue
request_body_debug_data
end

log_exception_to_personal_information_log(
e, error_class: error_class(method: 'create', exception_class: e.class), request:
)
raise
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# frozen_string_literal: true

module DecisionReviews
module V1
module NoticeOfDisagreements
class ContestableIssuesController < AppealsBaseController
service_tag 'board-appeal'

def index
render json: decision_review_service
.get_notice_of_disagreement_contestable_issues(user: current_user)
.body
rescue => e
log_exception_to_personal_information_log e,
error_class:
"#{self.class.name}#index exception #{e.class} (NOD_V1)"
raise
end
end
end
end
end
Loading

0 comments on commit ad7406c

Please sign in to comment.