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
17 changes: 17 additions & 0 deletions jobs/Backend/Task/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
bin/
obj/
*.user
*.suo
*.userosscache
*.sln.docstates

[Dd]ebug/
[Rr]elease/
bld/

.idea/
*.iml
.DS_Store

*.swp
*.swo
20 changes: 0 additions & 20 deletions jobs/Backend/Task/Currency.cs

This file was deleted.

23 changes: 0 additions & 23 deletions jobs/Backend/Task/ExchangeRate.cs

This file was deleted.

19 changes: 0 additions & 19 deletions jobs/Backend/Task/ExchangeRateProvider.cs

This file was deleted.

29 changes: 28 additions & 1 deletion jobs/Backend/Task/ExchangeRateUpdater.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,33 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>ExchangeRateUpdater</RootNamespace>
</PropertyGroup>

</Project>
<ItemGroup>
<Compile Remove="test/**" />
<None Remove="test/**" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.2" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.2" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.1" />
<PackageReference Include="Polly" Version="7.2.3" />
<PackageReference Include="Polly.Extensions.Http" Version="3.0.0" />
<PackageReference Include="System.Xml.ReaderWriter" Version="4.3.1" />
</ItemGroup>

<ItemGroup>
<Content Include="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>

</Project>
43 changes: 0 additions & 43 deletions jobs/Backend/Task/Program.cs

This file was deleted.

18 changes: 18 additions & 0 deletions jobs/Backend/Task/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Extensions.Http": "Warning",
"ExchangeRateUpdater": "Information"
}
},
"CnbApi": {
"BaseUrl": "https://www.cnb.cz/cs/financni-trhy/devizovy-trh/kurzy-devizoveho-trhu/kurzy-devizoveho-trhu/denni_kurz.xml",
"TimeoutSeconds": 30,
"RetryCount": 3,
"CircuitBreakerFailureThreshold": 5,
"CircuitBreakerDurationSeconds": 30
}
}

12 changes: 12 additions & 0 deletions jobs/Backend/Task/src/Configuration/CnbApiOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace ExchangeRateUpdater;

public class CnbApiOptions
{
public const string SectionName = "CnbApi";

public string BaseUrl { get; set; } = string.Empty;
public int TimeoutSeconds { get; set; }
public int RetryCount { get; set; }
public int CircuitBreakerFailureThreshold { get; set; }
public int CircuitBreakerDurationSeconds { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Polly;
using Polly.Extensions.Http;

namespace ExchangeRateUpdater;

public static class DependencyInjectionExtensions
{
public static IServiceCollection ConfigureServices(this IServiceCollection services, IConfiguration configuration)
{
services.Configure<CnbApiOptions>(configuration.GetSection(CnbApiOptions.SectionName));
services.AddScoped<ICnbResponseParser, CnbResponseParser>();

services.AddHttpClient<ICnbExchangeRateService, CnbExchangeRateService>()
.AddPolicyHandler((serviceProvider, _) => GetRetryPolicy(serviceProvider))
.AddPolicyHandler((serviceProvider, _) => GetCircuitBreakerPolicy(serviceProvider));

services.AddScoped<ExchangeRateProvider>();

return services;
}

private static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy(IServiceProvider serviceProvider)
{
var options = serviceProvider.GetRequiredService<IOptions<CnbApiOptions>>().Value;

return HttpPolicyExtensions
.HandleTransientHttpError()
.WaitAndRetryAsync(options.RetryCount, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
}

private static IAsyncPolicy<HttpResponseMessage> GetCircuitBreakerPolicy(IServiceProvider serviceProvider)
{
var options = serviceProvider.GetRequiredService<IOptions<CnbApiOptions>>().Value;

return HttpPolicyExtensions
.HandleTransientHttpError()
.CircuitBreakerAsync(options.CircuitBreakerFailureThreshold, TimeSpan.FromSeconds(options.CircuitBreakerDurationSeconds));
}
}
19 changes: 19 additions & 0 deletions jobs/Backend/Task/src/Constants/CnbConstants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace ExchangeRateUpdater;

public static class CnbConstants
{
public const string BaseCurrencyCode = "CZK";
public const string DateFormat = "dd.MM.yyyy";

// XML element and attribute names from CNB response (in Czech)
public const string XmlRootElementName = "kurzy";
public const string XmlRowElementName = "radek";
public const string DateAttributeName = "datum";
public const string CodeAttributeName = "kod";
public const string AmountAttributeName = "mnozstvi";
public const string RateAttributeName = "kurz";

// Culture and query parameters
public const string CzechCultureCode = "cs-CZ";
public const string DateQueryParameter = "date";
}
12 changes: 12 additions & 0 deletions jobs/Backend/Task/src/Exceptions/CnbApiException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace ExchangeRateUpdater;

public class CnbApiException : Exception
{
public CnbApiException(string message) : base(message)
{
}

public CnbApiException(string message, Exception innerException) : base(message, innerException)
{
}
}
21 changes: 21 additions & 0 deletions jobs/Backend/Task/src/Models/Currency.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace ExchangeRateUpdater;

public class Currency
{
public string Code { get; }

/// <summary>
/// Three-letter ISO 4217 code of the currency.
/// </summary>
public Currency(string code)
{
if (string.IsNullOrWhiteSpace(code) || code.Length != 3)
{
throw new ArgumentException($"Currency code must be exactly 3 characters, got '{code}'", nameof(code));
}

Code = code.ToUpperInvariant();
}

public override string ToString() => Code;
}
23 changes: 23 additions & 0 deletions jobs/Backend/Task/src/Models/ExchangeRate.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace ExchangeRateUpdater;

public class ExchangeRate
{
public Currency SourceCurrency { get; }
public Currency TargetCurrency { get; }
public decimal Value { get; }

public ExchangeRate(Currency sourceCurrency, Currency targetCurrency, decimal value)
{
SourceCurrency = sourceCurrency ?? throw new ArgumentNullException(nameof(sourceCurrency));
TargetCurrency = targetCurrency ?? throw new ArgumentNullException(nameof(targetCurrency));

if (value <= 0)
{
throw new ArgumentException("Exchange rate must be greater than zero", nameof(value));
}

Value = value;
}

public override string ToString() => $"{SourceCurrency}/{TargetCurrency}={Value}";
}
65 changes: 65 additions & 0 deletions jobs/Backend/Task/src/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace ExchangeRateUpdater;

public static class Program
{
private static readonly Currency[] 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("INR")
};

public static async Task Main()
{
var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.Build();

var services = new ServiceCollection();

services.AddLogging(builder =>
{
builder.AddConsole();
builder.AddConfiguration(configuration.GetSection("Logging"));
});

services.ConfigureServices(configuration);

using var serviceProvider = services.BuildServiceProvider();
var provider = serviceProvider.GetRequiredService<ExchangeRateProvider>();

try
{
var rates = await provider.GetExchangeRatesAsync(Currencies);
var ratesList = rates.ToList();

if (ratesList.Count == 0)
{
Console.WriteLine("No exchange rates were retrieved.");
return;
}

foreach (var rate in ratesList)
{
Console.WriteLine(rate.ToString());
}
}
catch (Exception ex)
{
Console.WriteLine($"Could not retrieve exchange rates: '{ex.Message}'.");
}

Console.ReadLine();
}
}
Loading