unittests
This commit is contained in:
@@ -134,12 +134,12 @@ public class ManualExcelImportService : IManualExcelImportService
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
return 0m;
|
||||
|
||||
if (decimal.TryParse(text, NumberStyles.Any, CultureInfo.InvariantCulture, out decimalValue))
|
||||
return decimalValue;
|
||||
if (decimal.TryParse(text, NumberStyles.Any, CultureInfo.GetCultureInfo("de-CH"), out decimalValue))
|
||||
return decimalValue;
|
||||
if (decimal.TryParse(text, NumberStyles.Any, CultureInfo.GetCultureInfo("de-DE"), out decimalValue))
|
||||
return decimalValue;
|
||||
if (decimal.TryParse(text, NumberStyles.Any, CultureInfo.InvariantCulture, out decimalValue))
|
||||
return decimalValue;
|
||||
|
||||
return 0m;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Text.Json;
|
||||
using TrafagSalesExporter.Data;
|
||||
using TrafagSalesExporter.Models;
|
||||
using TrafagSalesExporter.Services;
|
||||
|
||||
namespace TrafagSalesExporter.Tests;
|
||||
|
||||
public class ConfigTransferServiceTests : IDisposable
|
||||
{
|
||||
private readonly SqliteConnection _connection;
|
||||
private readonly TestDbContextFactory _dbFactory;
|
||||
private readonly ConfigTransferService _service;
|
||||
|
||||
public ConfigTransferServiceTests()
|
||||
{
|
||||
_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 ConfigTransferService(_dbFactory);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_connection.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExportJsonAsync_Excludes_Secrets_When_Requested()
|
||||
{
|
||||
await SeedExportConfigurationAsync();
|
||||
|
||||
var json = await _service.ExportJsonAsync(includeSecrets: false);
|
||||
var package = JsonSerializer.Deserialize<ConfigTransferPackage>(json)
|
||||
?? throw new InvalidOperationException("Package missing.");
|
||||
|
||||
Assert.False(package.IncludesSecrets);
|
||||
Assert.NotNull(package.ExportSettings);
|
||||
Assert.Null(package.ExportSettings.SapUsername);
|
||||
Assert.Null(package.ExportSettings.SapPassword);
|
||||
Assert.NotNull(package.SharePointConfig);
|
||||
Assert.Null(package.SharePointConfig.ClientSecret);
|
||||
|
||||
var server = Assert.Single(package.HanaServers);
|
||||
Assert.Null(server.Username);
|
||||
Assert.Null(server.Password);
|
||||
|
||||
var site = Assert.Single(package.Sites);
|
||||
Assert.Null(site.UsernameOverride);
|
||||
Assert.Null(site.PasswordOverride);
|
||||
Assert.Equal("C:\\imports\\manual.xlsx", site.ManualImportFilePath);
|
||||
|
||||
var rule = Assert.Single(package.FieldTransformationRules);
|
||||
Assert.Equal("Record", rule.RuleScope);
|
||||
Assert.Equal("FirstNonEmpty", rule.TransformationType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ImportJsonAsync_Preserves_Existing_Secrets_When_Import_Excludes_Secrets()
|
||||
{
|
||||
await SeedExistingSecretsAsync();
|
||||
|
||||
var package = new ConfigTransferPackage
|
||||
{
|
||||
IncludesSecrets = false,
|
||||
SharePointConfig = new ConfigTransferSharePoint
|
||||
{
|
||||
SiteUrl = "https://new.sharepoint.local",
|
||||
ExportFolder = "/new",
|
||||
TenantId = "new-tenant",
|
||||
ClientId = "new-client",
|
||||
ClientSecret = null
|
||||
},
|
||||
ExportSettings = new ConfigTransferExportSettings
|
||||
{
|
||||
DateFilter = "2026-01-01",
|
||||
TimerHour = 5,
|
||||
TimerMinute = 30,
|
||||
TimerEnabled = false,
|
||||
DebugLoggingEnabled = true,
|
||||
LocalSiteExportFolder = "D:\\site",
|
||||
LocalConsolidatedExportFolder = "D:\\consolidated",
|
||||
SapUsername = null,
|
||||
SapPassword = null,
|
||||
Bi1Username = null,
|
||||
Bi1Password = null,
|
||||
SageUsername = null,
|
||||
SagePassword = null
|
||||
},
|
||||
HanaServers =
|
||||
[
|
||||
new ConfigTransferHanaServer
|
||||
{
|
||||
Key = "server-1",
|
||||
Name = "Server A",
|
||||
Host = "hana-a",
|
||||
Port = 30015,
|
||||
Username = null,
|
||||
Password = null,
|
||||
DatabaseName = "DB1",
|
||||
UseSsl = true,
|
||||
ValidateCertificate = false,
|
||||
AdditionalParams = "x=y"
|
||||
}
|
||||
],
|
||||
Sites =
|
||||
[
|
||||
new ConfigTransferSite
|
||||
{
|
||||
Key = "site-1",
|
||||
HanaServerKey = "server-1",
|
||||
Schema = "schema_a",
|
||||
TSC = "TRCH",
|
||||
Land = "Schweiz",
|
||||
SourceSystem = "MANUAL_EXCEL",
|
||||
UsernameOverride = null,
|
||||
PasswordOverride = null,
|
||||
ManualImportFilePath = "D:\\manual\\trch.xlsx",
|
||||
ManualImportLastUploadedAtUtc = new DateTime(2026, 4, 16, 8, 0, 0, DateTimeKind.Utc),
|
||||
IsActive = true
|
||||
}
|
||||
],
|
||||
FieldTransformationRules =
|
||||
[
|
||||
new FieldTransformationRule
|
||||
{
|
||||
SourceSystem = "MANUAL_EXCEL",
|
||||
SourceField = "",
|
||||
TargetField = nameof(SalesRecord.CustomerName),
|
||||
TransformationType = "FirstNonEmpty",
|
||||
RuleScope = "Record",
|
||||
Argument = "CustomerName|SupplierName|Name",
|
||||
SortOrder = 10,
|
||||
IsActive = true
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
await _service.ImportJsonAsync(JsonSerializer.Serialize(package));
|
||||
|
||||
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||
var settings = await db.ExportSettings.SingleAsync();
|
||||
var sharePoint = await db.SharePointConfigs.SingleAsync();
|
||||
var server = await db.HanaServers.SingleAsync();
|
||||
var site = await db.Sites.SingleAsync();
|
||||
var rule = await db.FieldTransformationRules.SingleAsync();
|
||||
|
||||
Assert.Equal("preserved-sap-user", settings.SapUsername);
|
||||
Assert.Equal("preserved-sap-password", settings.SapPassword);
|
||||
Assert.Equal("preserved-bi1-user", settings.Bi1Username);
|
||||
Assert.Equal("preserved-sage-password", settings.SagePassword);
|
||||
|
||||
Assert.Equal("preserved-sharepoint-secret", sharePoint.ClientSecret);
|
||||
Assert.Equal("new-tenant", sharePoint.TenantId);
|
||||
|
||||
Assert.Equal("preserved-server-user", server.Username);
|
||||
Assert.Equal("preserved-server-password", server.Password);
|
||||
Assert.True(server.UseSsl);
|
||||
|
||||
Assert.Equal("preserved-site-user", site.UsernameOverride);
|
||||
Assert.Equal("preserved-site-password", site.PasswordOverride);
|
||||
Assert.Equal("D:\\manual\\trch.xlsx", site.ManualImportFilePath);
|
||||
Assert.Equal("MANUAL_EXCEL", site.SourceSystem);
|
||||
|
||||
Assert.Equal("Record", rule.RuleScope);
|
||||
Assert.Equal("FirstNonEmpty", rule.TransformationType);
|
||||
}
|
||||
|
||||
private async Task SeedExportConfigurationAsync()
|
||||
{
|
||||
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||
db.SharePointConfigs.Add(new SharePointConfig
|
||||
{
|
||||
SiteUrl = "https://sharepoint.local",
|
||||
ExportFolder = "/exports",
|
||||
TenantId = "tenant",
|
||||
ClientId = "client",
|
||||
ClientSecret = "secret"
|
||||
});
|
||||
db.ExportSettings.Add(new ExportSettings
|
||||
{
|
||||
SapUsername = "sap-user",
|
||||
SapPassword = "sap-password",
|
||||
Bi1Username = "bi1-user",
|
||||
Bi1Password = "bi1-password",
|
||||
SageUsername = "sage-user",
|
||||
SagePassword = "sage-password"
|
||||
});
|
||||
db.HanaServers.Add(new HanaServer
|
||||
{
|
||||
Id = 1,
|
||||
Name = "Server A",
|
||||
Host = "hana-a",
|
||||
Port = 30015,
|
||||
Username = "server-user",
|
||||
Password = "server-password",
|
||||
DatabaseName = "DB1"
|
||||
});
|
||||
db.Sites.Add(new Site
|
||||
{
|
||||
Id = 1,
|
||||
HanaServerId = 1,
|
||||
Schema = "schema_a",
|
||||
TSC = "TRCH",
|
||||
Land = "Schweiz",
|
||||
SourceSystem = "MANUAL_EXCEL",
|
||||
UsernameOverride = "site-user",
|
||||
PasswordOverride = "site-password",
|
||||
ManualImportFilePath = "C:\\imports\\manual.xlsx",
|
||||
ManualImportLastUploadedAtUtc = new DateTime(2026, 4, 16, 7, 0, 0, DateTimeKind.Utc),
|
||||
IsActive = true
|
||||
});
|
||||
db.FieldTransformationRules.Add(new FieldTransformationRule
|
||||
{
|
||||
SourceSystem = "MANUAL_EXCEL",
|
||||
SourceField = "",
|
||||
TargetField = nameof(SalesRecord.CustomerName),
|
||||
TransformationType = "FirstNonEmpty",
|
||||
RuleScope = "Record",
|
||||
Argument = "CustomerName|SupplierName|Name",
|
||||
SortOrder = 10,
|
||||
IsActive = true
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
private async Task SeedExistingSecretsAsync()
|
||||
{
|
||||
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||
db.SharePointConfigs.Add(new SharePointConfig
|
||||
{
|
||||
SiteUrl = "https://old.sharepoint.local",
|
||||
ExportFolder = "/old",
|
||||
TenantId = "old-tenant",
|
||||
ClientId = "old-client",
|
||||
ClientSecret = "preserved-sharepoint-secret"
|
||||
});
|
||||
db.ExportSettings.Add(new ExportSettings
|
||||
{
|
||||
SapUsername = "preserved-sap-user",
|
||||
SapPassword = "preserved-sap-password",
|
||||
Bi1Username = "preserved-bi1-user",
|
||||
Bi1Password = "preserved-bi1-password",
|
||||
SageUsername = "preserved-sage-user",
|
||||
SagePassword = "preserved-sage-password"
|
||||
});
|
||||
db.HanaServers.Add(new HanaServer
|
||||
{
|
||||
Id = 1,
|
||||
Name = "Server A",
|
||||
Host = "hana-a",
|
||||
Port = 30015,
|
||||
Username = "preserved-server-user",
|
||||
Password = "preserved-server-password",
|
||||
DatabaseName = "DB1"
|
||||
});
|
||||
db.Sites.Add(new Site
|
||||
{
|
||||
Id = 1,
|
||||
HanaServerId = 1,
|
||||
Schema = "schema_a",
|
||||
TSC = "TRCH",
|
||||
Land = "Schweiz",
|
||||
SourceSystem = "MANUAL_EXCEL",
|
||||
UsernameOverride = "preserved-site-user",
|
||||
PasswordOverride = "preserved-site-password",
|
||||
IsActive = true
|
||||
});
|
||||
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,195 @@
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TrafagSalesExporter.Data;
|
||||
using TrafagSalesExporter.Models;
|
||||
using TrafagSalesExporter.Services;
|
||||
|
||||
namespace TrafagSalesExporter.Tests;
|
||||
|
||||
public class ManagementCockpitServiceTests : IDisposable
|
||||
{
|
||||
private readonly SqliteConnection _connection;
|
||||
private readonly TestDbContextFactory _dbFactory;
|
||||
private readonly ManagementCockpitService _service;
|
||||
|
||||
public ManagementCockpitServiceTests()
|
||||
{
|
||||
_connection = new SqliteConnection("DataSource=:memory:");
|
||||
_connection.Open();
|
||||
|
||||
var options = new DbContextOptionsBuilder<AppDbContext>()
|
||||
.UseSqlite(_connection)
|
||||
.Options;
|
||||
|
||||
using (var db = new AppDbContext(options))
|
||||
{
|
||||
db.Database.EnsureCreated();
|
||||
if (!db.Sites.Any())
|
||||
{
|
||||
db.Sites.Add(new Site
|
||||
{
|
||||
Id = 1,
|
||||
HanaServerId = null,
|
||||
Schema = "test",
|
||||
TSC = "TEST",
|
||||
Land = "Testland",
|
||||
SourceSystem = "SAP",
|
||||
IsActive = true
|
||||
});
|
||||
db.SaveChanges();
|
||||
}
|
||||
}
|
||||
|
||||
_dbFactory = new TestDbContextFactory(options);
|
||||
_service = new ManagementCockpitService(_dbFactory);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_connection.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAvailableCentralYearsAsync_Returns_Distinct_Ordered_Years()
|
||||
{
|
||||
await SeedCentralRowsAsync(
|
||||
CreateRow("SAP", "CH", "TRCH", "INV-1", "CHF", 100m, new DateTime(2025, 1, 10)),
|
||||
CreateRow("SAP", "CH", "TRCH", "INV-2", "CHF", 200m, new DateTime(2026, 2, 10)),
|
||||
CreateRow("SAP", "CH", "TRCH", "INV-3", "CHF", 300m, null, new DateTime(2026, 3, 5)));
|
||||
|
||||
var years = await _service.GetAvailableCentralYearsAsync();
|
||||
|
||||
Assert.Equal([2025, 2026], years);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AnalyzeCentralAsync_Uses_InvoiceDate_Or_ExtractionDate_And_Builds_Monthly_Daily_And_Source_Totals()
|
||||
{
|
||||
await SeedCentralRowsAsync(
|
||||
CreateRow("SAP", "Schweiz", "TRCH", "INV-1", "CHF", 100m, new DateTime(2025, 1, 10)),
|
||||
CreateRow("MANUAL_EXCEL", "Deutschland", "TRDE", "INV-2", "EUR", 50m, new DateTime(2025, 1, 11)),
|
||||
CreateRow("SAP", "Deutschland", "TRDE", "INV-3", "EUR", 25m, null, new DateTime(2025, 1, 12)),
|
||||
CreateRow("SAP", "Schweiz", "TRCH", "INV-4", "CHF", 70m, new DateTime(2026, 2, 5)));
|
||||
|
||||
var result = await _service.AnalyzeCentralAsync(2025, 1);
|
||||
|
||||
Assert.Equal(2025, result.Filter.Year);
|
||||
Assert.Equal(1, result.Filter.Month);
|
||||
Assert.Equal(3, result.Summary.RowCount);
|
||||
Assert.Equal(3, result.Summary.InvoiceCount);
|
||||
Assert.Equal(2, result.Summary.SiteCount);
|
||||
Assert.Equal(2, result.Summary.CountryCount);
|
||||
Assert.Equal(2, result.Summary.CurrencyCount);
|
||||
Assert.Equal(new DateTime(2025, 1, 10), result.Summary.PeriodStart);
|
||||
Assert.Equal(new DateTime(2025, 1, 12), result.Summary.PeriodEnd);
|
||||
|
||||
var yearly2025Chf = Assert.Single(result.YearlyTotals, x => x.Year == 2025 && x.Currency == "CHF");
|
||||
Assert.Equal(100m, yearly2025Chf.SalesValue);
|
||||
|
||||
var yearly2025Eur = Assert.Single(result.YearlyTotals, x => x.Year == 2025 && x.Currency == "EUR");
|
||||
Assert.Equal(75m, yearly2025Eur.SalesValue);
|
||||
|
||||
var januaryChf = Assert.Single(result.MonthlyTotals, x => x.Label == "2025-01" && x.Currency == "CHF");
|
||||
Assert.Equal(100m, januaryChf.SalesValue);
|
||||
|
||||
var januaryEur = Assert.Single(result.MonthlyTotals, x => x.Label == "2025-01" && x.Currency == "EUR");
|
||||
Assert.Equal(75m, januaryEur.SalesValue);
|
||||
|
||||
Assert.Equal(3, result.DailyTotals.Count);
|
||||
Assert.Contains(result.DailyTotals, x => x.Label == "2025-01-12" && x.Currency == "EUR" && x.SalesValue == 25m);
|
||||
|
||||
var sapTotal = Assert.Single(result.SourceSystemTotals, x => x.Label == "SAP" && x.Currency == "CHF");
|
||||
Assert.Equal(100m, sapTotal.SalesValue);
|
||||
|
||||
var manualTotal = Assert.Single(result.SourceSystemTotals, x => x.Label == "MANUAL_EXCEL" && x.Currency == "EUR");
|
||||
Assert.Equal(50m, manualTotal.SalesValue);
|
||||
|
||||
var germanyEur = Assert.Single(result.CountryTotals, x => x.Label == "Deutschland" && x.Currency == "EUR");
|
||||
Assert.Equal(75m, germanyEur.SalesValue);
|
||||
Assert.Equal(2, germanyEur.InvoiceCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AnalyzeCentralAsync_With_Year_Only_Does_Not_Build_DailyTotals()
|
||||
{
|
||||
await SeedCentralRowsAsync(
|
||||
CreateRow("SAP", "Schweiz", "TRCH", "INV-1", "CHF", 100m, new DateTime(2025, 1, 10)),
|
||||
CreateRow("SAP", "Schweiz", "TRCH", "INV-2", "CHF", 150m, new DateTime(2025, 2, 10)));
|
||||
|
||||
var result = await _service.AnalyzeCentralAsync(2025, null);
|
||||
|
||||
Assert.Empty(result.DailyTotals);
|
||||
Assert.Equal(2, result.MonthlyTotals.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AnalyzeCentralAsync_Throws_When_No_Rows_Exist_For_Selected_Period()
|
||||
{
|
||||
await SeedCentralRowsAsync(
|
||||
CreateRow("SAP", "Schweiz", "TRCH", "INV-1", "CHF", 100m, new DateTime(2025, 1, 10)));
|
||||
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => _service.AnalyzeCentralAsync(2026, 1));
|
||||
|
||||
Assert.Contains("gewählten Zeitraum", ex.Message);
|
||||
}
|
||||
|
||||
private async Task SeedCentralRowsAsync(params CentralSalesRecord[] rows)
|
||||
{
|
||||
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||
db.CentralSalesRecords.RemoveRange(db.CentralSalesRecords);
|
||||
await db.SaveChangesAsync();
|
||||
db.CentralSalesRecords.AddRange(rows);
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
private static CentralSalesRecord CreateRow(string sourceSystem, string land, string tsc, string invoiceNumber, string currency, decimal salesValue, DateTime? invoiceDate, DateTime? extractionDate = null)
|
||||
{
|
||||
return new CentralSalesRecord
|
||||
{
|
||||
SiteId = 1,
|
||||
StoredAtUtc = DateTime.UtcNow,
|
||||
SourceSystem = sourceSystem,
|
||||
ExtractionDate = extractionDate ?? invoiceDate ?? DateTime.UtcNow.Date,
|
||||
Tsc = tsc,
|
||||
InvoiceNumber = invoiceNumber,
|
||||
PositionOnInvoice = 1,
|
||||
Material = "MAT",
|
||||
Name = "Article",
|
||||
ProductGroup = "PG",
|
||||
Quantity = 1m,
|
||||
SupplierNumber = "SUP",
|
||||
SupplierName = "Supplier",
|
||||
SupplierCountry = "CH",
|
||||
CustomerNumber = "CUS",
|
||||
CustomerName = "Customer",
|
||||
CustomerCountry = "CH",
|
||||
CustomerIndustry = "Industry",
|
||||
StandardCost = 1m,
|
||||
StandardCostCurrency = currency,
|
||||
PurchaseOrderNumber = "PO",
|
||||
SalesPriceValue = salesValue,
|
||||
SalesCurrency = currency,
|
||||
Incoterms2020 = "DAP",
|
||||
SalesResponsibleEmployee = "Alice",
|
||||
InvoiceDate = invoiceDate,
|
||||
OrderDate = invoiceDate?.AddDays(-2),
|
||||
Land = land,
|
||||
DocumentType = "Invoice"
|
||||
};
|
||||
}
|
||||
|
||||
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,221 @@
|
||||
using ClosedXML.Excel;
|
||||
using TrafagSalesExporter.Models;
|
||||
using TrafagSalesExporter.Services;
|
||||
|
||||
namespace TrafagSalesExporter.Tests;
|
||||
|
||||
public class ManualExcelImportServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task ReadSalesRecordsAsync_Reads_Expected_Columns_From_Exporter_Format()
|
||||
{
|
||||
var site = new Site
|
||||
{
|
||||
TSC = "TRCH",
|
||||
Land = "Schweiz"
|
||||
};
|
||||
var filePath = CreateWorkbook(workbook =>
|
||||
{
|
||||
var ws = workbook.Worksheets.Add("Sales");
|
||||
WriteHeaders(ws);
|
||||
ws.Cell(2, 1).Value = "15.04.2026 13:45:00";
|
||||
ws.Cell(2, 2).Value = "TRDE";
|
||||
ws.Cell(2, 3).Value = "INV-100";
|
||||
ws.Cell(2, 4).Value = 7;
|
||||
ws.Cell(2, 5).Value = "MAT-1";
|
||||
ws.Cell(2, 6).Value = "Pressure Sensor";
|
||||
ws.Cell(2, 7).Value = "PG-A";
|
||||
ws.Cell(2, 8).Value = 2.5m;
|
||||
ws.Cell(2, 9).Value = "SUP-1";
|
||||
ws.Cell(2, 10).Value = "Supplier";
|
||||
ws.Cell(2, 11).Value = "DE";
|
||||
ws.Cell(2, 12).Value = "CUST-1";
|
||||
ws.Cell(2, 13).Value = "Customer";
|
||||
ws.Cell(2, 14).Value = "CH";
|
||||
ws.Cell(2, 15).Value = "Industry";
|
||||
ws.Cell(2, 16).Value = 10.25m;
|
||||
ws.Cell(2, 17).Value = "EUR";
|
||||
ws.Cell(2, 18).Value = "PO-1";
|
||||
ws.Cell(2, 19).Value = 21.40m;
|
||||
ws.Cell(2, 20).Value = "EUR";
|
||||
ws.Cell(2, 21).Value = "DAP";
|
||||
ws.Cell(2, 22).Value = "Alice";
|
||||
ws.Cell(2, 23).Value = "14.04.2026";
|
||||
ws.Cell(2, 24).Value = "10.04.2026";
|
||||
ws.Cell(2, 25).Value = "Deutschland";
|
||||
ws.Cell(2, 26).Value = "Invoice";
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
var service = new ManualExcelImportService();
|
||||
|
||||
var rows = await service.ReadSalesRecordsAsync(filePath, site);
|
||||
|
||||
var row = Assert.Single(rows);
|
||||
Assert.Equal("TRDE", row.Tsc);
|
||||
Assert.Equal("INV-100", row.InvoiceNumber);
|
||||
Assert.Equal(7, row.PositionOnInvoice);
|
||||
Assert.Equal("MAT-1", row.Material);
|
||||
Assert.Equal(2.5m, row.Quantity);
|
||||
Assert.Equal(10.25m, row.StandardCost);
|
||||
Assert.Equal(21.40m, row.SalesPriceValue);
|
||||
Assert.Equal("Deutschland", row.Land);
|
||||
Assert.Equal("Invoice", row.DocumentType);
|
||||
Assert.Equal(new DateTime(2026, 4, 14), row.InvoiceDate);
|
||||
Assert.Equal(new DateTime(2026, 4, 10), row.OrderDate);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadSalesRecordsAsync_Uses_Site_Fallbacks_When_Tsc_And_Land_Are_Missing()
|
||||
{
|
||||
var site = new Site
|
||||
{
|
||||
TSC = "TRCH",
|
||||
Land = "Schweiz"
|
||||
};
|
||||
var filePath = CreateWorkbook(workbook =>
|
||||
{
|
||||
var ws = workbook.Worksheets.Add("Sales");
|
||||
WriteHeaders(ws);
|
||||
ws.Cell(2, 3).Value = "INV-200";
|
||||
ws.Cell(2, 5).Value = "MAT-2";
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
var service = new ManualExcelImportService();
|
||||
|
||||
var rows = await service.ReadSalesRecordsAsync(filePath, site);
|
||||
|
||||
var row = Assert.Single(rows);
|
||||
Assert.Equal("TRCH", row.Tsc);
|
||||
Assert.Equal("Schweiz", row.Land);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadSalesRecordsAsync_Parses_German_Decimal_Text_And_Skips_Empty_Rows()
|
||||
{
|
||||
var site = new Site
|
||||
{
|
||||
TSC = "TRAT",
|
||||
Land = "Oesterreich"
|
||||
};
|
||||
var filePath = CreateWorkbook(workbook =>
|
||||
{
|
||||
var ws = workbook.Worksheets.Add("Sales");
|
||||
WriteHeaders(ws);
|
||||
ws.Cell(2, 3).Value = "INV-300";
|
||||
ws.Cell(2, 8).Value = "1,50";
|
||||
ws.Cell(2, 16).Value = "3,25";
|
||||
ws.Cell(2, 19).Value = "7,90";
|
||||
ws.Cell(3, 1).Value = "";
|
||||
ws.Cell(3, 2).Value = "";
|
||||
ws.Cell(3, 3).Value = "";
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
var service = new ManualExcelImportService();
|
||||
|
||||
var rows = await service.ReadSalesRecordsAsync(filePath, site);
|
||||
|
||||
var row = Assert.Single(rows);
|
||||
Assert.Equal(1.50m, row.Quantity);
|
||||
Assert.Equal(3.25m, row.StandardCost);
|
||||
Assert.Equal(7.90m, row.SalesPriceValue);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadSalesRecordsAsync_Throws_When_InvoiceNumber_Header_Is_Missing()
|
||||
{
|
||||
var site = new Site
|
||||
{
|
||||
TSC = "TRCH",
|
||||
Land = "Schweiz"
|
||||
};
|
||||
var filePath = CreateWorkbook(workbook =>
|
||||
{
|
||||
var ws = workbook.Worksheets.Add("Sales");
|
||||
ws.Cell(1, 1).Value = "TSC";
|
||||
ws.Cell(1, 2).Value = "Material";
|
||||
ws.Cell(2, 1).Value = "TRCH";
|
||||
ws.Cell(2, 2).Value = "MAT-3";
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
var service = new ManualExcelImportService();
|
||||
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => service.ReadSalesRecordsAsync(filePath, site));
|
||||
|
||||
Assert.Contains("Invoice Number", ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
private static string CreateWorkbook(Action<XLWorkbook> fillWorkbook)
|
||||
{
|
||||
var filePath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}.xlsx");
|
||||
using var workbook = new XLWorkbook();
|
||||
fillWorkbook(workbook);
|
||||
workbook.SaveAs(filePath);
|
||||
return filePath;
|
||||
}
|
||||
|
||||
private static void WriteHeaders(IXLWorksheet ws)
|
||||
{
|
||||
var headers = new[]
|
||||
{
|
||||
"extraction date",
|
||||
"TSC",
|
||||
"Invoice Number",
|
||||
"Position on invoice",
|
||||
"Material",
|
||||
"Name",
|
||||
"Product Group",
|
||||
"Quantity",
|
||||
"Supplier number",
|
||||
"Supplier name",
|
||||
"Supplier country",
|
||||
"Customer number",
|
||||
"Customer name",
|
||||
"Customer country",
|
||||
"Customer Industry",
|
||||
"Standard cost",
|
||||
"Standard Cost Currency",
|
||||
"Purchase Order number",
|
||||
"Sales Price/Value",
|
||||
"Sales Currency",
|
||||
"Incoterms 2020",
|
||||
"Sales responsible employee",
|
||||
"invoice date",
|
||||
"order date",
|
||||
"Land",
|
||||
"Document Type"
|
||||
};
|
||||
|
||||
for (var i = 0; i < headers.Length; i++)
|
||||
{
|
||||
ws.Cell(1, i + 1).Value = headers[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
using TrafagSalesExporter.Models;
|
||||
using TrafagSalesExporter.Services;
|
||||
|
||||
namespace TrafagSalesExporter.Tests;
|
||||
|
||||
public class RecordTransformationServiceTests
|
||||
{
|
||||
private readonly RecordTransformationService _service;
|
||||
|
||||
public RecordTransformationServiceTests()
|
||||
{
|
||||
ITransformationStrategy[] valueStrategies =
|
||||
[
|
||||
new CopyTransformationStrategy(),
|
||||
new UppercaseTransformationStrategy(),
|
||||
new LowercaseTransformationStrategy(),
|
||||
new PrefixTransformationStrategy(),
|
||||
new SuffixTransformationStrategy(),
|
||||
new ReplaceTransformationStrategy(),
|
||||
new ConstantTransformationStrategy()
|
||||
];
|
||||
IRecordTransformationStrategy[] recordStrategies =
|
||||
[
|
||||
new FirstNonEmptyRecordTransformationStrategy()
|
||||
];
|
||||
|
||||
_service = new RecordTransformationService(valueStrategies, recordStrategies);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Apply_Ignores_Inactive_Rules()
|
||||
{
|
||||
var records = new List<SalesRecord>
|
||||
{
|
||||
new() { Material = "abc" }
|
||||
};
|
||||
var rules = new[]
|
||||
{
|
||||
new FieldTransformationRule
|
||||
{
|
||||
IsActive = false,
|
||||
RuleScope = "Value",
|
||||
SourceField = nameof(SalesRecord.Material),
|
||||
TargetField = nameof(SalesRecord.Material),
|
||||
TransformationType = "Uppercase",
|
||||
SortOrder = 10
|
||||
}
|
||||
};
|
||||
|
||||
_service.Apply(records, rules);
|
||||
|
||||
Assert.Equal("abc", records[0].Material);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Apply_Uses_SortOrder_For_Multiple_Value_Rules()
|
||||
{
|
||||
var records = new List<SalesRecord>
|
||||
{
|
||||
new() { Material = "abc" }
|
||||
};
|
||||
var rules = new[]
|
||||
{
|
||||
new FieldTransformationRule
|
||||
{
|
||||
IsActive = true,
|
||||
RuleScope = "Value",
|
||||
SourceField = nameof(SalesRecord.Material),
|
||||
TargetField = nameof(SalesRecord.Material),
|
||||
TransformationType = "Uppercase",
|
||||
SortOrder = 20
|
||||
},
|
||||
new FieldTransformationRule
|
||||
{
|
||||
IsActive = true,
|
||||
RuleScope = "Value",
|
||||
SourceField = nameof(SalesRecord.Material),
|
||||
TargetField = nameof(SalesRecord.Material),
|
||||
TransformationType = "Prefix",
|
||||
Argument = "X-",
|
||||
SortOrder = 10
|
||||
}
|
||||
};
|
||||
|
||||
_service.Apply(records, rules);
|
||||
|
||||
Assert.Equal("X-ABC", records[0].Material);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Apply_Uses_Record_Strategy_When_RuleScope_Is_Record()
|
||||
{
|
||||
var records = new List<SalesRecord>
|
||||
{
|
||||
new()
|
||||
{
|
||||
CustomerName = "",
|
||||
SupplierName = "Supplier A",
|
||||
Name = "Name A"
|
||||
}
|
||||
};
|
||||
var rules = new[]
|
||||
{
|
||||
new FieldTransformationRule
|
||||
{
|
||||
IsActive = true,
|
||||
RuleScope = "Record",
|
||||
TargetField = nameof(SalesRecord.CustomerName),
|
||||
TransformationType = "FirstNonEmpty",
|
||||
Argument = $"{nameof(SalesRecord.CustomerName)}|{nameof(SalesRecord.SupplierName)}|{nameof(SalesRecord.Name)}",
|
||||
SortOrder = 10
|
||||
}
|
||||
};
|
||||
|
||||
_service.Apply(records, rules);
|
||||
|
||||
Assert.Equal("Supplier A", records[0].CustomerName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Apply_Converts_Value_To_Target_Type()
|
||||
{
|
||||
var records = new List<SalesRecord>
|
||||
{
|
||||
new() { Material = "42" }
|
||||
};
|
||||
var rules = new[]
|
||||
{
|
||||
new FieldTransformationRule
|
||||
{
|
||||
IsActive = true,
|
||||
RuleScope = "Value",
|
||||
SourceField = nameof(SalesRecord.Material),
|
||||
TargetField = nameof(SalesRecord.PositionOnInvoice),
|
||||
TransformationType = "Copy",
|
||||
SortOrder = 10
|
||||
}
|
||||
};
|
||||
|
||||
_service.Apply(records, rules);
|
||||
|
||||
Assert.Equal(42, records[0].PositionOnInvoice);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.2" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\TrafagSalesExporter.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,28 @@
|
||||
using TrafagSalesExporter.Services;
|
||||
|
||||
namespace TrafagSalesExporter.Tests;
|
||||
|
||||
public class TransformationCatalogTests
|
||||
{
|
||||
[Fact]
|
||||
public void Catalog_Returns_Value_And_Record_Strategies()
|
||||
{
|
||||
ITransformationStrategy[] valueStrategies =
|
||||
[
|
||||
new CopyTransformationStrategy(),
|
||||
new ConstantTransformationStrategy()
|
||||
];
|
||||
IRecordTransformationStrategy[] recordStrategies =
|
||||
[
|
||||
new FirstNonEmptyRecordTransformationStrategy()
|
||||
];
|
||||
|
||||
var catalog = new TransformationCatalog(valueStrategies, recordStrategies);
|
||||
|
||||
var all = catalog.GetAll();
|
||||
|
||||
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 == "Record" && x.Key == "FirstNonEmpty");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using TrafagSalesExporter.Models;
|
||||
using TrafagSalesExporter.Services;
|
||||
|
||||
namespace TrafagSalesExporter.Tests;
|
||||
|
||||
public class TransformationStrategiesTests
|
||||
{
|
||||
[Fact]
|
||||
public void ReplaceStrategy_Replaces_Text_Using_Argument_Syntax()
|
||||
{
|
||||
var strategy = new ReplaceTransformationStrategy();
|
||||
|
||||
var result = strategy.Transform("Intercompany Kunde", "Intercompany=>Extern");
|
||||
|
||||
Assert.Equal("Extern Kunde", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConstantStrategy_Returns_Argument_Ignoring_SourceValue()
|
||||
{
|
||||
var strategy = new ConstantTransformationStrategy();
|
||||
|
||||
var result = strategy.Transform("ignored", "CHF");
|
||||
|
||||
Assert.Equal("CHF", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FirstNonEmptyRecordStrategy_Uses_First_Non_Empty_Field_From_Argument_List()
|
||||
{
|
||||
var strategy = new FirstNonEmptyRecordTransformationStrategy();
|
||||
var record = new SalesRecord
|
||||
{
|
||||
CustomerName = "",
|
||||
SupplierName = "Fallback Supplier",
|
||||
Name = "Article Name"
|
||||
};
|
||||
var rule = new FieldTransformationRule
|
||||
{
|
||||
RuleScope = "Record",
|
||||
TargetField = nameof(SalesRecord.CustomerName),
|
||||
TransformationType = "FirstNonEmpty",
|
||||
Argument = $"{nameof(SalesRecord.CustomerName)}|{nameof(SalesRecord.SupplierName)}|{nameof(SalesRecord.Name)}"
|
||||
};
|
||||
|
||||
strategy.Transform(record, rule);
|
||||
|
||||
Assert.Equal("Fallback Supplier", record.CustomerName);
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,13 @@
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="TrafagSalesExporter.Tests\**\*.cs" />
|
||||
<Content Remove="TrafagSalesExporter.Tests\**\*" />
|
||||
<EmbeddedResource Remove="TrafagSalesExporter.Tests\**\*" />
|
||||
<None Remove="TrafagSalesExporter.Tests\**\*" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="CheckHanaClient" BeforeTargets="ResolveAssemblyReferences">
|
||||
<Warning Condition="!Exists('$(HanaClientDll)')"
|
||||
Text="SAP HANA Client DLL nicht gefunden: $(HanaClientDll). Bitte SAP HANA Client installieren (https://tools.hana.ondemand.com) oder MSBuild-Property 'HanaClientDll' setzen." />
|
||||
|
||||
@@ -1,20 +1,46 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.14.37012.4 d17.14
|
||||
VisualStudioVersion = 17.14.37012.4
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TrafagSalesExporter", "TrafagSalesExporter.csproj", "{49B56D6D-731C-6482-4A5C-82EAEEBCE593}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TrafagSalesExporter.Tests", "TrafagSalesExporter.Tests\TrafagSalesExporter.Tests.csproj", "{A72D1AFB-E49E-4920-B783-EFFE1132435D}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Debug|x64 = Debug|x64
|
||||
Debug|x86 = Debug|x86
|
||||
Release|Any CPU = Release|Any CPU
|
||||
Release|x64 = Release|x64
|
||||
Release|x86 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{49B56D6D-731C-6482-4A5C-82EAEEBCE593}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{49B56D6D-731C-6482-4A5C-82EAEEBCE593}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{49B56D6D-731C-6482-4A5C-82EAEEBCE593}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{49B56D6D-731C-6482-4A5C-82EAEEBCE593}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{49B56D6D-731C-6482-4A5C-82EAEEBCE593}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{49B56D6D-731C-6482-4A5C-82EAEEBCE593}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{49B56D6D-731C-6482-4A5C-82EAEEBCE593}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{49B56D6D-731C-6482-4A5C-82EAEEBCE593}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{49B56D6D-731C-6482-4A5C-82EAEEBCE593}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{49B56D6D-731C-6482-4A5C-82EAEEBCE593}.Release|x64.Build.0 = Release|Any CPU
|
||||
{49B56D6D-731C-6482-4A5C-82EAEEBCE593}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{49B56D6D-731C-6482-4A5C-82EAEEBCE593}.Release|x86.Build.0 = Release|Any CPU
|
||||
{A72D1AFB-E49E-4920-B783-EFFE1132435D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A72D1AFB-E49E-4920-B783-EFFE1132435D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A72D1AFB-E49E-4920-B783-EFFE1132435D}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{A72D1AFB-E49E-4920-B783-EFFE1132435D}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{A72D1AFB-E49E-4920-B783-EFFE1132435D}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{A72D1AFB-E49E-4920-B783-EFFE1132435D}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{A72D1AFB-E49E-4920-B783-EFFE1132435D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A72D1AFB-E49E-4920-B783-EFFE1132435D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A72D1AFB-E49E-4920-B783-EFFE1132435D}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{A72D1AFB-E49E-4920-B783-EFFE1132435D}.Release|x64.Build.0 = Release|Any CPU
|
||||
{A72D1AFB-E49E-4920-B783-EFFE1132435D}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{A72D1AFB-E49E-4920-B783-EFFE1132435D}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
Reference in New Issue
Block a user