You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A comprehensive guide to the complete proof review lifecycle in MAT Vulcan - from submission through approval/rejection, resubmission, and integration with the medical certification workflow.
# app/controllers/constituent_portal/proofs/proofs_controller.rbdefresubmit# This action handles resubmission of proofs# ... (rate limit and authorization checks)ActiveRecord::Base.transactiondoresult=ProofAttachmentService.attach_proof({application: @application,proof_type: params[:proof_type],blob_or_file: params[:"#{params[:proof_type]}_proof_upload"],# File paramstatus: :not_reviewed,# Default status for constituent uploadsadmin: current_user,# Constituent is the actorsubmission_method: :web,metadata: {ip_address: request.remote_ip}})raise"Failed to attach proof: #{result[:error]&.message}"unlessresult[:success]# Audit event for proof submission is handled by the `track_submission` method in this controller.# Application status (e.g., needs_review_since) is updated via ProofManageable concern.# Note: `ProofAttachmentService` sets `Current.proof_attachment_service_context = true`# during its execution. This causes the `ProofManageable` concern to skip its own audit event creation,# which prevents duplicate events.endredirect_toconstituent_portal_application_path(@application),notice: 'Proof submitted successfully'end
3.2 · Paper Application Submission
# app/services/applications/paper_application_service.rbdefprocess_proof_uploadsCurrent.paper_context=true# Set paper context for the entire flowbegin# Process income proofincome_result=process_proof(:income)returnfalseunlessincome_result# Process residency proofresidency_result=process_proof(:residency)returnfalseunlessresidency_resulttrueensureCurrent.paper_context=nil# Always clear the Current attributeendendprivatedefprocess_proof(type)action=extract_proof_action(type)# 'accept' or 'reject'caseactionwhen'accept'# Calls ProofAttachmentService.attach_proof internallyprocess_accept_proof(type)when'reject'# Calls ProofAttachmentService.reject_proof_without_attachment internallyprocess_reject_proof(type)elsetrue# No action specified, proceedendend# Note on Audit Events in PaperApplicationService:# - When a proof is accepted with a file, `ProofAttachmentService` creates a `#{type}_proof_attached` audit event.# - When a proof is accepted without a file (in a paper context), `PaperApplicationService` creates a `proof_submitted` audit event.# - When a proof is rejected, `ProofAttachmentService` creates a `#{type}_proof_rejected` audit event.
3.3 · Email Submission via Action Mailbox
# app/mailboxes/proof_submission_mailbox.rbdefprocess# Create an audit record for the initial email receiptcreate_audit_record# Process each attachment in the emailmail.attachments.eachdo |attachment|
# Determine proof type based on email subject or contentproof_type=determine_proof_type(mail.subject,mail.body.decoded)# Attach the file to the application's proof using the serviceattach_proof(attachment,proof_type)end# Notify admin of new proof submission (after all attachments are processed)notify_adminendprivatedefattach_proof(attachment,proof_type)# ... (blob creation)# Use the ProofAttachmentService to consistently handle attachmentsresult=ProofAttachmentService.attach_proof({application: application,proof_type: proof_type,blob_or_file: blob,status: :not_reviewed,admin: nil,# No admin for email submissionssubmission_method: :email,metadata: {email_subject: mail.subject,email_from: mail.from.first}})raise"Failed to attach proof: #{result[:error]&.message}"unlessresult[:success]# ProofAttachmentService handles the attachment and notifications.# Note on Audit Events in ProofSubmissionMailbox:# - `create_audit_record` creates a `proof_submission_received` event.# - `ProofAttachmentService` creates a `#{proof_type}_proof_attached` event.# - `notify_admin` creates a `proof_submission_processed` event.end
4 · Review Process
4.1 · Admin Review Interface
Controller
Route
Purpose
Admin::ProofReviewsController
/admin/applications/:id/proof_reviews
Main review interface
Admin::ScannedProofsController
/admin/applications/:id/scanned_proofs
Upload scanned documents
Admin::ApplicationsController
/admin/applications
Application management
4.2 · Review Workflow
# app/controllers/admin/applications_controller.rb# This action handles updating proof status (approving/rejecting)defupdate_proof_statusadmin_user=validate_and_prepare_admin_user# Ensures current_user is an admin# Instantiate and call the ProofReviewServiceservice=ProofReviewService.new(@application,admin_user,params)result=service.call# This calls Applications::ProofReviewer internally# Handle the result from the serviceifresult.success?# ProofReviewService (and Applications::ProofReviewer) handles:# - Creating ProofReview record# - Updating application proof status fields# - Creating audit events# - Sending notifications# - Checking for auto-approvalhandle_successful_review# This method handles redirect/turbo_stream responseelse# Handle failure (e.g., validation errors from service)respond_todo |format|
format.html{render:show,status: :unprocessable_entity,alert: result.message}format.turbo_streamdoflash.now[:error]=result.messagerenderturbo_stream: turbo_stream.update('flash',partial: 'shared/flash')endendendend
4.3 · Core Review Logic
# app/services/applications/proof_reviewer.rbdefreview(proof_type:,status:,rejection_reason: nil,notes: nil)Rails.logger.info"Starting review with proof_type: #{proof_type.inspect}, status: #{status.inspect}"@proof_type_key=proof_type.to_s@status_key=status.to_sApplicationRecord.transactiondo# Create ProofReview record@proof_review=@application.proof_reviews.find_or_initialize_by(proof_type: @proof_type_key,status: @status_key)@proof_review.assign_attributes(admin: @admin,notes: notes,rejection_reason: rejection_reason)# If the record is being updated, the `on: :create` `set_reviewed_at` callback# does not run. `reviewed_at` is explicitly updated to reflect the new review action.# If it's a new record, the `on: :create` callback sets the timestamp.# `reviewed_at` is validated for presence, so it is always set before save!.if@proof_review.new_record?# The `set_reviewed_at` callback handles this via `before_validation :set_reviewed_at, on: :create`.else@proof_review.reviewed_at=Time.currentend@proof_review.save!# Update application proof status directly (bypasses callbacks)update_application_status# Explicitly purge attachment if proof was rejectedpurge_if_rejectedendtrue# Indicate successrescueStandardError=>eRails.logger.error"Proof review failed: #{e.message}"raise# Re-raise to ensure errors are visibleendprivatedefupdate_application_status# ... (validation for attachment presence if approving)# Update the specific proof status columncolumn_name="#{@proof_type_key}_proof_status"status_enum_value=Application.send(column_name.pluralize.to_s).fetch(@status_key.to_sym)@application.update_column(column_name,status_enum_value)@application.reload# Reload to get latest state for auto-approval check# Check if auto-approval is now possiblecheck_for_auto_approvalenddefcheck_for_auto_approval# Only check for auto-approval if application is not already approvedreturnif@application.status_approved?# Auto-approval requires all three: income, residency, AND medical certification approvedif@application.income_proof_status_approved? &&
@application.residency_proof_status_approved? &&
@application.medical_certification_status_approved?# Update application status to approved and create audit event@application.update_column(:status,Application.statuses[:approved])Event.create!(user: @admin,# Admin who triggered the final approval.# Note: If auto-approved via `ApplicationStatusManagement` (e.g., by a medical cert update),# the 'user' for the `application_auto_approved` event is `nil` (system).# This is the expected behavior for system-driven audit logging for auto-approval.action: 'application_auto_approved',auditable: @application,metadata: {application_id: @application.id,trigger: "proof_#{@proof_type_key}_approved"})Rails.logger.info"Application #{@application.id} auto-approved after all requirements met"endend
# app/models/concerns/application_status_management.rbafter_save:handle_status_change,if: :saved_change_to_status?after_save:auto_approve_if_eligible,if: :requirements_met_for_approval?private# Handles transitions to specific statuses that trigger automated actions.# Currently triggers the auto-request for medical certification when transitioning to 'awaiting_documents'.defhandle_status_changereturnunlessstatus_previously_changed?(to: 'awaiting_documents')handle_awaiting_documents_transitionend# Triggered when the application status transitions to 'awaiting_documents'.# Checks if income and residency proofs are approved.# If so, updates the medical certification status to 'requested' and sends an email to the medical provider.defhandle_awaiting_documents_transition# Ensure income and residency proofs are approvedreturnunlessall_proofs_approved?# Avoid re-requesting if already requestedreturnifmedical_certification_status_requested?# Update certification status and send emailwith_lockdoupdate!(medical_certification_status: :requested)MedicalProviderMailer.request_certification(self).deliver_laterendend# Checks if the application itself is eligible for auto-approval.# Eligibility requires income proof, residency proof, AND medical certification to be approved.# This method is triggered after save if any of the relevant proof statuses change.defrequirements_met_for_approval?# Only run this when relevant fields have changedreturnfalseunlesssaved_change_to_income_proof_status? ||
saved_change_to_residency_proof_status? ||
saved_change_to_medical_certification_status?# Only auto-approve applications that aren't already approvedreturnfalseifstatus_approved?# Check if all requirements are met (income, residency, and medical certification approved)all_requirements_met?enddefall_requirements_met?income_proof_status_approved? &&
residency_proof_status_approved? &&
medical_certification_status_approved?end# Auto-approves the application when all requirements are met# Uses proper Rails update mechanisms to ensure audit trails are createddefauto_approve_if_eligibleprevious_status=statusupdate_application_status_to_approvedcreate_auto_approval_audit_event(previous_status)end# Updates the application status using the model's status update method# This ensures proper status change records are createddefupdate_application_status_to_approvedupdate_status('approved',user: nil,notes: 'Auto-approved based on all requirements being met')end# Creates an audit event for the auto-approvaldefcreate_auto_approval_audit_event(previous_status)returnunlessdefined?(Event) && Event.respond_to?(:create)beginEvent.create!(user: nil,# nil user indicates system actionaction: 'application_auto_approved',metadata: {application_id: id,old_status: previous_status,new_status: status,timestamp: Time.current.iso8601,auto_approval: true})rescueStandardError=>e# Log error but don't prevent the auto-approvalRails.logger.error("Failed to create event for auto-approval: #{e.message}")endend
6 · Resubmission Process
6.1 · Portal Resubmission
# app/controllers/constituent_portal/dashboards_controller.rbdefcan_resubmit_proof?(application,proof_type,max_submissions)# Only allow resubmission for rejected proofsstatus_method="#{proof_type}_proof_status_rejected?"returnfalseunlessapplication.send(status_method)# Check if under the maximum number of allowed resubmissionssubmission_count=count_proof_submissions(application,proof_type)submission_count < max_submissionsend
6.2 · Email Resubmission
# app/mailboxes/proof_submission_mailbox.rbbefore_processing:check_max_rejectionsbefore_processing:check_rate_limitdefcheck_max_rejectionsmax_rejections=Policy.get('max_proof_rejections')returnunlessmax_rejections.present? && application.total_rejections.present?returnunlessapplication.total_rejections >= max_rejectionsbounce_with_notification(:max_rejections_reached,'Maximum number of proof submission attempts reached')end
7 · Audit Trail & Events
7.1 · Automatic Audit Creation
# app/models/concerns/proof_manageable.rbdefcreate_proof_submission_audit# Guard clause to prevent infinite recursionreturnif@creating_proof_auditreturnunlessproof_attachments_changed?# Skip if ProofAttachmentService is handling the audit (paper context or service context)# This prevents duplicate events when using the centralized service.returnifCurrent.paper_context? || Current.proof_attachment_service_context?# Set flag to prevent reentry@creating_proof_audit=truebegin# Audit each proof type if it has changedaudit_specific_proof_change('income')audit_specific_proof_change('residency')ensure# Reset the flag, even if an exception occurs@creating_proof_audit=falseendendprivatedefaudit_specific_proof_change(proof_type)returnunlessspecific_proof_changed?(proof_type)create_audit_record_for_proof(proof_type)enddefcreate_audit_record_for_proof(proof_type)attachment=public_send("#{proof_type}_proof")blob=attachment.blobactor=Current.user || userAuditEventService.log(action: "#{proof_type}_proof_submitted",# This event is suppressed when ProofAttachmentService is activeactor: actor,auditable: self,metadata: {proof_type: proof_type,blob_id: blob&.id,content_type: blob&.content_type,byte_size: blob&.byte_size,filename: blob&.filename.to_s,ip_address: Current.ip_address,user_agent: Current.user_agent})end# Note on Event Action Names:# `ProofAttachmentService` is the canonical source for audit events related to proof attachments.# It creates events with the action `#{proof_type}_proof_attached`.# The `ProofManageable` concern's `#{proof_type}_proof_submitted` events are suppressed# when `ProofAttachmentService` is active to prevent duplication and ensure consistency.
7.2 · Review Audit Events
# app/models/proof_review.rbprivatedefhandle_post_review_actions# ... (status checks and transaction)# Send appropriate notification based on statusifstatus_rejected?send_notification('proof_rejected',:proof_rejected,{proof_type: proof_type,rejection_reason: rejection_reason})elsesend_notification('proof_approved',:proof_approved,{proof_type: proof_type})endend# Creates a notification record and sends the email using the new NotificationService# Notification failures don't interrupt the proof review processdefsend_notification(action_name,_mail_method,metadata)# Log the audit event firstAuditEventService.log(action: action_name,actor: admin,auditable: application,metadata: metadata)# Then, send the notification without the audit flagNotificationService.create_and_deliver!(type: action_name,recipient: application.user,actor: admin,notifiable: application,metadata: metadata,channel: :email)rescueStandardError=>eRails.logger.error"Failed to send #{action_name} notification via NotificationService: #{e.message}"# Don't re-raise - notification errors shouldn't fail the whole operationend
8 · Medical Certification Integration
8.1 · Automatic Medical Cert Requests
# app/models/concerns/application_status_management.rbafter_save:handle_status_change,if: :saved_change_to_status?private# Handles transitions to specific statuses that trigger automated actions.# Currently triggers the auto-request for medical certification when transitioning to 'awaiting_documents'.defhandle_status_changereturnunlessstatus_previously_changed?(to: 'awaiting_documents')handle_awaiting_documents_transitionend# Triggered when the application status transitions to 'awaiting_documents'.# Checks if income and residency proofs are approved.# If so, updates the medical certification status to 'requested' and sends an email to the medical provider.defhandle_awaiting_documents_transition# Ensure income and residency proofs are approvedreturnunlessall_proofs_approved?# Avoid re-requesting if already requestedreturnifmedical_certification_status_requested?# Update certification status and send emailwith_lockdoupdate!(medical_certification_status: :requested)MedicalProviderMailer.request_certification(self).deliver_laterendend
8.2 · Medical Cert as Proof Type
# Medical certifications are treated as a special proof type# with their own status field and workflow integration# Check if medical certification is considered "complete" for application processing# This is typically checked by looking at the medical_certification_status field directly.# For example: application.medical_certification_status_received? || application.medical_certification_status_approved?# This method is used internally by ApplicationStatusManagement for auto-approval# It checks if all three required components (income, residency, medical cert) are approved# (See ApplicationStatusManagement#all_requirements_met?)defall_requirements_met?income_proof_status_approved? &&
residency_proof_status_approved? &&
medical_certification_status_approved?end# Check if medical certification is not required (i.e., not yet requested)# This is typically checked by looking at the medical_certification_status field directly.# For example: application.medical_certification_status_not_requested?
9 · Background Jobs & Monitoring
9.1 · Automated Monitoring
Job
Purpose
Schedule
ProofReviewReminderJob
Notify admins of stale reviews
Daily
ProofConsistencyCheckJob
Validate data integrity
Weekly
ProofAttachmentMetricsJob**
Monitor failure rates
Hourly
CleanupOldProofsJob
Archive old attachments
Daily
9.2 · Failure Rate Monitoring
# app/jobs/proof_attachment_metrics_job.rbSUCCESS_RATE_THRESHOLD=95.0# Alert if success rate falls below 95%MINIMUM_FAILURES_THRESHOLD=5# Only alert if we have at least 5 failuresdefperformRails.logger.info'Analyzing Proof Submission Failure Rates'# Get recent proof submission events (last 24 hours)recent_events=Event.where(action: 'proof_submitted').where('created_at > ?',24.hours.ago)total_submissions=recent_events.countfailed_submissions=recent_events.where("metadata->>'success' = ?",'false').countsuccessful_submissions=total_submissions - failed_submissions# Calculate success ratesuccess_rate=iftotal_submissions > 0(successful_submissions.to_f / total_submissions * 100).round(1)else100.0endRails.logger.info"Proof Submission Analysis (Last 24 Hours): " \
"Total: #{total_submissions}, " \
"Successful: #{successful_submissions}, " \
"Failed: #{failed_submissions}, " \
"Success Rate: #{success_rate}%"# Alert administrators if failure rate is too high and minimum failures threshold is metifsuccess_rate < SUCCESS_RATE_THRESHOLD && failed_submissions >= MINIMUM_FAILURES_THRESHOLDalert_administrators(success_rate,total_submissions,failed_submissions)endRails.logger.info'Proof submission failure rate analysis completed'end
10 · Frontend Integration
10.1 · Stimulus Controllers
Controller
Purpose
File Location
DocumentProofHandlerController
Admin proof accept/reject UI
app/javascript/controllers/users/
ProofStatusController
Show/hide sections based on status
app/javascript/controllers/reviews/
RejectionFormController
Dynamic rejection reason forms
app/javascript/controllers/forms/
10.2 · Dynamic UI Behavior
// app/javascript/controllers/reviews/proof_status_controller.jstoggle(event){// Check for both "approved" and "accepted" values to support both proofs and medical certificationsconstisApproved=event.target.value==="approved"||event.target.value==="accepted"// Use setVisible utility for consistent visibility managementthis.withTarget('uploadSection',(target)=>setVisible(target,isApproved));this.withTarget('rejectionSection',(target)=>setVisible(target,!isApproved));}
11 · Testing Patterns
11.1 · Service Testing
# Focus on transaction safety and error handlingdescribeApplications::ProofReviewerdo# Note: Notification failures do NOT roll back the ProofReview record or application status update# because NotificationService.create_and_deliver! rescues errors and ProofReview#send_notification# is called after ProofReview is saved.it'creates a ProofReview record and updates application status on success'doexpect{service.review(proof_type: 'income',status: 'approved')}.tochange(ProofReview,:count).by(1)expect(@application.reload.income_proof_status_approved?).tobetrueendit'does not roll back ProofReview on notification failure'doallow(NotificationService).toreceive(:create_and_deliver!).and_raise(StandardError)# Simulate notification failureexpect{service.review(proof_type: 'income',status: 'approved')}.tochange(ProofReview,:count).by(1)# ProofReview should still be createdexpect(@application.reload.income_proof_status_approved?).tobetrue# Application status should still be updatedendit'rolls back on critical database errors during review'doallow_any_instance_of(ProofReview).toreceive(:save!).and_raise(ActiveRecord::RecordInvalid)expect{service.review(proof_type: 'income',status: 'approved')}.toraise_error(ActiveRecord::RecordInvalid)expect(ProofReview.count).toeq(0)# Should not create a recordexpect(@application.reload.income_proof_status_not_reviewed?).tobetrue# Should not update statusendend
11.2 · Integration Testing
# Test complete workflows end-to-enddescribe'Proof Review Workflow'doit'handles complete approval process from constituent submission to admin approval'do# Setup: Create an application in a state ready for proof submissionapplication=create(:application,:in_progress,user: constituent)# Ensure medical certification is approved for auto-approval to triggerapplication.update_column(:medical_certification_status,Application.medical_certification_statuses[:approved])# Constituent submits income proof# Use the correct route and parameters for resubmissionpostresubmit_proof_document_constituent_portal_application_path(application,proof_type: 'income'),params: {income_proof_upload: fixture_file_upload('test_proof.pdf','application/pdf')}# Constituent submits residency proofpostresubmit_proof_document_constituent_portal_application_path(application,proof_type: 'residency'),params: {residency_proof_upload: fixture_file_upload('test_proof.pdf','application/pdf')}# Admin reviews and approves income proof# Use the correct route and action for admin reviewpatchupdate_proof_status_admin_application_path(application),params: {proof_type: 'income',status: 'approved'}# Admin reviews and approves residency proofpatchupdate_proof_status_admin_application_path(application),params: {proof_type: 'residency',status: 'approved'}# Verify application status is now approved (auto-approved)expect(application.reload.status_approved?).tobetrue# Verify notifications were sent (ProofReview model triggers these)expect(NotificationService).tohave_received(:create_and_deliver!).with(type: 'proof_approved',recipient: application.user,actor: anything,# Can be admin or systemnotifiable: application,metadata: hash_including(proof_type: 'income')).at_least(:once)expect(NotificationService).tohave_received(:create_and_deliver!).with(type: 'proof_approved',recipient: application.user,actor: anything,notifiable: application,metadata: hash_including(proof_type: 'residency')).at_least(:once)# Verify auto-approval eventexpect(Event.where(action: 'application_auto_approved',auditable: application)).toexistendend
12 · Common Troubleshooting
12.1 · Status Inconsistencies
Problem: Application status doesn't match proof statuses Solution: Run ProofConsistencyCheckJob or use Rails console:
# Fix inconsistent application (if proofs are approved but application status is not)# If an application's proof statuses (income, residency, medical certification) are all 'approved'# but the application status itself is not 'approved', you can trigger the auto-approval logic# by ensuring the relevant proof statuses are correctly set and then saving the application.# The `ApplicationStatusManagement#auto_approve_if_eligible` callback will then re-evaluate.# Example: If income_proof_status was manually changed in DB and auto-approval didn't triggerapp=Application.find(123)# Ensure all relevant proof statuses are correctly set to approvedapp.income_proof_status=:approvedapp.residency_proof_status=:approvedapp.medical_certification_status=:approvedapp.save!# This will trigger the `auto_approve_if_eligible` callback if conditions are met# Alternatively, an administrator can manually approve the application via the UI or console:# app.approve!(user: User.find_by(email: 'admin@example.com'))
12.2 · Missing Audit Trails
Problem: Proof submissions not creating audit events Check: ProofManageable#create_proof_submission_audit method and @creating_proof_audit flag
# app/models/concerns/proof_manageable.rbdefcreate_proof_submission_audit# Guard clause to prevent infinite recursionreturnif@creating_proof_auditreturnunlessproof_attachments_changed?# Skip if ProofAttachmentService is handling the audit (paper context)returnifCurrent.paper_context?# Set flag to prevent reentry@creating_proof_audit=truebegin# Audit each proof type if it has changedaudit_specific_proof_change('income')audit_specific_proof_change('residency')ensure# Reset the flag, even if an exception occurs@creating_proof_audit=falseendendprivatedefaudit_specific_proof_change(proof_type)returnunlessspecific_proof_changed?(proof_type)create_audit_record_for_proof(proof_type)enddefcreate_audit_record_for_proof(proof_type)attachment=public_send("#{proof_type}_proof")blob=attachment.blobactor=Current.user || userAuditEventService.log(action: "#{proof_type}_proof_submitted",actor: actor,auditable: self,metadata: {proof_type: proof_type,blob_id: blob&.id,content_type: blob&.content_type,byte_size: blob&.byte_size,filename: blob&.filename.to_s,ip_address: Current.ip_address,user_agent: Current.user_agent})end
12.3 · Email Processing Failures
Problem: Emailed proofs not being processed Debug: Check /rails/conductor/action_mailbox/inbound_emails and mailbox routing logic
13 · Future Enhancements
13.1 · Planned Improvements
DocuSeal Integration: Digital signature workflow for medical certifications
Mobile Upload Optimization: Enhanced mobile photo capture
13.2 · Technical Debt
Proof Type Enumeration: Centralize proof type definitions
Status Field Consolidation: Consider JSON column for complex statuses
Notification Template Standardization: Move all messages to NotificationComposer