# Implementation Plan: Bank2FF Refactoring ## 1. Objective Refactor the `bank2ff` application from a prototype script into a robust, testable, and observable production-grade CLI tool. The application must synchronize bank transactions from GoCardless to Firefly III, ensuring: - **Clean Architecture**: Decoupling of business logic from API clients. - **Testability**: Ability to unit test core logic without external dependencies. - **Multi-Currency Support**: Accurate capturing of foreign amounts and currencies. - **Idempotency**: Preventing duplicate transactions in Firefly III. ## 2. Architecture: Hexagonal (Ports & Adapters) The application will be structured into three distinct layers: 1. **Core (Domain)**: Pure Rust, no external API dependencies. Defines the `BankTransaction` model and the `Ports` (traits) for interacting with the world. 2. **Adapters**: Implementations of the Ports. - `GoCardlessAdapter`: Implements `TransactionSource`. - `FireflyAdapter`: Implements `TransactionDestination`. 3. **Application**: Configuration, CLI parsing, and wiring (`main.rs`). ### Directory Structure ```text bank2ff/src/ ├── core/ │ ├── mod.rs │ ├── models.rs # Domain entities (BankTransaction, Account) │ ├── ports.rs # Traits (TransactionSource, TransactionDestination) │ └── sync.rs # Core business logic (The "Use Case") ├── adapters/ │ ├── mod.rs │ ├── gocardless/ │ │ ├── mod.rs │ │ ├── client.rs # Wrapper around generated client (Auth/RateLimits) │ │ └── mapper.rs # Logic to map API response -> Domain Model │ └── firefly/ │ │ ├── mod.rs │ │ └── client.rs # Implementation of TransactionDestination └── main.rs # Entry point, wiring, and config loading ``` ## 3. Core definitions ### `src/core/models.rs` The domain model must support multi-currency data. ```rust pub struct BankTransaction { pub internal_id: String, // Source ID (GoCardless transactionId) pub date: NaiveDate, // Booking date pub amount: Decimal, // Amount in account currency pub currency: String, // Account currency code (e.g., EUR) pub foreign_amount: Option, // Original amount (if currency exchange occurred) pub foreign_currency: Option,// Original currency code pub description: String, pub counterparty_name: Option, pub counterparty_iban: Option, } ``` ### `src/core/ports.rs` Traits to decouple the architecture. ```rust #[async_trait] pub trait TransactionSource: Send + Sync { async fn get_accounts(&self) -> Result>; async fn get_transactions(&self, account_id: &str, start: NaiveDate, end: NaiveDate) -> Result>; } #[async_trait] pub trait TransactionDestination: Send + Sync { async fn resolve_account_id(&self, iban: &str) -> Result>; async fn ingest_transactions(&self, account_id: &str, transactions: Vec) -> Result; } ``` ## 4. Implementation Steps ### Phase 1: Infrastructure & Core 1. **Setup**: Create the directory structure. 2. **Dependencies**: Add `anyhow`, `thiserror`, `tracing`, `tracing-subscriber`, `async-trait`, `rust_decimal`, `chrono`. 3. **Core**: Implement `models.rs` and `ports.rs`. ### Phase 2: GoCardless Adapter (Source) 1. **Token Management**: Create a wrapper struct that holds the `gocardless_bankaccount_data_api` configuration and manages the Access/Refresh token lifecycle. It should check validity before every request. 2. **Mapping Logic**: - Map `transactionAmount.amount` -> `BankTransaction.amount`. - **Multi-Currency**: Inspect `currencyExchange`. - If present, map `sourceCurrency` -> `foreign_currency`. - Calculate `foreign_amount` using `exchangeRate` if `instructedAmount` is missing. - Map `transactionId` -> `internal_id`. 3. **Trait Implementation**: Implement `TransactionSource`. ### Phase 3: Firefly Adapter (Destination) 1. **Client Wrapper**: Initialize `firefly_iii_api` client with API Key. 2. **Resolution**: Implement `resolve_account_id` by querying Firefly accounts by IBAN (using the search/list endpoint). 3. **Ingestion**: - Map `BankTransaction` to `TransactionSplitStore`. - **Crucial**: Set `external_id` = `BankTransaction.internal_id`. - Handle Credits vs Debits (Swap Source/Destination logic). - Call `store_transaction` endpoint. - Handle 422/409 errors gracefully (log duplicates). ### Phase 4: Synchronization Engine 1. Implement `src/core/sync.rs`. 2. Logic: - Fetch accounts from Source. - For each account, find Destination ID. - Fetch transactions (default: last 30 days). - Filter/Process (optional). - Ingest to Destination. - Log statistics using `tracing`. ### Phase 5: Wiring 1. Refactor `main.rs`. 2. Initialize `tracing` for structured logging. 3. Load Config (Env variables). 4. Instantiate Adapters. 5. Run the Sync Engine. ## 5. Multi-Currency Handling Specifics - **GoCardless Spec**: The `currencyExchange` array contains the details. - **Logic**: - If `currencyExchange` is not null/empty: - `foreign_currency` = `currencyExchange[0].sourceCurrency` - `foreign_amount` = `amount` * `currencyExchange[0].exchangeRate` (Validation: check if `unitCurrency` affects this). - Ensure `Decimal` precision is handled correctly. ## 6. Testability & Observability - **Tests**: Write a unit test for `core/sync.rs` using Mock structs for Source/Destination to verify the flow logic. - **Logs**: Use `tracing::info!` for high-level progress ("Synced Account X") and `tracing::debug!` for details ("Transaction Y mapped to Z").