diff --git a/banks2ff/src/adapters/gocardless/mapper.rs b/banks2ff/src/adapters/gocardless/mapper.rs index d270d7c..db01151 100644 --- a/banks2ff/src/adapters/gocardless/mapper.rs +++ b/banks2ff/src/adapters/gocardless/mapper.rs @@ -27,14 +27,31 @@ pub fn map_transaction(tx: Transaction) -> Result { if let Some(exchanges) = tx.currency_exchange { if let Some(exchange) = exchanges.first() { - if let (Some(source_curr), Some(rate_str)) = - (&exchange.source_currency, &exchange.exchange_rate) - { - foreign_currency = Some(source_curr.clone()); + if let (Some(source_curr), Some(target_curr), Some(rate_str)) = ( + &exchange.source_currency, + &exchange.target_currency, + &exchange.exchange_rate, + ) { if let Ok(rate) = Decimal::from_str(rate_str) { - let calc = amount.abs() * rate; - let sign = amount.signum(); - foreign_amount = Some(calc * sign); + if !rate.is_zero() { + let (foreign_curr, calc) = if currency == *target_curr { + // Transaction is in target currency, foreign is source + (source_curr.clone(), amount.abs() / rate) + } else if currency == *source_curr { + // Transaction is in source currency, foreign is target + (target_curr.clone(), amount.abs() * rate) + } else { + // Unexpected currency configuration, skip + tracing::warn!("Transaction currency '{}' does not match exchange source '{}' or target '{}', skipping foreign amount calculation", + currency, source_curr, target_curr); + (String::new(), Decimal::ZERO) // dummy values, will be skipped + }; + if !foreign_curr.is_empty() { + foreign_currency = Some(foreign_curr); + let sign = amount.signum(); + foreign_amount = Some(calc * sign); + } + } } } } @@ -149,7 +166,7 @@ mod tests { } #[test] - fn test_map_multicurrency_transaction() { + fn test_map_multicurrency_transaction_target_to_source() { let t = Transaction { transaction_id: Some("124".into()), entry_reference: None, @@ -167,7 +184,7 @@ mod tests { }, currency_exchange: Some(vec![CurrencyExchange { source_currency: Some("USD".into()), - exchange_rate: Some("1.10".into()), + exchange_rate: Some("2.0".into()), unit_currency: None, target_currency: Some("EUR".into()), }]), @@ -193,13 +210,65 @@ mod tests { assert_eq!(res.amount, Decimal::new(-1000, 2)); assert_eq!(res.foreign_currency, Some("USD".to_string())); - // 10.00 * 1.10 = 11.00. Sign should be preserved (-11.00) - assert_eq!(res.foreign_amount, Some(Decimal::new(-1100, 2))); + // Transaction in target (EUR), foreign in source (USD): 10.00 / 2.0 = 5.00, sign preserved (-5.00) + assert_eq!(res.foreign_amount, Some(Decimal::new(-500, 2))); // Description fallback to creditor name assert_eq!(res.description, "US Shop"); } + #[test] + fn test_map_multicurrency_transaction_source_to_target() { + let t = Transaction { + transaction_id: Some("125".into()), + entry_reference: None, + end_to_end_id: None, + mandate_id: None, + check_id: None, + creditor_id: None, + booking_date: Some("2023-01-03".into()), + value_date: None, + booking_date_time: None, + value_date_time: None, + transaction_amount: TransactionAmount { + amount: "-10.00".into(), + currency: "USD".into(), + }, + currency_exchange: Some(vec![CurrencyExchange { + source_currency: Some("USD".into()), + exchange_rate: Some("2.0".into()), + unit_currency: None, + target_currency: Some("EUR".into()), + }]), + creditor_name: Some("EU Shop".into()), + creditor_account: None, + ultimate_creditor: None, + debtor_name: None, + debtor_account: None, + ultimate_debtor: None, + remittance_information_unstructured: None, + remittance_information_unstructured_array: None, + remittance_information_structured: None, + remittance_information_structured_array: None, + additional_information: None, + purpose_code: None, + bank_transaction_code: None, + proprietary_bank_transaction_code: None, + internal_transaction_id: None, + }; + + let res = map_transaction(t).unwrap(); + assert_eq!(res.internal_id, "125"); + assert_eq!(res.amount, Decimal::new(-1000, 2)); + assert_eq!(res.foreign_currency, Some("EUR".to_string())); + + // Transaction in source (USD), foreign in target (EUR): 10.00 * 2.0 = 20.00, sign preserved (-20.00) + assert_eq!(res.foreign_amount, Some(Decimal::new(-2000, 2))); + + // Description fallback to creditor name + assert_eq!(res.description, "EU Shop"); + } + #[test] fn test_validate_amount_zero() { let amount = Decimal::ZERO; @@ -307,7 +376,7 @@ mod tests { } #[test] - fn test_map_transaction_invalid_foreign_amount() { + fn test_map_transaction_invalid_exchange_rate() { let t = Transaction { transaction_id: Some("127".into()), entry_reference: None, @@ -325,7 +394,7 @@ mod tests { }, currency_exchange: Some(vec![CurrencyExchange { source_currency: Some("USD".into()), - exchange_rate: Some("0".into()), // This will make foreign_amount zero + exchange_rate: Some("0".into()), // Invalid rate is handled by not setting foreign_amount unit_currency: None, target_currency: Some("EUR".into()), }]), @@ -346,7 +415,8 @@ mod tests { internal_transaction_id: None, }; - assert!(map_transaction(t).is_err()); + let res = map_transaction(t).unwrap(); + assert_eq!(res.foreign_amount, None); // Invalid rate results in no foreign_amount } #[test]