From ab81c729c79cce3771a3d8cd23a436517c745281 Mon Sep 17 00:00:00 2001 From: Jacob Kiers Date: Wed, 19 Nov 2025 20:37:53 +0000 Subject: [PATCH] Add high-level planning --- specs/planning.md | 129 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 specs/planning.md diff --git a/specs/planning.md b/specs/planning.md new file mode 100644 index 0000000..3090df6 --- /dev/null +++ b/specs/planning.md @@ -0,0 +1,129 @@ +# 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").