ai_usagebar/moonshot/
types.rs1use serde::Deserialize;
11
12use crate::error::{AppError, Result};
13use crate::usage::{MoonshotSnapshot, finite_amount};
14
15#[derive(Debug, Clone, Deserialize)]
19pub struct BalanceEnvelope {
20 pub code: i64,
21 pub data: BalanceData,
22 pub status: bool,
23}
24
25impl BalanceEnvelope {
26 pub fn check_ok(&self) -> Result<()> {
28 if self.status && self.code == 0 {
29 return Ok(());
30 }
31 Err(AppError::Schema(format!(
32 "moonshot: API reported failure (code {}, status {})",
33 self.code, self.status
34 )))
35 }
36}
37
38#[derive(Debug, Clone, Deserialize)]
41pub struct BalanceData {
42 pub available_balance: f64,
43 pub voucher_balance: f64,
44 pub cash_balance: f64,
45}
46
47pub fn to_snapshot(data: BalanceData, currency: &str) -> Result<MoonshotSnapshot> {
48 Ok(MoonshotSnapshot {
49 available: finite_amount("moonshot", "available_balance", data.available_balance)?,
50 voucher: finite_amount("moonshot", "voucher_balance", data.voucher_balance)?,
51 cash: finite_amount("moonshot", "cash_balance", data.cash_balance)?,
52 currency: currency.to_string(),
53 })
54}
55
56#[cfg(test)]
57mod tests {
58 use super::*;
59
60 #[test]
61 fn parses_documented_envelope() {
62 let raw = r#"{
63 "code": 0,
64 "data": {
65 "available_balance": 49.58894,
66 "voucher_balance": 46.58893,
67 "cash_balance": 3.00001
68 },
69 "scode": "0x0",
70 "status": true
71 }"#;
72 let env: BalanceEnvelope = serde_json::from_str(raw).unwrap();
73 assert_eq!(env.code, 0);
74 assert!(env.status);
75 env.check_ok().unwrap();
76 let snap = to_snapshot(env.data, "USD").unwrap();
77 assert!((snap.available - 49.58894).abs() < 1e-9);
78 assert!((snap.voucher - 46.58893).abs() < 1e-9);
79 assert!((snap.cash - 3.00001).abs() < 1e-9);
80 assert_eq!(snap.currency, "USD");
81 }
82
83 #[test]
84 fn missing_message_field_is_fine() {
85 let raw = r#"{"code":0,"data":{"available_balance":1.0,"voucher_balance":0.0,
87 "cash_balance":1.0},"status":true}"#;
88 let env: BalanceEnvelope = serde_json::from_str(raw).unwrap();
89 assert_eq!(env.data.available_balance, 1.0);
90 }
91
92 #[test]
93 fn in_band_failure_is_rejected() {
94 let raw = r#"{"code":40100,"data":{"available_balance":0.0,
96 "voucher_balance":0.0,"cash_balance":0.0},"status":false}"#;
97 let env: BalanceEnvelope = serde_json::from_str(raw).unwrap();
98 assert!(env.check_ok().is_err());
99
100 let raw2 = r#"{"code":500,"data":{"available_balance":0.0,
102 "voucher_balance":0.0,"cash_balance":0.0},"status":true}"#;
103 let env2: BalanceEnvelope = serde_json::from_str(raw2).unwrap();
104 assert!(env2.check_ok().is_err());
105 }
106
107 #[test]
108 fn missing_fields_are_a_schema_error_not_zero() {
109 assert!(serde_json::from_str::<BalanceEnvelope>("{}").is_err());
110 assert!(
112 serde_json::from_str::<BalanceEnvelope>(
113 r#"{"code":0,"data":{"available_balance":1.0},"status":true}"#
114 )
115 .is_err()
116 );
117 }
118
119 #[test]
120 fn non_finite_balance_is_rejected() {
121 let d = BalanceData {
122 available_balance: f64::INFINITY,
123 voucher_balance: 0.0,
124 cash_balance: 0.0,
125 };
126 assert!(to_snapshot(d, "USD").is_err());
127 }
128}