Bet Matched Payload — Implementation Plan (Phase 1b)
Execute task-by-task. Backend task is a settlement/money path → adversarial review required.
Goal: Show a rich "Bet Matched" (green) and "Bet Partially Matched (X%)" (yellow) toast carrying Back/Lay "<selection>" @ "<matched_odds>", by enriching the order:status WS event with the fields the frontend needs.
Base: feat/bet-matched-payload off origin/dev (@ f67ca91f2). NOT stacked on #1040 — dev lacks the message catalog, so the frontend is done in dev's native classifyOrderStatusToast style (inline copy, {kind,title,body}, toast[kind] dispatch). Expected merge-time reconciliation with #1040 in orderStatus.ts + useWebSocket.ts.
Global Constraints (financial-security)
- Never fabricate financial/domain values.
matchedOdds,matchedStake,stake,selectionName,betTypeare money/identity fields. Attach to the emit ONLY when genuinely in scope; NEVER?? 0/|| ''/ guessed values. - Graceful fallback: if the frontend receives a matched event WITHOUT
selectionName/matchedOdds, it renders the EXISTING generic copy (Bet Accepted/Your bet has been matched.) — never a partial/blank interpolation. %guard:pct = matchedStake / stake * 100only when both present andstake > 0; otherwise omit the%from the title. No default.- Q3 unchanged: declined / lapsed / cancel-terminal branches in
classifyOrderStatusToastkeep identical copy + behavior. - Partial-match tone on this dev base =
warning(yellow triangle). The Tick+Yellow icon lives in #1040; upgrade at reconciliation. Do NOT port #1040's Toast.tsx here.
Task 1 — Backend: enrich the order:status emit (MONEY PATH — adversarial review)
Files:
backend/src/exchanges/adapters/bifrost/BifrostBetConsumer.ts— the emit carryingmatched/partially_matchedstatus (themapBetStatusToOrderStatus/publishStatuspath; locate by content — line ~390-region is the VOID path, the matched path is the main snapshot publish). Theorderrow is in scope there.backend/src/services/orderState/applier.ts:~961— the Betfair post-commit publish (txResultin scope).backend/src/services/clientWs.ts:~75—OrderStatusUpdateinterface.- Test: extend the existing
order:statuspublish assertion (backend/src/jobs/__tests__/runOrderSyncLapseFlip.test.tsor the Bifrost consumer test) to assert the new fields on a matched publish.
Add these OPTIONAL fields to the matched/partially_matched publishes (mirror SettlementNotification enrichment in clientWs.ts):
selectionName, betType ('back'|'lay'), matchedOdds (number), matchedStake (number), stake (number, requested).
Steps:
- Verify what's cheaply in scope at each emit: Bifrost main path has the
orderrow (order.selectionName,order.betType,order.stake, matched odds/stake from the snapshot mapping).applier.tshastxResult+ the loaded order — confirm whethermatchedOdds/matchedStake/selectionName/betTypeare reachable there; if a field requires an extra query it did not already do, prefer passing it from an already-loaded object, else OMIT it (never fabricate). - Add the fields to
clientWs.tsOrderStatusUpdateas optional. - Add the fields to the Bifrost matched publish and the
applier.tspublish, guarded (only when present). - Extend the publish test to assert the fields appear on a matched publish; run backend typecheck (
npx tsc --noEmitinbackend/, or the project's backend build/test command — detect it). - Commit:
feat(ws): enrich order:status matched publish with selection/side/matched-odds/stake
Adversarial review focus: no fabricated/defaulted financial value; fields omitted (not zeroed) when unavailable; existing payload consumers unaffected by the added optional fields; the void/reversal emits are NOT forced to carry fake matched data.
Task 2 — Frontend: interface + classifier + wiring + tests
Files:
strykr-fe/src/hooks/useWebSocket.ts—OrderStatusUpdateinterface +order:statushandler call site.strykr-fe/src/lib/orderStatus.ts—classifyOrderStatusToast.- Test:
strykr-fe/src/lib/orderStatus.test.mjs.
Steps:
- Add optional fields to
useWebSocket.tsOrderStatusUpdate:selectionName?,betType?('back'|'lay'),matchedOdds?: number,matchedStake?: number,stake?: number. - Change
classifyOrderStatusToastsignature to accept the enriched data (an object, e.g.classifyOrderStatusToast(update: { status; settlementOutcome?; selectionName?; betType?; matchedOdds?; matchedStake?; stake? })), keeping return type{ kind, title, body } | null.- matched branch: if
selectionName&&matchedOddspresent →{ kind:'success', title:'Bet Matched', body:${Side} "${selectionName}" @ "${matchedOdds}"}(Side = betType==='lay'?'Lay':'Back'). Else → the EXISTING generic{ kind:'success', title:'Bet Accepted', body:'Your bet has been matched.' }. - NEW
partially_matchedbranch: computepctonly ifmatchedStake&&stake > 0→Math.round(matchedStake/stake*100). TitleBet Partially Matched (${pct}%)when pct known, elseBet Partially Matched. Body = sameSide "sel" @ oddswhen present, else omitted.kind:'warning'. - Q3 branches (declined/lapsed/cancel-terminal/void) unchanged.
- matched branch: if
- Update the
useWebSocket.tscall site to pass the whole update object; dispatch unchanged (toast[kind]/ existing pattern). - Extend
orderStatus.test.mjs: (a) matched with fields → rich body; (b) matched WITHOUT fields → generic fallback; (c)partially_matchedwith fields → title has%+ body; (d)partially_matchedwithout matchedStake/stake → no%, no crash; (e) a Q3 case unchanged. - Verify:
node --test src/lib/orderStatus.test.mjs,npm test,npx tsc --noEmit. - Commit:
feat(messages): rich Bet Matched / Partially Matched toast copy from enriched payload
Verification (whole feature)
- Backend: typecheck + publish test green.
- Frontend: full
npm test+tscclean. - No fabricated financial values anywhere (grep the diff for
?? 0,|| 0,|| ''on the new fields).