Add exchange rate service with caching and fallback API

ExchangeRateService fetches rates from fawazahmed0/exchange-api with
Frankfurter API as fallback. Rates are cached in SQLite for 24 hours.
Includes response parsers, 30 supported currencies, and 8 unit tests
covering parsing, caching, and same-currency identity.
This commit is contained in:
2026-03-02 00:14:50 +02:00
parent a3ffc531b9
commit e342272dbe
2 changed files with 285 additions and 0 deletions

View File

@@ -387,6 +387,39 @@ impl Database {
rows.collect()
}
// -- Exchange Rates --
pub fn get_cached_rate(&self, base: &str, target: &str) -> SqlResult<Option<ExchangeRate>> {
match self.conn.query_row(
"SELECT base, target, rate, fetched_at FROM exchange_rates WHERE base = ?1 AND target = ?2",
params![base.to_lowercase(), target.to_lowercase()],
|row| {
Ok(ExchangeRate {
base: row.get(0)?,
target: row.get(1)?,
rate: row.get(2)?,
fetched_at: row.get(3)?,
})
},
) {
Ok(rate) => Ok(Some(rate)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(e),
}
}
pub fn cache_rates(&self, base: &str, rates: &std::collections::HashMap<String, f64>) -> SqlResult<()> {
let now = chrono::Utc::now().to_rfc3339();
let base_lower = base.to_lowercase();
let mut stmt = self.conn.prepare(
"INSERT OR REPLACE INTO exchange_rates (base, target, rate, fetched_at) VALUES (?1, ?2, ?3, ?4)"
)?;
for (target, rate) in rates {
stmt.execute(params![base_lower, target.to_lowercase(), rate, now])?;
}
Ok(())
}
// -- Settings --
pub fn get_setting(&self, key: &str) -> SqlResult<Option<String>> {