using System.Diagnostics; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; using ExpenseTracker.Application.Common.Exceptions; using ExpenseTracker.Application.Common.Interfaces.Services; using ExpenseTracker.Domain.Enums; namespace ExpenseTracker.Infrastructure.Services; public sealed class CurrencyConverterService : ICurrencyConverterService { private readonly ILogger _logger; private readonly HttpClient _httpClient; private readonly string? _apiKey; private readonly JsonSerializerSettings _jsonSerializerSettings = new JsonSerializerSettings() { ContractResolver = new CamelCasePropertyNamesContractResolver() }; private Dictionary> exchangeRateCache; public CurrencyConverterService( ILogger logger, IHttpClientFactory httpClientFactory, IConfiguration configuration) { _logger = logger; _httpClient = httpClientFactory.CreateClient("CurrencyConverterService"); _apiKey = configuration.GetSection("FreeCurrencyAPI").GetValue("ApiKey"); InitializeCache(); } public async Task GetLatestExchangeRate( Currency fromCurrency, Currency toCurrency, CancellationToken cancellationToken) { if (fromCurrency.Equals(toCurrency)) { return 1.0; } var currentDate = DateOnly.FromDateTime(DateTime.UtcNow); if (TryGetExchangeRateFromCache(currentDate, fromCurrency, toCurrency, out double? result)) { return (double)result; } HttpResponseMessage? response; try { response = await _httpClient.GetAsync( $"v1/latest" + $"?apikey={_apiKey}" + $"&base_currency={fromCurrency.ToString()}" + $"¤cies={toCurrency.ToString()}", cancellationToken); response.EnsureSuccessStatusCode(); } catch (HttpRequestException exception) { _logger.LogError($"{exception.Message}\n{exception.StackTrace}"); throw new CurrencyConverterException(exception.Message); } var json = await response.Content.ReadAsStringAsync(cancellationToken); _logger.LogDebug( "{@DateUtc} {@TimeUtc} {@TraceId} {@SpanId} Received data: {@data}", DateTime.UtcNow.ToString("yyyy-MM-dd"), DateTime.UtcNow.ToString("HH:mm:ss.FFF"), Activity.Current?.TraceId.ToString(), Activity.Current?.SpanId.ToString(), json); dynamic deserealizedObject = JsonConvert.DeserializeObject(json); var exchangeRate = (double)deserealizedObject["data"][toCurrency.ToString()]; AddToCache(currentDate, fromCurrency, toCurrency, exchangeRate); return exchangeRate; } public async Task GetHistoricalExchangeRate( Currency fromCurrency, Currency toCurrency, DateOnly date, CancellationToken cancellationToken) { { if (fromCurrency.Equals(toCurrency)) return 1.0; } if (TryGetExchangeRateFromCache(date, fromCurrency, toCurrency, out double? result)) { return (double)result; } HttpResponseMessage? response; try { response = await _httpClient.GetAsync( $"v1/historical" + $"?apikey={_apiKey}" + $"&base_currency={fromCurrency.ToString()}" + $"¤cies={toCurrency.ToString()}" + $"&date={date.ToString("yyyy-MM-dd")}", cancellationToken); response.EnsureSuccessStatusCode(); } catch (HttpRequestException exception) { _logger.LogError($"{exception.Message}\n{exception.StackTrace}"); throw new CurrencyConverterException(exception.Message); } var json = await response.Content.ReadAsStringAsync(cancellationToken); _logger.LogDebug( "{@DateUtc} {@TimeUtc} {@TraceId} {@SpanId} Received data: {@data}", DateTime.UtcNow.ToString("yyyy-MM-dd"), DateTime.UtcNow.ToString("HH:mm:ss.FFF"), Activity.Current?.TraceId.ToString(), Activity.Current?.SpanId.ToString(), json); dynamic deserealizedObject = JsonConvert.DeserializeObject(json); var exchangeRate = (double)deserealizedObject["data"][date.ToString("yyyy-MM-dd")][toCurrency.ToString()]; AddToCache(date, fromCurrency, toCurrency, exchangeRate); return exchangeRate; } private void InitializeCache() { exchangeRateCache = new Dictionary>(); } private void AddToCache(DateOnly date, Currency fromCurrency, Currency toCurrency, double exchangeRate) { if (!exchangeRateCache.ContainsKey(date)) { var newDict = new Dictionary<(Currency, Currency), double>(); exchangeRateCache.Add(date, newDict); } if (!exchangeRateCache[date].ContainsKey((fromCurrency, toCurrency))) { exchangeRateCache[date].Add((fromCurrency, toCurrency), exchangeRate); _logger.LogDebug( "{@DateUtc} {@TimeUtc} {@TraceId} {@SpanId} Add data to cache: {@Date} {@FromCurrency} -> {@ToCurrency} {@ExchangeRate}", DateTime.UtcNow.ToString("yyyy-MM-dd"), DateTime.UtcNow.ToString("HH:mm:ss.FFF"), Activity.Current?.TraceId.ToString(), Activity.Current?.SpanId.ToString(), date, fromCurrency, toCurrency, exchangeRate); } } private bool TryGetExchangeRateFromCache(DateOnly date, Currency fromCurrency, Currency toCurrency, out double? exchangeRate) { if (exchangeRateCache.ContainsKey(date) && exchangeRateCache[date].ContainsKey((fromCurrency, toCurrency))) { exchangeRate = exchangeRateCache[date][(fromCurrency, toCurrency)]; _logger.LogDebug( "{@DateUtc} {@TimeUtc} {@TraceId} {@SpanId} Returned value from cache: {@Date} {@FromCurrency} -> {@ToCurrency} {@Value}", DateTime.UtcNow.ToString("yyyy-MM-dd"), DateTime.UtcNow.ToString("HH:mm:ss.FFF"), Activity.Current?.TraceId.ToString(), Activity.Current?.SpanId.ToString(), date, fromCurrency, toCurrency, exchangeRate); return true; } exchangeRate = null; return false; } }