use crate::client::Client;
use crate::error::Error;
use nordnet_model::ids::AccountId;
use nordnet_model::models::accounts::{
Account, AccountInfo, AccountTransactionsToday, LedgerInformation, Position, Trade,
};
#[derive(Debug, Clone, Default)]
pub struct ListAccountsQuery {
pub include_credit_accounts: Option<bool>,
}
#[derive(Debug, Clone, Default)]
pub struct AccountInfoQuery {
pub include_interest_rate: Option<bool>,
pub include_short_pos_margin: Option<bool>,
}
#[derive(Debug, Clone, Default)]
pub struct ListPositionsQuery {
pub include_instrument_loans: Option<bool>,
pub include_intraday_limit: Option<bool>,
}
#[derive(Debug, Clone, Default)]
pub struct ReturnsTodayQuery {
pub include_credit_account: Option<bool>,
}
#[derive(Debug, Clone, Default)]
pub struct ListAccountTradesQuery {
pub days: Option<i64>,
}
fn build_query(pairs: &[(&str, String)]) -> String {
if pairs.is_empty() {
return String::new();
}
let mut url = match reqwest::Url::parse("http://_/") {
Ok(u) => u,
Err(_) => return String::new(),
};
{
let mut qs = url.query_pairs_mut();
for (k, v) in pairs {
qs.append_pair(k, v);
}
}
url.query().unwrap_or("").to_owned()
}
fn append_bool(pairs: &mut Vec<(&'static str, String)>, k: &'static str, v: Option<bool>) {
if let Some(b) = v {
pairs.push((k, b.to_string()));
}
}
impl Client {
#[doc(alias = "GET /accounts")]
pub async fn list_accounts(&self, query: ListAccountsQuery) -> Result<Vec<Account>, Error> {
let mut pairs = Vec::new();
append_bool(
&mut pairs,
"include_credit_accounts",
query.include_credit_accounts,
);
let qs = build_query(&pairs);
let path = if qs.is_empty() {
"/accounts".to_owned()
} else {
format!("/accounts?{qs}")
};
match self.get::<Vec<Account>>(&path).await {
Ok(v) => Ok(v),
Err(Error::Decode { ref body, .. }) if body.trim().is_empty() => Ok(vec![]),
Err(e) => Err(e),
}
}
#[doc(alias = "GET /accounts/{accid}/info")]
pub async fn get_account_info(
&self,
accid: AccountId,
query: AccountInfoQuery,
) -> Result<Vec<AccountInfo>, Error> {
let mut pairs = Vec::new();
append_bool(
&mut pairs,
"include_interest_rate",
query.include_interest_rate,
);
append_bool(
&mut pairs,
"include_short_pos_margin",
query.include_short_pos_margin,
);
let qs = build_query(&pairs);
let path = if qs.is_empty() {
format!("/accounts/{accid}/info")
} else {
format!("/accounts/{accid}/info?{qs}")
};
self.get::<Vec<AccountInfo>>(&path).await
}
#[doc(alias = "GET /accounts/{accid}/ledgers")]
pub async fn list_ledgers(&self, accid: AccountId) -> Result<LedgerInformation, Error> {
let path = format!("/accounts/{accid}/ledgers");
self.get::<LedgerInformation>(&path).await
}
#[doc(alias = "GET /accounts/{accid}/positions")]
pub async fn list_positions(
&self,
accid: AccountId,
query: ListPositionsQuery,
) -> Result<Vec<Position>, Error> {
let mut pairs = Vec::new();
append_bool(
&mut pairs,
"include_instrument_loans",
query.include_instrument_loans,
);
append_bool(
&mut pairs,
"include_intraday_limit",
query.include_intraday_limit,
);
let qs = build_query(&pairs);
let path = if qs.is_empty() {
format!("/accounts/{accid}/positions")
} else {
format!("/accounts/{accid}/positions?{qs}")
};
match self.get::<Vec<Position>>(&path).await {
Ok(v) => Ok(v),
Err(Error::Decode { ref body, .. }) if body.trim().is_empty() => Ok(vec![]),
Err(e) => Err(e),
}
}
#[doc(alias = "GET /accounts/{accid}/returns/transactions/today")]
pub async fn get_returns_today(
&self,
accid: AccountId,
query: ReturnsTodayQuery,
) -> Result<Vec<AccountTransactionsToday>, Error> {
let mut pairs = Vec::new();
append_bool(
&mut pairs,
"include_credit_account",
query.include_credit_account,
);
let qs = build_query(&pairs);
let path = if qs.is_empty() {
format!("/accounts/{accid}/returns/transactions/today")
} else {
format!("/accounts/{accid}/returns/transactions/today?{qs}")
};
match self.get::<Vec<AccountTransactionsToday>>(&path).await {
Ok(v) => Ok(v),
Err(Error::Decode { ref body, .. }) if body.trim().is_empty() => Ok(vec![]),
Err(e) => Err(e),
}
}
#[doc(alias = "GET /accounts/{accid}/trades")]
pub async fn list_account_trades(
&self,
accid: AccountId,
query: ListAccountTradesQuery,
) -> Result<Vec<Trade>, Error> {
let mut pairs = Vec::new();
if let Some(d) = query.days {
pairs.push(("days", d.to_string()));
}
let qs = build_query(&pairs);
let path = if qs.is_empty() {
format!("/accounts/{accid}/trades")
} else {
format!("/accounts/{accid}/trades?{qs}")
};
match self.get::<Vec<Trade>>(&path).await {
Ok(v) => Ok(v),
Err(Error::Decode { ref body, .. }) if body.trim().is_empty() => Ok(vec![]),
Err(e) => Err(e),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_query_empty_when_no_pairs() {
assert_eq!(build_query(&[]), "");
}
#[test]
fn build_query_pairs_in_order_and_encoded() {
let qs = build_query(&[
("days", "7".to_owned()),
("include_credit_accounts", "true".to_owned()),
("name", "a&b".to_owned()),
]);
assert_eq!(qs, "days=7&include_credit_accounts=true&name=a%26b");
}
#[test]
fn append_bool_skips_when_none() {
let mut pairs = Vec::new();
append_bool(&mut pairs, "include_credit_accounts", None);
assert!(pairs.is_empty());
}
#[test]
fn append_bool_emits_lowercase_when_some() {
let mut pairs = Vec::new();
append_bool(&mut pairs, "include_credit_accounts", Some(true));
append_bool(&mut pairs, "include_intraday_limit", Some(false));
assert_eq!(
pairs,
vec![
("include_credit_accounts", "true".to_owned()),
("include_intraday_limit", "false".to_owned()),
]
);
}
}