import 'dart:async'; import 'dart:convert'; 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('api_key')) { redactedQuery['api_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_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/$date', { 'api_key': apiKey, 'base': 'USD', 'currencies': 'SEK', }); _log('Daily rate request: ${_redactedUri(uri)}'); final response = await _getWithRateLimitRetry(uri, operation: 'Daily rate'); if (response.statusCode != 200) { throw StateError('ForexRateAPI 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('ForexRateAPI returned success=false. error=$error'); } final rates = json['rates'] as Map?; final sek = rates?['SEK']; if (sek == null) { throw StateError('SEK rate was missing in API response.'); } _log('Daily rate parsed SEK quote: $sek'); 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 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); } /// Fetches USD->SEK cross-rates for every day of [year] in a single timeseries /// API call. Returns a map of 'yyyy-MM-dd' -> rate, or null if the timeseries /// endpoint is not available on the current subscription plan. Future?> tryFetchYearRates(int year) async { if (apiKey == 'REPLACE_WITH_YOUR_FOREXRATE_API_KEY') { throw StateError('ForexRateAPI key is not configured.'); } return null; } }