Skip to main content

schwab_api/
query.rs

1use chrono::{Duration, Utc};
2
3/// ISO-8601 format required by Schwab order/transaction endpoints.
4pub fn iso8601_ms(dt: chrono::DateTime<Utc>) -> String {
5    dt.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string()
6}
7
8/// Per-account orders: default last 30 days (API allows up to 1 year).
9pub fn default_order_window() -> (String, String) {
10    window_days(30)
11}
12
13/// All-accounts orders: `fromEnteredTime` must be within 60 days of today.
14pub fn default_orders_all_window() -> (String, String) {
15    window_days(60)
16}
17
18/// Transactions: default last 30 days (API allows up to 1 year).
19pub fn default_transaction_window() -> (String, String) {
20    window_days(30)
21}
22
23fn window_days(days: i64) -> (String, String) {
24    let to = Utc::now();
25    let from = to - Duration::days(days);
26    (iso8601_ms(from), iso8601_ms(to))
27}
28
29/// Schwab requires both ends of a range when either is supplied.
30pub fn resolve_time_range(
31    from: Option<&str>,
32    to: Option<&str>,
33    default: impl FnOnce() -> (String, String),
34) -> Result<(String, String), String> {
35    match (from, to) {
36        (Some(f), Some(t)) => Ok((f.to_string(), t.to_string())),
37        (None, None) => Ok(default()),
38        _ => Err("Provide both range parameters or omit both to use CLI defaults".into()),
39    }
40}
41
42#[cfg(test)]
43mod tests {
44    use super::*;
45
46    #[test]
47    fn default_window_is_valid_iso8601() {
48        let (from, to) = default_order_window();
49        assert!(from.ends_with('Z'));
50        assert!(to.ends_with('Z'));
51        assert!(from < to);
52    }
53
54    #[test]
55    fn resolve_requires_both_or_neither() {
56        assert!(resolve_time_range(Some("a"), None, default_order_window).is_err());
57        assert!(resolve_time_range(None, None, default_order_window).is_ok());
58    }
59}