Completely replace implementation #1
129
specs/planning.md
Normal file
129
specs/planning.md
Normal file
@@ -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<Decimal>, // Original amount (if currency exchange occurred)
|
||||
pub foreign_currency: Option<String>,// Original currency code
|
||||
pub description: String,
|
||||
pub counterparty_name: Option<String>,
|
||||
pub counterparty_iban: Option<String>,
|
||||
}
|
||||
```
|
||||
|
||||
### `src/core/ports.rs`
|
||||
Traits to decouple the architecture.
|
||||
|
||||
```rust
|
||||
#[async_trait]
|
||||
pub trait TransactionSource: Send + Sync {
|
||||
async fn get_accounts(&self) -> Result<Vec<Account>>;
|
||||
async fn get_transactions(&self, account_id: &str, start: NaiveDate, end: NaiveDate) -> Result<Vec<BankTransaction>>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait TransactionDestination: Send + Sync {
|
||||
async fn resolve_account_id(&self, iban: &str) -> Result<Option<String>>;
|
||||
async fn ingest_transactions(&self, account_id: &str, transactions: Vec<BankTransaction>) -> Result<IngestResult>;
|
||||
}
|
||||
```
|
||||
|
||||
## 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").
|
||||
Reference in New Issue
Block a user