use ib_flex::{parse_activity_flex, AssetCategory, BuySell, OpenClose, PutCall};
#[test]
fn test_parse_minimal_statement() {
let xml = include_str!("fixtures/activity_minimal.xml");
let result = parse_activity_flex(xml);
assert!(result.is_ok(), "Failed to parse XML: {:?}", result.err());
let statement = result.unwrap();
assert_eq!(statement.account_id, "U1234567");
assert_eq!(statement.from_date.to_string(), "2025-01-15");
assert_eq!(statement.to_date.to_string(), "2025-01-15");
}
#[test]
fn test_parse_trades() {
let xml = include_str!("fixtures/activity_minimal.xml");
let statement = parse_activity_flex(xml).unwrap();
assert_eq!(statement.trades.items.len(), 1);
let trade = &statement.trades.items[0];
assert_eq!(trade.symbol, "AAPL");
assert_eq!(trade.asset_category, AssetCategory::Stock);
assert_eq!(trade.buy_sell, Some(BuySell::Buy));
assert_eq!(trade.quantity.unwrap().to_string(), "100");
assert_eq!(trade.price.unwrap().to_string(), "185.50");
}
#[test]
fn test_empty_sections() {
let xml = include_str!("fixtures/activity_minimal.xml");
let statement = parse_activity_flex(xml).unwrap();
assert_eq!(statement.positions.items.len(), 0);
assert_eq!(statement.cash_transactions.items.len(), 0);
assert_eq!(statement.corporate_actions.items.len(), 0);
}
#[test]
fn test_securities_info_parsing() {
let xml = include_str!("fixtures/activity_simple.xml");
let result = parse_activity_flex(xml);
match result {
Ok(statement) => {
println!(
"Successfully parsed with {} securities",
statement.securities_info.items.len()
);
}
Err(e) => {
println!("Expected error with complex XML: {}", e);
}
}
}
#[test]
fn test_commission_calculation() {
let xml = include_str!("fixtures/activity_minimal.xml");
let statement = parse_activity_flex(xml).unwrap();
let total_commission: rust_decimal::Decimal = statement
.trades
.items
.iter()
.filter_map(|t| t.commission)
.sum();
assert_eq!(total_commission.to_string(), "-1.00");
}
#[test]
fn test_trade_proceeds() {
let xml = include_str!("fixtures/activity_minimal.xml");
let statement = parse_activity_flex(xml).unwrap();
let trade = &statement.trades.items[0];
assert_eq!(trade.proceeds.unwrap().to_string(), "-18550.00");
assert_eq!(
trade.net_cash.map(|d| d.to_string()),
Some("-18551.00".to_string())
);
}
#[test]
fn test_account_id_extraction() {
let xml = include_str!("fixtures/activity_minimal.xml");
let statement = parse_activity_flex(xml).unwrap();
assert!(!statement.account_id.is_empty());
assert_eq!(statement.account_id, "U1234567");
}
#[test]
fn test_date_range_parsing() {
let xml = include_str!("fixtures/activity_minimal.xml");
let statement = parse_activity_flex(xml).unwrap();
assert!(statement.from_date <= statement.to_date);
}
#[test]
fn test_parse_options_trades() {
let xml = include_str!("fixtures/activity_options.xml");
let statement = parse_activity_flex(xml).unwrap();
assert_eq!(statement.trades.items.len(), 4);
let call_trade = &statement.trades.items[0];
assert_eq!(call_trade.symbol, "AAPL 250221C00180000");
assert_eq!(call_trade.asset_category, AssetCategory::Option);
assert_eq!(call_trade.put_call, Some(PutCall::Call));
assert_eq!(call_trade.strike.unwrap().to_string(), "180");
assert_eq!(call_trade.multiplier.unwrap().to_string(), "100");
assert_eq!(call_trade.underlying_symbol, Some("AAPL".to_string()));
assert_eq!(call_trade.open_close, Some(OpenClose::Open));
}
#[test]
fn test_parse_options_short_put() {
let xml = include_str!("fixtures/activity_options.xml");
let statement = parse_activity_flex(xml).unwrap();
let put_trade = &statement.trades.items[1];
assert_eq!(put_trade.symbol, "TSLA 250221P00200000");
assert_eq!(put_trade.put_call, Some(PutCall::Put));
assert_eq!(put_trade.strike.unwrap().to_string(), "200");
assert_eq!(put_trade.quantity.unwrap().to_string(), "-2");
assert_eq!(put_trade.underlying_symbol, Some("TSLA".to_string()));
}
#[test]
fn test_parse_options_assignment() {
let xml = include_str!("fixtures/activity_options.xml");
let statement = parse_activity_flex(xml).unwrap();
let assignment = &statement.trades.items[2];
assert_eq!(assignment.symbol, "MSFT 250115C00400000");
assert_eq!(assignment.open_close, Some(OpenClose::Close));
assert!(assignment.fifo_pnl_realized.is_some());
assert_eq!(assignment.fifo_pnl_realized.unwrap().to_string(), "1050");
}
#[test]
fn test_parse_options_positions() {
let xml = include_str!("fixtures/activity_options.xml");
let statement = parse_activity_flex(xml).unwrap();
assert_eq!(statement.positions.items.len(), 2);
let long_pos = &statement.positions.items[0];
assert_eq!(long_pos.quantity.to_string(), "1");
assert_eq!(long_pos.asset_category, AssetCategory::Option);
let short_pos = &statement.positions.items[1];
assert_eq!(short_pos.quantity.to_string(), "-2");
}
#[test]
fn test_parse_futures_trades() {
let xml = include_str!("fixtures/activity_futures.xml");
let statement = parse_activity_flex(xml).unwrap();
assert_eq!(statement.trades.items.len(), 4);
let es_trade = &statement.trades.items[0];
assert_eq!(es_trade.symbol, "ESH5");
assert_eq!(es_trade.asset_category, AssetCategory::Future);
assert_eq!(es_trade.multiplier.unwrap().to_string(), "50");
assert_eq!(es_trade.underlying_symbol, Some("ES".to_string()));
}
#[test]
fn test_parse_futures_commodity() {
let xml = include_str!("fixtures/activity_futures.xml");
let statement = parse_activity_flex(xml).unwrap();
let gc_trade = &statement.trades.items[3];
assert_eq!(gc_trade.symbol, "GCG5");
}
#[test]
fn test_parse_futures_positions() {
let xml = include_str!("fixtures/activity_futures.xml");
let statement = parse_activity_flex(xml).unwrap();
assert_eq!(statement.positions.items.len(), 3);
let es_pos = &statement.positions.items[0];
assert_eq!(es_pos.quantity.to_string(), "2");
assert_eq!(es_pos.asset_category, AssetCategory::Future);
}
#[test]
fn test_parse_forex_trades() {
let xml = include_str!("fixtures/activity_forex.xml");
let statement = parse_activity_flex(xml).unwrap();
assert_eq!(statement.trades.items.len(), 4);
let eur_trade = &statement.trades.items[0];
assert_eq!(eur_trade.symbol, "EUR.USD");
assert_eq!(eur_trade.asset_category, AssetCategory::Cash);
assert_eq!(eur_trade.quantity.unwrap().to_string(), "50000");
}
#[test]
fn test_parse_forex_positions() {
let xml = include_str!("fixtures/activity_forex.xml");
let statement = parse_activity_flex(xml).unwrap();
assert_eq!(statement.positions.items.len(), 3);
let eur_pos = &statement.positions.items[0];
assert_eq!(eur_pos.quantity.to_string(), "50000");
assert_eq!(eur_pos.currency, "EUR");
}
#[test]
fn test_parse_forex_conversion_rates() {
let xml = include_str!("fixtures/activity_forex.xml");
let statement = parse_activity_flex(xml).unwrap();
assert_eq!(statement.conversion_rates.items.len(), 5);
let eur_rate = statement
.conversion_rates
.items
.iter()
.find(|r| r.from_currency == "EUR")
.unwrap();
assert_eq!(eur_rate.rate.to_string(), "1.0855");
}
#[test]
fn test_parse_bonds_trades() {
let xml = include_str!("fixtures/activity_bonds.xml");
let statement = parse_activity_flex(xml).unwrap();
assert_eq!(statement.trades.items.len(), 4);
let treasury = &statement.trades.items[0];
assert_eq!(treasury.symbol, "T 4.5 11/15/33");
assert_eq!(treasury.asset_category, AssetCategory::Bond);
}
#[test]
fn test_parse_bonds_corporate() {
let xml = include_str!("fixtures/activity_bonds.xml");
let statement = parse_activity_flex(xml).unwrap();
let corp_bond = &statement.trades.items[1];
assert_eq!(corp_bond.symbol, "AAPL 3.85 08/04/46");
}
#[test]
fn test_parse_bonds_positions() {
let xml = include_str!("fixtures/activity_bonds.xml");
let statement = parse_activity_flex(xml).unwrap();
assert_eq!(statement.positions.items.len(), 3);
for pos in &statement.positions.items {
assert_eq!(pos.asset_category, AssetCategory::Bond);
assert!(pos.quantity > rust_decimal::Decimal::ZERO);
}
}
#[test]
fn test_parse_corporate_actions() {
let xml = include_str!("fixtures/activity_corporate_actions.xml");
let statement = parse_activity_flex(xml).unwrap();
assert_eq!(statement.corporate_actions.items.len(), 8);
}
#[test]
fn test_parse_dividend_action() {
let xml = include_str!("fixtures/activity_corporate_actions.xml");
let statement = parse_activity_flex(xml).unwrap();
let dividend = &statement.corporate_actions.items[0];
assert_eq!(dividend.symbol, "AAPL");
assert_eq!(dividend.action_type, Some("DI".to_string()));
assert_eq!(dividend.quantity.unwrap().to_string(), "400");
assert_eq!(dividend.amount.unwrap().to_string(), "100.00");
}
#[test]
fn test_parse_stock_split() {
let xml = include_str!("fixtures/activity_corporate_actions.xml");
let statement = parse_activity_flex(xml).unwrap();
let split = &statement.corporate_actions.items[1];
assert_eq!(split.symbol, "TSLA");
assert_eq!(split.action_type, Some("FS".to_string()));
}
#[test]
fn test_parse_merger() {
let xml = include_str!("fixtures/activity_corporate_actions.xml");
let statement = parse_activity_flex(xml).unwrap();
let merger = &statement.corporate_actions.items[3];
assert_eq!(merger.symbol, "ACQUIRED");
assert_eq!(merger.action_type, Some("TC".to_string()));
assert_eq!(merger.fifo_pnl_realized.unwrap().to_string(), "1500.00");
}
#[test]
fn test_parse_cash_transactions() {
let xml = include_str!("fixtures/activity_cash.xml");
let statement = parse_activity_flex(xml).unwrap();
assert_eq!(statement.cash_transactions.items.len(), 15);
}
#[test]
fn test_parse_deposit_withdrawal() {
let xml = include_str!("fixtures/activity_cash.xml");
let statement = parse_activity_flex(xml).unwrap();
let deposit = &statement.cash_transactions.items[0];
assert_eq!(deposit.amount.to_string(), "10000.00");
assert_eq!(
deposit.transaction_type,
Some("Deposits & Withdrawals".to_string())
);
let withdrawal = &statement.cash_transactions.items[1];
assert_eq!(withdrawal.amount.to_string(), "-5000.00");
}
#[test]
fn test_parse_interest_transactions() {
let xml = include_str!("fixtures/activity_cash.xml");
let statement = parse_activity_flex(xml).unwrap();
let credit = &statement.cash_transactions.items[2];
assert_eq!(
credit.transaction_type,
Some("Broker Interest Paid".to_string())
);
assert!(credit.amount > rust_decimal::Decimal::ZERO);
}
#[test]
fn test_parse_dividend_cash() {
let xml = include_str!("fixtures/activity_cash.xml");
let statement = parse_activity_flex(xml).unwrap();
let dividend = &statement.cash_transactions.items[4];
assert_eq!(dividend.symbol.clone().unwrap(), "AAPL");
assert_eq!(dividend.transaction_type, Some("Dividends".to_string()));
assert_eq!(dividend.amount.to_string(), "150.00");
}
#[test]
fn test_parse_warrants() {
let xml = include_str!("fixtures/activity_warrants.xml");
let statement = parse_activity_flex(xml).unwrap();
assert_eq!(statement.trades.items.len(), 2);
let buy = &statement.trades.items[0];
assert_eq!(buy.asset_category, AssetCategory::Warrant);
assert_eq!(buy.symbol, "ABC WS");
assert_eq!(buy.buy_sell, Some(BuySell::Buy));
assert_eq!(buy.quantity.unwrap().to_string(), "100");
let sell = &statement.trades.items[1];
assert_eq!(sell.asset_category, AssetCategory::Warrant);
assert_eq!(sell.buy_sell, Some(BuySell::Sell));
assert!(sell.fifo_pnl_realized.is_some());
}
#[test]
fn test_parse_warrant_position() {
let xml = include_str!("fixtures/activity_warrants.xml");
let statement = parse_activity_flex(xml).unwrap();
assert_eq!(statement.positions.items.len(), 1);
let position = &statement.positions.items[0];
assert_eq!(position.asset_category, AssetCategory::Warrant);
assert_eq!(position.symbol, "ABC WS");
assert_eq!(position.quantity.to_string(), "50");
}
#[test]
fn test_parse_tbills() {
let xml = include_str!("fixtures/activity_tbills.xml");
let statement = parse_activity_flex(xml).unwrap();
assert_eq!(statement.trades.items.len(), 1);
let trade = &statement.trades.items[0];
assert_eq!(trade.asset_category, AssetCategory::Bill);
assert_eq!(trade.symbol, "912796ZX9");
assert_eq!(trade.buy_sell, Some(BuySell::Buy));
assert_eq!(trade.quantity.unwrap().to_string(), "10000");
}
#[test]
fn test_parse_tbill_position() {
let xml = include_str!("fixtures/activity_tbills.xml");
let statement = parse_activity_flex(xml).unwrap();
assert_eq!(statement.positions.items.len(), 1);
let position = &statement.positions.items[0];
assert_eq!(position.asset_category, AssetCategory::Bill);
assert_eq!(position.quantity.to_string(), "10000");
}
#[test]
fn test_parse_tbill_maturity() {
let xml = include_str!("fixtures/activity_tbills.xml");
let statement = parse_activity_flex(xml).unwrap();
assert_eq!(statement.corporate_actions.items.len(), 1);
let action = &statement.corporate_actions.items[0];
assert_eq!(action.asset_category, Some(AssetCategory::Bill));
assert_eq!(action.action_type, Some("TC".to_string()));
}
#[test]
fn test_parse_cfds() {
let xml = include_str!("fixtures/activity_cfds.xml");
let statement = parse_activity_flex(xml).unwrap();
assert_eq!(statement.trades.items.len(), 2);
let buy = &statement.trades.items[0];
assert_eq!(buy.asset_category, AssetCategory::Cfd);
assert_eq!(buy.symbol, "AAPL");
assert_eq!(buy.buy_sell, Some(BuySell::Buy));
let sell = &statement.trades.items[1];
assert_eq!(sell.asset_category, AssetCategory::Cfd);
assert_eq!(sell.buy_sell, Some(BuySell::Sell));
assert!(sell.fifo_pnl_realized.is_some());
}
#[test]
fn test_parse_cfd_financing() {
let xml = include_str!("fixtures/activity_cfds.xml");
let statement = parse_activity_flex(xml).unwrap();
assert_eq!(statement.cash_transactions.items.len(), 1);
let financing = &statement.cash_transactions.items[0];
assert_eq!(financing.asset_category, Some(AssetCategory::Cfd));
assert_eq!(financing.transaction_type, Some("Other Fees".to_string()));
}
#[test]
fn test_parse_cancelled_trades() {
let xml = include_str!("fixtures/activity_cancelled_trades.xml");
let statement = parse_activity_flex(xml).unwrap();
assert_eq!(statement.trades.items.len(), 4);
let buy = &statement.trades.items[0];
assert_eq!(buy.buy_sell, Some(BuySell::Buy));
assert_eq!(buy.symbol, "MSFT");
let cancel_buy = &statement.trades.items[1];
assert_eq!(cancel_buy.buy_sell, Some(BuySell::CancelBuy));
assert_eq!(cancel_buy.symbol, "MSFT");
let sell = &statement.trades.items[2];
assert_eq!(sell.buy_sell, Some(BuySell::Sell));
assert_eq!(sell.symbol, "GOOGL");
let cancel_sell = &statement.trades.items[3];
assert_eq!(cancel_sell.buy_sell, Some(BuySell::CancelSell));
assert_eq!(cancel_sell.symbol, "GOOGL");
}
#[test]
fn test_parse_fractional_shares() {
let xml = include_str!("fixtures/activity_fractional_shares.xml");
let statement = parse_activity_flex(xml).unwrap();
assert_eq!(statement.trades.items.len(), 4);
let buy = &statement.trades.items[0];
assert_eq!(buy.symbol, "AMZN");
assert_eq!(buy.quantity.unwrap().to_string(), "2.5");
let sell = &statement.trades.items[1];
assert_eq!(sell.quantity.unwrap().to_string(), "-1.25");
}
#[test]
fn test_parse_fractional_cancellation() {
let xml = include_str!("fixtures/activity_fractional_shares.xml");
let statement = parse_activity_flex(xml).unwrap();
let cancel = &statement.trades.items[3];
assert_eq!(cancel.buy_sell, Some(BuySell::CancelBuy));
}
#[test]
fn test_parse_fractional_position() {
let xml = include_str!("fixtures/activity_fractional_shares.xml");
let statement = parse_activity_flex(xml).unwrap();
assert_eq!(statement.positions.items.len(), 1);
let position = &statement.positions.items[0];
assert_eq!(position.symbol, "AMZN");
assert_eq!(position.quantity.to_string(), "1.25");
}
#[test]
fn test_parse_complex_corporate_actions() {
let xml = include_str!("fixtures/activity_complex_corporate_actions.xml");
let statement = parse_activity_flex(xml).unwrap();
assert_eq!(statement.corporate_actions.items.len(), 10);
}
#[test]
fn test_parse_choice_dividend() {
let xml = include_str!("fixtures/activity_complex_corporate_actions.xml");
let statement = parse_activity_flex(xml).unwrap();
let choice = &statement.corporate_actions.items[0];
assert_eq!(choice.action_type, Some("CD".to_string()));
let _delivery = &statement.corporate_actions.items[1];
}
#[test]
fn test_parse_tender_offer() {
let xml = include_str!("fixtures/activity_complex_corporate_actions.xml");
let statement = parse_activity_flex(xml).unwrap();
let tender = &statement.corporate_actions.items[2];
assert_eq!(tender.action_type, Some("TO".to_string()));
assert_eq!(tender.quantity.unwrap().to_string(), "-50");
let issue = &statement.corporate_actions.items[3];
assert_eq!(issue.proceeds.unwrap().to_string(), "3500.00");
}
#[test]
fn test_parse_bond_conversion() {
let xml = include_str!("fixtures/activity_complex_corporate_actions.xml");
let statement = parse_activity_flex(xml).unwrap();
let conversion = &statement.corporate_actions.items[4];
assert_eq!(conversion.action_type, Some("BC".to_string()));
assert_eq!(conversion.asset_category, Some(AssetCategory::Bond));
let issue = &statement.corporate_actions.items[5];
assert_eq!(issue.asset_category, Some(AssetCategory::Stock));
assert_eq!(issue.quantity.unwrap().to_string(), "250");
}
#[test]
fn test_parse_bond_maturity() {
let xml = include_str!("fixtures/activity_complex_corporate_actions.xml");
let statement = parse_activity_flex(xml).unwrap();
let maturity = &statement.corporate_actions.items[6];
assert_eq!(maturity.action_type, Some("BM".to_string()));
assert_eq!(maturity.asset_category, Some(AssetCategory::Bond));
assert_eq!(maturity.proceeds.unwrap().to_string(), "5000.00");
}
#[test]
fn test_parse_coupon_payment() {
let xml = include_str!("fixtures/activity_complex_corporate_actions.xml");
let statement = parse_activity_flex(xml).unwrap();
let coupon = &statement.corporate_actions.items[7];
assert_eq!(coupon.action_type, Some("CP".to_string()));
assert_eq!(coupon.proceeds.unwrap().to_string(), "200.00");
}
#[test]
fn test_parse_rights_issue() {
let xml = include_str!("fixtures/activity_complex_corporate_actions.xml");
let statement = parse_activity_flex(xml).unwrap();
let rights = &statement.corporate_actions.items[8];
assert_eq!(rights.action_type, Some("RI".to_string()));
let subscribe = &statement.corporate_actions.items[9];
assert_eq!(subscribe.action_type, Some("SR".to_string()));
}