ig_client/application/services/
account_service.rs

1use crate::application::services::AccountService;
2use crate::{
3    application::models::account::{
4        AccountActivity, AccountInfo, Positions, TransactionHistory, WorkingOrders,
5    },
6    config::Config,
7    error::AppError,
8    session::interface::IgSession,
9    transport::http_client::IgHttpClient,
10};
11use async_trait::async_trait;
12use reqwest::Method;
13use std::sync::Arc;
14use tracing::{debug, info};
15
16/// Implementation of the account service
17pub struct AccountServiceImpl<T: IgHttpClient> {
18    config: Arc<Config>,
19    client: Arc<T>,
20}
21
22impl<T: IgHttpClient> AccountServiceImpl<T> {
23    /// Creates a new instance of the account service
24    pub fn new(config: Arc<Config>, client: Arc<T>) -> Self {
25        Self { config, client }
26    }
27
28    /// Gets the current configuration
29    ///
30    /// # Returns
31    /// * The current configuration as an `Arc<Config>`
32    pub fn get_config(&self) -> Arc<Config> {
33        self.config.clone()
34    }
35
36    /// Sets a new configuration
37    ///
38    /// # Arguments
39    /// * `config` - The new configuration to use
40    pub fn set_config(&mut self, config: Arc<Config>) {
41        self.config = config;
42    }
43}
44
45#[async_trait]
46impl<T: IgHttpClient + 'static> AccountService for AccountServiceImpl<T> {
47    async fn get_accounts(&self, session: &IgSession) -> Result<AccountInfo, AppError> {
48        info!("Getting account information");
49
50        let result = self
51            .client
52            .request::<(), AccountInfo>(Method::GET, "accounts", session, None, "1")
53            .await?;
54
55        debug!(
56            "Account information obtained: {} accounts",
57            result.accounts.len()
58        );
59        Ok(result)
60    }
61
62    async fn get_positions(&self, session: &IgSession) -> Result<Positions, AppError> {
63        debug!("Getting open positions");
64
65        let result = self
66            .client
67            .request::<(), Positions>(Method::GET, "positions", session, None, "2")
68            .await?;
69
70        debug!("Positions obtained: {} positions", result.positions.len());
71        Ok(result)
72    }
73
74    /// Retrieves the current open positions and applies a client-side filter to them.
75    ///
76    /// This function first delegates to `get_positions` to fetch all open positions
77    /// and then narrows the result set in-memory using the provided `filter`.
78    /// The exact matching logic is implementation-defined for now (based on how the
79    /// filter predicate is applied within this function).
80    ///
81    /// Logging:
82    /// - Emits a debug log before and after fetching/filtering to aid observability.
83    ///
84    /// Arguments:
85    /// - `filter`: A string used to filter the list of positions. The matching
86    ///   semantics depend on the predicate used in this function (e.g., substring
87    ///   match against one or more `Position` fields).
88    /// - `session`: An authenticated IG session used to authorize the HTTP request.
89    ///
90    /// Returns:
91    /// - `Ok(Positions)`: The filtered collection of open positions.
92    /// - `Err(AppError)`: If fetching or processing fails. Possible error sources
93    ///   include network issues, deserialization errors, authorization failures,
94    ///   rate limiting, or invalid input.
95    ///
96    async fn get_positions_w_filter(
97        &self,
98        filter: &str,
99        session: &IgSession,
100    ) -> Result<Positions, AppError> {
101        debug!("Getting open positions with filter: {}", filter);
102
103        let mut positions = self.get_positions(session).await?;
104
105        positions
106            .positions
107            .retain(|position| position.market.epic.contains(filter));
108
109        debug!(
110            "Positions obtained after filtering: {} positions",
111            positions.positions.len()
112        );
113        Ok(positions)
114    }
115
116    async fn get_working_orders(&self, session: &IgSession) -> Result<WorkingOrders, AppError> {
117        info!("Getting working orders");
118
119        let result = self
120            .client
121            .request::<(), WorkingOrders>(Method::GET, "workingorders", session, None, "2")
122            .await?;
123
124        debug!(
125            "Working orders obtained: {} orders",
126            result.working_orders.len()
127        );
128        Ok(result)
129    }
130
131    async fn get_activity(
132        &self,
133        session: &IgSession,
134        from: &str,
135        to: &str,
136    ) -> Result<AccountActivity, AppError> {
137        let path = format!("history/activity?from={from}&to={to}&pageSize=500");
138        info!("Getting account activity");
139
140        let result = self
141            .client
142            .request::<(), AccountActivity>(Method::GET, &path, session, None, "3")
143            .await?;
144
145        debug!(
146            "Account activity obtained: {} activities",
147            result.activities.len()
148        );
149        Ok(result)
150    }
151
152    async fn get_activity_with_details(
153        &self,
154        session: &IgSession,
155        from: &str,
156        to: &str,
157    ) -> Result<AccountActivity, AppError> {
158        let path = format!("history/activity?from={from}&to={to}&detailed=true&pageSize=500");
159        info!("Getting detailed account activity");
160
161        let result = self
162            .client
163            .request::<(), AccountActivity>(Method::GET, &path, session, None, "3")
164            .await?;
165
166        debug!(
167            "Detailed account activity obtained: {} activities",
168            result.activities.len()
169        );
170        Ok(result)
171    }
172
173    async fn get_transactions(
174        &self,
175        session: &IgSession,
176        from: &str,
177        to: &str,
178    ) -> Result<TransactionHistory, AppError> {
179        const PAGE_SIZE: u32 = 200;
180        let mut all_transactions = Vec::new();
181        let mut current_page = 1;
182        #[allow(unused_assignments)]
183        let mut last_metadata = None;
184
185        loop {
186            let path = format!(
187                "history/transactions?from={from}&to={to}&pageSize={PAGE_SIZE}&pageNumber={current_page}"
188            );
189            info!("Getting transaction history page {}", current_page);
190
191            let result = self
192                .client
193                .request::<(), TransactionHistory>(Method::GET, &path, session, None, "2")
194                .await?;
195
196            let total_pages = result.metadata.page_data.total_pages as u32;
197            last_metadata = Some(result.metadata);
198            all_transactions.extend(result.transactions);
199
200            if current_page >= total_pages {
201                break;
202            }
203            current_page += 1;
204        }
205
206        debug!(
207            "Total transaction history obtained: {} transactions",
208            all_transactions.len()
209        );
210
211        Ok(TransactionHistory {
212            transactions: all_transactions,
213            metadata: last_metadata
214                .ok_or_else(|| AppError::InvalidInput("Could not retrieve metadata".to_string()))?,
215        })
216    }
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222    use crate::config::Config;
223    use crate::transport::http_client::IgHttpClientImpl;
224    use crate::utils::rate_limiter::RateLimitType;
225    use std::sync::Arc;
226
227    #[test]
228    fn test_get_and_set_config() {
229        let config = Arc::new(Config::with_rate_limit_type(
230            RateLimitType::TradingAccount,
231            0.7,
232        ));
233        let client = Arc::new(IgHttpClientImpl::new(config.clone()));
234        let mut service = AccountServiceImpl::new(config.clone(), client.clone());
235
236        let cfg1 = service.get_config();
237        assert!(Arc::ptr_eq(&cfg1, &config));
238
239        let new_cfg = Arc::new(Config::default());
240        service.set_config(new_cfg.clone());
241        assert!(Arc::ptr_eq(&service.get_config(), &new_cfg));
242    }
243}