Skip to content

manual verification: review follow-ups and persona capture fix - #336

Merged
24c02 merged 5 commits into
mainfrom
fix/manual-verification-review-followups
Oct 6, 2026
Merged

24c02 merged 5 commits into
mainfrom
fix/manual-verification-review-followups

Conversation

@leowilkin

Copy link
Copy Markdown
Member

follow-ups from the code review of #306, plus the production fix for the persona capture flow.

production fix

every "scan with your camera" start was 400ing. the capture template's prefill field keys are underscored (name_first, name_last, email_address) where the kyc templates use hyphens, and persona rejects prefill for unknown keys. keys corrected, and persona api errors now include the detail field so the next one says why in sentry.

review findings

  1. denial double-notified the user. denying a case fired the shared rejected_* mailers, which carry the internal rejection reason + details and a resubmit link, on top of the reason-free case email. the user-facing mailers are suppressed for manual calls; the staff-only slack deactivation ping on a fatal reason stays.
  2. persona capture skipped consent. the camera path submitted docs without recording attestation, biometric consent or the submitted fields the direct-upload path requires. both paths now start from one form that persists them, and submit_docs! has a guard so no caller can advance without them. a webhook arriving without consent on record drops the capture. direct upload is now strict server-side about the four required fields too.
  3. deletion left case pii. submitted_fields, the persona signal snapshot, inquiry ids, access tokens, event ip/ua, free-text event data, comment bodies, qa sample notes and checklist notes all survived a privacy deletion. scrubbed in place; rows and event keys stay so the audit shape survives.
  4. link reminder locked out active users. the reminder scope rotated tokens for users who had already opened their link. consumed tokens are excluded and rechecked under lock.
  5. resend persisted the token before checking the transition. now with_lock, check first, and a plain resend keeps access_token_used_at. only request-redo re-arms the gate.
  6. break-glass audit event written before authz/save. moved into the save transaction with the persisted reason.
  7. cal.com webhooks weren't idempotent. state checked before any write, everything under lock, duplicate deliveries are quiet, reschedules mail once per actual change, stale-uid cancellations ignored. ignored deliveries leave a call_booking_ignored event.
  8. case destroy always raised. events/comments/documents leave the destroy cascade so the paranoid soft-delete works; hard delete of a case raises on purpose.
  9. tampered upload params 500'd. non-file and zero-byte values get a flash.
  10. persona photos hardcoded as jpeg. sniffed with marcel; unsupported types rejected with an audit event; a capture with zero usable files no longer advances.

behaviour changes worth a glance

  • direct upload now rejects a submission missing legal name, dob, document type or issuing authority (previously html required only).
  • a persona capture whose downloads all fail used to advance to docs submitted with zero documents. it now refuses and resets the inquiry.
  • staff will see two new event keys in case timelines: call_booking_ignored and capture_files_rejected.

testing

all touched spec files: 212 examples, 0 failures. rubocop clean on changed files.

… notes

the privacy deletion tombstoned the identity and purged blobs but left
submitted_fields, the persona signal snapshot, inquiry ids, access
tokens, event ip/user agent, free-text event data, comment bodies, qa
sample notes and checklist notes behind on verification cases. scrub
them in place (rows and event keys stay so the audit shape survives),
and include case paper trail versions and case document break-glass
records in the version cleanup.
…ul save

the document_break_glass event was written in the finder, before pundit
and before the record saved, so a blank reason or an unauthorised user
left an append-only event recording access that never happened. write
it inside the save transaction with the persisted reason.
booking fields were written before the aasm check and the scheduled
mail fired on every delivery, so retries double-mailed and a reschedule
on a frozen case mutated it before raising. check allowed states first,
do all writes under with_lock in one transaction, treat an identical
uid+start as a duplicate, and ignore cancellations for a stale uid.
ignored deliveries leave a call_booking_ignored audit event.
persona puts the generic status text in title and the actual cause in
detail; we only surfaced title, so sentry showed 'Bad request' with no
reason.
- denial no longer fires the shared rejected_* mailers, which relayed
  the internal reason and a resubmit link to the user on top of the
  reason-free case email. the staff-only slack deactivation ping on a
  fatal reason is kept.
- the persona capture path required no attestation, biometric consent
  or submitted fields. both paths now start from one form that records
  them, and submit_docs! refuses to transition without them from any
  caller. a webhook arriving without consent on record drops the
  capture and clears the inquiry.
- the link reminder scope rotated tokens for users already mid-flow,
  locking them out. consumed tokens are excluded and rechecked under
  lock.
- rotate_access_link! persisted the new token before checking the
  transition. it now runs under with_lock with the check first, and a
  plain resend keeps access_token_used_at; only request-redo re-arms
  the gate.
- events, comments and documents leave the destroy cascade, so a
  paranoid soft-delete of a case no longer trips the append-only event
  guard. hard delete of a case raises on purpose.
- non-file or zero-byte upload params get a friendly flash instead of a
  500.
- persona downloads are sniffed with marcel instead of hardcoded
  image/jpeg; unsupported files are rejected with an audit event and a
  capture with zero usable files no longer advances.
- the capture template's prefill keys are underscored, not hyphenated;
  the hyphenated keys were 400ing every capture start in production.
@24c02
24c02 merged commit f735c6d into main Oct 6, 2026
7 of 8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants