feat(accounts): capability state machine + headline status nomenclature (ENG-516)#452
feat(accounts): capability state machine + headline status nomenclature (ENG-516)#452islandbitcoin wants to merge 3 commits into
Conversation
…re (ENG-516)
Retire Pro/International as user-facing tiers; rename Merchant to Business.
Model the account as capability flags {verified, bankPayout, business,
usdAccount} and derive the internal level (L1-L3) from them. Expose
capabilities + statusHeadline (light headline status: Trial / Verified /
Business) on ConsumerAccount and AuditedAccount, and add the
accountCapabilityUpgradeRequest mutation so clients request a single
capability instead of a whole tier. Spec in docs/account-capabilities.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review — capability state machine (ENG-516)Domain derivation module + the 15-test matrix are solid. Four items to action before merge: 1. 🔴 CI is red — new mutation not registered in 2. 🟠 An unverified (L0) account can mint an L2 upgrade request. 3. 🟡 4. 🟡 Minor: the read-model derived level can exceed the still-exposed stored |
…unt typing (ENG-516) Review follow-ups on the capability state machine: - Register accountCapabilityUpgradeRequest in the API-key scope map (BLOCKED, like businessAccountUpgradeRequest). The deny-by-default completeness test was red because the new mutation was unmapped. - requestCapabilityUpgrade now rejects unverified accounts: the state machine derives L0 for !verified, so a request from one would otherwise become a level-skipping L2 ERPNext request (validate() only guards requested<=current). - Type the L3 upgrade's bankAccount as optional and drop the `as BankAccount` cast — a business upgrade legitimately reuses a bank already on file, and validate() already reads it via optional chaining. - Add unit tests for requestCapabilityUpgrade (unverified, already-has, missing-bank, L2 add-bank, L3 reuse-on-file). - Doc note on derived-vs-stored level divergence. Verified: tsc --noEmit clean; full unit suite 134 suites / 1057 passed (api-key-scope-enforcement now green). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…eral (ENG-516) Review follow-ups: - getAccountCapabilities memoizes per account object (WeakMap): the capabilities and statusHeadline GraphQL fields each resolved it separately, costing two serial ERPNext round-trips per query — and N×2 on admin views that list accounts. One lookup now serves every field resolution for the request; no cross-request staleness since each request fetches a fresh account object. - BRIDGE_KYC_APPROVED constant typed against Account["bridgeKycStatus"] replaces the bare "approved" magic string in the derivation, so an upstream status rename is a compile error instead of a silently-false usdAccount. - getAccountCapabilities unit tests: ERPNext-failure fallback (the branch that runs during an ERPNext incident), no-erpParty skip, bank-on-file, memoization semantics. - requestCapabilityUpgrade return annotation was Promise<Promise<...>> via ReturnType of an async fn; unwrapped. tsc clean; full unit suite 135 suites / 1064 passed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Summary
Backend half of ENG-516 (Account Upgrade Revamp). Retires Pro and International as user-facing tiers, renames Merchant → Business, and models the account as a set of capability flags from which the internal level is derived. Companion frappe-flash-admin PR adopts the new names in the admin panel; mobile presentation lands with ENG-513.
Product decision (2026-07-14): light headline status — the account leads with one word (Trial → Verified → Business) with capability badges as supporting detail.
The state machine
capabilities/statusHeadlineresolve from one memoized derivation per account per request (WeakMap keyed on the account object) — one ERPNext lookup no matter how many fields ask; the Bridge-KYC "approved" literal is a typed domain constant (BRIDGE_KYC_APPROVED).src/domain/accounts/capabilities.ts— pure derivation:deriveLevelFromCapabilities,deriveStatusHeadline,deriveCapabilitiesForAccount(read model over stored level + ERPNext bank accounts +bridgeKycStatus, with grandfathering for existing L2/L3 accounts).capabilities: AccountCapabilities!andstatusHeadline: AccountStatusHeadline!onConsumerAccount(public) andAuditedAccount(admin).levelstays exposed but documented as internal.accountCapabilityUpgradeRequestmutation — the client asks for one capability (BANK_PAYOUTorBUSINESS); the target level is derived server-side and the request flows down the existing ERPNext Account Upgrade Request pipeline unchanged.businessAccountUpgradeRequest(whole-tier) remains for existing clients.docs/account-capabilities.md.Testing
TEST="capabilities|request-capability-upgrade|get-account-capabilities" yarn test:unit— 27 tests: derivation matrix, headline mapping, grandfathering, round-trip;requestCapabilityUpgraderouting (unverified / already-has / missing-bank / L2 add-bank / L3 reuse-on-file);getAccountCapabilitiesERPNext-failure fallback, no-erpParty skip, and memoization semantics.yarn tsc-checkclean; SDL regenerated viayarn write-sdl(public + admin + supergraph).Local testing
🤖 Generated with Claude Code