From 044b22dbb1bfa3793c65b4ce506b6dfa74e9f9b2 Mon Sep 17 00:00:00 2001 From: jdiazbello Date: Sat, 2 Aug 2025 08:31:51 +0200 Subject: [PATCH 01/11] Update gitignore --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index fd3586545..2650ad75f 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,7 @@ node_modules bower_components npm-debug.log +/jobs/Backend/Task/.vs/ExchangeRateUpdater/CopilotIndices/17.14.939.21063/SemanticSymbols.db +*.db-shm +*.db-wal +*.vsidx From ac780008aa5f99e49e8355622b80829cb6172172 Mon Sep 17 00:00:00 2001 From: jdiazbello Date: Sat, 2 Aug 2025 12:26:54 +0200 Subject: [PATCH 02/11] Modify Structure --- .../DesignTimeBuild/.dtbcache.v2 | Bin 0 -> 312 bytes .../v17/DocumentLayout.backup.json | 280 ++++++++++++++++++ .../v17/DocumentLayout.json | 280 ++++++++++++++++++ jobs/Backend/Task/App.cs | 54 ++++ jobs/Backend/Task/Currency.cs | 27 +- jobs/Backend/Task/ExchangeRate.cs | 23 -- jobs/Backend/Task/ExchangeRateProvider.cs | 19 -- jobs/Backend/Task/ExchangeRateUpdater.csproj | 6 + .../Factories/ExchangeRateProviderFactory.cs | 27 ++ .../Task/Interfaces/IExchangeRateProvider.cs | 10 + .../Models/Countries/CZE/CnbExchangeRate.cs | 24 ++ .../Countries/CZE/CnbExchangeRateTable.cs | 13 + .../Countries/CZE/CnbExchangeRatesResponse.cs | 19 ++ jobs/Backend/Task/Models/CountryIsoAlpha3.cs | 6 + jobs/Backend/Task/Models/ExchangeRate.cs | 22 ++ jobs/Backend/Task/Program.cs | 50 +--- .../Countries/CZE/CzeExchangeRateProvider.cs | 68 +++++ .../Task/Services/ExchangeRateProvider.cs | 15 + jobs/Backend/Task/Startup.cs | 27 ++ 19 files changed, 878 insertions(+), 92 deletions(-) create mode 100644 jobs/Backend/Task/.vs/ExchangeRateUpdater/DesignTimeBuild/.dtbcache.v2 create mode 100644 jobs/Backend/Task/.vs/ExchangeRateUpdater/v17/DocumentLayout.backup.json create mode 100644 jobs/Backend/Task/.vs/ExchangeRateUpdater/v17/DocumentLayout.json create mode 100644 jobs/Backend/Task/App.cs delete mode 100644 jobs/Backend/Task/ExchangeRate.cs delete mode 100644 jobs/Backend/Task/ExchangeRateProvider.cs create mode 100644 jobs/Backend/Task/Factories/ExchangeRateProviderFactory.cs create mode 100644 jobs/Backend/Task/Interfaces/IExchangeRateProvider.cs create mode 100644 jobs/Backend/Task/Models/Countries/CZE/CnbExchangeRate.cs create mode 100644 jobs/Backend/Task/Models/Countries/CZE/CnbExchangeRateTable.cs create mode 100644 jobs/Backend/Task/Models/Countries/CZE/CnbExchangeRatesResponse.cs create mode 100644 jobs/Backend/Task/Models/CountryIsoAlpha3.cs create mode 100644 jobs/Backend/Task/Models/ExchangeRate.cs create mode 100644 jobs/Backend/Task/Services/Countries/CZE/CzeExchangeRateProvider.cs create mode 100644 jobs/Backend/Task/Services/ExchangeRateProvider.cs create mode 100644 jobs/Backend/Task/Startup.cs diff --git a/jobs/Backend/Task/.vs/ExchangeRateUpdater/DesignTimeBuild/.dtbcache.v2 b/jobs/Backend/Task/.vs/ExchangeRateUpdater/DesignTimeBuild/.dtbcache.v2 new file mode 100644 index 0000000000000000000000000000000000000000..48b225e7c7f784d350cebd39cb44d94cb8f6b25f GIT binary patch literal 312 zcmW+y%}xR_7%U>{$peW;6Q98BLc5EelwCQPm=Kj)4}RNz>=u@mwA~1V1BuV$Q}{xC z1Q+HoGq?G&~iFR$3_T(uQ3DTrs$f5{k0+L4XAkz`Vj7DM*iQzCEC3BXH_^WaFrpgBE{O=cbW2|e#b0eA+YVUZd zeAN_uh2M@ZOyT$##1ggSpWv4KWg{wRJHLa+*;=-2?}b~pYdXg_S{(KA currencies = new[] + { + new Currency("USD"), + new Currency("EUR"), + new Currency("CZK"), + new Currency("JPY"), + new Currency("KES"), + new Currency("RUB"), + new Currency("THB"), + new Currency("TRY"), + new Currency("XYZ") +}; + + public App( + TextWriter output, + ExchangeRateProviderFactory factory) + { + _output = output; + _factory = factory; + } + + public async Task Run() + { + try + { + var provider = _factory.CreateProvider(CountryIsoAlpha3.CZE); + var rates = await provider.GetExchangeRates(currencies); + _output.WriteLine($"Successfully retrieved {rates.Count()} exchange rates:"); + foreach (var rate in rates) + { + _output.WriteLine(rate.ToString()); + } + } + catch (Exception e) + { + _output.WriteLine($"Could not retrieve exchange rates: '{e.Message}'"); + } + } +} diff --git a/jobs/Backend/Task/Currency.cs b/jobs/Backend/Task/Currency.cs index f375776f2..057eb8183 100644 --- a/jobs/Backend/Task/Currency.cs +++ b/jobs/Backend/Task/Currency.cs @@ -1,20 +1,19 @@ -namespace ExchangeRateUpdater +namespace ExchangeRateUpdater; + +public class Currency { - public class Currency + public Currency(string code) { - public Currency(string code) - { - Code = code; - } + Code = code; + } - /// - /// Three-letter ISO 4217 code of the currency. - /// - public string Code { get; } + /// + /// Three-letter ISO 4217 code of the currency. + /// + public string Code { get; } - public override string ToString() - { - return Code; - } + public override string ToString() + { + return Code; } } diff --git a/jobs/Backend/Task/ExchangeRate.cs b/jobs/Backend/Task/ExchangeRate.cs deleted file mode 100644 index 58c5bb10e..000000000 --- a/jobs/Backend/Task/ExchangeRate.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace ExchangeRateUpdater -{ - public class ExchangeRate - { - public ExchangeRate(Currency sourceCurrency, Currency targetCurrency, decimal value) - { - SourceCurrency = sourceCurrency; - TargetCurrency = targetCurrency; - Value = value; - } - - public Currency SourceCurrency { get; } - - public Currency TargetCurrency { get; } - - public decimal Value { get; } - - public override string ToString() - { - return $"{SourceCurrency}/{TargetCurrency}={Value}"; - } - } -} diff --git a/jobs/Backend/Task/ExchangeRateProvider.cs b/jobs/Backend/Task/ExchangeRateProvider.cs deleted file mode 100644 index 6f82a97fb..000000000 --- a/jobs/Backend/Task/ExchangeRateProvider.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Collections.Generic; -using System.Linq; - -namespace ExchangeRateUpdater -{ - public class ExchangeRateProvider - { - /// - /// Should return exchange rates among the specified currencies that are defined by the source. But only those defined - /// by the source, do not return calculated exchange rates. E.g. if the source contains "CZK/USD" but not "USD/CZK", - /// do not return exchange rate "USD/CZK" with value calculated as 1 / "CZK/USD". If the source does not provide - /// some of the currencies, ignore them. - /// - public IEnumerable GetExchangeRates(IEnumerable currencies) - { - return Enumerable.Empty(); - } - } -} diff --git a/jobs/Backend/Task/ExchangeRateUpdater.csproj b/jobs/Backend/Task/ExchangeRateUpdater.csproj index 2fc654a12..9e7f948f5 100644 --- a/jobs/Backend/Task/ExchangeRateUpdater.csproj +++ b/jobs/Backend/Task/ExchangeRateUpdater.csproj @@ -5,4 +5,10 @@ net6.0 + + + + + + \ No newline at end of file diff --git a/jobs/Backend/Task/Factories/ExchangeRateProviderFactory.cs b/jobs/Backend/Task/Factories/ExchangeRateProviderFactory.cs new file mode 100644 index 000000000..c0fd5242b --- /dev/null +++ b/jobs/Backend/Task/Factories/ExchangeRateProviderFactory.cs @@ -0,0 +1,27 @@ +using System; +using ExchangeRateUpdater.Interfaces; +using ExchangeRateUpdater.Models; +using ExchangeRateUpdater.Services; +using ExchangeRateUpdater.Services.Countries.CZE; +using Microsoft.Extensions.DependencyInjection; + +namespace ExchangeRateUpdater.Factories; + +public class ExchangeRateProviderFactory +{ + private readonly IServiceProvider _serviceProvider; + + public ExchangeRateProviderFactory(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public IExchangeRateProvider CreateProvider(CountryIsoAlpha3 country) + { + return country switch + { + CountryIsoAlpha3.CZE => _serviceProvider.GetRequiredService(), + _ => _serviceProvider.GetRequiredService() + }; + } +} diff --git a/jobs/Backend/Task/Interfaces/IExchangeRateProvider.cs b/jobs/Backend/Task/Interfaces/IExchangeRateProvider.cs new file mode 100644 index 000000000..9b285292b --- /dev/null +++ b/jobs/Backend/Task/Interfaces/IExchangeRateProvider.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using ExchangeRateUpdater.Models; + +namespace ExchangeRateUpdater.Interfaces; + +public interface IExchangeRateProvider +{ + Task> GetExchangeRates(IEnumerable currencies); +} diff --git a/jobs/Backend/Task/Models/Countries/CZE/CnbExchangeRate.cs b/jobs/Backend/Task/Models/Countries/CZE/CnbExchangeRate.cs new file mode 100644 index 000000000..c3916e962 --- /dev/null +++ b/jobs/Backend/Task/Models/Countries/CZE/CnbExchangeRate.cs @@ -0,0 +1,24 @@ +using System.Xml.Serialization; + +namespace ExchangeRateUpdater.Models.Countries.CZE; + +public class CnbExchangeRate +{ + [XmlAttribute("kod")] + public string Code { get; set; } + + [XmlAttribute("mena")] + public string CurrencyName { get; set; } + + [XmlAttribute("mnozstvi")] + public int Amount { get; set; } + + [XmlAttribute("kurz")] + public string RateRaw { get; set; } // Ej: "13,854" + + [XmlAttribute("zeme")] + public string Country { get; set; } + + [XmlIgnore] + public decimal Rate => decimal.Parse(RateRaw.Replace(',', '.'), System.Globalization.CultureInfo.InvariantCulture); +} diff --git a/jobs/Backend/Task/Models/Countries/CZE/CnbExchangeRateTable.cs b/jobs/Backend/Task/Models/Countries/CZE/CnbExchangeRateTable.cs new file mode 100644 index 000000000..579d4c578 --- /dev/null +++ b/jobs/Backend/Task/Models/Countries/CZE/CnbExchangeRateTable.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using System.Xml.Serialization; + +namespace ExchangeRateUpdater.Models.Countries.CZE; + +public class CnbExchangeRateTable +{ + [XmlAttribute("typ")] + public string Type { get; set; } + + [XmlElement("radek")] + public List Rates { get; set; } +} \ No newline at end of file diff --git a/jobs/Backend/Task/Models/Countries/CZE/CnbExchangeRatesResponse.cs b/jobs/Backend/Task/Models/Countries/CZE/CnbExchangeRatesResponse.cs new file mode 100644 index 000000000..ef40e9813 --- /dev/null +++ b/jobs/Backend/Task/Models/Countries/CZE/CnbExchangeRatesResponse.cs @@ -0,0 +1,19 @@ +using System.Xml.Serialization; + +namespace ExchangeRateUpdater.Models.Countries.CZE; + +[XmlRoot("kurzy")] +public class CnbExchangeRatesResponse +{ + [XmlAttribute("banka")] + public string Bank { get; set; } + + [XmlAttribute("datum")] + public string Date { get; set; } + + [XmlAttribute("poradi")] + public string Sequence { get; set; } + + [XmlElement("tabulka")] + public CnbExchangeRateTable Table { get; set; } +} diff --git a/jobs/Backend/Task/Models/CountryIsoAlpha3.cs b/jobs/Backend/Task/Models/CountryIsoAlpha3.cs new file mode 100644 index 000000000..ccffeeb6d --- /dev/null +++ b/jobs/Backend/Task/Models/CountryIsoAlpha3.cs @@ -0,0 +1,6 @@ +namespace ExchangeRateUpdater.Models; + +public enum CountryIsoAlpha3 +{ + CZE +} diff --git a/jobs/Backend/Task/Models/ExchangeRate.cs b/jobs/Backend/Task/Models/ExchangeRate.cs new file mode 100644 index 000000000..a415ffa1d --- /dev/null +++ b/jobs/Backend/Task/Models/ExchangeRate.cs @@ -0,0 +1,22 @@ +namespace ExchangeRateUpdater.Models; + +public class ExchangeRate +{ + public ExchangeRate(Currency sourceCurrency, Currency targetCurrency, decimal value) + { + SourceCurrency = sourceCurrency; + TargetCurrency = targetCurrency; + Value = value; + } + + public Currency SourceCurrency { get; } + + public Currency TargetCurrency { get; } + + public decimal Value { get; } + + public override string ToString() + { + return $"{SourceCurrency}/{TargetCurrency}={Value}"; + } +} diff --git a/jobs/Backend/Task/Program.cs b/jobs/Backend/Task/Program.cs index 379a69b1f..d36e29d0b 100644 --- a/jobs/Backend/Task/Program.cs +++ b/jobs/Backend/Task/Program.cs @@ -1,43 +1,21 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; -namespace ExchangeRateUpdater +namespace ExchangeRateUpdater; + +public static class Program { - public static class Program + public static async Task Main(string[] args) { - private static IEnumerable currencies = new[] - { - new Currency("USD"), - new Currency("EUR"), - new Currency("CZK"), - new Currency("JPY"), - new Currency("KES"), - new Currency("RUB"), - new Currency("THB"), - new Currency("TRY"), - new Currency("XYZ") - }; - - public static void Main(string[] args) - { - try - { - var provider = new ExchangeRateProvider(); - var rates = provider.GetExchangeRates(currencies); - - Console.WriteLine($"Successfully retrieved {rates.Count()} exchange rates:"); - foreach (var rate in rates) - { - Console.WriteLine(rate.ToString()); - } - } - catch (Exception e) + var host = Host.CreateDefaultBuilder(args) + .ConfigureServices((context, services) => { - Console.WriteLine($"Could not retrieve exchange rates: '{e.Message}'."); - } + Startup.ConfigureServices(services); + }) + .Build(); - Console.ReadLine(); - } + var app = host.Services.GetRequiredService(); + await app.Run(); } } diff --git a/jobs/Backend/Task/Services/Countries/CZE/CzeExchangeRateProvider.cs b/jobs/Backend/Task/Services/Countries/CZE/CzeExchangeRateProvider.cs new file mode 100644 index 000000000..fde8c8d51 --- /dev/null +++ b/jobs/Backend/Task/Services/Countries/CZE/CzeExchangeRateProvider.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using System.Xml; +using System.Xml.Serialization; +using ExchangeRateUpdater.Interfaces; +using ExchangeRateUpdater.Models; +using ExchangeRateUpdater.Models.Countries.CZE; + +namespace ExchangeRateUpdater.Services.Countries.CZE; + +public class CzeExchangeRateProvider : IExchangeRateProvider +{ + private readonly HttpClient _httpClient; + + public CzeExchangeRateProvider(HttpClient httpClient) + { + _httpClient = httpClient; + } + + public async Task> GetExchangeRates(IEnumerable currencies) + { + try + { + var response = await _httpClient.GetAsync("https://www.cnb.cz/cs/financni_trhy/devizovy_trh/kurzy_devizoveho_trhu/denni_kurz.xml"); + response.EnsureSuccessStatusCode(); + + using var stream = await response.Content.ReadAsStreamAsync(); + using var reader = XmlReader.Create(stream, new XmlReaderSettings + { + DtdProcessing = DtdProcessing.Parse + }); + + var serializer = new XmlSerializer(typeof(CnbExchangeRatesResponse)); + var result = (CnbExchangeRatesResponse)serializer.Deserialize(reader); + + + return MapToExchangeRates(result, currencies); + } + catch (System.Exception e) + { + + throw; + } + } + + private IEnumerable MapToExchangeRates( + CnbExchangeRatesResponse response, + IEnumerable requestedCurrencies) + { + const string baseCurrencyCode = "CZK"; + + var requestedCodes = requestedCurrencies.Select(c => c.Code).ToHashSet(StringComparer.OrdinalIgnoreCase); + + return response.Table.Rates + .Where(rate => requestedCodes.Contains(rate.Code)) + .Select(rate => + { + var source = new Currency(baseCurrencyCode); + var target = new Currency(rate.Code); + var value = rate.Rate / rate.Amount; + + return new ExchangeRate(source, target, value); + }); + } +} diff --git a/jobs/Backend/Task/Services/ExchangeRateProvider.cs b/jobs/Backend/Task/Services/ExchangeRateProvider.cs new file mode 100644 index 000000000..01f83c283 --- /dev/null +++ b/jobs/Backend/Task/Services/ExchangeRateProvider.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using ExchangeRateUpdater.Interfaces; +using ExchangeRateUpdater.Models; + +namespace ExchangeRateUpdater.Services; + +public class ExchangeRateProvider : IExchangeRateProvider +{ + public Task> GetExchangeRates(IEnumerable currencies) + { + return Task.FromResult(Enumerable.Empty()); + } +} diff --git a/jobs/Backend/Task/Startup.cs b/jobs/Backend/Task/Startup.cs new file mode 100644 index 000000000..4e8bebc04 --- /dev/null +++ b/jobs/Backend/Task/Startup.cs @@ -0,0 +1,27 @@ +using System; +using ExchangeRateUpdater.Factories; +using ExchangeRateUpdater.Interfaces; +using ExchangeRateUpdater.Services; +using ExchangeRateUpdater.Services.Countries.CZE; +using Microsoft.Extensions.DependencyInjection; + +namespace ExchangeRateUpdater; + +public static class Startup +{ + public static void ConfigureServices(IServiceCollection services) + { + services.AddTransient(); + services.AddSingleton(Console.Out); + services.AddExhangeProviders(); + } + + public static void AddExhangeProviders(this IServiceCollection services) + { + services.AddTransient(); + services.AddTransient(); + + services.AddTransient(); + services.AddHttpClient(); + } +} From ebd1e897fd2e33fef069d777c450f82ae9157210 Mon Sep 17 00:00:00 2001 From: jdiazbello Date: Sat, 2 Aug 2025 14:50:19 +0200 Subject: [PATCH 03/11] Add Cache and settings --- .../DesignTimeBuild/.dtbcache.v2 | Bin 312 -> 96445 bytes .../config/applicationhost.config | 1016 +++++++++++++++++ .../v17/DocumentLayout.backup.json | 230 ++-- .../v17/DocumentLayout.json | 162 ++- jobs/Backend/Task/App.cs | 26 +- jobs/Backend/Task/ExchangeRateUpdater.csproj | 27 +- jobs/Backend/Task/Interfaces/ICacheService.cs | 14 + jobs/Backend/Task/Models/Cache/CacheObject.cs | 14 + .../Task/Models/Cache/CacheSettings.cs | 14 + ...{CnbExchangeRate.cs => CzeExchangeRate.cs} | 2 +- ...geRateTable.cs => CzeExchangeRateTable.cs} | 4 +- ...esponse.cs => CzeExchangeRatesResponse.cs} | 4 +- .../Task/Models/Countries/CZE/CzeSettings.cs | 15 + jobs/Backend/Task/Program.cs | 19 +- .../Task/Properties/launchSettings.json | 10 + .../Task/Services/Cache/MemoryCacheService.cs | 32 + .../Task/Services/Cache/RedisCacheService.cs | 35 + .../Countries/CZE/CzeExchangeRateProvider.cs | 74 +- jobs/Backend/Task/Startup.cs | 33 +- jobs/Backend/Task/appsettings.dev.json | 9 + jobs/Backend/Task/appsettings.json | 20 + 21 files changed, 1526 insertions(+), 234 deletions(-) create mode 100644 jobs/Backend/Task/.vs/ExchangeRateUpdater/config/applicationhost.config create mode 100644 jobs/Backend/Task/Interfaces/ICacheService.cs create mode 100644 jobs/Backend/Task/Models/Cache/CacheObject.cs create mode 100644 jobs/Backend/Task/Models/Cache/CacheSettings.cs rename jobs/Backend/Task/Models/Countries/CZE/{CnbExchangeRate.cs => CzeExchangeRate.cs} (95%) rename jobs/Backend/Task/Models/Countries/CZE/{CnbExchangeRateTable.cs => CzeExchangeRateTable.cs} (71%) rename jobs/Backend/Task/Models/Countries/CZE/{CnbExchangeRatesResponse.cs => CzeExchangeRatesResponse.cs} (79%) create mode 100644 jobs/Backend/Task/Models/Countries/CZE/CzeSettings.cs create mode 100644 jobs/Backend/Task/Properties/launchSettings.json create mode 100644 jobs/Backend/Task/Services/Cache/MemoryCacheService.cs create mode 100644 jobs/Backend/Task/Services/Cache/RedisCacheService.cs create mode 100644 jobs/Backend/Task/appsettings.dev.json create mode 100644 jobs/Backend/Task/appsettings.json diff --git a/jobs/Backend/Task/.vs/ExchangeRateUpdater/DesignTimeBuild/.dtbcache.v2 b/jobs/Backend/Task/.vs/ExchangeRateUpdater/DesignTimeBuild/.dtbcache.v2 index 48b225e7c7f784d350cebd39cb44d94cb8f6b25f..a9e8fdb528d088cc108c25f12f02b690e0d04b57 100644 GIT binary patch literal 96445 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 delta 131 zcmXZOu@S;B3_wxaAwW&X3RF@e*`Z^C>|k5=9KOo}q@rXE8g>#J00+N$|M}`4&h5?W z=1a)FkWX9~poot`5YG~(h)j?N$m19$U)iHD2@MQPvF4g`iHJb!_1GbX+kLj_WX-Ry UyZ;|Ps#(;k(@r$bJ_2G#K)t^fc4 diff --git a/jobs/Backend/Task/.vs/ExchangeRateUpdater/config/applicationhost.config b/jobs/Backend/Task/.vs/ExchangeRateUpdater/config/applicationhost.config new file mode 100644 index 000000000..0d88f0db3 --- /dev/null +++ b/jobs/Backend/Task/.vs/ExchangeRateUpdater/config/applicationhost.config @@ -0,0 +1,1016 @@ + + + + + + + +
+
+
+
+
+
+
+
+ + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+
+ + +
+
+
+
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jobs/Backend/Task/.vs/ExchangeRateUpdater/v17/DocumentLayout.backup.json b/jobs/Backend/Task/.vs/ExchangeRateUpdater/v17/DocumentLayout.backup.json index 6599ef869..09a846570 100644 --- a/jobs/Backend/Task/.vs/ExchangeRateUpdater/v17/DocumentLayout.backup.json +++ b/jobs/Backend/Task/.vs/ExchangeRateUpdater/v17/DocumentLayout.backup.json @@ -2,37 +2,45 @@ "Version": 1, "WorkspaceRootPath": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\", "Documents": [ + { + "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\currency.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", + "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:currency.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + }, { "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\app.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:app.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" }, { - "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\startup.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", - "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:startup.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\services\\exchangerateprovider.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", + "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:services\\exchangerateprovider.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" }, { "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\services\\countries\\cze\\czeexchangerateprovider.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:services\\countries\\cze\\czeexchangerateprovider.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" }, { - "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\factories\\exchangerateproviderfactory.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", - "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:factories\\exchangerateproviderfactory.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\models\\exchangerate.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", + "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:models\\exchangerate.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" }, { - "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\services\\exchangerateprovider.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", - "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:services\\exchangerateprovider.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\models\\countryisoalpha3.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", + "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:models\\countryisoalpha3.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" }, { - "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\exchangerateupdater.csproj||{FA3CD31E-987B-443A-9B81-186104E8DAC1}|", - "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:exchangerateupdater.csproj||{FA3CD31E-987B-443A-9B81-186104E8DAC1}|" + "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\models\\countries\\cze\\czesettings.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", + "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:models\\countries\\cze\\czesettings.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" }, { - "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\interfaces\\iexchangerateprovider.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", - "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:interfaces\\iexchangerateprovider.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\models\\countries\\cze\\czeexchangeratetable.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", + "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:models\\countries\\cze\\czeexchangeratetable.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" }, { - "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\models\\countryisoalpha3.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", - "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:models\\countryisoalpha3.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\models\\countries\\cze\\czeexchangeratesresponse.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", + "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:models\\countries\\cze\\czeexchangeratesresponse.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + }, + { + "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\models\\countries\\cze\\czeexchangerate.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", + "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:models\\countries\\cze\\czeexchangerate.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" } ], "DocumentGroupContainers": [ @@ -42,73 +50,8 @@ "DocumentGroups": [ { "DockedWidth": 200, - "SelectedChildIndex": 1, + "SelectedChildIndex": 26, "Children": [ - { - "$type": "Document", - "DocumentIndex": 1, - "Title": "Startup.cs", - "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Startup.cs", - "RelativeDocumentMoniker": "Startup.cs", - "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Startup.cs*", - "RelativeToolTip": "Startup.cs*", - "ViewState": "AgIAAAAAAAAAAAAAAAAAABQAAAA9AAAAAAAAAA==", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", - "WhenOpened": "2025-08-02T10:20:27.19Z", - "EditorCaption": "" - }, - { - "$type": "Document", - "DocumentIndex": 0, - "Title": "App.cs", - "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\App.cs", - "RelativeDocumentMoniker": "App.cs", - "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\App.cs*", - "RelativeToolTip": "App.cs*", - "ViewState": "AgIAABAAAAAAAAAAAAAQwB4AAAAPAAAAAAAAAA==", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", - "WhenOpened": "2025-08-02T10:20:24.41Z", - "EditorCaption": "" - }, - { - "$type": "Document", - "DocumentIndex": 4, - "Title": "ExchangeRateProvider.cs", - "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Services\\ExchangeRateProvider.cs", - "RelativeDocumentMoniker": "Services\\ExchangeRateProvider.cs", - "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Services\\ExchangeRateProvider.cs", - "RelativeToolTip": "Services\\ExchangeRateProvider.cs", - "ViewState": "AgIAAAAAAAAAAAAAAAAAABAAAAAFAAAAAAAAAA==", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", - "WhenOpened": "2025-08-02T10:15:20.545Z", - "EditorCaption": "" - }, - { - "$type": "Document", - "DocumentIndex": 2, - "Title": "CzeExchangeRateProvider.cs", - "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Services\\Countries\\CZE\\CzeExchangeRateProvider.cs", - "RelativeDocumentMoniker": "Services\\Countries\\CZE\\CzeExchangeRateProvider.cs", - "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Services\\Countries\\CZE\\CzeExchangeRateProvider.cs", - "RelativeToolTip": "Services\\Countries\\CZE\\CzeExchangeRateProvider.cs", - "ViewState": "AgIAAAAAAAAAAAAAAAAqwA4AAAAoAAAAAAAAAA==", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", - "WhenOpened": "2025-08-02T10:16:01.31Z", - "EditorCaption": "" - }, - { - "$type": "Document", - "DocumentIndex": 5, - "Title": "ExchangeRateUpdater", - "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\ExchangeRateUpdater.csproj", - "RelativeDocumentMoniker": "ExchangeRateUpdater.csproj", - "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\ExchangeRateUpdater.csproj*", - "RelativeToolTip": "ExchangeRateUpdater.csproj*", - "ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000758|", - "WhenOpened": "2025-08-02T10:14:03.149Z", - "EditorCaption": "" - }, { "$type": "Bookmark", "Name": "ST:129:0:{116d2292-e37d-41cd-a077-ebacac4c8cc4}" @@ -213,43 +156,134 @@ "$type": "Bookmark", "Name": "ST:129:0:{13b12e3e-c1b4-4539-9371-4fe9a0d523fc}" }, + { + "$type": "Document", + "DocumentIndex": 0, + "Title": "Currency.cs", + "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Currency.cs", + "RelativeDocumentMoniker": "Currency.cs", + "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Currency.cs", + "RelativeToolTip": "Currency.cs", + "ViewState": "AgIAAAAAAAAAAAAAAAAAAA8AAAAFAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", + "WhenOpened": "2025-08-02T11:56:35.988Z", + "EditorCaption": "" + }, + { + "$type": "Document", + "DocumentIndex": 1, + "Title": "App.cs", + "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\App.cs", + "RelativeDocumentMoniker": "App.cs", + "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\App.cs", + "RelativeToolTip": "App.cs", + "ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", + "WhenOpened": "2025-08-02T11:56:34.333Z", + "EditorCaption": "" + }, + { + "$type": "Document", + "DocumentIndex": 2, + "Title": "ExchangeRateProvider.cs", + "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Services\\ExchangeRateProvider.cs", + "RelativeDocumentMoniker": "Services\\ExchangeRateProvider.cs", + "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Services\\ExchangeRateProvider.cs", + "RelativeToolTip": "Services\\ExchangeRateProvider.cs", + "ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", + "WhenOpened": "2025-08-02T11:56:32.884Z", + "EditorCaption": "" + }, { "$type": "Document", "DocumentIndex": 3, - "Title": "ExchangeRateProviderFactory.cs", - "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Factories\\ExchangeRateProviderFactory.cs", - "RelativeDocumentMoniker": "Factories\\ExchangeRateProviderFactory.cs", - "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Factories\\ExchangeRateProviderFactory.cs*", - "RelativeToolTip": "Factories\\ExchangeRateProviderFactory.cs*", - "ViewState": "AgIAAAAAAAAAAAAAAAAAABoAAABjAAAAAAAAAA==", + "Title": "CzeExchangeRateProvider.cs", + "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Services\\Countries\\CZE\\CzeExchangeRateProvider.cs", + "RelativeDocumentMoniker": "Services\\Countries\\CZE\\CzeExchangeRateProvider.cs", + "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Services\\Countries\\CZE\\CzeExchangeRateProvider.cs*", + "RelativeToolTip": "Services\\Countries\\CZE\\CzeExchangeRateProvider.cs*", + "ViewState": "AgIAAAAAAAAAAAAAAAAAABUAAAAAAAAAAAAAAA==", "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", - "WhenOpened": "2025-08-02T10:14:25.712Z", + "WhenOpened": "2025-08-02T11:56:25.321Z", "EditorCaption": "" }, { "$type": "Document", - "DocumentIndex": 6, - "Title": "IExchangeRateProvider.cs", - "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Interfaces\\IExchangeRateProvider.cs", - "RelativeDocumentMoniker": "Interfaces\\IExchangeRateProvider.cs", - "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Interfaces\\IExchangeRateProvider.cs", - "RelativeToolTip": "Interfaces\\IExchangeRateProvider.cs", - "ViewState": "AgIAAAAAAAAAAAAAAAAAAAYAAAAmAAAAAAAAAA==", + "DocumentIndex": 4, + "Title": "ExchangeRate.cs", + "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Models\\ExchangeRate.cs", + "RelativeDocumentMoniker": "Models\\ExchangeRate.cs", + "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Models\\ExchangeRate.cs*", + "RelativeToolTip": "Models\\ExchangeRate.cs*", + "ViewState": "AgIAAAAAAAAAAAAAAAAAAAoAAAAAAAAAAAAAAA==", "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", - "WhenOpened": "2025-08-02T10:12:15.386Z", + "WhenOpened": "2025-08-02T11:56:13.773Z", "EditorCaption": "" }, { "$type": "Document", - "DocumentIndex": 7, + "DocumentIndex": 5, "Title": "CountryIsoAlpha3.cs", "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Models\\CountryIsoAlpha3.cs", "RelativeDocumentMoniker": "Models\\CountryIsoAlpha3.cs", - "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Models\\CountryIsoAlpha3.cs", - "RelativeToolTip": "Models\\CountryIsoAlpha3.cs", - "ViewState": "AgIAAAAAAAAAAAAAAAAAAAoAAAAHAAAAAAAAAA==", + "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Models\\CountryIsoAlpha3.cs*", + "RelativeToolTip": "Models\\CountryIsoAlpha3.cs*", + "ViewState": "AgIAAAAAAAAAAAAAAAAAAAIAAAAGAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", + "WhenOpened": "2025-08-02T11:56:09.285Z", + "EditorCaption": "" + }, + { + "$type": "Document", + "DocumentIndex": 6, + "Title": "CzeSettings.cs", + "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Models\\Countries\\CZE\\CzeSettings.cs", + "RelativeDocumentMoniker": "Models\\Countries\\CZE\\CzeSettings.cs", + "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Models\\Countries\\CZE\\CzeSettings.cs*", + "RelativeToolTip": "Models\\Countries\\CZE\\CzeSettings.cs*", + "ViewState": "AgIAAAAAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", + "WhenOpened": "2025-08-02T11:55:54.978Z", + "EditorCaption": "" + }, + { + "$type": "Document", + "DocumentIndex": 7, + "Title": "CzeExchangeRateTable.cs", + "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Models\\Countries\\CZE\\CzeExchangeRateTable.cs", + "RelativeDocumentMoniker": "Models\\Countries\\CZE\\CzeExchangeRateTable.cs", + "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Models\\Countries\\CZE\\CzeExchangeRateTable.cs*", + "RelativeToolTip": "Models\\Countries\\CZE\\CzeExchangeRateTable.cs*", + "ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", + "WhenOpened": "2025-08-02T11:55:54.422Z", + "EditorCaption": "" + }, + { + "$type": "Document", + "DocumentIndex": 8, + "Title": "CzeExchangeRatesResponse.cs", + "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Models\\Countries\\CZE\\CzeExchangeRatesResponse.cs", + "RelativeDocumentMoniker": "Models\\Countries\\CZE\\CzeExchangeRatesResponse.cs", + "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Models\\Countries\\CZE\\CzeExchangeRatesResponse.cs*", + "RelativeToolTip": "Models\\Countries\\CZE\\CzeExchangeRatesResponse.cs*", + "ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", + "WhenOpened": "2025-08-02T11:55:53.905Z", + "EditorCaption": "" + }, + { + "$type": "Document", + "DocumentIndex": 9, + "Title": "CzeExchangeRate.cs", + "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Models\\Countries\\CZE\\CzeExchangeRate.cs", + "RelativeDocumentMoniker": "Models\\Countries\\CZE\\CzeExchangeRate.cs", + "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Models\\Countries\\CZE\\CzeExchangeRate.cs*", + "RelativeToolTip": "Models\\Countries\\CZE\\CzeExchangeRate.cs*", + "ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", - "WhenOpened": "2025-08-02T10:11:58.605Z", + "WhenOpened": "2025-08-02T11:55:50.507Z", "EditorCaption": "" } ] diff --git a/jobs/Backend/Task/.vs/ExchangeRateUpdater/v17/DocumentLayout.json b/jobs/Backend/Task/.vs/ExchangeRateUpdater/v17/DocumentLayout.json index 1b2054aaf..985f7c8d8 100644 --- a/jobs/Backend/Task/.vs/ExchangeRateUpdater/v17/DocumentLayout.json +++ b/jobs/Backend/Task/.vs/ExchangeRateUpdater/v17/DocumentLayout.json @@ -6,33 +6,21 @@ "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\services\\countries\\cze\\czeexchangerateprovider.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:services\\countries\\cze\\czeexchangerateprovider.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" }, - { - "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\services\\exchangerateprovider.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", - "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:services\\exchangerateprovider.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" - }, - { - "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\app.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", - "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:app.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" - }, { "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\startup.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:startup.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" }, { - "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\factories\\exchangerateproviderfactory.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", - "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:factories\\exchangerateproviderfactory.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" - }, - { - "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\exchangerateupdater.csproj||{FA3CD31E-987B-443A-9B81-186104E8DAC1}|", - "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:exchangerateupdater.csproj||{FA3CD31E-987B-443A-9B81-186104E8DAC1}|" + "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\services\\exchangerateprovider.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", + "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:services\\exchangerateprovider.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" }, { - "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\interfaces\\iexchangerateprovider.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", - "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:interfaces\\iexchangerateprovider.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\appsettings.json||{90A6B3A7-C1A3-4009-A288-E2FF89E96FA0}", + "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:appsettings.json||{90A6B3A7-C1A3-4009-A288-E2FF89E96FA0}" }, { - "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\models\\countryisoalpha3.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", - "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:models\\countryisoalpha3.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\models\\countries\\cze\\czeexchangerate.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", + "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:models\\countries\\cze\\czeexchangerate.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" } ], "DocumentGroupContainers": [ @@ -42,7 +30,7 @@ "DocumentGroups": [ { "DockedWidth": 200, - "SelectedChildIndex": 29, + "SelectedChildIndex": 27, "Children": [ { "$type": "Bookmark", @@ -150,106 +138,54 @@ }, { "$type": "Document", - "DocumentIndex": 3, - "Title": "Startup.cs", - "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Startup.cs", - "RelativeDocumentMoniker": "Startup.cs", - "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Startup.cs", - "RelativeToolTip": "Startup.cs", - "ViewState": "AgIAAAAAAAAAAAAAAAAAABQAAAA9AAAAAAAAAA==", + "DocumentIndex": 0, + "Title": "CzeExchangeRateProvider.cs", + "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Services\\Countries\\CZE\\CzeExchangeRateProvider.cs", + "RelativeDocumentMoniker": "Services\\Countries\\CZE\\CzeExchangeRateProvider.cs", + "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Services\\Countries\\CZE\\CzeExchangeRateProvider.cs", + "RelativeToolTip": "Services\\Countries\\CZE\\CzeExchangeRateProvider.cs", + "ViewState": "AgIAAAQAAAAAAAAAAAAAAEYAAAA6AAAAAAAAAA==", "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", - "WhenOpened": "2025-08-02T10:20:27.19Z", + "WhenOpened": "2025-08-02T12:03:42.017Z", "EditorCaption": "" }, { "$type": "Document", "DocumentIndex": 2, - "Title": "App.cs", - "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\App.cs", - "RelativeDocumentMoniker": "App.cs", - "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\App.cs", - "RelativeToolTip": "App.cs", - "ViewState": "AgIAAAAAAAAAAAAAAAAAAAYAAAAAAAAAAAAAAA==", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", - "WhenOpened": "2025-08-02T10:20:24.41Z", - "EditorCaption": "" - }, - { - "$type": "Document", - "DocumentIndex": 1, "Title": "ExchangeRateProvider.cs", "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Services\\ExchangeRateProvider.cs", "RelativeDocumentMoniker": "Services\\ExchangeRateProvider.cs", "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Services\\ExchangeRateProvider.cs", "RelativeToolTip": "Services\\ExchangeRateProvider.cs", - "ViewState": "AgIAAAAAAAAAAAAAAAAAAAEAAAACAAAAAAAAAA==", + "ViewState": "AgIAAAAAAAAAAAAAAAAAAAwAAABBAAAAAAAAAA==", "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", - "WhenOpened": "2025-08-02T10:15:20.545Z", + "WhenOpened": "2025-08-02T12:03:38.764Z", "EditorCaption": "" }, { "$type": "Document", - "DocumentIndex": 0, - "Title": "CzeExchangeRateProvider.cs", - "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Services\\Countries\\CZE\\CzeExchangeRateProvider.cs", - "RelativeDocumentMoniker": "Services\\Countries\\CZE\\CzeExchangeRateProvider.cs", - "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Services\\Countries\\CZE\\CzeExchangeRateProvider.cs", - "RelativeToolTip": "Services\\Countries\\CZE\\CzeExchangeRateProvider.cs", - "ViewState": "AgIAAAAAAAAAAAAAAAAqwAsAAAAGAAAAAAAAAA==", + "DocumentIndex": 1, + "Title": "Startup.cs", + "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Startup.cs", + "RelativeDocumentMoniker": "Startup.cs", + "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Startup.cs", + "RelativeToolTip": "Startup.cs", + "ViewState": "AgIAAAAAAAAAAAAAAAAAAAcAAAAvAAAAAAAAAA==", "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", - "WhenOpened": "2025-08-02T10:16:01.31Z", - "EditorCaption": "" - }, - { - "$type": "Document", - "DocumentIndex": 5, - "Title": "ExchangeRateUpdater", - "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\ExchangeRateUpdater.csproj", - "RelativeDocumentMoniker": "ExchangeRateUpdater.csproj", - "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\ExchangeRateUpdater.csproj", - "RelativeToolTip": "ExchangeRateUpdater.csproj", - "ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000758|", - "WhenOpened": "2025-08-02T10:14:03.149Z", + "WhenOpened": "2025-08-02T11:58:17.871Z", "EditorCaption": "" }, { "$type": "Document", "DocumentIndex": 4, - "Title": "ExchangeRateProviderFactory.cs", - "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Factories\\ExchangeRateProviderFactory.cs", - "RelativeDocumentMoniker": "Factories\\ExchangeRateProviderFactory.cs", - "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Factories\\ExchangeRateProviderFactory.cs", - "RelativeToolTip": "Factories\\ExchangeRateProviderFactory.cs", - "ViewState": "AgIAAAAAAAAAAAAAAAAAABYAAABfAAAAAAAAAA==", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", - "WhenOpened": "2025-08-02T10:14:25.712Z", - "EditorCaption": "" - }, - { - "$type": "Document", - "DocumentIndex": 6, - "Title": "IExchangeRateProvider.cs", - "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Interfaces\\IExchangeRateProvider.cs", - "RelativeDocumentMoniker": "Interfaces\\IExchangeRateProvider.cs", - "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Interfaces\\IExchangeRateProvider.cs", - "RelativeToolTip": "Interfaces\\IExchangeRateProvider.cs", - "ViewState": "AgIAAAAAAAAAAAAAAAAAAAYAAAAmAAAAAAAAAA==", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", - "WhenOpened": "2025-08-02T10:12:15.386Z", - "EditorCaption": "" - }, - { - "$type": "Document", - "DocumentIndex": 7, - "Title": "CountryIsoAlpha3.cs", - "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Models\\CountryIsoAlpha3.cs", - "RelativeDocumentMoniker": "Models\\CountryIsoAlpha3.cs", - "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Models\\CountryIsoAlpha3.cs", - "RelativeToolTip": "Models\\CountryIsoAlpha3.cs", - "ViewState": "AgIAAAAAAAAAAAAAAAAAAAQAAAAHAAAAAAAAAA==", + "Title": "CzeExchangeRate.cs", + "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Models\\Countries\\CZE\\CzeExchangeRate.cs", + "RelativeDocumentMoniker": "Models\\Countries\\CZE\\CzeExchangeRate.cs", + "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Models\\Countries\\CZE\\CzeExchangeRate.cs", + "RelativeToolTip": "Models\\Countries\\CZE\\CzeExchangeRate.cs", + "ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", - "WhenOpened": "2025-08-02T10:11:58.605Z", + "WhenOpened": "2025-08-02T11:56:45.274Z", "EditorCaption": "" } ] @@ -275,6 +211,40 @@ ] } ] + }, + { + "Orientation": 0, + "VerticalTabListWidth": 256, + "FloatingWindowState": { + "Id": "cb493138-76e1-44ef-8f8d-b28f8bf2dfa9", + "Display": 1, + "X": 775, + "Y": 303, + "Width": 1276, + "Height": 880, + "WindowState": 0 + }, + "DocumentGroups": [ + { + "DockedWidth": 200, + "SelectedChildIndex": 0, + "Children": [ + { + "$type": "Document", + "DocumentIndex": 3, + "Title": "appsettings.json", + "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\appsettings.json", + "RelativeDocumentMoniker": "appsettings.json", + "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\appsettings.json", + "RelativeToolTip": "appsettings.json", + "ViewState": "AgIAAAMAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.001642|", + "WhenOpened": "2025-08-02T11:56:53.158Z", + "EditorCaption": "" + } + ] + } + ] } ] } \ No newline at end of file diff --git a/jobs/Backend/Task/App.cs b/jobs/Backend/Task/App.cs index 03ba39496..bc6f6c7c5 100644 --- a/jobs/Backend/Task/App.cs +++ b/jobs/Backend/Task/App.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using ExchangeRateUpdater.Factories; using ExchangeRateUpdater.Models; +using Microsoft.Extensions.Logging; namespace ExchangeRateUpdater; @@ -12,24 +13,27 @@ public class App { private readonly TextWriter _output; private readonly ExchangeRateProviderFactory _factory; + private readonly ILogger _logger; private static readonly IEnumerable currencies = new[] { - new Currency("USD"), - new Currency("EUR"), - new Currency("CZK"), - new Currency("JPY"), - new Currency("KES"), - new Currency("RUB"), - new Currency("THB"), - new Currency("TRY"), - new Currency("XYZ") -}; + new Currency("USD"), + new Currency("EUR"), + new Currency("CZK"), + new Currency("JPY"), + new Currency("KES"), + new Currency("RUB"), + new Currency("THB"), + new Currency("TRY"), + new Currency("XYZ") + }; public App( + ILogger logger, TextWriter output, ExchangeRateProviderFactory factory) { + _logger = logger; _output = output; _factory = factory; } @@ -38,6 +42,7 @@ public async Task Run() { try { + _logger.LogDebug("App executing"); var provider = _factory.CreateProvider(CountryIsoAlpha3.CZE); var rates = await provider.GetExchangeRates(currencies); _output.WriteLine($"Successfully retrieved {rates.Count()} exchange rates:"); @@ -45,6 +50,7 @@ public async Task Run() { _output.WriteLine(rate.ToString()); } + _logger.LogDebug("App finalize."); } catch (Exception e) { diff --git a/jobs/Backend/Task/ExchangeRateUpdater.csproj b/jobs/Backend/Task/ExchangeRateUpdater.csproj index 9e7f948f5..b190a7409 100644 --- a/jobs/Backend/Task/ExchangeRateUpdater.csproj +++ b/jobs/Backend/Task/ExchangeRateUpdater.csproj @@ -6,9 +6,30 @@ - - - + + + + + + + Always + + + Always + + + + + + + + + + + + + + \ No newline at end of file diff --git a/jobs/Backend/Task/Interfaces/ICacheService.cs b/jobs/Backend/Task/Interfaces/ICacheService.cs new file mode 100644 index 000000000..6e70c6c5e --- /dev/null +++ b/jobs/Backend/Task/Interfaces/ICacheService.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ExchangeRateUpdater.Interfaces +{ + public interface ICacheService + { + Task GetAsync(string key); + Task SetAsync(string key, T value, TimeSpan ttl); + } +} diff --git a/jobs/Backend/Task/Models/Cache/CacheObject.cs b/jobs/Backend/Task/Models/Cache/CacheObject.cs new file mode 100644 index 000000000..aed513bec --- /dev/null +++ b/jobs/Backend/Task/Models/Cache/CacheObject.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ExchangeRateUpdater.Models.Cache +{ + public class CacheObject + { + public T Data { get; set; } + public DateTimeOffset DataExtractionTimeUTC { get; set; } + } +} diff --git a/jobs/Backend/Task/Models/Cache/CacheSettings.cs b/jobs/Backend/Task/Models/Cache/CacheSettings.cs new file mode 100644 index 000000000..881d0cfa3 --- /dev/null +++ b/jobs/Backend/Task/Models/Cache/CacheSettings.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ExchangeRateUpdater.Models.Cache +{ + public class CacheSettings + { + public string Provider { get; set; } = "Memory"; // or "Redis" + public string RedisConfiguration { get; set; } = string.Empty; + } +} diff --git a/jobs/Backend/Task/Models/Countries/CZE/CnbExchangeRate.cs b/jobs/Backend/Task/Models/Countries/CZE/CzeExchangeRate.cs similarity index 95% rename from jobs/Backend/Task/Models/Countries/CZE/CnbExchangeRate.cs rename to jobs/Backend/Task/Models/Countries/CZE/CzeExchangeRate.cs index c3916e962..63d4824e4 100644 --- a/jobs/Backend/Task/Models/Countries/CZE/CnbExchangeRate.cs +++ b/jobs/Backend/Task/Models/Countries/CZE/CzeExchangeRate.cs @@ -2,7 +2,7 @@ namespace ExchangeRateUpdater.Models.Countries.CZE; -public class CnbExchangeRate +public class CzeExchangeRate { [XmlAttribute("kod")] public string Code { get; set; } diff --git a/jobs/Backend/Task/Models/Countries/CZE/CnbExchangeRateTable.cs b/jobs/Backend/Task/Models/Countries/CZE/CzeExchangeRateTable.cs similarity index 71% rename from jobs/Backend/Task/Models/Countries/CZE/CnbExchangeRateTable.cs rename to jobs/Backend/Task/Models/Countries/CZE/CzeExchangeRateTable.cs index 579d4c578..610c2c030 100644 --- a/jobs/Backend/Task/Models/Countries/CZE/CnbExchangeRateTable.cs +++ b/jobs/Backend/Task/Models/Countries/CZE/CzeExchangeRateTable.cs @@ -3,11 +3,11 @@ namespace ExchangeRateUpdater.Models.Countries.CZE; -public class CnbExchangeRateTable +public class CzeExchangeRateTable { [XmlAttribute("typ")] public string Type { get; set; } [XmlElement("radek")] - public List Rates { get; set; } + public List Rates { get; set; } } \ No newline at end of file diff --git a/jobs/Backend/Task/Models/Countries/CZE/CnbExchangeRatesResponse.cs b/jobs/Backend/Task/Models/Countries/CZE/CzeExchangeRatesResponse.cs similarity index 79% rename from jobs/Backend/Task/Models/Countries/CZE/CnbExchangeRatesResponse.cs rename to jobs/Backend/Task/Models/Countries/CZE/CzeExchangeRatesResponse.cs index ef40e9813..bc40e4e9f 100644 --- a/jobs/Backend/Task/Models/Countries/CZE/CnbExchangeRatesResponse.cs +++ b/jobs/Backend/Task/Models/Countries/CZE/CzeExchangeRatesResponse.cs @@ -3,7 +3,7 @@ namespace ExchangeRateUpdater.Models.Countries.CZE; [XmlRoot("kurzy")] -public class CnbExchangeRatesResponse +public class CzeExchangeRatesResponse { [XmlAttribute("banka")] public string Bank { get; set; } @@ -15,5 +15,5 @@ public class CnbExchangeRatesResponse public string Sequence { get; set; } [XmlElement("tabulka")] - public CnbExchangeRateTable Table { get; set; } + public CzeExchangeRateTable Table { get; set; } } diff --git a/jobs/Backend/Task/Models/Countries/CZE/CzeSettings.cs b/jobs/Backend/Task/Models/Countries/CZE/CzeSettings.cs new file mode 100644 index 000000000..e917b0494 --- /dev/null +++ b/jobs/Backend/Task/Models/Countries/CZE/CzeSettings.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ExchangeRateUpdater.Models.Countries.CZE +{ + public class CzeSettings + { + public string BaseUrl { get; set; } = string.Empty; + public int TtlInSeconds { get; set; } = 0; + public string UpdateHourInLocalTime { get; set; } = "00:00:00"; + } +} diff --git a/jobs/Backend/Task/Program.cs b/jobs/Backend/Task/Program.cs index d36e29d0b..11230e76b 100644 --- a/jobs/Backend/Task/Program.cs +++ b/jobs/Backend/Task/Program.cs @@ -1,4 +1,6 @@ using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -9,9 +11,24 @@ public static class Program public static async Task Main(string[] args) { var host = Host.CreateDefaultBuilder(args) + .ConfigureAppConfiguration((hostingContext, config) => + { + var env = hostingContext.HostingEnvironment; + + config.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); + + config.AddEnvironmentVariables(); + }) + .ConfigureLogging((context, logging) => + { + logging.ClearProviders(); + logging.AddConfiguration(context.Configuration.GetSection("Logging")); + logging.AddConsole(); + }) .ConfigureServices((context, services) => { - Startup.ConfigureServices(services); + Startup.ConfigureServices(services, context.Configuration); }) .Build(); diff --git a/jobs/Backend/Task/Properties/launchSettings.json b/jobs/Backend/Task/Properties/launchSettings.json new file mode 100644 index 000000000..364905a25 --- /dev/null +++ b/jobs/Backend/Task/Properties/launchSettings.json @@ -0,0 +1,10 @@ +{ + "profiles": { + "MyConsoleApp": { + "commandName": "Project", + "environmentVariables": { + "DOTNET_ENVIRONMENT": "dev" + } + } + } +} \ No newline at end of file diff --git a/jobs/Backend/Task/Services/Cache/MemoryCacheService.cs b/jobs/Backend/Task/Services/Cache/MemoryCacheService.cs new file mode 100644 index 000000000..3970991b4 --- /dev/null +++ b/jobs/Backend/Task/Services/Cache/MemoryCacheService.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using ExchangeRateUpdater.Interfaces; +using Microsoft.Extensions.Caching.Memory; + +namespace ExchangeRateUpdater.Services.Cache +{ + public class MemoryCacheService : ICacheService + { + private readonly IMemoryCache _memoryCache; + + public MemoryCacheService(IMemoryCache memoryCache) + { + _memoryCache = memoryCache; + } + + public Task GetAsync(string key) + { + _memoryCache.TryGetValue(key, out T value); + return Task.FromResult(value); + } + + public Task SetAsync(string key, T value, TimeSpan ttl) + { + _memoryCache.Set(key, value, ttl); + return Task.CompletedTask; + } + } +} diff --git a/jobs/Backend/Task/Services/Cache/RedisCacheService.cs b/jobs/Backend/Task/Services/Cache/RedisCacheService.cs new file mode 100644 index 000000000..b7c5ee2e1 --- /dev/null +++ b/jobs/Backend/Task/Services/Cache/RedisCacheService.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using ExchangeRateUpdater.Interfaces; +using Microsoft.Extensions.Caching.Distributed; + +namespace ExchangeRateUpdater.Services.Cache +{ + public class RedisCacheService : ICacheService + { + private readonly IDistributedCache _distributedCache; + + public RedisCacheService(IDistributedCache distributedCache) + { + _distributedCache = distributedCache; + } + + public async Task GetAsync(string key) + { + var json = await _distributedCache.GetStringAsync(key); + return json is null ? default : JsonSerializer.Deserialize(json); + } + + public async Task SetAsync(string key, T value, TimeSpan ttl) + { + var json = JsonSerializer.Serialize(value); + await _distributedCache.SetStringAsync(key, json, new DistributedCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = ttl + }); + } + } diff --git a/jobs/Backend/Task/Services/Countries/CZE/CzeExchangeRateProvider.cs b/jobs/Backend/Task/Services/Countries/CZE/CzeExchangeRateProvider.cs index fde8c8d51..a3313e9f9 100644 --- a/jobs/Backend/Task/Services/Countries/CZE/CzeExchangeRateProvider.cs +++ b/jobs/Backend/Task/Services/Countries/CZE/CzeExchangeRateProvider.cs @@ -1,5 +1,7 @@ using System; +using System.Collections; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Net.Http; using System.Threading.Tasks; @@ -7,62 +9,98 @@ using System.Xml.Serialization; using ExchangeRateUpdater.Interfaces; using ExchangeRateUpdater.Models; +using ExchangeRateUpdater.Models.Cache; using ExchangeRateUpdater.Models.Countries.CZE; +using Microsoft.Extensions.Options; namespace ExchangeRateUpdater.Services.Countries.CZE; public class CzeExchangeRateProvider : IExchangeRateProvider { + private const string BaseCurrencyCode = "CZK"; + private const string CacheKey = "CZE_data"; + private const string TimeZone = "Central European Standard Time"; private readonly HttpClient _httpClient; + public readonly CzeSettings _settings; + private readonly ICacheService _cache; - public CzeExchangeRateProvider(HttpClient httpClient) + public CzeExchangeRateProvider( + HttpClient httpClient, + IOptionsSnapshot options, + ICacheService cache) { _httpClient = httpClient; + _settings = options.Value; + _cache = cache; } public async Task> GetExchangeRates(IEnumerable currencies) { - try + if (string.IsNullOrWhiteSpace(_settings.BaseUrl)) { - var response = await _httpClient.GetAsync("https://www.cnb.cz/cs/financni_trhy/devizovy_trh/kurzy_devizoveho_trhu/denni_kurz.xml"); - response.EnsureSuccessStatusCode(); + return Enumerable.Empty(); + } - using var stream = await response.Content.ReadAsStreamAsync(); - using var reader = XmlReader.Create(stream, new XmlReaderSettings + var existing = await _cache.GetAsync>(CacheKey); + if (existing != null) + { + var updateTime = GetUpdateHourInUTC(); + if (existing.DataExtractionTimeUTC > updateTime) { - DtdProcessing = DtdProcessing.Parse - }); + return MapToExchangeRates(existing.Data, currencies); + } + } - var serializer = new XmlSerializer(typeof(CnbExchangeRatesResponse)); - var result = (CnbExchangeRatesResponse)serializer.Deserialize(reader); + var response = await _httpClient.GetAsync(_settings.BaseUrl); + response.EnsureSuccessStatusCode(); + using var stream = await response.Content.ReadAsStreamAsync(); + using var reader = XmlReader.Create(stream, new XmlReaderSettings + { + DtdProcessing = DtdProcessing.Parse + }); + + var serializer = new XmlSerializer(typeof(CzeExchangeRatesResponse)); + var result = (CzeExchangeRatesResponse)serializer.Deserialize(reader); + if (result is not null && result.Table is not null && result.Table.Rates.Any()) + { + var cacheObject = new CacheObject + { + Data = result, + DataExtractionTimeUTC = DateTimeOffset.UtcNow, + }; + await _cache.SetAsync(CacheKey, cacheObject, TimeSpan.FromSeconds(_settings.TtlInSeconds)); return MapToExchangeRates(result, currencies); } - catch (System.Exception e) - { - throw; - } + return Enumerable.Empty(); } private IEnumerable MapToExchangeRates( - CnbExchangeRatesResponse response, + CzeExchangeRatesResponse response, IEnumerable requestedCurrencies) { - const string baseCurrencyCode = "CZK"; - var requestedCodes = requestedCurrencies.Select(c => c.Code).ToHashSet(StringComparer.OrdinalIgnoreCase); return response.Table.Rates .Where(rate => requestedCodes.Contains(rate.Code)) .Select(rate => { - var source = new Currency(baseCurrencyCode); + var source = new Currency(BaseCurrencyCode); var target = new Currency(rate.Code); var value = rate.Rate / rate.Amount; return new ExchangeRate(source, target, value); }); } + + private DateTimeOffset GetUpdateHourInUTC() + { + TimeSpan time = TimeSpan.ParseExact(_settings.UpdateHourInLocalTime, "c", CultureInfo.InvariantCulture); + DateTime localDateTime = DateTime.Today.Add(time); + TimeZoneInfo czechTimeZone = TimeZoneInfo.FindSystemTimeZoneById(TimeZone); + DateTimeOffset czechDateTimeOffset = new(localDateTime, czechTimeZone.GetUtcOffset(localDateTime)); + return czechDateTimeOffset.ToUniversalTime(); + } } diff --git a/jobs/Backend/Task/Startup.cs b/jobs/Backend/Task/Startup.cs index 4e8bebc04..ad6d90877 100644 --- a/jobs/Backend/Task/Startup.cs +++ b/jobs/Backend/Task/Startup.cs @@ -1,27 +1,54 @@ using System; using ExchangeRateUpdater.Factories; using ExchangeRateUpdater.Interfaces; +using ExchangeRateUpdater.Models.Cache; +using ExchangeRateUpdater.Models.Countries.CZE; using ExchangeRateUpdater.Services; +using ExchangeRateUpdater.Services.Cache; using ExchangeRateUpdater.Services.Countries.CZE; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace ExchangeRateUpdater; public static class Startup { - public static void ConfigureServices(IServiceCollection services) + public static void ConfigureServices(IServiceCollection services, IConfiguration configuration) { services.AddTransient(); services.AddSingleton(Console.Out); - services.AddExhangeProviders(); + services.AddExhangeProviders(configuration); } - public static void AddExhangeProviders(this IServiceCollection services) + public static void AddExhangeProviders(this IServiceCollection services, IConfiguration configuration) { services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddHttpClient(); + services.Configure(configuration.GetSection("ExchangeProviders:CZE")); + } + + public static void AddCache(this IServiceCollection services, IConfiguration configuration) + { + services.Configure(configuration.GetSection("CacheSettings")); + services.AddMemoryCache(); + services.AddStackExchangeRedisCache(options => + { + var redisConfig = configuration.GetSection("CacheSettings:RedisConfiguration").Value; + options.Configuration = redisConfig; + }); + + var provider = configuration.GetValue("CacheSettings:Provider"); + + if (string.Equals(provider, "Redis", StringComparison.OrdinalIgnoreCase)) + { + services.AddSingleton(); + } + else + { + services.AddSingleton(); + } } } diff --git a/jobs/Backend/Task/appsettings.dev.json b/jobs/Backend/Task/appsettings.dev.json new file mode 100644 index 000000000..5c38ab469 --- /dev/null +++ b/jobs/Backend/Task/appsettings.dev.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "Microsoft": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} \ No newline at end of file diff --git a/jobs/Backend/Task/appsettings.json b/jobs/Backend/Task/appsettings.json new file mode 100644 index 000000000..be28f64ee --- /dev/null +++ b/jobs/Backend/Task/appsettings.json @@ -0,0 +1,20 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "ExchangeProviders": { + "CZE": { + "BaseUrl": "https://www.cnb.cz/cs/financni_trhy/devizovy_trh/kurzy_devizoveho_trhu/denni_kurz.xml", + "TtlInSeconds": 86400, //24 hours + "UpdateHourInLocalTime": "14:30:00" + } + }, + "CacheSettings": { + "Provider": "Memory", // or "Redis" + "RedisConfiguration": "localhost:6379" + } +} \ No newline at end of file From 7c15cbfe75ed451ba9663b2b3a620249c9f1db01 Mon Sep 17 00:00:00 2001 From: jdiazbello Date: Sat, 2 Aug 2025 17:54:35 +0200 Subject: [PATCH 04/11] Update gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 2650ad75f..f97af7241 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,6 @@ npm-debug.log *.db-shm *.db-wal *.vsidx +*.v2 +/jobs/Backend/Task/FileCache +*.bin From 066aa01acb1401151620a6182c1d30650ae281cf Mon Sep 17 00:00:00 2001 From: jdiazbello Date: Sat, 2 Aug 2025 17:56:09 +0200 Subject: [PATCH 05/11] Add FileCache and DateTiemProvider --- jobs/Backend/Task/App.cs | 9 +- jobs/Backend/Task/ExchangeRateUpdater.csproj | 4 + jobs/Backend/Task/Interfaces/ICacheService.cs | 14 +- .../Task/Interfaces/IDateTimeProvider.cs | 8 ++ jobs/Backend/Task/Models/Cache/CacheObject.cs | 15 +-- .../Task/Models/Cache/CacheSettings.cs | 15 +-- .../Task/Models/Countries/CZE/CzeSettings.cs | 17 +-- .../Task/Services/Cache/FileCacheService.cs | 103 +++++++++++++++ .../Task/Services/Cache/MemoryCacheService.cs | 32 ----- .../Task/Services/Cache/RedisCacheService.cs | 43 +++---- .../Countries/CZE/CzeExchangeRateProvider.cs | 120 ++++++++++++++---- .../Backend/Task/Services/DateTimeProvider.cs | 9 ++ jobs/Backend/Task/Startup.cs | 36 ++++-- jobs/Backend/Task/appsettings.dev.json | 6 + jobs/Backend/Task/appsettings.json | 2 +- 15 files changed, 291 insertions(+), 142 deletions(-) create mode 100644 jobs/Backend/Task/Interfaces/IDateTimeProvider.cs create mode 100644 jobs/Backend/Task/Services/Cache/FileCacheService.cs delete mode 100644 jobs/Backend/Task/Services/Cache/MemoryCacheService.cs create mode 100644 jobs/Backend/Task/Services/DateTimeProvider.cs diff --git a/jobs/Backend/Task/App.cs b/jobs/Backend/Task/App.cs index bc6f6c7c5..50ee75dee 100644 --- a/jobs/Backend/Task/App.cs +++ b/jobs/Backend/Task/App.cs @@ -42,18 +42,21 @@ public async Task Run() { try { - _logger.LogDebug("App executing"); + _logger.LogInformation("Application started execution."); var provider = _factory.CreateProvider(CountryIsoAlpha3.CZE); var rates = await provider.GetExchangeRates(currencies); - _output.WriteLine($"Successfully retrieved {rates.Count()} exchange rates:"); + var count = rates.Count(); + _logger.LogInformation("Successfully retrieved {Count} exchange rates.", count); + _output.WriteLine($"Successfully retrieved {count} exchange rates:"); foreach (var rate in rates) { _output.WriteLine(rate.ToString()); } - _logger.LogDebug("App finalize."); + _logger.LogInformation("Application execution finalized successfully."); } catch (Exception e) { + _logger.LogError(e, "An error occurred while retrieving exchange rates."); _output.WriteLine($"Could not retrieve exchange rates: '{e.Message}'"); } } diff --git a/jobs/Backend/Task/ExchangeRateUpdater.csproj b/jobs/Backend/Task/ExchangeRateUpdater.csproj index b190a7409..74d2e3fed 100644 --- a/jobs/Backend/Task/ExchangeRateUpdater.csproj +++ b/jobs/Backend/Task/ExchangeRateUpdater.csproj @@ -32,4 +32,8 @@ + + + + \ No newline at end of file diff --git a/jobs/Backend/Task/Interfaces/ICacheService.cs b/jobs/Backend/Task/Interfaces/ICacheService.cs index 6e70c6c5e..784441cdb 100644 --- a/jobs/Backend/Task/Interfaces/ICacheService.cs +++ b/jobs/Backend/Task/Interfaces/ICacheService.cs @@ -1,14 +1,10 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Threading.Tasks; -namespace ExchangeRateUpdater.Interfaces +namespace ExchangeRateUpdater.Interfaces; + +public interface ICacheService { - public interface ICacheService - { - Task GetAsync(string key); - Task SetAsync(string key, T value, TimeSpan ttl); - } + Task GetAsync(string key); + Task SetAsync(string key, T value, TimeSpan ttl); } diff --git a/jobs/Backend/Task/Interfaces/IDateTimeProvider.cs b/jobs/Backend/Task/Interfaces/IDateTimeProvider.cs new file mode 100644 index 000000000..5699c99bb --- /dev/null +++ b/jobs/Backend/Task/Interfaces/IDateTimeProvider.cs @@ -0,0 +1,8 @@ +using System; + +namespace ExchangeRateUpdater.Interfaces; + +public interface IDateTimeProvider +{ + DateTimeOffset UtcNow { get; } +} diff --git a/jobs/Backend/Task/Models/Cache/CacheObject.cs b/jobs/Backend/Task/Models/Cache/CacheObject.cs index aed513bec..9377ff848 100644 --- a/jobs/Backend/Task/Models/Cache/CacheObject.cs +++ b/jobs/Backend/Task/Models/Cache/CacheObject.cs @@ -1,14 +1,9 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -namespace ExchangeRateUpdater.Models.Cache +namespace ExchangeRateUpdater.Models.Cache; + +public class CacheObject { - public class CacheObject - { - public T Data { get; set; } - public DateTimeOffset DataExtractionTimeUTC { get; set; } - } + public T Data { get; set; } + public DateTimeOffset DataExtractionTimeUTC { get; set; } } diff --git a/jobs/Backend/Task/Models/Cache/CacheSettings.cs b/jobs/Backend/Task/Models/Cache/CacheSettings.cs index 881d0cfa3..f80352d90 100644 --- a/jobs/Backend/Task/Models/Cache/CacheSettings.cs +++ b/jobs/Backend/Task/Models/Cache/CacheSettings.cs @@ -1,14 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +namespace ExchangeRateUpdater.Models.Cache; -namespace ExchangeRateUpdater.Models.Cache +public class CacheSettings { - public class CacheSettings - { - public string Provider { get; set; } = "Memory"; // or "Redis" - public string RedisConfiguration { get; set; } = string.Empty; - } + public string Provider { get; set; } = "Memory"; // or "Redis" + public string RedisConfiguration { get; set; } = string.Empty; } diff --git a/jobs/Backend/Task/Models/Countries/CZE/CzeSettings.cs b/jobs/Backend/Task/Models/Countries/CZE/CzeSettings.cs index e917b0494..31939fa66 100644 --- a/jobs/Backend/Task/Models/Countries/CZE/CzeSettings.cs +++ b/jobs/Backend/Task/Models/Countries/CZE/CzeSettings.cs @@ -1,15 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +namespace ExchangeRateUpdater.Models.Countries.CZE; -namespace ExchangeRateUpdater.Models.Countries.CZE +public class CzeSettings { - public class CzeSettings - { - public string BaseUrl { get; set; } = string.Empty; - public int TtlInSeconds { get; set; } = 0; - public string UpdateHourInLocalTime { get; set; } = "00:00:00"; - } + public string BaseUrl { get; set; } = string.Empty; + public int TtlInSeconds { get; set; } = 0; + public string UpdateHourInLocalTime { get; set; } = "00:00:00"; } diff --git a/jobs/Backend/Task/Services/Cache/FileCacheService.cs b/jobs/Backend/Task/Services/Cache/FileCacheService.cs new file mode 100644 index 000000000..747ea8711 --- /dev/null +++ b/jobs/Backend/Task/Services/Cache/FileCacheService.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using ExchangeRateUpdater.Interfaces; +using System.Text.Json; + +namespace ExchangeRateUpdater.Services.Cache; + +public class FileCacheService : ICacheService +{ + private readonly string _filePath; + private readonly object _lock = new(); + private readonly Dictionary _cache; + private readonly JsonSerializerOptions _jsonOptions = new() + { + PropertyNameCaseInsensitive = true, + WriteIndented = true + }; + + public FileCacheService(string filePath) + { + _filePath = filePath; + Directory.CreateDirectory(Path.GetDirectoryName(_filePath)); + if (File.Exists(_filePath)) + { + var json = File.ReadAllText(_filePath); + _cache = JsonSerializer.Deserialize>(json, _jsonOptions) + ?? new Dictionary(); + } + else + { + _cache = new Dictionary(); + } + + CleanupExpiredEntries(); + } + + public Task GetAsync(string key) + { + lock (_lock) + { + if (_cache.TryGetValue(key, out var entry)) + { + if (entry.ExpiresAt == null || entry.ExpiresAt > DateTime.UtcNow) + { + return Task.FromResult(JsonSerializer.Deserialize(entry.JsonValue, _jsonOptions)); + } + else + { + _cache.Remove(key); + Save(); + } + } + } + + return Task.FromResult(default); + } + + public Task SetAsync(string key, T value, TimeSpan ttl) + { + var entry = new CacheEntry + { + JsonValue = JsonSerializer.Serialize(value, _jsonOptions), + ExpiresAt = DateTime.UtcNow.Add(ttl) + }; + + lock (_lock) + { + _cache[key] = entry; + Save(); + } + + return Task.CompletedTask; + } + + private void Save() + { + var json = JsonSerializer.Serialize(_cache, _jsonOptions); + File.WriteAllText(_filePath, json); + } + + private void CleanupExpiredEntries() + { + var expiredKeys = _cache + .Where(kv => kv.Value.ExpiresAt != null && kv.Value.ExpiresAt <= DateTime.UtcNow) + .Select(kv => kv.Key) + .ToList(); + + foreach (var key in expiredKeys) + _cache.Remove(key); + + if (expiredKeys.Any()) + Save(); + } + + private class CacheEntry + { + public string JsonValue { get; set; } = string.Empty; + public DateTime? ExpiresAt { get; set; } + } +} diff --git a/jobs/Backend/Task/Services/Cache/MemoryCacheService.cs b/jobs/Backend/Task/Services/Cache/MemoryCacheService.cs deleted file mode 100644 index 3970991b4..000000000 --- a/jobs/Backend/Task/Services/Cache/MemoryCacheService.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using ExchangeRateUpdater.Interfaces; -using Microsoft.Extensions.Caching.Memory; - -namespace ExchangeRateUpdater.Services.Cache -{ - public class MemoryCacheService : ICacheService - { - private readonly IMemoryCache _memoryCache; - - public MemoryCacheService(IMemoryCache memoryCache) - { - _memoryCache = memoryCache; - } - - public Task GetAsync(string key) - { - _memoryCache.TryGetValue(key, out T value); - return Task.FromResult(value); - } - - public Task SetAsync(string key, T value, TimeSpan ttl) - { - _memoryCache.Set(key, value, ttl); - return Task.CompletedTask; - } - } -} diff --git a/jobs/Backend/Task/Services/Cache/RedisCacheService.cs b/jobs/Backend/Task/Services/Cache/RedisCacheService.cs index b7c5ee2e1..89bbd9da2 100644 --- a/jobs/Backend/Task/Services/Cache/RedisCacheService.cs +++ b/jobs/Backend/Task/Services/Cache/RedisCacheService.cs @@ -1,35 +1,32 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Text.Json; using System.Threading.Tasks; using ExchangeRateUpdater.Interfaces; using Microsoft.Extensions.Caching.Distributed; -namespace ExchangeRateUpdater.Services.Cache +namespace ExchangeRateUpdater.Services.Cache; + +public class RedisCacheService : ICacheService { - public class RedisCacheService : ICacheService - { - private readonly IDistributedCache _distributedCache; + private readonly IDistributedCache _distributedCache; - public RedisCacheService(IDistributedCache distributedCache) - { - _distributedCache = distributedCache; - } + public RedisCacheService(IDistributedCache distributedCache) + { + _distributedCache = distributedCache; + } - public async Task GetAsync(string key) - { - var json = await _distributedCache.GetStringAsync(key); - return json is null ? default : JsonSerializer.Deserialize(json); - } + public async Task GetAsync(string key) + { + var json = await _distributedCache.GetStringAsync(key); + return json is null ? default : JsonSerializer.Deserialize(json); + } - public async Task SetAsync(string key, T value, TimeSpan ttl) + public async Task SetAsync(string key, T value, TimeSpan ttl) + { + var json = JsonSerializer.Serialize(value); + await _distributedCache.SetStringAsync(key, json, new DistributedCacheEntryOptions { - var json = JsonSerializer.Serialize(value); - await _distributedCache.SetStringAsync(key, json, new DistributedCacheEntryOptions - { - AbsoluteExpirationRelativeToNow = ttl - }); - } + AbsoluteExpirationRelativeToNow = ttl + }); } +} \ No newline at end of file diff --git a/jobs/Backend/Task/Services/Countries/CZE/CzeExchangeRateProvider.cs b/jobs/Backend/Task/Services/Countries/CZE/CzeExchangeRateProvider.cs index a3313e9f9..dbddbb9dc 100644 --- a/jobs/Backend/Task/Services/Countries/CZE/CzeExchangeRateProvider.cs +++ b/jobs/Backend/Task/Services/Countries/CZE/CzeExchangeRateProvider.cs @@ -1,5 +1,4 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -11,6 +10,7 @@ using ExchangeRateUpdater.Models; using ExchangeRateUpdater.Models.Cache; using ExchangeRateUpdater.Models.Countries.CZE; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace ExchangeRateUpdater.Services.Countries.CZE; @@ -23,58 +23,120 @@ public class CzeExchangeRateProvider : IExchangeRateProvider private readonly HttpClient _httpClient; public readonly CzeSettings _settings; private readonly ICacheService _cache; + private readonly IDateTimeProvider _dateTimeProvider; + private readonly ILogger _logger; public CzeExchangeRateProvider( HttpClient httpClient, IOptionsSnapshot options, - ICacheService cache) + ICacheService cache, + IDateTimeProvider dateTimeProvider, + ILogger logger) { _httpClient = httpClient; _settings = options.Value; _cache = cache; + _dateTimeProvider = dateTimeProvider; + _logger = logger; } public async Task> GetExchangeRates(IEnumerable currencies) { + _logger.LogInformation("Starting exchange rate retrieval."); + if (string.IsNullOrWhiteSpace(_settings.BaseUrl)) { + _logger.LogWarning("Base URL is not configured. Aborting exchange rate retrieval."); return Enumerable.Empty(); } - var existing = await _cache.GetAsync>(CacheKey); - if (existing != null) + var cachedRates = await TryGetValidCachedRatesAsync(currencies); + if (cachedRates is not null) { - var updateTime = GetUpdateHourInUTC(); - if (existing.DataExtractionTimeUTC > updateTime) - { - return MapToExchangeRates(existing.Data, currencies); - } + _logger.LogInformation("Valid cached exchange rates found. Returning cached data."); + return cachedRates; } - var response = await _httpClient.GetAsync(_settings.BaseUrl); - response.EnsureSuccessStatusCode(); + _logger.LogInformation("No valid cache found. Fetching exchange rates from remote source."); - using var stream = await response.Content.ReadAsStreamAsync(); - using var reader = XmlReader.Create(stream, new XmlReaderSettings + var latestRates = await FetchRatesFromCzechBankAsync(); + if (latestRates is null) { - DtdProcessing = DtdProcessing.Parse - }); + _logger.LogWarning("Failed to retrieve or deserialize exchange rates from remote source."); + return Enumerable.Empty(); + } - var serializer = new XmlSerializer(typeof(CzeExchangeRatesResponse)); - var result = (CzeExchangeRatesResponse)serializer.Deserialize(reader); + _logger.LogInformation("Successfully fetched exchange rates. Caching results."); + await CacheRatesAsync(latestRates); - if (result is not null && result.Table is not null && result.Table.Rates.Any()) + _logger.LogInformation("Returning newly fetched exchange rates."); + return MapToExchangeRates(latestRates, currencies); + } + + private async Task> TryGetValidCachedRatesAsync(IEnumerable currencies) + { + var cached = await _cache.GetAsync>(CacheKey); + if (cached is null) { - var cacheObject = new CacheObject + _logger.LogDebug("No cache entry found for exchange rates."); + return null; + } + + var updateCutoff = GetUpdateHourInUTC(); + if (cached.DataExtractionTimeUTC > updateCutoff) + { + _logger.LogDebug("Cached exchange rates are still valid (extracted at {ExtractionTime}, cutoff is {Cutoff}).", + cached.DataExtractionTimeUTC, updateCutoff); + return MapToExchangeRates(cached.Data, currencies); + } + + _logger.LogDebug("Cached exchange rates are expired (extracted at {ExtractionTime}, cutoff is {Cutoff}).", + cached.DataExtractionTimeUTC, updateCutoff); + + return null; + } + + private async Task FetchRatesFromCzechBankAsync() + { + try + { + _logger.LogDebug("Sending HTTP request to {Url}.", _settings.BaseUrl); + + var response = await _httpClient.GetAsync(_settings.BaseUrl); + response.EnsureSuccessStatusCode(); + + _logger.LogDebug("Received successful HTTP response."); + + using var stream = await response.Content.ReadAsStreamAsync(); + using var reader = XmlReader.Create(stream, new XmlReaderSettings { - Data = result, - DataExtractionTimeUTC = DateTimeOffset.UtcNow, - }; - await _cache.SetAsync(CacheKey, cacheObject, TimeSpan.FromSeconds(_settings.TtlInSeconds)); - return MapToExchangeRates(result, currencies); + DtdProcessing = DtdProcessing.Parse + }); + + var serializer = new XmlSerializer(typeof(CzeExchangeRatesResponse)); + var deserialized = serializer.Deserialize(reader) as CzeExchangeRatesResponse; + + _logger.LogDebug("Successfully deserialized exchange rates XML."); + return deserialized; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error while fetching or parsing exchange rates from {Url}.", _settings.BaseUrl); + return null; } + } + + private async Task CacheRatesAsync(CzeExchangeRatesResponse rates) + { + var cacheObject = new CacheObject + { + Data = rates, + DataExtractionTimeUTC = _dateTimeProvider.UtcNow + }; - return Enumerable.Empty(); + var ttl = TimeSpan.FromSeconds(_settings.TtlInSeconds); + await _cache.SetAsync(CacheKey, cacheObject, ttl); + _logger.LogDebug("Exchange rates cached with TTL of {TtlSeconds} seconds.", _settings.TtlInSeconds); } private IEnumerable MapToExchangeRates( @@ -98,9 +160,11 @@ private IEnumerable MapToExchangeRates( private DateTimeOffset GetUpdateHourInUTC() { TimeSpan time = TimeSpan.ParseExact(_settings.UpdateHourInLocalTime, "c", CultureInfo.InvariantCulture); - DateTime localDateTime = DateTime.Today.Add(time); - TimeZoneInfo czechTimeZone = TimeZoneInfo.FindSystemTimeZoneById(TimeZone); - DateTimeOffset czechDateTimeOffset = new(localDateTime, czechTimeZone.GetUtcOffset(localDateTime)); + var now = _dateTimeProvider.UtcNow; + var localDate = TimeZoneInfo.ConvertTime(now, TimeZoneInfo.FindSystemTimeZoneById(TimeZone)).Date; + DateTime localDateTime = localDate.Add(time); + var timeZone = TimeZoneInfo.FindSystemTimeZoneById(TimeZone); + DateTimeOffset czechDateTimeOffset = new(localDateTime, timeZone.GetUtcOffset(localDateTime)); return czechDateTimeOffset.ToUniversalTime(); } } diff --git a/jobs/Backend/Task/Services/DateTimeProvider.cs b/jobs/Backend/Task/Services/DateTimeProvider.cs new file mode 100644 index 000000000..0ad14960b --- /dev/null +++ b/jobs/Backend/Task/Services/DateTimeProvider.cs @@ -0,0 +1,9 @@ +using System; +using ExchangeRateUpdater.Interfaces; + +namespace ExchangeRateUpdater.Services; + +public class DateTimeProvider : IDateTimeProvider +{ + public DateTimeOffset UtcNow => DateTimeOffset.UtcNow; +} diff --git a/jobs/Backend/Task/Startup.cs b/jobs/Backend/Task/Startup.cs index ad6d90877..a15e5a615 100644 --- a/jobs/Backend/Task/Startup.cs +++ b/jobs/Backend/Task/Startup.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using ExchangeRateUpdater.Factories; using ExchangeRateUpdater.Interfaces; using ExchangeRateUpdater.Models.Cache; @@ -16,8 +17,10 @@ public static class Startup public static void ConfigureServices(IServiceCollection services, IConfiguration configuration) { services.AddTransient(); + services.AddSingleton(); services.AddSingleton(Console.Out); services.AddExhangeProviders(configuration); + services.AddCache(configuration); } public static void AddExhangeProviders(this IServiceCollection services, IConfiguration configuration) @@ -33,22 +36,29 @@ public static void AddExhangeProviders(this IServiceCollection services, IConfig public static void AddCache(this IServiceCollection services, IConfiguration configuration) { services.Configure(configuration.GetSection("CacheSettings")); - services.AddMemoryCache(); - services.AddStackExchangeRedisCache(options => - { - var redisConfig = configuration.GetSection("CacheSettings:RedisConfiguration").Value; - options.Configuration = redisConfig; - }); - var provider = configuration.GetValue("CacheSettings:Provider"); + var provider = configuration.GetValue("CacheSettings:Provider") ?? "file"; - if (string.Equals(provider, "Redis", StringComparison.OrdinalIgnoreCase)) - { - services.AddSingleton(); - } - else + switch (provider.Trim().ToLowerInvariant()) { - services.AddSingleton(); + case "redis": + services.AddStackExchangeRedisCache(options => + { + var redisConfig = configuration.GetSection("CacheSettings:RedisConfiguration").Value; + options.Configuration = redisConfig; + }); + services.AddSingleton(); + break; + + case "file": + var filePath = configuration.GetValue("CacheSettings:FileCachePath") ?? "filecache.json"; + if (!Path.IsPathRooted(filePath)) + { + var baseDir = AppContext.BaseDirectory; + filePath = Path.GetFullPath(Path.Combine(baseDir, filePath)); + } + services.AddSingleton(new FileCacheService(filePath)); + break; } } } diff --git a/jobs/Backend/Task/appsettings.dev.json b/jobs/Backend/Task/appsettings.dev.json index 5c38ab469..3dea5c7ac 100644 --- a/jobs/Backend/Task/appsettings.dev.json +++ b/jobs/Backend/Task/appsettings.dev.json @@ -5,5 +5,11 @@ "Microsoft": "Information", "Microsoft.Hosting.Lifetime": "Information" } + }, + + "CacheSettings": { + "Provider": "File", // "Redis" + "RedisConfiguration": "localhost:6379", + "FileCachePath": "../../../FileCache/filecache.json" } } \ No newline at end of file diff --git a/jobs/Backend/Task/appsettings.json b/jobs/Backend/Task/appsettings.json index be28f64ee..8ce93f6e7 100644 --- a/jobs/Backend/Task/appsettings.json +++ b/jobs/Backend/Task/appsettings.json @@ -14,7 +14,7 @@ } }, "CacheSettings": { - "Provider": "Memory", // or "Redis" + "Provider": "File", //"Redis" "RedisConfiguration": "localhost:6379" } } \ No newline at end of file From f5b601ef626f109ab3fb4a22a1ba98f848e41128 Mon Sep 17 00:00:00 2001 From: jdiazbello Date: Sat, 2 Aug 2025 18:33:26 +0200 Subject: [PATCH 06/11] Add Tests --- .../CzeExchangeRateProviderTests.cs | 205 ++++++++++++ .../ExchangeRateUpdater.Tests.csproj | 29 ++ .../FileCacheServiceTests.cs | 50 +++ .../LoggerExtensions.cs | 17 + .../ExchangeRateUpdater.Tests/StartupTests.cs | 80 +++++ .../ExchangeRateUpdater.Tests/Usings.cs | 1 + .../v17/DocumentLayout.backup.json | 304 ++++++++++++------ .../v17/DocumentLayout.json | 148 +++++---- .../v17/TestStore/0/000.testlog | Bin 0 -> 188001 bytes .../v17/TestStore/0/testlog.manifest | Bin 0 -> 24 bytes jobs/Backend/Task/ExchangeRateUpdater.sln | 13 +- jobs/Backend/Task/{ => Models}/Currency.cs | 2 +- jobs/Backend/Task/Program.cs | 8 +- 13 files changed, 707 insertions(+), 150 deletions(-) create mode 100644 jobs/Backend/ExchangeRateUpdater.Tests/CzeExchangeRateProviderTests.cs create mode 100644 jobs/Backend/ExchangeRateUpdater.Tests/ExchangeRateUpdater.Tests.csproj create mode 100644 jobs/Backend/ExchangeRateUpdater.Tests/FileCacheServiceTests.cs create mode 100644 jobs/Backend/ExchangeRateUpdater.Tests/LoggerExtensions.cs create mode 100644 jobs/Backend/ExchangeRateUpdater.Tests/StartupTests.cs create mode 100644 jobs/Backend/ExchangeRateUpdater.Tests/Usings.cs create mode 100644 jobs/Backend/Task/.vs/ExchangeRateUpdater/v17/TestStore/0/000.testlog create mode 100644 jobs/Backend/Task/.vs/ExchangeRateUpdater/v17/TestStore/0/testlog.manifest rename jobs/Backend/Task/{ => Models}/Currency.cs (87%) diff --git a/jobs/Backend/ExchangeRateUpdater.Tests/CzeExchangeRateProviderTests.cs b/jobs/Backend/ExchangeRateUpdater.Tests/CzeExchangeRateProviderTests.cs new file mode 100644 index 000000000..845746416 --- /dev/null +++ b/jobs/Backend/ExchangeRateUpdater.Tests/CzeExchangeRateProviderTests.cs @@ -0,0 +1,205 @@ +using System.Net; +using System.Xml.Serialization; +using ExchangeRateUpdater.Interfaces; +using ExchangeRateUpdater.Models; +using ExchangeRateUpdater.Models.Cache; +using ExchangeRateUpdater.Models.Countries.CZE; +using ExchangeRateUpdater.Services.Countries.CZE; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Moq; +using Moq.Protected; + +namespace ExchangeRateUpdater.Tests; + +public class CzeExchangeRateProviderTests +{ + private readonly Mock _cacheMock = new(); + private readonly Mock _dateTimeProviderMock = new(); + private readonly Mock> _loggerMock = new(); + private readonly Mock> _optionsMock = new(); + private readonly HttpClient _httpClient; + + private readonly CzeSettings _settings = new() + { + BaseUrl = "http://fake-url", + TtlInSeconds = 60, + UpdateHourInLocalTime = "06:00:00" + }; + + private readonly List _currencies = new() + { + new Currency("USD"), + new Currency("EUR") + }; + + public CzeExchangeRateProviderTests() + { + _optionsMock.Setup(o => o.Value).Returns(_settings); + + var handlerMock = new Mock(MockBehavior.Strict); + _httpClient = new HttpClient(handlerMock.Object); + + _dateTimeProviderMock.Setup(d => d.UtcNow).Returns(new DateTimeOffset(2025, 8, 2, 0, 0, 0, TimeSpan.Zero)); + } + + [Fact] + public async Task GetExchangeRates_ReturnsEmpty_WhenBaseUrlIsEmpty() + { + _settings.BaseUrl = null; + var provider = CreateProvider(); + + var result = await provider.GetExchangeRates(_currencies); + + Assert.Empty(result); + _loggerMock.VerifyLog(LogLevel.Warning, "Base URL is not configured", Times.Once()); + } + + [Fact] + public async Task GetExchangeRates_ReturnsCachedData_IfCacheValid() + { + var cachedResponse = new CzeExchangeRatesResponse + { + Table = new CzeExchangeRateTable + { + Rates = new List + { + new() { Code = "USD", Amount = 1, RateRaw = "22,0" }, + new() { Code = "EUR", Amount = 1, RateRaw = "24.0" }, + } + } + }; + + var cacheObject = new CacheObject + { + Data = cachedResponse, + DataExtractionTimeUTC = DateTimeOffset.UtcNow.AddHours(1) + }; + + _cacheMock.Setup(c => c.GetAsync>(It.IsAny())) + .ReturnsAsync(cacheObject); + + var provider = CreateProvider(); + + var result = await provider.GetExchangeRates(_currencies); + + Assert.NotEmpty(result); + Assert.Contains(result, r => r.TargetCurrency.Code == "USD" && r.Value == 22); + Assert.Contains(result, r => r.TargetCurrency.Code == "EUR" && r.Value == 24); + } + + [Fact] + public async Task GetExchangeRates_FetchesFromRemoteAndCaches_WhenNoValidCache() + { + var response = new CzeExchangeRatesResponse + { + Table = new CzeExchangeRateTable + { + Rates = new List + { + new() { Code = "USD", Amount = 1, RateRaw = "22,0" }, + new() { Code = "EUR", Amount = 1, RateRaw = "24.0" }, + } + } + }; + + var memoryStream = SerializeToXmlStream(response); + + var handlerMock = new Mock(MockBehavior.Strict); + handlerMock + .Protected() + .Setup>("SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StreamContent(memoryStream) + }); + + _cacheMock.Setup(c => c.GetAsync>(It.IsAny())) + .ReturnsAsync((CacheObject)null); + + _cacheMock.Setup(c => c.SetAsync(It.IsAny(), It.IsAny>(), It.IsAny())) + .Returns(Task.CompletedTask) + .Verifiable(); + + var provider = CreateProvider(handlerMock.Object); + + var result = await provider.GetExchangeRates(_currencies); + + Assert.NotEmpty(result); + Assert.Contains(result, r => r.TargetCurrency.Code == "USD" && r.Value == 22); + Assert.Contains(result, r => r.TargetCurrency.Code == "EUR" && r.Value == 24); + + _cacheMock.Verify(); + } + + [Fact] + public async Task GetExchangeRates_ReturnsEmpty_WhenRemoteFetchFails() + { + var handlerMock = new Mock(MockBehavior.Strict); + handlerMock + .Protected() + .Setup>("SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .ThrowsAsync(new HttpRequestException("Network error")); + + _cacheMock.Setup(c => c.GetAsync>(It.IsAny())) + .ReturnsAsync((CacheObject?)null); + + var provider = CreateProvider(handlerMock.Object); + + var result = await provider.GetExchangeRates(_currencies); + + Assert.Empty(result); + } + + [Fact] + public void GetUpdateHourInUTC_ReturnsCorrectUtcDateTime() + { + var fixedNow = new DateTimeOffset(2024, 1, 1, 10, 0, 0, TimeSpan.Zero); + _dateTimeProviderMock.Setup(d => d.UtcNow).Returns(fixedNow); + + var provider = CreateProvider(); + + var result = provider.GetType() + .GetMethod("GetUpdateHourInUTC", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) + ?.Invoke(provider, null); + + Assert.IsType(result); + + var utcResult = (DateTimeOffset)result; + + var czechZone = TimeZoneInfo.FindSystemTimeZoneById("Central European Standard Time"); + var expectedLocal = new DateTime(fixedNow.Year, fixedNow.Month, fixedNow.Day, 6, 0, 0); + var expectedUtc = new DateTimeOffset(expectedLocal, czechZone.GetUtcOffset(expectedLocal)).ToUniversalTime(); + + Assert.Equal(expectedUtc, utcResult); + } + + private static MemoryStream SerializeToXmlStream(CzeExchangeRatesResponse response) + { + var serializer = new XmlSerializer(typeof(CzeExchangeRatesResponse)); + var ms = new MemoryStream(); + using (var writer = new StreamWriter(ms, new System.Text.UTF8Encoding(true), 1024, leaveOpen: true)) + { + serializer.Serialize(writer, response); + } + ms.Position = 0; + return ms; + } + + private CzeExchangeRateProvider CreateProvider(HttpMessageHandler? handler = null) + { + var client = handler == null ? _httpClient : new HttpClient(handler); + + return new CzeExchangeRateProvider( + client, + _optionsMock.Object, + _cacheMock.Object, + _dateTimeProviderMock.Object, + _loggerMock.Object); + } +} \ No newline at end of file diff --git a/jobs/Backend/ExchangeRateUpdater.Tests/ExchangeRateUpdater.Tests.csproj b/jobs/Backend/ExchangeRateUpdater.Tests/ExchangeRateUpdater.Tests.csproj new file mode 100644 index 000000000..a419bc35c --- /dev/null +++ b/jobs/Backend/ExchangeRateUpdater.Tests/ExchangeRateUpdater.Tests.csproj @@ -0,0 +1,29 @@ + + + + net6.0 + enable + enable + + false + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/jobs/Backend/ExchangeRateUpdater.Tests/FileCacheServiceTests.cs b/jobs/Backend/ExchangeRateUpdater.Tests/FileCacheServiceTests.cs new file mode 100644 index 000000000..3ea240019 --- /dev/null +++ b/jobs/Backend/ExchangeRateUpdater.Tests/FileCacheServiceTests.cs @@ -0,0 +1,50 @@ +using ExchangeRateUpdater.Services.Cache; + +namespace ExchangeRateUpdater.Tests; + +public class FileCacheServiceTests : IDisposable +{ + private readonly string _tempFilePath; + private readonly FileCacheService _fileCacheService; + + public FileCacheServiceTests() + { + _tempFilePath = Path.Combine(Path.GetTempPath(), $"testcache_{Guid.NewGuid()}.json"); + _fileCacheService = new FileCacheService(_tempFilePath); + } + + [Fact] + public async Task SetAndGetAsync_StoresAndRetrievesValue() + { + var key = "test-key"; + var testObject = new TestCacheObject { Value = "Hello, World!" }; + var ttl = TimeSpan.FromMinutes(5); + + await _fileCacheService.SetAsync(key, testObject, ttl); + + var cached = await _fileCacheService.GetAsync(key); + + Assert.NotNull(cached); + Assert.Equal(testObject.Value, cached.Value); + } + + [Fact] + public async Task GetAsync_ReturnsNull_WhenKeyDoesNotExist() + { + var cached = await _fileCacheService.GetAsync("non-existent-key"); + Assert.Null(cached); + } + + public void Dispose() + { + if (File.Exists(_tempFilePath)) + { + File.Delete(_tempFilePath); + } + } + + private class TestCacheObject + { + public string Value { get; set; } + } +} diff --git a/jobs/Backend/ExchangeRateUpdater.Tests/LoggerExtensions.cs b/jobs/Backend/ExchangeRateUpdater.Tests/LoggerExtensions.cs new file mode 100644 index 000000000..c2d1c611d --- /dev/null +++ b/jobs/Backend/ExchangeRateUpdater.Tests/LoggerExtensions.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.Logging; +using Moq; + +namespace ExchangeRateUpdater.Tests; +public static class LoggerExtensions +{ + public static void VerifyLog(this Mock> loggerMock, LogLevel level, string messageFragment, Times times) + { + loggerMock.Verify(x => x.Log( + level, + It.IsAny(), + It.Is((v, t) => v.ToString()!.Contains(messageFragment)), + It.IsAny(), + (Func)It.IsAny()), + times); + } +} \ No newline at end of file diff --git a/jobs/Backend/ExchangeRateUpdater.Tests/StartupTests.cs b/jobs/Backend/ExchangeRateUpdater.Tests/StartupTests.cs new file mode 100644 index 000000000..0928d48e2 --- /dev/null +++ b/jobs/Backend/ExchangeRateUpdater.Tests/StartupTests.cs @@ -0,0 +1,80 @@ +using ExchangeRateUpdater.Interfaces; +using ExchangeRateUpdater.Models.Countries.CZE; +using ExchangeRateUpdater.Services.Cache; +using ExchangeRateUpdater.Services.Countries.CZE; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace ExchangeRateUpdater.Tests; + +public class StartupTests +{ + [Fact] + public void ConfigureServices_RegistersExpectedServices() + { + // Arrange + var services = new ServiceCollection(); + + var inMemorySettings = new Dictionary + { + {"CacheSettings:Provider", "file"}, + {"CacheSettings:FileCachePath", "testcache.json"}, + {"ExchangeProviders:CZE:BaseUrl", "http://example.com"}, + {"ExchangeProviders:CZE:TtlInSeconds", "60"}, + {"ExchangeProviders:CZE:UpdateHourInLocalTime", "06:00:00"} + }; + IConfiguration configuration = new ConfigurationBuilder() + .AddInMemoryCollection(inMemorySettings) + .Build(); + + // Act + Startup.ConfigureServices(services, configuration); + var provider = services.BuildServiceProvider(); + + // Assert + var app = provider.GetService(); + Assert.NotNull(app); + + var dateTimeProvider = provider.GetService(); + Assert.NotNull(dateTimeProvider); + + var cacheService = provider.GetService(); + Assert.NotNull(cacheService); + Assert.IsType(cacheService); + + + var czeSettings = provider.GetService>(); + Assert.NotNull(czeSettings); + Assert.Equal("http://example.com", czeSettings.Value.BaseUrl); + + var httpClientFactory = provider.GetService(); + Assert.NotNull(httpClientFactory); + var httpClient = httpClientFactory.CreateClient(nameof(CzeExchangeRateProvider)); + Assert.NotNull(httpClient); + } + + [Fact] + public void AddCache_RegistersRedisCache_WhenProviderIsRedis() + { + // Arrange + var services = new ServiceCollection(); + + var inMemorySettings = new Dictionary + { + {"CacheSettings:Provider", "redis"}, + {"CacheSettings:RedisConfiguration", "localhost:6379"} + }; + IConfiguration configuration = new ConfigurationBuilder() + .AddInMemoryCollection(inMemorySettings) + .Build(); + + // Act + Startup.ConfigureServices(services, configuration); + var provider = services.BuildServiceProvider(); + + // Assert + var cacheService = provider.GetService(); + Assert.NotNull(cacheService); + Assert.IsType(cacheService); + } +} diff --git a/jobs/Backend/ExchangeRateUpdater.Tests/Usings.cs b/jobs/Backend/ExchangeRateUpdater.Tests/Usings.cs new file mode 100644 index 000000000..8c927eb74 --- /dev/null +++ b/jobs/Backend/ExchangeRateUpdater.Tests/Usings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/jobs/Backend/Task/.vs/ExchangeRateUpdater/v17/DocumentLayout.backup.json b/jobs/Backend/Task/.vs/ExchangeRateUpdater/v17/DocumentLayout.backup.json index 09a846570..9ed221850 100644 --- a/jobs/Backend/Task/.vs/ExchangeRateUpdater/v17/DocumentLayout.backup.json +++ b/jobs/Backend/Task/.vs/ExchangeRateUpdater/v17/DocumentLayout.backup.json @@ -3,16 +3,25 @@ "WorkspaceRootPath": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\", "Documents": [ { - "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\currency.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", - "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:currency.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\startup.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", + "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:startup.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + }, + { + "AbsoluteMoniker": "D:0:0:{1414190F-2DEA-401A-A614-E26DCD42C432}|..\\ExchangeRateUpdater.Tests\\ExchangeRateUpdater.Tests.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\exchangerateupdater.tests\\filecacheservicetests.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + }, + { + "AbsoluteMoniker": "D:0:0:{1414190F-2DEA-401A-A614-E26DCD42C432}|..\\ExchangeRateUpdater.Tests\\ExchangeRateUpdater.Tests.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\exchangerateupdater.tests\\startuptests.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + }, + { + "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\program.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", + "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:program.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" }, { "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\app.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:app.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" }, { - "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\services\\exchangerateprovider.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", - "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:services\\exchangerateprovider.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + "AbsoluteMoniker": "D:0:0:{1414190F-2DEA-401A-A614-E26DCD42C432}|..\\ExchangeRateUpdater.Tests\\ExchangeRateUpdater.Tests.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\exchangerateupdater.tests\\czeexchangerateprovidertests.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" }, { "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\services\\countries\\cze\\czeexchangerateprovider.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", @@ -23,24 +32,30 @@ "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:models\\exchangerate.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" }, { - "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\models\\countryisoalpha3.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", - "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:models\\countryisoalpha3.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\models\\countries\\cze\\czeexchangeratetable.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", + "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:models\\countries\\cze\\czeexchangeratetable.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" }, { - "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\models\\countries\\cze\\czesettings.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", - "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:models\\countries\\cze\\czesettings.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\models\\countries\\cze\\czeexchangerate.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", + "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:models\\countries\\cze\\czeexchangerate.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" }, { - "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\models\\countries\\cze\\czeexchangeratetable.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", - "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:models\\countries\\cze\\czeexchangeratetable.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\interfaces\\idatetimeprovider.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", + "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:interfaces\\idatetimeprovider.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" }, { - "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\models\\countries\\cze\\czeexchangeratesresponse.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", - "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:models\\countries\\cze\\czeexchangeratesresponse.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\interfaces\\iexchangerateprovider.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", + "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:interfaces\\iexchangerateprovider.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" }, { - "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\models\\countries\\cze\\czeexchangerate.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", - "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:models\\countries\\cze\\czeexchangerate.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + "AbsoluteMoniker": "D:0:0:{1414190F-2DEA-401A-A614-E26DCD42C432}|..\\ExchangeRateUpdater.Tests\\ExchangeRateUpdater.Tests.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\exchangerateupdater.tests\\loggerextensions.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + }, + { + "AbsoluteMoniker": "D:0:0:{1414190F-2DEA-401A-A614-E26DCD42C432}|..\\ExchangeRateUpdater.Tests\\ExchangeRateUpdater.Tests.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\exchangerateupdater.tests\\usings.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + }, + { + "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\services\\exchangerateprovider.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", + "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:services\\exchangerateprovider.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" } ], "DocumentGroupContainers": [ @@ -50,7 +65,7 @@ "DocumentGroups": [ { "DockedWidth": 200, - "SelectedChildIndex": 26, + "SelectedChildIndex": 28, "Children": [ { "$type": "Bookmark", @@ -158,132 +173,145 @@ }, { "$type": "Document", - "DocumentIndex": 0, - "Title": "Currency.cs", - "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Currency.cs", - "RelativeDocumentMoniker": "Currency.cs", - "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Currency.cs", - "RelativeToolTip": "Currency.cs", - "ViewState": "AgIAAAAAAAAAAAAAAAAAAA8AAAAFAAAAAAAAAA==", + "DocumentIndex": 2, + "Title": "StartupTests.cs", + "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\ExchangeRateUpdater.Tests\\StartupTests.cs", + "RelativeDocumentMoniker": "..\\ExchangeRateUpdater.Tests\\StartupTests.cs", + "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\ExchangeRateUpdater.Tests\\StartupTests.cs", + "RelativeToolTip": "..\\ExchangeRateUpdater.Tests\\StartupTests.cs", + "ViewState": "AgIAACoAAAAAAAAAAAAgwE4AAAAFAAAAAAAAAA==", "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", - "WhenOpened": "2025-08-02T11:56:35.988Z", + "WhenOpened": "2025-08-02T16:24:23.772Z", "EditorCaption": "" }, { "$type": "Document", "DocumentIndex": 1, - "Title": "App.cs", - "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\App.cs", - "RelativeDocumentMoniker": "App.cs", - "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\App.cs", - "RelativeToolTip": "App.cs", - "ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "Title": "FileCacheServiceTests.cs", + "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\ExchangeRateUpdater.Tests\\FileCacheServiceTests.cs", + "RelativeDocumentMoniker": "..\\ExchangeRateUpdater.Tests\\FileCacheServiceTests.cs", + "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\ExchangeRateUpdater.Tests\\FileCacheServiceTests.cs", + "RelativeToolTip": "..\\ExchangeRateUpdater.Tests\\FileCacheServiceTests.cs", + "ViewState": "AgIAAAAAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAA==", "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", - "WhenOpened": "2025-08-02T11:56:34.333Z", + "WhenOpened": "2025-08-02T16:23:16.61Z", "EditorCaption": "" }, { "$type": "Document", - "DocumentIndex": 2, - "Title": "ExchangeRateProvider.cs", - "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Services\\ExchangeRateProvider.cs", - "RelativeDocumentMoniker": "Services\\ExchangeRateProvider.cs", - "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Services\\ExchangeRateProvider.cs", - "RelativeToolTip": "Services\\ExchangeRateProvider.cs", - "ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "DocumentIndex": 0, + "Title": "Startup.cs", + "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Startup.cs", + "RelativeDocumentMoniker": "Startup.cs", + "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Startup.cs", + "RelativeToolTip": "Startup.cs", + "ViewState": "AgIAAAAAAAAAAAAAAAAAACMAAABfAAAAAAAAAA==", "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", - "WhenOpened": "2025-08-02T11:56:32.884Z", + "WhenOpened": "2025-08-02T16:21:45.442Z", "EditorCaption": "" }, { "$type": "Document", "DocumentIndex": 3, - "Title": "CzeExchangeRateProvider.cs", - "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Services\\Countries\\CZE\\CzeExchangeRateProvider.cs", - "RelativeDocumentMoniker": "Services\\Countries\\CZE\\CzeExchangeRateProvider.cs", - "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Services\\Countries\\CZE\\CzeExchangeRateProvider.cs*", - "RelativeToolTip": "Services\\Countries\\CZE\\CzeExchangeRateProvider.cs*", - "ViewState": "AgIAAAAAAAAAAAAAAAAAABUAAAAAAAAAAAAAAA==", + "Title": "Program.cs", + "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Program.cs", + "RelativeDocumentMoniker": "Program.cs", + "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Program.cs", + "RelativeToolTip": "Program.cs", + "ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", - "WhenOpened": "2025-08-02T11:56:25.321Z", + "WhenOpened": "2025-08-02T16:21:42.091Z", "EditorCaption": "" }, { "$type": "Document", - "DocumentIndex": 4, - "Title": "ExchangeRate.cs", - "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Models\\ExchangeRate.cs", - "RelativeDocumentMoniker": "Models\\ExchangeRate.cs", - "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Models\\ExchangeRate.cs*", - "RelativeToolTip": "Models\\ExchangeRate.cs*", - "ViewState": "AgIAAAAAAAAAAAAAAAAAAAoAAAAAAAAAAAAAAA==", + "DocumentIndex": 5, + "Title": "CzeExchangeRateProviderTests.cs", + "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\ExchangeRateUpdater.Tests\\CzeExchangeRateProviderTests.cs", + "RelativeDocumentMoniker": "..\\ExchangeRateUpdater.Tests\\CzeExchangeRateProviderTests.cs", + "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\ExchangeRateUpdater.Tests\\CzeExchangeRateProviderTests.cs", + "RelativeToolTip": "..\\ExchangeRateUpdater.Tests\\CzeExchangeRateProviderTests.cs", + "ViewState": "AgIAAAAAAAAAAAAAAAAAAIoAAABKAAAAAAAAAA==", "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", - "WhenOpened": "2025-08-02T11:56:13.773Z", + "WhenOpened": "2025-08-02T15:57:01.588Z", "EditorCaption": "" }, { "$type": "Document", - "DocumentIndex": 5, - "Title": "CountryIsoAlpha3.cs", - "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Models\\CountryIsoAlpha3.cs", - "RelativeDocumentMoniker": "Models\\CountryIsoAlpha3.cs", - "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Models\\CountryIsoAlpha3.cs*", - "RelativeToolTip": "Models\\CountryIsoAlpha3.cs*", - "ViewState": "AgIAAAAAAAAAAAAAAAAAAAIAAAAGAAAAAAAAAA==", + "DocumentIndex": 10, + "Title": "IDateTimeProvider.cs", + "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Interfaces\\IDateTimeProvider.cs", + "RelativeDocumentMoniker": "Interfaces\\IDateTimeProvider.cs", + "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Interfaces\\IDateTimeProvider.cs", + "RelativeToolTip": "Interfaces\\IDateTimeProvider.cs", + "ViewState": "AgIAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAA==", "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", - "WhenOpened": "2025-08-02T11:56:09.285Z", + "WhenOpened": "2025-08-02T15:55:17.525Z", "EditorCaption": "" }, { "$type": "Document", - "DocumentIndex": 6, - "Title": "CzeSettings.cs", - "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Models\\Countries\\CZE\\CzeSettings.cs", - "RelativeDocumentMoniker": "Models\\Countries\\CZE\\CzeSettings.cs", - "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Models\\Countries\\CZE\\CzeSettings.cs*", - "RelativeToolTip": "Models\\Countries\\CZE\\CzeSettings.cs*", - "ViewState": "AgIAAAAAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAA==", + "DocumentIndex": 11, + "Title": "IExchangeRateProvider.cs", + "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Interfaces\\IExchangeRateProvider.cs", + "RelativeDocumentMoniker": "Interfaces\\IExchangeRateProvider.cs", + "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Interfaces\\IExchangeRateProvider.cs", + "RelativeToolTip": "Interfaces\\IExchangeRateProvider.cs", + "ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", - "WhenOpened": "2025-08-02T11:55:54.978Z", + "WhenOpened": "2025-08-02T15:55:15.094Z", "EditorCaption": "" }, { "$type": "Document", - "DocumentIndex": 7, - "Title": "CzeExchangeRateTable.cs", - "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Models\\Countries\\CZE\\CzeExchangeRateTable.cs", - "RelativeDocumentMoniker": "Models\\Countries\\CZE\\CzeExchangeRateTable.cs", - "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Models\\Countries\\CZE\\CzeExchangeRateTable.cs*", - "RelativeToolTip": "Models\\Countries\\CZE\\CzeExchangeRateTable.cs*", + "DocumentIndex": 14, + "Title": "ExchangeRateProvider.cs", + "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Services\\ExchangeRateProvider.cs", + "RelativeDocumentMoniker": "Services\\ExchangeRateProvider.cs", + "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Services\\ExchangeRateProvider.cs", + "RelativeToolTip": "Services\\ExchangeRateProvider.cs", "ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", - "WhenOpened": "2025-08-02T11:55:54.422Z", + "WhenOpened": "2025-08-02T15:55:10.992Z", "EditorCaption": "" }, { "$type": "Document", - "DocumentIndex": 8, - "Title": "CzeExchangeRatesResponse.cs", - "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Models\\Countries\\CZE\\CzeExchangeRatesResponse.cs", - "RelativeDocumentMoniker": "Models\\Countries\\CZE\\CzeExchangeRatesResponse.cs", - "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Models\\Countries\\CZE\\CzeExchangeRatesResponse.cs*", - "RelativeToolTip": "Models\\Countries\\CZE\\CzeExchangeRatesResponse.cs*", - "ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "DocumentIndex": 12, + "Title": "LoggerExtensions.cs", + "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\ExchangeRateUpdater.Tests\\LoggerExtensions.cs", + "RelativeDocumentMoniker": "..\\ExchangeRateUpdater.Tests\\LoggerExtensions.cs", + "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\ExchangeRateUpdater.Tests\\LoggerExtensions.cs", + "RelativeToolTip": "..\\ExchangeRateUpdater.Tests\\LoggerExtensions.cs", + "ViewState": "AgIAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAA==", "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", - "WhenOpened": "2025-08-02T11:55:53.905Z", + "WhenOpened": "2025-08-02T16:02:02.386Z", "EditorCaption": "" }, { "$type": "Document", - "DocumentIndex": 9, - "Title": "CzeExchangeRate.cs", - "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Models\\Countries\\CZE\\CzeExchangeRate.cs", - "RelativeDocumentMoniker": "Models\\Countries\\CZE\\CzeExchangeRate.cs", - "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Models\\Countries\\CZE\\CzeExchangeRate.cs*", - "RelativeToolTip": "Models\\Countries\\CZE\\CzeExchangeRate.cs*", + "DocumentIndex": 4, + "Title": "App.cs", + "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\App.cs", + "RelativeDocumentMoniker": "App.cs", + "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\App.cs", + "RelativeToolTip": "App.cs", + "ViewState": "AgIAABcAAAAAAAAAAAAQwDEAAABRAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", + "WhenOpened": "2025-08-02T15:52:36.833Z", + "EditorCaption": "" + }, + { + "$type": "Document", + "DocumentIndex": 13, + "Title": "Usings.cs", + "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\ExchangeRateUpdater.Tests\\Usings.cs", + "RelativeDocumentMoniker": "..\\ExchangeRateUpdater.Tests\\Usings.cs", + "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\ExchangeRateUpdater.Tests\\Usings.cs", + "RelativeToolTip": "..\\ExchangeRateUpdater.Tests\\Usings.cs", "ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", - "WhenOpened": "2025-08-02T11:55:50.507Z", + "WhenOpened": "2025-08-02T15:57:28.228Z", "EditorCaption": "" } ] @@ -309,6 +337,100 @@ ] } ] + }, + { + "Orientation": 0, + "VerticalTabListWidth": 256, + "FloatingWindowState": { + "Id": "64a7b947-f702-48e2-8c80-163b64d61eba", + "Display": 1, + "X": 1007, + "Y": -40, + "Width": 858, + "Height": 742, + "WindowState": 0 + }, + "DocumentGroups": [ + { + "DockedWidth": 200, + "SelectedChildIndex": 0, + "Children": [ + { + "$type": "Document", + "DocumentIndex": 7, + "Title": "ExchangeRate.cs", + "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Models\\ExchangeRate.cs", + "RelativeDocumentMoniker": "Models\\ExchangeRate.cs", + "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Models\\ExchangeRate.cs", + "RelativeToolTip": "Models\\ExchangeRate.cs", + "ViewState": "AgIAAAAAAAAAAAAAAAAAAA0AAAAiAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", + "WhenOpened": "2025-08-02T16:05:21.722Z", + "EditorCaption": "" + }, + { + "$type": "Document", + "DocumentIndex": 8, + "Title": "CzeExchangeRateTable.cs", + "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Models\\Countries\\CZE\\CzeExchangeRateTable.cs", + "RelativeDocumentMoniker": "Models\\Countries\\CZE\\CzeExchangeRateTable.cs", + "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Models\\Countries\\CZE\\CzeExchangeRateTable.cs", + "RelativeToolTip": "Models\\Countries\\CZE\\CzeExchangeRateTable.cs", + "ViewState": "AgIAAAAAAAAAAAAAAAAAAAsAAAAfAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", + "WhenOpened": "2025-08-02T16:04:44.734Z", + "EditorCaption": "" + }, + { + "$type": "Document", + "DocumentIndex": 9, + "Title": "CzeExchangeRate.cs", + "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Models\\Countries\\CZE\\CzeExchangeRate.cs", + "RelativeDocumentMoniker": "Models\\Countries\\CZE\\CzeExchangeRate.cs", + "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Models\\Countries\\CZE\\CzeExchangeRate.cs", + "RelativeToolTip": "Models\\Countries\\CZE\\CzeExchangeRate.cs", + "ViewState": "AgIAAAAAAAAAAAAAAAAAAAQAAAAcAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", + "WhenOpened": "2025-08-02T16:03:19.974Z", + "EditorCaption": "" + } + ] + } + ] + }, + { + "Orientation": 0, + "VerticalTabListWidth": 256, + "FloatingWindowState": { + "Id": "4992753e-af97-424d-a7c0-074954eebdd2", + "Display": 1, + "X": 682, + "Y": 82, + "Width": 1313, + "Height": 880, + "WindowState": 2 + }, + "DocumentGroups": [ + { + "DockedWidth": 200, + "SelectedChildIndex": 0, + "Children": [ + { + "$type": "Document", + "DocumentIndex": 6, + "Title": "CzeExchangeRateProvider.cs", + "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Services\\Countries\\CZE\\CzeExchangeRateProvider.cs", + "RelativeDocumentMoniker": "Services\\Countries\\CZE\\CzeExchangeRateProvider.cs", + "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Services\\Countries\\CZE\\CzeExchangeRateProvider.cs", + "RelativeToolTip": "Services\\Countries\\CZE\\CzeExchangeRateProvider.cs", + "ViewState": "AgIAAEIAAAAAAAAAAAAawFQAAAAAAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", + "WhenOpened": "2025-08-02T15:54:59.523Z", + "EditorCaption": "" + } + ] + } + ] } ] } \ No newline at end of file diff --git a/jobs/Backend/Task/.vs/ExchangeRateUpdater/v17/DocumentLayout.json b/jobs/Backend/Task/.vs/ExchangeRateUpdater/v17/DocumentLayout.json index 985f7c8d8..801bc754c 100644 --- a/jobs/Backend/Task/.vs/ExchangeRateUpdater/v17/DocumentLayout.json +++ b/jobs/Backend/Task/.vs/ExchangeRateUpdater/v17/DocumentLayout.json @@ -3,20 +3,24 @@ "WorkspaceRootPath": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\", "Documents": [ { - "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\services\\countries\\cze\\czeexchangerateprovider.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", - "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:services\\countries\\cze\\czeexchangerateprovider.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\program.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", + "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:program.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" }, { "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\startup.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:startup.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" }, { - "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\services\\exchangerateprovider.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", - "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:services\\exchangerateprovider.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\services\\countries\\cze\\czeexchangerateprovider.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", + "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:services\\countries\\cze\\czeexchangerateprovider.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + }, + { + "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\models\\exchangerate.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", + "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:models\\exchangerate.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" }, { - "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\appsettings.json||{90A6B3A7-C1A3-4009-A288-E2FF89E96FA0}", - "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:appsettings.json||{90A6B3A7-C1A3-4009-A288-E2FF89E96FA0}" + "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\models\\countries\\cze\\czeexchangeratetable.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", + "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:models\\countries\\cze\\czeexchangeratetable.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" }, { "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\models\\countries\\cze\\czeexchangerate.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", @@ -30,7 +34,7 @@ "DocumentGroups": [ { "DockedWidth": 200, - "SelectedChildIndex": 27, + "SelectedChildIndex": 26, "Children": [ { "$type": "Bookmark", @@ -139,27 +143,14 @@ { "$type": "Document", "DocumentIndex": 0, - "Title": "CzeExchangeRateProvider.cs", - "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Services\\Countries\\CZE\\CzeExchangeRateProvider.cs", - "RelativeDocumentMoniker": "Services\\Countries\\CZE\\CzeExchangeRateProvider.cs", - "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Services\\Countries\\CZE\\CzeExchangeRateProvider.cs", - "RelativeToolTip": "Services\\Countries\\CZE\\CzeExchangeRateProvider.cs", - "ViewState": "AgIAAAQAAAAAAAAAAAAAAEYAAAA6AAAAAAAAAA==", + "Title": "Program.cs", + "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Program.cs", + "RelativeDocumentMoniker": "Program.cs", + "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Program.cs*", + "RelativeToolTip": "Program.cs*", + "ViewState": "AgIAAAAAAAAAAAAAAAAAACAAAAATAAAAAAAAAA==", "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", - "WhenOpened": "2025-08-02T12:03:42.017Z", - "EditorCaption": "" - }, - { - "$type": "Document", - "DocumentIndex": 2, - "Title": "ExchangeRateProvider.cs", - "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Services\\ExchangeRateProvider.cs", - "RelativeDocumentMoniker": "Services\\ExchangeRateProvider.cs", - "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Services\\ExchangeRateProvider.cs", - "RelativeToolTip": "Services\\ExchangeRateProvider.cs", - "ViewState": "AgIAAAAAAAAAAAAAAAAAAAwAAABBAAAAAAAAAA==", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", - "WhenOpened": "2025-08-02T12:03:38.764Z", + "WhenOpened": "2025-08-02T16:29:16.461Z", "EditorCaption": "" }, { @@ -170,22 +161,9 @@ "RelativeDocumentMoniker": "Startup.cs", "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Startup.cs", "RelativeToolTip": "Startup.cs", - "ViewState": "AgIAAAAAAAAAAAAAAAAAAAcAAAAvAAAAAAAAAA==", + "ViewState": "AgIAAAAAAAAAAAAAAAAAACMAAABfAAAAAAAAAA==", "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", - "WhenOpened": "2025-08-02T11:58:17.871Z", - "EditorCaption": "" - }, - { - "$type": "Document", - "DocumentIndex": 4, - "Title": "CzeExchangeRate.cs", - "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Models\\Countries\\CZE\\CzeExchangeRate.cs", - "RelativeDocumentMoniker": "Models\\Countries\\CZE\\CzeExchangeRate.cs", - "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Models\\Countries\\CZE\\CzeExchangeRate.cs", - "RelativeToolTip": "Models\\Countries\\CZE\\CzeExchangeRate.cs", - "ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", - "WhenOpened": "2025-08-02T11:56:45.274Z", + "WhenOpened": "2025-08-02T16:21:45.442Z", "EditorCaption": "" } ] @@ -216,12 +194,12 @@ "Orientation": 0, "VerticalTabListWidth": 256, "FloatingWindowState": { - "Id": "cb493138-76e1-44ef-8f8d-b28f8bf2dfa9", + "Id": "64a7b947-f702-48e2-8c80-163b64d61eba", "Display": 1, - "X": 775, - "Y": 303, - "Width": 1276, - "Height": 880, + "X": 1007, + "Y": -40, + "Width": 858, + "Height": 742, "WindowState": 0 }, "DocumentGroups": [ @@ -232,14 +210,74 @@ { "$type": "Document", "DocumentIndex": 3, - "Title": "appsettings.json", - "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\appsettings.json", - "RelativeDocumentMoniker": "appsettings.json", - "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\appsettings.json", - "RelativeToolTip": "appsettings.json", - "ViewState": "AgIAAAMAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAA==", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.001642|", - "WhenOpened": "2025-08-02T11:56:53.158Z", + "Title": "ExchangeRate.cs", + "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Models\\ExchangeRate.cs", + "RelativeDocumentMoniker": "Models\\ExchangeRate.cs", + "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Models\\ExchangeRate.cs", + "RelativeToolTip": "Models\\ExchangeRate.cs", + "ViewState": "AgIAAAAAAAAAAAAAAAAAAA0AAAAiAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", + "WhenOpened": "2025-08-02T16:05:21.722Z", + "EditorCaption": "" + }, + { + "$type": "Document", + "DocumentIndex": 4, + "Title": "CzeExchangeRateTable.cs", + "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Models\\Countries\\CZE\\CzeExchangeRateTable.cs", + "RelativeDocumentMoniker": "Models\\Countries\\CZE\\CzeExchangeRateTable.cs", + "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Models\\Countries\\CZE\\CzeExchangeRateTable.cs", + "RelativeToolTip": "Models\\Countries\\CZE\\CzeExchangeRateTable.cs", + "ViewState": "AgIAAAAAAAAAAAAAAAAAAAsAAAAfAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", + "WhenOpened": "2025-08-02T16:04:44.734Z", + "EditorCaption": "" + }, + { + "$type": "Document", + "DocumentIndex": 5, + "Title": "CzeExchangeRate.cs", + "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Models\\Countries\\CZE\\CzeExchangeRate.cs", + "RelativeDocumentMoniker": "Models\\Countries\\CZE\\CzeExchangeRate.cs", + "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Models\\Countries\\CZE\\CzeExchangeRate.cs", + "RelativeToolTip": "Models\\Countries\\CZE\\CzeExchangeRate.cs", + "ViewState": "AgIAAAAAAAAAAAAAAAAAAAQAAAAcAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", + "WhenOpened": "2025-08-02T16:03:19.974Z", + "EditorCaption": "" + } + ] + } + ] + }, + { + "Orientation": 0, + "VerticalTabListWidth": 256, + "FloatingWindowState": { + "Id": "4992753e-af97-424d-a7c0-074954eebdd2", + "Display": 1, + "X": 682, + "Y": 82, + "Width": 1313, + "Height": 880, + "WindowState": 2 + }, + "DocumentGroups": [ + { + "DockedWidth": 200, + "SelectedChildIndex": 0, + "Children": [ + { + "$type": "Document", + "DocumentIndex": 2, + "Title": "CzeExchangeRateProvider.cs", + "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Services\\Countries\\CZE\\CzeExchangeRateProvider.cs", + "RelativeDocumentMoniker": "Services\\Countries\\CZE\\CzeExchangeRateProvider.cs", + "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Services\\Countries\\CZE\\CzeExchangeRateProvider.cs", + "RelativeToolTip": "Services\\Countries\\CZE\\CzeExchangeRateProvider.cs", + "ViewState": "AgIAABQAAAAAAAAAAAAEwCkAAAAAAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", + "WhenOpened": "2025-08-02T15:54:59.523Z", "EditorCaption": "" } ] diff --git a/jobs/Backend/Task/.vs/ExchangeRateUpdater/v17/TestStore/0/000.testlog b/jobs/Backend/Task/.vs/ExchangeRateUpdater/v17/TestStore/0/000.testlog new file mode 100644 index 0000000000000000000000000000000000000000..4f7842c0bae73520ea63f84d5a8d7a74f4ce1b57 GIT binary patch literal 188001 zcmeHw3zQtydFG6S2oJ}0j31bX8HYJYfbOWO?ybiiS;s~r3B*I_=z$3u8ScGxuW0D$ z7TrBUlJ_{)IU9m;Y$OuJ28@BT0b{U2ctrvGc-!LG>y2?64ZCKWS4d8>%f=@L>^0t9 zkiCCZ&!}ITQT6nUOwIl?cF{B4qpG_9cmMj|-^V{*T3ssY4GVgDdOi;SkG}rbaXmeI zd&l5ae3)15x5mFn^xu}l#CtFGoVuXeA6g2&1tT^!A>oelNnlAz9GhE01fk~}yV~!D z)DE2>B-HaO$?!3w5dS&EnPTbZNAZ zE3O>bTH0~-r?1ty<)WxJ7P6~rzp7*}`R;Xp*?X|3=kWi^_l*4(zN%js8XVqO)P>^k z#rdMXEVE@)XIDJEPwZT-v)TOcio6;f(YaD_cuS^q?Wi0c(Ki-{uglBg@N%K9*ST=G z@k*NRVOVBz!z;8L-7=ihC2zo*{G$V5Hv8nH|Iq!lKi|FUk?8g>m)B`k8rV<}nNso3 z=Y6HO{kQkm-mJ`fX7hsh5J_V}_0sh_iX}ZVFqF?`;h)IlbH#xRHAo_(1}@P%t`ON# zy;fuj*EpLtjem4~sgTKS*}S;r=gpfo|FBntFZ6Dn?mxM>Z~QOU7BbsF?r>2BeQ8E6 z?z;kn0hhKc7_g%6#eGAgS&*(?meZr9g2*oJTRSSV83k9oArBH*<}0lNH}s_zzRv^Q zdbP88Dp-hF{O5O9cg`X_r`%$yc;NA~&t9J&EhxPrQ&jnFx}d|e&yKfJW$T06U;fTi ze_!<`Me~$fyJ~g1w_`=~>*IdoGQx-@az=SklGD z;D+0-Qj%~g43Qe_2iG&4sE{%jjDkSvu-Z=*7omjrG_yV8DC!YPeFkQO8-2vG9IE2W zscnqnOl^!+x&&LIxT=sJS*J(xC4E6Ij5kIxHUie<;|&u3rgFji|Lvo=OKQ*aS$t$2 z%Uu$Qv~`z!qHULS0FoG^{TDuW&FNii8P}GS+DsDLg?$ik6%gUOni!kQKuAHUG}Zn< z1z=L!f*4_ZVzEFG;RYTYq+!%h$6~x;%VcJ1%d}a_wT%-m(khY37AyT%-+4c7lsUCg zB9XRk6sK*Y92FWd9#7cvv-iHQi|t`T&V84KL_3rc!6evECJc$gbfBeganHaLg#r9| zXed}&HY3tynn=zZ;t1wS>9NS#YS1(#D8I+PX0o!^TJsiYUx+#u>kSV8OiUdmoP0fh&b3f%X(}RcI06 zSQ1`2wjUVI8RZb02|~&}M-j&`PAr=-qHR|aCPFK;95-|WJK7Sx)pMr~yDWilf&y<_ zY~5P(=8g~@IUF_UX>%av$FTs1ho4Lo>sCDt4X8ng$kLB+I`j4B~CXEL+}l|lx-5aW}>b-x{h4hS}mg5PFa052DOSJTEhW0W$05$d?RS!Yr~WZ8n#<(YfWlH&y4i;%%Jp+YV~z2vu10v zMP}dq*t1VO*oDmCL1`!z;4Nu5IE-0Dh7NdF+;$YWUXINmLm{cXrOZ!s?-p&S<rLCi- zp?t0+GLW`79G5OE*=9fe+zBrqe0}eK`PK6JJ?uK-GyEqLAAGEqje>u#n9u53QY!xA zfvrzHxuzMUa61#JvX%^5{Nu!XFZG;tVzpnxZ5v}n3SR*RO#q=Yz$bC-0OG~I2M=kh zgXeg>M)ikzv^?k3&$iK)J-h2q`v)(&VEu-bE8*(A=ip<}*EP`+z4H9RfBDDp z7yo@E**t2fp`}i@I1*n{gp*4O&`Ih?y{tl2EXK0E`0I z*^ag?-GPbRu%eOi8(reU$yPM_K+R4zBPZ{lSj&&pmhk+Ye>Cx8Q?do>Y%SxHx2)xt zZvPK|wY%Bf=m?$MG1QHW5T8>B+1lWN8$X1cujhhVbeVLd+kw&2u$Ix&I$Fj>hu3o6 zlNS^ryILAplP|^D*+u8}MPVk$=e^MD_Vx9NQs4AU|MoOV2X+WtMJCtxyUVQ&W`4wPjOVO!L59cD`oUSwjkw!W5eXgFHN^Cnr#_ye^=BQASuOUO^W zVCF^F4Fxet%3R7XY<~N^o0`?^@3NLAPMsYkUV{nW z8LcBZh6&&JIUiM3#9^g4;amBA_x~1Rz&?A+_7Ap$I7!Vy`K^QE0vZi!n4B{JJT{YAW;v^aW@_TMR=~LZ+ILS#mEi0(oYY5^w1>)uqY)upEFJ-?I zgShh5T_?7KI7wcp^o49n{U3+5?Km6?_2irl6 zj$JQS@*cfG03Ex2bnHe3u=Ql}CYs$xDMzoMV;4nhu&1KCR;^>GiP5p^=_$v_;<)oM z4(r!y9}&c<0Wmsu2x92qodwI~*lA*P?4S!BHF3H@TODIcYlfwmb8XI6W3mHFG0~hz zy*Yu79fCOZAV$XyL7aLJW5y0aoOTdn#tt)f(ctJ*r=2llhagTrh|#e_b0YPc7#%wV zaq2;gjvazH?I1?S4sGJ}+r*f$Lrt7^O^m}+P!p$L6Qg5?nmF~E7#%wVaoRyV9mg&S z4N3XcE7orN>*hhu9Zkhda*_@PIoGCQ#|}VrIN}5#nE5#a6c@*uq{O$C7if#Qo(=pj z)a9NQp#lQ2(6_ZGt+K#k*~*#W8#1dk@=^qz9{DQC7%%1J^sQhsgCt;Zps3GhEP z@%m0lU^*!y%~X6QoyfX50n{REe$|UFw#zyROG$ard!Kp@sZhFH*CJ>jU`ax(QvT?g%N}?V41nJOcT0uc_5LSfLY6mf ze&~Z&*6(R@LYDZ0V-Jiz|Fihhx83!(|Jrijz;Tg;SEama&wpOi&G>wmc}i+#2i_=C!`+BMT7d+LEs#LKY~oPhSEH7; zfbqrh9RaKs+>sqP3Jqe2tmSC&7f&XJ!|p_r7*46J{<(d3PrSHyHpCF0yhRKz?zr*? z?P5s6<5KSLyX)M~HnSLXL>A+Oj-;FhggU@F8FJv<32op(vpp;NeMI2|yCE^dU4s}R zdnS?qIV^^GW54;@|9K2~VQk7~lo#g1HNY+r2Pa;P%?5H~1GNRY`sK6Nwj(zQ+e^9U zoU0z|MuE80@j-<<#8*Ib#Er*^6uv{G=Q)xo2D-)Xj#6geFmjVq%KXC*mTY-_Tnt-Y zp9L}0?1>gJJiqTg332o9_Ojmfe7CG!cU^4lTx^ z#nw~|WiR!dx&Z8dn^Wc4G$OSEmN3@=4rBpDx?ae9r7X*6s*sIlXf#^_e!XQmJqlcM z*`p(C@uns|?!|rMKc>6gqd8EDY&0t#-E;`&y)N!c(?pdT%TV)JAY`M#0d465adRje z*=SG`>q|2-U7ENhOkV>rveDpxwp5uD&4nU05F^bff;d$mM#m28Gt*w5iH;q%d!)bJ z106fm#A(;W=-8nqPQNBb#||}d+BGq@K%*v3zb0;O(}+WCn6bl*9cJu26PB0WL3?g$ zu14;td(?!S`z{NKb|@u+Nkd!{hQwhy(9*X!oTS>%EXH|gC{zg9j7XPhA~|!2BbX~C z)Elb6jcNwx#Y7&l0}vmM*gcBmb}1#dn~r0bq&pc~pha$rUe|F4)ynPa+KIm`UftF( zw_FtU#zJ;=aplO?(vG-0((ye_xdRQ)p3kTy1aupO9&|s40WEmsg5a?QdMd&1nL)v~Y4mk^Bwx}o9#)A=w%D;w zZ%!|?i2J3pj{D>GeU2^AQ;GYFU5UH17U&N4b&I9eBIqBV^LJ0Q2s&LY(68_L*hu}J zrf7kVK3H#o{$QW~p_coGo~R>$X zq$KpEl6}4KY zy6ePtO`Ie-gBhUS&EwtGL-}l0t5PPPD-K+!bGndG1DEI>SBUJWUMn(%Yn)BTt}lB| zwt%<96|iRu8xPahc!-W2I(F#Tp<@T;bR0yE46p@b?;JSY{$Z~OUqEwW)8<+98+TXu zpNJC=REmGkU%{R&)Wqr6#LY?7noW$39kzg{sRbMzJJiJK*TmSfg*{u?vxONubnLFz zxn;i6(3TPUQo~$s9_Y=-uyz77cBqL{uZhvILlCDO#OTfN z3Vsz|K6Q^Diqhp64 zPCJN??$~{EeoxQd-l!ivK6FIn0BjA^(l8)RIm#wR3yUb%7{UdNanph1+E~?oTY4eo z3Wyw-wuvh&pM=u5L~y0-fZ4{Cc732)^@_>om9NTV^^j24>h-#?Eu-`XT`Uy`*6Y#* zx$r_b7I);-)$2?7f-b^uV3ctoqqpf|JdCGO_|}6AXSAh$Mq9NwG&sDms0+p6i}OW& zS!Tk4iW^DZqx0 z3@}w39~}@us1l9Sdg6#5tS*`KUmiH}ADoj6yYcQg-aqHjo-Q^kw>--;z_lW|Ly1j+ zN`?E#L#R$)nP`@|xRw@+pL z?e{Dld-k@GpH~+p1_V5z=FLBE*%8)DB@o5$bs^p^CbMYmdPbktuF7BZZ>%?Ncj7h{9Hg^zSoP5C`HpY*A2%n8?)l-f)Z+Xd_T$R`17 zTm@jl*n${gd}6Uc5#a_N9He2?PzN{?8_bC~Vig_GoS0MQ1V$p!oJhMlar8)J`z-P# z@x^lPk7Hi7>Ap4AU;wYu5zP&*3B`9SKYGzzIG5NiGE>-4aYbi zyD`ofVndra?KUxn*f7L~A+|}PUl?LTbK=;U6BuGcb0Y2L1cunqoJgrTfg!eD!0zGy zm1`tXNW7XV;?+8cF~o+NIHj5xtJqKz&m|sr9F&BPUD_QxYu_Se1BZ^cb=l4^e#Kqs!}DB>8# ziDffJwCzg5L}-PUP2WS|-IdUw-eyFV4T9y--FPWpfw$oQ0CmMCk@D34jTW zcu>l%Y>!LjJLT8b+;Nc2U5?8M_@ek1Moe4XZb+ez)BJ1~jQtoP(wG|;pKn;Ir zi;!s8$;1&_5U!xJU2z{e2fXs4_dfO7Q(ee9$w{EH&-XYq68JwduhJ#x`Y*033U5Go$Nvi!Y zZ^N^gZ%80qhj?6BB-9{o?ZI3P+YJR}R$bh&wGtm@QQTkbO57W_hN7e|%8wRS=QeH_ zns|f{&l^tNor5mM3jc!y4Yx!jk|T&50cZ0h3IH#x$B+@py?z$CS*>6ZmseO6!PI zDh87csi+BvGQ$KFo^1^Cxx@WXIq>*V(q*1y@mLJw*45!@8u2)Q76C0WH{W}A7XqTv zlibq*ao|A=LuVWqO+(#)fLxexKv4?|rd=q@v#A4*?9lNvkvT9jokG|Cv|0{O3kX+L%Vk3|^t z1Ga~XoF6|3sKq?tmp{7ZvIm}o^=cSA1b=Fcf05|FEr*Hs?w+6Q6fKTPB*f$5LxtV- z{wJP(?u3^QzWMl}6F*tMr(KVqefFh!C9*4X+cJfGE*c_p_Srv~_~6(BqtE{={&e;a z9~rx-8JciW6BpQW-SLkT@4eJ>)```A#|EI;R!G8tgW3okn}il<1s+NVd<9Fmw%dX3 zi0`!KuSGj5y2(#QhvTC|Z~gx6xo3C1EvaHgPTv0gr`MhVwiL!g*%ky73^S9lsB?n8_1rlsc|NSO;m0|TYQ@@Y%Ye6y>Td4%*X=CQfpaYCnTnYCJSBrrfm|fp|3pggFdJb_8>!^q8#zx2}ngl3GDcoKj8P zk~XS=xOClJdt=c*{Ou(N+d+)OQP8o=F78`9Dzg~{bIdp7Va)t8C?^7q5&9CcAwCcE z=FP`W6XS3cEVWE~sU;3a!8mbBaY}9CI*5;6J8^3lG<6krDo z@_taB&4K_rIBe*hh${UmD1lI%&WsID- z=ki;P?^n-B2GZ*JouD*IJ@35uH{bicF1BRIxJ9AE5;|TXsUnPt%J6~8#`l>PQO}Bz z4iw2c)CoM{LZ>1H{yYIKYlCZ+#C1B%6ysj8 zxOr@%r*w5Oexovf|7)MXeVKaJhOr+FXa0Gdw>1)JykW+k{`J8ao^Brwgiv*WjQ@sDu{Q1pU@C?!~_sVY|c8#FOi z_Ms+Dy(Y#45P~@EAjSj`niDBCC+e|tOaLK>QxD>!CxF^RY)R5O<%hN{47zC)NOF=+ zb4PKs9+N$DgGIF=s9ev&)Pu@nZ|yoQo@+AC-1_BTwu3lHBBcEK4>tT^Hy}=Ol1{TN zaZ&^`BXbVKHMvG@`*jdku7Bl$hvQ_&+k4;s#t}elDfkwQ*wlnD|4jKLuq1_!Rc;9p zgdPmeLMmYFJH&1mZF8z%z#QQ|3}w|W%%GzJhO&Ag^A)sq8%?9(RLm7N%*Z1?6EMHk zm5hWgg+553bPzQi+9n>|I^F?3^_X6)9Z6H|NTnf8+&q7{)_1tbJoa|1i8t+h>*jV% z%o9u)z%d4>iI1Ho#$*S^iPIh@u3)kQL7a9FW3mI|#Ho)HW3mH5oO%#XC)tsN%n2Ez zL~e^-2ka2d{Wvqv80C3XXkd8IJg^vb+`t0Tl_>5B1S`HTym*vp62_?Ia^w1TFFpyG z6Ea54ps?fCk4YG#I+1mA#HM+mZSOOmJ^5GdvQ9$ggp5%$A?r>VqdF0MbCjb+@cXSt zPHh)_5;7-bjG76-Cu5B2MBL5sfEID{^Dh5NySS5(IU!@z^u*m6V^jx%Ztk#Oi9Am{2OhPub z?w>9@zFoFSD5T2eU%$^FPgO$McEnTFfplAZtd^bof#c`D=k{j4Cg8=H;AEPbuj$kU z5s#|p2oLBT0~xVBx*;XNY6bL7)KV@?8i)wapel^|fpp*lfdhG778n79Z!loMwiOJ4 z>VHMpd~ye5#vCi1|AUHId>QhjewhGM!P|1 zdv?IwhOO6HD0P^%D9UX1{@L)FQ@-AXaBRn+hC|Mr88f#;WauEpS^k#K@AFg|4GwQi)sjr)Zs?E{|~;rwxO;dI)m+^K|GtALs% z^tCGw{$-1DOc%}DOOO7Kp1Mkyg61vyV4ddePo2s)x}hcuY-3Z;17yZ*yi5WKv=RXj z;kn8S0&M{ya|g(*QjvQ;Oj~*FVTgYsK z=*a*O-s?*b8||}h zj>8G7ZrOhyCOdH6^1Cs_hRF_$PNY6M(E#GsGLUFYJ|;Ub*)anl>0{^ERdDh>c5kP> zdmAU;W8-T|8($kH8ey^nHF4@SF(x|@#AydHCOgoaNU1qdj}zlC7zALKg3oyC$Ps3ZQ^v=#0?oc?6E@-ryj)EV}~G4 zCx|iaj5cxlZQ^>I7<=qc6Q^GjW7-+h&X{%<4B}v}2}9yA4IyOT;t=#IFTI2I+%&JJ zXK&2I*f|-a2|4#&7831HD7ru5Nvi$KVo;rLDA6v|=S!Dq2+2V-&JjS&Ej?yK4QfM( z?T(8MEsS%NXim%}b7DGa=g?B{Ef_>^O-Q(-d=gla636D25JBkq#xB3G`R((%0dbO( z09SfQ?GPfzgnFJ!sG=_6+GB(Zrnt?F6pGeCT$?B!P1-;Z&m|D!qh321wov45KqUk!zwncV#6vntYU*s^eaSmRIe48!ZpsOP3w0QBll|d zLk5eF-Kl1q#}2F5uqZz5Me$h0hH>KASNgN8i5oyXohml8iP0uTn;30kw25`0n91js z1p`*}y|`~^G+P=i=w&%QS}KU6?~&b=?rh>D8DUI2<3PdL9Vm$P#Ap+z-X?AYF{Yg{ z*^%~S2ad7A0-*F40Abo0HF4TC@pRJ8F4s6I{<3%E*h33T*Hq>`vw1;$i0YOXjJqq{T&>IPEqu zw(O%hkxp}>Q4?cl2WsNfYhnzsA&Aos;^~Cgu!;>GyLW+OS1$mn6u$K!t89I6`^%N} zx8JjL?AhB!eqOoiaqskaT7&-a9XI$8{;W+vnHT>q+34W%@O>{o-_6_!oKK8oj8l>^ zc3H)S$qv-Szk5yG5GTfD2Sz8-8J%bVF(x|@#OVZaV~7ot9T=TRdvpSm9jJ-ZuZc0) zfton=ni!KE2;#JZ7?T}nPNdYFz+?wb080A=AWU{(w{z;dog3oBnCw7vBAw<$1Bfx% zfgnyNh%woL=0xhviAE6P@DvQOr9H%k!&A_lNWVFO$qv-SY1hP<>_APNQcaA>4or4n zvIECh;TWs6qjIYLq1hgu(hw)cWCxlP=`<%AK#a)_1aUe++!!au&JK)Dq&+%;ogJu& z)31rKvja78>NPPYI}pTa2Qel)(40uAIf2OzOm<+h1Ct$??7(D)1z)835d9Q=z0`B+ zf@*)rxJ3iy5?3)zgfRh;@H6p!riB{{YNS)`w;bvOo^XjWG$d5moY;YH5my?AxuI + { + options.TimestampFormat = "[HH:mm:ss] "; + options.IncludeScopes = false; + options.SingleLine = true; + options.UseUtcTimestamp = false; + }); }) .ConfigureServices((context, services) => { From 3a33c1fac28c3293dfc5c98a59a35dfa87bd0c00 Mon Sep 17 00:00:00 2001 From: jdiazbello Date: Sat, 2 Aug 2025 18:41:07 +0200 Subject: [PATCH 07/11] Improve Test --- .../CzeExchangeRateProviderTests.cs | 2 +- jobs/Backend/Task/appsettings.dev.json | 9 +++++++-- jobs/Backend/Task/appsettings.json | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/jobs/Backend/ExchangeRateUpdater.Tests/CzeExchangeRateProviderTests.cs b/jobs/Backend/ExchangeRateUpdater.Tests/CzeExchangeRateProviderTests.cs index 845746416..5eb1babee 100644 --- a/jobs/Backend/ExchangeRateUpdater.Tests/CzeExchangeRateProviderTests.cs +++ b/jobs/Backend/ExchangeRateUpdater.Tests/CzeExchangeRateProviderTests.cs @@ -97,7 +97,7 @@ public async Task GetExchangeRates_FetchesFromRemoteAndCaches_WhenNoValidCache() { Rates = new List { - new() { Code = "USD", Amount = 1, RateRaw = "22,0" }, + new() { Code = "USD", Amount = 2, RateRaw = "44,0" }, new() { Code = "EUR", Amount = 1, RateRaw = "24.0" }, } } diff --git a/jobs/Backend/Task/appsettings.dev.json b/jobs/Backend/Task/appsettings.dev.json index 3dea5c7ac..b96da3510 100644 --- a/jobs/Backend/Task/appsettings.dev.json +++ b/jobs/Backend/Task/appsettings.dev.json @@ -6,10 +6,15 @@ "Microsoft.Hosting.Lifetime": "Information" } }, - + "ExchangeProviders": { + "CZE": { + "BaseUrl": "https://www.cnb.cz/cs/financni_trhy/devizovy_trh/kurzy_devizoveho_trhu/denni_kurz.xml", + "TtlInSeconds": 60, + "UpdateHourInLocalTime": "14:30:00" + } + }, "CacheSettings": { "Provider": "File", // "Redis" - "RedisConfiguration": "localhost:6379", "FileCachePath": "../../../FileCache/filecache.json" } } \ No newline at end of file diff --git a/jobs/Backend/Task/appsettings.json b/jobs/Backend/Task/appsettings.json index 8ce93f6e7..99c504f2c 100644 --- a/jobs/Backend/Task/appsettings.json +++ b/jobs/Backend/Task/appsettings.json @@ -14,7 +14,7 @@ } }, "CacheSettings": { - "Provider": "File", //"Redis" + "Provider": "Redis", "RedisConfiguration": "localhost:6379" } } \ No newline at end of file From 2e24bbeeb4f9528f919bc4d94ea78907fcfd8dc1 Mon Sep 17 00:00:00 2001 From: jdiazbello Date: Sun, 3 Aug 2025 08:46:53 +0200 Subject: [PATCH 08/11] Update gitignroe --- .gitignore | 1 + .../v17/TestStore/0/000.testlog | Bin 188001 -> 0 bytes 2 files changed, 1 insertion(+) delete mode 100644 jobs/Backend/Task/.vs/ExchangeRateUpdater/v17/TestStore/0/000.testlog diff --git a/.gitignore b/.gitignore index f97af7241..e5b205537 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ npm-debug.log *.v2 /jobs/Backend/Task/FileCache *.bin +/jobs/Backend/Task/.vs/ExchangeRateUpdater/v17/TestStore/0/000.testlog diff --git a/jobs/Backend/Task/.vs/ExchangeRateUpdater/v17/TestStore/0/000.testlog b/jobs/Backend/Task/.vs/ExchangeRateUpdater/v17/TestStore/0/000.testlog deleted file mode 100644 index 4f7842c0bae73520ea63f84d5a8d7a74f4ce1b57..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 188001 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 Date: Sun, 3 Aug 2025 08:47:17 +0200 Subject: [PATCH 09/11] update gitingore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index e5b205537..dc1332829 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,4 @@ npm-debug.log /jobs/Backend/Task/FileCache *.bin /jobs/Backend/Task/.vs/ExchangeRateUpdater/v17/TestStore/0/000.testlog +/.vs From f55eea5efcd5a99f32cd4478af84de476d455c84 Mon Sep 17 00:00:00 2001 From: jdiazbello Date: Sun, 3 Aug 2025 08:53:05 +0200 Subject: [PATCH 10/11] Improve GetUpdateHourInUTC and use TimeZoneConverter to convet in all platforms --- jobs/Backend/Task/App.cs | 4 +++- jobs/Backend/Task/ExchangeRateUpdater.csproj | 1 + .../Countries/CZE/CzeExchangeRateProvider.cs | 23 +++++++++++++------ 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/jobs/Backend/Task/App.cs b/jobs/Backend/Task/App.cs index 50ee75dee..67ed6112c 100644 --- a/jobs/Backend/Task/App.cs +++ b/jobs/Backend/Task/App.cs @@ -28,6 +28,8 @@ public class App new Currency("XYZ") }; + private static readonly CountryIsoAlpha3 country = CountryIsoAlpha3.CZE; + public App( ILogger logger, TextWriter output, @@ -43,7 +45,7 @@ public async Task Run() try { _logger.LogInformation("Application started execution."); - var provider = _factory.CreateProvider(CountryIsoAlpha3.CZE); + var provider = _factory.CreateProvider(country); var rates = await provider.GetExchangeRates(currencies); var count = rates.Count(); _logger.LogInformation("Successfully retrieved {Count} exchange rates.", count); diff --git a/jobs/Backend/Task/ExchangeRateUpdater.csproj b/jobs/Backend/Task/ExchangeRateUpdater.csproj index 74d2e3fed..9c3ac81e0 100644 --- a/jobs/Backend/Task/ExchangeRateUpdater.csproj +++ b/jobs/Backend/Task/ExchangeRateUpdater.csproj @@ -30,6 +30,7 @@ + diff --git a/jobs/Backend/Task/Services/Countries/CZE/CzeExchangeRateProvider.cs b/jobs/Backend/Task/Services/Countries/CZE/CzeExchangeRateProvider.cs index dbddbb9dc..80250f117 100644 --- a/jobs/Backend/Task/Services/Countries/CZE/CzeExchangeRateProvider.cs +++ b/jobs/Backend/Task/Services/Countries/CZE/CzeExchangeRateProvider.cs @@ -12,6 +12,7 @@ using ExchangeRateUpdater.Models.Countries.CZE; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using TimeZoneConverter; namespace ExchangeRateUpdater.Services.Countries.CZE; @@ -159,12 +160,20 @@ private IEnumerable MapToExchangeRates( private DateTimeOffset GetUpdateHourInUTC() { - TimeSpan time = TimeSpan.ParseExact(_settings.UpdateHourInLocalTime, "c", CultureInfo.InvariantCulture); - var now = _dateTimeProvider.UtcNow; - var localDate = TimeZoneInfo.ConvertTime(now, TimeZoneInfo.FindSystemTimeZoneById(TimeZone)).Date; - DateTime localDateTime = localDate.Add(time); - var timeZone = TimeZoneInfo.FindSystemTimeZoneById(TimeZone); - DateTimeOffset czechDateTimeOffset = new(localDateTime, timeZone.GetUtcOffset(localDateTime)); - return czechDateTimeOffset.ToUniversalTime(); + TimeSpan localUpdateTime = TimeSpan.ParseExact( + _settings.UpdateHourInLocalTime, + "c", + CultureInfo.InvariantCulture); + + var timeZoneInfo = TZConvert.GetTimeZoneInfo(TimeZone); + + var currentUtc = _dateTimeProvider.UtcNow; + var localDate = TimeZoneInfo.ConvertTime(currentUtc, timeZoneInfo).Date; + + DateTime localDateTime = localDate.Add(localUpdateTime); + + var localDateTimeOffset = new DateTimeOffset(localDateTime, timeZoneInfo.GetUtcOffset(localDateTime)); + + return localDateTimeOffset.ToUniversalTime(); } } From fd65286ad087f77b6340771c1570a14cd08789d5 Mon Sep 17 00:00:00 2001 From: jdiazbello Date: Sun, 3 Aug 2025 09:26:07 +0200 Subject: [PATCH 11/11] Add ReadMe --- jobs/Backend/Task/ReadMe.md | 111 ++++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 jobs/Backend/Task/ReadMe.md diff --git a/jobs/Backend/Task/ReadMe.md b/jobs/Backend/Task/ReadMe.md new file mode 100644 index 000000000..0b553f18b --- /dev/null +++ b/jobs/Backend/Task/ReadMe.md @@ -0,0 +1,111 @@ +# Exchange Rate Provider - Modular & Extensible System + +This project introduces a modular system for retrieving and managing exchange rates from multiple countries, with a focus on clean architecture, environment configuration, and maintainability. The initial implementation supports only the **Czech Republic**, but is fully extensible to other countries. + +--- + +## Architecture Overview + +A **factory pattern** has been introduced to encapsulate the logic of retrieving exchange rates per country. Currently, only the Czech Republic (CZE) is implemented, but support for other countries can be easily added by implementing the `IExchangeRateProvider` interface. + +--- + +## Dependency Injection + +The project has been updated to use dependency injection for all services and components, enabling better testability, separation of concerns, and integration with ASP.NET Core or other DI containers. + +--- + +## Logging + +Logging has been configured by default to output to the console. This configuration is environment-specific and can be adjusted for other targets such as files, cloud logging platforms, or databases depending on the deployment context. + +--- + +## Environment Configuration + +Environment-specific configuration has been implemented, starting with the `Development` environment. The `appsettings.Development.json` file includes: + +- Minimal log configuration +- Exchange rate provider settings (currently for CZE only), including: + - Source URL + - Minimum cache duration + - Local update time +- Cache configuration (in development, a JSON file-based cache is used) + +For production environments, the configuration can be easily adapted to use more robust systems such as **Redis** for caching. + +--- + +## Caching + +A caching layer has been added to reduce the number of calls to the **Czech National Bank (CNB)** API. This prevents unnecessary requests and conserves network bandwidth. Cached values are stored with timestamps to determine validity based on the update time. + +--- + +## Time Management (UTC Standardization) + +To handle time zones correctly and consistently across environments, all timestamps are transformed and stored in **UTC**. This standardization helps ensure consistency in time comparisons, regardless of local server configuration. + +A custom interface `IDateTimeProvider` has been introduced to abstract UTC time retrieval, aiding unit testing and mocking. + +Because the `appsettings` uses **local time** strings (e.g., `"08:00:00"`), the [TimeZoneConverter](https://www.nuget.org/packages/TimeZoneConverter) NuGet package has been installed. This enables reliable conversion of the time zone `"Central European Standard Time"` into platform-compatible `TimeZoneInfo` objects, ensuring support across Windows and Linux environments. + +--- + +## Unit Testing + +Unit tests have been added to validate core functionality such as: +- Time zone conversions +- Cache validity checks +- Factory provider resolution +- Rate retrieval and mapping + +These tests ensure the system behaves correctly and can be safely extended. + +--- + +## Adding a New Country + +To support a new country's exchange rates: + +1. **Add it to the enum**: + Extend `CountryIsoAlpha3` with the new country code. + +2. **Implement the provider**: + Create a new class that implements `IExchangeRateProvider`. + +3. **Register in the factory**: + Add your new implementation to the `ExchangeRateProviderFactory`. + +4. **Add configuration**: + Extend the `ExchangeProviders` section in `appsettings.*.json` with required fields such as: + - URL + - Cache settings + - Update schedule + +5. **Create settings model**: + Define a `Settings` class matching the configuration structure for the new country. + +6. **Register services**: + Add service registration in the `AddExchangeProviders` method or module. + +--- + +## Project Highlights + +- Factory pattern for country-specific logic +- Dependency injection support +- Configurable per environment +- Unified UTC time handling +- Efficient caching +- Unit-tested architecture +- Easily extensible for additional countries + +--- + + +## Conclusion + +This architecture enables scalable and environment-aware handling of exchange rates for multiple countries, promoting clean design and operational flexibility. By adhering to SOLID principles, the system is easy to extend, test, and configure for real-world use. +