Skip to content

feat(user/realunit): expose user capabilities + structured ALREADY_REGISTERED status#3733

Open
Blume1977 wants to merge 2 commits into
developfrom
feat/user-capabilities
Open

feat(user/realunit): expose user capabilities + structured ALREADY_REGISTERED status#3733
Blume1977 wants to merge 2 commits into
developfrom
feat/user-capabilities

Conversation

@Blume1977
Copy link
Copy Markdown
Collaborator

Summary

Wave 3 of the realunit-app API-as-Decision-Authority plan (DFXswiss/realunit-app:docs/api-authority-plan.md). Two additive changes that let the realunit-app stop interpreting backend state into UI affordances.

What changes

UserV2Dto.capabilities

New UserCapabilitiesDto field on the /v2/user response. Surfaces per-action flags the realunit-app cubits were re-deriving locally from KYC step status:

  • canEditName / canEditAddress: false once PersonalData is in any review or completed state (data is locked to keep client and KYC attestation aligned).
  • canEditMail / canEditPhone: false only on KYC-terminated accounts.
  • supportAvailable: requires a verified mail.

UserDtoMapper.computeCapabilities mirrors the rules the realunit-app cubits encode today (settings_user_data_page.dart:239, settings_edit_name_cubit.dart:22, settings_contact_page.dart:54-67).

RealUnitRegistrationStatus.ALREADY_REGISTERED

New enum value. completeRegistration and completeRegistrationForWalletAddress return it instead of throwing BadRequestException when the wallet is already registered for the user. The realunit-app currently catches the 400 and treats it as a success — surfacing the success as a structured status removes the "papering over an error" pattern and lets the app distinguish the merge / retry path cleanly from other 400s.

The signature-mismatch refinement from #3731 is preserved — same wallet + different signature still throws 400; same wallet + same signature is the idempotent ALREADY_REGISTERED path. This PR doesn't conflict with #3731.

What is intentionally NOT in this PR

  • requiredWorkflow on SellPaymentInfoDto was considered (audit V9) but BitBox vs software-wallet selection is unavoidably a client-side device fact — the API can't substitute for the client knowing what hardware it has. Will be addressed differently in a future iteration (e.g. by exposing supportedSignMethods rather than a single workflow choice).

Backwards compatibility

Both changes are additive:

  • Old clients ignore the new capabilities field and continue to derive editability locally.
  • ALREADY_REGISTERED is a new enum value — existing switch-fall-through clients treat it the same as FORWARDING_FAILED (no behaviour worse than a generic failure). The 400-throw path that some app builds rely on for the merge flow is gracefully replaced by a successful status (better than what the app currently swallows).

Tests

  • user-dto.mapper.spec.ts adds a mapUser: capabilities block covering all five flags across happy path, PersonalData-locked, KYC-terminated, and no-mail fixtures.

Local verification

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

Test plan (manual, DEV)

  • GET /v2/user for a clean Level-20 user → capabilities.canEditName=true.
  • GET /v2/user for a user with PersonalData in ManualReviewcapabilities.canEditName=false.
  • GET /v2/user for a no-mail user → capabilities.supportAvailable=false.
  • POST /v1/realunit/register/wallet for an already-registered wallet → 201 { \"status\": \"already_registered\" } (was 400).

Companion app PR

Will open immediately on the realunit-app side to consume capabilities + ALREADY_REGISTERED.

Blume1977 added a commit to Blume1977/realunit-app that referenced this pull request May 21, 2026
…tion status

Wave 3.2 of the API-as-Decision-Authority audit
(`docs/api-authority-plan.md`), companion to API PR DFXswiss/api#3733.

The app's settings + registration cubits used to derive UI gating from
KYC step status and silently swallow 'already registered' 400s as
success. Both signals are now first-class fields on the API response.

DTO mirrors
-----------

- `UserDto` (`/v2/user`) gains `capabilities: UserCapabilitiesDto`
  with `canEditName / canEditMail / canEditPhone / canEditAddress /
  supportAvailable`. All flags default to `false` so a pre-#3733
  backend degrades to 'no edit affordances' rather than offering an
  action the backend would reject.
- `RegistrationStatus.alreadyRegistered` mirrors the new
  `RealUnitRegistrationStatus.ALREADY_REGISTERED` enum value.

Consumers
---------

- `settings_contact_cubit` reads
  `userDto.capabilities.supportAvailable` instead of `mail != null`.
  State field renamed `emailSet` → `supportAvailable` for consistency.
- `settings_user_data_cubit` now fetches `/v2/user` alongside
  `/v2/kyc` and exposes `capabilities` on its Success state. The
  `SettingsUserDataPage` wires each row's `onEdit` to
  `canEditName / canEditPhone / canEditAddress`; rows without
  capability omit the Edit button. `_UserDataRow` drops its
  `statusLabel == null` co-gating — the badge is informational, the
  capability is the authority.
- `settings_edit_name_cubit` drops the `currentStep?.status ==
  inReview` interpretation. The upstream capability gate prevents the
  cubit from being instantiated when editing is forbidden, and the
  pending branch now only fires defensively when the session lacks a URL.
- `kyc_registration_submit_cubit` no longer treats *any*
  `ApiException` after a successful sign as
  `Success(completed)`. Backend rejection of an already-registered
  wallet is now a structured `Success(alreadyRegistered)` from the
  API, and `KycCubit.checkKyc` resolves the next step from there.
  All other ApiExceptions surface as failures as they always should.

Tests
-----

- `settings_contact_cubit_test`: assertions renamed and now exercise
  the API `supportAvailable` capability fixture rather than mail
  presence as a proxy.
- `settings_user_data_cubit_test`: every fixture now mocks
  `kycService.getUser()` (the new third call site) and seeds
  `capabilities` where relevant.
- `settings_edit_name_cubit_test`: "inReview → Pending" case
  rephrased to "no URL → Pending". The "no URL → Failure" case is
  inverted to Pending (matches the cubit's new defensive branch).
- `kyc_registration_submit_cubit_test`: the
  "swallow-ApiException-as-success" case is replaced by an explicit
  "backend returns alreadyRegistered status → Success(alreadyRegistered)"
  and a complementary "ApiException → Failure" case so the silent-mask
  pattern doesn't regress.

Verification
------------

- `flutter analyze` — clean
- `flutter test` — **1417 / 1417 passing**
…GISTERED status

Wave 3 of the realunit-app API-as-Decision-Authority plan
(`DFXswiss/realunit-app:docs/api-authority-plan.md`). Two changes that let
the realunit-app stop interpreting backend state into UI affordances:

UserV2Dto.capabilities
----------------------

New `UserCapabilitiesDto` field on the `/v2/user` response. Surfaces
per-action flags the realunit-app cubits were re-deriving locally from
KYC step status (`settings_user_data_page.dart:239`,
`settings_edit_name_cubit.dart:22`, `settings_contact_page.dart:54-67`):

- `canEditName` / `canEditAddress`: false once PersonalData is in any
  review or completed state (data is locked to keep client and KYC
  attestation aligned).
- `canEditMail` / `canEditPhone`: false only on KYC-terminated accounts.
- `supportAvailable`: requires a verified mail.

`UserDtoMapper.computeCapabilities` mirrors the rules the cubits encode
today. A separate app-side PR consumes the field and drops the local
interpretation.

RealUnitRegistrationStatus.ALREADY_REGISTERED
---------------------------------------------

New enum value. `completeRegistration` and `completeRegistrationForWalletAddress`
return it instead of throwing `BadRequestException` when the wallet is
already registered for the user. The realunit-app currently catches the
400 and treats it as a success — surfacing the success as a structured
status removes the "papering over an error" pattern and lets the app
distinguish the merge / retry path cleanly from other 400s.

Backwards compatibility
-----------------------

Both changes are additive. Old clients ignore the new `capabilities`
field and continue to derive editability locally. The `ALREADY_REGISTERED`
status is a new enum value — existing clients that switch-fall-through
will treat it the same as `FORWARDING_FAILED` (no behaviour change worse
than a generic failure).

Tests
-----

- `user-dto.mapper.spec.ts` adds a `mapUser: capabilities` block covering
  all five flags across happy path, PersonalData-locked, KYC-terminated,
  and no-mail fixtures.
- Existing tests cover the two `return RealUnitRegistrationStatus.ALREADY_REGISTERED`
  call sites by absence of any new throw (`completeRegistration` /
  `completeRegistrationForWalletAddress` did not have happy-path tests for
  the already-registered branch; PR #3731 addresses the idempotency
  semantics in the same area and adds dedicated coverage).

Local verification
------------------

- `npm run type-check` — clean
- `npm run lint` — clean
- `npm test` — **943 / 943 passing**
@Blume1977 Blume1977 force-pushed the feat/user-capabilities branch from 28c4a99 to 78e9605 Compare May 21, 2026 15:05
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 21, 2026

⚠️ Unverified Commits (2)

The following commits are not signed/verified:

  • 78e9605 feat(user/realunit): expose user capabilities + structured ALREADY_REGISTERED status (Blume1977)
  • c9cc7de 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 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