use crate::client::SchwabClient;
use crate::error::Result;
use crate::secrets::{AccountNumber, CustomerId};
#[derive(Debug)]
pub struct UserPreferences<'a> {
client: &'a SchwabClient,
}
impl<'a> UserPreferences<'a> {
pub(crate) fn new(client: &'a SchwabClient) -> Self {
Self { client }
}
pub async fn get(&self) -> Result<Vec<UserPreference>> {
self.client.trader_http().get_json("/userPreference").await
}
}
#[derive(Debug, Clone, serde::Deserialize)]
#[non_exhaustive]
pub struct UserPreference {
#[serde(rename = "accounts", default)]
pub accounts: Vec<UserPreferenceAccount>,
#[serde(rename = "streamerInfo", default)]
pub streamer_info: Vec<StreamerInfo>,
#[serde(rename = "offers", default)]
pub offers: Vec<Offer>,
}
#[derive(Debug, Clone, serde::Deserialize)]
#[non_exhaustive]
pub struct UserPreferenceAccount {
#[serde(rename = "accountNumber")]
pub account_number: Option<AccountNumber>,
#[serde(rename = "primaryAccount", default)]
pub primary_account: bool,
#[serde(rename = "type")]
pub account_type: Option<String>,
#[serde(rename = "nickName")]
pub nickname: Option<String>,
#[serde(rename = "accountColor")]
pub account_color: Option<String>,
#[serde(rename = "displayAcctId")]
pub display_account_id: Option<AccountNumber>,
#[serde(rename = "autoPositionEffect", default)]
pub auto_position_effect: bool,
}
#[derive(Debug, Clone, serde::Deserialize)]
#[non_exhaustive]
pub struct StreamerInfo {
#[serde(rename = "streamerSocketUrl")]
pub streamer_socket_url: Option<String>,
#[serde(rename = "schwabClientCustomerId")]
pub schwab_client_customer_id: Option<CustomerId>,
#[serde(rename = "schwabClientCorrelId")]
pub schwab_client_correlation_id: Option<String>,
#[serde(rename = "schwabClientChannel")]
pub schwab_client_channel: Option<String>,
#[serde(rename = "schwabClientFunctionId")]
pub schwab_client_function_id: Option<String>,
}
#[derive(Debug, Clone, serde::Deserialize, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct Offer {
#[serde(rename = "level2Permissions", default)]
pub level2_permissions: bool,
#[serde(rename = "mktDataPermission")]
pub market_data_permission: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn deserializes_canonical_payload() {
let body = r#"[
{
"accounts": [
{
"accountNumber": "12345678",
"primaryAccount": true,
"type": "MARGIN",
"nickName": "main",
"accountColor": "Green",
"displayAcctId": "...5678",
"autoPositionEffect": false
}
],
"streamerInfo": [
{
"streamerSocketUrl": "wss://streamer-api.schwab.com/ws",
"schwabClientCustomerId": "CUSTID",
"schwabClientCorrelId": "abc-123",
"schwabClientChannel": "N9",
"schwabClientFunctionId": "APIAPP"
}
],
"offers": [
{
"level2Permissions": true,
"mktDataPermission": "NP"
}
]
}
]"#;
let prefs: Vec<UserPreference> = serde_json::from_str(body).unwrap();
assert_eq!(prefs.len(), 1);
let p = &prefs[0];
assert_eq!(p.accounts.len(), 1);
assert!(p.accounts[0].primary_account);
assert_eq!(p.accounts[0].nickname.as_deref(), Some("main"));
assert_eq!(p.streamer_info.len(), 1);
assert_eq!(
p.streamer_info[0].streamer_socket_url.as_deref(),
Some("wss://streamer-api.schwab.com/ws"),
);
assert_eq!(p.offers.len(), 1);
assert!(p.offers[0].level2_permissions);
assert_eq!(p.offers[0].market_data_permission.as_deref(), Some("NP"));
}
#[test]
fn deserializes_minimal_payload() {
let body = r#"[
{
"accounts": [{}],
"streamerInfo": [{}],
"offers": [{}]
}
]"#;
let prefs: Vec<UserPreference> = serde_json::from_str(body).unwrap();
assert_eq!(prefs.len(), 1);
let p = &prefs[0];
assert_eq!(p.accounts.len(), 1);
assert!(p.accounts[0].account_number.is_none());
assert!(!p.accounts[0].primary_account);
assert!(!p.accounts[0].auto_position_effect);
assert!(p.accounts[0].nickname.is_none());
assert_eq!(p.streamer_info.len(), 1);
assert!(p.streamer_info[0].streamer_socket_url.is_none());
assert!(p.streamer_info[0].schwab_client_customer_id.is_none());
assert_eq!(p.offers.len(), 1);
assert!(!p.offers[0].level2_permissions);
assert!(p.offers[0].market_data_permission.is_none());
}
#[test]
fn deserializes_when_top_level_arrays_missing() {
let prefs: Vec<UserPreference> = serde_json::from_str("[{}]").unwrap();
assert_eq!(prefs.len(), 1);
assert!(prefs[0].accounts.is_empty());
assert!(prefs[0].streamer_info.is_empty());
assert!(prefs[0].offers.is_empty());
}
#[test]
fn deserializes_empty_array() {
let prefs: Vec<UserPreference> = serde_json::from_str("[]").unwrap();
assert!(prefs.is_empty());
}
}