diff --git a/app/controllers/v0/benefits_claims_controller.rb b/app/controllers/v0/benefits_claims_controller.rb index baae7d7ca4e7..97dc4948ce82 100644 --- a/app/controllers/v0/benefits_claims_controller.rb +++ b/app/controllers/v0/benefits_claims_controller.rb @@ -10,6 +10,7 @@ require 'lighthouse/benefits_documents/update_documents_status_service' module V0 + # rubocop:disable Metrics/ClassLength class BenefitsClaimsController < ApplicationController include InboundRequestLogging include V0::Concerns::MultiProviderSupport @@ -28,6 +29,48 @@ class BenefitsClaimsController < ApplicationController FEATURE_USE_TITLE_GENERATOR_WEB = 'cst_use_claim_title_generator_web' FEATURE_MULTI_CLAIM_PROVIDER = 'cst_multi_claim_provider' + DEFAULT_UPLOAD_DESTINATION_KEY = 'benefits_claims' + IVC_CHAMPVA_UPLOAD_DESTINATION_KEY = 'ivc_champva_supporting_documents' + IVC_CHAMPVA_FINALIZE_DESTINATION_KEY = 'ivc_champva_docs_only_resubmission' + + UPLOAD_DESTINATION_KEY_BY_PROVIDER = { + 'lighthouse' => DEFAULT_UPLOAD_DESTINATION_KEY, + 'ivc_champva' => IVC_CHAMPVA_UPLOAD_DESTINATION_KEY + }.freeze + + IVC_CHAMPVA_FORM_ID_BY_CLAIM_TYPE = { + 'CHAMPVA application' => '10-10D-EXTENDED', + 'Other Health Insurance' => '10-7959C', + 'Foreign Medical Program registration' => '10-7959F-1', + 'Foreign Medical Program claim' => '10-7959F-2', + 'CHAMPVA claim' => '10-7959A' + }.freeze + + IVC_CHAMPVA_10_10D_EXTENDED_DOCUMENT_TYPE_OPTIONS = [ + 'Court ordered adoption papers', + 'Birth certificate', + 'Certificate of civil union', + 'Divorce decree', + 'Marriage certificate', + 'Front of Medicare Parts A or B card', + 'Back of Medicare Parts A or B card', + 'Front of Medicare Part C card', + 'Back of Medicare Part C card', + 'Front of Medicare Part D card', + 'Back of Medicare Part D card', + 'Front of health insurance card', + 'Back of health insurance card', + 'Other document', + 'School enrollment certification form', + 'Enrollment letter', + 'Letter from the SSA' + ].map { |option| { 'value' => option, 'label' => option } }.freeze + + IVC_CHAMPVA_DOCUMENT_TYPE_OPTIONS_BY_FORM_ID = { + '10-10D-EXTENDED' => IVC_CHAMPVA_10_10D_EXTENDED_DOCUMENT_TYPE_OPTIONS + }.freeze + + IVC_CHAMPVA_ACCEPTED_FILE_TYPES = %w[pdf jpg jpeg png].freeze def index claims = if Flipper.enabled?(FEATURE_MULTI_CLAIM_PROVIDER, @current_user) @@ -35,12 +78,14 @@ def index else service.get_claims end + champva_enhanced_flow_enabled = Flipper.enabled?(:form1010d_enhanced_flow_enabled, @current_user) check_for_birls_id check_for_file_number claims['data'].each do |claim| update_claim_type_language(claim) + add_upload_metadata(claim, champva_enhanced_flow_enabled:) end claim_ids = claims['data'].map { |claim| claim['id'] } @@ -63,7 +108,9 @@ def show # Legacy single-provider path: Apply Lighthouse-specific transforms here get_legacy_claim(params[:id]) end + champva_enhanced_flow_enabled = Flipper.enabled?(:form1010d_enhanced_flow_enabled, @current_user) update_claim_type_language(claim['data']) + add_upload_metadata(claim['data'], champva_enhanced_flow_enabled:) # Document uploads to EVSS require a birls_id; This restriction should # be removed when we move to Lighthouse Benefits Documents for document uploads @@ -180,6 +227,37 @@ def update_claim_type_language(claim) end end + def add_upload_metadata(claim, champva_enhanced_flow_enabled: false) + metadata = build_upload_metadata_for_claim(claim, champva_enhanced_flow_enabled:) + return if metadata.blank? + + claim['attributes'] ||= {} + claim['attributes']['uploadMetadata'] = metadata + end + + def build_upload_metadata_for_claim(claim, champva_enhanced_flow_enabled: false) + claim_attributes = claim['attributes'] || {} + provider = claim_attributes['provider'].presence + destination_key = UPLOAD_DESTINATION_KEY_BY_PROVIDER.fetch(provider, DEFAULT_UPLOAD_DESTINATION_KEY) + + metadata = { 'uploadDestinationKey' => destination_key } + + if destination_key == IVC_CHAMPVA_UPLOAD_DESTINATION_KEY + form_id = IVC_CHAMPVA_FORM_ID_BY_CLAIM_TYPE[claim_attributes['claimType']] + metadata['formId'] = form_id if form_id.present? + metadata['acceptedFileTypes'] = IVC_CHAMPVA_ACCEPTED_FILE_TYPES + if form_id == '10-10D-EXTENDED' && champva_enhanced_flow_enabled + metadata['finalizeDestinationKey'] = IVC_CHAMPVA_FINALIZE_DESTINATION_KEY + metadata['submissionType'] = 'existing' + end + + document_type_options = IVC_CHAMPVA_DOCUMENT_TYPE_OPTIONS_BY_FORM_ID[form_id] + metadata['documentTypeOptions'] = document_type_options if document_type_options.present? + end + + metadata + end + def add_evidence_submissions(claim, evidence_submissions) non_duplicate_submissions = filter_duplicate_evidence_submissions(evidence_submissions, claim) tracked_items = claim['attributes']['trackedItems'] @@ -328,7 +406,10 @@ def report_evidence_submission_metrics(endpoint, evidence_submissions) end def fetch_evidence_submissions(claim_ids, endpoint) - EvidenceSubmission.where(claim_id: claim_ids) + query_ids = resolve_evidence_submission_claim_ids(claim_ids) + return EvidenceSubmission.none if query_ids.empty? + + EvidenceSubmission.where(claim_id: query_ids) rescue => e ::Rails.logger.error( "BenefitsClaimsController##{endpoint} Error fetching evidence submissions", @@ -342,6 +423,17 @@ def fetch_evidence_submissions(claim_ids, endpoint) EvidenceSubmission.none end + def resolve_evidence_submission_claim_ids(claim_ids) + identifiers = Array(claim_ids).compact.map(&:to_s) + return [] if identifiers.empty? + + numeric_ids = identifiers.grep(/\A\d+\z/).map(&:to_i) + uuid_ids = identifiers.grep_v(/\A\d+\z/) + numeric_ids += IvcChampvaForm.where(form_uuid: uuid_ids).pluck(:id) if uuid_ids.any? + + numeric_ids.uniq + end + def update_evidence_submissions_for_claim(claim_id, evidence_submissions) # Get pending evidence submissions as an ActiveRecord relation # PENDING = successfully sent to Lighthouse with request_id, awaiting final status @@ -441,30 +533,60 @@ def handle_error(claim_id, response, lighthouse_document_request_ids, error_sour def add_evidence_submissions_to_claims(claims, all_evidence_submissions, endpoint) return if claims.empty? - # Group evidence submissions by claim_id for efficient lookup - evidence_submissions_by_claim = all_evidence_submissions.group_by(&:claim_id) + evidence_submissions_by_claim_id = all_evidence_submissions.group_by(&:claim_id) + ivc_form_ids_by_uuid = {} - # Add evidence submissions to each claim - claims.each do |claim| - claim_id = claim['id'].to_i - evidence_submissions = evidence_submissions_by_claim[claim_id] || [] + assign_evidence_submissions_to_claims( + claims, + evidence_submissions_by_claim_id, + ivc_form_ids_by_uuid + ) + rescue ArgumentError + ensure_claims_have_evidence_submissions(claims) + rescue => e + log_add_evidence_submissions_error(claims, endpoint, e) + end + def assign_evidence_submissions_to_claims(claims, evidence_submissions_by_claim_id, ivc_form_ids_by_uuid) + claims.each do |claim| + evidence_submissions = evidence_submissions_for_claim( + claim, + evidence_submissions_by_claim_id, + ivc_form_ids_by_uuid + ) claim['attributes']['evidenceSubmissions'] = add_evidence_submissions(claim, evidence_submissions) end - rescue => e - # Log error but don't fail the request - graceful degradation - # Frontend already handles missing evidenceSubmissions attribute + end + + def ensure_claims_have_evidence_submissions(claims) + claims.each do |claim| + claim['attributes']['evidenceSubmissions'] ||= [] + end + end + + def log_add_evidence_submissions_error(claims, endpoint, error) claim_ids = claims.map { |claim| claim['id'] } ::Rails.logger.error( "BenefitsClaimsController##{endpoint} Error adding evidence submissions", - { - claim_ids:, - error_class: e.class.name - } + { claim_ids:, error_class: error.class.name } ) end + def evidence_submissions_for_claim(claim, evidence_submissions_by_claim_id, ivc_form_ids_by_uuid) + provider = claim.dig('attributes', 'provider') + claim_id = claim['id'].to_s + return non_champva_evidence_submissions(claim_id, evidence_submissions_by_claim_id) if provider != 'ivc_champva' + + ivc_form_ids_by_uuid[claim_id] ||= IvcChampvaForm.where(form_uuid: claim_id).pluck(:id) + ivc_form_ids_by_uuid[claim_id].flat_map { |form_id| evidence_submissions_by_claim_id[form_id] || [] } + end + + def non_champva_evidence_submissions(claim_id, evidence_submissions_by_claim_id) + numeric_claim_id = Integer(claim_id, 10) + evidence_submissions_by_claim_id[numeric_claim_id] || [] + end + def recently_polled_request_ids?(claim_id, request_ids) cache_record = EvidenceSubmissionPollStore.find(claim_id.to_s) return false if cache_record.nil? @@ -500,4 +622,5 @@ def cache_polled_request_ids(claim_id, request_ids) ) end end + # rubocop:enable Metrics/ClassLength end diff --git a/app/swagger/swagger/schemas/benefits_claims.rb b/app/swagger/swagger/schemas/benefits_claims.rb index 8d253e8ada96..caeacffdaa0a 100644 --- a/app/swagger/swagger/schemas/benefits_claims.rb +++ b/app/swagger/swagger/schemas/benefits_claims.rb @@ -89,6 +89,31 @@ class BenefitsClaims key :description, 'Base claim type used for title generation' key :example, 'Compensation' end + property :uploadMetadata do + key :type, :object + key :description, 'Upload routing metadata used by clients to choose upload destination and payload' + property :uploadDestinationKey, type: :string, example: 'benefits_claims' + property :formId, type: %i[string null], example: '10-10D-EXTENDED' + property :finalizeDestinationKey, type: %i[string null], example: 'ivc_champva_docs_only_resubmission' + property :submissionType, type: %i[string null], example: 'existing' + property :acceptedFileTypes do + key :type, :array + key :description, 'Optional list of accepted file extensions for this upload destination' + items do + key :type, :string + key :example, 'pdf' + end + end + property :documentTypeOptions do + key :type, :array + key :description, 'Optional list of provider-specific document type choices for uploader dropdowns' + items do + key :type, :object + property :value, type: :string, example: 'Birth certificate' + property :label, type: :string, example: 'Birth certificate' + end + end + end property :supportingDocuments do key :type, :array diff --git a/config/benefits_claims/claim_status_meta/ivc_champva/default.json b/config/benefits_claims/claim_status_meta/ivc_champva/default.json index 9de351ca1d0c..2b8ae0286479 100644 --- a/config/benefits_claims/claim_status_meta/ivc_champva/default.json +++ b/config/benefits_claims/claim_status_meta/ivc_champva/default.json @@ -12,7 +12,7 @@ "emptyState": "You don't need to do anything right now. If there's an update, we'll mail you a letter." }, "files": { - "simpleLayout": true, + "simpleLayout": false, "headerTitle": "Application files", "description": "If you need to add or update your supporting documents, you can submit them online, by mail, or by fax. This could be your personal information, health insurance, or school status.", "sectionTitle": "Application files", diff --git a/config/features.yml b/config/features.yml index abefe57d3993..3b1ee8404654 100644 --- a/config/features.yml +++ b/config/features.yml @@ -455,6 +455,9 @@ features: champva_convert_to_pdf_on_upload: actor_type: user description: Converts supporting documents to PDF at upload time instead of final submission to improve submit latency + form1010d_enhanced_flow_enabled: + actor_type: user + description: Enables enhanced docs-only resubmission flow for CHAMPVA 10-10D-EXTENDED submissions champva_stamper_logging: actor_type: user description: Enables logging of the desired stamp text diff --git a/lib/benefits_claims/providers/ivc_champva/claim_builder.rb b/lib/benefits_claims/providers/ivc_champva/claim_builder.rb index f28aea4d0125..2c6219bb491f 100644 --- a/lib/benefits_claims/providers/ivc_champva/claim_builder.rb +++ b/lib/benefits_claims/providers/ivc_champva/claim_builder.rb @@ -15,6 +15,8 @@ module ClaimBuilder 'vha_10_10d_2027' => 'CHAMPVA application', '10-10d' => 'CHAMPVA application', '10-10d-extended' => 'CHAMPVA application', + '10-10d-extended-existing' => 'CHAMPVA application', + '10-10d-extended-enrollment' => 'CHAMPVA application', '10-7959c' => 'Other Health Insurance', '10-7959f-1' => 'Foreign Medical Program registration', '10-7959f-2' => 'Foreign Medical Program claim', @@ -23,6 +25,7 @@ module ClaimBuilder PROCESSED_STATUSES = ['processed', 'manually processed'].freeze ERROR_STATUSES = ['error', 'failed', 'rejected', 'submission failed'].freeze + INTERNAL_DOCS_ONLY_1010D_FILE_NAME_PATTERN = /_vha_10_10d(?:_supporting_doc-\d+)?\.pdf\z/i def self.build_claim_response(records, user = nil) records = Array(records) @@ -79,7 +82,7 @@ def self.claim_phase_dates_for(representative, status) end def self.build_supporting_documents(records) - records.map do |record| + records.reject { |record| internal_docs_only_artifact?(record) }.map do |record| BenefitsClaims::Responses::SupportingDocument.new( document_id: record.id.to_s, document_type_label: nil, @@ -90,6 +93,13 @@ def self.build_supporting_documents(records) end end + def self.internal_docs_only_artifact?(record) + form_number = record.form_number.to_s + file_name = record.file_name.to_s + + form_number.start_with?('10-10D-EXTENDED-') && file_name.match?(INTERNAL_DOCS_ONLY_1010D_FILE_NAME_PATTERN) + end + def self.format_date(value) value&.to_date&.iso8601 end diff --git a/lib/forms/submission_statuses/formatters/ivc_champva_formatter.rb b/lib/forms/submission_statuses/formatters/ivc_champva_formatter.rb index 42a5c8d9bbfd..1e78274ff33f 100644 --- a/lib/forms/submission_statuses/formatters/ivc_champva_formatter.rb +++ b/lib/forms/submission_statuses/formatters/ivc_champva_formatter.rb @@ -7,8 +7,11 @@ module SubmissionStatuses module Formatters class IvcChampvaFormatter < BaseFormatter FORM_TYPE_MAP = { - '10-10d-extended' => '10-10D' + '10-10d-extended' => '10-10D', + '10-10d-extended-existing' => '10-10D', + '10-10d-extended-enrollment' => '10-10D' }.freeze + DOCS_ONLY_FORM_NUMBER_PATTERN = /\A10-10d-extended-(existing|enrollment)\z/i STATUS_MAP = { # PEGA statuses @@ -43,7 +46,9 @@ def merge_record(_submission_map, _status) end def build_submissions_map(submissions) - submissions.each_with_object({}) do |submission, hash| + filtered_submissions = submissions.reject { |submission| docs_only_submission?(submission) } + + filtered_submissions.each_with_object({}) do |submission, hash| hash[submission.form_uuid.to_s] = OpenStruct.new( id: submission.form_uuid.to_s, detail: submission.case_id, @@ -57,6 +62,10 @@ def build_submissions_map(submissions) end end + def docs_only_submission?(submission) + submission.form_number.to_s.match?(DOCS_ONLY_FORM_NUMBER_PATTERN) + end + def normalize_status(submission) [submission.pega_status, submission.ves_status, submission.s3_status].each do |raw_status| next if raw_status.blank? diff --git a/modules/ivc_champva/app/controllers/ivc_champva/v1/uploads_controller.rb b/modules/ivc_champva/app/controllers/ivc_champva/v1/uploads_controller.rb index a0a74c263a43..1a9386bf2180 100644 --- a/modules/ivc_champva/app/controllers/ivc_champva/v1/uploads_controller.rb +++ b/modules/ivc_champva/app/controllers/ivc_champva/v1/uploads_controller.rb @@ -28,6 +28,8 @@ class UploadsController < ApplicationController 'an error occurred while verifying stamp:', 'unable to find file' ].freeze + DOCS_ONLY_RESUBMISSION_SUBMISSION_TYPES = %w[existing enrollment].freeze + DOCS_ONLY_RESUBMISSION_FORM_NUMBERS = %w[10-10D-EXTENDED].freeze def submit(form_data = nil) Datadog::Tracing.trace('IVC Champva Forms - Submit Form') do @@ -96,6 +98,20 @@ def submit_champva_app_merged log_error_and_respond("Error submitting merged form: #{e.message}", e) end + def submit_docs_only_resubmission + parsed_form_data = parse_docs_only_payload + ensure_docs_only_resubmission_enabled(parsed_form_data) + validate_docs_only_resubmission!(parsed_form_data) + hydrate_docs_only_resubmission_data(parsed_form_data) + + response = process_docs_only_resubmission(parsed_form_data) + render json: response.fetch(:json), status: response.fetch(:status) + rescue ArgumentError => e + handle_docs_only_resubmission_argument_error(e) + rescue => e + handle_docs_only_resubmission_unexpected_error(e) + end + ## # Handles PEGA/S3 file uploads and VES submission # @@ -104,7 +120,7 @@ def submit_champva_app_merged # # @return [Hash] response from build_json def handle_file_uploads_wrapper(form_id, parsed_form_data) - if should_process_ves?(form_id) + if should_process_ves?(form_id) && !docs_only_resubmission_flow_enabled?(parsed_form_data) handle_ves_submission(form_id, parsed_form_data) else statuses, error_messages = handle_file_uploads(form_id, parsed_form_data) @@ -443,6 +459,8 @@ def submit_supporting_documents # rubocop:disable Metrics/MethodLength attachment.save end + persist_claim_evidence_submission(attachment) + launch_background_job(attachment, params[:form_id].to_s, params['attachment_id']) if Flipper.enabled?(:champva_claims_llm_validation, @current_user) @@ -579,6 +597,216 @@ def tempfile_from_attachment(attachment, form_id) private + def parse_docs_only_payload + parsed_form_data = JSON.parse(params.to_json) + parsed_form_data['form_number'] ||= '10-10D-EXTENDED' + parsed_form_data + end + + def process_docs_only_resubmission(parsed_form_data) + form_id = form_id_for_form_number(parsed_form_data['form_number']) + Datadog::Tracing.active_trace&.set_tag('form_id', form_id) + + response = handle_file_uploads_wrapper(form_id, parsed_form_data) + mark_docs_only_evidence_submissions_received(parsed_form_data) if successful_upload_response?(response[:status]) + response + end + + def ensure_docs_only_resubmission_enabled(parsed_form_data) + return if docs_only_resubmission_flow_enabled?(parsed_form_data) + + raise ArgumentError, 'documents-only resubmission flow is not enabled for this payload' + end + + def form_id_for_form_number(form_number) + form_id = FORM_NUMBER_MAP[form_number] + return form_id if form_id.present? + + raise ArgumentError, "Unsupported form number: #{form_number}" + end + + def handle_docs_only_resubmission_argument_error(error) + message = error.message + Rails.logger.error("Validation error in CHAMPVA docs-only resubmission: #{message}") + render json: { error_message: message }, status: :unprocessable_entity + end + + def handle_docs_only_resubmission_unexpected_error(error) + message = error.message + Rails.logger.error("Docs-only resubmission error: #{message}") + Rails.logger.error(error.backtrace.join("\n")) + render json: { error_message: "Error: #{message}" }, status: :internal_server_error + end + + def persist_claim_evidence_submission(attachment) + raw_claim_id = params[:claim_id] + return if raw_claim_id.blank? + + logger = Rails.logger + claim_id = claim_id_for_evidence_submission(raw_claim_id, logger) + return if claim_id.blank? + + user_account = user_account_for_evidence_submission(logger) + return if user_account.blank? + + file_name = uploaded_file_name_for_evidence_submission(attachment, logger) + return if file_name.blank? + + create_evidence_submission_record(claim_id, user_account, file_name) + rescue ArgumentError, TypeError + # `claim_id` is optional for legacy upload paths. + nil + rescue => e + logger&.error("Failed to persist CHAMPVA evidence submission: #{e.class} #{e.message}") + nil + end + + def claim_id_for_evidence_submission(raw_claim_id, logger) + claim_id = resolve_claim_record_ids(raw_claim_id).first + return claim_id if claim_id.present? + + logger.warn('Skipping CHAMPVA evidence submission persistence: provided claim_id was unresolvable') + nil + end + + def user_account_for_evidence_submission(logger) + user_account = current_user_account_for_evidence_submission + return user_account if user_account.present? + + logger.warn('Skipping CHAMPVA evidence submission persistence: missing user_account') + nil + end + + def uploaded_file_name_for_evidence_submission(attachment, logger) + file_name = uploaded_file_name(params['file'], attachment) + return file_name if file_name.present? + + logger.warn('Skipping CHAMPVA evidence submission persistence: missing file_name') + nil + end + + def create_evidence_submission_record(claim_id, user_account, file_name) + EvidenceSubmission.create( + claim_id:, + tracked_item_id: nil, + upload_status: BenefitsDocuments::Constants::UPLOAD_STATUS[:CREATED], + user_account:, + template_metadata: evidence_submission_template_metadata(file_name).to_json + ) + end + + def evidence_submission_template_metadata(file_name) + document_type = params[:attachment_id].presence || 'Supporting document' + + { + personalisation: { + document_type:, + file_name:, + obfuscated_file_name: BenefitsDocuments::Utilities::Helpers.generate_obscured_file_name(file_name), + date_submitted: BenefitsDocuments::Utilities::Helpers.format_date_for_mailers(Time.zone.now), + date_failed: nil + } + } + end + + def successful_upload_response?(status) + status.to_i == 200 + end + + def mark_docs_only_evidence_submissions_received(parsed_form_data) + claim_ids = resolve_claim_record_ids(parsed_form_data['claim_id']) + return if claim_ids.blank? + + submitted_file_names = submitted_supporting_doc_file_names(parsed_form_data) + return if submitted_file_names.blank? + + pending_submissions = pending_evidence_submissions_for_claim_ids(claim_ids) + + updated_count = 0 + pending_submissions.each do |submission| + updated_count += 1 if mark_submission_received_if_matches(submission, submitted_file_names) + end + + Rails.logger.info( + "Marked #{updated_count} CHAMPVA evidence submission(s) as SUCCESS for claim_ids=#{claim_ids.join(',')}" + ) + end + + def submitted_supporting_doc_file_names(parsed_form_data) + Array(parsed_form_data['supporting_docs']) + .filter_map { |doc| normalize_file_name(doc['name']) } + .uniq + end + + def pending_evidence_submissions_for_claim_ids(claim_ids) + pending_statuses = [ + BenefitsDocuments::Constants::UPLOAD_STATUS[:CREATED], + BenefitsDocuments::Constants::UPLOAD_STATUS[:QUEUED], + BenefitsDocuments::Constants::UPLOAD_STATUS[:PENDING] + ] + + EvidenceSubmission.where(claim_id: claim_ids, upload_status: pending_statuses) + .where('created_at >= ?', 2.days.ago) + .order(created_at: :desc) + .limit(50) + end + + def resolve_claim_record_ids(raw_claim_id) + claim_id = raw_claim_id.to_s + return [] if claim_id.blank? + return [Integer(claim_id, 10)] if claim_id.match?(/\A\d+\z/) + + IvcChampvaForm.where(form_uuid: claim_id).order(:created_at).pluck(:id) + end + + def mark_submission_received_if_matches(submission, submitted_file_names) + file_name = submission_file_name(submission) + return false if file_name.blank? || submitted_file_names.exclude?(file_name) + + submission.update!( + upload_status: BenefitsDocuments::Constants::UPLOAD_STATUS[:SUCCESS], + acknowledgement_date: Time.current + ) + true + rescue JSON::ParserError, TypeError + false + end + + def submission_file_name(submission) + metadata = JSON.parse(submission.template_metadata) + normalize_file_name(metadata.dig('personalisation', 'file_name')) + end + + def normalize_file_name(file_name) + file_name.to_s.strip.downcase.presence + end + + def resolve_claim_record_id(raw_claim_id) + claim_id = raw_claim_id.to_s + return nil if claim_id.blank? + return Integer(claim_id, 10) if claim_id.match?(/\A\d+\z/) + + IvcChampvaForm.where(form_uuid: claim_id).order(updated_at: :desc).limit(1).pick(:id) + rescue ArgumentError, TypeError + nil + end + + def current_user_account_for_evidence_submission + return nil if @current_user&.user_account_uuid.blank? + + UserAccount.find_by(id: @current_user.user_account_uuid) + end + + def uploaded_file_name(source_file, attachment) + attachment_file = attachment.file + return source_file.original_filename if source_file.respond_to?(:original_filename) + return attachment_file.original_filename if attachment_file.respond_to?(:original_filename) + return attachment_file.metadata['filename'] if attachment_file.respond_to?(:metadata) + return File.basename(attachment_file.path) if attachment_file.respond_to?(:path) + + nil + end + def content_type_from_extension(ext) case ext.downcase when '.pdf' @@ -862,14 +1090,7 @@ def get_attachment_ids_and_form(parsed_form_data) # Optionally add a supporting document with arbitrary form-defined values. add_blank_doc_and_stamp(form, parsed_form_data) - # DataDog Tracking - form.track_user_identity - form.track_current_user_loa(@current_user) - form.track_email_usage - - if Flipper.enabled?(:champva_update_datadog_tracking, @current_user) && form.respond_to?(:track_submission) - form.track_submission(@current_user) - end + track_form_submission_metrics(form) attachment_ids = build_attachment_ids(base_form_id, parsed_form_data, applicant_rounded_number) attachment_ids = [base_form_id] if attachment_ids.empty? @@ -1026,6 +1247,10 @@ def add_blank_doc_and_stamp(form, parsed_form_data) # - generation of VES JSON files def get_file_paths_and_metadata(parsed_form_data) Datadog::Tracing.trace('IVC Champva Forms - Get File Paths and Metadata and Other Work') do + if docs_only_resubmission_flow_enabled?(parsed_form_data) + return get_docs_only_resubmission_file_paths_and_metadata(parsed_form_data) + end + attachment_ids, form = get_attachment_ids_and_form(parsed_form_data) # Use the actual form ID for PDF generation, but legacy form ID for S3/metadata @@ -1054,6 +1279,141 @@ def get_file_paths_and_metadata(parsed_form_data) end end + def docs_only_resubmission?(parsed_form_data) + return false unless DOCS_ONLY_RESUBMISSION_FORM_NUMBERS.include?(parsed_form_data['form_number'].to_s) + + submission_type = parsed_form_data['submission_type'].to_s.strip.downcase + DOCS_ONLY_RESUBMISSION_SUBMISSION_TYPES.include?(submission_type) + end + + def docs_only_resubmission_flow_enabled?(parsed_form_data) + docs_only_resubmission?(parsed_form_data) && + Flipper.enabled?(:form1010d_enhanced_flow_enabled, @current_user) + end + + def validate_docs_only_resubmission!(parsed_form_data) + if parsed_form_data['claim_id'].blank? + raise ArgumentError, 'claim_id is required for documents-only resubmission' + end + if parsed_form_data['submission_type'].blank? + raise ArgumentError, 'submission_type is required for documents-only resubmission' + end + + docs = parsed_form_data['supporting_docs'] + raise ArgumentError, 'supporting documents are required for documents-only resubmission' if docs.blank? + + docs.each_with_index do |doc, index| + validate_docs_only_supporting_doc(doc, index) + end + end + + def validate_docs_only_supporting_doc(doc, index) + raise ArgumentError, "supporting_docs[#{index}] must be an object" unless doc.respond_to?(:[]) + + file_name = doc['name'] + raise ArgumentError, "supporting_docs[#{index}] is missing name" if file_name.blank? + + confirmation_code = doc['confirmation_code'] + raise ArgumentError, "supporting_docs[#{index}] is missing confirmation_code" if confirmation_code.blank? + + return if PersistentAttachments::MilitaryRecords.exists?(guid: confirmation_code) + + raise ArgumentError, + "supporting_docs[#{index}] confirmation_code could not be resolved to an existing attachment" + end + + def hydrate_docs_only_resubmission_data(parsed_form_data) + source_form = IvcChampvaForm.where(form_uuid: parsed_form_data['claim_id'].to_s).order(updated_at: :desc).first + raise ArgumentError, 'claim_id could not be resolved to an existing CHAMPVA form' if source_form.blank? + + hydrate_primary_contact_info(parsed_form_data, source_form) + hydrate_veteran_info(parsed_form_data, source_form) + hydrate_default_applicant(parsed_form_data, source_form) + end + + def hydrate_primary_contact_info(parsed_form_data, source_form) + primary_contact_info = parsed_form_data['primary_contact_info'] ||= {} + primary_contact_info['email'] ||= source_form.email + + name = primary_contact_info['name'] ||= {} + name['first'] ||= source_form.first_name + name['last'] ||= source_form.last_name + end + + def hydrate_veteran_info(parsed_form_data, source_form) + veteran = parsed_form_data['veteran'] ||= {} + + full_name = veteran['full_name'] ||= {} + full_name['first'] ||= source_form.first_name + full_name['last'] ||= source_form.last_name + + address = veteran['address'] ||= {} + address['country'] ||= 'USA' + address['postal_code'] ||= '00000' + end + + def hydrate_default_applicant(parsed_form_data, source_form) + return if parsed_form_data['applicants'].present? + + parsed_form_data['applicants'] = [{ + 'applicant_name' => { + 'first' => source_form.first_name, + 'last' => source_form.last_name + }, + 'vet_relationship' => 'spouse' + }] + end + + def get_docs_only_resubmission_file_paths_and_metadata(parsed_form_data) + Datadog::Tracing.trace('IVC Champva Forms - Get docs-only paths and metadata') do + base_form_id = form_id_for_form_number(parsed_form_data['form_number']) + form = IvcChampva::FormVersionManager.create_form_instance(base_form_id, parsed_form_data, @current_user) + track_form_submission_metrics(form) + + attachment_ids = supporting_document_ids(parsed_form_data) + if attachment_ids.blank? + raise ArgumentError, 'supporting documents must resolve to at least one attachment id for upload' + end + + submission_type = parsed_form_data['submission_type'].to_s.upcase + raw_metadata = form.metadata.merge( + 'uuid' => parsed_form_data['claim_id'].to_s, + 'submissionType' => parsed_form_data['submission_type'].to_s, + 'docType' => "#{parsed_form_data['form_number']}-#{submission_type}" + ) + metadata = IvcChampva::MetadataValidator.validate(raw_metadata) + + file_paths = docs_only_resubmission_supporting_paths_from_form(form) + + [file_paths, metadata.merge({ 'attachment_ids' => attachment_ids })] + end + end + + def docs_only_resubmission_supporting_paths_from_form(form) + placeholder_path = IvcChampva::Attachments.get_blank_page + begin + file_paths = form.handle_attachments(placeholder_path) + file_paths.shift + + if file_paths.empty? + raise ArgumentError, 'no supporting document files could be resolved for documents-only resubmission' + end + + file_paths + ensure + FileUtils.rm_f(placeholder_path) + end + end + + def track_form_submission_metrics(form) + form.track_user_identity + form.track_current_user_loa(@current_user) + form.track_email_usage + if Flipper.enabled?(:champva_update_datadog_tracking, @current_user) && form.respond_to?(:track_submission) + form.track_submission(@current_user) + end + end + def get_form_id form_number = params[:form_number] raise 'Missing/malformed form_number in params' unless form_number diff --git a/modules/ivc_champva/config/routes.rb b/modules/ivc_champva/config/routes.rb index 8a88417a9bac..9a5dde6bb4be 100644 --- a/modules/ivc_champva/config/routes.rb +++ b/modules/ivc_champva/config/routes.rb @@ -4,6 +4,7 @@ namespace :v1, defaults: { format: 'json' } do post '/forms', to: 'uploads#submit' post '/forms/10-10d-ext', to: 'uploads#submit_champva_app_merged' + post '/forms/docs_only_resubmission', to: 'uploads#submit_docs_only_resubmission' post '/forms/submit_supporting_documents', to: 'uploads#submit_supporting_documents' post '/forms/status_updates', to: 'pega#update_status' end diff --git a/modules/ivc_champva/spec/requests/ivc_champva/v1/forms/uploads_spec.rb b/modules/ivc_champva/spec/requests/ivc_champva/v1/forms/uploads_spec.rb index 159962cd1c3d..78955173c7e1 100644 --- a/modules/ivc_champva/spec/requests/ivc_champva/v1/forms/uploads_spec.rb +++ b/modules/ivc_champva/spec/requests/ivc_champva/v1/forms/uploads_spec.rb @@ -639,6 +639,67 @@ expect(PersistentAttachment.last).to be_a(PersistentAttachments::MilitaryRecords) end end + + it 'creates an evidence submission when claim_id is provided' do + clamscan = double(safe?: true) + allow(Common::VirusScan).to receive(:scan).and_return(clamscan) + user_account = create(:user_account) + + allow_any_instance_of(IvcChampva::V1::UploadsController) + .to receive(:current_user_account_for_evidence_submission) + .and_return(user_account) + + expect do + post '/ivc_champva/v1/forms/submit_supporting_documents', + params: { form_id: '10-10D', claim_id: 12_345, file:, attachment_id: 'Birth certificate' } + end.to change(EvidenceSubmission, :count).by(1) + + submission = EvidenceSubmission.last + expect(submission.claim_id).to eq(12_345) + expect(submission.upload_status).to eq(BenefitsDocuments::Constants::UPLOAD_STATUS[:CREATED]) + expect(submission.user_account_id).to eq(user_account.id) + expect(JSON.parse(submission.template_metadata)['personalisation']).to include( + 'document_type' => 'Birth certificate', + 'file_name' => 'doctors-note.gif' + ) + end + + it 'maps UUID claim_id to the underlying CHAMPVA form record id' do + clamscan = double(safe?: true) + allow(Common::VirusScan).to receive(:scan).and_return(clamscan) + user_account = create(:user_account) + form = create(:ivc_champva_form) + + allow_any_instance_of(IvcChampva::V1::UploadsController) + .to receive(:current_user_account_for_evidence_submission) + .and_return(user_account) + + expect do + post '/ivc_champva/v1/forms/submit_supporting_documents', + params: { form_id: '10-10D', claim_id: form.form_uuid, file: } + end.to change(EvidenceSubmission, :count).by(1) + + expect(EvidenceSubmission.last.claim_id).to eq(form.id) + end + + it 'maps UUID claim_id to a stable CHAMPVA form id when multiple records share the UUID' do + clamscan = double(safe?: true) + allow(Common::VirusScan).to receive(:scan).and_return(clamscan) + user_account = create(:user_account) + first_form = create(:ivc_champva_form) + create(:ivc_champva_form, form_uuid: first_form.form_uuid) + + allow_any_instance_of(IvcChampva::V1::UploadsController) + .to receive(:current_user_account_for_evidence_submission) + .and_return(user_account) + + expect do + post '/ivc_champva/v1/forms/submit_supporting_documents', + params: { form_id: '10-10D', claim_id: first_form.form_uuid, file: } + end.to change(EvidenceSubmission, :count).by(1) + + expect(EvidenceSubmission.last.claim_id).to eq(first_form.id) + end end context 'LLM response integration' do @@ -812,6 +873,82 @@ end end + describe '#submit_docs_only_resubmission' do + let(:claim_uuid) { SecureRandom.uuid } + let!(:claim_form) do + create( + :ivc_champva_form, + form_uuid: claim_uuid, + first_name: 'Pat', + last_name: 'Veteran', + email: 'pat@example.com' + ) + end + let!(:newer_claim_form_same_uuid) do + create( + :ivc_champva_form, + form_uuid: claim_uuid, + first_name: 'Pat', + last_name: 'Veteran', + email: 'pat@example.com' + ) + end + let!(:evidence_submission) do + EvidenceSubmission.create!( + claim_id: claim_form.id, + user_account: create(:user_account), + upload_status: BenefitsDocuments::Constants::UPLOAD_STATUS[:CREATED], + template_metadata: { + personalisation: { + file_name: 'birth-certificate.png', + document_type: 'Birth certificate' + } + }.to_json + ) + end + let(:payload) do + { + form_number: '10-10D-EXTENDED', + submission_type: 'existing', + claim_id: claim_uuid, + supporting_docs: [ + { + confirmation_code: 'd1fde9a6-b48f-4763-9cd5-9f06f32a6b56', + attachment_id: 'Birth certificate', + name: 'birth-certificate.png' + } + ] + } + end + + before do + allow(PersistentAttachments::MilitaryRecords).to receive(:exists?).and_return(true) + end + + it 'accepts docs-only resubmission when the flow is enabled' do + allow(Flipper).to receive(:enabled?).and_call_original + allow(Flipper).to receive(:enabled?).with(:form1010d_enhanced_flow_enabled, anything).and_return(true) + allow_any_instance_of(IvcChampva::V1::UploadsController) + .to receive(:handle_file_uploads_wrapper) + .and_return({ json: {}, status: 200 }) + + post '/ivc_champva/v1/forms/docs_only_resubmission', params: payload, as: :json + + expect(response).to have_http_status(:ok) + expect(evidence_submission.reload.upload_status).to eq(BenefitsDocuments::Constants::UPLOAD_STATUS[:SUCCESS]) + end + + it 'returns 422 when docs-only flow is disabled' do + allow(Flipper).to receive(:enabled?).and_call_original + allow(Flipper).to receive(:enabled?).with(:form1010d_enhanced_flow_enabled, anything).and_return(false) + + post '/ivc_champva/v1/forms/docs_only_resubmission', params: payload, as: :json + + expect(response).to have_http_status(:unprocessable_entity) + expect(JSON.parse(response.body)['error_message']).to include('not enabled') + end + end + describe '#unlock_file' do let(:controller) { IvcChampva::V1::UploadsController.new } let(:file) { fixture_file_upload('locked_pdf_password_is_test.pdf') } @@ -1616,6 +1753,39 @@ end end + describe '#get_docs_only_resubmission_file_paths_and_metadata' do + let(:controller) { IvcChampva::V1::UploadsController.new } + let(:claim_uuid) { SecureRandom.uuid } + let(:parsed_form_data) do + { + 'form_number' => '10-10D-EXTENDED', + 'submission_type' => 'existing', + 'claim_id' => claim_uuid, + 'supporting_docs' => [{ 'confirmation_code' => 'abc', 'attachment_id' => 'Birth certificate' }] + } + end + let(:form_instance) { double('FormInstance', metadata: { 'uuid' => SecureRandom.uuid }) } + + before do + allow(controller).to receive(:form_id_for_form_number).with('10-10D-EXTENDED').and_return('vha_10_10d') + allow(IvcChampva::FormVersionManager).to receive(:create_form_instance).and_return(form_instance) + allow(controller).to receive(:track_form_submission_metrics) + allow(controller).to receive_messages( + supporting_document_ids: ['Birth certificate'], + docs_only_resubmission_supporting_paths_from_form: ['/tmp/supporting.pdf'] + ) + allow(IvcChampva::MetadataValidator).to receive(:validate) { |metadata| metadata } + end + + it 'reuses the original claim UUID so supporting docs append to the existing case' do + _file_paths, metadata = controller.send(:get_docs_only_resubmission_file_paths_and_metadata, parsed_form_data) + + expect(metadata['uuid']).to eq(claim_uuid) + expect(metadata['docType']).to eq('10-10D-EXTENDED-EXISTING') + expect(metadata['attachment_ids']).to eq(['Birth certificate']) + end + end + describe '#build_json' do let(:controller) { IvcChampva::V1::UploadsController.new } diff --git a/spec/controllers/v0/benefits_claims_controller_spec.rb b/spec/controllers/v0/benefits_claims_controller_spec.rb index 2868b19b5429..39f8de21f566 100644 --- a/spec/controllers/v0/benefits_claims_controller_spec.rb +++ b/spec/controllers/v0/benefits_claims_controller_spec.rb @@ -1095,7 +1095,7 @@ def get_claims allow(EvidenceSubmission).to receive(:where).and_call_original # Mock to raise error when fetching evidence submissions for this specific claim - allow(EvidenceSubmission).to receive(:where).with(claim_id: claim_id.to_s) + allow(EvidenceSubmission).to receive(:where).with(claim_id: [claim_id.to_i]) .and_raise(StandardError, 'Database connection error') end @@ -2463,4 +2463,90 @@ def get_claims end end end + + describe '#build_upload_metadata_for_claim' do + it 'returns benefits_claims destination for lighthouse claims' do + claim = { + 'attributes' => { + 'provider' => 'lighthouse', + 'claimType' => 'Compensation' + } + } + + metadata = controller.send(:build_upload_metadata_for_claim, claim) + + expect(metadata).to eq({ 'uploadDestinationKey' => 'benefits_claims' }) + end + + context 'for mapped CHAMPVA claim types' do + let(:claim) do + { + 'attributes' => { + 'provider' => 'ivc_champva', + 'claimType' => 'CHAMPVA application' + } + } + end + + let(:base_metadata) do + { + 'uploadDestinationKey' => 'ivc_champva_supporting_documents', + 'formId' => '10-10D-EXTENDED', + 'acceptedFileTypes' => %w[pdf jpg jpeg png], + 'documentTypeOptions' => [ + { 'value' => 'Court ordered adoption papers', 'label' => 'Court ordered adoption papers' }, + { 'value' => 'Birth certificate', 'label' => 'Birth certificate' }, + { 'value' => 'Certificate of civil union', 'label' => 'Certificate of civil union' }, + { 'value' => 'Divorce decree', 'label' => 'Divorce decree' }, + { 'value' => 'Marriage certificate', 'label' => 'Marriage certificate' }, + { 'value' => 'Front of Medicare Parts A or B card', 'label' => 'Front of Medicare Parts A or B card' }, + { 'value' => 'Back of Medicare Parts A or B card', 'label' => 'Back of Medicare Parts A or B card' }, + { 'value' => 'Front of Medicare Part C card', 'label' => 'Front of Medicare Part C card' }, + { 'value' => 'Back of Medicare Part C card', 'label' => 'Back of Medicare Part C card' }, + { 'value' => 'Front of Medicare Part D card', 'label' => 'Front of Medicare Part D card' }, + { 'value' => 'Back of Medicare Part D card', 'label' => 'Back of Medicare Part D card' }, + { 'value' => 'Front of health insurance card', 'label' => 'Front of health insurance card' }, + { 'value' => 'Back of health insurance card', 'label' => 'Back of health insurance card' }, + { 'value' => 'Other document', 'label' => 'Other document' }, + { 'value' => 'School enrollment certification form', 'label' => 'School enrollment certification form' }, + { 'value' => 'Enrollment letter', 'label' => 'Enrollment letter' }, + { 'value' => 'Letter from the SSA', 'label' => 'Letter from the SSA' } + ] + } + end + + it 'includes docs-only finalize metadata when enhanced flow flipper is enabled' do + metadata = controller.send( + :build_upload_metadata_for_claim, + claim, + champva_enhanced_flow_enabled: true + ) + + expect(metadata).to eq( + base_metadata.merge( + 'finalizeDestinationKey' => 'ivc_champva_docs_only_resubmission', + 'submissionType' => 'existing' + ) + ) + end + + it 'omits docs-only finalize metadata when enhanced flow flipper is disabled' do + metadata = controller.send( + :build_upload_metadata_for_claim, + claim, + champva_enhanced_flow_enabled: false + ) + + expect(metadata).to eq(base_metadata) + end + end + + it 'falls back to default destination when provider is missing' do + claim = { 'attributes' => { 'claimType' => 'Compensation' } } + + metadata = controller.send(:build_upload_metadata_for_claim, claim) + + expect(metadata).to eq({ 'uploadDestinationKey' => 'benefits_claims' }) + end + end end diff --git a/spec/lib/benefits_claims/providers/ivc_champva/claim_builder_spec.rb b/spec/lib/benefits_claims/providers/ivc_champva/claim_builder_spec.rb new file mode 100644 index 000000000000..2c50900194e3 --- /dev/null +++ b/spec/lib/benefits_claims/providers/ivc_champva/claim_builder_spec.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe BenefitsClaims::Providers::IvcChampva::ClaimBuilder do + describe '.claim_type_for' do + it 'maps docs-only resubmission 10-10d form numbers to CHAMPVA application' do + expect(described_class.claim_type_for('10-10D-EXTENDED-EXISTING')).to eq('CHAMPVA application') + expect(described_class.claim_type_for('10-10D-EXTENDED-ENROLLMENT')).to eq('CHAMPVA application') + end + end + + describe '.build_supporting_documents' do + it 'filters internal docs-only generated 10-10D files from user-facing supporting documents' do + created_at = Time.zone.parse('2026-04-21 11:30:00') + user_file_record = double( + id: 101, + form_number: '10-10D-EXTENDED-EXISTING', + file_name: 'Screenshot 2026-04-21 at 9.22.07 AM.png', + created_at: + ) + internal_main_pdf_record = double( + id: 102, + form_number: '10-10D-EXTENDED-EXISTING', + file_name: 'abc_vha_10_10d.pdf', + created_at: + ) + internal_supporting_pdf_record = double( + id: 103, + form_number: '10-10D-EXTENDED-EXISTING', + file_name: 'abc_vha_10_10d_supporting_doc-0.pdf', + created_at: + ) + + supporting_documents = described_class.build_supporting_documents( + [user_file_record, internal_main_pdf_record, internal_supporting_pdf_record] + ) + + expect(supporting_documents.map(&:original_file_name)).to eq( + ['Screenshot 2026-04-21 at 9.22.07 AM.png'] + ) + end + end +end diff --git a/spec/lib/forms/submission_statuses/formatters/ivc_champva_formatter_spec.rb b/spec/lib/forms/submission_statuses/formatters/ivc_champva_formatter_spec.rb index 9695299562ba..339b77ae6225 100644 --- a/spec/lib/forms/submission_statuses/formatters/ivc_champva_formatter_spec.rb +++ b/spec/lib/forms/submission_statuses/formatters/ivc_champva_formatter_spec.rb @@ -17,8 +17,8 @@ pega_status: 'Processed' ) - dataset = instance_double( - Forms::SubmissionStatuses::Dataset, + dataset = double( + 'Dataset', submissions?: true, submissions: [submission], intake_statuses?: false, @@ -41,8 +41,8 @@ pega_status: 'Processed' ) - dataset = instance_double( - Forms::SubmissionStatuses::Dataset, + dataset = double( + 'Dataset', submissions?: true, submissions: [submission], intake_statuses?: false, @@ -54,6 +54,27 @@ expect(result.first.form_type).to eq('10-10D') end + it 'excludes 10-10D-EXTENDED-EXISTING from card display' do + submission = create( + :ivc_champva_form, + form_uuid: SecureRandom.uuid, + form_number: '10-10D-EXTENDED-EXISTING', + pega_status: 'Submitted' + ) + + dataset = double( + 'Dataset', + submissions?: true, + submissions: [submission], + intake_statuses?: false, + intake_statuses: nil + ) + + result = formatter.format_data(dataset) + + expect(result).to be_empty + end + it 'maps PEGA Not Processed status to error (action needed)' do submission = create( :ivc_champva_form, @@ -62,8 +83,8 @@ pega_status: 'Not Processed' ) - dataset = instance_double( - Forms::SubmissionStatuses::Dataset, + dataset = double( + 'Dataset', submissions?: true, submissions: [submission], intake_statuses?: false, @@ -85,8 +106,8 @@ s3_status: 'failed' ) - dataset = instance_double( - Forms::SubmissionStatuses::Dataset, + dataset = double( + 'Dataset', submissions?: true, submissions: [submission], intake_statuses?: false, @@ -108,8 +129,8 @@ s3_status: 'Submitted' ) - dataset = instance_double( - Forms::SubmissionStatuses::Dataset, + dataset = double( + 'Dataset', submissions?: true, submissions: [submission], intake_statuses?: false, @@ -131,8 +152,8 @@ s3_status: 'queued' ) - dataset = instance_double( - Forms::SubmissionStatuses::Dataset, + dataset = double( + 'Dataset', submissions?: true, submissions: [submission], intake_statuses?: false, @@ -143,5 +164,32 @@ expect(result.first.status).to eq('pending') end + + it 'excludes docs-only supporting-document submissions from application cards' do + docs_only_submission = create( + :ivc_champva_form, + form_uuid: SecureRandom.uuid, + form_number: '10-10D-EXTENDED-EXISTING', + pega_status: 'Submitted' + ) + real_application_submission = create( + :ivc_champva_form, + form_uuid: SecureRandom.uuid, + form_number: '10-10D-EXTENDED', + pega_status: 'Submitted' + ) + + dataset = double( + 'Dataset', + submissions?: true, + submissions: [docs_only_submission, real_application_submission], + intake_statuses?: false, + intake_statuses: nil + ) + + result = formatter.format_data(dataset) + + expect(result.map(&:id)).to contain_exactly(real_application_submission.form_uuid.to_s) + end end end