feat(host): wire ownership-backed squad manager through Core
Thread the manager assignment between the FIFA squad path and Core: - CoreAccess gains get_squad_manager/set_squad_manager (GET/PUT /club/manager); HttpCoreClient implements both. - project_active_squad fetches the assigned manager owned item and passes it to the projector (non-fatal on error/absence). - handle_put_squad authorizes the resolved manager against the active club (like a slot) and persists it via set_squad_manager after the atomic squad replace; fails loudly, never silently drops it.
This commit is contained in:
+100
-10
@@ -682,6 +682,24 @@ pub trait CoreAccess: Send + Sync {
|
||||
/// Replace the active squad's canonical slots + opaque extension atomically.
|
||||
fn replace_squad(&self, req: &CoreReplaceRequest) -> Result<CoreReplaceResult, CoreError>;
|
||||
|
||||
/// The owned instance id assigned as the active squad's **manager**, or
|
||||
/// `None` (`GET /club/manager`). Default: `None` — a transport without the
|
||||
/// endpoint simply projects no manager (non-fatal, like an absent
|
||||
/// assignment). The production `HttpCoreClient` overrides it.
|
||||
fn get_squad_manager(&self) -> Result<Option<String>, CoreError> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Assign (`Some`) or clear (`None`) the active squad's **manager**
|
||||
/// (`PUT /club/manager`). Default: unimplemented — the production
|
||||
/// `HttpCoreClient` overrides it; a transport that cannot persist the
|
||||
/// assignment MUST fail loudly rather than silently drop it.
|
||||
fn set_squad_manager(&self, _owned_card_id: Option<&str>) -> Result<(), CoreError> {
|
||||
Err(CoreError::Parse(
|
||||
"Core squad manager write is not implemented".into(),
|
||||
))
|
||||
}
|
||||
|
||||
fn list_sbcs(&self) -> Result<Vec<CoreSbcDefinition>, CoreError> {
|
||||
Err(CoreError::Parse(
|
||||
"Core SBC access is not implemented".into(),
|
||||
@@ -821,6 +839,41 @@ impl CoreAccess for HttpCoreClient {
|
||||
})
|
||||
}
|
||||
|
||||
fn get_squad_manager(&self) -> Result<Option<String>, CoreError> {
|
||||
let url = format!("{}/club/manager", self.base_url);
|
||||
let resp = self
|
||||
.client
|
||||
.get(&url)
|
||||
.header("X-OpenFUT-Game", &self.game)
|
||||
.send()
|
||||
.map_err(|e| CoreError::Http(e.to_string()))?;
|
||||
let status = resp.status().as_u16();
|
||||
if !(200..300).contains(&status) {
|
||||
return Err(CoreError::Status(status));
|
||||
}
|
||||
let v: Value = resp.json().map_err(|e| CoreError::Parse(e.to_string()))?;
|
||||
Ok(v.get("manager")
|
||||
.and_then(|m| m.get("owned_card_id"))
|
||||
.and_then(|x| x.as_str())
|
||||
.map(str::to_string))
|
||||
}
|
||||
|
||||
fn set_squad_manager(&self, owned_card_id: Option<&str>) -> Result<(), CoreError> {
|
||||
let url = format!("{}/club/manager", self.base_url);
|
||||
let resp = self
|
||||
.client
|
||||
.put(&url)
|
||||
.header("X-OpenFUT-Game", &self.game)
|
||||
.json(&json!({ "owned_card_id": owned_card_id }))
|
||||
.send()
|
||||
.map_err(|e| CoreError::Http(e.to_string()))?;
|
||||
let status = resp.status().as_u16();
|
||||
if !(200..300).contains(&status) {
|
||||
return Err(CoreError::Status(status));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn list_sbcs(&self) -> Result<Vec<CoreSbcDefinition>, CoreError> {
|
||||
let response = self
|
||||
.client
|
||||
@@ -1839,6 +1892,15 @@ fn project_active_squad(deps: &SquadDeps<'_>) -> HostProjection {
|
||||
is_on_bench: s.is_on_bench,
|
||||
})
|
||||
.collect();
|
||||
// The ownership-backed manager assignment (Core `squad_managers`). A fetch
|
||||
// error or absent endpoint yields no manager — non-fatal, a squad renders
|
||||
// without one, never fabricated.
|
||||
let manager = deps
|
||||
.core
|
||||
.get_squad_manager()
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|id| owned_by_id.get(&id).cloned());
|
||||
let input = SquadProjectionInput {
|
||||
fifa_squad_id: ACTIVE_SQUAD_WIRE_ID,
|
||||
name: read.name,
|
||||
@@ -1846,6 +1908,7 @@ fn project_active_squad(deps: &SquadDeps<'_>) -> HostProjection {
|
||||
slots,
|
||||
ext: SquadExtInput::Fresh(ext),
|
||||
owned: &owned_by_id,
|
||||
manager,
|
||||
};
|
||||
match project_squad(&input, deps.resolver, deps.entities) {
|
||||
Ok(SquadProjection::Projected(v)) => HostProjection::Squad(v),
|
||||
@@ -1932,6 +1995,18 @@ pub fn handle_put_squad(body: &[u8], deps: &SquadDeps<'_>) -> (WireResponse, Squ
|
||||
);
|
||||
}
|
||||
}
|
||||
// The manager is a resolved, owned assignment too: authorize it like a slot.
|
||||
if let Some(mgr) = &build.canonical.manager_owned_card_id {
|
||||
if !owned_set.contains(mgr) {
|
||||
return (
|
||||
error_response(403, "not_owned"),
|
||||
SquadLog {
|
||||
outcome: "unauthorized_manager",
|
||||
detail: mgr.clone(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
// Commit canonical + extension atomically. No Python fallback on failure.
|
||||
let req = CoreReplaceRequest {
|
||||
name: build.canonical.name.clone(),
|
||||
@@ -1956,22 +2031,37 @@ pub fn handle_put_squad(body: &[u8], deps: &SquadDeps<'_>) -> (WireResponse, Squ
|
||||
ext_schema_version: EXT_SCHEMA_VERSION,
|
||||
ext_payload: build.extension.to_payload(),
|
||||
};
|
||||
match deps.core.replace_squad(&req) {
|
||||
Ok(_) => (
|
||||
json_response(&save_ack(put.id)),
|
||||
SquadLog {
|
||||
outcome: "ok",
|
||||
detail: String::new(),
|
||||
},
|
||||
),
|
||||
Err(e) => (
|
||||
if let Err(e) = deps.core.replace_squad(&req) {
|
||||
return (
|
||||
error_response(502, "core_error"),
|
||||
SquadLog {
|
||||
outcome: "core_error",
|
||||
detail: e.to_string(),
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
// Persist the ownership-backed manager assignment (migration 0023). It was
|
||||
// authorized above and Core re-validates club ownership; fail loudly on a
|
||||
// transport error rather than silently dropping the manager.
|
||||
if let Err(e) = deps
|
||||
.core
|
||||
.set_squad_manager(build.canonical.manager_owned_card_id.as_deref())
|
||||
{
|
||||
return (
|
||||
error_response(502, "core_error"),
|
||||
SquadLog {
|
||||
outcome: "manager_error",
|
||||
detail: e.to_string(),
|
||||
},
|
||||
);
|
||||
}
|
||||
(
|
||||
json_response(&save_ack(put.id)),
|
||||
SquadLog {
|
||||
outcome: "ok",
|
||||
detail: String::new(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// `GET …/squad/list` — served from Core + the ONE projector. Stale/Missing are
|
||||
|
||||
Reference in New Issue
Block a user