Skip to main content

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, betType are 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 * 100 only when both present and stake > 0; otherwise omit the % from the title. No default.
  • Q3 unchanged: declined / lapsed / cancel-terminal branches in classifyOrderStatusToast keep 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 carrying matched/partially_matched status (the mapBetStatusToOrderStatus/publishStatus path; locate by content — line ~390-region is the VOID path, the matched path is the main snapshot publish). The order row is in scope there.
  • backend/src/services/orderState/applier.ts:~961 — the Betfair post-commit publish (txResult in scope).
  • backend/src/services/clientWs.ts:~75OrderStatusUpdate interface.
  • Test: extend the existing order:status publish assertion (backend/src/jobs/__tests__/runOrderSyncLapseFlip.test.ts or 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 order row (order.selectionName, order.betType, order.stake, matched odds/stake from the snapshot mapping). applier.ts has txResult + the loaded order — confirm whether matchedOdds/matchedStake/selectionName/betType are 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.ts OrderStatusUpdate as optional.
  • Add the fields to the Bifrost matched publish and the applier.ts publish, guarded (only when present).
  • Extend the publish test to assert the fields appear on a matched publish; run backend typecheck (npx tsc --noEmit in backend/, 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.tsOrderStatusUpdate interface + order:status handler call site.
  • strykr-fe/src/lib/orderStatus.tsclassifyOrderStatusToast.
  • Test: strykr-fe/src/lib/orderStatus.test.mjs.

Steps:

  • Add optional fields to useWebSocket.ts OrderStatusUpdate: selectionName?, betType? ('back'|'lay'), matchedOdds?: number, matchedStake?: number, stake?: number.
  • Change classifyOrderStatusToast signature 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 && matchedOdds present → { 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_matched branch: compute pct only if matchedStake && stake > 0Math.round(matchedStake/stake*100). Title Bet Partially Matched (${pct}%) when pct known, else Bet Partially Matched. Body = same Side "sel" @ odds when present, else omitted. kind:'warning'.
    • Q3 branches (declined/lapsed/cancel-terminal/void) unchanged.
  • Update the useWebSocket.ts call 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_matched with fields → title has % + body; (d) partially_matched without 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 + tsc clean.
  • No fabricated financial values anywhere (grep the diff for ?? 0, || 0, || '' on the new fields).