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