funman300 31ab4a683e feat(economy): atomic market sale settlement — transfer ownership, credit seller, destroy fee
Core had no way to settle a real sale. Every "buy" MINTED a new owned_cards row
(services/economy.rs::purchase_item), so a sold card existed twice; the seller was
never credited; no fee arithmetic existed anywhere in the project; and no
/economy/* route could even name a counterparty, since all eight resolve one club
from the X-OpenFUT-Game active profile.

Adds settle_sale() beside the existing tx-scoped primitives, so it inherits the
module's proven atomicity (pool.acquire + BEGIN IMMEDIATE + finish) rather than
re-deriving it: debit buyer gross -> evict from squads -> transfer the EXISTING row
-> credit seller gross-fee. The fee is simply never credited anywhere, which is
what destroys it. Exposed as POST /economy/settle-sale, the one economy route that
names clubs explicitly because a sale has two sides; an omitted buyer means a
counterparty OUTSIDE the modelled economy, never a silent fallback to the active
club (which would settle a club against itself).

Ownership moves by UPDATE ... WHERE id = ? AND club_id = ?, an ownership CAS. No
INSERT and no DELETE on the two-party path, so duplication is ruled out
structurally, not by an assertion.

Two bugs found by writing the tests rather than by reading the code:

1. Deriving the seller from CURRENT ownership let two racing buyers BOTH succeed —
   after the first sale the item belonged to B, so the second call read B as the
   seller and chain-sold it B -> C. Core has no listing concept and could not
   notice the replay. The seller is now the caller's EXPECTED owner and every
   ownership statement is predicated on it, which makes the CAS authoritative about
   "already sold" independently of caller-side listing state.

2. Debiting before transferring made a replay fail as "insufficient balance" (the
   buyer had spent the coins on the sale that succeeded), so the ownership guard was
   shadowed and settling_the_same_sale_twice_pays_once passed WITHOUT exercising
   the guard it named. Ownership is now judged first; the test asserts NotFound
   specifically and adds a cheap affordable replay that only ownership can refuse.

Squad eviction is mandatory, not cosmetic: squad_players.owned_card_id is a FK and
the pool enables foreign_keys, so an Outside sale of a squadded card would fail
outright, and a transfer preserves the row id so a stale lineup row would leave the
PREVIOUS owner fielding a card they no longer own.

Tests: 21 service (canonical 15,000/750/14,250 fixture, conservation, squad
eviction, Outside retirement, 8 invalid paths, zero-price, replay, two-buyer race)
+ 6 route-level over HTTP with two parties. Core 194 pass.

Deliberately NOT done: no wire/route output change, no deployment, and nothing yet
decides that a player's listing has sold — the seller-facing sold wire state needs
live client evidence and must not be guessed.
2026-08-18 00:51:20 +00:00
2026-06-25 14:54:51 -07:00
2026-06-25 14:54:51 -07:00
2026-06-25 14:54:51 -07:00
2026-06-25 14:54:51 -07:00

OpenFUT Core

Offline Ultimate Team backend — game-independent.

OpenFUT Core is the heart of the OpenFUT project: a fully offline, single-player FUT-style backend written in Rust. It is deliberately decoupled from any specific game, though it is designed to power a FIFA 23 offline experience.


What it does

  • Creates and manages local player profiles and clubs
  • Manages coins, XP, and progression
  • Generates packs from weighted JSON definitions
  • Tracks your full card collection
  • Squad builder with formations and chemistry (chemistry calculations: WIP)
  • Objectives engine (daily, weekly, lifetime, milestone)
  • SBC (Squad Building Challenge) engine with JSON-defined challenges
  • Match result processing with coin and XP rewards
  • NPC transfer market with daily refreshes
  • Statistics tracking
  • Fully moddable via JSON data files

Tech Stack

  • Rust + Axum (HTTP framework)
  • Tokio (async runtime)
  • SQLite + SQLx (database + migrations)
  • Serde (JSON data layer)
  • tower-http (middleware: CORS, tracing)

Quick Start

# Build
cargo build --release

# Run (creates openfut.db in current directory)
./target/release/openfut-core

# Or with custom config
DATABASE_URL=sqlite://./myclub.db LISTEN_ADDR=127.0.0.1:8080 ./target/release/openfut-core

Environment Variables

Variable Default Description
LISTEN_ADDR 127.0.0.1:8080 Address to listen on
DATABASE_URL sqlite://openfut.db SQLite database path
DATA_DIR data Path to JSON data files

API Routes

Method Path Description
GET /health Health check
POST /auth/local Create first-run profile + club
GET /profile Get active profile
GET /club Get active club with coins
GET /cards Browse all card definitions
GET /collection Get owned cards
GET /packs List unopened packs
POST /packs/open/:pack_id Open a pack
GET /squad Get active squad
POST /squad Save squad
GET /objectives List objectives with progress
POST /matches/result Submit match result + receive rewards
GET /sbc List SBC definitions
POST /sbc/submit Submit SBC solution
GET /market Browse NPC transfer market
POST /market/buy Buy listing
POST /market/sell Quick-sell card
POST /market/refresh Refresh NPC listings
GET /statistics Get match/pack/SBC stats

First Run

# Create your profile
curl -X POST http://localhost:8080/auth/local \
  -H 'Content-Type: application/json' \
  -d '{"username": "Player 1"}'

# Check your club (5000 coins + a gold pack waiting)
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 \
  -H 'Content-Type: application/json' \
  -d '{"squad_id":"any","opponent_name":"Beginner AI","goals_for":3,"goals_against":0,"mode":"squad_battles"}'

Modding

All game content lives in data/. Drop JSON files into the appropriate folder and restart.

data/
  cards/      ← CardDefinition[]
  packs/      ← PackDefinition[]
  objectives/ ← ObjectiveDefinition[]
  sbcs/       ← SbcDefinition[]
  events/     ← (future)

See docs/modding.md for schema reference.


Development

cargo fmt
cargo clippy -- -D warnings
cargo test

License

MIT — see LICENSE

S
Description
Offline Ultimate Team backend — game-independent core
Readme 634 MiB
Languages
Rust 100%