import 'dart:convert'; import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import 'package:intl/intl.dart'; 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; void _log(String message) { if (kDebugMode) { debugPrint('[ForexRateApiService] $message'); } } String _redactedUri(Uri uri) { final redactedQuery = {...uri.queryParameters}; if (redactedQuery.containsKey('access_key')) { redactedQuery['access_key'] = '***REDACTED***'; } return uri.replace(queryParameters: redactedQuery).toString(); } bool _isRateLimitedResponse(http.Response response) { if (response.statusCode == 429) { return true; } try { final parsed = jsonDecode(response.body) as Map; final error = parsed['error'] as Map?; final code = error?['code']?.toString().toLowerCase(); return code == 'rate_limit_reached'; } catch (_) { return false; } } Future _getWithRateLimitRetry( Uri uri, { required String operation, }) async { const maxAttempts = 3; for (var attempt = 1; attempt <= maxAttempts; attempt++) { final response = await httpClient.get(uri); _log('$operation HTTP status: ${response.statusCode}'); _log('$operation raw response: ${response.body}'); final shouldRetry = _isRateLimitedResponse(response); if (!shouldRetry || attempt == maxAttempts) { return response; } final delayMs = 1200 * attempt; _log( '$operation rate limited; retrying in ${delayMs}ms (attempt ${attempt + 1}/$maxAttempts)', ); await Future.delayed(Duration(milliseconds: delayMs)); } throw StateError('Unexpected rate-limit retry flow for $operation.'); } Future fetchUsdToSekRate({required DateTime estDate}) async { if (apiKey == 'REPLACE_WITH_YOUR_EXCHANGE_RATE_API_KEY') { throw StateError('ExchangeratesAPI key is not configured.'); } final date = DateFormat('yyyy-MM-dd').format(estDate); final uri = Uri.https( 'api.exchangeratesapi.io', '/v1/$date', // Free-tier plans can restrict custom base currencies. // We request EUR-based USD and SEK rates and derive USD->SEK via cross-rate. {'access_key': apiKey, 'symbols': 'USD,SEK'}, ); _log('Daily rate request: ${_redactedUri(uri)}'); final response = await _getWithRateLimitRetry(uri, operation: 'Daily rate'); if (response.statusCode != 200) { throw StateError( 'ExchangeratesAPI request failed (${response.statusCode}).', ); } final json = jsonDecode(response.body) as Map; final success = json['success'] as bool? ?? false; if (!success) { final error = json['error']; _log('Daily rate success=false. error=$error'); throw StateError('ExchangeratesAPI returned success=false. error=$error'); } final rates = json['rates'] as Map?; final usdPerEur = rates?['USD']; final sekPerEur = rates?['SEK']; if (usdPerEur == null || sekPerEur == null) { throw StateError('USD/SEK rates were missing in API response.'); } final usdPerEurValue = (usdPerEur as num).toDouble(); final sekPerEurValue = (sekPerEur as num).toDouble(); if (usdPerEurValue == 0) { throw StateError('USD rate was zero; cannot derive USD->SEK.'); } final usdToSek = sekPerEurValue / usdPerEurValue; _log( 'Daily rate parsed usdPerEur=$usdPerEurValue sekPerEur=$sekPerEurValue derivedUsdToSek=$usdToSek', ); return usdToSek; } Future convertUsdToSek({ required DateTime estDate, required double amount, }) async { if (apiKey == 'REPLACE_WITH_YOUR_EXCHANGE_RATE_API_KEY') { throw StateError('ExchangeratesAPI key is not configured.'); } final quote = await fetchUsdToSekRate(estDate: estDate); final result = quote * amount; _log( 'Computed conversion from daily rate quote=$quote result=$result amount=$amount date=${DateFormat('yyyy-MM-dd').format(estDate)}', ); return ForexConversionResult(quote: quote, result: result); } }