use chrono::{DateTime, Utc};
use serde::Deserialize;
use crate::error::{AppError, Result};
use crate::usage::GrokbotSnapshot;
#[derive(Debug, Clone, Deserialize, Default)]
#[serde(default, rename_all = "camelCase")]
pub struct SandUsageStatus {
current_period_start: Option<String>,
next_reset_timestamp_utc: Option<String>,
usage_percent: Option<PercentOrString>,
has_available_usage: bool,
has_non_zero_included_limit: bool,
on_demand_settings: OnDemandSettings,
grok_plan_label: Option<String>,
cursor_plan_name: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Default)]
#[serde(default, rename_all = "camelCase")]
struct OnDemandSettings {
visible: bool,
eligible: bool,
enabled: bool,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
enum PercentOrString {
Int(i64),
Text(String),
}
impl SandUsageStatus {
pub fn into_snapshot(self) -> Result<GrokbotSnapshot> {
let plan = [self.grok_plan_label, self.cursor_plan_name]
.into_iter()
.flatten()
.map(|label| label.trim().to_string())
.find(|label| !label.is_empty())
.unwrap_or_else(|| "Grok Bot".to_string());
let period_start = parse_timestamp("currentPeriodStart", self.current_period_start)?;
let reset_at = parse_timestamp("nextResetTimestampUtc", self.next_reset_timestamp_utc)?;
let window = match (period_start, reset_at) {
(Some(start), Some(reset)) if reset > start => Some(reset - start),
_ => None,
};
let weekly_pct = if self.has_non_zero_included_limit {
let percent = self
.usage_percent
.ok_or_else(|| AppError::Schema("grokbot: missing usagePercent".into()))?;
parse_percent(percent)?
} else {
0
};
Ok(GrokbotSnapshot {
plan,
has_included_allowance: self.has_non_zero_included_limit,
weekly_pct,
has_available_usage: self.has_available_usage,
on_demand_enabled: self.on_demand_settings.enabled,
period_start,
reset_at,
window,
})
}
}
fn parse_percent(value: PercentOrString) -> Result<i32> {
let raw = match value {
PercentOrString::Int(n) => n as f64,
PercentOrString::Text(s) => s.trim().parse::<f64>().map_err(|_| {
AppError::Schema(format!("grokbot: usagePercent is not numeric (got {s:?})"))
})?,
};
if !raw.is_finite() || !(0.0..=100.0).contains(&raw) {
return Err(AppError::Schema(format!(
"grokbot: usagePercent out of range: {raw}"
)));
}
Ok(raw.round() as i32)
}
fn parse_timestamp(field: &str, value: Option<String>) -> Result<Option<DateTime<Utc>>> {
match value {
None => Ok(None),
Some(s) if s.trim().is_empty() => Ok(None),
Some(s) => DateTime::parse_from_rfc3339(&s)
.map(|dt| Some(dt.with_timezone(&Utc)))
.map_err(|e| AppError::Schema(format!("grokbot: unparseable {field}: {e}"))),
}
}
#[cfg(test)]
mod tests {
use super::*;
const CAPTURE: &str = r#"{"currentPeriodStart":"2026-09-11T18:43:19.645Z","nextResetTimestampUtc":"2026-09-18T18:43:19.645Z","usagePercent":0,"hasAvailableUsage":true,"hasNonZeroIncludedLimit":true,"onDemandSettings":{"visible":true,"eligible":true,"enabled":false,"dashboardUrl":"https://cursor.com/dashboard?team=acct-123-secret"},"grokPlanLabel":"Grok Bot Plan","cursorPlanName":"Pro"}"#;
#[test]
fn the_verbatim_capture_parses() {
let snap = serde_json::from_str::<SandUsageStatus>(CAPTURE)
.unwrap()
.into_snapshot()
.unwrap();
assert_eq!(snap.plan, "Grok Bot Plan");
assert!(snap.has_included_allowance);
assert_eq!(snap.weekly_pct, 0);
assert!(snap.has_available_usage);
assert!(!snap.on_demand_enabled);
assert_eq!(
snap.period_start.map(|dt| dt.to_rfc3339()),
Some("2026-09-11T18:43:19.645+00:00".to_string())
);
assert_eq!(
snap.reset_at.map(|dt| dt.to_rfc3339()),
Some("2026-09-18T18:43:19.645+00:00".to_string())
);
assert_eq!(snap.window, Some(chrono::Duration::days(7)));
}
#[test]
fn the_dashboard_url_cannot_be_held_or_rendered() {
let status = serde_json::from_str::<SandUsageStatus>(CAPTURE).unwrap();
let rendered = format!("{status:?}");
assert!(!rendered.contains("dashboardUrl"), "{rendered}");
assert!(!rendered.contains("acct-123-secret"), "{rendered}");
let snap = status.into_snapshot().unwrap();
assert!(!format!("{snap:?}").contains("acct-123-secret"));
}
#[test]
fn usage_percent_accepts_an_int_or_a_numeric_string() {
for (raw, expected) in [
(r#""usagePercent": 42"#, 42),
(r#""usagePercent": "42""#, 42),
] {
let json = format!(r#"{{"hasNonZeroIncludedLimit": true, {raw}}}"#);
let snap = serde_json::from_str::<SandUsageStatus>(&json)
.unwrap()
.into_snapshot()
.unwrap();
assert_eq!(snap.weekly_pct, expected, "{raw}");
}
}
#[test]
fn out_of_range_and_non_numeric_percents_are_schema_drift() {
for raw in [
r#""usagePercent": 150"#,
r#""usagePercent": -1"#,
r#""usagePercent": "NaN""#,
r#""usagePercent": "garbage""#,
r#""usagePercent": "101""#,
] {
let json = format!(r#"{{"hasNonZeroIncludedLimit": true, {raw}}}"#);
let err = serde_json::from_str::<SandUsageStatus>(&json)
.unwrap()
.into_snapshot()
.unwrap_err();
assert!(matches!(err, AppError::Schema(_)), "{raw}: {err:?}");
}
}
#[test]
fn a_missing_reset_is_none_not_an_error() {
let json = r#"{"hasNonZeroIncludedLimit": true, "usagePercent": 10}"#;
let snap = serde_json::from_str::<SandUsageStatus>(json)
.unwrap()
.into_snapshot()
.unwrap();
assert_eq!(snap.reset_at, None);
assert_eq!(snap.period_start, None);
assert_eq!(snap.window, None);
}
#[test]
fn an_unparseable_reset_is_schema_drift() {
let json = r#"{"hasNonZeroIncludedLimit": true, "usagePercent": 10,
"nextResetTimestampUtc": "next tuesday"}"#;
let err = serde_json::from_str::<SandUsageStatus>(json)
.unwrap()
.into_snapshot()
.unwrap_err();
assert!(err.to_string().contains("nextResetTimestampUtc"), "{err}");
}
#[test]
fn no_included_limit_is_the_no_allowance_state_not_zero_percent() {
let json = r#"{"hasNonZeroIncludedLimit": false, "hasAvailableUsage": false,
"usagePercent": 0, "grokPlanLabel": "Grok Bot Plan"}"#;
let snap = serde_json::from_str::<SandUsageStatus>(json)
.unwrap()
.into_snapshot()
.unwrap();
assert!(!snap.has_included_allowance);
assert_eq!(snap.weekly_pct, 0);
let json = r#"{"hasNonZeroIncludedLimit": true}"#;
let err = serde_json::from_str::<SandUsageStatus>(json)
.unwrap()
.into_snapshot()
.unwrap_err();
assert!(err.to_string().contains("usagePercent"), "{err}");
}
#[test]
fn the_on_demand_note_needs_an_exhausted_pool_and_on_demand_enabled() {
let base = |pct: i64, available: bool, enabled: bool| {
let json = format!(
r#"{{"hasNonZeroIncludedLimit": true, "usagePercent": {pct},
"hasAvailableUsage": {available},
"onDemandSettings": {{"enabled": {enabled}}}}}"#
);
serde_json::from_str::<SandUsageStatus>(&json)
.unwrap()
.into_snapshot()
.unwrap()
};
assert!(base(100, true, true).on_demand_note().is_some());
assert!(base(100, true, false).on_demand_note().is_none());
assert!(base(100, false, true).on_demand_note().is_none());
assert!(base(99, true, true).on_demand_note().is_none());
}
#[test]
fn the_plan_label_prefers_grok_and_falls_back_to_cursor_plan() {
let json =
r#"{"hasNonZeroIncludedLimit": true, "usagePercent": 5, "cursorPlanName": "Pro"}"#;
let snap = serde_json::from_str::<SandUsageStatus>(json)
.unwrap()
.into_snapshot()
.unwrap();
assert_eq!(snap.plan, "Pro");
let json = r#"{"hasNonZeroIncludedLimit": true, "usagePercent": 5}"#;
let snap = serde_json::from_str::<SandUsageStatus>(json)
.unwrap()
.into_snapshot()
.unwrap();
assert_eq!(snap.plan, "Grok Bot");
}
}