Built for people who think better out loud.
at main 135 lines 3.7 kB view raw
1use chrono::{Datelike, Duration, TimeZone, Utc}; 2use sqlx::PgPool; 3 4use crate::domain::{UsageDecision, UsagePeriod, UsageSummary}; 5use crate::infrastructure::db::entitlements as entitlements_db; 6 7const FREE_TRANSCRIPTION_LIMIT_SECONDS: f64 = 1200.0; 8const TRANSCRIPTION_FEATURE_KEY: &str = "transcription"; 9 10#[derive(Debug)] 11/// Errors emitted while resolving entitlements. 12pub enum EntitlementError { 13 DatabaseError(String), 14} 15 16impl EntitlementError { 17 pub fn message(&self) -> String { 18 match self { 19 Self::DatabaseError(message) => message.clone(), 20 } 21 } 22} 23 24/// Summarizes transcription usage and limits for a user. 25pub async fn transcription_usage_summary( 26 db_pool: &PgPool, 27 user_did: &str, 28) -> Result<UsageSummary, EntitlementError> { 29 if cfg!(test) { 30 let period = current_month_period(); 31 return Ok(UsageSummary { 32 limit_seconds: Some(FREE_TRANSCRIPTION_LIMIT_SECONDS), 33 used_seconds: 0.0, 34 period, 35 }); 36 } 37 38 let period = current_month_period(); 39 let limit_seconds = entitlements_db::resolve_feature_limit( 40 db_pool, 41 user_did, 42 TRANSCRIPTION_FEATURE_KEY, 43 ) 44 .await 45 .map_err(|err| EntitlementError::DatabaseError(err.to_string()))? 46 .unwrap_or(Some(FREE_TRANSCRIPTION_LIMIT_SECONDS)); 47 let used_seconds = entitlements_db::load_usage( 48 db_pool, 49 user_did, 50 TRANSCRIPTION_FEATURE_KEY, 51 &period, 52 ) 53 .await 54 .map_err(|err| EntitlementError::DatabaseError(err.to_string()))?; 55 56 Ok(UsageSummary { 57 limit_seconds, 58 used_seconds, 59 period, 60 }) 61} 62 63/// Result of evaluating transcription access for a user. 64pub enum TranscriptionEntitlement { 65 Allowed { usage: UsageSummary }, 66 Exceeded { 67 usage: UsageSummary, 68 limit_seconds: f64, 69 used_seconds: f64, 70 }, 71} 72 73/// Evaluates whether the user can request another transcription. 74pub async fn transcription_entitlement( 75 db_pool: &PgPool, 76 user_did: &str, 77) -> Result<TranscriptionEntitlement, EntitlementError> { 78 let usage = transcription_usage_summary(db_pool, user_did).await?; 79 match usage.decision() { 80 UsageDecision::Allowed => Ok(TranscriptionEntitlement::Allowed { usage }), 81 UsageDecision::Exceeded { 82 limit_seconds, 83 used_seconds, 84 } => Ok( 85 TranscriptionEntitlement::Exceeded { 86 usage, 87 limit_seconds, 88 used_seconds, 89 }, 90 ), 91 } 92} 93 94/// Records transcription usage for the current billing period. 95pub async fn record_transcription_usage( 96 db_pool: &PgPool, 97 user_did: &str, 98 seconds: f64, 99) -> Result<(), EntitlementError> { 100 if cfg!(test) { 101 return Ok(()); 102 } 103 104 let period = current_month_period(); 105 entitlements_db::record_feature_usage( 106 db_pool, 107 user_did, 108 TRANSCRIPTION_FEATURE_KEY, 109 &period, 110 seconds, 111 ) 112 .await 113 .map_err(|err| EntitlementError::DatabaseError(err.to_string()))?; 114 115 Ok(()) 116} 117 118fn current_month_period() -> UsagePeriod { 119 let now = Utc::now(); 120 let start = Utc 121 .with_ymd_and_hms(now.year(), now.month(), 1, 0, 0, 0) 122 .single() 123 .unwrap_or(now); 124 let end = if now.month() == 12 { 125 Utc.with_ymd_and_hms(now.year() + 1, 1, 1, 0, 0, 0) 126 .single() 127 .unwrap_or(start + Duration::days(31)) 128 } else { 129 Utc.with_ymd_and_hms(now.year(), now.month() + 1, 1, 0, 0, 0) 130 .single() 131 .unwrap_or(start + Duration::days(31)) 132 }; 133 134 UsagePeriod { start, end } 135}