From a1f67e71e27d6ca83da01ab4311c706b099a0086 Mon Sep 17 00:00:00 2001 From: Hans Kokx Date: Thu, 19 Mar 2026 20:46:58 +0100 Subject: [PATCH] Update API references from OpenExchange to ForexRateAPI in code and documentation Signed-off-by: Hans Kokx --- README.md | 4 +- lib/screens/settings_screen.dart | 2 +- lib/services/open_exchange_service.dart | 76 +++++++++++++++++++++---- lib/state/rent_controller.dart | 40 ++++++++----- 4 files changed, 95 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 2e6d3d0..ee110f2 100644 --- a/README.md +++ b/README.md @@ -23,10 +23,10 @@ Simple Flutter app to track US rental income and convert to SEK for Swedish tax ## Setup -1. Set your Open Exchange Rates app key in `lib/state/rent_controller.dart`: +1. Set your FX API app key in `lib/state/rent_controller.dart`: ```dart -const String openExchangeApiKey = 'REPLACE_WITH_YOUR_OPENEXCHANGE_API_KEY'; +const String forexRateApiKey = 'REPLACE_WITH_YOUR_FOREXRATE_API_KEY'; ``` 1. Install dependencies: diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 0f3e6ad..8080682 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -112,7 +112,7 @@ class _SettingsScreenState extends State { ), const SizedBox(height: 16), const Text( - 'OpenExchange API key is configured in code constant openExchangeApiKey.', + 'ForexRateAPI key is configured in code constant forexRateApiKey.', ), ], ), diff --git a/lib/services/open_exchange_service.dart b/lib/services/open_exchange_service.dart index 3ad5c82..e12be88 100644 --- a/lib/services/open_exchange_service.dart +++ b/lib/services/open_exchange_service.dart @@ -3,30 +3,42 @@ import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:intl/intl.dart'; -class OpenExchangeService { - OpenExchangeService({required this.httpClient, required this.apiKey}); +class ForexConversionResult { + ForexConversionResult({required this.quote, required this.result}); + + final double quote; + final double result; +} + +class ForexRateApiService { + ForexRateApiService({required this.httpClient, required this.apiKey}); final http.Client httpClient; final String apiKey; Future fetchUsdToSekRate({required DateTime estDate}) async { - if (apiKey == 'REPLACE_WITH_YOUR_OPENEXCHANGE_API_KEY') { - throw StateError('OpenExchange API key is not configured.'); + if (apiKey == 'REPLACE_WITH_YOUR_FOREXRATE_API_KEY') { + throw StateError('ForexRateAPI key is not configured.'); } final date = DateFormat('yyyy-MM-dd').format(estDate); - final uri = Uri.https( - 'openexchangerates.org', - '/api/historical/$date.json', - {'app_id': apiKey, 'symbols': 'SEK'}, - ); + final uri = Uri.https('api.forexrateapi.com', '/v1/$date', { + 'api_key': apiKey, + 'base': 'USD', + 'currencies': 'SEK', + }); final response = await httpClient.get(uri); if (response.statusCode != 200) { - throw StateError('OpenExchange request failed (${response.statusCode}).'); + throw StateError('ForexRateAPI request failed (${response.statusCode}).'); } final json = jsonDecode(response.body) as Map; + final success = json['success'] as bool? ?? false; + if (!success) { + throw StateError('ForexRateAPI returned success=false.'); + } + final rates = json['rates'] as Map?; final sek = rates?['SEK']; if (sek == null) { @@ -35,4 +47,48 @@ class OpenExchangeService { return (sek as num).toDouble(); } + + Future convertUsdToSek({ + required DateTime estDate, + required double amount, + }) async { + if (apiKey == 'REPLACE_WITH_YOUR_FOREXRATE_API_KEY') { + throw StateError('ForexRateAPI key is not configured.'); + } + + final date = DateFormat('yyyy-MM-dd').format(estDate); + final uri = + Uri.https('api.forexrateapi.com', '/v1/convert', { + 'api_key': apiKey, + 'from': 'USD', + 'to': 'SEK', + 'amount': amount.toStringAsFixed(2), + 'date': date, + }); + + final response = await httpClient.get(uri); + if (response.statusCode != 200) { + throw StateError('ForexRateAPI convert failed (${response.statusCode}).'); + } + + final json = jsonDecode(response.body) as Map; + final success = json['success'] as bool? ?? false; + if (!success) { + throw StateError('ForexRateAPI convert returned success=false.'); + } + + final info = json['info'] as Map?; + final quote = info?['quote']; + final result = json['result']; + if (quote == null || result == null) { + throw StateError( + 'ForexRateAPI convert response was missing quote/result.', + ); + } + + return ForexConversionResult( + quote: (quote as num).toDouble(), + result: (result as num).toDouble(), + ); + } } diff --git a/lib/state/rent_controller.dart b/lib/state/rent_controller.dart index 5fd90c6..d2c7a32 100644 --- a/lib/state/rent_controller.dart +++ b/lib/state/rent_controller.dart @@ -13,7 +13,7 @@ import 'package:rental_income_tracker/services/storage_service.dart'; import 'package:rental_income_tracker/services/time_service.dart'; import 'package:timezone/timezone.dart' as tz; -const String openExchangeApiKey = 'REPLACE_WITH_YOUR_OPENEXCHANGE_API_KEY'; +const String forexRateApiKey = 'REPLACE_WITH_YOUR_FOREXRATE_API_KEY'; class RentTableRow { RentTableRow({ @@ -40,19 +40,19 @@ class RentTableRow { class RentController extends ChangeNotifier { RentController({ StorageService? storageService, - OpenExchangeService? exchangeService, + ForexRateApiService? exchangeService, }) : _storageService = storageService ?? StorageService(), _exchangeService = exchangeService ?? - OpenExchangeService( + ForexRateApiService( httpClient: http.Client(), - apiKey: openExchangeApiKey, + apiKey: forexRateApiKey, ) { _notificationService = NotificationService(_onNotificationAction); } final StorageService _storageService; - final OpenExchangeService _exchangeService; + final ForexRateApiService _exchangeService; late NotificationService _notificationService; AppSettings _settings = AppSettings.defaults(); @@ -146,12 +146,21 @@ class RentController extends ChangeNotifier { throw StateError('Set rent amount in Settings before marking paid.'); } + final key = TimeService.monthKey(year, month); + if (_records.containsKey(key)) { + await _notificationService.cancelForMonth(year, month); + await _syncNotificationScheduleForCurrentMonth(); + return; + } + final paidAt = DateTime.now().toUtc(); + final usd = _settings.rentUsd; final estPaidAt = tz.TZDateTime.from(paidAt, TimeService.est); final estDate = DateTime(estPaidAt.year, estPaidAt.month, estPaidAt.day); - final rate = await _exchangeService.fetchUsdToSekRate(estDate: estDate); - final usd = _settings.rentUsd; - final sek = usd * rate; + final conversion = await _exchangeService.convertUsdToSek( + estDate: estDate, + amount: usd, + ); final onTime = assumeOnTime || TimeService.isOnTimeByDeadline(paidAt, year, month); @@ -159,8 +168,8 @@ class RentController extends ChangeNotifier { year: year, month: month, usdAmount: usd, - sekAmount: sek, - usdToSekRate: rate, + sekAmount: conversion.result, + usdToSekRate: conversion.quote, paidAtUtc: paidAt, onTime: onTime, source: source, @@ -194,9 +203,12 @@ class RentController extends ChangeNotifier { continue; } - final estDate = DateTime(year, month, 1); - final rate = await _exchangeService.fetchUsdToSekRate(estDate: estDate); final usd = _settings.rentUsd; + final estDate = DateTime(year, month, 1); + final conversion = await _exchangeService.convertUsdToSek( + estDate: estDate, + amount: usd, + ); final paidAtUtc = tz.TZDateTime( TimeService.est, @@ -210,8 +222,8 @@ class RentController extends ChangeNotifier { year: year, month: month, usdAmount: usd, - sekAmount: usd * rate, - usdToSekRate: rate, + sekAmount: conversion.result, + usdToSekRate: conversion.quote, paidAtUtc: paidAtUtc, onTime: true, source: 'backfill',