tests wähurngsverwaltung
This commit is contained in:
@@ -2,6 +2,47 @@
|
|||||||
|
|
||||||
Stand: 2026-04-15
|
Stand: 2026-04-15
|
||||||
|
|
||||||
|
## Nachtrag 2026-04-17
|
||||||
|
|
||||||
|
Der dokumentierte Stand in diesem Handoff war bei der Waehrungslogik nicht mehr aktuell.
|
||||||
|
|
||||||
|
Inzwischen gilt:
|
||||||
|
|
||||||
|
- Kurstabellen fuer `CurrencyExchangeRates` sind im System vorhanden
|
||||||
|
- `Settings` enthaelt bereits eine Pflegeoberflaeche fuer Wechselkurse
|
||||||
|
- `ExchangeRateImportService` importiert ECB-Tageskurse nach `CurrencyExchangeRates`
|
||||||
|
- `NormalizeCurrencyCode` ist als Value-Transformation vorhanden
|
||||||
|
- `ConvertCurrency` ist als Record-Transformation vorhanden
|
||||||
|
- `Program.cs` registriert beide Strategien sowie `CurrencyExchangeRateService` und `ExchangeRateImportService`
|
||||||
|
|
||||||
|
Wichtig:
|
||||||
|
|
||||||
|
- die Roh-Auswertung im `Management Cockpit` rechnet Stand heute weiterhin bewusst **nicht** in CHF um
|
||||||
|
- dort bleibt der Umsatz weiterhin in `Sales Currency`
|
||||||
|
- die Waehrungsumrechnung ist aktuell Teil des allgemeinen Transformations-/Mapping-Systems, nicht der Cockpit-Rohsicht
|
||||||
|
|
||||||
|
Zusatzlich wurden am 2026-04-17 fehlende Unit-Tests fuer die Waehrungslogik nachgezogen:
|
||||||
|
|
||||||
|
- `CurrencyExchangeRateServiceTests`
|
||||||
|
- `ExchangeRateImportServiceTests`
|
||||||
|
- Erweiterungen in
|
||||||
|
- `TransformationStrategiesTests`
|
||||||
|
- `RecordTransformationServiceTests`
|
||||||
|
- `TransformationCatalogTests`
|
||||||
|
|
||||||
|
Aktueller Teststatus nach diesem Nachtrag:
|
||||||
|
|
||||||
|
```text
|
||||||
|
dotnet test .\TrafagSalesExporter.Tests\TrafagSalesExporter.Tests.csproj --verbosity minimal
|
||||||
|
```
|
||||||
|
|
||||||
|
Ergebnis:
|
||||||
|
|
||||||
|
- erfolgreich
|
||||||
|
- `31/31` Tests gruen
|
||||||
|
- bekannte Warnung bleibt:
|
||||||
|
- SAP HANA Architekturwarnung `MSB3270`
|
||||||
|
|
||||||
## Nachtrag 2026-04-16
|
## Nachtrag 2026-04-16
|
||||||
|
|
||||||
Seit dem letzten Handoff wurden weitere Funktionen umgesetzt, die unten im alten Stand noch nicht voll enthalten sind.
|
Seit dem letzten Handoff wurden weitere Funktionen umgesetzt, die unten im alten Stand noch nicht voll enthalten sind.
|
||||||
|
|||||||
@@ -2,6 +2,31 @@
|
|||||||
|
|
||||||
Stand: 2026-04-15
|
Stand: 2026-04-15
|
||||||
|
|
||||||
|
## Nachtrag 2026-04-17
|
||||||
|
|
||||||
|
Der Punkt `CHF-Umrechnung / Wechselkurse` ist nicht mehr komplett offen.
|
||||||
|
|
||||||
|
Der aktuelle Ist-Stand ist:
|
||||||
|
|
||||||
|
- `CurrencyExchangeRateService` ist implementiert
|
||||||
|
- `ExchangeRateImportService` importiert ECB-Kurse
|
||||||
|
- `NormalizeCurrencyCode` und `ConvertCurrency` sind im Transformationssystem registriert
|
||||||
|
- fehlende Unit-Tests dafuer wurden am 2026-04-17 ergaenzt
|
||||||
|
|
||||||
|
Neuer Teststand:
|
||||||
|
|
||||||
|
- `dotnet test .\TrafagSalesExporter.Tests\TrafagSalesExporter.Tests.csproj --verbosity minimal`
|
||||||
|
- erfolgreich
|
||||||
|
- `31/31` Tests gruen
|
||||||
|
|
||||||
|
Was fuer Waehrungen trotzdem noch offen bleibt:
|
||||||
|
|
||||||
|
- fachlicher Einsatz der `ConvertCurrency`-Regeln in echten Standortkonfigurationen pruefen
|
||||||
|
- UI-Flow fuer Wechselkurspflege in `Settings.razor` manuell gegenpruefen
|
||||||
|
- ECB-Import einmal real ueber die UI bzw. App-Funktion pruefen
|
||||||
|
- bestaetigen, fuer welche Sichten CHF die Zielwaehrung sein soll
|
||||||
|
- Management-Cockpit-Rohsicht nur dann auf CHF umstellen, wenn fachlich gewuenscht
|
||||||
|
|
||||||
## Nachtrag 2026-04-16
|
## Nachtrag 2026-04-16
|
||||||
|
|
||||||
Seit dem letzten Stand kamen mehrere groessere Erweiterungen dazu. Die offenen Punkte unten muessen deshalb im neuen Kontext gelesen werden.
|
Seit dem letzten Stand kamen mehrere groessere Erweiterungen dazu. Die offenen Punkte unten muessen deshalb im neuen Kontext gelesen werden.
|
||||||
@@ -131,7 +156,7 @@ Dateien:
|
|||||||
Noch nicht final umsetzen ohne Rueckmeldung Fachseite:
|
Noch nicht final umsetzen ohne Rueckmeldung Fachseite:
|
||||||
|
|
||||||
- Intercompany-Filter
|
- Intercompany-Filter
|
||||||
- CHF-Umrechnung / Wechselkurse
|
- fachliche Nutzung der CHF-Umrechnung in Cockpit / Reports
|
||||||
- Budgetvergleich
|
- Budgetvergleich
|
||||||
- Gruppenlogik
|
- Gruppenlogik
|
||||||
- Spartenlogik
|
- Spartenlogik
|
||||||
@@ -144,13 +169,14 @@ Diese Punkte sollen spaeter moeglichst dynamisch auf dem neuen Transformations-/
|
|||||||
Wenn weiter in Tests investiert wird, sind die naechsten Kandidaten:
|
Wenn weiter in Tests investiert wird, sind die naechsten Kandidaten:
|
||||||
|
|
||||||
- `ExportOrchestrationService`
|
- `ExportOrchestrationService`
|
||||||
|
- spaeter End-to-End-Tests fuer den Wechselkurs-/Transformationspfad
|
||||||
- spaeter evtl. SQLite-nahe Integrationstests fuer `DatabaseInitializationService`
|
- spaeter evtl. SQLite-nahe Integrationstests fuer `DatabaseInitializationService`
|
||||||
|
|
||||||
Aktueller Teststatus:
|
Aktueller Teststatus:
|
||||||
|
|
||||||
- `dotnet test TrafagSalesExporter.sln --verbosity minimal`
|
- `dotnet test .\TrafagSalesExporter.Tests\TrafagSalesExporter.Tests.csproj --verbosity minimal`
|
||||||
- erfolgreich
|
- erfolgreich
|
||||||
- `18/18` Tests gruen
|
- `31/31` Tests gruen
|
||||||
|
|
||||||
## 7. Referenzdatei
|
## 7. Referenzdatei
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using TrafagSalesExporter.Data;
|
||||||
|
using TrafagSalesExporter.Models;
|
||||||
|
using TrafagSalesExporter.Services;
|
||||||
|
|
||||||
|
namespace TrafagSalesExporter.Tests;
|
||||||
|
|
||||||
|
public class CurrencyExchangeRateServiceTests : IDisposable
|
||||||
|
{
|
||||||
|
private readonly SqliteConnection _connection;
|
||||||
|
private readonly TestDbContextFactory _dbFactory;
|
||||||
|
private readonly CurrencyExchangeRateService _service;
|
||||||
|
|
||||||
|
public CurrencyExchangeRateServiceTests()
|
||||||
|
{
|
||||||
|
_connection = new SqliteConnection("DataSource=:memory:");
|
||||||
|
_connection.Open();
|
||||||
|
|
||||||
|
var options = new DbContextOptionsBuilder<AppDbContext>()
|
||||||
|
.UseSqlite(_connection)
|
||||||
|
.Options;
|
||||||
|
|
||||||
|
using var db = new AppDbContext(options);
|
||||||
|
db.Database.EnsureCreated();
|
||||||
|
|
||||||
|
_dbFactory = new TestDbContextFactory(options);
|
||||||
|
_service = new CurrencyExchangeRateService(_dbFactory);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_connection.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ResolveRate_Returns_Direct_Rate_For_Valid_Date()
|
||||||
|
{
|
||||||
|
await SeedRatesAsync(new CurrencyExchangeRate
|
||||||
|
{
|
||||||
|
FromCurrency = "USD",
|
||||||
|
ToCurrency = "EUR",
|
||||||
|
Rate = 0.92m,
|
||||||
|
ValidFrom = new DateTime(2026, 1, 1),
|
||||||
|
ValidTo = null,
|
||||||
|
IsActive = true
|
||||||
|
});
|
||||||
|
|
||||||
|
var rate = _service.ResolveRate("USD", "EUR", new DateTime(2026, 4, 1));
|
||||||
|
|
||||||
|
Assert.Equal(0.92m, rate);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ResolveRate_Uses_Inverse_Rate_When_Only_Reverse_Rate_Exists()
|
||||||
|
{
|
||||||
|
await SeedRatesAsync(new CurrencyExchangeRate
|
||||||
|
{
|
||||||
|
FromCurrency = "EUR",
|
||||||
|
ToCurrency = "CHF",
|
||||||
|
Rate = 1.10m,
|
||||||
|
ValidFrom = new DateTime(2026, 1, 1),
|
||||||
|
ValidTo = null,
|
||||||
|
IsActive = true
|
||||||
|
});
|
||||||
|
|
||||||
|
var rate = _service.ResolveRate("CHF", "EUR", new DateTime(2026, 4, 1));
|
||||||
|
|
||||||
|
Assert.NotNull(rate);
|
||||||
|
Assert.Equal(1m / 1.10m, rate!.Value, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ResolveRate_Uses_Eur_Cross_Rate_When_No_Direct_Rate_Exists()
|
||||||
|
{
|
||||||
|
await SeedRatesAsync(
|
||||||
|
new CurrencyExchangeRate
|
||||||
|
{
|
||||||
|
FromCurrency = "CHF",
|
||||||
|
ToCurrency = "EUR",
|
||||||
|
Rate = 0.95m,
|
||||||
|
ValidFrom = new DateTime(2026, 1, 1),
|
||||||
|
ValidTo = null,
|
||||||
|
IsActive = true
|
||||||
|
},
|
||||||
|
new CurrencyExchangeRate
|
||||||
|
{
|
||||||
|
FromCurrency = "EUR",
|
||||||
|
ToCurrency = "USD",
|
||||||
|
Rate = 1.08m,
|
||||||
|
ValidFrom = new DateTime(2026, 1, 1),
|
||||||
|
ValidTo = null,
|
||||||
|
IsActive = true
|
||||||
|
});
|
||||||
|
|
||||||
|
var rate = _service.ResolveRate("CHF", "USD", new DateTime(2026, 4, 1));
|
||||||
|
|
||||||
|
Assert.NotNull(rate);
|
||||||
|
Assert.Equal(1.026m, rate!.Value, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("$", "USD")]
|
||||||
|
[InlineData("US$", "USD")]
|
||||||
|
[InlineData("EUR", "EUR")]
|
||||||
|
[InlineData("sfr", "CHF")]
|
||||||
|
[InlineData("cad", "CAD")]
|
||||||
|
[InlineData("xyz", "XYZ")]
|
||||||
|
public void NormalizeCurrencyCode_Normalizes_Known_And_Unknown_Codes(string input, string expected)
|
||||||
|
{
|
||||||
|
var normalized = _service.NormalizeCurrencyCode(input);
|
||||||
|
|
||||||
|
Assert.Equal(expected, normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SeedRatesAsync(params CurrencyExchangeRate[] rates)
|
||||||
|
{
|
||||||
|
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||||
|
db.CurrencyExchangeRates.RemoveRange(db.CurrencyExchangeRates);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
db.CurrencyExchangeRates.AddRange(rates);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TestDbContextFactory : IDbContextFactory<AppDbContext>
|
||||||
|
{
|
||||||
|
private readonly DbContextOptions<AppDbContext> _options;
|
||||||
|
|
||||||
|
public TestDbContextFactory(DbContextOptions<AppDbContext> options)
|
||||||
|
{
|
||||||
|
_options = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AppDbContext CreateDbContext() => new(_options);
|
||||||
|
|
||||||
|
public Task<AppDbContext> CreateDbContextAsync(CancellationToken cancellationToken = default)
|
||||||
|
=> Task.FromResult(new AppDbContext(_options));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Text;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using TrafagSalesExporter.Data;
|
||||||
|
using TrafagSalesExporter.Services;
|
||||||
|
|
||||||
|
namespace TrafagSalesExporter.Tests;
|
||||||
|
|
||||||
|
public class ExchangeRateImportServiceTests : IDisposable
|
||||||
|
{
|
||||||
|
private readonly SqliteConnection _connection;
|
||||||
|
private readonly TestDbContextFactory _dbFactory;
|
||||||
|
|
||||||
|
public ExchangeRateImportServiceTests()
|
||||||
|
{
|
||||||
|
_connection = new SqliteConnection("DataSource=:memory:");
|
||||||
|
_connection.Open();
|
||||||
|
|
||||||
|
var options = new DbContextOptionsBuilder<AppDbContext>()
|
||||||
|
.UseSqlite(_connection)
|
||||||
|
.Options;
|
||||||
|
|
||||||
|
using var db = new AppDbContext(options);
|
||||||
|
db.Database.EnsureCreated();
|
||||||
|
|
||||||
|
_dbFactory = new TestDbContextFactory(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_connection.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RefreshEcbRatesAsync_Imports_Rates_And_Replaces_Previous_Ecb_Rows_For_Same_Day()
|
||||||
|
{
|
||||||
|
const string xml = """
|
||||||
|
<gesmes:Envelope xmlns:gesmes="http://www.gesmes.org/xml/2002-08-01" xmlns="http://www.ecb.int/vocabulary/2002-08-01/eurofxref">
|
||||||
|
<Cube>
|
||||||
|
<Cube time="2026-04-17">
|
||||||
|
<Cube currency="USD" rate="1.1200" />
|
||||||
|
<Cube currency="CHF" rate="0.9700" />
|
||||||
|
</Cube>
|
||||||
|
</Cube>
|
||||||
|
</gesmes:Envelope>
|
||||||
|
""";
|
||||||
|
|
||||||
|
await using (var db = await _dbFactory.CreateDbContextAsync())
|
||||||
|
{
|
||||||
|
db.CurrencyExchangeRates.Add(new()
|
||||||
|
{
|
||||||
|
FromCurrency = "EUR",
|
||||||
|
ToCurrency = "USD",
|
||||||
|
Rate = 1.10m,
|
||||||
|
ValidFrom = new DateTime(2026, 4, 17),
|
||||||
|
Notes = "ECB daily reference rate",
|
||||||
|
IsActive = true
|
||||||
|
});
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
var service = new ExchangeRateImportService(
|
||||||
|
new FakeHttpClientFactory(xml),
|
||||||
|
_dbFactory);
|
||||||
|
|
||||||
|
var result = await service.RefreshEcbRatesAsync();
|
||||||
|
|
||||||
|
Assert.Equal(2, result.ImportedCount);
|
||||||
|
Assert.Equal(new DateTime(2026, 4, 17), result.RateDate);
|
||||||
|
Assert.Equal("ECB", result.SourceName);
|
||||||
|
|
||||||
|
await using var verifyDb = await _dbFactory.CreateDbContextAsync();
|
||||||
|
var rates = await verifyDb.CurrencyExchangeRates
|
||||||
|
.OrderBy(x => x.ToCurrency)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
Assert.Equal(2, rates.Count);
|
||||||
|
Assert.Collection(rates,
|
||||||
|
chf =>
|
||||||
|
{
|
||||||
|
Assert.Equal("EUR", chf.FromCurrency);
|
||||||
|
Assert.Equal("CHF", chf.ToCurrency);
|
||||||
|
Assert.Equal(0.97m, chf.Rate);
|
||||||
|
},
|
||||||
|
usd =>
|
||||||
|
{
|
||||||
|
Assert.Equal("EUR", usd.FromCurrency);
|
||||||
|
Assert.Equal("USD", usd.ToCurrency);
|
||||||
|
Assert.Equal(1.12m, usd.Rate);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TestDbContextFactory : IDbContextFactory<AppDbContext>
|
||||||
|
{
|
||||||
|
private readonly DbContextOptions<AppDbContext> _options;
|
||||||
|
|
||||||
|
public TestDbContextFactory(DbContextOptions<AppDbContext> options)
|
||||||
|
{
|
||||||
|
_options = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AppDbContext CreateDbContext() => new(_options);
|
||||||
|
|
||||||
|
public Task<AppDbContext> CreateDbContextAsync(CancellationToken cancellationToken = default)
|
||||||
|
=> Task.FromResult(new AppDbContext(_options));
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class FakeHttpClientFactory : IHttpClientFactory
|
||||||
|
{
|
||||||
|
private readonly string _xml;
|
||||||
|
|
||||||
|
public FakeHttpClientFactory(string xml)
|
||||||
|
{
|
||||||
|
_xml = xml;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HttpClient CreateClient(string name)
|
||||||
|
{
|
||||||
|
return new HttpClient(new FakeHttpMessageHandler(_xml));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class FakeHttpMessageHandler : HttpMessageHandler
|
||||||
|
{
|
||||||
|
private readonly string _xml;
|
||||||
|
|
||||||
|
public FakeHttpMessageHandler(string xml)
|
||||||
|
{
|
||||||
|
_xml = xml;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent(_xml, Encoding.UTF8, "application/xml")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,7 +21,8 @@ public class RecordTransformationServiceTests
|
|||||||
];
|
];
|
||||||
IRecordTransformationStrategy[] recordStrategies =
|
IRecordTransformationStrategy[] recordStrategies =
|
||||||
[
|
[
|
||||||
new FirstNonEmptyRecordTransformationStrategy()
|
new FirstNonEmptyRecordTransformationStrategy(),
|
||||||
|
new ConvertCurrencyRecordTransformationStrategy(new FakeCurrencyExchangeRateService())
|
||||||
];
|
];
|
||||||
|
|
||||||
_service = new RecordTransformationService(valueStrategies, recordStrategies);
|
_service = new RecordTransformationService(valueStrategies, recordStrategies);
|
||||||
@@ -141,4 +142,43 @@ public class RecordTransformationServiceTests
|
|||||||
|
|
||||||
Assert.Equal(42, records[0].PositionOnInvoice);
|
Assert.Equal(42, records[0].PositionOnInvoice);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Apply_Uses_ConvertCurrency_Record_Strategy()
|
||||||
|
{
|
||||||
|
var records = new List<SalesRecord>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
SalesPriceValue = 100m,
|
||||||
|
SalesCurrency = "CHF",
|
||||||
|
InvoiceDate = new DateTime(2026, 4, 17)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var rules = new[]
|
||||||
|
{
|
||||||
|
new FieldTransformationRule
|
||||||
|
{
|
||||||
|
IsActive = true,
|
||||||
|
RuleScope = "Record",
|
||||||
|
TargetField = nameof(SalesRecord.SalesPriceValue),
|
||||||
|
TransformationType = "ConvertCurrency",
|
||||||
|
Argument = "amountField=SalesPriceValue;currencyField=SalesCurrency;targetCurrency=EUR;dateField=InvoiceDate;targetCurrencyField=SalesCurrency",
|
||||||
|
SortOrder = 10
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
_service.Apply(records, rules);
|
||||||
|
|
||||||
|
Assert.Equal(95m, records[0].SalesPriceValue);
|
||||||
|
Assert.Equal("EUR", records[0].SalesCurrency);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class FakeCurrencyExchangeRateService : ICurrencyExchangeRateService
|
||||||
|
{
|
||||||
|
public decimal? ResolveRate(string fromCurrency, string toCurrency, DateTime? effectiveDate) => 0.95m;
|
||||||
|
|
||||||
|
public string NormalizeCurrencyCode(string? currencyCode)
|
||||||
|
=> currencyCode?.Trim().ToUpperInvariant() ?? string.Empty;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,11 +10,13 @@ public class TransformationCatalogTests
|
|||||||
ITransformationStrategy[] valueStrategies =
|
ITransformationStrategy[] valueStrategies =
|
||||||
[
|
[
|
||||||
new CopyTransformationStrategy(),
|
new CopyTransformationStrategy(),
|
||||||
new ConstantTransformationStrategy()
|
new ConstantTransformationStrategy(),
|
||||||
|
new NormalizeCurrencyCodeTransformationStrategy()
|
||||||
];
|
];
|
||||||
IRecordTransformationStrategy[] recordStrategies =
|
IRecordTransformationStrategy[] recordStrategies =
|
||||||
[
|
[
|
||||||
new FirstNonEmptyRecordTransformationStrategy()
|
new FirstNonEmptyRecordTransformationStrategy(),
|
||||||
|
new ConvertCurrencyRecordTransformationStrategy(new FakeCurrencyExchangeRateService())
|
||||||
];
|
];
|
||||||
|
|
||||||
var catalog = new TransformationCatalog(valueStrategies, recordStrategies);
|
var catalog = new TransformationCatalog(valueStrategies, recordStrategies);
|
||||||
@@ -23,6 +25,16 @@ public class TransformationCatalogTests
|
|||||||
|
|
||||||
Assert.Contains(all, x => x.RuleScope == "Value" && x.Key == "Copy");
|
Assert.Contains(all, x => x.RuleScope == "Value" && x.Key == "Copy");
|
||||||
Assert.Contains(all, x => x.RuleScope == "Value" && x.Key == "Constant");
|
Assert.Contains(all, x => x.RuleScope == "Value" && x.Key == "Constant");
|
||||||
|
Assert.Contains(all, x => x.RuleScope == "Value" && x.Key == "NormalizeCurrencyCode");
|
||||||
Assert.Contains(all, x => x.RuleScope == "Record" && x.Key == "FirstNonEmpty");
|
Assert.Contains(all, x => x.RuleScope == "Record" && x.Key == "FirstNonEmpty");
|
||||||
|
Assert.Contains(all, x => x.RuleScope == "Record" && x.Key == "ConvertCurrency");
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class FakeCurrencyExchangeRateService : ICurrencyExchangeRateService
|
||||||
|
{
|
||||||
|
public decimal? ResolveRate(string fromCurrency, string toCurrency, DateTime? effectiveDate) => 1m;
|
||||||
|
|
||||||
|
public string NormalizeCurrencyCode(string? currencyCode)
|
||||||
|
=> currencyCode?.Trim().ToUpperInvariant() ?? string.Empty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,4 +47,79 @@ public class TransformationStrategiesTests
|
|||||||
|
|
||||||
Assert.Equal("Fallback Supplier", record.CustomerName);
|
Assert.Equal("Fallback Supplier", record.CustomerName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NormalizeCurrencyCodeStrategy_Uses_BuiltIn_And_Custom_Aliases()
|
||||||
|
{
|
||||||
|
var strategy = new NormalizeCurrencyCodeTransformationStrategy();
|
||||||
|
|
||||||
|
var normalizedDollar = strategy.Transform("$", null);
|
||||||
|
var normalizedRupee = strategy.Transform("rs", null);
|
||||||
|
var normalizedCustom = strategy.Transform("fr.", "fr.=>CHF");
|
||||||
|
|
||||||
|
Assert.Equal("USD", normalizedDollar);
|
||||||
|
Assert.Equal("INR", normalizedRupee);
|
||||||
|
Assert.Equal("CHF", normalizedCustom);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ConvertCurrencyRecordStrategy_Converts_Amount_And_Updates_Target_Currency()
|
||||||
|
{
|
||||||
|
var exchangeRateService = new FakeCurrencyExchangeRateService(rate: 0.95m);
|
||||||
|
var strategy = new ConvertCurrencyRecordTransformationStrategy(exchangeRateService);
|
||||||
|
var record = new SalesRecord
|
||||||
|
{
|
||||||
|
SalesPriceValue = 100m,
|
||||||
|
SalesCurrency = "CHF",
|
||||||
|
InvoiceDate = new DateTime(2026, 4, 17)
|
||||||
|
};
|
||||||
|
var rule = new FieldTransformationRule
|
||||||
|
{
|
||||||
|
RuleScope = "Record",
|
||||||
|
TargetField = nameof(SalesRecord.SalesPriceValue),
|
||||||
|
TransformationType = "ConvertCurrency",
|
||||||
|
Argument = "amountField=SalesPriceValue;currencyField=SalesCurrency;targetCurrency=EUR;dateField=InvoiceDate;targetCurrencyField=SalesCurrency;round=2"
|
||||||
|
};
|
||||||
|
|
||||||
|
strategy.Transform(record, rule);
|
||||||
|
|
||||||
|
Assert.Equal("CHF", exchangeRateService.LastFromCurrency);
|
||||||
|
Assert.Equal("EUR", exchangeRateService.LastToCurrency);
|
||||||
|
Assert.Equal(new DateTime(2026, 4, 17), exchangeRateService.LastEffectiveDate);
|
||||||
|
Assert.Equal(95.00m, record.SalesPriceValue);
|
||||||
|
Assert.Equal("EUR", record.SalesCurrency);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class FakeCurrencyExchangeRateService : ICurrencyExchangeRateService
|
||||||
|
{
|
||||||
|
private readonly decimal? _rate;
|
||||||
|
|
||||||
|
public FakeCurrencyExchangeRateService(decimal? rate)
|
||||||
|
{
|
||||||
|
_rate = rate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string LastFromCurrency { get; private set; } = string.Empty;
|
||||||
|
public string LastToCurrency { get; private set; } = string.Empty;
|
||||||
|
public DateTime? LastEffectiveDate { get; private set; }
|
||||||
|
|
||||||
|
public decimal? ResolveRate(string fromCurrency, string toCurrency, DateTime? effectiveDate)
|
||||||
|
{
|
||||||
|
LastFromCurrency = fromCurrency;
|
||||||
|
LastToCurrency = toCurrency;
|
||||||
|
LastEffectiveDate = effectiveDate;
|
||||||
|
return _rate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string NormalizeCurrencyCode(string? currencyCode)
|
||||||
|
{
|
||||||
|
var trimmed = currencyCode?.Trim() ?? string.Empty;
|
||||||
|
return trimmed switch
|
||||||
|
{
|
||||||
|
"$" => "USD",
|
||||||
|
"SFR" => "CHF",
|
||||||
|
_ => trimmed.ToUpperInvariant()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user