Skip to main content

ccboard_core/
usage_estimator.rs

1//! Usage estimation based on billing blocks and subscription plan
2//!
3//! Provides estimated usage metrics (today, week, month) with comparison
4//! to subscription plan limits.
5
6use crate::models::billing_block::BillingBlockManager;
7use chrono::{Datelike, Local, NaiveDate};
8
9/// Subscription plan types with approximate monthly budgets
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum SubscriptionPlan {
12    /// Claude Pro (~$20/month)
13    Pro,
14    /// Claude Max 5x (~$100/month)
15    Max5x,
16    /// Claude Max 20x (~$200/month)
17    Max20x,
18    /// API usage (pay-as-you-go, no fixed limit)
19    Api,
20    /// Unknown/unset plan
21    Unknown,
22}
23
24impl SubscriptionPlan {
25    /// Parse plan from string (from settings.json)
26    pub fn from_str(s: &str) -> Self {
27        match s.to_lowercase().as_str() {
28            "pro" => Self::Pro,
29            "max5x" | "max-5x" | "max_5x" => Self::Max5x,
30            "max20x" | "max-20x" | "max_20x" => Self::Max20x,
31            "api" => Self::Api,
32            _ => Self::Unknown,
33        }
34    }
35
36    /// Get approximate monthly budget in USD
37    ///
38    /// Note: These are subscription costs, not spending limits.
39    /// Max plans have rate limits (requests/day), not fixed monthly budgets.
40    /// Use these values as reference points for cost estimation.
41    pub fn monthly_budget_usd(self) -> Option<f64> {
42        match self {
43            Self::Pro => Some(20.0),
44            Self::Max5x => Some(50.0), // Updated from $100 to $50 (2025 pricing)
45            Self::Max20x => Some(200.0),
46            Self::Api => None, // Pay-as-you-go, no fixed limit
47            Self::Unknown => None,
48        }
49    }
50
51    /// Get display name
52    pub fn display_name(self) -> &'static str {
53        match self {
54            Self::Pro => "Claude Pro",
55            Self::Max5x => "Claude Max 5x",
56            Self::Max20x => "Claude Max 20x",
57            Self::Api => "API (Pay-as-you-go)",
58            Self::Unknown => "Unknown Plan",
59        }
60    }
61}
62
63/// Estimated usage metrics
64#[derive(Debug, Clone, Default)]
65pub struct UsageEstimate {
66    /// Cost today in USD
67    pub cost_today: f64,
68    /// Cost this week in USD
69    pub cost_week: f64,
70    /// Cost this month in USD
71    pub cost_month: f64,
72    /// Subscription plan
73    pub plan: SubscriptionPlan,
74    /// Monthly budget (if applicable)
75    pub budget_usd: Option<f64>,
76}
77
78impl UsageEstimate {
79    /// Calculate percentage used for today (if budget exists)
80    pub fn percent_today(&self) -> Option<f64> {
81        self.budget_usd
82            .map(|budget| (self.cost_today / budget * 100.0).min(100.0))
83    }
84
85    /// Calculate percentage used for week (if budget exists)
86    pub fn percent_week(&self) -> Option<f64> {
87        self.budget_usd
88            .map(|budget| (self.cost_week / budget * 100.0).min(100.0))
89    }
90
91    /// Calculate percentage used for month (if budget exists)
92    pub fn percent_month(&self) -> Option<f64> {
93        self.budget_usd
94            .map(|budget| (self.cost_month / budget * 100.0).min(100.0))
95    }
96}
97
98impl Default for SubscriptionPlan {
99    fn default() -> Self {
100        Self::Unknown
101    }
102}
103
104/// Calculate usage estimate from billing blocks
105pub fn calculate_usage_estimate(
106    billing_blocks: &BillingBlockManager,
107    plan: SubscriptionPlan,
108) -> UsageEstimate {
109    let now = Local::now();
110    let today = now.date_naive();
111    let week_start = today - chrono::Duration::days(today.weekday().num_days_from_monday() as i64);
112    let month_start = NaiveDate::from_ymd_opt(today.year(), today.month(), 1).unwrap();
113
114    let mut cost_today = 0.0;
115    let mut cost_week = 0.0;
116    let mut cost_month = 0.0;
117
118    // Sum costs from billing blocks
119    for (block, usage) in billing_blocks.get_all_blocks() {
120        let block_date = block.date;
121        let cost = usage.total_cost;
122
123        if block_date == today {
124            cost_today += cost;
125        }
126        if block_date >= week_start {
127            cost_week += cost;
128        }
129        if block_date >= month_start {
130            cost_month += cost;
131        }
132    }
133
134    UsageEstimate {
135        cost_today,
136        cost_week,
137        cost_month,
138        plan,
139        budget_usd: plan.monthly_budget_usd(),
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    #[test]
148    fn test_parse_plan() {
149        assert_eq!(SubscriptionPlan::from_str("pro"), SubscriptionPlan::Pro);
150        assert_eq!(SubscriptionPlan::from_str("max5x"), SubscriptionPlan::Max5x);
151        assert_eq!(
152            SubscriptionPlan::from_str("max-20x"),
153            SubscriptionPlan::Max20x
154        );
155        assert_eq!(SubscriptionPlan::from_str("api"), SubscriptionPlan::Api);
156        assert_eq!(
157            SubscriptionPlan::from_str("unknown"),
158            SubscriptionPlan::Unknown
159        );
160    }
161
162    #[test]
163    fn test_monthly_budget() {
164        assert_eq!(SubscriptionPlan::Pro.monthly_budget_usd(), Some(20.0));
165        assert_eq!(SubscriptionPlan::Max5x.monthly_budget_usd(), Some(50.0));
166        assert_eq!(SubscriptionPlan::Max20x.monthly_budget_usd(), Some(200.0));
167        assert_eq!(SubscriptionPlan::Api.monthly_budget_usd(), None);
168        assert_eq!(SubscriptionPlan::Unknown.monthly_budget_usd(), None);
169    }
170
171    #[test]
172    fn test_percent_calculation() {
173        let estimate = UsageEstimate {
174            cost_today: 5.0,
175            cost_week: 15.0,
176            cost_month: 40.0,
177            plan: SubscriptionPlan::Max5x,
178            budget_usd: Some(50.0),
179        };
180
181        assert_eq!(estimate.percent_today(), Some(10.0));
182        assert_eq!(estimate.percent_week(), Some(30.0));
183        assert_eq!(estimate.percent_month(), Some(80.0));
184    }
185
186    #[test]
187    fn test_no_budget() {
188        let estimate = UsageEstimate {
189            cost_today: 5.0,
190            cost_week: 15.0,
191            cost_month: 40.0,
192            plan: SubscriptionPlan::Api,
193            budget_usd: None,
194        };
195
196        assert_eq!(estimate.percent_today(), None);
197        assert_eq!(estimate.percent_week(), None);
198        assert_eq!(estimate.percent_month(), None);
199    }
200}