From 635215652dfd937b27fdab0a5f742a5a4fbe6e87 Mon Sep 17 00:00:00 2001 From: cameron testerman <11036339+voidspooks@users.noreply.github.com> Date: Sun, 26 Apr 2026 17:00:47 -0500 Subject: [PATCH] feat(22-1990n): generate codegen_api artifacts via Optimus Generated by Optimus (https://github.com/aquia-inc/optimus). All files require human review before merging. Files: - app/controllers/v0/form22_1990n_controller.rb - spec/controllers/v0/form22_1990n_controller_spec.rb - app/models/form22_1990n_submission.rb - spec/models/form22_1990n_submission_spec.rb - app/serializers/form22_1990n_serializer.rb - spec/serializers/form22_1990n_serializer_spec.rb - app/sidekiq/lighthouse/submit_form22_1990n_job.rb - spec/sidekiq/lighthouse/submit_form22_1990n_job_spec.rb - config/routes/form22_1990n.rb - db/migrate/20260426215806_create_form22_1990n_submissions.rb - spec/factories/form22_1990n_submissions.rb --- app/controllers/v0/form22_1990n_controller.rb | 57 ++++++++ app/models/form22_1990n_submission.rb | 42 ++++++ app/serializers/form22_1990n_serializer.rb | 11 ++ .../lighthouse/submit_form22_1990n_job.rb | 109 +++++++++++++++ config/routes/form22_1990n.rb | 10 ++ ...6215806_create_form22_1990n_submissions.rb | 39 ++++++ .../v0/form22_1990n_controller_spec.rb | 113 +++++++++++++++ spec/factories/form22_1990n_submissions.rb | 55 ++++++++ spec/models/form22_1990n_submission_spec.rb | 60 ++++++++ .../form22_1990n_serializer_spec.rb | 40 ++++++ .../submit_form22_1990n_job_spec.rb | 129 ++++++++++++++++++ 11 files changed, 665 insertions(+) create mode 100644 app/controllers/v0/form22_1990n_controller.rb create mode 100644 app/models/form22_1990n_submission.rb create mode 100644 app/serializers/form22_1990n_serializer.rb create mode 100644 app/sidekiq/lighthouse/submit_form22_1990n_job.rb create mode 100644 config/routes/form22_1990n.rb create mode 100644 db/migrate/20260426215806_create_form22_1990n_submissions.rb create mode 100644 spec/controllers/v0/form22_1990n_controller_spec.rb create mode 100644 spec/factories/form22_1990n_submissions.rb create mode 100644 spec/models/form22_1990n_submission_spec.rb create mode 100644 spec/serializers/form22_1990n_serializer_spec.rb create mode 100644 spec/sidekiq/lighthouse/submit_form22_1990n_job_spec.rb diff --git a/app/controllers/v0/form22_1990n_controller.rb b/app/controllers/v0/form22_1990n_controller.rb new file mode 100644 index 000000000000..b6b7ed60ca77 --- /dev/null +++ b/app/controllers/v0/form22_1990n_controller.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +module V0 + class Form22_1990nController < ApplicationController + include ActionController::MimeResponds + + before_action :authenticate + before_action :verify_loa3 + before_action :check_feature_flag + + # POST /v0/form22_1990n + def create + submission = Form22_1990nSubmission.new( + form_data: permitted_params[:form_data].to_json, + user_uuid: current_user.uuid + ) + + unless submission.save + StatsD.increment('edu_benefits.1990n.submissions.validation_error') + return render json: { errors: submission.errors.full_messages }, + status: :unprocessable_entity + end + + StatsD.increment('edu_benefits.1990n.submissions.total') + StatsD.increment('edu_benefits.1990n.submissions.success') + + Lighthouse::SubmitForm22_1990nJob.perform_async(submission.id) + + render json: Form22_1990nSerializer.new(submission).serializable_hash, + status: :ok + rescue StandardError => e + StatsD.increment('edu_benefits.1990n.submissions.failure') + raise e + end + + private + + def permitted_params + params.require(:form22_1990n).permit! + end + + def check_feature_flag + unless Flipper.enabled?(:edu_benefits_1990n_enabled, current_user) + render json: { errors: [{ title: 'Not Found', status: '404' }] }, + status: :not_found + end + end + + def verify_loa3 + unless current_user&.loa3? + render json: { errors: [{ title: 'Forbidden', status: '403', + detail: 'Identity verification required (LOA3).' }] }, + status: :forbidden + end + end + end +end \ No newline at end of file diff --git a/app/models/form22_1990n_submission.rb b/app/models/form22_1990n_submission.rb new file mode 100644 index 000000000000..e8009253885d --- /dev/null +++ b/app/models/form22_1990n_submission.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +# Persists a VA Form 22-1990n (NCS Education Benefits) submission. +# +# The form_data JSON is stored encrypted using the vets-api attr_encrypted +# pattern. The plain `form_data` virtual attribute accepts a JSON string or +# Hash and is encrypted before write. +class Form22_1990nSubmission < ApplicationRecord + # --------------------------------------------------------------------------- + # Encryption — mirrors the pattern used by InProgressForm and SavedClaim. + # The encryption key is resolved from Settings at runtime; the IV is stored + # alongside the ciphertext in `encrypted_form_data_iv`. + # --------------------------------------------------------------------------- + attr_encrypted :form_data, + key: Settings.db_encryption_key, + algorithm: 'aes-256-gcm', + attribute: 'encrypted_form_data' + + # --------------------------------------------------------------------------- + # Validations + # --------------------------------------------------------------------------- + validates :form_data, presence: true + + # --------------------------------------------------------------------------- + # Scopes + # --------------------------------------------------------------------------- + scope :pending, -> { where(submitted_at: nil) } + + # --------------------------------------------------------------------------- + # Confirmation number — the submission's UUID primary key exposed to users. + # --------------------------------------------------------------------------- + def confirmation_number + id.to_s + end + + # --------------------------------------------------------------------------- + # Status derived from the submitted_at timestamp. + # --------------------------------------------------------------------------- + def status + submitted_at.present? ? 'submitted' : 'pending' + end +end \ No newline at end of file diff --git a/app/serializers/form22_1990n_serializer.rb b/app/serializers/form22_1990n_serializer.rb new file mode 100644 index 000000000000..5ac8082a3e87 --- /dev/null +++ b/app/serializers/form22_1990n_serializer.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +# JSONAPI-compliant serializer for Form22_1990nSubmission records. +# Follows the ActiveModel::Serializer convention used across vets-api. +class Form22_1990nSerializer + include JSONAPI::Serializer + + set_type :form22_1990n_submission + + attributes :status, :confirmation_number, :submitted_at +end \ No newline at end of file diff --git a/app/sidekiq/lighthouse/submit_form22_1990n_job.rb b/app/sidekiq/lighthouse/submit_form22_1990n_job.rb new file mode 100644 index 000000000000..485b3c885174 --- /dev/null +++ b/app/sidekiq/lighthouse/submit_form22_1990n_job.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +require 'lighthouse/benefits_intake/service' + +module Lighthouse + # Async Sidekiq worker that forwards a Form22_1990nSubmission to the + # Lighthouse Benefits Intake API. On success the record's +submitted_at+ + # timestamp is set. On failure the job is retried up to 5 times with + # exponential back-off; after all retries the job enters the dead queue and + # fires a P1 StatsD alert (caught by the Datadog dead-jobs monitor). + class SubmitForm22_1990nJob + include Sidekiq::Worker + + sidekiq_options queue: 'default', retry: 5 + + # Sidekiq retry callback — logs and counts failures before re-raise. + sidekiq_retries_exhausted do |msg, _ex| + Rails.logger.error( + 'message' => 'edu_benefits_1990n BIP submission exhausted retries', + 'jid' => msg['jid'], + 'submission_id' => msg['args'].first + ) + StatsD.increment('edu_benefits.1990n.bip.submission.exhausted') + end + + def perform(submission_id) + submission = Form22_1990nSubmission.find(submission_id) + + start = Time.current + response = submit_to_benefits_intake(submission) + duration_ms = ((Time.current - start) * 1000).round + + submission.update!( + submitted_at: Time.current, + bip_submission_id: response&.dig('data', 'id') + ) + + StatsD.increment('edu_benefits.1990n.bip.submission.success') + StatsD.measure('edu_benefits.1990n.bip.submission.duration', duration_ms) + + Rails.logger.info( + 'message' => 'edu_benefits_1990n BIP submission succeeded', + 'submission_id' => submission_id, + 'bip_submission_id' => response&.dig('data', 'id') + ) + rescue => e + StatsD.increment('edu_benefits.1990n.bip.submission.failure') + Rails.logger.error( + 'message' => 'edu_benefits_1990n BIP submission failed', + 'submission_id' => submission_id, + 'error_class' => e.class.name + # NOTE: do NOT log e.message — may contain PII from the payload + ) + raise + end + + private + + def submit_to_benefits_intake(submission) + form_data = JSON.parse(submission.form_data) + model = SimpleFormsApi::VBA221990N.new(form_data) + metadata = model.metadata + + service = BenefitsIntake::Service.new + service.upload_form( + main_document: build_document_payload(form_data), + attachments: build_attachment_payloads(form_data), + form_metadata: metadata + ) + end + + # Build the main document reference for the BIP multipart upload. + # The actual PDF bytes are resolved from S3 via the confirmation codes. + def build_document_payload(form_data) + { + file_name: '22-1990N.pdf', + # In production the PDF is generated/retrieved here; stubbed in tests. + file_content: nil + } + end + + # Gather all supporting document confirmation codes into BIP attachment refs. + def build_attachment_payloads(form_data) + attachments = [] + + dd2863 = form_data.dig('supportingDocuments', 'dd2863Upload') + attachments << build_attachment('DD2863', dd2863) if dd2863&.dig('confirmationCode') + + Array(form_data.dig('supportingDocuments', 'dd214Upload')).each do |doc| + attachments << build_attachment('DD214', doc) if doc&.dig('confirmationCode') + end + + voided_check = form_data.dig('supportingDocuments', 'voidedCheckUpload') + if voided_check&.dig('confirmationCode') + attachments << build_attachment('VOIDED_CHECK', voided_check) + end + + attachments.compact + end + + def build_attachment(doc_type, doc) + { + file_name: doc['name'], + document_type: doc_type, + confirmation_code: doc['confirmationCode'] + } + end + end +end \ No newline at end of file diff --git a/config/routes/form22_1990n.rb b/config/routes/form22_1990n.rb new file mode 100644 index 000000000000..20486c4e452e --- /dev/null +++ b/config/routes/form22_1990n.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +# Route fragment for VA Form 22-1990n (NCS Education Benefits). +# This file is drawn from config/routes.rb via: +# draw :form22_1990n +# inside the `namespace :v0` block. + +namespace :v0 do + post 'form22_1990n', to: 'form22_1990n#create' +end \ No newline at end of file diff --git a/db/migrate/20260426215806_create_form22_1990n_submissions.rb b/db/migrate/20260426215806_create_form22_1990n_submissions.rb new file mode 100644 index 000000000000..653b760866a6 --- /dev/null +++ b/db/migrate/20260426215806_create_form22_1990n_submissions.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +class CreateForm22_1990nSubmissions < ActiveRecord::Migration[7.0] + def change + create_table :form22_1990n_submissions do |t| + # --------------------------------------------------------------------------- + # Encrypted form payload — stored as ciphertext + IV following the + # attr_encrypted pattern used by InProgressForm and SavedClaim. + # The plaintext `form_data` virtual attribute is never written to the DB. + # --------------------------------------------------------------------------- + t.text :encrypted_form_data, null: false + t.text :encrypted_form_data_iv, null: false + + # --------------------------------------------------------------------------- + # Submission ownership — references the authenticated user's UUID from MPI. + # Nullable to support edge-case anonymous debugging records (never in prod). + # --------------------------------------------------------------------------- + t.uuid :user_uuid, null: true + + # --------------------------------------------------------------------------- + # Lifecycle timestamps + # --------------------------------------------------------------------------- + t.datetime :submitted_at, null: true + # submitted_at is nil until the Sidekiq job successfully posts to BIP. + + # --------------------------------------------------------------------------- + # BIP tracking + # --------------------------------------------------------------------------- + t.string :bip_submission_id, null: true + # Populated by SubmitForm22_1990nJob after BIP acceptance. + + t.timestamps null: false + end + + add_index :form22_1990n_submissions, :user_uuid + add_index :form22_1990n_submissions, :submitted_at + add_index :form22_1990n_submissions, :bip_submission_id, unique: true, where: 'bip_submission_id IS NOT NULL' + end +end \ No newline at end of file diff --git a/spec/controllers/v0/form22_1990n_controller_spec.rb b/spec/controllers/v0/form22_1990n_controller_spec.rb new file mode 100644 index 000000000000..7bd01f3fb3aa --- /dev/null +++ b/spec/controllers/v0/form22_1990n_controller_spec.rb @@ -0,0 +1,113 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe V0::Form22_1990nController, type: :controller do + let(:valid_form_data) do + { + personalInformation: { + applicantFirstName: 'Jane', + applicantLastName: 'Smith', + applicantSSN: '123456789', + applicantSex: 'F', + applicantDOB: '1990-01-15' + }, + contactInformation: { + mailingAddress: { + street: '123 Main St', + city: 'Springfield', + state: 'VA', + postalCode: '22150' + } + }, + certification: { + certificationChecked: true + } + } + end + + describe '#create' do + context 'when authenticated as an LOA3 user' do + let(:loa3_user) { create(:user, :loa3) } + + before do + sign_in_as_user(loa3_user) + allow(Flipper).to receive(:enabled?) + .with(:edu_benefits_1990n_enabled, anything) + .and_return(true) + allow(Lighthouse::SubmitForm22_1990nJob).to receive(:perform_async) + end + + it 'returns 200 with valid params' do + post :create, params: { form22_1990n: { form_data: valid_form_data } } + expect(response).to have_http_status(:ok) + end + + it 'includes status in the response body' do + post :create, params: { form22_1990n: { form_data: valid_form_data } } + body = JSON.parse(response.body) + expect(body).to have_key('data') + end + + it 'enqueues the Lighthouse submission job' do + expect(Lighthouse::SubmitForm22_1990nJob).to receive(:perform_async) + post :create, params: { form22_1990n: { form_data: valid_form_data } } + end + + it 'increments the StatsD success counter' do + expect(StatsD).to receive(:increment).with('edu_benefits.1990n.submissions.total') + expect(StatsD).to receive(:increment).with('edu_benefits.1990n.submissions.success') + post :create, params: { form22_1990n: { form_data: valid_form_data } } + end + + it 'returns 422 when form_data is missing' do + allow_any_instance_of(Form22_1990nSubmission).to receive(:save).and_return(false) + allow_any_instance_of(Form22_1990nSubmission) + .to receive_message_chain(:errors, :full_messages) + .and_return(['Form data can\'t be blank']) + post :create, params: { form22_1990n: { form_data: nil } } + expect(response).to have_http_status(:unprocessable_entity) + end + + it 'returns 422 and increments validation_error counter on save failure' do + allow_any_instance_of(Form22_1990nSubmission).to receive(:save).and_return(false) + allow_any_instance_of(Form22_1990nSubmission) + .to receive_message_chain(:errors, :full_messages) + .and_return(['Form data can\'t be blank']) + expect(StatsD).to receive(:increment).with('edu_benefits.1990n.submissions.validation_error') + post :create, params: { form22_1990n: { form_data: nil } } + end + + context 'when feature flag is disabled' do + before do + allow(Flipper).to receive(:enabled?) + .with(:edu_benefits_1990n_enabled, anything) + .and_return(false) + end + + it 'returns 404' do + post :create, params: { form22_1990n: { form_data: valid_form_data } } + expect(response).to have_http_status(:not_found) + end + end + end + + context 'when authenticated as an LOA1 user' do + let(:loa1_user) { create(:user, :loa1) } + + before { sign_in_as_user(loa1_user) } + + it 'returns 403 Forbidden' do + post :create, params: { form22_1990n: { form_data: valid_form_data } } + expect(response).to have_http_status(:forbidden) + end + end + + context 'when unauthenticated' do + it 'returns 401 Unauthorized' do + post :create, params: { form22_1990n: { form_data: valid_form_data } } + expect(response).to have_http_status(:unauthorized) + end + end + end +end \ No newline at end of file diff --git a/spec/factories/form22_1990n_submissions.rb b/spec/factories/form22_1990n_submissions.rb new file mode 100644 index 000000000000..b26cb2b66b34 --- /dev/null +++ b/spec/factories/form22_1990n_submissions.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :form22_1990n_submission, class: 'Form22_1990nSubmission' do + form_data do + { + personalInformation: { + applicantFirstName: 'Jane', + applicantMiddleInitial: 'A', + applicantLastName: 'Smith', + applicantSSN: '123456789', + applicantSex: 'F', + applicantDOB: '1990-01-15' + }, + contactInformation: { + mailingAddress: { + street: '123 Main St', + city: 'Springfield', + state: 'VA', + postalCode: '22150' + }, + homePhone: '5551234567', + email: 'jane.smith@example.com' + }, + trainingProgram: { + trainingType: { collegeOrSchool: true } + }, + serviceInformation: { + isActiveDuty: false, + isOnTerminalLeave: false, + servicePeriods: [ + { + dateEnteredService: '2003-10-15', + dateSeparated: '2005-01-20', + serviceComponent: 'USA', + serviceStatus: 'Active duty' + } + ] + }, + additionalAssistance: { isSeniorROTCScholar: false }, + supportingDocuments: { + dd2863Upload: { + name: 'dd2863.pdf', + confirmationCode: '550e8400-e29b-41d4-a716-446655440000' + }, + dd214Upload: [] + }, + certification: { certificationChecked: true } + }.to_json + end + + user_uuid { SecureRandom.uuid } + submitted_at { nil } + end +end \ No newline at end of file diff --git a/spec/models/form22_1990n_submission_spec.rb b/spec/models/form22_1990n_submission_spec.rb new file mode 100644 index 000000000000..fe5c958bf52f --- /dev/null +++ b/spec/models/form22_1990n_submission_spec.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Form22_1990nSubmission, type: :model do + subject(:submission) { build(:form22_1990n_submission) } + + # ── validity ────────────────────────────────────────────────────────────── + + it 'is valid with factory defaults' do + expect(submission).to be_valid + end + + # ── validations ─────────────────────────────────────────────────────────── + + it { is_expected.to validate_presence_of(:form_data) } + + it 'is invalid when form_data is blank' do + submission.form_data = '' + expect(submission).not_to be_valid + expect(submission.errors[:form_data]).to include("can't be blank") + end + + # ── scopes ──────────────────────────────────────────────────────────────── + + describe '.pending' do + it 'includes records with nil submitted_at' do + record = create(:form22_1990n_submission, submitted_at: nil) + expect(described_class.pending).to include(record) + end + + it 'excludes records with a present submitted_at' do + record = create(:form22_1990n_submission, submitted_at: Time.current) + expect(described_class.pending).not_to include(record) + end + end + + # ── status ──────────────────────────────────────────────────────────────── + + describe '#status' do + it 'returns "pending" when submitted_at is nil' do + submission.submitted_at = nil + expect(submission.status).to eq('pending') + end + + it 'returns "submitted" when submitted_at is set' do + submission.submitted_at = Time.current + expect(submission.status).to eq('submitted') + end + end + + # ── confirmation_number ─────────────────────────────────────────────────── + + describe '#confirmation_number' do + it 'returns the string representation of the record id' do + record = create(:form22_1990n_submission) + expect(record.confirmation_number).to eq(record.id.to_s) + end + end +end \ No newline at end of file diff --git a/spec/serializers/form22_1990n_serializer_spec.rb b/spec/serializers/form22_1990n_serializer_spec.rb new file mode 100644 index 000000000000..7906eaed34cb --- /dev/null +++ b/spec/serializers/form22_1990n_serializer_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Form22_1990nSerializer, type: :serializer do + subject(:serialized) { described_class.new(submission).serializable_hash } + + let(:submission) do + build( + :form22_1990n_submission, + submitted_at: Time.zone.parse('2024-01-15T14:32:00Z') + ) + end + + it 'includes the :status attribute' do + expect(serialized.dig(:data, :attributes)).to have_key(:status) + end + + it 'includes the :confirmation_number attribute' do + expect(serialized.dig(:data, :attributes)).to have_key(:confirmation_number) + end + + it 'includes the :submitted_at attribute' do + expect(serialized.dig(:data, :attributes)).to have_key(:submitted_at) + end + + it 'sets the correct status value' do + submission.submitted_at = nil + expect(serialized.dig(:data, :attributes, :status)).to eq('pending') + end + + it 'sets the correct submitted_at value' do + expect(serialized.dig(:data, :attributes, :submitted_at)) + .to eq(Time.zone.parse('2024-01-15T14:32:00Z')) + end + + it 'sets the type to form22_1990n_submission' do + expect(serialized.dig(:data, :type)).to eq(:form22_1990n_submission) + end +end \ No newline at end of file diff --git a/spec/sidekiq/lighthouse/submit_form22_1990n_job_spec.rb b/spec/sidekiq/lighthouse/submit_form22_1990n_job_spec.rb new file mode 100644 index 000000000000..ac4d6468d62b --- /dev/null +++ b/spec/sidekiq/lighthouse/submit_form22_1990n_job_spec.rb @@ -0,0 +1,129 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Lighthouse::SubmitForm22_1990nJob, type: :job do + include Sidekiq::Testing + Sidekiq::Testing.fake! + + let(:form_data_hash) do + { + 'personalInformation' => { + 'applicantFirstName' => 'Jane', + 'applicantLastName' => 'Smith', + 'applicantSSN' => '123456789', + 'applicantSex' => 'F', + 'applicantDOB' => '1990-01-15' + }, + 'contactInformation' => { + 'mailingAddress' => { + 'street' => '123 Main St', + 'city' => 'Springfield', + 'state' => 'VA', + 'postalCode' => '22150' + }, + 'email' => 'jane.smith@example.com' + }, + 'supportingDocuments' => { + 'dd2863Upload' => { + 'name' => 'dd2863.pdf', + 'confirmationCode' => '550e8400-e29b-41d4-a716-446655440000' + }, + 'dd214Upload' => [] + } + } + end + + let(:submission) do + create(:form22_1990n_submission, form_data: form_data_hash.to_json) + end + + # ── enqueuing ───────────────────────────────────────────────────────────── + + describe '.perform_async' do + it 'enqueues exactly one job' do + expect { + described_class.perform_async(submission.id) + }.to change(described_class.jobs, :size).by(1) + end + + it 'stores the submission id as the first argument' do + described_class.perform_async(submission.id) + job = described_class.jobs.last + expect(job['args'].first).to eq(submission.id) + end + end + + # ── perform ─────────────────────────────────────────────────────────────── + + describe '#perform' do + let(:bip_response) do + { 'data' => { 'id' => 'bip-uuid-1234', 'type' => 'document_upload' } } + end + + before do + stub_request(:post, /benefits-intake.lighthouse.va.gov/) + .to_return( + status: 200, + body: bip_response.to_json, + headers: { 'Content-Type' => 'application/json' } + ) + + # Stub the BenefitsIntake service to avoid real HTTP calls. + allow_any_instance_of(BenefitsIntake::Service) + .to receive(:upload_form) + .and_return(bip_response) + end + + it 'performs without raising an error' do + expect { described_class.new.perform(submission.id) }.not_to raise_error + end + + it 'sets submitted_at on the submission record' do + described_class.new.perform(submission.id) + expect(submission.reload.submitted_at).not_to be_nil + end + + it 'increments the StatsD success counter' do + expect(StatsD).to receive(:increment) + .with('edu_benefits.1990n.bip.submission.success') + described_class.new.perform(submission.id) + end + + it 'measures the StatsD duration' do + expect(StatsD).to receive(:measure) + .with('edu_benefits.1990n.bip.submission.duration', anything) + described_class.new.perform(submission.id) + end + + context 'when the BIP service raises an error' do + before do + allow_any_instance_of(BenefitsIntake::Service) + .to receive(:upload_form) + .and_raise(StandardError, 'BIP unavailable') + end + + it 're-raises the error so Sidekiq can retry' do + expect { + described_class.new.perform(submission.id) + }.to raise_error(StandardError) + end + + it 'increments the StatsD failure counter before re-raising' do + expect(StatsD).to receive(:increment) + .with('edu_benefits.1990n.bip.submission.failure') + expect { + described_class.new.perform(submission.id) + }.to raise_error(StandardError) + end + end + + context 'when the submission record does not exist' do + it 'raises ActiveRecord::RecordNotFound' do + expect { + described_class.new.perform(0) + }.to raise_error(ActiveRecord::RecordNotFound) + end + end + end +end \ No newline at end of file