add fixture-backed admin and ios scaffold
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
pub use crate::events::{
|
||||
EventManifestSnapshot, EventMediaSnapshot, EventSnapshot, MarketSnapshot, OutcomeSnapshot,
|
||||
};
|
||||
pub use crate::markets::{OddsVersionSnapshot, OutcomeOddsSnapshot};
|
||||
pub use crate::settlement::SettlementSnapshot;
|
||||
@@ -9,10 +9,9 @@ pub use crate::analytics::{AnalyticsBatchRequest, AnalyticsEventInput, Attribute
|
||||
pub use crate::bets::{BetIntentRecord, BetIntentRequest, BetIntentResponse};
|
||||
pub use crate::events::{EventManifestSnapshot, EventSnapshot};
|
||||
pub use crate::events::{MarketSnapshot, OutcomeSnapshot};
|
||||
pub use crate::admin::{EventMediaSnapshot, OddsVersionSnapshot, OutcomeOddsSnapshot, SettlementSnapshot};
|
||||
pub use crate::experiments::ExperimentConfigSnapshot;
|
||||
pub use crate::localization::LocalizationBundleSnapshot;
|
||||
pub use crate::markets::{OddsVersionSnapshot, OutcomeOddsSnapshot};
|
||||
pub use crate::settlement::SettlementSnapshot;
|
||||
pub use crate::sessions::{SessionSnapshot, SessionStartRequest};
|
||||
|
||||
use crate::{
|
||||
@@ -257,6 +256,54 @@ impl AppState {
|
||||
.map_err(|_| AppError::not_found("No active session"))
|
||||
}
|
||||
|
||||
pub async fn admin_create_event_manifest(
|
||||
&self,
|
||||
manifest: EventManifestSnapshot,
|
||||
) -> Result<EventManifestSnapshot, AppError> {
|
||||
let created = self.events.insert_manifest(manifest.clone()).await;
|
||||
|
||||
for market in &created.markets {
|
||||
self.markets.insert_market(market.clone()).await;
|
||||
}
|
||||
|
||||
Ok(created)
|
||||
}
|
||||
|
||||
pub async fn admin_create_market(
|
||||
&self,
|
||||
market: MarketSnapshot,
|
||||
) -> Result<MarketSnapshot, AppError> {
|
||||
let Some(_) = self.event(market.event_id).await else {
|
||||
return Err(AppError::not_found("Event not found"));
|
||||
};
|
||||
|
||||
self.markets.insert_market(market.clone()).await;
|
||||
let _ = self.events.insert_market(market.event_id, market.clone()).await;
|
||||
Ok(market)
|
||||
}
|
||||
|
||||
pub async fn admin_publish_odds(
|
||||
&self,
|
||||
odds: OddsVersionSnapshot,
|
||||
) -> Result<OddsVersionSnapshot, AppError> {
|
||||
let Some(_) = self.markets.market(odds.market_id).await else {
|
||||
return Err(AppError::not_found("Market not found"));
|
||||
};
|
||||
|
||||
Ok(self.markets.publish_odds(odds).await)
|
||||
}
|
||||
|
||||
pub async fn admin_publish_settlement(
|
||||
&self,
|
||||
settlement: SettlementSnapshot,
|
||||
) -> Result<SettlementSnapshot, AppError> {
|
||||
let Some(market) = self.markets.market(settlement.market_id).await else {
|
||||
return Err(AppError::not_found("Market not found"));
|
||||
};
|
||||
|
||||
Ok(self.settlements.upsert(market.event_id, settlement).await)
|
||||
}
|
||||
|
||||
pub async fn next_event(&self) -> Option<EventSnapshot> {
|
||||
self.events.next_event().await
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
use serde_json as json;
|
||||
|
||||
fn main() {
|
||||
let fixture = hermes_backend::fixtures::build_sample_study_fixture();
|
||||
let output = json::to_string_pretty(&fixture).expect("valid test event fixture");
|
||||
println!("{output}");
|
||||
}
|
||||
@@ -5,6 +5,8 @@ use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::RwLock;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::fixtures::SampleStudyFixture;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct EventSnapshot {
|
||||
pub id: Uuid,
|
||||
@@ -76,9 +78,22 @@ struct EventsState {
|
||||
|
||||
impl EventsStore {
|
||||
pub fn new() -> Self {
|
||||
Self::with_sample_data()
|
||||
Self::from_fixture(&crate::fixtures::load_sample_study_fixture())
|
||||
}
|
||||
|
||||
pub fn from_fixture(fixture: &SampleStudyFixture) -> Self {
|
||||
let mut manifests_by_event_id = HashMap::new();
|
||||
manifests_by_event_id.insert(fixture.manifest.event.id, fixture.manifest.clone());
|
||||
|
||||
Self {
|
||||
inner: Arc::new(RwLock::new(EventsState {
|
||||
feed_order: vec![fixture.manifest.event.id],
|
||||
manifests_by_event_id,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code, unused_variables)]
|
||||
pub fn with_sample_data() -> Self {
|
||||
let event_id = Uuid::parse_str("11111111-1111-1111-1111-111111111111").expect("valid uuid");
|
||||
let market_id = Uuid::parse_str("22222222-2222-2222-2222-222222222222").expect("valid uuid");
|
||||
@@ -152,12 +167,32 @@ impl EventsStore {
|
||||
let mut manifests_by_event_id = HashMap::new();
|
||||
manifests_by_event_id.insert(event_id, manifest);
|
||||
|
||||
Self {
|
||||
inner: Arc::new(RwLock::new(EventsState {
|
||||
feed_order: vec![event_id],
|
||||
manifests_by_event_id,
|
||||
})),
|
||||
Self::from_fixture(&crate::fixtures::load_sample_study_fixture())
|
||||
}
|
||||
|
||||
pub async fn insert_manifest(&self, manifest: EventManifestSnapshot) -> EventManifestSnapshot {
|
||||
let mut state = self.inner.write().await;
|
||||
let event_id = manifest.event.id;
|
||||
if !state.manifests_by_event_id.contains_key(&event_id) {
|
||||
state.feed_order.push(event_id);
|
||||
}
|
||||
state.manifests_by_event_id.insert(event_id, manifest.clone());
|
||||
manifest
|
||||
}
|
||||
|
||||
pub async fn insert_market(&self, event_id: Uuid, market: MarketSnapshot) -> bool {
|
||||
let mut state = self.inner.write().await;
|
||||
let Some(manifest) = state.manifests_by_event_id.get_mut(&event_id) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let Some(existing) = manifest.markets.iter_mut().find(|existing| existing.id == market.id) else {
|
||||
manifest.markets.push(market);
|
||||
return true;
|
||||
};
|
||||
|
||||
*existing = market;
|
||||
true
|
||||
}
|
||||
|
||||
pub async fn next_event(&self) -> Option<EventSnapshot> {
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json as json;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
events::{
|
||||
EventManifestSnapshot, EventMediaSnapshot, EventSnapshot, MarketSnapshot, OutcomeSnapshot,
|
||||
},
|
||||
markets::{OddsVersionSnapshot, OutcomeOddsSnapshot},
|
||||
settlement::SettlementSnapshot,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct SettlementFixture {
|
||||
pub event_id: Uuid,
|
||||
pub settlement: SettlementSnapshot,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct SampleStudyFixture {
|
||||
pub manifest: EventManifestSnapshot,
|
||||
pub odds_versions: Vec<OddsVersionSnapshot>,
|
||||
pub settlement: SettlementFixture,
|
||||
}
|
||||
|
||||
pub fn load_sample_study_fixture() -> SampleStudyFixture {
|
||||
let raw = include_str!("../../../fixtures/manifests/sample_event.json");
|
||||
json::from_str(raw).expect("valid sample study fixture")
|
||||
}
|
||||
|
||||
pub fn build_sample_study_fixture() -> SampleStudyFixture {
|
||||
let event_id = Uuid::parse_str("11111111-1111-1111-1111-111111111111").expect("valid uuid");
|
||||
let market_id = Uuid::parse_str("22222222-2222-2222-2222-222222222222").expect("valid uuid");
|
||||
let odds_version_id =
|
||||
Uuid::parse_str("66666666-6666-6666-6666-666666666666").expect("valid uuid");
|
||||
let lock_at = parse_ts("2099-01-01T00:10:00Z");
|
||||
let settle_at = parse_ts("2099-01-01T00:25:00Z");
|
||||
let created_at = parse_ts("2099-01-01T00:00:00Z");
|
||||
|
||||
let home_outcome_id =
|
||||
Uuid::parse_str("44444444-4444-4444-4444-444444444444").expect("valid uuid");
|
||||
let away_outcome_id =
|
||||
Uuid::parse_str("55555555-5555-5555-5555-555555555555").expect("valid uuid");
|
||||
|
||||
let event = EventSnapshot {
|
||||
id: event_id,
|
||||
sport_type: "football".to_string(),
|
||||
source_ref: "sample-event-001".to_string(),
|
||||
title_en: "Late winner chance".to_string(),
|
||||
title_sv: "Möjlighet till segermål".to_string(),
|
||||
status: "prefetch_ready".to_string(),
|
||||
preview_start_ms: 0,
|
||||
preview_end_ms: 45_000,
|
||||
reveal_start_ms: 50_000,
|
||||
reveal_end_ms: 90_000,
|
||||
lock_at,
|
||||
settle_at,
|
||||
};
|
||||
|
||||
let media = EventMediaSnapshot {
|
||||
id: Uuid::parse_str("33333333-3333-3333-3333-333333333333").expect("valid uuid"),
|
||||
event_id,
|
||||
media_type: "hls_main".to_string(),
|
||||
hls_master_url: "https://cdn.example.com/hermes/sample-event/master.m3u8".to_string(),
|
||||
poster_url: Some("https://cdn.example.com/hermes/sample-event/poster.jpg".to_string()),
|
||||
duration_ms: 90_000,
|
||||
preview_start_ms: 0,
|
||||
preview_end_ms: 45_000,
|
||||
reveal_start_ms: 50_000,
|
||||
reveal_end_ms: 90_000,
|
||||
};
|
||||
|
||||
let outcomes = vec![
|
||||
OutcomeSnapshot {
|
||||
id: home_outcome_id,
|
||||
market_id,
|
||||
outcome_code: "home".to_string(),
|
||||
label_key: "outcome.home".to_string(),
|
||||
sort_order: 1,
|
||||
},
|
||||
OutcomeSnapshot {
|
||||
id: away_outcome_id,
|
||||
market_id,
|
||||
outcome_code: "away".to_string(),
|
||||
label_key: "outcome.away".to_string(),
|
||||
sort_order: 2,
|
||||
},
|
||||
];
|
||||
|
||||
let market = MarketSnapshot {
|
||||
id: market_id,
|
||||
event_id,
|
||||
question_key: "market.sample.winner".to_string(),
|
||||
market_type: "winner".to_string(),
|
||||
status: "open".to_string(),
|
||||
lock_at,
|
||||
settlement_rule_key: "settle_on_match_winner".to_string(),
|
||||
outcomes,
|
||||
};
|
||||
|
||||
let odds_version = OddsVersionSnapshot {
|
||||
id: odds_version_id,
|
||||
market_id,
|
||||
version_no: 1,
|
||||
created_at,
|
||||
is_current: true,
|
||||
odds: vec![
|
||||
OutcomeOddsSnapshot {
|
||||
id: Uuid::parse_str("77777777-7777-7777-7777-777777777777").expect("valid uuid"),
|
||||
odds_version_id,
|
||||
outcome_id: home_outcome_id,
|
||||
decimal_odds: 1.85,
|
||||
fractional_num: 17,
|
||||
fractional_den: 20,
|
||||
},
|
||||
OutcomeOddsSnapshot {
|
||||
id: Uuid::parse_str("88888888-8888-8888-8888-888888888888").expect("valid uuid"),
|
||||
odds_version_id,
|
||||
outcome_id: away_outcome_id,
|
||||
decimal_odds: 2.05,
|
||||
fractional_num: 21,
|
||||
fractional_den: 20,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
let settlement = SettlementFixture {
|
||||
event_id,
|
||||
settlement: SettlementSnapshot {
|
||||
id: Uuid::parse_str("99999999-9999-9999-9999-999999999999").expect("valid uuid"),
|
||||
market_id,
|
||||
settled_at: settle_at,
|
||||
winning_outcome_id: home_outcome_id,
|
||||
},
|
||||
};
|
||||
|
||||
SampleStudyFixture {
|
||||
manifest: EventManifestSnapshot {
|
||||
event,
|
||||
media: vec![media],
|
||||
markets: vec![market],
|
||||
},
|
||||
odds_versions: vec![odds_version],
|
||||
settlement,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_ts(value: &str) -> DateTime<Utc> {
|
||||
DateTime::parse_from_rfc3339(value)
|
||||
.expect("valid timestamp")
|
||||
.with_timezone(&Utc)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
pub mod analytics;
|
||||
pub mod admin;
|
||||
pub mod bets;
|
||||
pub mod app_state;
|
||||
pub mod config;
|
||||
@@ -8,6 +9,7 @@ pub mod db;
|
||||
pub mod events;
|
||||
pub mod error;
|
||||
pub mod experiments;
|
||||
pub mod fixtures;
|
||||
pub mod localization;
|
||||
pub mod markets;
|
||||
pub mod settlement;
|
||||
|
||||
@@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::RwLock;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::fixtures::SampleStudyFixture;
|
||||
use crate::events::{MarketSnapshot, OutcomeSnapshot};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
@@ -42,9 +43,46 @@ struct MarketsState {
|
||||
|
||||
impl MarketsStore {
|
||||
pub fn new() -> Self {
|
||||
Self::with_sample_data()
|
||||
Self::from_fixture(&crate::fixtures::load_sample_study_fixture())
|
||||
}
|
||||
|
||||
pub fn from_fixture(fixture: &SampleStudyFixture) -> Self {
|
||||
let mut markets_by_event_id: HashMap<Uuid, Vec<MarketSnapshot>> = HashMap::new();
|
||||
let mut markets_by_id = HashMap::new();
|
||||
let mut outcomes_by_id = HashMap::new();
|
||||
let mut current_odds_by_market_id = HashMap::new();
|
||||
|
||||
for market_fixture in &fixture.manifest.markets {
|
||||
let market = market_fixture.clone();
|
||||
markets_by_event_id.entry(market.event_id).or_default().push(market.clone());
|
||||
markets_by_id.insert(market.id, market.clone());
|
||||
|
||||
for outcome in &market.outcomes {
|
||||
outcomes_by_id.insert(outcome.id, outcome.clone());
|
||||
}
|
||||
|
||||
let Some(odds_version) = fixture
|
||||
.odds_versions
|
||||
.iter()
|
||||
.find(|odds_version| odds_version.market_id == market.id)
|
||||
.cloned() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
current_odds_by_market_id.insert(market.id, odds_version);
|
||||
}
|
||||
|
||||
Self {
|
||||
inner: Arc::new(RwLock::new(MarketsState {
|
||||
markets_by_event_id,
|
||||
markets_by_id,
|
||||
outcomes_by_id,
|
||||
current_odds_by_market_id,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code, unused_variables)]
|
||||
pub fn with_sample_data() -> Self {
|
||||
let event_id = Uuid::parse_str("11111111-1111-1111-1111-111111111111").expect("valid uuid");
|
||||
let market_id = Uuid::parse_str("22222222-2222-2222-2222-222222222222").expect("valid uuid");
|
||||
@@ -123,14 +161,35 @@ impl MarketsStore {
|
||||
let mut current_odds_by_market_id = HashMap::new();
|
||||
current_odds_by_market_id.insert(market_id, odds_version);
|
||||
|
||||
Self {
|
||||
inner: Arc::new(RwLock::new(MarketsState {
|
||||
markets_by_event_id,
|
||||
markets_by_id,
|
||||
outcomes_by_id,
|
||||
current_odds_by_market_id,
|
||||
})),
|
||||
Self::from_fixture(&crate::fixtures::load_sample_study_fixture())
|
||||
}
|
||||
|
||||
pub async fn insert_market(&self, market: MarketSnapshot) -> MarketSnapshot {
|
||||
let mut state = self.inner.write().await;
|
||||
|
||||
state
|
||||
.markets_by_event_id
|
||||
.entry(market.event_id)
|
||||
.or_default()
|
||||
.retain(|existing| existing.id != market.id);
|
||||
state
|
||||
.markets_by_event_id
|
||||
.entry(market.event_id)
|
||||
.or_default()
|
||||
.push(market.clone());
|
||||
state.markets_by_id.insert(market.id, market.clone());
|
||||
|
||||
for outcome in &market.outcomes {
|
||||
state.outcomes_by_id.insert(outcome.id, outcome.clone());
|
||||
}
|
||||
|
||||
market
|
||||
}
|
||||
|
||||
pub async fn publish_odds(&self, odds: OddsVersionSnapshot) -> OddsVersionSnapshot {
|
||||
let mut state = self.inner.write().await;
|
||||
state.current_odds_by_market_id.insert(odds.market_id, odds.clone());
|
||||
odds
|
||||
}
|
||||
|
||||
pub async fn markets_for_event(&self, event_id: Uuid) -> Option<Vec<MarketSnapshot>> {
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
use axum::{extract::{Extension, Json}, http::StatusCode};
|
||||
|
||||
use crate::{
|
||||
app_state::{AppState, EventManifestSnapshot, MarketSnapshot, OddsVersionSnapshot, SettlementSnapshot},
|
||||
error::AppError,
|
||||
};
|
||||
|
||||
pub async fn create_event(
|
||||
Extension(state): Extension<AppState>,
|
||||
Json(payload): Json<EventManifestSnapshot>,
|
||||
) -> Result<(StatusCode, Json<EventManifestSnapshot>), AppError> {
|
||||
let created = state.admin_create_event_manifest(payload).await?;
|
||||
Ok((StatusCode::CREATED, Json(created)))
|
||||
}
|
||||
|
||||
pub async fn create_market(
|
||||
Extension(state): Extension<AppState>,
|
||||
Json(payload): Json<MarketSnapshot>,
|
||||
) -> Result<(StatusCode, Json<MarketSnapshot>), AppError> {
|
||||
let created = state.admin_create_market(payload).await?;
|
||||
Ok((StatusCode::CREATED, Json(created)))
|
||||
}
|
||||
|
||||
pub async fn create_odds(
|
||||
Extension(state): Extension<AppState>,
|
||||
Json(payload): Json<OddsVersionSnapshot>,
|
||||
) -> Result<(StatusCode, Json<OddsVersionSnapshot>), AppError> {
|
||||
let created = state.admin_publish_odds(payload).await?;
|
||||
Ok((StatusCode::CREATED, Json(created)))
|
||||
}
|
||||
|
||||
pub async fn create_settlement(
|
||||
Extension(state): Extension<AppState>,
|
||||
Json(payload): Json<SettlementSnapshot>,
|
||||
) -> Result<(StatusCode, Json<SettlementSnapshot>), AppError> {
|
||||
let created = state.admin_publish_settlement(payload).await?;
|
||||
Ok((StatusCode::CREATED, Json(created)))
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod health;
|
||||
pub mod admin;
|
||||
pub mod analytics;
|
||||
pub mod bets;
|
||||
pub mod events;
|
||||
@@ -19,6 +20,10 @@ pub fn router() -> Router {
|
||||
.route("/api/v1/events/{event_id}/markets", get(markets::list_for_event))
|
||||
.route("/api/v1/events/{event_id}/result", get(results::show))
|
||||
.route("/api/v1/markets/{market_id}/odds/current", get(markets::current_odds))
|
||||
.route("/api/v1/admin/events", post(admin::create_event))
|
||||
.route("/api/v1/admin/markets", post(admin::create_market))
|
||||
.route("/api/v1/admin/odds", post(admin::create_odds))
|
||||
.route("/api/v1/admin/settlements", post(admin::create_settlement))
|
||||
.route("/api/v1/bets/intent", post(bets::submit))
|
||||
.route("/api/v1/bets/{bet_intent_id}", get(bets::show))
|
||||
.route("/api/v1/analytics/batch", post(analytics::batch))
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
use std::collections::HashMap;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use chrono::{DateTime, Duration as ChronoDuration, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::RwLock;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::fixtures::SampleStudyFixture;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct SettlementSnapshot {
|
||||
pub id: Uuid,
|
||||
@@ -14,15 +17,39 @@ pub struct SettlementSnapshot {
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct SettlementsStore {
|
||||
inner: Arc<RwLock<SettlementsState>>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct SettlementsState {
|
||||
settlements_by_event_id: HashMap<Uuid, SettlementSnapshot>,
|
||||
settlements_by_market_id: HashMap<Uuid, SettlementSnapshot>,
|
||||
}
|
||||
|
||||
impl SettlementsStore {
|
||||
pub fn new() -> Self {
|
||||
Self::with_sample_data()
|
||||
Self::from_fixture(&crate::fixtures::load_sample_study_fixture())
|
||||
}
|
||||
|
||||
pub fn from_fixture(fixture: &SampleStudyFixture) -> Self {
|
||||
let mut settlements_by_event_id = HashMap::new();
|
||||
settlements_by_event_id.insert(fixture.settlement.event_id, fixture.settlement.settlement.clone());
|
||||
|
||||
let mut settlements_by_market_id = HashMap::new();
|
||||
settlements_by_market_id.insert(
|
||||
fixture.settlement.settlement.market_id,
|
||||
fixture.settlement.settlement.clone(),
|
||||
);
|
||||
|
||||
Self {
|
||||
inner: Arc::new(RwLock::new(SettlementsState {
|
||||
settlements_by_event_id,
|
||||
settlements_by_market_id,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code, unused_variables)]
|
||||
pub fn with_sample_data() -> Self {
|
||||
let event_id = Uuid::parse_str("11111111-1111-1111-1111-111111111111").expect("valid uuid");
|
||||
let market_id = Uuid::parse_str("22222222-2222-2222-2222-222222222222").expect("valid uuid");
|
||||
@@ -41,23 +68,31 @@ impl SettlementsStore {
|
||||
let mut settlements_by_market_id = HashMap::new();
|
||||
settlements_by_market_id.insert(market_id, settlement.clone());
|
||||
|
||||
Self {
|
||||
settlements_by_event_id,
|
||||
settlements_by_market_id,
|
||||
}
|
||||
Self::from_fixture(&crate::fixtures::load_sample_study_fixture())
|
||||
}
|
||||
|
||||
pub async fn settlement_for_event(&self, event_id: Uuid) -> Option<SettlementSnapshot> {
|
||||
let Some(settlement) = self.settlements_by_event_id.get(&event_id) else {
|
||||
let state = self.inner.read().await;
|
||||
let Some(settlement) = state.settlements_by_event_id.get(&event_id) else {
|
||||
return None;
|
||||
};
|
||||
Some(settlement.clone())
|
||||
}
|
||||
|
||||
pub async fn settlement_for_market(&self, market_id: Uuid) -> Option<SettlementSnapshot> {
|
||||
let Some(settlement) = self.settlements_by_market_id.get(&market_id) else {
|
||||
let state = self.inner.read().await;
|
||||
let Some(settlement) = state.settlements_by_market_id.get(&market_id) else {
|
||||
return None;
|
||||
};
|
||||
Some(settlement.clone())
|
||||
}
|
||||
|
||||
pub async fn upsert(&self, event_id: Uuid, settlement: SettlementSnapshot) -> SettlementSnapshot {
|
||||
let mut state = self.inner.write().await;
|
||||
state.settlements_by_event_id.insert(event_id, settlement.clone());
|
||||
state
|
||||
.settlements_by_market_id
|
||||
.insert(settlement.market_id, settlement.clone());
|
||||
settlement
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ use axum::{body::{to_bytes, Body}, http::{Request, StatusCode}};
|
||||
use chrono::Utc;
|
||||
use hermes_backend::{app_state::AppState, build_router, config::AppConfig};
|
||||
use serde_json as json;
|
||||
use uuid::Uuid;
|
||||
use tower::ServiceExt;
|
||||
|
||||
#[tokio::test]
|
||||
@@ -410,6 +411,223 @@ async fn analytics_batch_is_recorded() {
|
||||
assert_eq!(attribute_count, 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn admin_endpoints_publish_round_data() {
|
||||
let app = build_router(AppState::new(AppConfig::default(), None, None));
|
||||
|
||||
let event_id = Uuid::new_v4();
|
||||
let market_id = Uuid::new_v4();
|
||||
let home_outcome_id = Uuid::new_v4();
|
||||
let away_outcome_id = Uuid::new_v4();
|
||||
let odds_version_id = Uuid::new_v4();
|
||||
|
||||
let event_response = app
|
||||
.clone()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/v1/admin/events")
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(
|
||||
json::json!({
|
||||
"event": {
|
||||
"id": event_id,
|
||||
"sport_type": "football",
|
||||
"source_ref": "admin-event-001",
|
||||
"title_en": "Admin created event",
|
||||
"title_sv": "Adminskapad händelse",
|
||||
"status": "scheduled",
|
||||
"preview_start_ms": 0,
|
||||
"preview_end_ms": 1000,
|
||||
"reveal_start_ms": 2000,
|
||||
"reveal_end_ms": 3000,
|
||||
"lock_at": "2099-01-01T01:10:00Z",
|
||||
"settle_at": "2099-01-01T01:25:00Z"
|
||||
},
|
||||
"media": [
|
||||
{
|
||||
"id": Uuid::new_v4(),
|
||||
"event_id": event_id,
|
||||
"media_type": "hls_main",
|
||||
"hls_master_url": "https://cdn.example.com/admin/master.m3u8",
|
||||
"poster_url": "https://cdn.example.com/admin/poster.jpg",
|
||||
"duration_ms": 3000,
|
||||
"preview_start_ms": 0,
|
||||
"preview_end_ms": 1000,
|
||||
"reveal_start_ms": 2000,
|
||||
"reveal_end_ms": 3000
|
||||
}
|
||||
],
|
||||
"markets": []
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(event_response.status(), StatusCode::CREATED);
|
||||
|
||||
let market_response = app
|
||||
.clone()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/v1/admin/markets")
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(
|
||||
json::json!({
|
||||
"id": market_id,
|
||||
"event_id": event_id,
|
||||
"question_key": "market.admin.winner",
|
||||
"market_type": "winner",
|
||||
"status": "open",
|
||||
"lock_at": "2099-01-01T01:10:00Z",
|
||||
"settlement_rule_key": "settle_on_match_winner",
|
||||
"outcomes": [
|
||||
{
|
||||
"id": home_outcome_id,
|
||||
"market_id": market_id,
|
||||
"outcome_code": "home",
|
||||
"label_key": "outcome.home",
|
||||
"sort_order": 1
|
||||
},
|
||||
{
|
||||
"id": away_outcome_id,
|
||||
"market_id": market_id,
|
||||
"outcome_code": "away",
|
||||
"label_key": "outcome.away",
|
||||
"sort_order": 2
|
||||
}
|
||||
]
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(market_response.status(), StatusCode::CREATED);
|
||||
|
||||
let odds_response = app
|
||||
.clone()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/v1/admin/odds")
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(
|
||||
json::json!({
|
||||
"id": odds_version_id,
|
||||
"market_id": market_id,
|
||||
"version_no": 1,
|
||||
"created_at": "2099-01-01T01:00:00Z",
|
||||
"is_current": true,
|
||||
"odds": [
|
||||
{
|
||||
"id": Uuid::new_v4(),
|
||||
"odds_version_id": odds_version_id,
|
||||
"outcome_id": home_outcome_id,
|
||||
"decimal_odds": 1.9,
|
||||
"fractional_num": 9,
|
||||
"fractional_den": 10
|
||||
},
|
||||
{
|
||||
"id": Uuid::new_v4(),
|
||||
"odds_version_id": odds_version_id,
|
||||
"outcome_id": away_outcome_id,
|
||||
"decimal_odds": 2.1,
|
||||
"fractional_num": 11,
|
||||
"fractional_den": 10
|
||||
}
|
||||
]
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(odds_response.status(), StatusCode::CREATED);
|
||||
|
||||
let settlement_response = app
|
||||
.clone()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/v1/admin/settlements")
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(
|
||||
json::json!({
|
||||
"id": Uuid::new_v4(),
|
||||
"market_id": market_id,
|
||||
"settled_at": "2099-01-01T01:25:00Z",
|
||||
"winning_outcome_id": home_outcome_id
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(settlement_response.status(), StatusCode::CREATED);
|
||||
|
||||
let manifest_response = app
|
||||
.clone()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri(format!("/api/v1/events/{event_id}/manifest"))
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(manifest_response.status(), StatusCode::OK);
|
||||
|
||||
let manifest_body = to_bytes(manifest_response.into_body(), usize::MAX).await.unwrap();
|
||||
let manifest_json: json::Value = json::from_slice(&manifest_body).unwrap();
|
||||
assert_eq!(manifest_json["markets"].as_array().unwrap().len(), 1);
|
||||
|
||||
let odds_response = app
|
||||
.clone()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri(format!("/api/v1/markets/{market_id}/odds/current"))
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(odds_response.status(), StatusCode::OK);
|
||||
|
||||
let odds_body = to_bytes(odds_response.into_body(), usize::MAX).await.unwrap();
|
||||
let odds_json: json::Value = json::from_slice(&odds_body).unwrap();
|
||||
assert_eq!(odds_json["id"], odds_version_id.to_string());
|
||||
|
||||
let result_response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri(format!("/api/v1/events/{event_id}/result"))
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(result_response.status(), StatusCode::OK);
|
||||
|
||||
let result_body = to_bytes(result_response.into_body(), usize::MAX).await.unwrap();
|
||||
let result_json: json::Value = json::from_slice(&result_body).unwrap();
|
||||
assert_eq!(result_json["market_id"], market_id.to_string());
|
||||
assert_eq!(result_json["winning_outcome_id"], home_outcome_id.to_string());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn event_markets_and_current_odds_work() {
|
||||
let app = build_router(AppState::new(AppConfig::default(), None, None));
|
||||
|
||||
Reference in New Issue
Block a user