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
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.

8 changes: 0 additions & 8 deletions jobs/Backend/Task/ExchangeRateUpdater.csproj

This file was deleted.

22 changes: 0 additions & 22 deletions jobs/Backend/Task/ExchangeRateUpdater.sln

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using Microsoft.AspNetCore.Mvc;
using ExchangeRateUpdater.Application.Interfaces;

namespace ExchangeRateUpdater.Api.Controllers;

[ApiController]
[Route("api/[controller]")]
public class ExchangeRatesController : ControllerBase
{
private readonly IExchangeRateService _service;
private readonly ILogger<ExchangeRatesController> _logger;

public ExchangeRatesController(IExchangeRateService service, ILogger<ExchangeRatesController> logger)
{
_service = service;
_logger = logger;
}

[HttpGet]
public async Task<ActionResult> GetAll(CancellationToken ct)
{
_logger.LogInformation("GET /api/exchangerates");
var result = await _service.GetAllRatesAsync(ct);
return Ok(result);
}

[HttpGet("{currencyCode}")]
public async Task<ActionResult> Get(string currencyCode, CancellationToken ct)
{
_logger.LogInformation("GET /api/exchangerates/{Currency}", currencyCode);
var result = await _service.GetRateByCurrencyAsync(currencyCode, ct);
return result is null ? NotFound() : Ok(result);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.9.0" />
<PackageReference Include="Asp.Versioning.Mvc" Version="8.0.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\ExchangeRateUpdater.Application\ExchangeRateUpdater.Application.csproj" />
<ProjectReference Include="..\ExchangeRateUpdater.Infrastructure\ExchangeRateUpdater.Infrastructure.csproj" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using ExchangeRateUpdater.Application.Interfaces;
using ExchangeRateUpdater.Application.Services;
using ExchangeRateUpdater.Domain.Interfaces;
using ExchangeRateUpdater.Infrastructure.Http;
using Microsoft.OpenApi.Models;

var builder = WebApplication.CreateBuilder(args);

// Swagger
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

// Controllers
builder.Services.AddControllers();

// Clean Architecture DI
builder.Services.AddHttpClient<IExchangeRateRepository, CnbHttpExchangeRateRepository>();
builder.Services.AddScoped<IExchangeRateService, ExchangeRateService>();

var app = builder.Build();

app.UseSwagger();
app.UseSwaggerUI(options => options.SwaggerEndpoint("/swagger/v1/swagger.json", "v1"));

app.MapControllers();

app.Run();
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:5131",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "https://localhost:7012;http://localhost:5131",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
namespace ExchangeRateUpdater.Application.DTOs;

public record ExchangeRateDto(string CurrencyCode, decimal RateToCZK);
public record RatesResponse(List<ExchangeRateDto> Rates, int TotalCount, DateTime UpdatedAt);
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\ExchangeRateUpdater.Domain\ExchangeRateUpdater.Domain.csproj" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using ExchangeRateUpdater.Application.DTOs;

namespace ExchangeRateUpdater.Application.Interfaces;

public interface IExchangeRateService
{
Task<RatesResponse> GetAllRatesAsync(CancellationToken cancellationToken = default);
Task<ExchangeRateDto?> GetRateByCurrencyAsync(string currencyCode, CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using ExchangeRateUpdater.Application.DTOs;
using ExchangeRateUpdater.Application.Interfaces;
using ExchangeRateUpdater.Domain.Interfaces;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;

namespace ExchangeRateUpdater.Application.Services;

public class ExchangeRateService : IExchangeRateService
{
private readonly IExchangeRateRepository _repository;
private readonly ILogger<ExchangeRateService> _logger;

public ExchangeRateService(IExchangeRateRepository repository, ILogger<ExchangeRateService>? logger = null)
{
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
_logger = logger ?? NullLogger<ExchangeRateService>.Instance;
}

public async Task<RatesResponse> GetAllRatesAsync(CancellationToken cancellationToken = default)
{
var ratesDict = await _repository.GetCurrentRatesAsync(cancellationToken);
var dtos = ratesDict.Values
.OrderBy(r => r.CurrencyCode)
.Select(r => new ExchangeRateDto(r.CurrencyCode, r.RateToCZK))
.ToList();

return new RatesResponse(dtos, dtos.Count, DateTime.UtcNow);
}

public async Task<ExchangeRateDto?> GetRateByCurrencyAsync(string currencyCode, CancellationToken cancellationToken = default)
{
var rates = await _repository.GetCurrentRatesAsync(cancellationToken);
return rates.TryGetValue(currencyCode.ToUpperInvariant(), out var rate)
? new ExchangeRateDto(rate.CurrencyCode, rate.RateToCZK)
: null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System;

namespace ExchangeRateUpdater.Domain.Entities;

/// <summary>
/// Official Exchange Rate CNB (1 unit to CZK).
/// Immutable for thread-safety in API.
/// </summary>
public record ExchangeRate
{
public string CurrencyCode { get; init; } = string.Empty; // ISO4217 uppercase
public decimal RateToCZK { get; init; } // Normalized rate /1
public DateTime UpdatedAt { get; init; } = DateTime.UtcNow;

public ExchangeRate(string code, decimal rate, DateTime updatedAt)
{
CurrencyCode = code.ToUpperInvariant();
RateToCZK = Math.Round(rate, 4); // CNB precision
UpdatedAt = updatedAt;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using ExchangeRateUpdater.Domain.Entities;

namespace ExchangeRateUpdater.Domain.Interfaces;

public interface IExchangeRateRepository
{
/// <summary>
/// GET current CNB rates.
/// </summary>
Task<IReadOnlyDictionary<string, ExchangeRate>> GetCurrentRatesAsync(CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\ExchangeRateUpdater.Domain\ExchangeRateUpdater.Domain.csproj" />
<ProjectReference Include="..\ExchangeRateUpdater.Application\ExchangeRateUpdater.Application.csproj" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using ExchangeRateUpdater.Domain.Entities;
using ExchangeRateUpdater.Domain.Interfaces;
using Microsoft.Extensions.Logging;
using System.Linq;
using System.Text.Json;


namespace ExchangeRateUpdater.Infrastructure.Http;

/// <summary>
/// CNB-specific HTTP adapter. Replaces with ECBHttpRepository without Domain changes.
/// HTTP details encapsulated.
/// </summary>
public class CnbHttpExchangeRateRepository : IExchangeRateRepository
{
private readonly HttpClient _httpClient;
private readonly ILogger<CnbHttpExchangeRateRepository> _logger;
private const string CnbJsonRatesUrl = "https://api.cnb.cz/cnbapi/exrates/daily?lang=EN";

public CnbHttpExchangeRateRepository(HttpClient httpClient, ILogger<CnbHttpExchangeRateRepository> logger)
{
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_httpClient.DefaultRequestHeaders.Add("User-Agent", "Mews-ExchangeRateUpdater/1.0");
}

public async Task<IReadOnlyDictionary<string, ExchangeRate>> GetCurrentRatesAsync(CancellationToken ct)
{
_logger.LogInformation("Fetching CNB JSON rates from {Url}", CnbJsonRatesUrl);

var response = await _httpClient.GetAsync(CnbJsonRatesUrl, ct);
response.EnsureSuccessStatusCode();

var json = await response.Content.ReadAsStringAsync(ct);
var rates = await ParseCnbJsonAsync(json);

_logger.LogInformation("Parsed {Count} JSON rates from CNB API", rates.Count);
return rates;
}

private async Task<Dictionary<string, ExchangeRate>> ParseCnbJsonAsync(string jsonContent)
{
var cnbResponse = JsonSerializer.Deserialize<CnbDailyResponse>(jsonContent);

return cnbResponse.rates.ToDictionary(
r => r.currencyCode,
r => new ExchangeRate(r.currencyCode, r.rate, DateTime.UtcNow));
}
}
Loading