Refactor ForexRateApiService to remove API key dependency and update README and launch configurations

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-20 10:40:28 +01:00
parent b37dec35f5
commit 67af59bd0d
9 changed files with 82 additions and 136 deletions
+39 -34
View File
@@ -41,35 +41,17 @@ class RentController extends ChangeNotifier {
RentController({
StorageService? storageService,
ForexRateApiService? exchangeService,
FrankfurterApiService? exchangeService,
NotificationService? notificationService,
}) : _storageService = storageService ?? StorageService(),
_exchangeService =
exchangeService ??
ForexRateApiService(
httpClient: http.Client(),
apiKey: _exchangeRateApiKeyFromEnv(),
) {
exchangeService ?? FrankfurterApiService(httpClient: http.Client()) {
_notificationService =
notificationService ?? NotificationService(_onNotificationAction);
}
static String _exchangeRateApiKeyFromEnv() {
const key = String.fromEnvironment('FOREX_RATE_API_KEY');
final trimmed = key.trim();
if (trimmed.isEmpty) {
if (kDebugMode) {
debugPrint(
'[RentController] FOREX_RATE_API_KEY missing from dart defines. Pass --dart-define-from-file=.env (or --dart-define) when building/running.',
);
}
return 'REPLACE_WITH_YOUR_FOREXRATE_API_KEY';
}
return trimmed;
}
final StorageService _storageService;
final ForexRateApiService _exchangeService;
final FrankfurterApiService _exchangeService;
late NotificationService _notificationService;
AppSettings _settings = AppSettings.defaults();
@@ -79,6 +61,33 @@ class RentController extends ChangeNotifier {
String? _errorMessage;
int _selectedYear = DateTime.now().year;
double? _lookupRateOnOrBefore(
Map<String, double> rates,
DateTime targetDate,
) {
final target = DateTime(targetDate.year, targetDate.month, targetDate.day);
DateTime? nearestDate;
double? nearestRate;
for (final entry in rates.entries) {
final parsed = DateTime.tryParse(entry.key);
if (parsed == null) {
continue;
}
final day = DateTime(parsed.year, parsed.month, parsed.day);
if (day.isAfter(target)) {
continue;
}
if (nearestDate == null || day.isAfter(nearestDate)) {
nearestDate = day;
nearestRate = entry.value;
}
}
return nearestRate;
}
AppSettings get settings => _settings;
bool get isLoading => _isLoading;
String? get errorMessage => _errorMessage;
@@ -334,27 +343,25 @@ class RentController extends ChangeNotifier {
final usd = _settings.rentUsd;
final estDate = DateTime(year, month, 1);
final ForexConversionResult conversion;
final ConversionResult conversion;
if (batchRates != null) {
final dateStr = DateFormat('yyyy-MM-dd').format(estDate);
final quote = batchRates[dateStr];
final quote = _lookupRateOnOrBefore(batchRates, estDate);
if (quote == null) {
throw StateError('Batch rates missing entry for $dateStr.');
throw StateError(
'Batch rates missing entry on or before ${DateFormat('yyyy-MM-dd').format(estDate)}.',
);
}
conversion = ForexConversionResult(
quote: quote,
result: quote * usd,
);
conversion = ConversionResult(quote: quote, result: quote * usd);
} else {
// Pace individual requests to avoid triggering the rate limit.
if (!isFirstIndividualFetch) {
await Future.delayed(const Duration(milliseconds: 700));
}
isFirstIndividualFetch = false;
conversion = await _exchangeService.convertUsdToSek(
final quote = await _exchangeService.fetchUsdToSekRate(
estDate: estDate,
amount: usd,
);
conversion = ConversionResult(quote: quote, result: quote * usd);
}
if (kDebugMode) {
@@ -385,9 +392,7 @@ class RentController extends ChangeNotifier {
savedMonths++;
} catch (err) {
final errorText = err.toString();
if (errorText.contains('429') ||
errorText.contains('104') ||
errorText.toLowerCase().contains('rate_limit_reached')) {
if (errorText.contains('429')) {
pausedByRateLimit = true;
_errorMessage =
'Backfill paused by API rate limits after $savedMonths new month(s). Run backfill again in a minute to continue.';