feat(fifa17): cut over FUT economy authority to Rust

The economy authority barrier. `handle_with_ip` now dispatches every
economy-touching route to Rust/Core via `try_handle_economy` BEFORE consulting
`classify()`, so a migrated route can never also reach the Python passthrough
(NEVER BOTH). With economy services wired (production `from_config`) an economy
route ALWAYS returns Some — fail-closed 503 on any Core error — so there is no
Python economy fallback. `userMassInfo` stays a hybrid by design: Python
supplies the non-economy envelope; Rust overlays BOTH the squad and the economy
fields (coins + unopened-pack count from Core), so no stale Python economy value
is visible.

Routing only — no handler/test changes buried here. Routes now Rust-owned:
/user/credits, /store/purchasegroup, /store/transaction, POST+GET /purchased,
PUT /item, item DELETE forms, /match (ut/delete), /auctionhouse, /tradePile,
/trade, ut/delete trade; plus the userMassInfo economy overlay.
This commit is contained in:
OpenFUT Agent
2026-08-13 22:39:36 +00:00
parent 3ba24a0faf
commit 93a46d4de7
+38 -3
View File
@@ -2084,6 +2084,21 @@ impl Server {
client_ip: Option<&str>,
) -> WireResponse {
let path = target.split('?').next().unwrap_or(target);
// ─────────────────────── Economy authority barrier ───────────────────
// Every economy-touching route is owned by Rust/Core. Classified and
// dispatched HERE, before `classify()`, so a migrated route can NEVER also
// reach the Python passthrough (NEVER BOTH). When the economy services are
// wired (production `from_config`), an economy route ALWAYS returns `Some`
// — fail-closed (503) on any Core error — so there is no Python economy
// fallback. `None` means "not an economy route" (or no economy wired, i.e.
// a bare test server), which falls through to the classifier below.
if let Some(resp) = self.try_handle_economy(method, target, headers, body, client_ip) {
eprintln!(
"utas-host owner=RUST route=economy method={} path={} status={}",
method, path, resp.status
);
return resp;
}
match classify(method, path) {
Route::Club => {
let query = target.split_once('?').map(|(_, q)| q).unwrap_or("");
@@ -2128,11 +2143,31 @@ impl Server {
}
Route::UserMassInfo => {
let deps = self.squad_deps();
let (resp, log) =
let (mut resp, log) =
handle_user_mass_info(method, target, headers, body, &deps, self.pass.as_ref());
// Overlay the authoritative Core economy (coins + unopened-pack
// count) so NO stale Python economy value is visible post-barrier.
// userMassInfo remains a hybrid by design: Python supplies the
// non-economy envelope; Rust owns the squad AND the economy fields.
let mut econ_overlaid = false;
if let Some(svc) = &self.economy {
if (200..300).contains(&resp.status) {
if let (Ok(coins), Ok(ents)) = (svc.econ.balance(), svc.econ.entitlements())
{
if let Ok(mut root) = serde_json::from_slice::<Value>(&resp.body) {
if overlay_massinfo_economy(&mut root, coins, ents.len()) {
if let Ok(nb) = serde_json::to_vec(&root) {
set_json_body(&mut resp, nb);
econ_overlaid = true;
}
}
}
}
}
}
eprintln!(
"utas-host owner=RUST_OVERLAY route=userMassInfo status={} squad_outcome={} detail=[{}]",
resp.status, log.outcome, log.detail
"utas-host owner=RUST_OVERLAY route=userMassInfo status={} squad_outcome={} econ_overlaid={} detail=[{}]",
resp.status, log.outcome, econ_overlaid, log.detail
);
resp
}