Files
banks2ff/docs/architecture.md
Jacob Kiers 74d362b412 Handle expired agreements and rewrite README
- Implement robust End User Agreement expiry detection and handling
- Add graceful error recovery for failed accounts
- Rewrite README.md to focus on user benefits
- Add documentation guidelines to AGENTS.md
2025-11-21 19:30:54 +01:00

5.5 KiB

Architecture Documentation

Overview

Banks2FF implements a Hexagonal (Ports & Adapters) Architecture to synchronize bank transactions from GoCardless to Firefly III. This architecture separates business logic from external concerns, making the system testable and maintainable.

Workspace Structure

banks2ff/
├── banks2ff/           # Main CLI application
│   └── src/
│       ├── core/       # Domain logic and models
│       ├── adapters/   # External service integrations
│       └── main.rs     # CLI entry point
├── firefly-client/     # Firefly III API client library
├── gocardless-client/  # GoCardless API client library
└── docs/              # Architecture documentation

Core Components

1. Domain Core (banks2ff/src/core/)

models.rs: Defines domain entities

  • BankTransaction: Core transaction model with multi-currency support
  • Account: Bank account representation
  • Supports foreign_amount and foreign_currency for international transactions

ports.rs: Defines abstraction traits

  • TransactionSource: Interface for fetching transactions (implemented by GoCardless adapter)
  • TransactionDestination: Interface for storing transactions (implemented by Firefly adapter)
  • Traits are mockable for isolated testing

sync.rs: Synchronization engine

  • run_sync(): Orchestrates the entire sync process
  • Implements "Healer" strategy for idempotency
  • Smart date range calculation (Last Transaction Date + 1 to Yesterday)

2. Adapters (banks2ff/src/adapters/)

gocardless/: GoCardless integration

  • client.rs: Wrapper for GoCardless client with token management
  • mapper.rs: Converts GoCardless API responses to domain models
  • cache.rs: Caches account mappings to reduce API calls
  • Correctly handles multi-currency via currencyExchange array parsing

firefly/: Firefly III integration

  • client.rs: Wrapper for Firefly client for transaction storage
  • Maps domain models to Firefly API format

3. API Clients

Both clients are hand-crafted using reqwest:

  • Strongly-typed DTOs for compile-time safety
  • Custom error handling with thiserror
  • Rate limit awareness and graceful degradation

Synchronization Process

The "Healer" strategy ensures idempotency with robust error handling:

  1. Account Discovery: Fetch active accounts from GoCardless (filtered by End User Agreement (EUA) validity)
  2. Agreement Validation: Check EUA expiry status for each account's requisition
  3. Account Matching: Match GoCardless accounts to Firefly asset accounts by IBAN
  4. Error-Aware Processing: Continue with valid accounts when some have expired agreements
  5. Date Window: Calculate sync range (Last Firefly transaction + 1 to Yesterday)
  6. Transaction Processing (with error recovery):
    • Search: Look for existing transaction using windowed heuristic (date ± 3 days, exact amount)
    • Heal: If found without external_id, update with GoCardless transaction ID
    • Skip: If found with matching external_id, ignore
    • Create: If not found, create new transaction in Firefly
    • Error Handling: Log issues but continue with other transactions/accounts

Key Features

Multi-Currency Support

  • Parses currencyExchange array from GoCardless responses
  • Calculates foreign_amount = amount * exchange_rate
  • Maps to Firefly's foreign_amount and foreign_currency_code fields

Rate Limit Management

  • Caching: Stores AccountId -> IBAN mappings to reduce requisition calls
  • Token Reuse: Maintains tokens until expiry to minimize auth requests
  • Graceful Handling: Continues sync for other accounts when encountering 429 errors

Agreement Expiry Handling

  • Proactive Validation: Checks End User Agreement (EUA) expiry before making API calls to avoid unnecessary requests
  • Reactive Recovery: Detects expired agreements from API 401 errors and skips affected accounts
  • Continued Operation: Maintains partial sync success even when some accounts are inaccessible
  • User Feedback: Provides detailed reporting on account status and re-authorization needs
  • Multiple Requisitions: Supports accounts linked to multiple requisitions, using the most recent valid one

Idempotency

  • GoCardless transactionId → Firefly external_id mapping
  • Windowed duplicate detection prevents double-creation
  • Historical transaction healing for pre-existing data

Data Flow

GoCardless API → GoCardlessAdapter → TransactionSource → SyncEngine → TransactionDestination → FireflyAdapter → Firefly API

Testing Strategy

  • Unit Tests: Core logic with mockall for trait mocking
  • Integration Tests: API clients with wiremock for HTTP mocking
  • Fixture Testing: Real JSON responses for adapter mapping validation
  • Isolation: Business logic tested without external dependencies

Error Handling

  • Custom Errors: thiserror for domain-specific error types including End User Agreement (EUA) expiry (SyncError::AgreementExpired)
  • Propagation: anyhow for error context across async boundaries
  • Graceful Degradation: Rate limits, network issues, and expired agreements don't crash entire sync
  • Partial Success: Continues processing available accounts when some fail
  • Structured Logging: tracing for observability and debugging with account-level context

Configuration Management

  • Environment variables loaded via dotenvy
  • Workspace-level dependency management
  • Feature flags for optional functionality
  • Secure credential handling (no hardcoded secrets)