Last updated Dec 2025 – verified against production code
Portal User ─────▶ ApplicationCreator ──────┐
│ ▾
Admin ──▶ PaperApplicationService ──▶ App │ EventDeduplication
│ ▾
ProofAttachmentService ←─────┘ Audit & Notification
▲ ▲ ▲
│ │ └─ MedicalCertificationService
│ └── VoucherAssignment
└── GuardianRelationship
All flows converge on one Application record, so every downstream service (events, proofs, notifications, vouchers) works the same no matter how the app started.
| Component | Purpose | Notes |
|---|---|---|
| ApplicationCreator | Portal self-service “happy path” | Runs in DB TX; fires events & notifications |
| PaperApplicationService | Admin data-entry path | Sets Current.paper_context to bypass online-only validations |
| EventDeduplicationService | 1-min window, priority pick | Used by audit views, dashboards, certification timelines |
| NotificationService | Email / SMS / in-app / fax | Postmark, Twilio integrations |
| ProofAttachmentService | Upload / approve / reject | Unified for web, email, paper |
| MedicalCertificationService | Request & track med certs | Updates status, sends provider faxes |
| VoucherAssignment | Issue & redeem vouchers | Auto-assign on approval, vendor redemption flow |
- Auth → Dashboard → “Create Application”
- 5-step form (type, personal, income, provider, proofs).
- Autosave every 2 s; live FPL check + file validation.
ApplicationCreatorservice:- Validates data → upserts user → sets guardian link (if needed) → creates Application → attaches proofs →
AuditEventService.log+NotificationService.
- Validates data → upserts user → sets guardian link (if needed) → creates Application → attaches proofs →
- Admin → Paper Apps → New
- Dynamic form (guardian search / create, proof accept w/ or w/o file).
- Wrap all logic with:
Current.paper_context = true
begin
process_paper_application # uses PaperApplicationService
ensure
Current.reset
end- Same downstream services as portal flow → single behaviour set.
- Admin timelines, user “Activity” tab, medical cert dashboard—all pull from deduped event lists.
- Dedup key:
[fingerprint, minute_bucket]→ pick highest priority (StatusChange > Event > Notification).
service = Applications::EventDeduplicationService.new
events = service.deduplicate(raw_events)When adding a new event type, just log it—the service handles dedup for you.
| Channel | Stack | Typical Use |
|---|---|---|
| In-app | Turbo + read/unread | All status changes |
| Postmark webhooks | Proof approved / rejected | |
| SMS | Twilio | Time-sensitive alerts |
| Fax | InterFax wrapper | Provider medical cert requests |
Create once, deliver many:
NotificationService.create_and_deliver!(
type: 'application_submitted',
recipient: application.user,
notifiable: application
)Delivery metadata (bounce, spam, sms status) is stored for audit & retries.
# Upload (user or admin)
ProofAttachmentService.attach_proof(...)
# Approve
ProofReviewService.new(app, admin, params).approve_proof(:income)
# Reject
ProofReviewService.new(app, admin, params).reject_proof(:income, 'blurry')Paper context auto-approves without a file.
MedicalCertificationService.request_certification- Bumps counter, logs event, fires fax/email.
- Provider replies by fax or email →
MedicalCertificationMailboxconsumes →MedicalCertificationAttachmentService.attach_certification. - Admin can reject or adjust status via UI; auto-approve logic checks all three proof types + income threshold.
draft ─▶ in_progress ─▶ approved* ─▶ voucher_assigned ─▶ redeemed
└▶ rejected
approved can be manual (admin) or automatic (check_auto_approval_eligibility).
All transitions create ApplicationStatusChange + notification.
GuardianRelationship.create!(
guardian_user: guardian,
dependent_user: dependent,
relationship_type: 'Parent'
)
application.user = dependent
application.managing_guardian = guardian- Notifications for dependent apps go to guardian, not child.
- Dependent contact:
email_strategy&phone_strategydecide whether to clone guardian info or use unique fields.
- Auto-issued right after approval if policy met.
- Stored in
voucherstable, 1-year expiry. - Vendor portal hits
redeem_voucherwhich createsVoucherTransaction+Invoice.
- FilterService handles index search & facets.
- Dashboard metrics pulled with raw SQL for speed.
- Bulk ops (
batch_approve,batch_reject) useApplication.batch_update_status. - AuditLogBuilder + EventDeduplication = fast, deduped history for show view.
| Service | Endpoint / Job | Purpose |
|---|---|---|
| Postmark | /webhooks/email_events |
Delivery / bounce / spam |
| Twilio | queued job callbacks | SMS status |
| Medical Cert Fax | /webhooks/medical_certifications |
Provider replies |
| ActiveStorage | background scans | Virus scan, metadata |
- New proof type? Add enum, extend
ProofAttachmentService, updatedetermine_proof_type. - New notification? Add type constant + template; call
NotificationService.create_and_deliver!. - New status? Update enum,
ApplicationStatusChange, auto-approval checks, and front-end filters. - New event? Just log it with
AuditEventService.log; dedup handles rest.
- Always set
Current.paper_contextin paper tests—or validations will fail. - Use
rails_requestkeys in JS to prevent duplicate AJAX hits on forms. - Phone numbers must be normalised (
555-123-4567) before uniqueness check. - Event floods – if you log many similar events in <60 s, the dedup window ensures dashboards stay sane.
- Voucher auto-assign runs after approval callbacks—don’t forget when stubbing in specs.