fix(bridge): submit matches to Core's exactly-once completion route

Core removed `POST /matches/result` as an economy path: it had no transaction
and no idempotency key, so it re-credited the same match on every call. The two
EA result routes and the dashboard now target `POST /matches/complete`.

The dashboard mints a fresh `match_identity` per submission — each click is a
distinct match — and opts into `expire_loans` / `advance_season`, which the old
route used to trigger implicitly. The mapper entries and the endpoint map record
that a body must carry `match_identity`; those EA mappings were already marked
"Needs capture", so the body shape stays unverified either way.
This commit is contained in:
funman300
2026-08-21 04:48:07 +00:00
parent c58e7326a1
commit 07e83fe36c
3 changed files with 18 additions and 7 deletions
+1 -1
View File
@@ -48,7 +48,7 @@ This document maps confirmed or suspected FIFA 23 FUT API endpoints to their Ope
| FUT Endpoint | Core Endpoint | Status | Notes |
|---|---|---|---|
| Unknown | `POST /matches/result` | ❌ | Needs capture |
| Unknown | `POST /matches/complete` | ❌ | Needs capture. Body must carry a `match_identity` (exactly-once key). `POST /matches/result` was removed — no transaction, no idempotency key. |
## Objectives
+9 -1
View File
@@ -1651,12 +1651,20 @@ async function submitMatch() {
const opponentName = currentOpponent?.opponent_name ?? `${diff.replace('_',' ')} Bot`;
try {
const r = await api('POST', '/matches/result', {
// Each click is a distinct match, so it mints its own identity: the
// exactly-once route keys idempotency on it, and the old /matches/result
// path (no transaction, no identity) is closed. The dashboard drives Core's
// own match mode, so it opts into Core loan expiry and season progression.
const r = await api('POST', '/matches/complete', {
match_identity: `dashboard-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`,
result: gf > ga ? 'win' : gf === ga ? 'draw' : 'loss',
squad_id: 'dashboard',
opponent_name: opponentName,
goals_for: gf,
goals_against: ga,
mode: 'squad_battles',
expire_loans: true,
advance_season: true,
});
const outcome = gf > ga ? 'Win' : gf === ga ? 'Draw' : 'Loss';
const outcomeColor = gf > ga ? '#3fb950' : gf === ga ? '#8b949e' : '#f85149';
+8 -5
View File
@@ -157,8 +157,10 @@ const EXACT: &[ExactRoute] = &[
},
ExactRoute {
ea_method: "POST", ea_path: "/ut/game/fut/result",
core_method: "POST", core_path: "/matches/result",
notes: "FUT match result submit → Core match result",
core_method: "POST", core_path: "/matches/complete",
notes: "FUT match result submit → Core exactly-once match completion. \
Core requires a match_identity on the body; /matches/result was \
removed because it had no transaction and no idempotency key.",
},
ExactRoute {
ea_method: "GET", ea_path: "/ut/game/fut/matches",
@@ -301,8 +303,9 @@ const EXACT: &[ExactRoute] = &[
},
ExactRoute {
ea_method: "POST", ea_path: "/ut/game/fut/rivals/result",
core_method: "POST", core_path: "/matches/result",
notes: "FUT rivals match result → Core match result",
core_method: "POST", core_path: "/matches/complete",
notes: "FUT rivals match result → Core exactly-once match completion \
(body must carry a match_identity).",
},
ExactRoute {
ea_method: "GET", ea_path: "/ut/game/fut/rivals/leaderboard",
@@ -783,7 +786,7 @@ mod tests {
fn test_rivals_result_maps() {
let m = map_to_core("POST", "/ut/game/fut/rivals/result");
assert!(m.is_some());
assert_eq!(m.unwrap().core_path, "/matches/result");
assert_eq!(m.unwrap().core_path, "/matches/complete");
}
#[test]