fix(matches): close the second, unguarded match-economy authority
CI / Build, lint & test (push) Successful in 3m15s
CI / Build, lint & test (push) Successful in 3m15s
`POST /matches/result` granted coins, XP, level-ups, statistics, four objective metrics, loan expiry, season progression and achievements across a dozen SEPARATE writes with no transaction and no idempotency key. Every call re-credited the same match, and any mid-way failure half-applied it. It sat beside `/matches/complete`, so nothing stopped one match being paid twice through two different doors. It cannot be made exactly-once in place: that needs a caller-supplied match identity, and this request shape has none. Deriving one from the body would collapse two legitimate matches with the same scoreline into one — the under-credit trap already documented for the `fp:` fallback. So the route fails closed: it rejects with a message naming `/matches/complete`, rather than 404, so a caller learns why. The behaviour it uniquely drove is kept, not deleted. `process_match` was the ONLY caller of loan expiry and Core's season model, so both move into `complete_match`'s transaction behind opt-in `expire_loans` / `advance_season` flags. Both default OFF, which keeps the FIFA 17 retail path byte-identical: FIFA 17 has its own loan and Seasons models, and Core's season END GRANTS coins and a pack — invisible economy on a path that never asked for it. Their pooled implementations are replaced by `expire_loans_tx` and `season::record_match_tx`, so a loan that expires or a season that ends commits with the match that caused it. Notifications (level-up / objective / loan / season) were pooled side effects of the removed path. They now emit from the route AFTER the commit — never inside the transaction, since a failed notification must not roll back a completed match — and only when `applied`, so a replay no longer re-notifies. The pooled path had no replay concept and notified every time. Also fixes a real bug this surfaced: `/auth/reset` never deleted `match_completions`, which carries un-cascaded foreign keys to BOTH `matches` and `profiles`. Any profile that completed a match through the authoritative route — i.e. every FIFA 17 profile after a retail match — failed to reset with a database error. It is now deleted first, and ordering is documented. Tests: the 20 integration call sites move to the authoritative route through one helper that mints a per-call identity (each call IS a distinct match). New coverage for the closed path: it rejects without moving the balance or writing history; Core progression stays off unless opted into; a replay does not duplicate notifications; and a profile that completed matches can still be reset.
This commit is contained in:
@@ -70,7 +70,7 @@ DATABASE_URL=sqlite://./myclub.db LISTEN_ADDR=127.0.0.1:8080 ./target/release/op
|
||||
| `GET` | `/squad` | Get active squad |
|
||||
| `POST` | `/squad` | Save squad |
|
||||
| `GET` | `/objectives` | List objectives with progress |
|
||||
| `POST` | `/matches/result` | Submit match result + receive rewards |
|
||||
| `POST` | `/matches/complete` | Complete a match exactly once + receive rewards |
|
||||
| `GET` | `/sbc` | List SBC definitions |
|
||||
| `POST` | `/sbc/submit` | Submit SBC solution |
|
||||
| `GET` | `/market` | Browse NPC transfer market |
|
||||
@@ -95,10 +95,11 @@ curl http://localhost:8080/club
|
||||
# Open your starter pack
|
||||
curl -X POST http://localhost:8080/packs/open/<pack_id>
|
||||
|
||||
# Submit a match win
|
||||
curl -X POST http://localhost:8080/matches/result \
|
||||
# Submit a match win. `match_identity` keys exactly-once economy: resubmitting the
|
||||
# same identity echoes the first result and grants nothing twice.
|
||||
curl -X POST http://localhost:8080/matches/complete \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"squad_id":"any","opponent_name":"Beginner AI","goals_for":3,"goals_against":0,"mode":"squad_battles"}'
|
||||
-d '{"match_identity":"match-1","result":"win","squad_id":"any","opponent_name":"Beginner AI","goals_for":3,"goals_against":0,"mode":"squad_battles"}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user