Skip to main content

ai_usagebar/deepseek/
types.rs

1//! Wire types for DeepSeek's `/user/balance` endpoint.
2
3use serde::Deserialize;
4
5use crate::usage::DeepseekSnapshot;
6
7#[derive(Debug, Clone, Deserialize, Default)]
8#[serde(default)]
9pub struct BalanceResponse {
10    pub is_available: bool,
11    pub balance_infos: Vec<BalanceInfo>,
12}
13
14#[derive(Debug, Clone, Deserialize, Default)]
15#[serde(default)]
16pub struct BalanceInfo {
17    pub currency: String,
18    pub total_balance: String,
19    pub granted_balance: String,
20    pub topped_up_balance: String,
21}
22
23impl BalanceResponse {
24    pub fn into_snapshot(self) -> DeepseekSnapshot {
25        // Prefer USD, fall back to CNY, then whatever's first.
26        let info = self
27            .balance_infos
28            .iter()
29            .find(|b| b.currency == "USD")
30            .or_else(|| self.balance_infos.iter().find(|b| b.currency == "CNY"))
31            .or_else(|| self.balance_infos.first())
32            .cloned()
33            .unwrap_or_default();
34
35        DeepseekSnapshot {
36            is_available: self.is_available,
37            balance: parse_f64(&info.total_balance),
38            granted: parse_f64(&info.granted_balance),
39            topped_up: parse_f64(&info.topped_up_balance),
40            currency: info.currency,
41        }
42    }
43}
44
45fn parse_f64(s: &str) -> f64 {
46    s.trim().parse::<f64>().unwrap_or(0.0)
47}
48
49#[cfg(test)]
50mod tests {
51    use super::*;
52
53    #[test]
54    fn parses_balance_response() {
55        let raw = r#"{
56            "is_available": true,
57            "balance_infos": [
58                {"currency": "CNY", "total_balance": "10.00", "granted_balance": "10.00", "topped_up_balance": "0.00"},
59                {"currency": "USD", "total_balance": "1.50", "granted_balance": "1.50", "topped_up_balance": "0.00"}
60            ]
61        }"#;
62        let r: BalanceResponse = serde_json::from_str(raw).unwrap();
63        let snap = r.into_snapshot();
64        assert!(snap.is_available);
65        assert_eq!(snap.currency, "USD");
66        assert!((snap.balance - 1.50).abs() < 1e-9);
67    }
68
69    #[test]
70    fn fallback_to_cny_when_no_usd() {
71        let raw = r#"{
72            "is_available": true,
73            "balance_infos": [
74                {"currency": "CNY", "total_balance": "20.00", "granted_balance": "20.00", "topped_up_balance": "0.00"}
75            ]
76        }"#;
77        let r: BalanceResponse = serde_json::from_str(raw).unwrap();
78        let snap = r.into_snapshot();
79        assert_eq!(snap.currency, "CNY");
80        assert!((snap.balance - 20.0).abs() < 1e-9);
81    }
82
83    #[test]
84    fn empty_balance_infos() {
85        let raw = r#"{"is_available": false, "balance_infos": []}"#;
86        let r: BalanceResponse = serde_json::from_str(raw).unwrap();
87        let snap = r.into_snapshot();
88        assert!(!snap.is_available);
89        assert_eq!(snap.balance, 0.0);
90        assert_eq!(snap.currency, "");
91    }
92}