add study flow endpoints
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user