add market and odds endpoints
This commit is contained in:
@@ -5,12 +5,15 @@ use sqlx::PgPool;
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
pub use crate::events::{EventManifestSnapshot, EventSnapshot};
|
pub use crate::events::{EventManifestSnapshot, EventSnapshot};
|
||||||
|
pub use crate::events::{MarketSnapshot, OutcomeSnapshot};
|
||||||
|
pub use crate::markets::{OddsVersionSnapshot, OutcomeOddsSnapshot};
|
||||||
pub use crate::sessions::{SessionSnapshot, SessionStartRequest};
|
pub use crate::sessions::{SessionSnapshot, SessionStartRequest};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::AppConfig,
|
config::AppConfig,
|
||||||
error::AppError,
|
error::AppError,
|
||||||
events::{EventsStore},
|
events::{EventsStore},
|
||||||
|
markets::MarketsStore,
|
||||||
sessions::{SessionDefaults, SessionsStore},
|
sessions::{SessionDefaults, SessionsStore},
|
||||||
users::{UserCreateRequest, UsersStore},
|
users::{UserCreateRequest, UsersStore},
|
||||||
};
|
};
|
||||||
@@ -22,6 +25,7 @@ pub struct AppState {
|
|||||||
pub database_pool: Option<PgPool>,
|
pub database_pool: Option<PgPool>,
|
||||||
pub redis_client: Option<RedisClient>,
|
pub redis_client: Option<RedisClient>,
|
||||||
events: EventsStore,
|
events: EventsStore,
|
||||||
|
markets: MarketsStore,
|
||||||
users: UsersStore,
|
users: UsersStore,
|
||||||
sessions: SessionsStore,
|
sessions: SessionsStore,
|
||||||
}
|
}
|
||||||
@@ -38,6 +42,7 @@ impl AppState {
|
|||||||
database_pool,
|
database_pool,
|
||||||
redis_client,
|
redis_client,
|
||||||
events: EventsStore::new(),
|
events: EventsStore::new(),
|
||||||
|
markets: MarketsStore::new(),
|
||||||
users: UsersStore::new(),
|
users: UsersStore::new(),
|
||||||
sessions: SessionsStore::new(),
|
sessions: SessionsStore::new(),
|
||||||
}
|
}
|
||||||
@@ -96,6 +101,14 @@ impl AppState {
|
|||||||
self.events.get_manifest(event_id).await
|
self.events.get_manifest(event_id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn markets_for_event(&self, event_id: Uuid) -> Option<Vec<MarketSnapshot>> {
|
||||||
|
self.markets.markets_for_event(event_id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn current_odds(&self, market_id: Uuid) -> Option<OddsVersionSnapshot> {
|
||||||
|
self.markets.current_odds(market_id).await
|
||||||
|
}
|
||||||
|
|
||||||
pub fn database_ready(&self) -> bool {
|
pub fn database_ready(&self) -> bool {
|
||||||
self.database_pool.is_some()
|
self.database_pool.is_some()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ pub mod config;
|
|||||||
pub mod db;
|
pub mod db;
|
||||||
pub mod events;
|
pub mod events;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
pub mod markets;
|
||||||
pub mod sessions;
|
pub mod sessions;
|
||||||
pub mod routes;
|
pub mod routes;
|
||||||
pub mod users;
|
pub mod users;
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
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::events::{MarketSnapshot, OutcomeSnapshot};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct OutcomeOddsSnapshot {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub odds_version_id: Uuid,
|
||||||
|
pub outcome_id: Uuid,
|
||||||
|
pub decimal_odds: f64,
|
||||||
|
pub fractional_num: i32,
|
||||||
|
pub fractional_den: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct OddsVersionSnapshot {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub market_id: Uuid,
|
||||||
|
pub version_no: i32,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub is_current: bool,
|
||||||
|
pub odds: Vec<OutcomeOddsSnapshot>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
pub struct MarketsStore {
|
||||||
|
inner: Arc<RwLock<MarketsState>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct MarketsState {
|
||||||
|
markets_by_event_id: HashMap<Uuid, Vec<MarketSnapshot>>,
|
||||||
|
markets_by_id: HashMap<Uuid, MarketSnapshot>,
|
||||||
|
current_odds_by_market_id: HashMap<Uuid, OddsVersionSnapshot>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MarketsStore {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::with_sample_data()
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
let odds_version_id = Uuid::parse_str("66666666-6666-6666-6666-666666666666").expect("valid uuid");
|
||||||
|
let preview_lock_at = Utc::now() + ChronoDuration::minutes(10);
|
||||||
|
let created_at = Utc::now();
|
||||||
|
|
||||||
|
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 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: preview_lock_at,
|
||||||
|
settlement_rule_key: "settle_on_match_winner".to_string(),
|
||||||
|
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 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 mut markets_by_event_id = HashMap::new();
|
||||||
|
markets_by_event_id.insert(event_id, vec![market.clone()]);
|
||||||
|
|
||||||
|
let mut markets_by_id = HashMap::new();
|
||||||
|
markets_by_id.insert(market_id, market);
|
||||||
|
|
||||||
|
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,
|
||||||
|
current_odds_by_market_id,
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn markets_for_event(&self, event_id: Uuid) -> Option<Vec<MarketSnapshot>> {
|
||||||
|
let state = self.inner.read().await;
|
||||||
|
let Some(markets) = state.markets_by_event_id.get(&event_id) else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
Some(markets.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn market(&self, market_id: Uuid) -> Option<MarketSnapshot> {
|
||||||
|
let state = self.inner.read().await;
|
||||||
|
let Some(market) = state.markets_by_id.get(&market_id) else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
Some(market.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn current_odds(&self, market_id: Uuid) -> Option<OddsVersionSnapshot> {
|
||||||
|
let state = self.inner.read().await;
|
||||||
|
let Some(odds) = state.current_odds_by_market_id.get(&market_id) else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
Some(odds.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
use axum::{extract::{Extension, Path}, Json};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{app_state::{AppState, MarketSnapshot, OddsVersionSnapshot}, error::AppError};
|
||||||
|
|
||||||
|
pub async fn list_for_event(
|
||||||
|
Path(event_id): Path<Uuid>,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
) -> Result<Json<Vec<MarketSnapshot>>, AppError> {
|
||||||
|
let Some(markets) = state.markets_for_event(event_id).await else {
|
||||||
|
return Err(AppError::not_found("Markets not found"));
|
||||||
|
};
|
||||||
|
Ok(Json(markets))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn current_odds(
|
||||||
|
Path(market_id): Path<Uuid>,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
) -> Result<Json<OddsVersionSnapshot>, AppError> {
|
||||||
|
let Some(odds) = state.current_odds(market_id).await else {
|
||||||
|
return Err(AppError::not_found("Current odds not found"));
|
||||||
|
};
|
||||||
|
Ok(Json(odds))
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
pub mod health;
|
pub mod health;
|
||||||
pub mod events;
|
pub mod events;
|
||||||
|
pub mod markets;
|
||||||
pub mod session;
|
pub mod session;
|
||||||
|
|
||||||
use axum::{routing::{get, post}, Router};
|
use axum::{routing::{get, post}, Router};
|
||||||
@@ -10,6 +11,8 @@ pub fn router() -> Router {
|
|||||||
.route("/api/v1/feed/next", get(events::next))
|
.route("/api/v1/feed/next", get(events::next))
|
||||||
.route("/api/v1/events/{event_id}", get(events::show))
|
.route("/api/v1/events/{event_id}", get(events::show))
|
||||||
.route("/api/v1/events/{event_id}/manifest", get(events::manifest))
|
.route("/api/v1/events/{event_id}/manifest", get(events::manifest))
|
||||||
|
.route("/api/v1/events/{event_id}/markets", get(markets::list_for_event))
|
||||||
|
.route("/api/v1/markets/{market_id}/odds/current", get(markets::current_odds))
|
||||||
.route("/api/v1/session/start", post(session::start))
|
.route("/api/v1/session/start", post(session::start))
|
||||||
.route("/api/v1/session/end", post(session::end))
|
.route("/api/v1/session/end", post(session::end))
|
||||||
.route("/api/v1/session/me", get(session::me))
|
.route("/api/v1/session/me", get(session::me))
|
||||||
|
|||||||
@@ -129,3 +129,56 @@ async fn feed_next_returns_a_manifestable_event() {
|
|||||||
assert!(manifest["media"].as_array().unwrap().len() >= 1);
|
assert!(manifest["media"].as_array().unwrap().len() >= 1);
|
||||||
assert!(manifest["markets"].as_array().unwrap().len() >= 1);
|
assert!(manifest["markets"].as_array().unwrap().len() >= 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn event_markets_and_current_odds_work() {
|
||||||
|
let app = build_router(AppState::new(AppConfig::default(), None, None));
|
||||||
|
|
||||||
|
let event_response = app
|
||||||
|
.clone()
|
||||||
|
.oneshot(Request::builder().uri("/api/v1/feed/next").body(Body::empty()).unwrap())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let event_body = to_bytes(event_response.into_body(), usize::MAX).await.unwrap();
|
||||||
|
let event: json::Value = json::from_slice(&event_body).unwrap();
|
||||||
|
let event_id = event["id"].as_str().unwrap().to_string();
|
||||||
|
|
||||||
|
let markets_response = app
|
||||||
|
.clone()
|
||||||
|
.oneshot(
|
||||||
|
Request::builder()
|
||||||
|
.uri(format!("/api/v1/events/{event_id}/markets"))
|
||||||
|
.body(Body::empty())
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(markets_response.status(), StatusCode::OK);
|
||||||
|
|
||||||
|
let markets_body = to_bytes(markets_response.into_body(), usize::MAX).await.unwrap();
|
||||||
|
let markets: json::Value = json::from_slice(&markets_body).unwrap();
|
||||||
|
let market_id = markets[0]["id"].as_str().unwrap().to_string();
|
||||||
|
|
||||||
|
assert_eq!(markets[0]["outcomes"].as_array().unwrap().len(), 2);
|
||||||
|
|
||||||
|
let odds_response = app
|
||||||
|
.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::Value = json::from_slice(&odds_body).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(odds["market_id"], market_id);
|
||||||
|
assert_eq!(odds["is_current"], true);
|
||||||
|
assert_eq!(odds["odds"].as_array().unwrap().len(), 2);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user