Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions jobs/Backend/Task/CnbClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using Polly;
using Polly.Retry;
using System;
using System.IO;
using System.Net.Http;

namespace ExchangeRateUpdater
{

internal class CnbClient : ICnbClient
{
private readonly CnbSettings _settings;
private readonly HttpClient _client = new();
private readonly RetryPolicy<string> _retryPolicy;

public CnbClient(CnbSettings settings)
{
_settings = settings;
_retryPolicy = Policy<string>
.Handle<HttpRequestException>()
.Or<IOException>()
.WaitAndRetry(_settings.RetryAttempts, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
}

public (bool Success, string RawData) GetRawDailyExchanges()
{
string rawData;

try
{
rawData = _retryPolicy.Execute(ReadRawData);
}
catch (HttpRequestException)
{
return (false, string.Empty);
}
catch (IOException)
{
return (false, string.Empty);
}

return (true, rawData);
}

private string ReadRawData()
{
using var request = new HttpRequestMessage(HttpMethod.Get, _settings.SourceUrl);
using var response = _client.Send(request);

response.EnsureSuccessStatusCode();

using var reader = new StreamReader(response.Content.ReadAsStream());
return reader.ReadToEnd();
}
}
}
5 changes: 5 additions & 0 deletions jobs/Backend/Task/CnbSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
namespace ExchangeRateUpdater
{
public record CnbSettings(string SourceUrl, string TargetCurrencyCode, int RetryAttempts);
}

26 changes: 26 additions & 0 deletions jobs/Backend/Task/ConfigurationReader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Microsoft.Extensions.Configuration;
using System;

namespace ExchangeRateUpdater
{
public class ConfigurationReader(IConfiguration config) : IConfigurationReader
{
private readonly IConfiguration _config = config;
private const string SectionName = "CnbSettings";

public CnbSettings ReadSettings()
{
var section = _config.GetSection("CnbSettings");
string url = section["SourceUrl"] ?? throw new InvalidOperationException($"{SectionName}:SourceUrl is missing in appsettings.json.");
string target = section["TargetCurrencyCode"] ?? "CZK";

if (!int.TryParse(section["RetryAttempts"], out int retryAttempts))
{
retryAttempts = 3; // Default fallback
}

return new CnbSettings(url, target, retryAttempts);
}
}
}

56 changes: 54 additions & 2 deletions jobs/Backend/Task/ExchangeRateProvider.cs
Original file line number Diff line number Diff line change
@@ -1,19 +1,71 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;

namespace ExchangeRateUpdater
{
public class ExchangeRateProvider
{
private readonly CnbSettings _settings;
private readonly Currency _targetCurrency;
private readonly ICnbClient _cnbClient;

public ExchangeRateProvider(CnbSettings settings, ICnbClient cnbClient)
{
_settings = settings;
_targetCurrency = new Currency(_settings.TargetCurrencyCode);
_cnbClient = cnbClient;
}

/// <summary>
/// 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.
/// </summary>
///
public IEnumerable<ExchangeRate> GetExchangeRates(IEnumerable<Currency> currencies)
{
return Enumerable.Empty<ExchangeRate>();
var requestedCurrencies = currencies.ToDictionary(c => c.Code, c => c, StringComparer.OrdinalIgnoreCase);

if (requestedCurrencies.Count == 0)
{
return [];

}

var (success, rawData) = _cnbClient.GetRawDailyExchanges();
if (!success)
{
return [];
}

var rates = ParseData(rawData, requestedCurrencies);
return rates;
}

private IEnumerable<ExchangeRate> ParseData(string rawData, Dictionary<string, Currency> requestedCurrencies)
{
var rates = new List<ExchangeRate>();
var lines = rawData.Split(["\n", "\r\n"], StringSplitOptions.RemoveEmptyEntries);

foreach (var line in lines.Skip(2))
{
var cols = line.Split('|');
if (cols.Length < 5) continue;

var code = cols[3].Trim();
if (requestedCurrencies.TryGetValue(code, out var sourceCurrency))
{
if (int.TryParse(cols[2].Trim(), out int amount) && amount > 0 &&
decimal.TryParse(cols[4].Trim(), NumberStyles.Any, CultureInfo.InvariantCulture, out decimal rateValue))
{
rates.Add(new ExchangeRate(sourceCurrency, _targetCurrency, rateValue / amount));
}
}
}
return rates;
}
}
}
14 changes: 13 additions & 1 deletion jobs/Backend/Task/ExchangeRateUpdater.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,19 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.7" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.7" />
<PackageReference Include="Polly" Version="8.6.6" />
</ItemGroup>

<ItemGroup>
<None Update="appSettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>

</Project>
7 changes: 7 additions & 0 deletions jobs/Backend/Task/ICnbClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace ExchangeRateUpdater
{
public interface ICnbClient
{
(bool Success, string RawData) GetRawDailyExchanges();
}
}
8 changes: 8 additions & 0 deletions jobs/Backend/Task/IConfigurationReader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace ExchangeRateUpdater
{
public interface IConfigurationReader
{
CnbSettings ReadSettings();
}
}

16 changes: 14 additions & 2 deletions jobs/Backend/Task/Program.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System;
using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace ExchangeRateUpdater
Expand All @@ -23,7 +25,17 @@ public static void Main(string[] args)
{
try
{
var provider = new ExchangeRateProvider();
var config = new ConfigurationBuilder()
.AddJsonFile(Path.Combine(AppContext.BaseDirectory, "appSettings.json"))
.Build();

var configurationReader = new ConfigurationReader(config);

var settings = configurationReader.ReadSettings();

var client = new CnbClient(settings);

var provider = new ExchangeRateProvider(settings, client);
var rates = provider.GetExchangeRates(currencies);

Console.WriteLine($"Successfully retrieved {rates.Count()} exchange rates:");
Expand Down
7 changes: 7 additions & 0 deletions jobs/Backend/Task/appSettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"CnbSettings": {
"SourceUrl": "https://www.cnb.cz/en/financial-markets/foreign-exchange-market/central-bank-exchange-rate-fixing/central-bank-exchange-rate-fixing/daily.txt",
"TargetCurrencyCode": "CZK",
"RetryAttempts": 3
}
}