KeySecure SP3 — cardholder onboarding
Lets a user whose Sumsub KYC is completed become a KeySecure cardholder (one per user), fill missing KYC fields in-app, track the provider review, resubmit after a rejection, and watches the merchant reserve fund. No card issuing yet.
What's in it
-
GET /keysecure/cardholder— status (NOT_STARTED,SUBMITTING,SUBMIT_FAILED,IN_REVIEW,APPROVED,REJECTED), plus the fields still missing and any blocking reason. -
POST /keysecure/cardholder— optional body{ gender, address{street, subStreet?, city, state, postCode?, country}, phone }. Missing fields are patched into the Sumsub applicant, then a Sumsub share token is created and sent tocardholder/apply.- 200 new status · 409 already in progress/approved · 422 missing/blocked (listed in the body) · 403 KYC not completed · 502 transient provider error · 503 not configured.
- New table
keysecure_cardholders(migration1789900000000), unique per user; fixedcardholder_unique_no = ahad_<userId>so the provider can never hold two cardholders for one user. - Pre-checks before apply: Latin name, ID card/passport, E.164 phone, email matching the account, English address text, Channel-01 restricted countries (13).
- Concurrency: per-user claim (
INSERT … ON CONFLICT DO NOTHING+SELECT … FOR UPDATE); verified on real Postgres (5 concurrent submits → exactly 1 apply). - Error mapping: 2001/2003/2004/outcome-unknown → reconcile via
cardholder/list; 2005–2012 →SUBMIT_FAILEDwith a fixed reason code (never the provider message); provably-not-sent failures release the slot so the user can retry at once. - Cron
KeySecureCardholderSyncSchedulerevery 5 min (Redis lock, 100 rows, compare-and-set writes, staleSUBMITTING> 30 min released). -
KeySecureReserveService+ 30-min monitor: warns when the reserve drops belowKEYSECURE_RESERVE_LOW_USD(bignumber; incomplete/unreadable data logs "balance unknown", never ok). -
SumsubShareService(separate share app token); its Sumsub error logs carry only operation + HTTP status (no PII).
Config (new env vars)
SUMSUB_SHARE_APP_TOKEN, SUMSUB_SHARE_SECRET_KEY, KEYSECURE_SUMSUB_RECIPIENT, KEYSECURE_RESERVE_LOW_USD (empty = monitor off). Without the Sumsub share vars the endpoints answer 503. Staging secrets not added yet.
Verification
-
yarn jest src/modules/keysecure src/modules/keysecure-cardholder src/modules/sumsub: 405 passed, 2 skipped (Postgres race spec, gated byRUN_DB_TESTS=1, run manually: 2/2). -
yarn buildOK; eslint clean on all touched files. - Migration up/down checked on a throwaway DB; built app booted on a DB copy (migration applied, DI ok, unauthenticated GET/POST → 401).
- Reviews: per-task Sonnet reviews, final whole-branch Opus review approved after 2 rounds.
Not in this MR / follow-ups
- Real sandbox check that KeySecure imports the fixedInfo gender/address and top-level phone/email we patch.
- Card issuing,
Card_updatehandler, webhook replay cron.