feat(kyc): expose decision fields (isRequired, canTrade, processStatus) so clients can stop re-implementing routing locally#3732
Open
Blume1977 wants to merge 2 commits into
Open
Conversation
|
5 tasks
Blume1977
added a commit
to Blume1977/realunit-app
that referenced
this pull request
May 21, 2026
…g queue Companion to the seven PRs that landed today (DFXswiss#491, DFXswiss#492, DFXswiss#493, DFXswiss#494, DFXswiss#495, DFXswiss#496, DFXswiss#497 + DFXswiss/api#3732, #3733). Adds: - A 'Shipped (2026-05-21)' table mapping each pair-PR to the V-IDs it closes, with the W2 pair flagged as the closure of the 2026-05-21 ident-misroute incident. - An 'Outstanding — next phase' section grouping the remainders: P0 follow-ups (V6c/V6d/V16/V20), Wave 4 (V17/V18/V14/V19 — new backend modules), and the P1/P2 long tail (V21–V27, V29–V33). Each item carries the same V-ID anchor used elsewhere so PR reviewers can cross-reference cleanly. Does not touch V<N> classifications or counts — those stay as the reviewer set them. Pure append at the end of the file.
Blume1977
added a commit
to Blume1977/realunit-app
that referenced
this pull request
May 21, 2026
…cal sets Closes the 2026-05-21 ident-misroute report and Wave 2.2 of the API-as-Decision-Authority audit. Companion to API PR DFXswiss/api#3732 (`feat/kyc-decision-fields`). The cubit used to re-implement the backend's own routing rule locally: - `_requiredStepNames` set (duplicate of `requiredKycSteps(userData)`) - `_minLevelForActions = 30` threshold (duplicate of the level check) - `actionableStatuses` / `pendingStatuses` sets (duplicate of how the backend tags step status) - A 30-line iteration over `kycSteps` that derived `KycCompleted` / `KycPending` / `_continueKyc` from filter results. After PR #3732 the API returns those decisions directly. The cubit now renders them: - `KycLevelDto.processStatus` (new field, mirrors `KycProcessStatus`) drives the top-level switch — `Completed` → `KycCompleted`, `PendingReview` → `KycPending(currentStep)`, `InProgress` → `_continueKyc`, `Failed` → `KycFailure`. - `KycStepDto.isRequired` (new field) selects which step to surface in the pending case. - `UserKycDto.canTrade` is parsed from `/v2/user` for downstream callers (buy/sell flows can render it instead of guessing from `level >= 30`). - `KycPageManager` drops the `requiredLevel` parameter (and its router plumbing) — the cubit no longer needs a threshold; `canTrade` / `processStatus` speak directly to the buy/sell question. Backwards-compat: all three new fields default to safe values (`processStatus = inProgress`, `isRequired = false`, `canTrade = false`) when the API response omits them, so a pre-#3732 backend keeps the app functional — it just falls through to `_continueKyc` for every check, which matches the old behaviour on the unhappy path. Local session gates stay local: `_legalDisclaimerAccepted` / `_registrationSignProduced` remain per-cubit-instance security gates. They do not encode business routing — they enforce a per-session ceremony on this device before any signed call. Position in the flow unchanged. Tests: - `kyc_cubit_test.dart` rewritten to drive the cubit via API fixtures (`processStatus` + `isRequired`) instead of the old level-based setup. The previous level-based + step-iteration cases collapse to five fixtures: completed, pendingReview, inProgress (→ _continueKyc), failed, and the session-gate path. The 403/TFA_REQUIRED matrix, generation-token regression, sign-gate sequencing are preserved. Verification: - `flutter analyze` — clean - `flutter test` — **1412 / 1412 passing**
… routing locally The realunit-app's `KycCubit` (`lib/screens/kyc/cubits/kyc/kyc_cubit.dart`) was rebuilding the API's own routing rule client-side: a hardcoded `_requiredStepNames` set, an `actionableStatuses` set, and a `_minLevelForActions = 30` threshold. That setup misroutes any high-level user whose Ident step has been re-issued by `checkDfxApproval` (2026-05-21 incident — user_data 338759, kycLevel 53 + Outdated Ident + InProgress Ident@seq 1 → app stuck on KycIdentPage). The app shouldn't be deciding that. Surface the three signals it needs directly in the DTOs so it can render verbatim: - `KycStepDto.isRequired: boolean` — populated from `requiredKycSteps(userData)` at mapping time. Clients drop their own duplicate sets and iterate `kycSteps.filter(s => s.isRequired)` instead. - `UserKycDto.canTrade: boolean` (`/v2/user`) — authoritative trading-permission flag, computed from kycLevel + required-step completion + non-blocking Ident/FinancialData state. A level-50 user with an Outdated Ident now correctly reports `canTrade: false`. - `KycLevelDto.processStatus: KycProcessStatus` (`InProgress | PendingReview | Completed | Failed`) — high-level KYC process state for clients that don't need step granularity. All additions are optional / nullable-shaped on the wire: existing clients keep working, new clients consume the new fields and delete their local logic in the matching app-side PR. Mapper-side change in one spot: - `KycInfoMapper.toDto` computes `requiredStepNames` once and threads it through `KycStepMapper.toStep(..., isRequired)` + the new `computeProcessStatus` helper. `KycStepMapper.toStep` gains a third (optional) `isRequired` parameter — defaulted to `false` so the ~hundred existing call sites compile unchanged. - `UserDtoMapper.computeCanTrade` mirrors the cubit's routing semantics exactly (level ≥ LEVEL_30 + all required steps Completed + no Outdated/InProgress/OnHold Ident or FinancialData step). Comment links back to the app-side `docs/api-authority-plan.md` Wave 2. Tests: - `user-dto.mapper.spec.ts` adds a fixture-based regression block that reproduces the 2026-05-21 user_data 338759 shape (level 50 + completed Ident + outdated Ident + in-progress Ident@seq 1 → `canTrade: false`) and the surrounding cases (clean level 50, level 20, outdated FinancialData, terminated KYC). - `new ConfigService()` in `beforeAll` to wire the `Config` singleton for sub-LEVEL_50 `tradingLimit` resolution. Local verification: - `npm run type-check` — clean - `npm run lint` — clean - `npm test` — **943 / 943 passing** (5 new tests; 938 baseline kept green) Wave 2.1 of the realunit-app API-as-Decision-Authority plan (`DFXswiss/realunit-app:docs/api-authority-plan.md`).
a69ef81 to
b6233a9
Compare
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.
Summary
The realunit-app's
KycCubit(lib/screens/kyc/cubits/kyc/kyc_cubit.dart) was rebuilding the API's own routing rule client-side: a hardcoded_requiredStepNamesset, anactionableStatusesset, and a_minLevelForActions = 30threshold. That setup misroutes any high-level user whose Ident step has been re-issued bycheckDfxApproval— exactly the 2026-05-21 incident report (user_data 338759: kycLevel 53 + an Outdated Ident step + a sequence-1 Ident step inInProgress→ the app sends the user back toKycIdentPageeven though the existing trading-relevant signals are all green).The app shouldn't be deciding that. This PR surfaces the three signals it needs directly in the DTOs so it can render the verdict verbatim and stop second-guessing.
Wave 2.1 of DFXswiss/realunit-app
docs/api-authority-plan.md. The companion app-side PR consumes these fields and deletes the local logic in one go (per the pair-PR convention defined in realunit-app#491).What changes
KycStepDto.isRequired: booleanPopulated from
requiredKycSteps(userData)at mapping time. Clients drop their own duplicate sets and iteratekycSteps.filter(s => s.isRequired)instead. The required-step list still lives inkyc.enum.ts— the DTO just exposes the per-user verdict.UserKycDto.canTrade: boolean(/v2/user)Authoritative trading-permission flag, computed in
UserDtoMapper.computeCanTrade:A level-50 user with an Outdated Ident now correctly reports
canTrade: false. Comment in the mapper links back to the app-side plan.KycLevelDto.processStatus: KycProcessStatusHigh-level KYC process state for clients that don't need step granularity:
InProgress— at least one required step is actionable by the userPendingReview— at least one required step is in backend review (InternalReview/ExternalReview/ManualReview/OnHold)Completed— all required steps CompletedFailed— KYC terminatedComputed in
KycInfoMapper.computeProcessStatus. Clients render this directly instead of inferring it fromkycSteps.How it threads through the mapper
KycInfoMapper.toDtocomputesrequiredStepNamesonce and threads it throughKycStepMapper.toStep(..., isRequired)+ the newcomputeProcessStatushelper.KycStepMapper.toStepgains a third (optional)isRequiredparameter — defaulted tofalseso the ~hundred existing call sites compile unchanged.Why this is safe for old clients
All three additions are optional / nullable-shaped on the wire:
KycStepDto.isRequireddefaults tofalseif the caller doesn't passisRequiredtoKycStepMapper.toStep(none of the existing call sites do)UserKycDto.canTradeis added as a new field; old clients ignore itKycLevelDto.processStatusis added as a new field; old clients ignore itNo schema reordering, no enum reordering, no breaking changes to existing responses.
Tests
user-dto.mapper.spec.tsadds a fixture-based regression block that reproduces the 2026-05-21 user_data 338759 shape (level 50 + completed Ident + outdated Ident + in-progress Ident@seq 1 →canTrade: false) and the surrounding cases (clean level 50, level 20, outdated FinancialData, terminated KYC).new ConfigService()inbeforeAllwires theConfigsingleton for sub-LEVEL_50tradingLimitresolution. (UserDtoMapper.mapUserreaches intouserData.tradingLimit, which usesConfig.tradingLimits.monthlyDefaultWoKycfor sub-LEVEL_50.)KycInfoMapper.computeProcessStatusis exercised indirectly through thecanTradefixtures; if reviewers want a direct unit test on the helper it can be added.Local verification
npm run type-check— cleannpm run lint— cleannpm test— 943 / 943 passing (938 baseline kept green + 5 new tests)Test plan
GET /v2/userreturnskyc.canTradereflecting the rule for a couple of fixture accounts (clean level-50, level-50 with Outdated ident, level-20).GET /v2/kycreturnsprocessStatusmatching the visible state in the admin tool.GET /v2/kycresponse for user_data 338759 →processStatus: 'InProgress', the InProgress Ident step hasisRequired: true.Out of scope / follow-ups
KycSessionDto.currentStepalready carries everything the app needs for routing the actionable step; no change here.UserV2Dto.capabilities,SellPaymentInfoDto.requiredWorkflow,RealUnitRegistrationStatus.ALREADY_REGISTERED) ship in a separate PR.Companion app PR
App-side consumption + local-logic removal: opening immediately after this merges. Will reference
Closes DFXswiss/api#NNNN(this PR).