diff --git a/banks2ff/src/adapters/firefly/client.rs b/banks2ff/src/adapters/firefly/client.rs index 8f499d0..3207d33 100644 --- a/banks2ff/src/adapters/firefly/client.rs +++ b/banks2ff/src/adapters/firefly/client.rs @@ -1,3 +1,4 @@ +use crate::core::cache::{AccountCache, CachedAccount}; use crate::core::config::Config; use crate::core::models::{Account, AccountSummary, BankTransaction}; use crate::core::ports::{TransactionDestination, TransactionMatch}; @@ -114,31 +115,44 @@ impl TransactionDestination for FireflyAdapter { let is_credit = tx.amount.is_sign_positive(); let transaction_type = if is_credit { "deposit" } else { "withdrawal" }; + // Determine source and destination based on IBAN linking + let (source_id, source_name, destination_id, destination_name) = if is_credit { + // Deposit: money coming in, source is counterparty, destination is user's account + let destination_id = Some(account_id.to_string()); + let (source_id, source_name) = if let Some(iban) = &tx.counterparty_iban { + if let Some(acc_id) = self.find_account_by_iban(iban) { + (Some(acc_id), None) + } else { + (None, tx.counterparty_name.clone()) + } + } else { + (None, tx.counterparty_name.clone()) + }; + (source_id, source_name, destination_id, None) + } else { + // Withdrawal: money going out, source is user's account, destination is counterparty + let source_id = Some(account_id.to_string()); + let (destination_id, destination_name) = if let Some(iban) = &tx.counterparty_iban { + if let Some(acc_id) = self.find_account_by_iban(iban) { + (Some(acc_id), None) + } else { + (None, tx.counterparty_name.clone()) + } + } else { + (None, tx.counterparty_name.clone()) + }; + (source_id, None, destination_id, destination_name) + }; + let split = TransactionSplitStore { transaction_type: transaction_type.to_string(), date: tx.date.format("%Y-%m-%d").to_string(), amount: tx.amount.abs().to_string(), description: tx.description.clone(), - source_id: if !is_credit { - Some(account_id.to_string()) - } else { - None - }, - source_name: if is_credit { - tx.counterparty_name.clone() - } else { - None - }, - destination_id: if is_credit { - Some(account_id.to_string()) - } else { - None - }, - destination_name: if !is_credit { - tx.counterparty_name.clone() - } else { - None - }, + source_id, + source_name, + destination_id, + destination_name, currency_code: Some(tx.currency.clone()), foreign_amount: tx.foreign_amount.map(|d| d.abs().to_string()), foreign_currency_code: tx.foreign_currency.clone(), @@ -172,7 +186,7 @@ impl TransactionDestination for FireflyAdapter { #[instrument(skip(self))] async fn discover_accounts(&self) -> Result> { let client = self.client.lock().await; - let accounts = client.get_accounts("").await?; + let accounts = client.get_accounts().await?; let mut result = Vec::new(); // Cache the accounts @@ -181,56 +195,57 @@ impl TransactionDestination for FireflyAdapter { crate::core::cache::AccountCache::load(self.config.cache.directory.clone(), encryption); for acc in accounts.data { - let is_active = acc.attributes.active.unwrap_or(true); - if is_active { - // Cache the full account details - let ff_account = crate::core::cache::FireflyAccount { - id: acc.id.clone(), - name: acc.attributes.name.clone(), - account_type: acc.attributes.account_type.clone(), - iban: acc.attributes.iban.clone(), - active: acc.attributes.active, - order: acc.attributes.order, - created_at: acc.attributes.created_at.clone(), - account_role: acc.attributes.account_role.clone(), - object_group_id: acc.attributes.object_group_id.clone(), - object_group_title: acc.attributes.object_group_title.clone(), - object_group_order: acc.attributes.object_group_order, - currency_id: acc.attributes.currency_id.clone(), - currency_name: acc.attributes.currency_name.clone(), - currency_code: acc.attributes.currency_code.clone(), - currency_symbol: acc.attributes.currency_symbol.clone(), - currency_decimal_places: acc.attributes.currency_decimal_places, - primary_currency_id: acc.attributes.primary_currency_id.clone(), - primary_currency_name: acc.attributes.primary_currency_name.clone(), - primary_currency_code: acc.attributes.primary_currency_code.clone(), - primary_currency_symbol: acc.attributes.primary_currency_symbol.clone(), - primary_currency_decimal_places: acc.attributes.primary_currency_decimal_places, - opening_balance: acc.attributes.opening_balance.clone(), - pc_opening_balance: acc.attributes.pc_opening_balance.clone(), - debt_amount: acc.attributes.debt_amount.clone(), - pc_debt_amount: acc.attributes.pc_debt_amount.clone(), - notes: acc.attributes.notes.clone(), - monthly_payment_date: acc.attributes.monthly_payment_date.clone(), - credit_card_type: acc.attributes.credit_card_type.clone(), - account_number: acc.attributes.account_number.clone(), - bic: acc.attributes.bic.clone(), - opening_balance_date: acc.attributes.opening_balance_date.clone(), - liability_type: acc.attributes.liability_type.clone(), - liability_direction: acc.attributes.liability_direction.clone(), - interest: acc.attributes.interest.clone(), - interest_period: acc.attributes.interest_period.clone(), - include_net_worth: acc.attributes.include_net_worth, - longitude: acc.attributes.longitude, - latitude: acc.attributes.latitude, - zoom_level: acc.attributes.zoom_level, - last_activity: acc.attributes.last_activity.clone(), - }; - cache.insert(crate::core::cache::CachedAccount::Firefly(Box::new( - ff_account, - ))); - cache.save(); + // Cache all accounts, regardless of active status + let ff_account = crate::core::cache::FireflyAccount { + id: acc.id.clone(), + name: acc.attributes.name.clone(), + account_type: acc.attributes.account_type.clone(), + iban: acc.attributes.iban.clone(), + active: acc.attributes.active, + order: acc.attributes.order, + created_at: acc.attributes.created_at.clone(), + account_role: acc.attributes.account_role.clone(), + object_group_id: acc.attributes.object_group_id.clone(), + object_group_title: acc.attributes.object_group_title.clone(), + object_group_order: acc.attributes.object_group_order, + currency_id: acc.attributes.currency_id.clone(), + currency_name: acc.attributes.currency_name.clone(), + currency_code: acc.attributes.currency_code.clone(), + currency_symbol: acc.attributes.currency_symbol.clone(), + currency_decimal_places: acc.attributes.currency_decimal_places, + primary_currency_id: acc.attributes.primary_currency_id.clone(), + primary_currency_name: acc.attributes.primary_currency_name.clone(), + primary_currency_code: acc.attributes.primary_currency_code.clone(), + primary_currency_symbol: acc.attributes.primary_currency_symbol.clone(), + primary_currency_decimal_places: acc.attributes.primary_currency_decimal_places, + opening_balance: acc.attributes.opening_balance.clone(), + pc_opening_balance: acc.attributes.pc_opening_balance.clone(), + debt_amount: acc.attributes.debt_amount.clone(), + pc_debt_amount: acc.attributes.pc_debt_amount.clone(), + notes: acc.attributes.notes.clone(), + monthly_payment_date: acc.attributes.monthly_payment_date.clone(), + credit_card_type: acc.attributes.credit_card_type.clone(), + account_number: acc.attributes.account_number.clone(), + bic: acc.attributes.bic.clone(), + opening_balance_date: acc.attributes.opening_balance_date.clone(), + liability_type: acc.attributes.liability_type.clone(), + liability_direction: acc.attributes.liability_direction.clone(), + interest: acc.attributes.interest.clone(), + interest_period: acc.attributes.interest_period.clone(), + include_net_worth: acc.attributes.include_net_worth, + longitude: acc.attributes.longitude, + latitude: acc.attributes.latitude, + zoom_level: acc.attributes.zoom_level, + last_activity: acc.attributes.last_activity.clone(), + }; + cache.insert(crate::core::cache::CachedAccount::Firefly(Box::new( + ff_account, + ))); + cache.save(); + // Only return active asset accounts for linking (existing behavior) + let is_active = acc.attributes.active.unwrap_or(true); + if is_active && acc.attributes.account_type == "asset" { result.push(Account { id: acc.id, name: Some(acc.attributes.name), @@ -250,22 +265,41 @@ impl TransactionDestination for FireflyAdapter { let mut summaries = Vec::new(); - // Use cached account data for display + // Use cached account data for display, filter to show only asset and liability accounts for (account_id, cached_account) in &cache.accounts { if let crate::core::cache::CachedAccount::Firefly(ff_account) = cached_account { - let summary = AccountSummary { - id: account_id.clone(), - name: Some(ff_account.name.clone()), - iban: ff_account.iban.clone().unwrap_or_else(|| "".to_string()), - currency: ff_account - .currency_code - .clone() - .unwrap_or_else(|| "EUR".to_string()), - }; - summaries.push(summary); + if ff_account.account_type == "asset" || ff_account.account_type == "liability" { + let summary = AccountSummary { + id: account_id.clone(), + name: Some(ff_account.name.clone()), + iban: ff_account.iban.clone().unwrap_or_else(|| "".to_string()), + currency: ff_account + .currency_code + .clone() + .unwrap_or_else(|| "EUR".to_string()), + }; + summaries.push(summary); + } } } Ok(summaries) } } + +impl FireflyAdapter { + fn find_account_by_iban(&self, iban: &str) -> Option { + let encryption = crate::core::encryption::Encryption::new(self.config.cache.key.clone()); + let cache = AccountCache::load(self.config.cache.directory.clone(), encryption); + for cached_account in cache.accounts.values() { + if let CachedAccount::Firefly(ff_account) = cached_account { + if ff_account.iban.as_ref() == Some(&iban.to_string()) + && ff_account.active.unwrap_or(true) + { + return Some(ff_account.id.clone()); + } + } + } + None + } +} diff --git a/firefly-client/src/client.rs b/firefly-client/src/client.rs index 4f9b4da..c4bfa2f 100644 --- a/firefly-client/src/client.rs +++ b/firefly-client/src/client.rs @@ -43,9 +43,8 @@ impl FireflyClient { } #[instrument(skip(self))] - pub async fn get_accounts(&self, _iban: &str) -> Result { - let mut url = self.base_url.join("/api/v1/accounts")?; - url.query_pairs_mut().append_pair("type", "asset"); + pub async fn get_accounts(&self) -> Result { + let url = self.base_url.join("/api/v1/accounts")?; self.get_authenticated(url).await }