Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Full project #659

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
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
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,10 @@
node_modules
bower_components
npm-debug.log
*.received.*
*.received/
*.vsidx
*.bin
*.manifest
*.testlog
*.v2
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using ExchangeRateUpdater.Domain.ValueObjects;

namespace ExchangeRateUpdater.Domain.Entities
{
public class DailyExchangeRate(Currency sourceCurrency, Currency targetCurrency, decimal value) : ExchangeRate(sourceCurrency, targetCurrency, value)
{

}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using ExchangeRateUpdater.Domain.ValueObjects;

namespace ExchangeRateUpdater.Domain.Entities
{
public abstract class ExchangeRate
{
public Currency SourceCurrency { get; }
public Currency TargetCurrency { get; }
public decimal Value { get; }

protected ExchangeRate(Currency sourceCurrency, Currency targetCurrency, decimal value)
{
SourceCurrency = sourceCurrency;
TargetCurrency = targetCurrency;
Value = value;
}

public override string ToString() => $"{SourceCurrency} / {TargetCurrency} = {Value}";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using ExchangeRateUpdater.Domain.ValueObjects;

namespace ExchangeRateUpdater.Domain.Entities
{
public class MonthYearExchangeRate(Currency sourceCurrency, Currency targetCurrency, decimal value, DateOnly date) : ExchangeRate(sourceCurrency, targetCurrency, value)
{
public DateOnly Date { get; } = date;
public override string ToString() => $"{Date:yyyy-MM-dd} | {base.ToString()}";
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using Newtonsoft.Json;

namespace ExchangeRateUpdater.Domain.Models
{
public class ExchangeRateRow
{
[JsonProperty("validFor")]
public DateOnly ValidFor { get; set; }

[JsonProperty("order")]
public int Order { get; set; }

[JsonProperty("country")]
public string Country { get; set; }

[JsonProperty("currency")]
public string Currency { get; set; }

[JsonProperty("amount")]
public int Amount { get; set; }

[JsonProperty("currencyCode")]
public string CurrencyCode { get; set; }

[JsonProperty("rate")]
public decimal Rate { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using Newtonsoft.Json;

namespace ExchangeRateUpdater.Domain.Models
{
public class ExchangeRatesResponse
{
[JsonProperty("rates")]
public List<ExchangeRateRow>? Rates { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace ExchangeRateUpdater.Domain.Shared
{
public class Settings
{
public required string CnbUrl { get; set; }
public required string DefaultCurrency { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace ExchangeRateUpdater.Domain.ValueObjects
{
public class Currency(string code)
{

/// <summary>
/// Three-letter ISO 4217 code of the currency.
/// </summary>
private string Code { get; } = code;
public override string ToString() => Code;

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace ExchangeRateUpdater.Infrastructure.Configuration
{
public static class ExchangeRateDateFormats
{
public const string FullDate = "yyyy-MM-dd";
public const string YearMonth = "yyyy-MM";
public const string Year = "yyyy";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace ExchangeRateUpdater.Infrastructure.Configuration
{
public static class ExchangeRateRoutes
{
public const string Daily = "daily";
public const string DailyCurrencyMonth = "daily-currency-month";
public const string DailyYear = "daily-year";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using Microsoft.Extensions.Configuration;

namespace ExchangeRateUpdater.Infrastructure.Configuration.Helpers
{
public static class SettingsHelper
{
private static IConfiguration _settings;

public static void Initialize()
{
var builder = new ConfigurationBuilder().AddJsonFile("appsettings.json");
_settings = builder.Build();
}

public static string GetCnbUrl() => _settings["cnbUrl"] ?? "https://api.cnb.cz/cnbapi/exrates/daily";
public static string GetDefaultCurrency() => _settings["DefaultCurrency"] ?? "CZK";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="LanguageExt.Core" Version="4.4.9" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\ExchangeRateUpdated.Infrastructure\ExchangeRateUpdater.Domain.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using ExchangeRateUpdater.Domain.Entities;
using ExchangeRateUpdater.Domain.Models;
using ExchangeRateUpdater.Domain.ValueObjects;
using ExchangeRateUpdater.Infrastructure.Configuration;

namespace ExchangeRateUpdater.Infrastructure.Extensions
{
public static class ExchangeRateExtension
{
public static IEnumerable<ExchangeRateRow> FilterByCurrencies(this IEnumerable<ExchangeRateRow> exchangeRateRows, IEnumerable<Currency>? currencies) =>
currencies is not null && currencies.Any() ?
exchangeRateRows.Where(x => currencies.Any(z => z.ToString().Equals(x.CurrencyCode, StringComparison.InvariantCultureIgnoreCase))) : exchangeRateRows;

public static IEnumerable<ExchangeRate> ToExchangeRates(this IEnumerable<ExchangeRateRow> exchangeRateRows, string scope, Currency targetCurrency) =>
scope switch
{
ExchangeRateRoutes.Daily => exchangeRateRows.Select(x => new DailyExchangeRate(new(x.CurrencyCode), targetCurrency, x.Rate / x.Amount)).Cast<ExchangeRate>().ToList(),
ExchangeRateRoutes.DailyCurrencyMonth or ExchangeRateRoutes.DailyYear => exchangeRateRows.Select(x => new MonthYearExchangeRate(new(x.CurrencyCode), targetCurrency, x.Rate / x.Amount, x.ValidFor)).Cast<ExchangeRate>().ToList(),
_ => throw new InvalidOperationException()
};

public static IEnumerable<Currency> ToCurrency(this string[] currencies) => currencies.Select(currency => new Currency(currency));

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using ExchangeRateUpdater.Domain.Models;
using LanguageExt.Common;
using Microsoft.Extensions.Caching.Memory;
using Newtonsoft.Json;

namespace ExchangeRateUpdater.Infrastructure.Services
{
public class ExchangeRateCacheManager(IMemoryCache memoryCache)
{
private readonly object _lock = new();
private readonly IMemoryCache _memoryCache = memoryCache;

public Result<List<ExchangeRateRow>> GetDailyRates(string date, string url)
{
lock (_lock)
{
var rates = _memoryCache.Get<List<ExchangeRateRow>>(date);
if (rates is not null)
return rates;

return SetDailyRatesAsync(date, url).Result;
}
}
private async Task<Result<List<ExchangeRateRow>>> SetDailyRatesAsync(string date, string url)
{
try
{
using var client = new HttpClient();
var response = await client.GetAsync(url);
var result = JsonConvert.DeserializeObject<ExchangeRatesResponse>(await response.Content.ReadAsStringAsync());
if (result is null || result.Rates is null || result.Rates.Count == 0)
return new Result<List<ExchangeRateRow>>(new ArgumentException("No data for the requested information"));
_memoryCache.Set(date, result.Rates, TimeOnly.MaxValue - TimeOnly.FromDateTime(DateTime.UtcNow));
return result.Rates;
}
catch (Exception ex)
{
return new Result<List<ExchangeRateRow>>(ex);
}
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using ExchangeRateUpdater.Domain.Entities;
using ExchangeRateUpdater.Domain.Shared;
using ExchangeRateUpdater.Domain.ValueObjects;
using ExchangeRateUpdater.Infrastructure.Configuration;
using ExchangeRateUpdater.Infrastructure.Extensions;
using LanguageExt;
using LanguageExt.Common;
using Microsoft.Extensions.Options;
using ExchangeRateUpdater.Infrastructure.Services.Interfaces;
using ExchangeRateUpdater.Infrastructure.Services.Helpers;

namespace ExchangeRateUpdater.Infrastructure.Services
{
public class ExchangeRateProvider(IOptions<Settings> settings, ExchangeRateCacheManager exchangeRateCacheManager) : IExchangeRateProvider
{
private readonly Settings _settings = settings.Value;
private readonly Currency targetCurrency = new(settings.Value.DefaultCurrency);
private readonly ExchangeRateCacheManager _cacheManager = exchangeRateCacheManager;

/// <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 Result<IEnumerable<ExchangeRate>> GetExchangeRates(string scope, string? date = null, string? currency = null, IEnumerable<Currency>? currencies = null)
{
if (!ValidationHelper.ValidateDateFormat(scope,ref date, out var formatException) ||
!ValidationHelper.ValidateCurrency(currency, out formatException) ||
!ValidationHelper.ValidateCurrency(currencies, out formatException))
return new(formatException);

var exchangeRates = _cacheManager.GetDailyRates(date, CurrentUrl(scope, date, currency));

return exchangeRates.Match<Result<IEnumerable<ExchangeRate>>>(
x => x.FilterByCurrencies(currencies).ToExchangeRates(scope, targetCurrency).ToList(),
ex => new(ex));
}

/// <summary>
/// Constructs the URL to retrieve exchange rates from the source, based on the scope and parameters.
/// </summary>
/// <param name="scope">The scope or type of exchange rates (e.g., daily, daily-year, daily-currency-month).</param>
/// <param name="date">The date or year-month combination for the exchange rate query.</param>
/// <param name="currency">The base currency for the query.</param>
/// <returns>A formatted URL string that can be used to retrieve exchange rates from the source.</returns>
private string CurrentUrl(string scope, string date, string? currency) => scope switch
{
ExchangeRateRoutes.Daily => $"{_settings.CnbUrl}/{scope}?date={date}",
ExchangeRateRoutes.DailyYear => $"{_settings.CnbUrl}/{scope}?year={date}",
ExchangeRateRoutes.DailyCurrencyMonth => $"{_settings.CnbUrl}/{scope}?currency={currency}&yearMonth={date}",
_ => throw new ArgumentException(),
};
}
}
Loading