1use crate::presentation::account::{
7 Account, AccountTransaction, Activity, ActivityMetadata, Position, TransactionMetadata,
8 WorkingOrder,
9};
10use crate::presentation::instrument::InstrumentType;
11use crate::presentation::market::{
12 Category, CategoryInstrument, CategoryInstrumentsMetadata, HistoricalPrice, MarketData,
13 MarketDetails, 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#[derive(
24 DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Default,
25)]
26pub struct DBEntryResponse {
27 pub symbol: String,
29 pub epic: String,
31 pub name: String,
33 pub instrument_type: InstrumentType,
35 pub exchange: String,
37 pub expiry: String,
39 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#[derive(DebugPretty, Clone, Serialize, Deserialize, Default)]
98pub struct MultipleMarketDetailsResponse {
99 #[serde(rename = "marketDetails")]
101 pub market_details: Vec<MarketDetails>,
102}
103
104impl MultipleMarketDetailsResponse {
105 #[must_use]
110 pub fn len(&self) -> usize {
111 self.market_details.len()
112 }
113
114 #[must_use]
119 pub fn is_empty(&self) -> bool {
120 self.market_details.is_empty()
121 }
122
123 #[must_use]
128 pub fn market_details(&self) -> &Vec<MarketDetails> {
129 &self.market_details
130 }
131
132 pub fn iter(&self) -> impl Iterator<Item = &MarketDetails> {
137 self.market_details.iter()
138 }
139}
140
141#[derive(DebugPretty, Clone, Serialize, Deserialize)]
143pub struct HistoricalPricesResponse {
144 pub prices: Vec<HistoricalPrice>,
146 #[serde(rename = "instrumentType")]
148 pub instrument_type: InstrumentType,
149 #[serde(rename = "allowance", skip_serializing_if = "Option::is_none", default)]
151 pub allowance: Option<PriceAllowance>,
152}
153
154impl HistoricalPricesResponse {
155 #[must_use]
160 pub fn len(&self) -> usize {
161 self.prices.len()
162 }
163
164 #[must_use]
169 pub fn is_empty(&self) -> bool {
170 self.prices.is_empty()
171 }
172
173 #[must_use]
178 pub fn prices(&self) -> &Vec<HistoricalPrice> {
179 &self.prices
180 }
181
182 pub fn iter(&self) -> impl Iterator<Item = &HistoricalPrice> {
187 self.prices.iter()
188 }
189}
190
191#[derive(DebugPretty, Clone, Serialize, Deserialize)]
193pub struct MarketSearchResponse {
194 pub markets: Vec<MarketData>,
196}
197
198impl MarketSearchResponse {
199 #[must_use]
204 pub fn len(&self) -> usize {
205 self.markets.len()
206 }
207
208 #[must_use]
213 pub fn is_empty(&self) -> bool {
214 self.markets.is_empty()
215 }
216
217 #[must_use]
222 pub fn markets(&self) -> &Vec<MarketData> {
223 &self.markets
224 }
225
226 pub fn iter(&self) -> impl Iterator<Item = &MarketData> {
231 self.markets.iter()
232 }
233}
234
235#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
237pub struct MarketNavigationResponse {
238 #[serde(default, deserialize_with = "deserialize_null_as_empty_vec")]
240 pub nodes: Vec<MarketNavigationNode>,
241 #[serde(default, deserialize_with = "deserialize_null_as_empty_vec")]
243 pub markets: Vec<MarketData>,
244}
245
246#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize, Default)]
248pub struct CategoriesResponse {
249 pub categories: Vec<Category>,
251}
252
253impl CategoriesResponse {
254 #[must_use]
259 pub fn len(&self) -> usize {
260 self.categories.len()
261 }
262
263 #[must_use]
268 pub fn is_empty(&self) -> bool {
269 self.categories.is_empty()
270 }
271
272 #[must_use]
277 pub fn categories(&self) -> &Vec<Category> {
278 &self.categories
279 }
280
281 pub fn iter(&self) -> impl Iterator<Item = &Category> {
286 self.categories.iter()
287 }
288}
289
290#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize, Default)]
292pub struct CategoryInstrumentsResponse {
293 pub instruments: Vec<CategoryInstrument>,
295 pub metadata: Option<CategoryInstrumentsMetadata>,
297}
298
299impl CategoryInstrumentsResponse {
300 #[must_use]
305 pub fn len(&self) -> usize {
306 self.instruments.len()
307 }
308
309 #[must_use]
314 pub fn is_empty(&self) -> bool {
315 self.instruments.is_empty()
316 }
317
318 #[must_use]
323 pub fn instruments(&self) -> &Vec<CategoryInstrument> {
324 &self.instruments
325 }
326
327 pub fn iter(&self) -> impl Iterator<Item = &CategoryInstrument> {
332 self.instruments.iter()
333 }
334}
335
336#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize, Default)]
338pub struct AccountsResponse {
339 pub accounts: Vec<Account>,
341}
342
343#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize, Default)]
345pub struct PositionsResponse {
346 pub positions: Vec<Position>,
348}
349
350impl PositionsResponse {
351 #[must_use]
362 pub fn compact_by_epic(positions: Vec<Position>) -> Vec<Position> {
363 let mut epic_map: HashMap<String, Position> = std::collections::HashMap::new();
364
365 for position in positions {
366 let epic = position.market.epic.clone();
367 epic_map
368 .entry(epic)
369 .and_modify(|existing| {
370 *existing = existing.clone() + position.clone();
371 })
372 .or_insert(position);
373 }
374
375 epic_map.into_values().collect()
376 }
377}
378
379#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
381pub struct WorkingOrdersResponse {
382 #[serde(rename = "workingOrders")]
384 pub working_orders: Vec<WorkingOrder>,
385}
386
387#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
389pub struct AccountActivityResponse {
390 pub activities: Vec<Activity>,
392 pub metadata: Option<ActivityMetadata>,
394}
395
396#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
398pub struct TransactionHistoryResponse {
399 pub transactions: Vec<AccountTransaction>,
401 pub metadata: TransactionMetadata,
403}
404
405#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
407pub struct CreateOrderResponse {
408 #[serde(rename = "dealReference")]
410 pub deal_reference: String,
411}
412
413#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
415pub struct ClosePositionResponse {
416 #[serde(rename = "dealReference")]
418 pub deal_reference: String,
419}
420
421#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
423pub struct UpdatePositionResponse {
424 #[serde(rename = "dealReference")]
426 pub deal_reference: String,
427}
428
429#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
431pub struct CreateWorkingOrderResponse {
432 #[serde(rename = "dealReference")]
434 pub deal_reference: String,
435}
436
437#[repr(u8)]
444#[derive(Debug, Clone, Copy, DisplaySimple, Serialize, Deserialize, PartialEq, Eq, Hash)]
445#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
446pub enum DealStatus {
447 Accepted,
449 Rejected,
451}
452
453#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
458pub struct AffectedDeal {
459 #[serde(rename = "dealId")]
461 pub deal_id: String,
462 #[serde(rename = "status")]
467 pub status: String,
468}
469
470#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
472pub struct OrderConfirmationResponse {
473 pub date: String,
475 #[serde(deserialize_with = "deserialize_nullable_status")]
478 pub status: Status,
479 pub reason: Option<String>,
481 #[serde(rename = "dealId")]
483 pub deal_id: Option<String>,
484 #[serde(rename = "dealReference")]
486 pub deal_reference: String,
487 #[serde(rename = "dealStatus")]
489 #[serde(default)]
490 pub deal_status: Option<DealStatus>,
491 pub epic: Option<String>,
493 #[serde(rename = "expiry")]
495 pub expiry: Option<String>,
496 #[serde(rename = "guaranteedStop")]
498 pub guaranteed_stop: Option<bool>,
499 #[serde(rename = "level")]
501 pub level: Option<f64>,
502 #[serde(rename = "limitDistance")]
504 pub limit_distance: Option<f64>,
505 #[serde(rename = "limitLevel")]
507 pub limit_level: Option<f64>,
508 pub size: Option<f64>,
510 #[serde(rename = "stopDistance")]
512 pub stop_distance: Option<f64>,
513 #[serde(rename = "stopLevel")]
515 pub stop_level: Option<f64>,
516 #[serde(rename = "trailingStop")]
518 pub trailing_stop: Option<bool>,
519 pub direction: Option<Direction>,
521 #[serde(rename = "affectedDeals")]
523 #[serde(default)]
524 pub affected_deals: Vec<AffectedDeal>,
525 #[serde(rename = "profit")]
527 #[serde(default)]
528 pub profit: Option<f64>,
529 #[serde(rename = "profitCurrency")]
531 #[serde(default)]
532 pub profit_currency: Option<String>,
533}
534
535impl std::fmt::Display for MultipleMarketDetailsResponse {
536 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
537 use prettytable::format;
538 use prettytable::{Cell, Row, Table};
539
540 let mut table = Table::new();
541
542 table.set_format(*format::consts::FORMAT_BOX_CHARS);
544
545 table.add_row(Row::new(vec![
547 Cell::new("INSTRUMENT NAME"),
548 Cell::new("EPIC"),
549 Cell::new("BID"),
550 Cell::new("OFFER"),
551 Cell::new("MID"),
552 Cell::new("SPREAD"),
553 Cell::new("EXPIRY"),
554 Cell::new("HIGH/LOW"),
555 ]));
556
557 let mut sorted_details = self.market_details.clone();
559 sorted_details.sort_by(|a, b| {
560 a.instrument
561 .name
562 .to_lowercase()
563 .cmp(&b.instrument.name.to_lowercase())
564 });
565
566 for details in &sorted_details {
568 let bid = details
569 .snapshot
570 .bid
571 .map(|b| format!("{:.2}", b))
572 .unwrap_or_else(|| "-".to_string());
573
574 let offer = details
575 .snapshot
576 .offer
577 .map(|o| format!("{:.2}", o))
578 .unwrap_or_else(|| "-".to_string());
579
580 let mid = match (details.snapshot.bid, details.snapshot.offer) {
581 (Some(b), Some(o)) => format!("{:.2}", (b + o) / 2.0),
582 _ => "-".to_string(),
583 };
584
585 let spread = match (details.snapshot.bid, details.snapshot.offer) {
586 (Some(b), Some(o)) => format!("{:.2}", o - b),
587 _ => "-".to_string(),
588 };
589
590 let expiry = details
592 .instrument
593 .expiry_details
594 .as_ref()
595 .map(|ed| {
596 ed.last_dealing_date
598 .split('T')
599 .next()
600 .unwrap_or(&ed.last_dealing_date)
601 .to_string()
602 })
603 .unwrap_or_else(|| {
604 details
605 .instrument
606 .expiry
607 .split('T')
608 .next()
609 .unwrap_or(&details.instrument.expiry)
610 .to_string()
611 });
612
613 let high_low = format!(
614 "{}/{}",
615 details
616 .snapshot
617 .high
618 .map(|h| format!("{:.2}", h))
619 .unwrap_or_else(|| "-".to_string()),
620 details
621 .snapshot
622 .low
623 .map(|l| format!("{:.2}", l))
624 .unwrap_or_else(|| "-".to_string())
625 );
626
627 let name = if details.instrument.name.len() > 30 {
629 format!("{}...", &details.instrument.name[0..27])
630 } else {
631 details.instrument.name.clone()
632 };
633
634 let epic = details.instrument.epic.clone();
636
637 table.add_row(Row::new(vec![
638 Cell::new(&name),
639 Cell::new(&epic),
640 Cell::new(&bid),
641 Cell::new(&offer),
642 Cell::new(&mid),
643 Cell::new(&spread),
644 Cell::new(&expiry),
645 Cell::new(&high_low),
646 ]));
647 }
648
649 write!(f, "{}", table)
650 }
651}
652
653impl std::fmt::Display for HistoricalPricesResponse {
654 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
655 use prettytable::format;
656 use prettytable::{Cell, Row, Table};
657
658 let mut table = Table::new();
659 table.set_format(*format::consts::FORMAT_BOX_CHARS);
660
661 table.add_row(Row::new(vec![
663 Cell::new("SNAPSHOT TIME"),
664 Cell::new("OPEN BID"),
665 Cell::new("OPEN ASK"),
666 Cell::new("HIGH BID"),
667 Cell::new("HIGH ASK"),
668 Cell::new("LOW BID"),
669 Cell::new("LOW ASK"),
670 Cell::new("CLOSE BID"),
671 Cell::new("CLOSE ASK"),
672 Cell::new("VOLUME"),
673 ]));
674
675 for price in &self.prices {
677 let open_bid = price
678 .open_price
679 .bid
680 .map(|v| format!("{:.4}", v))
681 .unwrap_or_else(|| "-".to_string());
682
683 let open_ask = price
684 .open_price
685 .ask
686 .map(|v| format!("{:.4}", v))
687 .unwrap_or_else(|| "-".to_string());
688
689 let high_bid = price
690 .high_price
691 .bid
692 .map(|v| format!("{:.4}", v))
693 .unwrap_or_else(|| "-".to_string());
694
695 let high_ask = price
696 .high_price
697 .ask
698 .map(|v| format!("{:.4}", v))
699 .unwrap_or_else(|| "-".to_string());
700
701 let low_bid = price
702 .low_price
703 .bid
704 .map(|v| format!("{:.4}", v))
705 .unwrap_or_else(|| "-".to_string());
706
707 let low_ask = price
708 .low_price
709 .ask
710 .map(|v| format!("{:.4}", v))
711 .unwrap_or_else(|| "-".to_string());
712
713 let close_bid = price
714 .close_price
715 .bid
716 .map(|v| format!("{:.4}", v))
717 .unwrap_or_else(|| "-".to_string());
718
719 let close_ask = price
720 .close_price
721 .ask
722 .map(|v| format!("{:.4}", v))
723 .unwrap_or_else(|| "-".to_string());
724
725 let volume = price
726 .last_traded_volume
727 .map(|v| v.to_string())
728 .unwrap_or_else(|| "-".to_string());
729
730 table.add_row(Row::new(vec![
731 Cell::new(&price.snapshot_time),
732 Cell::new(&open_bid),
733 Cell::new(&open_ask),
734 Cell::new(&high_bid),
735 Cell::new(&high_ask),
736 Cell::new(&low_bid),
737 Cell::new(&low_ask),
738 Cell::new(&close_bid),
739 Cell::new(&close_ask),
740 Cell::new(&volume),
741 ]));
742 }
743
744 writeln!(f, "{}", table)?;
746 writeln!(f, "\nSummary:")?;
747 writeln!(f, " Total price points: {}", self.prices.len())?;
748 writeln!(f, " Instrument type: {:?}", self.instrument_type)?;
749
750 if let Some(allowance) = &self.allowance {
751 writeln!(
752 f,
753 " Remaining allowance: {}",
754 allowance.remaining_allowance
755 )?;
756 writeln!(f, " Total allowance: {}", allowance.total_allowance)?;
757 }
758
759 Ok(())
760 }
761}
762
763impl std::fmt::Display for MarketSearchResponse {
764 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
765 use prettytable::format;
766 use prettytable::{Cell, Row, Table};
767
768 let mut table = Table::new();
769 table.set_format(*format::consts::FORMAT_BOX_CHARS);
770
771 table.add_row(Row::new(vec![
773 Cell::new("INSTRUMENT NAME"),
774 Cell::new("EPIC"),
775 Cell::new("BID"),
776 Cell::new("OFFER"),
777 Cell::new("MID"),
778 Cell::new("SPREAD"),
779 Cell::new("EXPIRY"),
780 Cell::new("TYPE"),
781 ]));
782
783 let mut sorted_markets = self.markets.clone();
785 sorted_markets.sort_by(|a, b| {
786 a.instrument_name
787 .to_lowercase()
788 .cmp(&b.instrument_name.to_lowercase())
789 });
790
791 for market in &sorted_markets {
793 let bid = market
794 .bid
795 .map(|b| format!("{:.4}", b))
796 .unwrap_or_else(|| "-".to_string());
797
798 let offer = market
799 .offer
800 .map(|o| format!("{:.4}", o))
801 .unwrap_or_else(|| "-".to_string());
802
803 let mid = match (market.bid, market.offer) {
804 (Some(b), Some(o)) => format!("{:.4}", (b + o) / 2.0),
805 _ => "-".to_string(),
806 };
807
808 let spread = match (market.bid, market.offer) {
809 (Some(b), Some(o)) => format!("{:.4}", o - b),
810 _ => "-".to_string(),
811 };
812
813 let name = if market.instrument_name.len() > 30 {
815 format!("{}...", &market.instrument_name[0..27])
816 } else {
817 market.instrument_name.clone()
818 };
819
820 let expiry = market
822 .expiry
823 .split('T')
824 .next()
825 .unwrap_or(&market.expiry)
826 .to_string();
827
828 let instrument_type = format!("{:?}", market.instrument_type);
829
830 table.add_row(Row::new(vec![
831 Cell::new(&name),
832 Cell::new(&market.epic),
833 Cell::new(&bid),
834 Cell::new(&offer),
835 Cell::new(&mid),
836 Cell::new(&spread),
837 Cell::new(&expiry),
838 Cell::new(&instrument_type),
839 ]));
840 }
841
842 writeln!(f, "{}", table)?;
843 writeln!(f, "\nTotal markets found: {}", self.markets.len())?;
844
845 Ok(())
846 }
847}
848
849#[derive(DebugPretty, Clone, Serialize, Deserialize, Default)]
855pub struct WatchlistsResponse {
856 pub watchlists: Vec<Watchlist>,
858}
859
860#[derive(DebugPretty, Clone, Serialize, Deserialize, Default)]
862pub struct Watchlist {
863 pub id: String,
865 pub name: String,
867 pub editable: bool,
869 pub deleteable: bool,
871 #[serde(rename = "defaultSystemWatchlist")]
873 pub default_system_watchlist: bool,
874}
875
876#[derive(DebugPretty, Clone, Serialize, Deserialize, Default)]
878pub struct CreateWatchlistResponse {
879 #[serde(rename = "watchlistId")]
881 pub watchlist_id: String,
882 pub status: String,
884}
885
886#[derive(DebugPretty, Clone, Serialize, Deserialize, Default)]
888pub struct WatchlistMarketsResponse {
889 pub markets: Vec<MarketData>,
891}
892
893#[derive(DebugPretty, Clone, Serialize, Deserialize, Default)]
895pub struct StatusResponse {
896 pub status: String,
898}
899
900#[derive(DebugPretty, Clone, Serialize, Deserialize, Default)]
906pub struct ClientSentimentResponse {
907 #[serde(rename = "clientSentiments")]
909 pub client_sentiments: Vec<MarketSentiment>,
910}
911
912#[derive(DebugPretty, Clone, Serialize, Deserialize, Default)]
914pub struct MarketSentiment {
915 #[serde(rename = "marketId")]
917 pub market_id: String,
918 #[serde(rename = "longPositionPercentage")]
920 pub long_position_percentage: f64,
921 #[serde(rename = "shortPositionPercentage")]
923 pub short_position_percentage: f64,
924}
925
926#[derive(DebugPretty, Clone, Serialize, Deserialize, Default)]
932pub struct IndicativeCostsResponse {
933 #[serde(rename = "indicativeQuoteReference")]
935 pub indicative_quote_reference: String,
936 #[serde(rename = "costsAndCharges")]
938 pub costs_and_charges: CostsAndCharges,
939}
940
941#[derive(DebugPretty, Clone, Serialize, Deserialize, Default)]
943pub struct CostsAndCharges {
944 #[serde(rename = "totalCostPercentage")]
946 pub total_cost_percentage: Option<f64>,
947 #[serde(rename = "totalCostAmount")]
949 pub total_cost_amount: Option<f64>,
950 pub currency: Option<String>,
952 #[serde(rename = "oneOffCosts")]
954 pub one_off_costs: Option<CostBreakdown>,
955 #[serde(rename = "ongoingCosts")]
957 pub ongoing_costs: Option<CostBreakdown>,
958 #[serde(rename = "transactionCosts")]
960 pub transaction_costs: Option<CostBreakdown>,
961 #[serde(rename = "incidentalCosts")]
963 pub incidental_costs: Option<CostBreakdown>,
964}
965
966#[derive(DebugPretty, Clone, Serialize, Deserialize, Default)]
968pub struct CostBreakdown {
969 pub percentage: Option<f64>,
971 pub amount: Option<f64>,
973}
974
975#[derive(DebugPretty, Clone, Serialize, Deserialize, Default)]
982pub struct CostsHistoryResponse {
983 #[serde(default)]
985 pub pagination: CostsHistoryPagination,
986 #[serde(rename = "costsAndChargesHistory", default)]
988 pub costs_and_charges_history: Vec<CostsHistoryEntry>,
989}
990
991#[derive(DebugPretty, Clone, Serialize, Deserialize, Default)]
993pub struct CostsHistoryPagination {
994 #[serde(rename = "pageSize", default)]
996 pub page_size: i64,
997 #[serde(rename = "pageNumber", default)]
999 pub page_number: i64,
1000 #[serde(rename = "totalPages", default)]
1002 pub total_pages: i64,
1003 #[serde(rename = "totalElements", default)]
1005 pub total_elements: i64,
1006}
1007
1008#[derive(DebugPretty, Clone, Serialize, Deserialize, Default)]
1010pub struct CostsHistoryEntry {
1011 #[serde(rename = "type", default)]
1013 pub entry_type: Option<String>,
1014 #[serde(default)]
1016 pub direction: Option<String>,
1017 #[serde(rename = "instrumentName", default)]
1019 pub instrument_name: Option<String>,
1020 #[serde(rename = "createdTimestamp", default)]
1022 pub created_timestamp: Option<String>,
1023 #[serde(rename = "indicativeQuoteReference", default)]
1026 pub indicative_quote_reference: Option<String>,
1027}
1028
1029#[derive(DebugPretty, Clone, Serialize, Deserialize, Default)]
1031pub struct DurableMediumResponse {
1032 pub document: String,
1034}
1035
1036#[derive(DebugPretty, Clone, Serialize, Deserialize, Default)]
1042pub struct AccountPreferencesResponse {
1043 #[serde(rename = "trailingStopsEnabled")]
1045 pub trailing_stops_enabled: bool,
1046}
1047
1048#[derive(DebugPretty, Clone, Serialize, Deserialize, Default)]
1054pub struct ApplicationDetailsResponse {
1055 #[serde(rename = "apiKey")]
1057 pub api_key: String,
1058 pub name: Option<String>,
1060 pub status: String,
1062 #[serde(rename = "allowanceAccountOverall")]
1064 pub allowance_account_overall: Option<i64>,
1065 #[serde(rename = "allowanceAccountTrading")]
1067 pub allowance_account_trading: Option<i64>,
1068 #[serde(rename = "concurrentSubscriptionsLimit")]
1070 pub concurrent_subscriptions_limit: Option<i64>,
1071 #[serde(rename = "createdDate")]
1073 pub created_date: Option<String>,
1074}
1075
1076#[derive(DebugPretty, Clone, Serialize, Deserialize)]
1082pub struct SinglePositionResponse {
1083 pub position: Position,
1085 pub market: MarketData,
1087}
1088
1089#[cfg(test)]
1090mod tests {
1091 use super::*;
1092 use crate::model::auth::{SessionResponse, V3Response};
1093 use crate::presentation::account::ActivityType;
1094
1095 fn roundtrip<T>(value: &T) -> T
1101 where
1102 T: serde::Serialize + serde::de::DeserializeOwned,
1103 {
1104 let json = serde_json::to_string(value).expect("serialize failed");
1105 serde_json::from_str(&json).expect("re-deserialize failed")
1106 }
1107
1108 #[test]
1109 fn test_accounts_response_deserialize_and_roundtrip() {
1110 let json = r#"{
1111 "accounts": [
1112 {
1113 "accountId": "ABC12",
1114 "accountName": "Demo CFD",
1115 "accountType": "CFD",
1116 "balance": {
1117 "balance": 10000.0,
1118 "deposit": 2000.0,
1119 "profitLoss": 150.5,
1120 "available": 8000.0
1121 },
1122 "currency": "EUR",
1123 "status": "ENABLED",
1124 "preferred": true
1125 }
1126 ]
1127 }"#;
1128
1129 let resp: AccountsResponse = serde_json::from_str(json).expect("deserialize failed");
1130 assert_eq!(resp.accounts.len(), 1);
1131 let acc = &resp.accounts[0];
1132 assert_eq!(acc.account_id, "ABC12");
1133 assert_eq!(acc.account_type, "CFD");
1134 assert!((acc.balance.available - 8000.0).abs() < 1e-9);
1135 assert!(acc.preferred);
1136
1137 let re = roundtrip(&resp);
1138 assert_eq!(re.accounts[0].account_id, "ABC12");
1139 assert_eq!(re.accounts[0].currency, "EUR");
1140 }
1141
1142 #[test]
1143 fn test_positions_response_deserialize_and_roundtrip() {
1144 let json = r#"{
1145 "positions": [
1146 {
1147 "position": {
1148 "contractSize": 1.0,
1149 "createdDate": "2025/07/01 10:00:00:000",
1150 "createdDateUTC": "2025-07-01T08:00:00",
1151 "dealId": "DIFAKE111",
1152 "dealReference": "REFFAKE111",
1153 "direction": "BUY",
1154 "limitLevel": null,
1155 "level": 100.5,
1156 "size": 2.0,
1157 "stopLevel": null,
1158 "trailingStep": null,
1159 "trailingStopDistance": null,
1160 "currency": "GBP",
1161 "controlledRisk": false,
1162 "limitedRiskPremium": null
1163 },
1164 "market": {
1165 "instrumentName": "FTSE 100",
1166 "expiry": "-",
1167 "epic": "IX.D.FTSE.DAILY.IP",
1168 "instrumentType": "INDICES",
1169 "lotSize": 1.0,
1170 "high": 7600.0,
1171 "low": 7500.0,
1172 "percentageChange": 0.5,
1173 "netChange": 30.0,
1174 "bid": 7550.0,
1175 "offer": 7551.0,
1176 "updateTime": "10:00:00",
1177 "updateTimeUTC": "08:00:00",
1178 "delayTime": 0,
1179 "streamingPricesAvailable": true,
1180 "marketStatus": "TRADEABLE",
1181 "scalingFactor": 1
1182 },
1183 "pnl": null
1184 }
1185 ]
1186 }"#;
1187
1188 let resp: PositionsResponse = serde_json::from_str(json).expect("deserialize failed");
1189 assert_eq!(resp.positions.len(), 1);
1190 let pos = &resp.positions[0];
1191 assert_eq!(pos.position.deal_id, "DIFAKE111");
1192 assert_eq!(pos.position.direction, Direction::Buy);
1193 assert!((pos.position.size - 2.0).abs() < 1e-9);
1194 assert_eq!(pos.market.epic, "IX.D.FTSE.DAILY.IP");
1195 assert_eq!(pos.market.bid, Some(7550.0));
1196 assert!(pos.pnl.is_none());
1197
1198 let re = roundtrip(&resp);
1199 assert_eq!(re.positions[0].position.deal_id, "DIFAKE111");
1200 assert_eq!(re.positions[0].market.epic, "IX.D.FTSE.DAILY.IP");
1201 }
1202
1203 #[test]
1204 fn test_working_orders_response_deserialize_and_roundtrip() {
1205 let json = r#"{
1208 "workingOrders": [
1209 {
1210 "workingOrderData": {
1211 "dealId": "DIFAKEWO1",
1212 "direction": "SELL",
1213 "epic": "CS.D.EURUSD.MINI.IP",
1214 "orderSize": 1.5,
1215 "orderLevel": 1.2345,
1216 "timeInForce": "GOOD_TILL_CANCELLED",
1217 "goodTillDate": null,
1218 "goodTillDateISO": null,
1219 "createdDate": "2025/07/01 09:30:00:000",
1220 "createdDateUTC": "2025-07-01T07:30:00",
1221 "guaranteedStop": false,
1222 "orderType": "LIMIT",
1223 "stopDistance": null,
1224 "limitDistance": null,
1225 "currencyCode": "USD",
1226 "dma": false,
1227 "limitedRiskPremium": null,
1228 "limitLevel": null,
1229 "stopLevel": null,
1230 "dealReference": "REFFAKEWO1"
1231 },
1232 "marketData": {
1233 "instrumentName": "EUR/USD Mini",
1234 "exchangeId": "FX",
1235 "expiry": "-",
1236 "marketStatus": "TRADEABLE",
1237 "epic": "CS.D.EURUSD.MINI.IP",
1238 "instrumentType": "CURRENCIES",
1239 "lotSize": 1.0,
1240 "high": 1.24,
1241 "low": 1.23,
1242 "percentageChange": 0.1,
1243 "netChange": 0.001,
1244 "bid": 1.2344,
1245 "offer": 1.2346,
1246 "updateTime": "09:30:00",
1247 "updateTimeUTC": "07:30:00",
1248 "delayTime": 0,
1249 "streamingPricesAvailable": true,
1250 "scalingFactor": 1
1251 }
1252 }
1253 ]
1254 }"#;
1255
1256 let resp: WorkingOrdersResponse = serde_json::from_str(json).expect("deserialize failed");
1257 assert_eq!(resp.working_orders.len(), 1);
1258 let order = &resp.working_orders[0];
1259 assert_eq!(order.working_order_data.epic, "CS.D.EURUSD.MINI.IP");
1260 assert_eq!(order.working_order_data.direction, Direction::Sell);
1261 assert!((order.working_order_data.order_size - 1.5).abs() < 1e-9);
1262 assert!((order.working_order_data.order_level - 1.2345).abs() < 1e-9);
1263 assert_eq!(
1264 order.market_data.instrument_type,
1265 InstrumentType::Currencies
1266 );
1267
1268 let re = roundtrip(&resp);
1269 assert_eq!(re.working_orders[0].working_order_data.deal_id, "DIFAKEWO1");
1270 assert_eq!(re.working_orders[0].market_data.epic, "CS.D.EURUSD.MINI.IP");
1271 }
1272
1273 #[test]
1274 fn test_account_activity_response_deserialize_and_roundtrip() {
1275 let json = r#"{
1276 "activities": [
1277 {
1278 "date": "2025-07-01T09:00:00",
1279 "dealId": "DIFAKEACT1",
1280 "epic": "IX.D.FTSE.DAILY.IP",
1281 "period": "DAY",
1282 "dealReference": "REFFAKEACT1",
1283 "type": "POSITION",
1284 "status": "ACCEPTED",
1285 "description": "Position opened",
1286 "channel": "WEB",
1287 "currency": "GBP",
1288 "level": "7550.0"
1289 }
1290 ],
1291 "metadata": { "paging": { "size": 50, "next": null } }
1292 }"#;
1293
1294 let resp: AccountActivityResponse = serde_json::from_str(json).expect("deserialize failed");
1295 assert_eq!(resp.activities.len(), 1);
1296 let act = &resp.activities[0];
1297 assert_eq!(act.deal_id.as_deref(), Some("DIFAKEACT1"));
1298 assert_eq!(act.activity_type, ActivityType::Position);
1299 assert_eq!(act.status, Some(Status::Accepted));
1300 assert!(resp.metadata.is_some());
1301
1302 let re = roundtrip(&resp);
1303 assert_eq!(re.activities[0].activity_type, ActivityType::Position);
1304 assert_eq!(
1305 re.activities[0].deal_reference.as_deref(),
1306 Some("REFFAKEACT1")
1307 );
1308 }
1309
1310 #[test]
1311 fn test_transaction_history_response_deserialize_and_roundtrip() {
1312 let json = r#"{
1313 "transactions": [
1314 {
1315 "date": "01/07/25",
1316 "dateUtc": "2025-07-01T08:00:00",
1317 "openDateUtc": "2025-06-30T08:00:00",
1318 "instrumentName": "FTSE 100",
1319 "period": "DAY",
1320 "profitAndLoss": "E150.50",
1321 "transactionType": "DEAL",
1322 "reference": "REFFAKETX1",
1323 "openLevel": "7500.0",
1324 "closeLevel": "7550.0",
1325 "size": "2",
1326 "currency": "GBP",
1327 "cashTransaction": false
1328 }
1329 ],
1330 "metadata": {
1331 "pageData": { "pageNumber": 1, "pageSize": 20, "totalPages": 1 },
1332 "size": 1
1333 }
1334 }"#;
1335
1336 let resp: TransactionHistoryResponse =
1337 serde_json::from_str(json).expect("deserialize failed");
1338 assert_eq!(resp.transactions.len(), 1);
1339 assert_eq!(resp.transactions[0].reference, "REFFAKETX1");
1340 assert_eq!(resp.transactions[0].profit_and_loss, "E150.50");
1341 assert_eq!(resp.metadata.size, 1);
1342 assert_eq!(resp.metadata.page_data.page_number, 1);
1343
1344 let re = roundtrip(&resp);
1345 assert_eq!(re.transactions[0].reference, "REFFAKETX1");
1346 assert_eq!(re.metadata.page_data.total_pages, 1);
1347 }
1348
1349 #[test]
1350 fn test_watchlists_response_deserialize_and_roundtrip() {
1351 let json = r#"{
1352 "watchlists": [
1353 {
1354 "id": "WL1",
1355 "name": "My Watchlist",
1356 "editable": true,
1357 "deleteable": true,
1358 "defaultSystemWatchlist": false
1359 }
1360 ]
1361 }"#;
1362
1363 let resp: WatchlistsResponse = serde_json::from_str(json).expect("deserialize failed");
1364 assert_eq!(resp.watchlists.len(), 1);
1365 let wl = &resp.watchlists[0];
1366 assert_eq!(wl.id, "WL1");
1367 assert_eq!(wl.name, "My Watchlist");
1368 assert!(wl.editable);
1369 assert!(!wl.default_system_watchlist);
1370
1371 let re = roundtrip(&resp);
1372 assert_eq!(re.watchlists[0].id, "WL1");
1373 assert!(re.watchlists[0].deleteable);
1374 }
1375
1376 #[test]
1377 fn test_client_sentiment_response_deserialize_and_roundtrip() {
1378 let json = r#"{
1379 "clientSentiments": [
1380 {
1381 "marketId": "EURUSD",
1382 "longPositionPercentage": 62.5,
1383 "shortPositionPercentage": 37.5
1384 }
1385 ]
1386 }"#;
1387
1388 let resp: ClientSentimentResponse = serde_json::from_str(json).expect("deserialize failed");
1389 assert_eq!(resp.client_sentiments.len(), 1);
1390 let s = &resp.client_sentiments[0];
1391 assert_eq!(s.market_id, "EURUSD");
1392 assert!((s.long_position_percentage - 62.5).abs() < 1e-9);
1393 assert!((s.short_position_percentage - 37.5).abs() < 1e-9);
1394
1395 let re = roundtrip(&resp);
1396 assert_eq!(re.client_sentiments[0].market_id, "EURUSD");
1397 }
1398
1399 #[test]
1400 fn test_indicative_costs_response_deserialize_and_roundtrip() {
1401 let json = r#"{
1402 "indicativeQuoteReference": "QREF-FAKE-1",
1403 "costsAndCharges": {
1404 "totalCostPercentage": 0.12,
1405 "totalCostAmount": 3.45,
1406 "currency": "GBP",
1407 "oneOffCosts": { "percentage": 0.05, "amount": 1.0 },
1408 "ongoingCosts": { "percentage": 0.02, "amount": 0.5 },
1409 "transactionCosts": { "percentage": 0.03, "amount": 1.2 },
1410 "incidentalCosts": { "percentage": 0.02, "amount": 0.75 }
1411 }
1412 }"#;
1413
1414 let resp: IndicativeCostsResponse = serde_json::from_str(json).expect("deserialize failed");
1415 assert_eq!(resp.indicative_quote_reference, "QREF-FAKE-1");
1416 assert_eq!(resp.costs_and_charges.total_cost_amount, Some(3.45));
1417 assert_eq!(resp.costs_and_charges.currency.as_deref(), Some("GBP"));
1418 let one_off = resp
1419 .costs_and_charges
1420 .one_off_costs
1421 .as_ref()
1422 .expect("oneOffCosts present");
1423 assert_eq!(one_off.amount, Some(1.0));
1424
1425 let re = roundtrip(&resp);
1426 assert_eq!(re.indicative_quote_reference, "QREF-FAKE-1");
1427 assert_eq!(re.costs_and_charges.total_cost_percentage, Some(0.12));
1428 }
1429
1430 #[test]
1431 fn test_costs_and_charges_deserialize_and_roundtrip() {
1432 let json = r#"{
1433 "totalCostPercentage": 0.20,
1434 "totalCostAmount": 5.0,
1435 "currency": "USD",
1436 "transactionCosts": { "percentage": 0.10, "amount": 2.5 }
1437 }"#;
1438
1439 let costs: CostsAndCharges = serde_json::from_str(json).expect("deserialize failed");
1440 assert_eq!(costs.total_cost_amount, Some(5.0));
1441 assert_eq!(costs.currency.as_deref(), Some("USD"));
1442 assert!(costs.one_off_costs.is_none());
1444 assert!(costs.ongoing_costs.is_none());
1445 let txn = costs
1446 .transaction_costs
1447 .as_ref()
1448 .expect("transactionCosts present");
1449 assert_eq!(txn.percentage, Some(0.10));
1450
1451 let re = roundtrip(&costs);
1452 assert_eq!(re.total_cost_amount, Some(5.0));
1453 }
1454
1455 #[test]
1456 fn test_costs_history_response_deserialize_and_roundtrip() {
1457 let json = r#"{
1460 "pagination": {"pageSize": 10, "pageNumber": 1, "totalPages": 23, "totalElements": 224},
1461 "costsAndChargesHistory": [
1462 {
1463 "type": "TRADE",
1464 "direction": "SELL",
1465 "instrumentName": "",
1466 "createdTimestamp": "2026-01-12T14:38:46.401",
1467 "indicativeQuoteReference": "00000000-2a79-4710-aa44-000000000000"
1468 }
1469 ]
1470 }"#;
1471
1472 let resp: CostsHistoryResponse = serde_json::from_str(json).expect("deserialize failed");
1473 assert_eq!(resp.pagination.page_size, 10);
1474 assert_eq!(resp.pagination.total_pages, 23);
1475 assert_eq!(resp.pagination.total_elements, 224);
1476 assert_eq!(resp.costs_and_charges_history.len(), 1);
1477 let entry = &resp.costs_and_charges_history[0];
1478 assert_eq!(entry.entry_type.as_deref(), Some("TRADE"));
1479 assert_eq!(entry.direction.as_deref(), Some("SELL"));
1480 assert_eq!(entry.instrument_name.as_deref(), Some(""));
1481 assert_eq!(
1482 entry.created_timestamp.as_deref(),
1483 Some("2026-01-12T14:38:46.401")
1484 );
1485 assert_eq!(
1486 entry.indicative_quote_reference.as_deref(),
1487 Some("00000000-2a79-4710-aa44-000000000000")
1488 );
1489
1490 let re = roundtrip(&resp);
1491 assert_eq!(re.pagination.total_elements, 224);
1492 assert_eq!(
1493 re.costs_and_charges_history[0].entry_type.as_deref(),
1494 Some("TRADE")
1495 );
1496 }
1497
1498 #[test]
1499 fn test_costs_history_response_minimal_payload_defaults() {
1500 let empty_window: CostsHistoryResponse = serde_json::from_str(
1502 r#"{"pagination":{"pageSize":20,"pageNumber":1,"totalPages":0,"totalElements":0},"costsAndChargesHistory":[]}"#,
1503 )
1504 .expect("empty-window payload must deserialize");
1505 assert!(empty_window.costs_and_charges_history.is_empty());
1506 assert_eq!(empty_window.pagination.total_pages, 0);
1507
1508 let minimal: CostsHistoryResponse =
1509 serde_json::from_str("{}").expect("minimal payload must deserialize");
1510 assert!(minimal.costs_and_charges_history.is_empty());
1511 assert_eq!(minimal.pagination.page_number, 0);
1512
1513 let minimal_entry: CostsHistoryEntry =
1514 serde_json::from_str("{}").expect("minimal entry must deserialize");
1515 assert_eq!(minimal_entry.entry_type, None);
1516 assert_eq!(minimal_entry.indicative_quote_reference, None);
1517 }
1518
1519 #[test]
1520 fn test_durable_medium_response_deserialize_and_roundtrip() {
1521 let json = r#"{ "document": "<html>Terms and Conditions</html>" }"#;
1522
1523 let resp: DurableMediumResponse = serde_json::from_str(json).expect("deserialize failed");
1524 assert_eq!(resp.document, "<html>Terms and Conditions</html>");
1525
1526 let re = roundtrip(&resp);
1527 assert_eq!(re.document, "<html>Terms and Conditions</html>");
1528 }
1529
1530 #[test]
1531 fn test_account_preferences_response_deserialize_and_roundtrip() {
1532 let json = r#"{ "trailingStopsEnabled": true }"#;
1533
1534 let resp: AccountPreferencesResponse =
1535 serde_json::from_str(json).expect("deserialize failed");
1536 assert!(resp.trailing_stops_enabled);
1537
1538 let re = roundtrip(&resp);
1539 assert!(re.trailing_stops_enabled);
1540 }
1541
1542 #[test]
1543 fn test_application_details_response_deserialize_and_roundtrip() {
1544 let json = r#"{
1547 "apiKey": "FAKE-API-KEY",
1548 "name": "My App",
1549 "status": "ENABLED",
1550 "allowanceAccountOverall": 3000000000,
1551 "allowanceAccountTrading": 1000,
1552 "concurrentSubscriptionsLimit": 40,
1553 "createdDate": "2025-01-01"
1554 }"#;
1555
1556 let resp: ApplicationDetailsResponse =
1557 serde_json::from_str(json).expect("deserialize failed");
1558 assert_eq!(resp.api_key, "FAKE-API-KEY");
1559 assert_eq!(resp.name.as_deref(), Some("My App"));
1560 assert_eq!(resp.status, "ENABLED");
1561 assert!(resp.allowance_account_overall > Some(i64::from(i32::MAX)));
1562 assert_eq!(resp.allowance_account_overall, Some(3_000_000_000));
1563 assert_eq!(resp.concurrent_subscriptions_limit, Some(40));
1564
1565 let re = roundtrip(&resp);
1566 assert_eq!(re.api_key, "FAKE-API-KEY");
1567 assert_eq!(re.allowance_account_overall, Some(3_000_000_000));
1568 assert_eq!(re.allowance_account_trading, Some(1000));
1569 }
1570
1571 #[test]
1572 fn test_categories_response_deserialize_and_roundtrip() {
1573 let json = r#"{
1574 "categories": [
1575 { "code": "INDICES", "nonTradeable": false },
1576 { "code": "CRYPTOCURRENCY", "nonTradeable": true }
1577 ]
1578 }"#;
1579
1580 let resp: CategoriesResponse = serde_json::from_str(json).expect("deserialize failed");
1581 assert_eq!(resp.len(), 2);
1582 assert_eq!(resp.categories[0].code, "INDICES");
1583 assert!(!resp.categories[0].non_tradeable);
1584 assert!(resp.categories[1].non_tradeable);
1585
1586 let re = roundtrip(&resp);
1587 assert_eq!(re.categories[1].code, "CRYPTOCURRENCY");
1588 }
1589
1590 #[test]
1591 fn test_historical_prices_response_deserialize_and_roundtrip() {
1592 let json = r#"{
1593 "prices": [
1594 {
1595 "snapshotTime": "2025:07:01-09:00:00",
1596 "openPrice": { "bid": 1.2340, "ask": 1.2342, "lastTraded": null },
1597 "highPrice": { "bid": 1.2350, "ask": 1.2352, "lastTraded": null },
1598 "lowPrice": { "bid": 1.2330, "ask": 1.2332, "lastTraded": null },
1599 "closePrice": { "bid": 1.2345, "ask": 1.2347, "lastTraded": null },
1600 "lastTradedVolume": 1234
1601 }
1602 ],
1603 "instrumentType": "CURRENCIES",
1604 "allowance": {
1605 "remainingAllowance": 9950,
1606 "totalAllowance": 10000,
1607 "allowanceExpiry": 604800
1608 }
1609 }"#;
1610
1611 let resp: HistoricalPricesResponse =
1612 serde_json::from_str(json).expect("deserialize failed");
1613 assert_eq!(resp.len(), 1);
1614 assert_eq!(resp.prices[0].snapshot_time, "2025:07:01-09:00:00");
1615 assert_eq!(resp.prices[0].close_price.bid, Some(1.2345));
1616 assert_eq!(resp.prices[0].last_traded_volume, Some(1234));
1617 assert_eq!(resp.instrument_type, InstrumentType::Currencies);
1618 let allowance = resp.allowance.as_ref().expect("allowance present");
1619 assert_eq!(allowance.remaining_allowance, 9950);
1620 assert_eq!(allowance.total_allowance, 10000);
1621
1622 let re = roundtrip(&resp);
1623 assert_eq!(re.prices[0].open_price.ask, Some(1.2342));
1624 assert_eq!(
1625 re.allowance.as_ref().map(|a| a.allowance_expiry),
1626 Some(604800)
1627 );
1628 }
1629
1630 #[test]
1631 fn test_v3_login_response_deserialize_and_roundtrip() {
1632 let json = r#"{
1636 "clientId": "FAKE-CLIENT-101",
1637 "accountId": "ABC12",
1638 "timezoneOffset": 1,
1639 "lightstreamerEndpoint": "https://demo-apd.marketdatasystems.com",
1640 "oauthToken": {
1641 "access_token": "FAKE-ACCESS-TOKEN",
1642 "refresh_token": "FAKE-REFRESH-TOKEN",
1643 "scope": "profile",
1644 "token_type": "Bearer",
1645 "expires_in": "60"
1646 }
1647 }"#;
1648
1649 let resp: V3Response = serde_json::from_str(json).expect("deserialize failed");
1650 assert_eq!(resp.client_id, "FAKE-CLIENT-101");
1651 assert_eq!(resp.account_id, "ABC12");
1652 assert_eq!(resp.oauth_token.access_token, "FAKE-ACCESS-TOKEN");
1653 assert_eq!(resp.oauth_token.refresh_token, "FAKE-REFRESH-TOKEN");
1654 assert_eq!(resp.oauth_token.token_type, "Bearer");
1655 assert_eq!(resp.oauth_token.expires_in, "60");
1656
1657 let session_resp: SessionResponse =
1659 serde_json::from_str(json).expect("session deserialize failed");
1660 assert!(session_resp.is_v3());
1661 assert!(session_resp.get_session().is_oauth());
1662
1663 let re = roundtrip(&resp);
1664 assert_eq!(re.oauth_token.access_token, "FAKE-ACCESS-TOKEN");
1665 assert_eq!(re.account_id, "ABC12");
1666 }
1667}