Repository navigation
Manual verification call flow - #306
Merged
Merged
Conversation
Verification cases with document collection, cal.com-scheduled review calls, backend review UI with four-eyes escalation, and persona capture fallthrough.
…ion-calls # Conflicts: # app/views/backend/identities/show.html.erb # config/initializers/active_record_encryption.rb
The previous dump came from the shared dev database and carried churn from other branches (dropped pg_trgm/slack_idp_groups, stray ahoy/yoti/two_factor columns, and a version pointing at a migration that doesn't exist). Rebuilt from origin/main's schema + this branch's migration.
…-escalation - replace the slack thread integration with native staff comments on cases (new verification_case_comments table, comment action + UI) - staff can open a skip-persona case for users who refuse persona: document capture happens live via getUserMedia on our page, no file picker, JPEG/PNG enforced server-side; the persona-selfie checklist item records as n/a on cases with no persona capture - BOOKING_CANCELLED now returns the case to docs_submitted and emails the user a rebook link instead of stranding them on a stale page - remove the hard auto-escalation rules; first-reviewer judgment stands, manual escalation + four-eyes stay
…ncellation email - skip-persona submissions now capture a selfie (front camera widget) alongside the document, stored as a 'selfie' document kind - the decision checklist item generalizes to 'document matches selfie (persona or live capture)'; still records n/a when a case has no selfie from either source - drop the auth-side cancellation email — cal.com already emails the attendee with a rebook path; we only reset the case state
Marking a no-show reopens the case for booking (like a cancellation) and logs call_no_show to the audit trail; un-marking is a no-op. The no-show payload carries bookingUid and no metadata, so case lookup now also matches on the stored booking uid.
…ifications/manual - /verify/manual* becomes /verifications/manual* to match the site's route nomenclature (path helpers unchanged) - both VerificationsController and Portal::VerificationsController now redirect to the open case, so 'ID Verification' in the portal lands users on their case instead of the legacy resubmission flow
Second opinions happen through case comments + a DM, not a state machine. Drops the escalated state/event, the four-eyes decide policy, escalated_to/escalation_reason on verifications, the escalate action + route + UI. Decisions now strictly require call_held. Also collapse the actions row when no action applies (stray gap between separators).
…ipient BreakGlassRecord's tracked recipient calls break_glassable.identity; VerificationCase::Document reaches its identity through the case, so delegate it.
Removes the post-decision retention stamp, the nightly purge job + cron, and the retention/purged columns. User-facing copy no longer promises deletion timelines (biometric consent + recording disclosure now describe encrypted storage with logged access; needs privacy review before prod). Account deletion via DeletionService now purges case document files — previously uncovered, and now the only deletion path for them.
…red-purge path - schema.rb rebuilt from main + migrations (main's dump was two migrations stale); CreateVerificationCases renamed to 20260806000001 to sort after main's newer migrations - main refactored DeletionService to collect/detach in-transaction and purge blobs after commit — case document coverage moved into collect_attachments/detach_attachments accordingly
Add a CASE shortcode and a home-page nav item for the verification cases queue, both gated to manual document verifiers and super admins, with request specs covering the link, the kbar entry, and the role gate.
decided not to re-verify alt-docs approvals on a timer, so remove the half-wired plumbing rather than ship an unenforced column: the verifications.expires_at column, the constant, #expired?, the backend expiry badges, and the 'in a year' user copy.
recordings aren't ingested by the app yet, so we can't vouch for how they're stored or who accesses them. keep the disclosure that calls are recorded and why; leave the biometric consent line, which the encrypted storage service and break-glass audit log actually back.
- withdraw: a terminal state reachable from any open state, so a case opened on the wrong identity or abandoned by the user can be closed without a decision. revokes the flag, invalidates the link, audited. - request redo: reviewer can send the user back to resubmit documents from docs_submitted. keeps existing evidence, clears the persona inquiry, rotates the link, emails the reviewer's message. - denied email: denials now tell the user, without relaying the internal rejection reason. - reminder: a daily good_job cron sends one fresh link to anyone still in link_sent after three days, recorded as a case event.
every fifth decision (by id) lands in a qa queue; a second verifier who didn't make the decision records agree/disagree with notes. disagreements post a case comment and every sample is an audit event. any decided case can also be second-looked voluntarily from its page.
…ansitions calls are no longer recorded. marking a call held now requires the reviewer to upload one screenshot showing the user and their document, stored as a case document behind break-glass like the rest of the evidence. the recording acknowledgement becomes a call-capture acknowledgement and the user and email copy say what actually happens. also: every state-changing backend action runs under a row lock so two reviewers racing on the same button get one success and one warning, and a qa sample refuses to overwrite one that landed first.
leowilkin
marked this pull request as ready for review
October 6, 2026 20:18
Member
Author
|
@24c02 this is out of draft and ready for a look when you have a sec. the body is up to date; the "env vars" section lists the three prod vars and the cal.com webhook that need setting up alongside the merge. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this is
A staff-initiated fallback for identity verification when Persona doesn't work out: staff open a case, the user gets a single-use link to submit documents, books a video call on our self-hosted cal.com (cal.hackclub.com), and a reviewer decides on the call. The durable outcome is a
Verification::ManualVerificationCall— the same polymorphic verification type as Persona approvals, so the rest of auth treats it identically.The flow
Staff opens a case from the identity backend page ("+ manual call case", optionally ticking skip persona). Enables a per-identity flipper flag + emails a single-use link.
User submits documents at
/verifications/manual(login + flag + one-time token). Two document tiers (government ID / alternative docs with a nudge back toward gov ID). Capture paths:User books the call — call-capture disclosure must be acknowledged (calls are not recorded; staff take one screenshot during the call), then the booking link (
CALCOM_MANUAL_VERIFICATION_BOOKING_URL+metadata[casePublicId]+ email prefill).cal.com webhooks (
/webhooks/calcom, HMAC viaX-Cal-Signature-256) drive state: created/rescheduled →call_scheduled; cancelled & no-show → back todocs_submittedso the user can rebook (cal.com owns those emails).Reviewer marks the call held (requires uploading one screenshot showing the user and their document, stored as a case document behind break-glass) → checklist + confidence + approve/deny. Decision creates the verification, closes the case, revokes the flag. Alternative-docs approvals are not time-limited (decided against a 12-month re-verification).
Off-ramps — a reviewer can request redo (user gets a fresh link and the reviewer's message, case drops back to link_sent, evidence kept) or withdraw any open case (flag revoked, link dead, audited; a new case can then be opened). Denials email the user without the internal reason. A daily good_job cron sends one reminder with a fresh link to anyone still in link_sent after three days.
Design decisions (deliberate, argued over)
call_held.DeletionService) purges case document files.verification_case_events).Env vars (undocumented elsewhere, ask Leo)
CALCOM_MANUAL_VERIFICATION_BOOKING_URL,CALCOM_WEBHOOK_SECRET,PERSONA_MANUAL_CAPTURE_TEMPLATE(+DEV_HOSTif testing webhooks through ngrok). Webhook events to subscribe: booking created / rescheduled / cancelled / no-show updated.Testing
CI doesn't run rspec — run the feature specs locally (~118 examples, all green):
Note:
.rspec's--patternloads the whole suite alongside your named files; add-P "<same files>"to isolate. ~39 persona/identity spec failures exist on main and are unrelated.Known TODOs (help welcome)
/backend/verification_cases) is only reachable via per-identity links/backend/verification_cases/qa); a second verifier records agree/disagree, disagreements post a case comment. Any decided case can be second-looked from its page.End-to-end tested against cal.hackclub.com through an ngrok tunnel (booking, reschedule, cancel, decision) on 2026-07-27.