add study flow endpoints

This commit is contained in:
2026-04-09 15:17:08 +02:00
parent d45202770e
commit a1dcebaec1
15 changed files with 992 additions and 16 deletions
+109
View File
@@ -0,0 +1,109 @@
use std::{collections::HashMap, sync::Arc};
use chrono::{DateTime, Utc};
use serde::Deserialize;
use tokio::sync::RwLock;
use uuid::Uuid;
use crate::sessions::SessionSnapshot;
#[derive(Clone, Debug, Deserialize)]
pub struct AnalyticsBatchRequest {
pub events: Vec<AnalyticsEventInput>,
}
#[derive(Clone, Debug, Deserialize)]
pub struct AnalyticsEventInput {
pub event_name: String,
pub occurred_at: DateTime<Utc>,
pub attributes: Option<Vec<AttributeInput>>,
}
#[derive(Clone, Debug, Deserialize)]
pub struct AttributeInput {
pub key: String,
pub value: String,
}
#[derive(Clone)]
pub struct AnalyticsStore {
inner: Arc<RwLock<AnalyticsState>>,
}
#[derive(Default)]
struct AnalyticsState {
event_types_by_name: HashMap<String, Uuid>,
events: Vec<AnalyticsEventSnapshot>,
attributes: Vec<AnalyticsEventAttributeSnapshot>,
}
#[derive(Clone)]
#[allow(dead_code)]
struct AnalyticsEventSnapshot {
id: Uuid,
analytics_event_type_id: Uuid,
session_id: Uuid,
user_id: Uuid,
occurred_at: DateTime<Utc>,
}
#[derive(Clone)]
#[allow(dead_code)]
struct AnalyticsEventAttributeSnapshot {
id: Uuid,
analytics_event_id: Uuid,
attribute_key: String,
attribute_value: String,
}
impl AnalyticsStore {
pub fn new() -> Self {
Self {
inner: Arc::new(RwLock::new(AnalyticsState::default())),
}
}
pub async fn ingest(&self, session: &SessionSnapshot, request: AnalyticsBatchRequest) -> usize {
let mut state = self.inner.write().await;
let mut inserted = 0usize;
for event in request.events {
let analytics_event_type_id = *state
.event_types_by_name
.entry(event.event_name.clone())
.or_insert_with(Uuid::new_v4);
let analytics_event_id = Uuid::new_v4();
state.events.push(AnalyticsEventSnapshot {
id: analytics_event_id,
analytics_event_type_id,
session_id: session.session_id,
user_id: session.user_id,
occurred_at: event.occurred_at,
});
let Some(attributes) = event.attributes else {
inserted += 1;
continue;
};
for attribute in attributes {
state.attributes.push(AnalyticsEventAttributeSnapshot {
id: Uuid::new_v4(),
analytics_event_id,
attribute_key: attribute.key,
attribute_value: attribute.value,
});
}
inserted += 1;
}
inserted
}
pub async fn counts(&self) -> (usize, usize) {
let state = self.inner.read().await;
(state.events.len(), state.attributes.len())
}
}
+214
View File
@@ -1,19 +1,33 @@
use std::time::Instant;
use chrono::Utc;
use redis::Client as RedisClient;
use sqlx::PgPool;
use uuid::Uuid;
pub use crate::analytics::{AnalyticsBatchRequest, AnalyticsEventInput, AttributeInput};
pub use crate::bets::{BetIntentRecord, BetIntentRequest, BetIntentResponse};
pub use crate::events::{EventManifestSnapshot, EventSnapshot};
pub use crate::events::{MarketSnapshot, OutcomeSnapshot};
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::{
analytics::AnalyticsStore,
bets::{
BetsStore, ACCEPTANCE_ACCEPTED, ACCEPTANCE_REJECTED_INVALID_MARKET,
ACCEPTANCE_REJECTED_INVALID_SESSION, ACCEPTANCE_REJECTED_TOO_LATE,
},
config::AppConfig,
error::AppError,
events::{EventsStore},
experiments::ExperimentsStore,
localization::LocalizationStore,
markets::MarketsStore,
settlement::SettlementsStore,
sessions::{SessionDefaults, SessionsStore},
users::{UserCreateRequest, UsersStore},
};
@@ -25,6 +39,11 @@ pub struct AppState {
pub database_pool: Option<PgPool>,
pub redis_client: Option<RedisClient>,
events: EventsStore,
bets: BetsStore,
settlements: SettlementsStore,
experiments: ExperimentsStore,
localization: LocalizationStore,
analytics: AnalyticsStore,
markets: MarketsStore,
users: UsersStore,
sessions: SessionsStore,
@@ -42,6 +61,11 @@ impl AppState {
database_pool,
redis_client,
events: EventsStore::new(),
bets: BetsStore::new(),
settlements: SettlementsStore::new(),
experiments: ExperimentsStore::new(),
localization: LocalizationStore::new(),
analytics: AnalyticsStore::new(),
markets: MarketsStore::new(),
users: UsersStore::new(),
sessions: SessionsStore::new(),
@@ -82,6 +106,150 @@ impl AppState {
self.sessions.current_session().await
}
fn build_bet_intent_record(
&self,
request: &BetIntentRequest,
user_id: Option<Uuid>,
accepted: bool,
acceptance_code: &str,
accepted_odds_version_id: Option<Uuid>,
) -> BetIntentRecord {
BetIntentRecord {
id: Uuid::new_v4(),
session_id: request.session_id,
user_id,
event_id: request.event_id,
market_id: request.market_id,
outcome_id: request.outcome_id,
idempotency_key: request.idempotency_key.clone(),
client_sent_at: request.client_sent_at,
server_received_at: Utc::now(),
accepted,
acceptance_code: acceptance_code.to_string(),
accepted_odds_version_id,
}
}
pub async fn submit_bet_intent(&self, request: BetIntentRequest) -> BetIntentResponse {
let Some(existing) = self
.bets
.get_by_idempotency_key(&request.idempotency_key)
.await
else {
return self.submit_bet_intent_fresh(request).await;
};
existing
}
async fn submit_bet_intent_fresh(&self, request: BetIntentRequest) -> BetIntentResponse {
let Some(session) = self.current_session().await else {
return self
.bets
.record(self.build_bet_intent_record(
&request,
None,
false,
ACCEPTANCE_REJECTED_INVALID_SESSION,
None,
))
.await;
};
if session.ended_at.is_some() || session.session_id != request.session_id {
return self
.bets
.record(self.build_bet_intent_record(
&request,
Some(session.user_id),
false,
ACCEPTANCE_REJECTED_INVALID_SESSION,
None,
))
.await;
}
let Some(market) = self.markets.market(request.market_id).await else {
return self
.bets
.record(self.build_bet_intent_record(
&request,
Some(session.user_id),
false,
ACCEPTANCE_REJECTED_INVALID_MARKET,
None,
))
.await;
};
if market.event_id != request.event_id {
return self
.bets
.record(self.build_bet_intent_record(
&request,
Some(session.user_id),
false,
ACCEPTANCE_REJECTED_INVALID_MARKET,
None,
))
.await;
}
let Some(outcome) = self.markets.outcome(request.outcome_id).await else {
return self
.bets
.record(self.build_bet_intent_record(
&request,
Some(session.user_id),
false,
ACCEPTANCE_REJECTED_INVALID_MARKET,
None,
))
.await;
};
if outcome.market_id != market.id || request.event_id != market.event_id {
return self
.bets
.record(self.build_bet_intent_record(
&request,
Some(session.user_id),
false,
ACCEPTANCE_REJECTED_INVALID_MARKET,
None,
))
.await;
}
if Utc::now() >= market.lock_at {
return self
.bets
.record(self.build_bet_intent_record(
&request,
Some(session.user_id),
false,
ACCEPTANCE_REJECTED_TOO_LATE,
None,
))
.await;
}
let accepted_odds_version_id = self.current_odds(market.id).await.map(|odds| odds.id);
self.bets
.record(self.build_bet_intent_record(
&request,
Some(session.user_id),
true,
ACCEPTANCE_ACCEPTED,
accepted_odds_version_id,
))
.await
}
pub async fn bet_intent(&self, bet_intent_id: Uuid) -> Option<BetIntentResponse> {
self.bets.get(bet_intent_id).await
}
pub async fn end_session(&self) -> Result<SessionSnapshot, AppError> {
self.sessions
.end_session()
@@ -101,6 +269,52 @@ impl AppState {
self.events.get_manifest(event_id).await
}
pub async fn settlement_for_event(&self, event_id: Uuid) -> Option<SettlementSnapshot> {
self.settlements.settlement_for_event(event_id).await
}
pub async fn experiment_config(&self) -> ExperimentConfigSnapshot {
let Some(session) = self.current_session().await else {
return self
.experiments
.config_for_variant(&self.config.default_experiment_variant);
};
if session.ended_at.is_some() {
return self
.experiments
.config_for_variant(&self.config.default_experiment_variant);
}
self.experiments.config_for_variant(&session.experiment_variant)
}
pub async fn localization_bundle(
&self,
locale_code: &str,
) -> Option<LocalizationBundleSnapshot> {
self.localization.bundle(locale_code)
}
pub async fn ingest_analytics_batch(
&self,
request: AnalyticsBatchRequest,
) -> Result<usize, AppError> {
let Some(session) = self.current_session().await else {
return Err(AppError::not_found("No active session"));
};
if session.ended_at.is_some() {
return Err(AppError::not_found("No active session"));
}
Ok(self.analytics.ingest(&session, request).await)
}
pub async fn analytics_counts(&self) -> (usize, usize) {
self.analytics.counts().await
}
pub async fn markets_for_event(&self, event_id: Uuid) -> Option<Vec<MarketSnapshot>> {
self.markets.markets_for_event(event_id).await
}
+116
View File
@@ -0,0 +1,116 @@
use std::{collections::HashMap, sync::Arc};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use tokio::sync::RwLock;
use uuid::Uuid;
pub const ACCEPTANCE_ACCEPTED: &str = "accepted";
pub const ACCEPTANCE_REJECTED_TOO_LATE: &str = "rejected_too_late";
pub const ACCEPTANCE_REJECTED_INVALID_MARKET: &str = "rejected_invalid_market";
pub const ACCEPTANCE_REJECTED_INVALID_SESSION: &str = "rejected_invalid_session";
pub const ACCEPTANCE_REJECTED_DUPLICATE: &str = "rejected_duplicate";
#[derive(Clone, Debug, Deserialize)]
pub struct BetIntentRequest {
pub session_id: Uuid,
pub event_id: Uuid,
pub market_id: Uuid,
pub outcome_id: Uuid,
pub idempotency_key: String,
pub client_sent_at: DateTime<Utc>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct BetIntentResponse {
pub id: Uuid,
pub accepted: bool,
pub acceptance_code: String,
pub accepted_odds_version_id: Option<Uuid>,
pub server_received_at: DateTime<Utc>,
}
#[derive(Clone, Debug)]
pub struct BetIntentRecord {
pub id: Uuid,
pub session_id: Uuid,
pub user_id: Option<Uuid>,
pub event_id: Uuid,
pub market_id: Uuid,
pub outcome_id: Uuid,
pub idempotency_key: String,
pub client_sent_at: DateTime<Utc>,
pub server_received_at: DateTime<Utc>,
pub accepted: bool,
pub acceptance_code: String,
pub accepted_odds_version_id: Option<Uuid>,
}
impl BetIntentRecord {
pub fn response(&self) -> BetIntentResponse {
BetIntentResponse {
id: self.id,
accepted: self.accepted,
acceptance_code: self.acceptance_code.clone(),
accepted_odds_version_id: self.accepted_odds_version_id,
server_received_at: self.server_received_at,
}
}
}
#[derive(Clone, Default)]
pub struct BetsStore {
inner: Arc<RwLock<BetsState>>,
}
#[derive(Default)]
struct BetsState {
intents_by_id: HashMap<Uuid, BetIntentRecord>,
intents_by_idempotency_key: HashMap<String, Uuid>,
}
impl BetsStore {
pub fn new() -> Self {
Self::default()
}
pub async fn get(&self, bet_intent_id: Uuid) -> Option<BetIntentResponse> {
let state = self.inner.read().await;
let Some(record) = state.intents_by_id.get(&bet_intent_id) else {
return None;
};
Some(record.response())
}
pub async fn get_by_idempotency_key(&self, idempotency_key: &str) -> Option<BetIntentResponse> {
let state = self.inner.read().await;
let Some(bet_intent_id) = state.intents_by_idempotency_key.get(idempotency_key).copied() else {
return None;
};
let Some(record) = state.intents_by_id.get(&bet_intent_id) else {
return None;
};
Some(record.response())
}
pub async fn record(&self, record: BetIntentRecord) -> BetIntentResponse {
let mut state = self.inner.write().await;
if let Some(existing_id) = state.intents_by_idempotency_key.get(&record.idempotency_key).copied() {
if let Some(existing) = state.intents_by_id.get(&existing_id) {
return existing.response();
}
}
let response = record.response();
state
.intents_by_idempotency_key
.insert(record.idempotency_key.clone(), record.id);
state.intents_by_id.insert(record.id, record);
response
}
pub async fn counts(&self) -> usize {
self.inner.read().await.intents_by_id.len()
}
}
+40
View File
@@ -0,0 +1,40 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ExperimentConfigSnapshot {
pub variant: String,
pub feature_flags: HashMap<String, bool>,
}
#[derive(Clone, Default)]
pub struct ExperimentsStore;
impl ExperimentsStore {
pub fn new() -> Self {
Self
}
pub fn config_for_variant(&self, variant: &str) -> ExperimentConfigSnapshot {
ExperimentConfigSnapshot {
variant: variant.to_string(),
feature_flags: feature_flags_for_variant(variant),
}
}
}
fn feature_flags_for_variant(variant: &str) -> HashMap<String, bool> {
let modern = variant == "modern";
let mut flags = HashMap::new();
flags.insert("control_mode".to_string(), !modern);
flags.insert("modern_mode".to_string(), modern);
flags.insert("animation_intensity_high".to_string(), modern);
flags.insert("haptics_enabled".to_string(), true);
flags.insert("autoplay_next_enabled".to_string(), true);
flags.insert("countdown_style_compact".to_string(), modern);
flags.insert("odds_update_visualization_enabled".to_string(), modern);
flags.insert("result_card_style_modern".to_string(), modern);
flags.insert("localization_source_contracts".to_string(), true);
flags
}
+5
View File
@@ -1,11 +1,16 @@
#![forbid(unsafe_code)]
pub mod analytics;
pub mod bets;
pub mod app_state;
pub mod config;
pub mod db;
pub mod events;
pub mod error;
pub mod experiments;
pub mod localization;
pub mod markets;
pub mod settlement;
pub mod sessions;
pub mod routes;
pub mod users;
+51
View File
@@ -0,0 +1,51 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use serde_json as json;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct LocalizationBundleSnapshot {
pub locale_code: String,
pub values: HashMap<String, String>,
}
#[derive(Clone)]
pub struct LocalizationStore {
bundles: HashMap<String, LocalizationBundleSnapshot>,
}
impl LocalizationStore {
pub fn new() -> Self {
let mut bundles = HashMap::new();
bundles.insert(
"en".to_string(),
load_bundle(
"en",
include_str!("../../../contracts/localization/en.json"),
),
);
bundles.insert(
"sv".to_string(),
load_bundle(
"sv",
include_str!("../../../contracts/localization/sv.json"),
),
);
Self { bundles }
}
pub fn bundle(&self, locale_code: &str) -> Option<LocalizationBundleSnapshot> {
let Some(bundle) = self.bundles.get(locale_code) else {
return None;
};
Some(bundle.clone())
}
}
fn load_bundle(locale_code: &str, raw: &str) -> LocalizationBundleSnapshot {
let values: HashMap<String, String> = json::from_str(raw).expect("valid localization bundle");
LocalizationBundleSnapshot {
locale_code: locale_code.to_string(),
values,
}
}
+33 -16
View File
@@ -36,6 +36,7 @@ pub struct MarketsStore {
struct MarketsState {
markets_by_event_id: HashMap<Uuid, Vec<MarketSnapshot>>,
markets_by_id: HashMap<Uuid, MarketSnapshot>,
outcomes_by_id: HashMap<Uuid, OutcomeSnapshot>,
current_odds_by_market_id: HashMap<Uuid, OddsVersionSnapshot>,
}
@@ -54,6 +55,23 @@ impl MarketsStore {
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 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,
@@ -62,22 +80,7 @@ impl MarketsStore {
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,
},
],
outcomes: outcomes.clone(),
};
let odds_version = OddsVersionSnapshot {
@@ -112,6 +115,11 @@ impl MarketsStore {
let mut markets_by_id = HashMap::new();
markets_by_id.insert(market_id, market);
let mut outcomes_by_id = HashMap::new();
for outcome in outcomes {
outcomes_by_id.insert(outcome.id, outcome);
}
let mut current_odds_by_market_id = HashMap::new();
current_odds_by_market_id.insert(market_id, odds_version);
@@ -119,6 +127,7 @@ impl MarketsStore {
inner: Arc::new(RwLock::new(MarketsState {
markets_by_event_id,
markets_by_id,
outcomes_by_id,
current_odds_by_market_id,
})),
}
@@ -140,6 +149,14 @@ impl MarketsStore {
Some(market.clone())
}
pub async fn outcome(&self, outcome_id: Uuid) -> Option<OutcomeSnapshot> {
let state = self.inner.read().await;
let Some(outcome) = state.outcomes_by_id.get(&outcome_id) else {
return None;
};
Some(outcome.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 {
+11
View File
@@ -0,0 +1,11 @@
use axum::{extract::Extension, http::StatusCode, Json};
use crate::{app_state::{AnalyticsBatchRequest, AppState}, error::AppError};
pub async fn batch(
Extension(state): Extension<AppState>,
Json(payload): Json<AnalyticsBatchRequest>,
) -> Result<StatusCode, AppError> {
state.ingest_analytics_batch(payload).await?;
Ok(StatusCode::ACCEPTED)
}
+23
View File
@@ -0,0 +1,23 @@
use axum::{extract::{Extension, Path}, http::StatusCode, Json};
use uuid::Uuid;
use crate::{app_state::{AppState, BetIntentRequest, BetIntentResponse}, error::AppError};
pub async fn submit(
Extension(state): Extension<AppState>,
Json(payload): Json<BetIntentRequest>,
) -> Result<(StatusCode, Json<BetIntentResponse>), AppError> {
let response = state.submit_bet_intent(payload).await;
Ok((StatusCode::CREATED, Json(response)))
}
pub async fn show(
Path(bet_intent_id): Path<Uuid>,
Extension(state): Extension<AppState>,
) -> Result<Json<BetIntentResponse>, AppError> {
let Some(intent) = state.bet_intent(bet_intent_id).await else {
return Err(AppError::not_found("Bet intent not found"));
};
Ok(Json(intent))
}
+7
View File
@@ -0,0 +1,7 @@
use axum::{extract::Extension, Json};
use crate::{app_state::{AppState, ExperimentConfigSnapshot}, error::AppError};
pub async fn config(Extension(state): Extension<AppState>) -> Result<Json<ExperimentConfigSnapshot>, AppError> {
Ok(Json(state.experiment_config().await))
}
+14
View File
@@ -0,0 +1,14 @@
use axum::{extract::{Extension, Path}, Json};
use crate::{app_state::{AppState, LocalizationBundleSnapshot}, error::AppError};
pub async fn bundle(
Path(locale_code): Path<String>,
Extension(state): Extension<AppState>,
) -> Result<Json<LocalizationBundleSnapshot>, AppError> {
let Some(bundle) = state.localization_bundle(&locale_code).await else {
return Err(AppError::not_found("Localization bundle not found"));
};
Ok(Json(bundle))
}
+11
View File
@@ -1,6 +1,11 @@
pub mod health;
pub mod analytics;
pub mod bets;
pub mod events;
pub mod experiments;
pub mod localization;
pub mod markets;
pub mod results;
pub mod session;
use axum::{routing::{get, post}, Router};
@@ -12,7 +17,13 @@ pub fn router() -> Router {
.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}/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/bets/intent", post(bets::submit))
.route("/api/v1/bets/{bet_intent_id}", get(bets::show))
.route("/api/v1/analytics/batch", post(analytics::batch))
.route("/api/v1/experiments/config", get(experiments::config))
.route("/api/v1/localization/{locale_code}", get(localization::bundle))
.route("/api/v1/session/start", post(session::start))
.route("/api/v1/session/end", post(session::end))
.route("/api/v1/session/me", get(session::me))
+15
View File
@@ -0,0 +1,15 @@
use axum::{extract::{Extension, Path}, Json};
use uuid::Uuid;
use crate::{app_state::{AppState, SettlementSnapshot}, error::AppError};
pub async fn show(
Path(event_id): Path<Uuid>,
Extension(state): Extension<AppState>,
) -> Result<Json<SettlementSnapshot>, AppError> {
let Some(settlement) = state.settlement_for_event(event_id).await else {
return Err(AppError::not_found("Settlement not found"));
};
Ok(Json(settlement))
}
+63
View File
@@ -0,0 +1,63 @@
use std::collections::HashMap;
use chrono::{DateTime, Duration as ChronoDuration, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SettlementSnapshot {
pub id: Uuid,
pub market_id: Uuid,
pub settled_at: DateTime<Utc>,
pub winning_outcome_id: Uuid,
}
#[derive(Clone, Default)]
pub struct SettlementsStore {
settlements_by_event_id: HashMap<Uuid, SettlementSnapshot>,
settlements_by_market_id: HashMap<Uuid, SettlementSnapshot>,
}
impl SettlementsStore {
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 winning_outcome_id = Uuid::parse_str("44444444-4444-4444-4444-444444444444").expect("valid uuid");
let settlement = SettlementSnapshot {
id: Uuid::parse_str("99999999-9999-9999-9999-999999999999").expect("valid uuid"),
market_id,
settled_at: Utc::now() - ChronoDuration::minutes(5),
winning_outcome_id,
};
let mut settlements_by_event_id = HashMap::new();
settlements_by_event_id.insert(event_id, settlement.clone());
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,
}
}
pub async fn settlement_for_event(&self, event_id: Uuid) -> Option<SettlementSnapshot> {
let Some(settlement) = self.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 {
return None;
};
Some(settlement.clone())
}
}