Skip to content

refactor(app): consume UserCapabilities + ALREADY_REGISTERED registration status#497

Merged
TaprootFreak merged 4 commits into
RealUnitCH:developfrom
Blume1977:refactor/use-user-capabilities
May 21, 2026
Merged

refactor(app): consume UserCapabilities + ALREADY_REGISTERED registration status#497
TaprootFreak merged 4 commits into
RealUnitCH:developfrom
Blume1977:refactor/use-user-capabilities

Conversation

@Blume1977
Copy link
Copy Markdown
Contributor

@Blume1977 Blume1977 commented May 21, 2026

Summary

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.

Changes

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 emailSetsupportAvailable for consistency with the API.
  • 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 "Change in review" badge stays as an informational label, 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 fires only 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.

Post-Review Fixes

Two follow-up commits address gaps surfaced during review of the original Wave 3.2 commit:

  • kyc_registration_page.dart listener generalisation (fix(kyc): KYC-Flow nach Sign auch fuer alreadyRegistered/pendingReview/forwardingFailed weiterfuehren) — the original listener only invoked markRegistrationSignProduced() + checkKyc() for RegistrationStatus.completed. With Wave 3.2's cubit emitting Success for every backend status the sign produces (completed / pendingReview / forwardingFailed / alreadyRegistered), branching by status caused the alreadyRegistered BitBox-sign path to hang on the registration page. The listener now lifts the sign gate and triggers checkKyc() on every Success; forwardingFailed additionally surfaces an informative registrationForwardingFailed SnackBar (new i18n key in DE + EN). Three new testWidgets cover the previously-uncovered statuses.
  • settings_edit_address_cubit migration (refactor(settings): edit-address-cubit analog zu edit-name auf canEditAddress-Gating umstellen) — V15 was only half-closed: SettingsUserDataPage already gates the Address-Edit button via capabilities.canEditAddress, but the address cubit still carried the old currentStep?.status == inReview branch + url == null throw. Cubit now mirrors settings_edit_name_cubit: no inReview interpretation, url == null resolves to SettingsEditAddressPending (defensive race branch) instead of throwing. Tests rephrased analogous to the name-cubit pass.

Audit alignment

docs/api-authority-audit.md V46 (registration-personal-step user-type dropdown) was previously marked "Closed by W3.1 / W3.2 (capabilities)" but is not in this PR's diff. The dropdown reads from a local RegistrationUserType.values enum, not from UserCapabilitiesDto, so closing V46 requires a separate API change (availableUserTypes capability or registration-endpoint extension). Re-marked as Deferred to a follow-up wave for accuracy.

Backwards compatibility

  • New capabilities field defaults to false everywhere — old backends silently disable Edit and Support affordances rather than offering unbacked actions.
  • RegistrationStatus.alreadyRegistered is a new enum value the old backend never sends — old client code paths that fall through switch would have hit the unhappy path the same way.

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 / settings_edit_address_cubit_test: "inReview → Pending" case rephrased to "no URL → Pending". The "no URL → Failure" case is inverted to Pending (matches the cubits' 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.
  • kyc_registration_page_test: three new testWidgets cover alreadyRegistered / pendingReview / forwardingFailed; the existing completed case additionally verifies markRegistrationSignProduced().

Verification

  • Rebased onto current develop (was rebaseable: false; rebase landed cleanly — no conflicts in buy/sell cubits, workflows, or anywhere else).
  • flutter analyze — clean
  • flutter test1438 / 1438 passing

Closes (audit, P0/P1)

  • V6 (registration-submit treats backend rejection as success)
  • V15 (settings edit hidden by KYC status interpretation — name + address)
  • V16 (support link hidden by local email check)

Tracked in docs/api-authority-audit.md.

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 (RealUnitCH#491, RealUnitCH#492, RealUnitCH#493, RealUnitCH#494,
RealUnitCH#495, RealUnitCH#496, RealUnitCH#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 Blume1977 force-pushed the refactor/use-user-capabilities branch from 995e17d to 7edd48f Compare May 21, 2026 14:58
@Blume1977 Blume1977 marked this pull request as ready for review May 21, 2026 15:18
@TaprootFreak
Copy link
Copy Markdown
Contributor

Review-Findings (Wave 3 — UserCapabilities + ALREADY_REGISTERED)

Blocker

1. Listener im KycRegistrationPage behandelt nur RegistrationStatus.completedalreadyRegistered (und pendingReview / forwardingFailed) verpuffen still
lib/screens/kyc/steps/registration/kyc_registration_page.dart:106-112 — Der Listener prüft ausschliesslich state.status == RegistrationStatus.completed und ruft nur dort markRegistrationSignProduced() + checkKyc(). Vor diesem PR hat der Cubit jede ApiException nach Sign als Success(completed) emittiert, also lief der Merge-/Already-Registered-Path durch genau diesen Branch. Mit Wave 3.2 emittiert der Cubit jetzt sauber Success(alreadyRegistered) — der Page-Listener tut damit aber nichts: kein markRegistrationSignProduced, kein checkKyc, kein Navigationswechsel. Der User sieht nach erfolgreichem EIP-712-Sign einen leeren Erfolg, der KYC-Flow hängt, und beim nächsten manuellen checkKyc schlägt das Sign-Gate fehl, weil _registrationSignProduced nie gesetzt wurde (lib/screens/kyc/cubits/kyc/kyc_cubit.dart:174-176).

Reproduktion: Wallet schon registriert → BitBox-Sign abschliessen → API liefert {status: "already_registered"} → Cubit emittiert Success(alreadyRegistered) → Listener fällt durch → UI verharrt auf Registration-Step.

Vorschlag: Den Branch verallgemeinern. Jeder Success impliziert einen erfolgreichen Sign (der Cubit emittiert ihn nur nach _signEip712 durch). Also unbedingt für jeden Status markRegistrationSignProduced() + checkKyc() rufen; ggf. nur für forwardingFailed einen abweichenden UX-Branch (SnackBar / Retry). Plus Page-Test analog zu kyc_registration_page_test.dart:128-141 für RegistrationStatus.alreadyRegistered ergänzen, sonst regrediert das Verhalten wieder lautlos.

2. settings_edit_address_cubit wurde nicht mitgezogen — Address-Edit hängt weiterhin am lokalen inReview-Check, obwohl die Page ihn schon entfernt hat
lib/screens/settings_user_data/subpages/edit_address/cubit/settings_edit_address_cubit.dart:22-28 — Der Cubit enthält noch das Pattern, das aus settings_edit_name_cubit entfernt wurde: if (session.currentStep?.status == KycStepStatus.inReview) → SettingsEditAddressPending plus throw Exception('No session URL returned') im no-URL-Fall. Die SettingsUserDataPage gateet den Edit-Button jetzt aber via capabilities.canEditAddress (Page-Diff Z. 105-117), gleichzeitig zieht der _UserDataRow-Refactor (Z. 240-249) das alte statusLabel == null-Cogating zurück. Resultat: Inkonsistenz zur expliziten Begründung im Name-Cubit-Kommentar ("inReview state is no longer reachable here"), zwei unterschiedliche Defensive-Patterns für identische Subflows, und im Threat-Model: falls die API ein canEditAddress: true bei einem in-review Step liefert, blockt der Cubit lokal — d.h. die App hält weiterhin lokale Capability-Logik gegen die API-Authority (V15 nur halb geschlossen).

Vorschlag: settings_edit_address_cubit analog zu settings_edit_name_cubit umstellen (kein inReview-Branch, url == null → Pending), Test im selben Schema anpassen. Falls bewusst out-of-scope: das im PR-Body und in docs/api-authority-audit.md als "follow-up Wave 3.3" markieren, sonst sieht der Audit-Closer V15 fälschlich als done.

Empfehlungen

3. markRegistrationSignProduced zementiert eine implizite Vor-Bedingung im Cubit
lib/screens/kyc/steps/registration/cubits/registration_submit/kyc_registration_submit_cubit.dart:76 — Der Listener-Code geht davon aus, dass jeder Success(*) nur nach erfolgreichem Sign emittiert wird. Das stimmt heute, ist aber nirgends typisiert. Vermutung: wenn die API in Zukunft mal Success(forwardingFailed) vor dem Sign liefert (z.B. Pre-Check), wäre markRegistrationSignProduced falsch.

Vorschlag: Im KycRegistrationSubmitSuccess ein zusätzliches Bool signProduced mitschicken, oder den Sign-Marker im Cubit setzen statt in der Page. Dann ist das Gate datengetrieben statt seiten-implizit.

4. RegistrationStatus.fromString wirft bei unbekanntem Status — das ist gut, sollte aber im Mapper (real_unit_registration_response_dto.dart:12) ein verständlicher Wrapper sein
Falls die API in einer Rollout-Phase einen neuen Status hinzufügt (merge_required?), bekommt der User eine generische Exception → KycRegistrationSubmitFailure(message) mit dem rohen Dart-Exception-Text. Kein Blocker, aber ein klarer Log-Hinweis "unbekannter Registration-Status vom Backend" wäre hilfreicher.

Was solide ist

  • UserCapabilitiesDto: false-Defaults + null-tolerantes fromJson halten den Rollout-Window sauber (alter Backend → keine Edit-Buttons, nicht: Edit-Buttons mit 400-Fail). Strikt im Sinne der API-Authority-Regel.
  • settings_user_data_page: kompletter Wegfall des statusLabel == null-Cogatings (Z. 240-249), onEdit: capabilities.canEdit* ? ... : null ist die richtige Single-Source-of-Truth.
  • Test-Coverage für kyc_registration_submit_cubit: beide Pfade (alreadyRegistered als Success + ApiException als Failure) sind gepinnt — die "silent swallow"-Regression kann auf Cubit-Ebene nicht zurückkommen. Lücke nur auf Page-Ebene (Finding 1).
  • settings_contact: emailSet → supportAvailable ist konsistent durchgezogen inkl. transaction_receipt_settings_states_test.dart, kein Stale-Naming übriggeblieben.

Geprüft und nicht beanstandet: Magic-String-Mapping ('already_registered') ist in dieselbe Switch-Funktion eingebettet, kein Fall-Through; UserCapabilitiesDto ist in Equatable.props der Success-States enthalten; kycService.getUser() ist in allen settings_user_data_cubit_test-Fixtures gemockt.

@TaprootFreak
Copy link
Copy Markdown
Contributor

Habe die zwei Blocker direkt im Branch gefixt, weil @Blume1977 gerade nicht aktiv ist.

B1 — Listener (kyc_registration_page.dart)

Branch verallgemeinert: jeder KycRegistrationSubmitSuccess impliziert einen durchgelaufenen _signEip712 (der Cubit emittiert Success nur nach completeRegistration). Daher wird markRegistrationSignProduced() + checkKyc() jetzt für jeden Status gerufen (completed, pendingReview, forwardingFailed, alreadyRegistered) statt nur für completed. Für forwardingFailed zusätzlich ein informativer SnackBar (registrationForwardingFailed in EN+DE arb), damit der User sieht dass das Backend retried.

Tests in kyc_registration_page_test.dart:

  • bestehender completed-Test verifiziert jetzt zusätzlich markRegistrationSignProduced.
  • drei neue testWidgets für alreadyRegistered, pendingReview und forwardingFailed.

B2 — settings_edit_address_cubit

Cubit deckungsgleich mit settings_edit_name_cubit:

  • KycStepStatus.inReview-Branch entfernt.
  • url == nullSettingsEditAddressPending statt throw Exception('No session URL returned').
  • Defensive-Kommentar analog zum Name-Cubit ergänzt.

Tests in settings_edit_address_cubit_test.dart:

  • "inReview → Pending" Case zu "no URL → Pending" umformuliert.
  • "no URL → Failure" zu "no URL → Pending" invertiert.

Verification

  • flutter analyze — clean (0 issues)
  • flutter test1441 / 1441 passing

Commits

  • 10a1f5c fix(kyc): KYC-Flow nach Sign auch fuer alreadyRegistered/pendingReview/forwardingFailed weiterfuehren
  • bdfecc7 refactor(settings): edit-address-cubit analog zu edit-name auf canEditAddress-Gating umstellen

Empfehlungen 3 + 4 (signProduced als Cubit-Feld typisieren, Mapper-Wrapper für unbekannte Status) sind nicht adressiert — bewusst out-of-scope für die Blocker-Fixes.

Blume1977 and others added 4 commits May 21, 2026 20:21
…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**
…w/forwardingFailed weiterfuehren

Der RegistrationPage-Listener hat bisher nur fuer
RegistrationStatus.completed markRegistrationSignProduced() + checkKyc()
gerufen. Mit Wave 3.2 emittiert der Cubit aber ueber den Bind an
_signEip712 fuer jeden Backend-Status (completed, pendingReview,
forwardingFailed, alreadyRegistered) einen Success — der Sign hat in
jedem Fall stattgefunden. Vorher fiel der alreadyRegistered-Path durch
und der Flow hing nach BitBox-Sign auf der Registration-Seite.

Die Branch-Bedingung entfaellt jetzt: jeder Success lift das Sign-Gate
und triggert checkKyc(). Fuer forwardingFailed wird zusaetzlich ein
informativer SnackBar gezeigt, damit der User sieht, dass die
API-Weiterleitung verzoegert ist (Retry uebernimmt das Backend).

Tests: drei neue testWidgets fuer alreadyRegistered, pendingReview und
forwardingFailed. Der bestehende completed-Test verifiziert jetzt
zusaetzlich markRegistrationSignProduced().
…tAddress-Gating umstellen

Wave 3.2 hat den inReview-Branch und das url==null-Throw aus
settings_edit_name_cubit entfernt — der gleiche Code stand aber noch im
settings_edit_address_cubit. Die SettingsUserDataPage gateet den
Address-Edit-Button bereits ueber capabilities.canEditAddress, also war
V15 nur halb geschlossen: lokale Capability-Logik stand neben der
API-Authority.

Cubit jetzt deckungsgleich mit dem Name-Cubit:
- kein KycStepStatus.inReview-Branch mehr (die parent page invokes
  diesen Cubit nur wenn canEditAddress true ist).
- url==null fuehrt zu SettingsEditAddressPending (defensive Branch fuer
  die Race zwischen Capability-Check und startStep) statt zu einer
  Exception.

Tests entsprechend angepasst — "inReview --> Pending" durch "no URL
--> Pending" ersetzt und der "no URL --> Failure"-Case zu Pending
gewandelt.
…lossen

Die V46-Markierung "Closed by W3.1 / W3.2 (capabilities)" passte nicht zum
PR-Diff: kyc_registration_personal_step.dart wird in diesem Wave nicht
angefasst, und die zugrundeliegende Quelle des Dropdowns ist
RegistrationUserType.values (lokales Enum) — nicht eine Capability-Liste
aus UserCapabilitiesDto.

V46 deckungsgleich mit V12/V13: braucht eine eigene API-Aenderung
(Capability-Feld availableUserTypes oder eigener Endpoint) plus
UI-Migration. Bis dahin als deferred fuer den naechsten Wave gefuehrt,
damit der Audit-Status mit dem Code-Stand uebereinstimmt.
@TaprootFreak TaprootFreak force-pushed the refactor/use-user-capabilities branch from bdfecc7 to 577abac Compare May 21, 2026 18:28
@TaprootFreak
Copy link
Copy Markdown
Contributor

Rebased + Polish-Pass eingespielt.

Rebase auf develop (18cb528):

Neue SHAs:

  • 1d72effrefactor(app): consume UserCapabilities + ALREADY_REGISTERED registration status (war 7edd48f)
  • b2ac70bfix(kyc): KYC-Flow nach Sign auch fuer alreadyRegistered/pendingReview/forwardingFailed weiterfuehren (war 10a1f5c)
  • 38310b5refactor(settings): edit-address-cubit analog zu edit-name auf canEditAddress-Gating umstellen (war bdfecc7)

Polish-Commit:

  • 577abacdocs(audit): V46 als deferred markieren — nicht durch W3.1/W3.2 geschlossen. Die V46-Markierung "Closed by W3.1 / W3.2 (capabilities)" stand im Audit-Doc, aber kyc_registration_personal_step.dart ist nicht im PR-Diff und die Dropdown-Quelle ist ein lokales Enum, keine Capability-Liste. Re-markiert als Deferred für die nächste Welle.

PR-Body aktualisiert: neuer Abschnitt "Post-Review Fixes" listet jetzt die Listener-Generalisierung in kyc_registration_page.dart, die settings_edit_address_cubit-Migration und den neuen registrationForwardingFailed-i18n-Key explizit auf. Verification-Abschnitt zeigt 1438 / 1438 passing (vorher 1417, der Anstieg ist die neue Test-Coverage aus den zwei Follow-up-Commits).

Lokal verifiziert:

  • dart run tool/generate_localization.dart + dart run build_runner build --delete-conflicting-outputs + dart run tool/generate_release_info.dart → sauber
  • flutter analyze → clean
  • flutter test → 1438 / 1438 passing

Force-Push erfolgte mit --force-with-lease gegen den vorherigen Head bdfecc7. Kein Squash.

@TaprootFreak TaprootFreak merged commit b44207f into RealUnitCH:develop May 21, 2026
4 checks passed
TaprootFreak added a commit that referenced this pull request May 23, 2026
…ordering in-app (#499) (#530)

## Summary

Reverts #499 ("refactor(reference-data): consume /v1/legal-document,
/v1/company-info, country.displayOrder").

The companion API-side PR (DFXswiss/api#3734) has been **closed
unmerged** — the decision is to keep legal-document URLs, company
contact info and country ordering **client-side in the app** rather than
serving them from the API. `/v1/legal-document` and `/v1/company-info`
do not exist in the API, so the app owns this data.

## What this restores

- `legal_documents_config.dart` — the hardcoded registration-agreement
PDF URLs (`_registrationAgreementPdfUrls`, de/en) and the
`pdfUrls`-based `LegalDocumentConfig`.
- `settings_contact_page.dart` — the hardcoded RealUnit company contact
tiles (phone, email, website, imprint).
- `country_field.dart` — the hardcoded `['CH','DE','IT','FR']` priority
ordering.

## What this removes

- `DfxLegalDocumentService`, `DfxCompanyInfoService` and their DTOs (+
tests) — the API-consumption layer added by #499.
- The `Country.displayOrder` field and the `/v1/legal-document` +
`/v1/company-info` wiring.

## Not affected

DFXswiss/api#3732 and #3733 stay merged in the API; their app companions
(#494 KYC routing, #497 capabilities) are untouched. Only the
reference-data refactor is reverted.

## Verification

- `flutter analyze` — No issues found.
- `flutter test` — all 1504 tests pass.
TaprootFreak added a commit that referenced this pull request May 28, 2026
…lessons (#593)

Companion to
[DFXswiss/api#3773](DFXswiss/api#3773). The
David-review back-and-forth on the Wave-3 reset produced a concrete set
of rules that should bind every future capability consumption in this
repo — this PR writes them down so the next contributor doesn't have to
rediscover them.

## Three docs updated

### 1. \`CONTRIBUTING.md\` — new sub-section "Consuming API capabilities
— eight rules"

Lives inside the existing "API as Decision Authority — CRITICAL"
section. Covers:

1. Read the capability shape, don't reconstruct it
2. Tile/button visibility for discoverable actions is unconditional
3. Map prerequisite types to UI components, not to business rules
4. Legacy backend tolerance — capability optional, sane fallback
5. No reactive 400-handling for what a capability could pre-tell
6. Pair-PR discipline
7. Tests pin the contract, not the implementation
8. Push back on capability shape that's over-engineered

Cross-references the API-side mirror in
[\`DFXswiss/api:CONTRIBUTING.md\`](https://github.com/DFXswiss/api/blob/develop/CONTRIBUTING.md).

### 2. \`docs/api-authority-plan.md\` — Wave-3 lessons-learned block

Documents the **six-PR** history of V9 (a single capability flag):

| PR | Direction | Outcome |
|---|---|---|
| [api#3733](DFXswiss/api#3733) | API:
\`+supportAvailable: bool\` | merged |
| [app#497](#497) | App:
consume \`supportAvailable\` bool | merged |
| [app#588](#588) | App:
unconditional Support tile | merged |
| [api#3761](DFXswiss/api#3761) | API:
\`-supportAvailable: bool\` | merged |
| [api#3767](DFXswiss/api#3767) | API:
ActionCapability tree (4 DTOs, 170 LOC) | **closed without merge** |
| [api#3772](DFXswiss/api#3772) | API:
\`createSupportTicket: { available, missingPrerequisite? }\` (91 LOC) |
merged |

Plus three "what we'd do differently" points and forward guidance for
Waves 4 and 5.

### 3. \`docs/api-authority-audit.md\` — V9 closed

Marked as closed by api#3772 with a link back to the lessons-learned
section (because the linear "closed by W3" mapping in the audit table
doesn't capture the actual non-linear history).

## Why now

Cyrill asked for these rules to be prominently documented in both repos
and in cross-repo working notes so the pattern doesn't drift on the next
capability. The API side gets the mirror PR
([DFXswiss/api#3773](DFXswiss/api#3773)).

## Verification

- Pure documentation — no code changes
- Markdown renders cleanly (verified)
- Cross-references resolve

## Companion PRs

- API rules:
[DFXswiss/api#3773](DFXswiss/api#3773)
- App pair-PR consuming the first capability following this pattern
(\`createSupportTicket\`): opening shortly on
\`refactor/consume-support-capability\`.
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