refactor(lane-3): transformer per-kind strategies — 952→553 lines#1915
refactor(lane-3): transformer per-kind strategies — 952→553 lines#1915Hugo0 wants to merge 2 commits intoqa/post-cutover-fe-fixesfrom
Conversation
Split the monolithic 51-arm switch in transactionTransformer.ts into
21 per-kind strategies under src/components/TransactionDetails/strategies/.
Composite-key registry maps {legacy entry type, TRANSACTION_INTENT:kind}
→ strategy. Public API (mapTransactionDataForDrawer) unchanged.
## Why
The old 3D dispatch (entry.type × intent.kind × userRole) was nested 3-4
levels deep; today's playtest revealed 5 missing/wrong branches that all
looked plausible at review. Adding a new kind required editing the right
level + getting all three dimensions right. Per-strategy files make each
kind grep-able in one place, so a new kind is "add a file + register it"
not "find the right nested arm".
## Scope
21 strategies, no behavioral change:
- legacy: direct-send, send-link, request, bank-send-link-claim,
withdraw, cashout, bank-offramp×2, bank-onramp×2, deposit,
manteca-qr, simplefi-qr, perk-reward
- intent: p2p-send (covers REQUEST_PAY), qr-pay, link-create,
crypto-deposit, crypto-withdraw, fiat-offramp, card-spend, card-refund
- fallback: intentFallback (unknown intent kinds → Sentry log + safe
default), legacyFallback (unknown legacy types → safe default)
Post-strategy globals (status mapping, reaper-failed override, derived
fields like explorer URL / token logos / initials) stay in
mapTransactionDataForDrawer — those run uniformly regardless of kind.
## Verified
- 33/33 transformer fixtures pass (locks the contract)
- 961/961 FE tests pass
- pnpm typecheck clean
- mapTransactionDataForDrawer signature unchanged → useReceiptViewModel
+ drawer rendering unaffected
## Notes
- Registry uses string-literal keys (not EHistoryEntryType.X) so tests
that mock useTransactionHistory still load the registry module.
- The intent fallback inlines the legacy default-arm Sentry call shape;
Lane 4 FE PR (#1914) consolidates it onto pipelineAlert. Two-line
conflict resolution at merge.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughAdds a strategy pattern for transaction-detail rendering: new strategy types, many intent and legacy strategy implementations, a registry dispatcher with fallbacks, and refactors the main transformer to use dispatched strategies instead of an inline switch. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
Review rate limit: 3/5 reviews remaining, refill in 12 minutes and 15 seconds. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/components/TransactionDetails/transactionTransformer.ts (1)
269-375:⚠️ Potential issue | 🟡 Minor
uiStatusfrom strategies is currently ignored.Line [269] applies
out.uiStatus, but Lines [291-375] always recomputeuiStatus, so strategy overrides never survive to the returned payload.Suggested fix
- if (out.uiStatus) uiStatus = out.uiStatus + const strategyUiStatus = out.uiStatus @@ if ( entry.type === EHistoryEntryType.BRIDGE_OFFRAMP || @@ } } + + if (strategyUiStatus) uiStatus = strategyUiStatus🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/TransactionDetails/transactionTransformer.ts` around lines 269 - 375, The code sets uiStatus from out.uiStatus but then always recomputes it, ignoring strategy overrides; update transactionTransformer.ts so that after assigning uiStatus = out.uiStatus you short-circuit the recompute logic (or wrap the two big status-switch blocks in a guard) and only run the EHistoryEntryType/EHistoryEntryType.BRIDGE_* and default entry.status switches when uiStatus is falsy/undefined; reference the uiStatus local, the out.uiStatus source, and the status-switch logic that checks entry.type and entry.status so strategy-provided uiStatus survives to the returned payload.
🧹 Nitpick comments (1)
src/components/TransactionDetails/strategies/legacy/request.ts (1)
39-42: SimplifynameForDetailsfallback to avoid awkward text.Line 39–Line 42 reuses the same fields in all fallback branches, which can degrade to unclear strings on bad/missing account data. Prefer a stable terminal fallback.
Proposed cleanup
- nameForDetails: - entry.recipientAccount?.username || - entry.recipientAccount?.identifier || - `Request From ${entry.recipientAccount?.username || entry.recipientAccount?.identifier}`, + nameForDetails: + entry.recipientAccount?.username || + entry.recipientAccount?.identifier || + 'Request',🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/TransactionDetails/strategies/legacy/request.ts` around lines 39 - 42, The current nameForDetails expression repeats the same fields and can produce awkward strings; replace it with a stable fallback like entry.recipientAccount?.username || entry.recipientAccount?.identifier || 'Unknown Sender' (i.e., remove the template `Request From ${...}` fallback and use a single final constant). Update the nameForDetails assignment in request.ts so it references entry.recipientAccount?.username, then entry.recipientAccount?.identifier, then the stable terminal fallback 'Unknown Sender'.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/components/TransactionDetails/strategies/fallback.ts`:
- Around line 17-23: The fallback predicate currently routes card refunds based
only on kind and parentRainTxId; update the conditional that returns
cardRefund(entry) to also require the provider === 'RAIN' check (i.e., ensure
the if tests (kind === 'OTHER' || kind === 'REFUND') &&
entry.extraData?.parentRainTxId && provider === 'RAIN') so the logic matches the
comment and avoids misrouting.
In `@src/components/TransactionDetails/strategies/intent/p2p-send.ts`:
- Around line 15-23: In the REQUEST_PAY && entry.extraData?.fulfillmentType ===
'bridge' && entry.userRole === 'SENDER' branch inside p2p-send.ts, the
isPeerActuallyUser flag is using entry.senderAccount?.isUser which is the wrong
side and can yield false positives; change the computation to rely on the
recipient side (entry.recipientAccount?.isUser) so isPeerActuallyUser becomes
!!entry.recipientAccount?.isUser || !!entry.senderAccount?.isUser (or simply
!!entry.recipientAccount?.isUser if only recipient matters) in the object
returned by this branch.
In `@src/components/TransactionDetails/strategies/legacy/send-link.ts`:
- Around line 15-16: The mapping in the send-link strategy is incorrectly
assigning recipientAccount.username to the UI fullName field; update the mapping
inside the send-link strategy (e.g., where you construct the recipient object in
send-link.ts) to use entry.recipientAccount?.fullName for fullName (and likewise
replace the username usage at the other occurrence around lines 60-61) while
leaving showFullName as entry.recipientAccount?.showFullName so the actual full
name is displayed.
---
Outside diff comments:
In `@src/components/TransactionDetails/transactionTransformer.ts`:
- Around line 269-375: The code sets uiStatus from out.uiStatus but then always
recomputes it, ignoring strategy overrides; update transactionTransformer.ts so
that after assigning uiStatus = out.uiStatus you short-circuit the recompute
logic (or wrap the two big status-switch blocks in a guard) and only run the
EHistoryEntryType/EHistoryEntryType.BRIDGE_* and default entry.status switches
when uiStatus is falsy/undefined; reference the uiStatus local, the out.uiStatus
source, and the status-switch logic that checks entry.type and entry.status so
strategy-provided uiStatus survives to the returned payload.
---
Nitpick comments:
In `@src/components/TransactionDetails/strategies/legacy/request.ts`:
- Around line 39-42: The current nameForDetails expression repeats the same
fields and can produce awkward strings; replace it with a stable fallback like
entry.recipientAccount?.username || entry.recipientAccount?.identifier ||
'Unknown Sender' (i.e., remove the template `Request From ${...}` fallback and
use a single final constant). Update the nameForDetails assignment in request.ts
so it references entry.recipientAccount?.username, then
entry.recipientAccount?.identifier, then the stable terminal fallback 'Unknown
Sender'.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: e3471e4a-b086-43c3-9902-3907e3ce11ca
📒 Files selected for processing (14)
src/components/TransactionDetails/strategies/fallback.tssrc/components/TransactionDetails/strategies/intent/card.tssrc/components/TransactionDetails/strategies/intent/crypto.tssrc/components/TransactionDetails/strategies/intent/fiat-offramp.tssrc/components/TransactionDetails/strategies/intent/link-create.tssrc/components/TransactionDetails/strategies/intent/p2p-send.tssrc/components/TransactionDetails/strategies/legacy/bank-send-link-claim.tssrc/components/TransactionDetails/strategies/legacy/direct-send.tssrc/components/TransactionDetails/strategies/legacy/external-account.tssrc/components/TransactionDetails/strategies/legacy/request.tssrc/components/TransactionDetails/strategies/legacy/send-link.tssrc/components/TransactionDetails/strategies/registry.tssrc/components/TransactionDetails/strategies/types.tssrc/components/TransactionDetails/transactionTransformer.ts
| // Card refunds come back with kind === 'REFUND' or kind === 'OTHER' | ||
| // alongside provider === RAIN. Scope strictly to these two kinds — | ||
| // guarding only on parentRainTxId would misroute any future intent | ||
| // that happens to carry the linkage. | ||
| if ((kind === 'OTHER' || kind === 'REFUND') && entry.extraData?.parentRainTxId) { | ||
| return cardRefund(entry) | ||
| } |
There was a problem hiding this comment.
Card-refund fallback predicate is missing the provider === 'RAIN' guard.
Line [21] currently routes by kind + parent linkage only, despite the comment/scoping intent. Add the provider check to avoid future misrouting.
Suggested fix
- if ((kind === 'OTHER' || kind === 'REFUND') && entry.extraData?.parentRainTxId) {
+ if (
+ (kind === 'OTHER' || kind === 'REFUND') &&
+ entry.extraData?.provider === 'RAIN' &&
+ entry.extraData?.parentRainTxId
+ ) {
return cardRefund(entry)
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Card refunds come back with kind === 'REFUND' or kind === 'OTHER' | |
| // alongside provider === RAIN. Scope strictly to these two kinds — | |
| // guarding only on parentRainTxId would misroute any future intent | |
| // that happens to carry the linkage. | |
| if ((kind === 'OTHER' || kind === 'REFUND') && entry.extraData?.parentRainTxId) { | |
| return cardRefund(entry) | |
| } | |
| // Card refunds come back with kind === 'REFUND' or kind === 'OTHER' | |
| // alongside provider === RAIN. Scope strictly to these two kinds — | |
| // guarding only on parentRainTxId would misroute any future intent | |
| // that happens to carry the linkage. | |
| if ( | |
| (kind === 'OTHER' || kind === 'REFUND') && | |
| entry.extraData?.provider === 'RAIN' && | |
| entry.extraData?.parentRainTxId | |
| ) { | |
| return cardRefund(entry) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/TransactionDetails/strategies/fallback.ts` around lines 17 -
23, The fallback predicate currently routes card refunds based only on kind and
parentRainTxId; update the conditional that returns cardRefund(entry) to also
require the provider === 'RAIN' check (i.e., ensure the if tests (kind ===
'OTHER' || kind === 'REFUND') && entry.extraData?.parentRainTxId && provider ===
'RAIN') so the logic matches the comment and avoids misrouting.
| if (kind === 'REQUEST_PAY' && entry.extraData?.fulfillmentType === 'bridge' && entry.userRole === 'SENDER') { | ||
| return { | ||
| direction: 'bank_request_fulfillment', | ||
| transactionCardType: 'bank_request_fulfillment', | ||
| nameForDetails: entry.recipientAccount?.username ?? entry.recipientAccount?.identifier ?? 'Recipient', | ||
| fullName: entry.recipientAccount?.fullName ?? '', | ||
| showFullName: entry.recipientAccount?.showFullName, | ||
| isPeerActuallyUser: !!entry.recipientAccount?.isUser || !!entry.senderAccount?.isUser, | ||
| isLinkTx: false, |
There was a problem hiding this comment.
isPeerActuallyUser is computed from the wrong side in REQUEST_PAY bridge sender flow.
Line [22] includes senderAccount?.isUser, which can produce false positives for peer-user badges. This should be based on the recipient in this branch.
Suggested fix
- isPeerActuallyUser: !!entry.recipientAccount?.isUser || !!entry.senderAccount?.isUser,
+ isPeerActuallyUser: !!entry.recipientAccount?.isUser,📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (kind === 'REQUEST_PAY' && entry.extraData?.fulfillmentType === 'bridge' && entry.userRole === 'SENDER') { | |
| return { | |
| direction: 'bank_request_fulfillment', | |
| transactionCardType: 'bank_request_fulfillment', | |
| nameForDetails: entry.recipientAccount?.username ?? entry.recipientAccount?.identifier ?? 'Recipient', | |
| fullName: entry.recipientAccount?.fullName ?? '', | |
| showFullName: entry.recipientAccount?.showFullName, | |
| isPeerActuallyUser: !!entry.recipientAccount?.isUser || !!entry.senderAccount?.isUser, | |
| isLinkTx: false, | |
| if (kind === 'REQUEST_PAY' && entry.extraData?.fulfillmentType === 'bridge' && entry.userRole === 'SENDER') { | |
| return { | |
| direction: 'bank_request_fulfillment', | |
| transactionCardType: 'bank_request_fulfillment', | |
| nameForDetails: entry.recipientAccount?.username ?? entry.recipientAccount?.identifier ?? 'Recipient', | |
| fullName: entry.recipientAccount?.fullName ?? '', | |
| showFullName: entry.recipientAccount?.showFullName, | |
| isPeerActuallyUser: !!entry.recipientAccount?.isUser, | |
| isLinkTx: false, |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/TransactionDetails/strategies/intent/p2p-send.ts` around lines
15 - 23, In the REQUEST_PAY && entry.extraData?.fulfillmentType === 'bridge' &&
entry.userRole === 'SENDER' branch inside p2p-send.ts, the isPeerActuallyUser
flag is using entry.senderAccount?.isUser which is the wrong side and can yield
false positives; change the computation to rely on the recipient side
(entry.recipientAccount?.isUser) so isPeerActuallyUser becomes
!!entry.recipientAccount?.isUser || !!entry.senderAccount?.isUser (or simply
!!entry.recipientAccount?.isUser if only recipient matters) in the object
returned by this branch.
| fullName: entry.recipientAccount?.username ?? '', | ||
| showFullName: entry.recipientAccount?.showFullName, |
There was a problem hiding this comment.
fullName is mapped from username instead of fullName.
Line 15 and Line 60 should pull recipientAccount.fullName; using username can suppress the intended full-name display.
Proposed fix
- fullName: entry.recipientAccount?.username ?? '',
+ fullName: entry.recipientAccount?.fullName ?? '',
@@
- fullName: entry.recipientAccount?.username ?? '',
+ fullName: entry.recipientAccount?.fullName ?? '',Also applies to: 60-61
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/TransactionDetails/strategies/legacy/send-link.ts` around
lines 15 - 16, The mapping in the send-link strategy is incorrectly assigning
recipientAccount.username to the UI fullName field; update the mapping inside
the send-link strategy (e.g., where you construct the recipient object in
send-link.ts) to use entry.recipientAccount?.fullName for fullName (and likewise
replace the username usage at the other occurrence around lines 60-61) while
leaving showFullName as entry.recipientAccount?.showFullName so the actual full
name is displayed.
…format
- Replace stringly-typed userRole compares ('SENDER'/'RECIPIENT') with
EHistoryUserRole enum (intent strategies were the stragglers).
- Hoist BANK_CLAIM constant in bank-send-link-claim.ts — guest and
claimant branches were byte-identical.
- Drop dead default initializers / narrative comments in
transactionTransformer.ts and fallback.ts.
- Apply prettier to 3 inherited unformatted files (PeanutDebug.tsx,
account-mask.utils.ts, transactionTransformer.test.ts) so the lint
gate passes on this stack.
There was a problem hiding this comment.
♻️ Duplicate comments (2)
src/components/TransactionDetails/strategies/intent/p2p-send.ts (1)
13-25:⚠️ Potential issue | 🟠 MajorUse recipient-only peer flag in the bridge
REQUEST_PAYsender branch.Line 23 still mixes in
entry.senderAccount?.isUser; in this branch the sender is the viewer, so this can produce false-positive peer badges.Suggested fix
- isPeerActuallyUser: !!entry.recipientAccount?.isUser || !!entry.senderAccount?.isUser, + isPeerActuallyUser: !!entry.recipientAccount?.isUser,🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/TransactionDetails/strategies/intent/p2p-send.ts` around lines 13 - 25, In the REQUEST_PAY branch of the p2p-send intent (the branch where kind === 'REQUEST_PAY' and entry.extraData?.fulfillmentType === 'bridge' and entry.userRole === EHistoryUserRole.SENDER), change the isPeerActuallyUser computation to use only the recipient account flag (entry.recipientAccount?.isUser) instead of OR-ing with entry.senderAccount?.isUser; update the isPeerActuallyUser property to reflect recipient-only peer detection so the viewer/sender doesn't cause false-positive peer badges.src/components/TransactionDetails/strategies/fallback.ts (1)
11-13:⚠️ Potential issue | 🟠 MajorAdd the missing
provider === 'RAIN'guard in refund fallback routing.At Line [11], the predicate still routes by kind +
parentRainTxIdonly. Because unmatched intents are funneled into this fallback (src/components/TransactionDetails/strategies/registry.ts:76-81), this can misroute future non-Rain intents that also carryparentRainTxId.Suggested fix
- if ((kind === 'OTHER' || kind === 'REFUND') && entry.extraData?.parentRainTxId) { + if ( + (kind === 'OTHER' || kind === 'REFUND') && + entry.extraData?.provider === 'RAIN' && + entry.extraData?.parentRainTxId + ) { return cardRefund(entry) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/TransactionDetails/strategies/fallback.ts` around lines 11 - 13, The fallback routing condition in fallback.ts currently checks only kind and entry.extraData.parentRainTxId and can misroute non-Rain intents; update the predicate so it also requires entry.provider === 'RAIN' before calling cardRefund(entry). Locate the if in the fallback strategy that returns cardRefund(entry) (referencing symbols: entry, kind, cardRefund) and change the condition to include the provider === 'RAIN' guard (e.g., && entry.provider === 'RAIN') so only Rain-provider refunds use this route.
🧹 Nitpick comments (1)
src/components/TransactionDetails/strategies/intent/link-create.ts (1)
22-22: HardennameForDetailsagainst empty-string account fields.Line 22 can still resolve to an empty label (
'') when account fields are empty strings. A non-empty fallback avoids blank UI in drawer headers.Suggested patch
- nameForDetails: entry.recipientAccount?.username ?? entry.recipientAccount?.identifier ?? '', + nameForDetails: + entry.recipientAccount?.username || + entry.recipientAccount?.identifier || + 'Sent to user',🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/TransactionDetails/strategies/intent/link-create.ts` at line 22, The nameForDetails assignment can still become an empty string if username or identifier are empty; change it to pick the first non-empty trimmed value from entry.recipientAccount?.username and entry.recipientAccount?.identifier and fall back to a non-empty label (e.g. 'Unknown account' or similar). Locate the nameForDetails usage in link-create.ts (the line using entry.recipientAccount?.username ?? entry.recipientAccount?.identifier ?? '') and replace the expression with logic that trims and checks length (or a small helper like firstNonEmpty) so blank strings are ignored before applying the final fallback.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@src/components/TransactionDetails/strategies/fallback.ts`:
- Around line 11-13: The fallback routing condition in fallback.ts currently
checks only kind and entry.extraData.parentRainTxId and can misroute non-Rain
intents; update the predicate so it also requires entry.provider === 'RAIN'
before calling cardRefund(entry). Locate the if in the fallback strategy that
returns cardRefund(entry) (referencing symbols: entry, kind, cardRefund) and
change the condition to include the provider === 'RAIN' guard (e.g., &&
entry.provider === 'RAIN') so only Rain-provider refunds use this route.
In `@src/components/TransactionDetails/strategies/intent/p2p-send.ts`:
- Around line 13-25: In the REQUEST_PAY branch of the p2p-send intent (the
branch where kind === 'REQUEST_PAY' and entry.extraData?.fulfillmentType ===
'bridge' and entry.userRole === EHistoryUserRole.SENDER), change the
isPeerActuallyUser computation to use only the recipient account flag
(entry.recipientAccount?.isUser) instead of OR-ing with
entry.senderAccount?.isUser; update the isPeerActuallyUser property to reflect
recipient-only peer detection so the viewer/sender doesn't cause false-positive
peer badges.
---
Nitpick comments:
In `@src/components/TransactionDetails/strategies/intent/link-create.ts`:
- Line 22: The nameForDetails assignment can still become an empty string if
username or identifier are empty; change it to pick the first non-empty trimmed
value from entry.recipientAccount?.username and
entry.recipientAccount?.identifier and fall back to a non-empty label (e.g.
'Unknown account' or similar). Locate the nameForDetails usage in link-create.ts
(the line using entry.recipientAccount?.username ??
entry.recipientAccount?.identifier ?? '') and replace the expression with logic
that trims and checks length (or a small helper like firstNonEmpty) so blank
strings are ignored before applying the final fallback.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 0e1033a3-92dd-498e-9815-744cb53ac347
📒 Files selected for processing (10)
src/components/TransactionDetails/__tests__/transactionTransformer.test.tssrc/components/TransactionDetails/strategies/fallback.tssrc/components/TransactionDetails/strategies/intent/crypto.tssrc/components/TransactionDetails/strategies/intent/fiat-offramp.tssrc/components/TransactionDetails/strategies/intent/link-create.tssrc/components/TransactionDetails/strategies/intent/p2p-send.tssrc/components/TransactionDetails/strategies/legacy/bank-send-link-claim.tssrc/components/TransactionDetails/transactionTransformer.tssrc/context/PeanutDebug.tsxsrc/utils/account-mask.utils.ts
✅ Files skipped from review due to trivial changes (4)
- src/context/PeanutDebug.tsx
- src/utils/account-mask.utils.ts
- src/components/TransactionDetails/strategies/intent/crypto.ts
- src/components/TransactionDetails/tests/transactionTransformer.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
- src/components/TransactionDetails/strategies/legacy/bank-send-link-claim.ts
- src/components/TransactionDetails/transactionTransformer.ts
- src/components/TransactionDetails/strategies/intent/fiat-offramp.ts
Summary
Split the monolithic 51-arm switch in `transactionTransformer.ts` into
21 per-kind strategy files under `strategies/`. Composite-key registry
maps `{legacy entry type, TRANSACTION_INTENT:kind}` → strategy. Public
API (`mapTransactionDataForDrawer`) unchanged; the receipt drawer +
`useReceiptViewModel` keep working without modification.
Why
The old 3D dispatch (`entry.type × intent.kind × userRole`) was nested
3-4 levels deep. Today's playtest revealed 5 missing/wrong branches that
all looked plausible at review. Adding a new kind required editing the
right level + getting all three dimensions right. Per-strategy files
make each kind grep-able in one place — adding a new kind is now "add a
file + register it" instead of "find the right nested arm".
Scope (21 strategies)
Legacy: direct-send, send-link, request, bank-send-link-claim,
withdraw, cashout, bank-offramp (×2 keys), bank-onramp (×2 keys),
deposit, manteca-qr, simplefi-qr, perk-reward
Intent: p2p-send (covers REQUEST_PAY), qr-pay, link-create,
crypto-deposit, crypto-withdraw, fiat-offramp, card-spend, card-refund
Fallback: intentFallback (unknown intent kinds → Sentry + safe default),
legacyFallback (unknown legacy types → safe default)
Post-strategy globals (status mapping, reaper-failed override, derived
fields: explorer URL, token logos, initials) stay in
`mapTransactionDataForDrawer` — those run uniformly regardless of kind.
Verified
Notes for review
tests mock `useTransactionHistory`, which breaks the enum re-export at
module-init time. Inlined string values match the enum.
Lane 4 FE PR refactor(lane-4): pipelineAlert() helper for FE Sentry signals #1914 consolidates that onto `pipelineAlert`; two-line
conflict resolution at merge.
Base
Stacked on `qa/post-cutover-fe-fixes` (PR #1912). Independent of Lanes 1, 2a, 4.