use chrono::{Datelike, Duration, TimeZone, Utc}; use sqlx::PgPool; use crate::domain::{UsageDecision, UsagePeriod, UsageSummary}; use crate::infrastructure::db::entitlements as entitlements_db; const FREE_TRANSCRIPTION_LIMIT_SECONDS: f64 = 1200.0; const TRANSCRIPTION_FEATURE_KEY: &str = "transcription"; #[derive(Debug)] /// Errors emitted while resolving entitlements. pub enum EntitlementError { DatabaseError(String), } impl EntitlementError { pub fn message(&self) -> String { match self { Self::DatabaseError(message) => message.clone(), } } } /// Summarizes transcription usage and limits for a user. pub async fn transcription_usage_summary( db_pool: &PgPool, user_did: &str, ) -> Result { if cfg!(test) { let period = current_month_period(); return Ok(UsageSummary { limit_seconds: Some(FREE_TRANSCRIPTION_LIMIT_SECONDS), used_seconds: 0.0, period, }); } let period = current_month_period(); let limit_seconds = entitlements_db::resolve_feature_limit( db_pool, user_did, TRANSCRIPTION_FEATURE_KEY, ) .await .map_err(|err| EntitlementError::DatabaseError(err.to_string()))? .unwrap_or(Some(FREE_TRANSCRIPTION_LIMIT_SECONDS)); let used_seconds = entitlements_db::load_usage( db_pool, user_did, TRANSCRIPTION_FEATURE_KEY, &period, ) .await .map_err(|err| EntitlementError::DatabaseError(err.to_string()))?; Ok(UsageSummary { limit_seconds, used_seconds, period, }) } /// Result of evaluating transcription access for a user. pub enum TranscriptionEntitlement { Allowed { usage: UsageSummary }, Exceeded { usage: UsageSummary, limit_seconds: f64, used_seconds: f64, }, } /// Evaluates whether the user can request another transcription. pub async fn transcription_entitlement( db_pool: &PgPool, user_did: &str, ) -> Result { let usage = transcription_usage_summary(db_pool, user_did).await?; match usage.decision() { UsageDecision::Allowed => Ok(TranscriptionEntitlement::Allowed { usage }), UsageDecision::Exceeded { limit_seconds, used_seconds, } => Ok( TranscriptionEntitlement::Exceeded { usage, limit_seconds, used_seconds, }, ), } } /// Records transcription usage for the current billing period. pub async fn record_transcription_usage( db_pool: &PgPool, user_did: &str, seconds: f64, ) -> Result<(), EntitlementError> { if cfg!(test) { return Ok(()); } let period = current_month_period(); entitlements_db::record_feature_usage( db_pool, user_did, TRANSCRIPTION_FEATURE_KEY, &period, seconds, ) .await .map_err(|err| EntitlementError::DatabaseError(err.to_string()))?; Ok(()) } fn current_month_period() -> UsagePeriod { let now = Utc::now(); let start = Utc .with_ymd_and_hms(now.year(), now.month(), 1, 0, 0, 0) .single() .unwrap_or(now); let end = if now.month() == 12 { Utc.with_ymd_and_hms(now.year() + 1, 1, 1, 0, 0, 0) .single() .unwrap_or(start + Duration::days(31)) } else { Utc.with_ymd_and_hms(now.year(), now.month() + 1, 1, 0, 0, 0) .single() .unwrap_or(start + Duration::days(31)) }; UsagePeriod { start, end } }