Skip to content

feat(accounts): expose upgrade verification status + fix evidence→ERPNext link mapping (ENG-608) - #515

Merged
islandbitcoin merged 3 commits into
mainfrom
feat/eng-608-idv-status
Sep 25, 2026
Merged

islandbitcoin merged 3 commits into
mainfrom
feat/eng-608-idv-status

Conversation

@islandbitcoin

@islandbitcoin islandbitcoin commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Backend half (PR 1) of ENG-608. Mobile (PR 2) consumes the new verification field.

What

1. Evidence → ERPNext link mapping fix (src/services/frappe/models/IdVerification.ts)

Verification Evidence.document_type is a Link → Identity Document Type and issuing_country a Link → Country (see verification_evidence.json in frappe-flash-admin). Phase 0 (#500) wrote the raw GraphQL input ("passport", "JM") into those fields. Frappe validates Links on insert, so any row carrying either value fails the whole POST /api/resource/ID Verification.

It has not bitten in prod because nothing sends typed evidence yet: today's only row is the legacy idDocument synthesized as ID_FRONT, which carries neither field. It would have bitten with the first mobile build that fills documentType/issuingCountry, and because the IDV insert is non-fatal by design, the symptom would have been "upgrade request created, no ID Verification, warn log only".

evidenceRowToErpnext now maps (kind, ISO-2) → the seeded registry code and ISO-2 → Frappe country name, built from admin_panel/setup.py IDENTITY_DOCUMENT_TYPES (JM, KY, TT, BB, BS, SV; 17 codes). Kinds are matched case-insensitively with punctuation collapsed (drivers_license, Driver's Licence, national id card, dui, nids, ...). Unmapped values omit the Link field (rest of the row still lands) and log a warning with the kind and country only. Codes/country names read back from ERPNext pass through, so the retention job's full-table rewrite round-trips unchanged. Full table in docs/id-verification.md §3.

2. New ERPNext reads (ErpNext.ts): getIdVerificationByUpgradeRequest(name) (list filtered by upgrade_request, modified desc, limit_page_length: 1, null when none) and getDecisionReason(code) (detail GET, null on 404). New DecisionReasonQueryError; error-map entries for it and IdVerificationQueryError.

3. AccountUpgradeRequest model gains decisionReason / reviewedAt (both exist on the AUR doctype).

4. GraphQL — AccountUpgradeRequest.verification: AccountUpgradeVerification!:

type AccountUpgradeVerification {
  status: AccountUpgradeVerificationStatus!
  reasonCode: String        # Decision Reason code, e.g. RESUBMIT_BLURRY
  reasonMessage: String     # its user_facing_message; never reviewer_note
  reviewedAt: Timestamp
}

enum AccountUpgradeVerificationStatus {
  SUBMITTED
  UNDER_REVIEW
  MORE_INFO_NEEDED
  APPROVED
  REJECTED
}

Derivation (pure, src/app/accounts/upgrade-verification-status.ts):

AUR IDV status
Approved any APPROVED
Rejected any REJECTED
Pending Resubmit requested MORE_INFO_NEEDED
Pending Ready for review / Checks unavailable UNDER_REVIEW
Pending Checks pending / none SUBMITTED

Reason prefers IDV decision_reason, then AUR's. The field never fails the query: an IDV read error derives from the AUR alone; a Decision Reason error or unknown code returns the code with a null message (both warn).

SDL and supergraph regenerated (yarn write-sdl), both carry the new type and enum.

Tests

  • IdVerification.spec.ts: registry table pinned to setup.py, 18 alias/country mappings, unmapped cases, PII-free warning, round-trip from ERPNext (+22)
  • ErpNext.idVerification.spec.ts: URL/filters/fields/order asserted for both new reads, 404→null, error wrapping (+9)
  • AccountUpgradeRequest.spec.ts: decision field hydration (+3)
  • upgrade-verification-status.spec.ts (new): every AUR×IDV cell + exhaustiveness sweep
  • get-upgrade-verification.spec.ts (new): reason preference, note never leaks, lookup failure paths, every returned status serializes through the real enum

yarn tsc-check, yarn eslint-check, yarn check:sdl, typos, prettier on touched files: green. Touched specs + test/flash/unit/graphql: 48 suites / 597 tests green.

Not in this PR

Mobile rendering of verification (PR 2). schema.graphql / supergraph.graphql were already non-prettier on main (generated), left as generated.

🤖 Generated with Claude Code

…Next link mapping (ENG-608)

Backend half of ENG-608.

Evidence → ERPNext link mapping. `Verification Evidence.document_type` and
`issuing_country` are Link fields (Identity Document Type / Country). Phase 0
wrote the raw GraphQL input ("passport", "JM"), which Frappe rejects on
insert. `evidenceRowToErpnext` now maps (kind, ISO-2) to the seeded registry
code (JM_PASSPORT, TT_DRIVERS_PERMIT, SV_DUI, ...) and ISO-2 to the Frappe
country name; unmapped values omit the Link field and log a warning without
PII. Codes and country names read back from ERPNext pass through, so the
retention job's rewrite round-trips.

Customer-facing status. `AccountUpgradeRequest.verification` returns
`{ status, reasonCode, reasonMessage, reviewedAt }` with
`AccountUpgradeVerificationStatus` = SUBMITTED | UNDER_REVIEW |
MORE_INFO_NEEDED | APPROVED | REJECTED, derived from the AUR decision plus
the latest ID Verification's review state. reasonMessage is the Decision
Reason's user_facing_message; reviewer_note is never exposed. Lookups never
fail the query.

New ErpNext reads: getIdVerificationByUpgradeRequest, getDecisionReason.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@linear

linear Bot commented Sep 24, 2026

Copy link
Copy Markdown

ENG-608

bobodread876 and others added 2 commits September 24, 2026 13:31
- evidenceRowToErpnext: pass read-back rows (rowName set) through verbatim
  instead of re-translating them through the local registry mirror; the
  retention job PUTs the full evidence table and would silently drop any
  operator-added document type / country. Spec added.
- Stop requesting reviewer_note from ERPNext and drop it from
  IdVerificationSummary; it was never read and only leak surface on the
  object feeding the public resolver.
- Remove the unused per-call Decision Reason cache parameter.
- Collapse dead `case undefined/null` labels into `default`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
toFrappeCountry / toIdentityDocumentTypeCode still claimed the verbatim
acceptance existed so read-back rows could round-trip, but read-back rows
now bypass both mappers in evidenceRowToErpnext. State the real reason:
a fresh GraphQL row may already carry a registry code / Frappe country
name.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@islandbitcoin
islandbitcoin merged commit 362c972 into main Sep 25, 2026
15 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