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.
|
/// Replace the active squad's canonical slots + opaque extension atomically.
|
||||||
fn replace_squad(&self, req: &CoreReplaceRequest) -> Result<CoreReplaceResult, CoreError>;
|
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> {
|
fn list_sbcs(&self) -> Result<Vec<CoreSbcDefinition>, CoreError> {
|
||||||
Err(CoreError::Parse(
|
Err(CoreError::Parse(
|
||||||
"Core SBC access is not implemented".into(),
|
"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> {
|
fn list_sbcs(&self) -> Result<Vec<CoreSbcDefinition>, CoreError> {
|
||||||
let response = self
|
let response = self
|
||||||
.client
|
.client
|
||||||
@@ -1839,6 +1892,15 @@ fn project_active_squad(deps: &SquadDeps<'_>) -> HostProjection {
|
|||||||
is_on_bench: s.is_on_bench,
|
is_on_bench: s.is_on_bench,
|
||||||
})
|
})
|
||||||
.collect();
|
.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 {
|
let input = SquadProjectionInput {
|
||||||
fifa_squad_id: ACTIVE_SQUAD_WIRE_ID,
|
fifa_squad_id: ACTIVE_SQUAD_WIRE_ID,
|
||||||
name: read.name,
|
name: read.name,
|
||||||
@@ -1846,6 +1908,7 @@ fn project_active_squad(deps: &SquadDeps<'_>) -> HostProjection {
|
|||||||
slots,
|
slots,
|
||||||
ext: SquadExtInput::Fresh(ext),
|
ext: SquadExtInput::Fresh(ext),
|
||||||
owned: &owned_by_id,
|
owned: &owned_by_id,
|
||||||
|
manager,
|
||||||
};
|
};
|
||||||
match project_squad(&input, deps.resolver, deps.entities) {
|
match project_squad(&input, deps.resolver, deps.entities) {
|
||||||
Ok(SquadProjection::Projected(v)) => HostProjection::Squad(v),
|
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.
|
// Commit canonical + extension atomically. No Python fallback on failure.
|
||||||
let req = CoreReplaceRequest {
|
let req = CoreReplaceRequest {
|
||||||
name: build.canonical.name.clone(),
|
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_schema_version: EXT_SCHEMA_VERSION,
|
||||||
ext_payload: build.extension.to_payload(),
|
ext_payload: build.extension.to_payload(),
|
||||||
};
|
};
|
||||||
match deps.core.replace_squad(&req) {
|
if let Err(e) = deps.core.replace_squad(&req) {
|
||||||
Ok(_) => (
|
return (
|
||||||
json_response(&save_ack(put.id)),
|
|
||||||
SquadLog {
|
|
||||||
outcome: "ok",
|
|
||||||
detail: String::new(),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Err(e) => (
|
|
||||||
error_response(502, "core_error"),
|
error_response(502, "core_error"),
|
||||||
SquadLog {
|
SquadLog {
|
||||||
outcome: "core_error",
|
outcome: "core_error",
|
||||||
detail: e.to_string(),
|
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
|
/// `GET …/squad/list` — served from Core + the ONE projector. Stale/Missing are
|
||||||
|
|||||||
Reference in New Issue
Block a user