ig_client/model/
responses.rs

1/******************************************************************************
2   Author: Joaquín Béjar García
3   Email: jb@taunais.com
4   Date: 19/10/25
5******************************************************************************/
6use crate::prelude::{Account, Activity, MarketDetails};
7use crate::presentation::account::{
8    AccountTransaction, ActivityMetadata, Position, TransactionMetadata, WorkingOrder,
9};
10use crate::presentation::instrument::InstrumentType;
11use crate::presentation::market::{
12    Category, CategoryInstrument, CategoryInstrumentsMetadata, HistoricalPrice, MarketData,
13    MarketNavigationNode, MarketNode, PriceAllowance,
14};
15use crate::presentation::order::{Direction, Status};
16use crate::utils::parsing::{deserialize_null_as_empty_vec, deserialize_nullable_status};
17use chrono::{DateTime, Utc};
18use pretty_simple_display::{DebugPretty, DisplaySimple};
19use serde::{Deserialize, Serialize};
20use std::collections::HashMap;
21
22/// Database entry response for market instruments
23#[derive(
24    DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Default,
25)]
26pub struct DBEntryResponse {
27    /// The trading symbol identifier
28    pub symbol: String,
29    /// The Epic identifier used by the exchange
30    pub epic: String,
31    /// Human-readable name of the instrument
32    pub name: String,
33    /// Instrument type classification
34    pub instrument_type: InstrumentType,
35    /// The exchange where this instrument is traded
36    pub exchange: String,
37    /// Expiration date and time for the instrument
38    pub expiry: String,
39    /// Timestamp of the last update to this record
40    pub last_update: DateTime<Utc>,
41}
42
43impl From<MarketNode> for DBEntryResponse {
44    fn from(value: MarketNode) -> Self {
45        let mut entry = DBEntryResponse::default();
46        if !value.markets.is_empty() {
47            let market = &value.markets[0];
48            entry.symbol = market
49                .epic
50                .split('.')
51                .nth(2)
52                .unwrap_or_default()
53                .to_string();
54            entry.epic = market.epic.clone();
55            entry.name = market.instrument_name.clone();
56            entry.instrument_type = market.instrument_type;
57            entry.exchange = "IG".to_string();
58            entry.expiry = market.expiry.clone();
59            entry.last_update = Utc::now();
60        }
61        entry
62    }
63}
64
65impl From<MarketData> for DBEntryResponse {
66    fn from(market: MarketData) -> Self {
67        DBEntryResponse {
68            symbol: market
69                .epic
70                .split('.')
71                .nth(2)
72                .unwrap_or_default()
73                .to_string(),
74            epic: market.epic.clone(),
75            name: market.instrument_name.clone(),
76            instrument_type: market.instrument_type,
77            exchange: "IG".to_string(),
78            expiry: market.expiry.clone(),
79            last_update: Utc::now(),
80        }
81    }
82}
83
84impl From<&MarketNode> for DBEntryResponse {
85    fn from(value: &MarketNode) -> Self {
86        DBEntryResponse::from(value.clone())
87    }
88}
89
90impl From<&MarketData> for DBEntryResponse {
91    fn from(market: &MarketData) -> Self {
92        DBEntryResponse::from(market.clone())
93    }
94}
95
96/// Response containing multiple market details
97#[derive(DebugPretty, Clone, Serialize, Deserialize, Default)]
98pub struct MultipleMarketDetailsResponse {
99    /// List of market details
100    #[serde(rename = "marketDetails")]
101    pub market_details: Vec<MarketDetails>,
102}
103
104impl MultipleMarketDetailsResponse {
105    /// Returns the number of market details in the response
106    ///
107    /// # Returns
108    /// Number of market details
109    #[must_use]
110    pub fn len(&self) -> usize {
111        self.market_details.len()
112    }
113
114    /// Returns true if the response contains no market details
115    ///
116    /// # Returns
117    /// True if empty, false otherwise
118    #[must_use]
119    pub fn is_empty(&self) -> bool {
120        self.market_details.is_empty()
121    }
122
123    /// Returns a reference to the market details vector
124    ///
125    /// # Returns
126    /// Reference to the vector of market details
127    #[must_use]
128    pub fn market_details(&self) -> &Vec<MarketDetails> {
129        &self.market_details
130    }
131
132    /// Returns an iterator over the market details
133    ///
134    /// # Returns
135    /// Iterator over market details
136    pub fn iter(&self) -> impl Iterator<Item = &MarketDetails> {
137        self.market_details.iter()
138    }
139}
140
141/// Model for historical prices
142#[derive(DebugPretty, Clone, Serialize, Deserialize)]
143pub struct HistoricalPricesResponse {
144    /// List of historical price points
145    pub prices: Vec<HistoricalPrice>,
146    /// Type of the instrument
147    #[serde(rename = "instrumentType")]
148    pub instrument_type: InstrumentType,
149    /// API usage allowance information
150    #[serde(rename = "allowance", skip_serializing_if = "Option::is_none", default)]
151    pub allowance: Option<PriceAllowance>,
152}
153
154impl HistoricalPricesResponse {
155    /// Returns the number of price points in the response
156    ///
157    /// # Returns
158    /// Number of price points
159    #[must_use]
160    pub fn len(&self) -> usize {
161        self.prices.len()
162    }
163
164    /// Returns true if the response contains no price points
165    ///
166    /// # Returns
167    /// True if empty, false otherwise
168    #[must_use]
169    pub fn is_empty(&self) -> bool {
170        self.prices.is_empty()
171    }
172
173    /// Returns a reference to the prices vector
174    ///
175    /// # Returns
176    /// Reference to the vector of historical prices
177    #[must_use]
178    pub fn prices(&self) -> &Vec<HistoricalPrice> {
179        &self.prices
180    }
181
182    /// Returns an iterator over the prices
183    ///
184    /// # Returns
185    /// Iterator over historical prices
186    pub fn iter(&self) -> impl Iterator<Item = &HistoricalPrice> {
187        self.prices.iter()
188    }
189}
190
191/// Model for market search results
192#[derive(DebugPretty, Clone, Serialize, Deserialize)]
193pub struct MarketSearchResponse {
194    /// List of markets matching the search criteria
195    pub markets: Vec<MarketData>,
196}
197
198impl MarketSearchResponse {
199    /// Returns the number of markets in the response
200    ///
201    /// # Returns
202    /// Number of markets
203    #[must_use]
204    pub fn len(&self) -> usize {
205        self.markets.len()
206    }
207
208    /// Returns true if the response contains no markets
209    ///
210    /// # Returns
211    /// True if empty, false otherwise
212    #[must_use]
213    pub fn is_empty(&self) -> bool {
214        self.markets.is_empty()
215    }
216
217    /// Returns a reference to the markets vector
218    ///
219    /// # Returns
220    /// Reference to the vector of markets
221    #[must_use]
222    pub fn markets(&self) -> &Vec<MarketData> {
223        &self.markets
224    }
225
226    /// Returns an iterator over the markets
227    ///
228    /// # Returns
229    /// Iterator over markets
230    pub fn iter(&self) -> impl Iterator<Item = &MarketData> {
231        self.markets.iter()
232    }
233}
234
235/// Response model for market navigation
236#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
237pub struct MarketNavigationResponse {
238    /// List of navigation nodes at the current level
239    #[serde(default, deserialize_with = "deserialize_null_as_empty_vec")]
240    pub nodes: Vec<MarketNavigationNode>,
241    /// List of markets at the current level
242    #[serde(default, deserialize_with = "deserialize_null_as_empty_vec")]
243    pub markets: Vec<MarketData>,
244}
245
246/// Response containing all categories of instruments enabled for the IG account
247#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize, Default)]
248pub struct CategoriesResponse {
249    /// List of categories
250    pub categories: Vec<Category>,
251}
252
253impl CategoriesResponse {
254    /// Returns the number of categories in the response
255    ///
256    /// # Returns
257    /// Number of categories
258    #[must_use]
259    pub fn len(&self) -> usize {
260        self.categories.len()
261    }
262
263    /// Returns true if the response contains no categories
264    ///
265    /// # Returns
266    /// True if empty, false otherwise
267    #[must_use]
268    pub fn is_empty(&self) -> bool {
269        self.categories.is_empty()
270    }
271
272    /// Returns a reference to the categories vector
273    ///
274    /// # Returns
275    /// Reference to the vector of categories
276    #[must_use]
277    pub fn categories(&self) -> &Vec<Category> {
278        &self.categories
279    }
280
281    /// Returns an iterator over the categories
282    ///
283    /// # Returns
284    /// Iterator over categories
285    pub fn iter(&self) -> impl Iterator<Item = &Category> {
286        self.categories.iter()
287    }
288}
289
290/// Response containing instruments for a specific category
291#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize, Default)]
292pub struct CategoryInstrumentsResponse {
293    /// List of instruments in the category
294    pub instruments: Vec<CategoryInstrument>,
295    /// Paging metadata
296    pub metadata: Option<CategoryInstrumentsMetadata>,
297}
298
299impl CategoryInstrumentsResponse {
300    /// Returns the number of instruments in the response
301    ///
302    /// # Returns
303    /// Number of instruments
304    #[must_use]
305    pub fn len(&self) -> usize {
306        self.instruments.len()
307    }
308
309    /// Returns true if the response contains no instruments
310    ///
311    /// # Returns
312    /// True if empty, false otherwise
313    #[must_use]
314    pub fn is_empty(&self) -> bool {
315        self.instruments.is_empty()
316    }
317
318    /// Returns a reference to the instruments vector
319    ///
320    /// # Returns
321    /// Reference to the vector of instruments
322    #[must_use]
323    pub fn instruments(&self) -> &Vec<CategoryInstrument> {
324        &self.instruments
325    }
326
327    /// Returns an iterator over the instruments
328    ///
329    /// # Returns
330    /// Iterator over instruments
331    pub fn iter(&self) -> impl Iterator<Item = &CategoryInstrument> {
332        self.instruments.iter()
333    }
334}
335
336/// Response containing user accounts
337#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize, Default)]
338pub struct AccountsResponse {
339    /// List of accounts owned by the user
340    pub accounts: Vec<Account>,
341}
342
343/// Open positions
344#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize, Default)]
345pub struct PositionsResponse {
346    /// List of open positions
347    pub positions: Vec<Position>,
348}
349
350impl PositionsResponse {
351    /// Compact positions by epic, combining positions with the same epic
352    ///
353    /// This method takes a vector of positions and returns a new vector where
354    /// positions with the same epic have been combined into a single position.
355    ///
356    /// # Arguments
357    /// * `positions` - A vector of positions to compact
358    ///
359    /// # Returns
360    /// A vector of positions with unique epics
361    pub fn compact_by_epic(positions: Vec<Position>) -> Vec<Position> {
362        let mut epic_map: HashMap<String, Position> = std::collections::HashMap::new();
363
364        for position in positions {
365            let epic = position.market.epic.clone();
366            epic_map
367                .entry(epic)
368                .and_modify(|existing| {
369                    *existing = existing.clone() + position.clone();
370                })
371                .or_insert(position);
372        }
373
374        epic_map.into_values().collect()
375    }
376}
377
378/// Working orders
379#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
380pub struct WorkingOrdersResponse {
381    /// List of pending working orders
382    #[serde(rename = "workingOrders")]
383    pub working_orders: Vec<WorkingOrder>,
384}
385
386/// Account activity
387#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
388pub struct AccountActivityResponse {
389    /// List of activities on the account
390    pub activities: Vec<Activity>,
391    /// Metadata about pagination
392    pub metadata: Option<ActivityMetadata>,
393}
394
395/// Transaction history
396#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
397pub struct TransactionHistoryResponse {
398    /// List of account transactions
399    pub transactions: Vec<AccountTransaction>,
400    /// Metadata about the transaction list
401    pub metadata: TransactionMetadata,
402}
403
404/// Response to order creation
405#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
406pub struct CreateOrderResponse {
407    /// Client-generated reference for the deal
408    #[serde(rename = "dealReference")]
409    pub deal_reference: String,
410}
411
412/// Response to closing a position
413#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
414pub struct ClosePositionResponse {
415    /// Client-generated reference for the closing deal
416    #[serde(rename = "dealReference")]
417    pub deal_reference: String,
418}
419
420/// Response to updating a position
421#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
422pub struct UpdatePositionResponse {
423    /// Client-generated reference for the update deal
424    #[serde(rename = "dealReference")]
425    pub deal_reference: String,
426}
427
428/// Response to working order creation
429#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
430pub struct CreateWorkingOrderResponse {
431    /// Client-generated reference for the deal
432    #[serde(rename = "dealReference")]
433    pub deal_reference: String,
434}
435
436/// Details of a confirmed order
437#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
438pub struct OrderConfirmationResponse {
439    /// Date and time of the confirmation
440    pub date: String,
441    /// Status of the order (accepted, rejected, etc.)
442    /// This can be null in some responses (e.g., when market is closed)
443    #[serde(deserialize_with = "deserialize_nullable_status")]
444    pub status: Status,
445    /// Reason for rejection if applicable
446    pub reason: Option<String>,
447    /// Unique identifier for the deal
448    #[serde(rename = "dealId")]
449    pub deal_id: Option<String>,
450    /// Client-generated reference for the deal
451    #[serde(rename = "dealReference")]
452    pub deal_reference: String,
453    /// Status of the deal
454    #[serde(rename = "dealStatus")]
455    pub deal_status: Option<String>,
456    /// Instrument EPIC identifier
457    pub epic: Option<String>,
458    /// Expiry date for the order
459    #[serde(rename = "expiry")]
460    pub expiry: Option<String>,
461    /// Whether a guaranteed stop was used
462    #[serde(rename = "guaranteedStop")]
463    pub guaranteed_stop: Option<bool>,
464    /// Price level of the order
465    #[serde(rename = "level")]
466    pub level: Option<f64>,
467    /// Distance for take profit
468    #[serde(rename = "limitDistance")]
469    pub limit_distance: Option<f64>,
470    /// Price level for take profit
471    #[serde(rename = "limitLevel")]
472    pub limit_level: Option<f64>,
473    /// Size/quantity of the order
474    pub size: Option<f64>,
475    /// Distance for stop loss
476    #[serde(rename = "stopDistance")]
477    pub stop_distance: Option<f64>,
478    /// Price level for stop loss
479    #[serde(rename = "stopLevel")]
480    pub stop_level: Option<f64>,
481    /// Whether a trailing stop was used
482    #[serde(rename = "trailingStop")]
483    pub trailing_stop: Option<bool>,
484    /// Direction of the order (buy or sell)
485    pub direction: Option<Direction>,
486}
487
488impl std::fmt::Display for MultipleMarketDetailsResponse {
489    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
490        use prettytable::format;
491        use prettytable::{Cell, Row, Table};
492
493        let mut table = Table::new();
494
495        // Set table format
496        table.set_format(*format::consts::FORMAT_BOX_CHARS);
497
498        // Add header
499        table.add_row(Row::new(vec![
500            Cell::new("INSTRUMENT NAME"),
501            Cell::new("EPIC"),
502            Cell::new("BID"),
503            Cell::new("OFFER"),
504            Cell::new("MID"),
505            Cell::new("SPREAD"),
506            Cell::new("EXPIRY"),
507            Cell::new("HIGH/LOW"),
508        ]));
509
510        // Sort by instrument name
511        let mut sorted_details = self.market_details.clone();
512        sorted_details.sort_by(|a, b| {
513            a.instrument
514                .name
515                .to_lowercase()
516                .cmp(&b.instrument.name.to_lowercase())
517        });
518
519        // Add rows
520        for details in &sorted_details {
521            let bid = details
522                .snapshot
523                .bid
524                .map(|b| format!("{:.2}", b))
525                .unwrap_or_else(|| "-".to_string());
526
527            let offer = details
528                .snapshot
529                .offer
530                .map(|o| format!("{:.2}", o))
531                .unwrap_or_else(|| "-".to_string());
532
533            let mid = match (details.snapshot.bid, details.snapshot.offer) {
534                (Some(b), Some(o)) => format!("{:.2}", (b + o) / 2.0),
535                _ => "-".to_string(),
536            };
537
538            let spread = match (details.snapshot.bid, details.snapshot.offer) {
539                (Some(b), Some(o)) => format!("{:.2}", o - b),
540                _ => "-".to_string(),
541            };
542
543            // Use expiry directly (shorter than last_dealing_date)
544            let expiry = details
545                .instrument
546                .expiry_details
547                .as_ref()
548                .map(|ed| {
549                    // Extract just the date part (YYYY-MM-DD)
550                    ed.last_dealing_date
551                        .split('T')
552                        .next()
553                        .unwrap_or(&ed.last_dealing_date)
554                        .to_string()
555                })
556                .unwrap_or_else(|| {
557                    details
558                        .instrument
559                        .expiry
560                        .split('T')
561                        .next()
562                        .unwrap_or(&details.instrument.expiry)
563                        .to_string()
564                });
565
566            let high_low = format!(
567                "{}/{}",
568                details
569                    .snapshot
570                    .high
571                    .map(|h| format!("{:.2}", h))
572                    .unwrap_or_else(|| "-".to_string()),
573                details
574                    .snapshot
575                    .low
576                    .map(|l| format!("{:.2}", l))
577                    .unwrap_or_else(|| "-".to_string())
578            );
579
580            // Truncate long names to make room for EPIC
581            let name = if details.instrument.name.len() > 30 {
582                format!("{}...", &details.instrument.name[0..27])
583            } else {
584                details.instrument.name.clone()
585            };
586
587            // Don't truncate EPIC - show it complete
588            let epic = details.instrument.epic.clone();
589
590            table.add_row(Row::new(vec![
591                Cell::new(&name),
592                Cell::new(&epic),
593                Cell::new(&bid),
594                Cell::new(&offer),
595                Cell::new(&mid),
596                Cell::new(&spread),
597                Cell::new(&expiry),
598                Cell::new(&high_low),
599            ]));
600        }
601
602        write!(f, "{}", table)
603    }
604}
605
606impl std::fmt::Display for HistoricalPricesResponse {
607    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
608        use prettytable::format;
609        use prettytable::{Cell, Row, Table};
610
611        let mut table = Table::new();
612        table.set_format(*format::consts::FORMAT_BOX_CHARS);
613
614        // Add header
615        table.add_row(Row::new(vec![
616            Cell::new("SNAPSHOT TIME"),
617            Cell::new("OPEN BID"),
618            Cell::new("OPEN ASK"),
619            Cell::new("HIGH BID"),
620            Cell::new("HIGH ASK"),
621            Cell::new("LOW BID"),
622            Cell::new("LOW ASK"),
623            Cell::new("CLOSE BID"),
624            Cell::new("CLOSE ASK"),
625            Cell::new("VOLUME"),
626        ]));
627
628        // Add rows
629        for price in &self.prices {
630            let open_bid = price
631                .open_price
632                .bid
633                .map(|v| format!("{:.4}", v))
634                .unwrap_or_else(|| "-".to_string());
635
636            let open_ask = price
637                .open_price
638                .ask
639                .map(|v| format!("{:.4}", v))
640                .unwrap_or_else(|| "-".to_string());
641
642            let high_bid = price
643                .high_price
644                .bid
645                .map(|v| format!("{:.4}", v))
646                .unwrap_or_else(|| "-".to_string());
647
648            let high_ask = price
649                .high_price
650                .ask
651                .map(|v| format!("{:.4}", v))
652                .unwrap_or_else(|| "-".to_string());
653
654            let low_bid = price
655                .low_price
656                .bid
657                .map(|v| format!("{:.4}", v))
658                .unwrap_or_else(|| "-".to_string());
659
660            let low_ask = price
661                .low_price
662                .ask
663                .map(|v| format!("{:.4}", v))
664                .unwrap_or_else(|| "-".to_string());
665
666            let close_bid = price
667                .close_price
668                .bid
669                .map(|v| format!("{:.4}", v))
670                .unwrap_or_else(|| "-".to_string());
671
672            let close_ask = price
673                .close_price
674                .ask
675                .map(|v| format!("{:.4}", v))
676                .unwrap_or_else(|| "-".to_string());
677
678            let volume = price
679                .last_traded_volume
680                .map(|v| v.to_string())
681                .unwrap_or_else(|| "-".to_string());
682
683            table.add_row(Row::new(vec![
684                Cell::new(&price.snapshot_time),
685                Cell::new(&open_bid),
686                Cell::new(&open_ask),
687                Cell::new(&high_bid),
688                Cell::new(&high_ask),
689                Cell::new(&low_bid),
690                Cell::new(&low_ask),
691                Cell::new(&close_bid),
692                Cell::new(&close_ask),
693                Cell::new(&volume),
694            ]));
695        }
696
697        // Add summary footer
698        writeln!(f, "{}", table)?;
699        writeln!(f, "\nSummary:")?;
700        writeln!(f, "  Total price points: {}", self.prices.len())?;
701        writeln!(f, "  Instrument type: {:?}", self.instrument_type)?;
702
703        if let Some(allowance) = &self.allowance {
704            writeln!(
705                f,
706                "  Remaining allowance: {}",
707                allowance.remaining_allowance
708            )?;
709            writeln!(f, "  Total allowance: {}", allowance.total_allowance)?;
710        }
711
712        Ok(())
713    }
714}
715
716impl std::fmt::Display for MarketSearchResponse {
717    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
718        use prettytable::format;
719        use prettytable::{Cell, Row, Table};
720
721        let mut table = Table::new();
722        table.set_format(*format::consts::FORMAT_BOX_CHARS);
723
724        // Add header
725        table.add_row(Row::new(vec![
726            Cell::new("INSTRUMENT NAME"),
727            Cell::new("EPIC"),
728            Cell::new("BID"),
729            Cell::new("OFFER"),
730            Cell::new("MID"),
731            Cell::new("SPREAD"),
732            Cell::new("EXPIRY"),
733            Cell::new("TYPE"),
734        ]));
735
736        // Sort by instrument name
737        let mut sorted_markets = self.markets.clone();
738        sorted_markets.sort_by(|a, b| {
739            a.instrument_name
740                .to_lowercase()
741                .cmp(&b.instrument_name.to_lowercase())
742        });
743
744        // Add rows
745        for market in &sorted_markets {
746            let bid = market
747                .bid
748                .map(|b| format!("{:.4}", b))
749                .unwrap_or_else(|| "-".to_string());
750
751            let offer = market
752                .offer
753                .map(|o| format!("{:.4}", o))
754                .unwrap_or_else(|| "-".to_string());
755
756            let mid = match (market.bid, market.offer) {
757                (Some(b), Some(o)) => format!("{:.4}", (b + o) / 2.0),
758                _ => "-".to_string(),
759            };
760
761            let spread = match (market.bid, market.offer) {
762                (Some(b), Some(o)) => format!("{:.4}", o - b),
763                _ => "-".to_string(),
764            };
765
766            // Truncate long names
767            let name = if market.instrument_name.len() > 30 {
768                format!("{}...", &market.instrument_name[0..27])
769            } else {
770                market.instrument_name.clone()
771            };
772
773            // Extract date from expiry
774            let expiry = market
775                .expiry
776                .split('T')
777                .next()
778                .unwrap_or(&market.expiry)
779                .to_string();
780
781            let instrument_type = format!("{:?}", market.instrument_type);
782
783            table.add_row(Row::new(vec![
784                Cell::new(&name),
785                Cell::new(&market.epic),
786                Cell::new(&bid),
787                Cell::new(&offer),
788                Cell::new(&mid),
789                Cell::new(&spread),
790                Cell::new(&expiry),
791                Cell::new(&instrument_type),
792            ]));
793        }
794
795        writeln!(f, "{}", table)?;
796        writeln!(f, "\nTotal markets found: {}", self.markets.len())?;
797
798        Ok(())
799    }
800}
801
802#[cfg(test)]
803mod tests {
804    use super::*;
805    use serde_json::Value;
806    use std::fs;
807
808    #[test]
809    fn test_deserialize_working_orders_from_file() {
810        // Load the JSON file
811        let json_content = fs::read_to_string("Data/working_orders.json")
812            .expect("Failed to read Data/working_orders.json");
813
814        // Parse as a generic JSON Value first to inspect the structure
815        let json_value: Value =
816            serde_json::from_str(&json_content).expect("Failed to parse JSON as Value");
817
818        println!(
819            "JSON structure:\n{}",
820            serde_json::to_string_pretty(&json_value).unwrap()
821        );
822
823        // Attempt to deserialize into WorkingOrdersResponse
824        let result: Result<WorkingOrdersResponse, _> = serde_json::from_str(&json_content);
825
826        match result {
827            Ok(response) => {
828                println!(
829                    "Successfully deserialized {} working orders",
830                    response.working_orders.len()
831                );
832                for (idx, order) in response.working_orders.iter().enumerate() {
833                    println!(
834                        "Order {}: epic={}, direction={:?}, size={}, level={}",
835                        idx + 1,
836                        order.working_order_data.epic,
837                        order.working_order_data.direction,
838                        order.working_order_data.order_size,
839                        order.working_order_data.order_level
840                    );
841                }
842            }
843            Err(e) => {
844                panic!(
845                    "Failed to deserialize WorkingOrdersResponse: {}\n\nJSON was:\n{}",
846                    e,
847                    serde_json::to_string_pretty(&json_value).unwrap()
848                );
849            }
850        }
851    }
852}