Skip to content

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
developfrom
feat/kyc-decision-fields
Open

feat(kyc): expose decision fields (isRequired, canTrade, processStatus) so clients can stop re-implementing routing locally#3732
Blume1977 wants to merge 2 commits into
developfrom
feat/kyc-decision-fields

Conversation

@Blume1977
Copy link
Copy Markdown
Collaborator

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 _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 — exactly the 2026-05-21 incident report (user_data 338759: kycLevel 53 + an Outdated Ident step + a sequence-1 Ident step in InProgress → the app sends the user back to KycIdentPage even 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: boolean

Populated from requiredKycSteps(userData) at mapping time. Clients drop their own duplicate sets and iterate kycSteps.filter(s => s.isRequired) instead. The required-step list still lives in kyc.enum.ts — the DTO just exposes the per-user verdict.

UserKycDto.canTrade: boolean (/v2/user)

Authoritative trading-permission flag, computed in UserDtoMapper.computeCanTrade:

canTrade = !isKycTerminated && !isBlocked
        && kycLevel >= LEVEL_30
        && requiredKycSteps.every(rs => hasCompletedStep(rs))
        && no Ident/FinancialData step in Outdated/InProgress/InReview/OnHold/Failed

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: KycProcessStatus

High-level KYC process state for clients that don't need step granularity:

  • InProgress — at least one required step is actionable by the user
  • PendingReview — at least one required step is in backend review (InternalReview/ExternalReview/ManualReview/OnHold)
  • Completed — all required steps Completed
  • Failed — KYC terminated

Computed in KycInfoMapper.computeProcessStatus. Clients render this directly instead of inferring it from kycSteps.

How it threads through the mapper

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.

Why this is safe for old clients

All three additions are optional / nullable-shaped on the wire:

  • KycStepDto.isRequired defaults to false if the caller doesn't pass isRequired to KycStepMapper.toStep (none of the existing call sites do)
  • UserKycDto.canTrade is added as a new field; old clients ignore it
  • KycLevelDto.processStatus is added as a new field; old clients ignore it

No schema reordering, no enum reordering, no breaking changes to existing responses.

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 wires the Config singleton for sub-LEVEL_50 tradingLimit resolution. (UserDtoMapper.mapUser reaches into userData.tradingLimit, which uses Config.tradingLimits.monthlyDefaultWoKyc for sub-LEVEL_50.)
  • KycInfoMapper.computeProcessStatus is exercised indirectly through the canTrade fixtures; if reviewers want a direct unit test on the helper it can be added.

Local verification

  • npm run type-check — clean
  • npm run lint — clean
  • npm test943 / 943 passing (938 baseline kept green + 5 new tests)

Test plan

  • Manual hit against DEV: GET /v2/user returns kyc.canTrade reflecting the rule for a couple of fixture accounts (clean level-50, level-50 with Outdated ident, level-20).
  • Manual hit against DEV: GET /v2/kyc returns processStatus matching the visible state in the admin tool.
  • Smoke: GET /v2/kyc response for user_data 338759 → processStatus: 'InProgress', the InProgress Ident step has isRequired: true.

Out of scope / follow-ups

  • KycSessionDto.currentStep already carries everything the app needs for routing the actionable step; no change here.
  • The other Wave-3 fields (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).

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 21, 2026

⚠️ Unverified Commits (2)

The following commits are not signed/verified:

  • ae50abf feat(kyc): expose decision fields so clients can stop re-implementing routing locally (Blume1977)
  • b6233a9 style: prettier (Blume1977)
How to sign commits
# SSH signing (recommended)
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
git config --global commit.gpgsign true

# Re-sign last commit
git commit --amend -S --no-edit
git push --force-with-lease

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**
Blume1977 added 2 commits May 21, 2026 17:02
… 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`).
@Blume1977 Blume1977 force-pushed the feat/kyc-decision-fields branch from a69ef81 to b6233a9 Compare May 21, 2026 15:05
@Blume1977 Blume1977 marked this pull request as ready for review May 21, 2026 15:18
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.

1 participant