import exxport settings, join over sap hana tables

This commit is contained in:
2026-04-14 11:34:43 +02:00
parent 36a22202bf
commit 59e195af71
21 changed files with 1369 additions and 16 deletions
@@ -0,0 +1,97 @@
using Microsoft.EntityFrameworkCore;
using TrafagSalesExporter.Data;
using TrafagSalesExporter.Models;
namespace TrafagSalesExporter.Services;
public class CentralSalesRecordService : ICentralSalesRecordService
{
private readonly IDbContextFactory<AppDbContext> _dbFactory;
public CentralSalesRecordService(IDbContextFactory<AppDbContext> dbFactory)
{
_dbFactory = dbFactory;
}
public async Task ReplaceForSiteAsync(Site site, IEnumerable<SalesRecord> records)
{
using var db = await _dbFactory.CreateDbContextAsync();
var existing = await db.CentralSalesRecords.Where(r => r.SiteId == site.Id).ToListAsync();
if (existing.Count > 0)
db.CentralSalesRecords.RemoveRange(existing);
var sourceSystem = string.IsNullOrWhiteSpace(site.SourceSystem) ? "SAP" : site.SourceSystem;
db.CentralSalesRecords.AddRange(records.Select(record => new CentralSalesRecord
{
StoredAtUtc = DateTime.UtcNow,
SiteId = site.Id,
SourceSystem = sourceSystem,
ExtractionDate = record.ExtractionDate,
Tsc = record.Tsc,
InvoiceNumber = record.InvoiceNumber,
PositionOnInvoice = record.PositionOnInvoice,
Material = record.Material,
Name = record.Name,
ProductGroup = record.ProductGroup,
Quantity = record.Quantity,
SupplierNumber = record.SupplierNumber,
SupplierName = record.SupplierName,
SupplierCountry = record.SupplierCountry,
CustomerNumber = record.CustomerNumber,
CustomerName = record.CustomerName,
CustomerCountry = record.CustomerCountry,
CustomerIndustry = record.CustomerIndustry,
StandardCost = record.StandardCost,
StandardCostCurrency = record.StandardCostCurrency,
PurchaseOrderNumber = record.PurchaseOrderNumber,
SalesPriceValue = record.SalesPriceValue,
SalesCurrency = record.SalesCurrency,
Incoterms2020 = record.Incoterms2020,
SalesResponsibleEmployee = record.SalesResponsibleEmployee,
InvoiceDate = record.InvoiceDate,
OrderDate = record.OrderDate,
Land = record.Land,
DocumentType = record.DocumentType
}));
await db.SaveChangesAsync();
}
public async Task<List<SalesRecord>> GetAllAsync()
{
using var db = await _dbFactory.CreateDbContextAsync();
return await db.CentralSalesRecords
.OrderBy(r => r.Land)
.ThenBy(r => r.Tsc)
.Select(r => new SalesRecord
{
ExtractionDate = r.ExtractionDate,
Tsc = r.Tsc,
InvoiceNumber = r.InvoiceNumber,
PositionOnInvoice = r.PositionOnInvoice,
Material = r.Material,
Name = r.Name,
ProductGroup = r.ProductGroup,
Quantity = r.Quantity,
SupplierNumber = r.SupplierNumber,
SupplierName = r.SupplierName,
SupplierCountry = r.SupplierCountry,
CustomerNumber = r.CustomerNumber,
CustomerName = r.CustomerName,
CustomerCountry = r.CustomerCountry,
CustomerIndustry = r.CustomerIndustry,
StandardCost = r.StandardCost,
StandardCostCurrency = r.StandardCostCurrency,
PurchaseOrderNumber = r.PurchaseOrderNumber,
SalesPriceValue = r.SalesPriceValue,
SalesCurrency = r.SalesCurrency,
Incoterms2020 = r.Incoterms2020,
SalesResponsibleEmployee = r.SalesResponsibleEmployee,
InvoiceDate = r.InvoiceDate,
OrderDate = r.OrderDate,
Land = r.Land,
DocumentType = r.DocumentType
})
.ToListAsync();
}
}
@@ -0,0 +1,317 @@
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using TrafagSalesExporter.Data;
using TrafagSalesExporter.Models;
namespace TrafagSalesExporter.Services;
public class ConfigTransferService : IConfigTransferService
{
private readonly IDbContextFactory<AppDbContext> _dbFactory;
private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true };
public ConfigTransferService(IDbContextFactory<AppDbContext> dbFactory)
{
_dbFactory = dbFactory;
}
public async Task<string> ExportJsonAsync(bool includeSecrets)
{
using var db = await _dbFactory.CreateDbContextAsync();
var sharePoint = await db.SharePointConfigs.FirstOrDefaultAsync();
var exportSettings = await db.ExportSettings.FirstOrDefaultAsync();
var hanaServers = await db.HanaServers.OrderBy(x => x.Name).ToListAsync();
var sites = await db.Sites.OrderBy(x => x.Land).ToListAsync();
var rules = await db.FieldTransformationRules.OrderBy(x => x.SortOrder).ThenBy(x => x.Id).ToListAsync();
var sapSources = await db.SapSourceDefinitions.OrderBy(x => x.SortOrder).ThenBy(x => x.Id).ToListAsync();
var sapJoins = await db.SapJoinDefinitions.OrderBy(x => x.SortOrder).ThenBy(x => x.Id).ToListAsync();
var sapMappings = await db.SapFieldMappings.OrderBy(x => x.SortOrder).ThenBy(x => x.Id).ToListAsync();
var serverKeyMap = hanaServers.ToDictionary(x => x.Id, _ => Guid.NewGuid().ToString("N"));
var siteKeyMap = sites.ToDictionary(x => x.Id, _ => Guid.NewGuid().ToString("N"));
var package = new ConfigTransferPackage
{
IncludesSecrets = includeSecrets,
SharePointConfig = sharePoint is null ? null : new ConfigTransferSharePoint
{
SiteUrl = sharePoint.SiteUrl,
ExportFolder = sharePoint.ExportFolder,
TenantId = sharePoint.TenantId,
ClientId = sharePoint.ClientId,
ClientSecret = includeSecrets ? sharePoint.ClientSecret : null
},
ExportSettings = exportSettings is null ? null : new ConfigTransferExportSettings
{
DateFilter = exportSettings.DateFilter,
TimerHour = exportSettings.TimerHour,
TimerMinute = exportSettings.TimerMinute,
TimerEnabled = exportSettings.TimerEnabled,
SapUsername = includeSecrets ? exportSettings.SapUsername : null,
SapPassword = includeSecrets ? exportSettings.SapPassword : null,
Bi1Username = includeSecrets ? exportSettings.Bi1Username : null,
Bi1Password = includeSecrets ? exportSettings.Bi1Password : null,
SageUsername = includeSecrets ? exportSettings.SageUsername : null,
SagePassword = includeSecrets ? exportSettings.SagePassword : null
},
HanaServers = hanaServers.Select(server => new ConfigTransferHanaServer
{
Key = serverKeyMap[server.Id],
Name = server.Name,
Host = server.Host,
Port = server.Port,
Username = includeSecrets ? server.Username : null,
Password = includeSecrets ? server.Password : null,
DatabaseName = server.DatabaseName,
UseSsl = server.UseSsl,
ValidateCertificate = server.ValidateCertificate,
AdditionalParams = server.AdditionalParams
}).ToList(),
Sites = sites.Select(site => new ConfigTransferSite
{
Key = siteKeyMap[site.Id],
HanaServerKey = site.HanaServerId.HasValue && serverKeyMap.TryGetValue(site.HanaServerId.Value, out var serverKey) ? serverKey : null,
Schema = site.Schema,
TSC = site.TSC,
Land = site.Land,
SourceSystem = site.SourceSystem,
UsernameOverride = includeSecrets ? site.UsernameOverride : null,
PasswordOverride = includeSecrets ? site.PasswordOverride : null,
SapServiceUrl = site.SapServiceUrl,
SapEntitySet = site.SapEntitySet,
SapEntitySetsCache = site.SapEntitySetsCache,
SapEntitySetsRefreshedAtUtc = site.SapEntitySetsRefreshedAtUtc,
IsActive = site.IsActive
}).ToList(),
FieldTransformationRules = rules.Select(r => new FieldTransformationRule
{
SourceSystem = r.SourceSystem,
SourceField = r.SourceField,
TargetField = r.TargetField,
TransformationType = r.TransformationType,
Argument = r.Argument,
SortOrder = r.SortOrder,
IsActive = r.IsActive
}).ToList(),
SapSourceDefinitions = sapSources.Select(s => new ConfigTransferSapSourceDefinition
{
SiteKey = siteKeyMap[s.SiteId],
Alias = s.Alias,
EntitySet = s.EntitySet,
IsPrimary = s.IsPrimary,
IsActive = s.IsActive,
SortOrder = s.SortOrder
}).ToList(),
SapJoinDefinitions = sapJoins.Select(j => new ConfigTransferSapJoinDefinition
{
SiteKey = siteKeyMap[j.SiteId],
LeftAlias = j.LeftAlias,
RightAlias = j.RightAlias,
LeftKeys = j.LeftKeys,
RightKeys = j.RightKeys,
JoinType = j.JoinType,
IsActive = j.IsActive,
SortOrder = j.SortOrder
}).ToList(),
SapFieldMappings = sapMappings.Select(m => new ConfigTransferSapFieldMapping
{
SiteKey = siteKeyMap[m.SiteId],
TargetField = m.TargetField,
SourceExpression = m.SourceExpression,
IsRequired = m.IsRequired,
IsActive = m.IsActive,
SortOrder = m.SortOrder
}).ToList()
};
return JsonSerializer.Serialize(package, JsonOptions);
}
public async Task ImportJsonAsync(string json)
{
var package = JsonSerializer.Deserialize<ConfigTransferPackage>(json, JsonOptions)
?? throw new InvalidOperationException("Konfigurationsdatei konnte nicht gelesen werden.");
using var db = await _dbFactory.CreateDbContextAsync();
var existingSharePoint = await db.SharePointConfigs.FirstOrDefaultAsync();
var existingSettings = await db.ExportSettings.FirstOrDefaultAsync();
var existingServers = await db.HanaServers.ToListAsync();
var existingSites = await db.Sites.ToListAsync();
var existingRules = await db.FieldTransformationRules.ToListAsync();
var existingSapSources = await db.SapSourceDefinitions.ToListAsync();
var existingSapJoins = await db.SapJoinDefinitions.ToListAsync();
var existingSapMappings = await db.SapFieldMappings.ToListAsync();
var existingCentralRecords = await db.CentralSalesRecords.ToListAsync();
var preservedSharePointSecret = existingSharePoint?.ClientSecret ?? string.Empty;
var preservedSecrets = existingSettings is null
? new ConfigTransferExportSettings()
: new ConfigTransferExportSettings
{
SapUsername = existingSettings.SapUsername,
SapPassword = existingSettings.SapPassword,
Bi1Username = existingSettings.Bi1Username,
Bi1Password = existingSettings.Bi1Password,
SageUsername = existingSettings.SageUsername,
SagePassword = existingSettings.SagePassword
};
var preservedServerSecrets = existingServers.ToDictionary(
x => BuildServerSignature(x.Name, x.Host, x.Port, x.DatabaseName),
x => (x.Username, x.Password));
var preservedSiteSecrets = existingSites.ToDictionary(
x => BuildSiteSignature(x.Land, x.TSC, x.Schema, x.SourceSystem),
x => (x.UsernameOverride, x.PasswordOverride));
if (existingSapMappings.Count > 0) db.SapFieldMappings.RemoveRange(existingSapMappings);
if (existingSapJoins.Count > 0) db.SapJoinDefinitions.RemoveRange(existingSapJoins);
if (existingSapSources.Count > 0) db.SapSourceDefinitions.RemoveRange(existingSapSources);
if (existingRules.Count > 0) db.FieldTransformationRules.RemoveRange(existingRules);
if (existingCentralRecords.Count > 0) db.CentralSalesRecords.RemoveRange(existingCentralRecords);
if (existingSites.Count > 0) db.Sites.RemoveRange(existingSites);
if (existingServers.Count > 0) db.HanaServers.RemoveRange(existingServers);
if (existingSharePoint is not null) db.SharePointConfigs.Remove(existingSharePoint);
if (existingSettings is not null) db.ExportSettings.Remove(existingSettings);
await db.SaveChangesAsync();
var newSharePoint = package.SharePointConfig is null ? new SharePointConfig() : new SharePointConfig
{
SiteUrl = package.SharePointConfig.SiteUrl,
ExportFolder = package.SharePointConfig.ExportFolder,
TenantId = package.SharePointConfig.TenantId,
ClientId = package.SharePointConfig.ClientId,
ClientSecret = package.IncludesSecrets ? package.SharePointConfig.ClientSecret ?? string.Empty : preservedSharePointSecret
};
db.SharePointConfigs.Add(newSharePoint);
var importedSettings = package.ExportSettings ?? new ConfigTransferExportSettings();
db.ExportSettings.Add(new ExportSettings
{
DateFilter = importedSettings.DateFilter,
TimerHour = importedSettings.TimerHour,
TimerMinute = importedSettings.TimerMinute,
TimerEnabled = importedSettings.TimerEnabled,
SapUsername = package.IncludesSecrets ? importedSettings.SapUsername ?? string.Empty : preservedSecrets.SapUsername ?? string.Empty,
SapPassword = package.IncludesSecrets ? importedSettings.SapPassword ?? string.Empty : preservedSecrets.SapPassword ?? string.Empty,
Bi1Username = package.IncludesSecrets ? importedSettings.Bi1Username ?? string.Empty : preservedSecrets.Bi1Username ?? string.Empty,
Bi1Password = package.IncludesSecrets ? importedSettings.Bi1Password ?? string.Empty : preservedSecrets.Bi1Password ?? string.Empty,
SageUsername = package.IncludesSecrets ? importedSettings.SageUsername ?? string.Empty : preservedSecrets.SageUsername ?? string.Empty,
SagePassword = package.IncludesSecrets ? importedSettings.SagePassword ?? string.Empty : preservedSecrets.SagePassword ?? string.Empty
});
var serverIdMap = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
foreach (var server in package.HanaServers)
{
preservedServerSecrets.TryGetValue(BuildServerSignature(server.Name, server.Host, server.Port, server.DatabaseName), out var preserved);
var entity = new HanaServer
{
Name = server.Name,
Host = server.Host,
Port = server.Port,
Username = package.IncludesSecrets ? server.Username ?? string.Empty : preserved.Username ?? string.Empty,
Password = package.IncludesSecrets ? server.Password ?? string.Empty : preserved.Password ?? string.Empty,
DatabaseName = server.DatabaseName,
UseSsl = server.UseSsl,
ValidateCertificate = server.ValidateCertificate,
AdditionalParams = server.AdditionalParams
};
db.HanaServers.Add(entity);
await db.SaveChangesAsync();
serverIdMap[server.Key] = entity.Id;
}
var siteIdMap = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
foreach (var site in package.Sites)
{
preservedSiteSecrets.TryGetValue(BuildSiteSignature(site.Land, site.TSC, site.Schema, site.SourceSystem), out var preserved);
var entity = new Site
{
HanaServerId = !string.IsNullOrWhiteSpace(site.HanaServerKey) && serverIdMap.TryGetValue(site.HanaServerKey, out var mappedServerId)
? mappedServerId
: null,
Schema = site.Schema,
TSC = site.TSC,
Land = site.Land,
SourceSystem = site.SourceSystem,
UsernameOverride = package.IncludesSecrets ? site.UsernameOverride ?? string.Empty : preserved.UsernameOverride ?? string.Empty,
PasswordOverride = package.IncludesSecrets ? site.PasswordOverride ?? string.Empty : preserved.PasswordOverride ?? string.Empty,
SapServiceUrl = site.SapServiceUrl,
SapEntitySet = site.SapEntitySet,
SapEntitySetsCache = site.SapEntitySetsCache,
SapEntitySetsRefreshedAtUtc = site.SapEntitySetsRefreshedAtUtc,
IsActive = site.IsActive
};
db.Sites.Add(entity);
await db.SaveChangesAsync();
siteIdMap[site.Key] = entity.Id;
}
if (package.FieldTransformationRules.Count > 0)
{
db.FieldTransformationRules.AddRange(package.FieldTransformationRules.Select(r => new FieldTransformationRule
{
SourceSystem = r.SourceSystem,
SourceField = r.SourceField,
TargetField = r.TargetField,
TransformationType = r.TransformationType,
Argument = r.Argument,
SortOrder = r.SortOrder,
IsActive = r.IsActive
}));
}
if (package.SapSourceDefinitions.Count > 0)
{
db.SapSourceDefinitions.AddRange(package.SapSourceDefinitions
.Where(x => siteIdMap.ContainsKey(x.SiteKey))
.Select(x => new SapSourceDefinition
{
SiteId = siteIdMap[x.SiteKey],
Alias = x.Alias,
EntitySet = x.EntitySet,
IsPrimary = x.IsPrimary,
IsActive = x.IsActive,
SortOrder = x.SortOrder
}));
}
if (package.SapJoinDefinitions.Count > 0)
{
db.SapJoinDefinitions.AddRange(package.SapJoinDefinitions
.Where(x => siteIdMap.ContainsKey(x.SiteKey))
.Select(x => new SapJoinDefinition
{
SiteId = siteIdMap[x.SiteKey],
LeftAlias = x.LeftAlias,
RightAlias = x.RightAlias,
LeftKeys = x.LeftKeys,
RightKeys = x.RightKeys,
JoinType = x.JoinType,
IsActive = x.IsActive,
SortOrder = x.SortOrder
}));
}
if (package.SapFieldMappings.Count > 0)
{
db.SapFieldMappings.AddRange(package.SapFieldMappings
.Where(x => siteIdMap.ContainsKey(x.SiteKey))
.Select(x => new SapFieldMapping
{
SiteId = siteIdMap[x.SiteKey],
TargetField = x.TargetField,
SourceExpression = x.SourceExpression,
IsRequired = x.IsRequired,
IsActive = x.IsActive,
SortOrder = x.SortOrder
}));
}
await db.SaveChangesAsync();
}
private static string BuildServerSignature(string name, string host, int port, string databaseName)
=> $"{name}|{host}|{port}|{databaseName}".ToUpperInvariant();
private static string BuildSiteSignature(string land, string tsc, string schema, string sourceSystem)
=> $"{land}|{tsc}|{schema}|{sourceSystem}".ToUpperInvariant();
}
@@ -7,22 +7,26 @@ namespace TrafagSalesExporter.Services;
public class ConsolidatedExportService : IConsolidatedExportService
{
private readonly IDbContextFactory<AppDbContext> _dbFactory;
private readonly ICentralSalesRecordService _centralSalesRecordService;
private readonly IExcelExportService _excelService;
private readonly ISharePointUploadService _sharePointService;
public ConsolidatedExportService(
IDbContextFactory<AppDbContext> dbFactory,
ICentralSalesRecordService centralSalesRecordService,
IExcelExportService excelService,
ISharePointUploadService sharePointService)
{
_dbFactory = dbFactory;
_centralSalesRecordService = centralSalesRecordService;
_excelService = excelService;
_sharePointService = sharePointService;
}
public async Task<string?> ExportAsync(List<SalesRecord> records)
{
if (records.Count == 0)
var consolidatedRecords = await _centralSalesRecordService.GetAllAsync();
if (consolidatedRecords.Count == 0)
return null;
using var db = await _dbFactory.CreateDbContextAsync();
@@ -31,7 +35,7 @@ public class ConsolidatedExportService : IConsolidatedExportService
var consolidatedPath = _excelService.CreateConsolidatedExcelFile(
outputDir,
DateTime.UtcNow.Date,
records
consolidatedRecords
.OrderBy(r => r.Land)
.ThenBy(r => r.Tsc)
.ThenByDescending(r => r.InvoiceDate ?? DateTime.MinValue)
@@ -43,6 +43,10 @@ public class DatabaseInitializationService : IDatabaseInitializationService
AddColumnIfMissing(db, "ExportSettings", "SageUsername", "TEXT NOT NULL DEFAULT ''");
AddColumnIfMissing(db, "ExportSettings", "SagePassword", "TEXT NOT NULL DEFAULT ''");
EnsureTransformationTable(db);
EnsureSapSourceTable(db);
EnsureSapJoinTable(db);
EnsureSapFieldMappingTable(db);
EnsureCentralSalesRecordTable(db);
}
private static void EnsureSitesTableSupportsOptionalHanaServer(AppDbContext db)
@@ -193,6 +197,115 @@ CREATE TABLE IF NOT EXISTS FieldTransformationRules (
cmd.ExecuteNonQuery();
}
private static void EnsureSapSourceTable(AppDbContext db)
{
var conn = db.Database.GetDbConnection();
if (conn.State != ConnectionState.Open)
conn.Open();
using var cmd = conn.CreateCommand();
cmd.CommandText = @"
CREATE TABLE IF NOT EXISTS SapSourceDefinitions (
Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
SiteId INTEGER NOT NULL,
Alias TEXT NOT NULL,
EntitySet TEXT NOT NULL,
IsPrimary INTEGER NOT NULL DEFAULT 0,
IsActive INTEGER NOT NULL DEFAULT 1,
SortOrder INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (SiteId) REFERENCES Sites (Id)
);";
cmd.ExecuteNonQuery();
}
private static void EnsureSapJoinTable(AppDbContext db)
{
var conn = db.Database.GetDbConnection();
if (conn.State != ConnectionState.Open)
conn.Open();
using var cmd = conn.CreateCommand();
cmd.CommandText = @"
CREATE TABLE IF NOT EXISTS SapJoinDefinitions (
Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
SiteId INTEGER NOT NULL,
LeftAlias TEXT NOT NULL,
RightAlias TEXT NOT NULL,
LeftKeys TEXT NOT NULL,
RightKeys TEXT NOT NULL,
JoinType TEXT NOT NULL DEFAULT 'Left',
IsActive INTEGER NOT NULL DEFAULT 1,
SortOrder INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (SiteId) REFERENCES Sites (Id)
);";
cmd.ExecuteNonQuery();
}
private static void EnsureSapFieldMappingTable(AppDbContext db)
{
var conn = db.Database.GetDbConnection();
if (conn.State != ConnectionState.Open)
conn.Open();
using var cmd = conn.CreateCommand();
cmd.CommandText = @"
CREATE TABLE IF NOT EXISTS SapFieldMappings (
Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
SiteId INTEGER NOT NULL,
TargetField TEXT NOT NULL,
SourceExpression TEXT NOT NULL,
IsRequired INTEGER NOT NULL DEFAULT 0,
IsActive INTEGER NOT NULL DEFAULT 1,
SortOrder INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (SiteId) REFERENCES Sites (Id)
);";
cmd.ExecuteNonQuery();
}
private static void EnsureCentralSalesRecordTable(AppDbContext db)
{
var conn = db.Database.GetDbConnection();
if (conn.State != ConnectionState.Open)
conn.Open();
using var cmd = conn.CreateCommand();
cmd.CommandText = @"
CREATE TABLE IF NOT EXISTS CentralSalesRecords (
Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
StoredAtUtc TEXT NOT NULL,
SiteId INTEGER NOT NULL,
SourceSystem TEXT NOT NULL,
ExtractionDate TEXT NOT NULL,
Tsc TEXT NOT NULL,
InvoiceNumber TEXT NOT NULL,
PositionOnInvoice INTEGER NOT NULL,
Material TEXT NOT NULL,
Name TEXT NOT NULL,
ProductGroup TEXT NOT NULL,
Quantity TEXT NOT NULL,
SupplierNumber TEXT NOT NULL,
SupplierName TEXT NOT NULL,
SupplierCountry TEXT NOT NULL,
CustomerNumber TEXT NOT NULL,
CustomerName TEXT NOT NULL,
CustomerCountry TEXT NOT NULL,
CustomerIndustry TEXT NOT NULL,
StandardCost TEXT NOT NULL,
StandardCostCurrency TEXT NOT NULL,
PurchaseOrderNumber TEXT NOT NULL,
SalesPriceValue TEXT NOT NULL,
SalesCurrency TEXT NOT NULL,
Incoterms2020 TEXT NOT NULL,
SalesResponsibleEmployee TEXT NOT NULL,
InvoiceDate TEXT NULL,
OrderDate TEXT NULL,
Land TEXT NOT NULL,
DocumentType TEXT NOT NULL,
FOREIGN KEY (SiteId) REFERENCES Sites (Id)
);";
cmd.ExecuteNonQuery();
}
private static void SeedIfEmpty(AppDbContext db)
{
if (db.HanaServers.Any())
@@ -0,0 +1,9 @@
using TrafagSalesExporter.Models;
namespace TrafagSalesExporter.Services;
public interface ICentralSalesRecordService
{
Task ReplaceForSiteAsync(Site site, IEnumerable<SalesRecord> records);
Task<List<SalesRecord>> GetAllAsync();
}
@@ -0,0 +1,7 @@
namespace TrafagSalesExporter.Services;
public interface IConfigTransferService
{
Task<string> ExportJsonAsync(bool includeSecrets);
Task ImportJsonAsync(string json);
}
@@ -0,0 +1,15 @@
using TrafagSalesExporter.Models;
namespace TrafagSalesExporter.Services;
public interface ISapCompositionService
{
Task<List<SalesRecord>> BuildSalesRecordsAsync(
Site site,
IReadOnlyList<SapSourceDefinition> sources,
IReadOnlyList<SapJoinDefinition> joins,
IReadOnlyList<SapFieldMapping> mappings,
string username,
string password,
CancellationToken cancellationToken = default);
}
@@ -0,0 +1,222 @@
using System.Globalization;
using TrafagSalesExporter.Models;
namespace TrafagSalesExporter.Services;
public class SapCompositionService : ISapCompositionService
{
private readonly ISapGatewayService _sapGatewayService;
public SapCompositionService(ISapGatewayService sapGatewayService)
{
_sapGatewayService = sapGatewayService;
}
public async Task<List<SalesRecord>> BuildSalesRecordsAsync(
Site site,
IReadOnlyList<SapSourceDefinition> sources,
IReadOnlyList<SapJoinDefinition> joins,
IReadOnlyList<SapFieldMapping> mappings,
string username,
string password,
CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(site.SapServiceUrl))
throw new InvalidOperationException($"Standort '{site.Land}' hat keine SAP Service URL.");
var activeSources = sources
.Where(s => s.IsActive)
.OrderBy(s => s.SortOrder)
.ThenBy(s => s.Id)
.ToList();
if (activeSources.Count == 0)
throw new InvalidOperationException($"Standort '{site.Land}' hat keine aktiven SAP-Quellen.");
var primarySource = activeSources.FirstOrDefault(s => s.IsPrimary) ?? activeSources.First();
var sourceRows = new Dictionary<string, List<Dictionary<string, object?>>>(StringComparer.OrdinalIgnoreCase);
foreach (var source in activeSources)
{
var rows = await _sapGatewayService.GetEntityRowsAsync(site.SapServiceUrl, source.EntitySet, username, password, cancellationToken);
sourceRows[source.Alias] = rows;
}
var composedRows = sourceRows[primarySource.Alias]
.Select(r => PrefixRow(primarySource.Alias, r))
.ToList();
foreach (var join in joins.Where(j => j.IsActive).OrderBy(j => j.SortOrder).ThenBy(j => j.Id))
{
if (!sourceRows.TryGetValue(join.RightAlias, out var rightRows))
continue;
composedRows = ApplyLeftJoin(composedRows, join.LeftAlias, join.LeftKeys, join.RightAlias, join.RightKeys, rightRows);
}
return composedRows
.Select(row => MapToSalesRecord(site, row, mappings))
.ToList();
}
private static Dictionary<string, object?> PrefixRow(string alias, Dictionary<string, object?> row)
=> row.ToDictionary(kvp => $"{alias}.{kvp.Key}", kvp => kvp.Value, StringComparer.OrdinalIgnoreCase);
private static List<Dictionary<string, object?>> ApplyLeftJoin(
List<Dictionary<string, object?>> leftRows,
string leftAlias,
string leftKeys,
string rightAlias,
string rightKeys,
List<Dictionary<string, object?>> rightRows)
{
var leftKeyParts = SplitKeys(leftKeys);
var rightKeyParts = SplitKeys(rightKeys);
if (leftKeyParts.Count == 0 || leftKeyParts.Count != rightKeyParts.Count)
return leftRows;
var rightLookup = rightRows
.GroupBy(r => BuildKey(r, rightKeyParts))
.ToDictionary(g => g.Key, g => g.ToList(), StringComparer.OrdinalIgnoreCase);
var results = new List<Dictionary<string, object?>>();
foreach (var leftRow in leftRows)
{
var leftKey = BuildKey(leftRow, leftAlias, leftKeyParts);
if (rightLookup.TryGetValue(leftKey, out var matches) && matches.Count > 0)
{
foreach (var match in matches)
{
var merged = new Dictionary<string, object?>(leftRow, StringComparer.OrdinalIgnoreCase);
foreach (var kvp in PrefixRow(rightAlias, match))
merged[kvp.Key] = kvp.Value;
results.Add(merged);
}
}
else
{
results.Add(leftRow);
}
}
return results;
}
private static SalesRecord MapToSalesRecord(Site site, Dictionary<string, object?> row, IReadOnlyList<SapFieldMapping> mappings)
{
var record = new SalesRecord
{
ExtractionDate = DateTime.UtcNow,
Tsc = site.TSC,
Land = site.Land,
DocumentType = "SAP"
};
foreach (var mapping in mappings.Where(m => m.IsActive).OrderBy(m => m.SortOrder).ThenBy(m => m.Id))
{
var value = EvaluateExpression(row, mapping.SourceExpression);
ApplyValue(record, mapping.TargetField, value);
}
if (record.ExtractionDate == default)
record.ExtractionDate = DateTime.UtcNow;
if (string.IsNullOrWhiteSpace(record.Tsc))
record.Tsc = site.TSC;
if (string.IsNullOrWhiteSpace(record.Land))
record.Land = site.Land;
return record;
}
private static object? EvaluateExpression(Dictionary<string, object?> row, string expression)
{
if (string.IsNullOrWhiteSpace(expression))
return null;
var value = expression.Trim();
if (value.StartsWith('='))
return value[1..];
if (row.TryGetValue(value, out var direct))
return direct;
return null;
}
private static void ApplyValue(SalesRecord record, string targetField, object? value)
{
var property = typeof(SalesRecord).GetProperty(targetField);
if (property is null)
return;
try
{
if (property.PropertyType == typeof(string))
{
property.SetValue(record, value?.ToString() ?? string.Empty);
return;
}
if (property.PropertyType == typeof(int))
{
if (int.TryParse(value?.ToString(), NumberStyles.Any, CultureInfo.InvariantCulture, out var intValue))
property.SetValue(record, intValue);
return;
}
if (property.PropertyType == typeof(decimal))
{
if (decimal.TryParse(value?.ToString(), NumberStyles.Any, CultureInfo.InvariantCulture, out var decimalValue))
property.SetValue(record, decimalValue);
return;
}
if (property.PropertyType == typeof(DateTime?) || property.PropertyType == typeof(DateTime))
{
if (TryParseDate(value?.ToString(), out var date))
property.SetValue(record, date);
}
}
catch
{
// ignore invalid mappings and continue with remaining fields
}
}
private static bool TryParseDate(string? value, out DateTime date)
{
date = default;
if (string.IsNullOrWhiteSpace(value))
return false;
var trimmed = value.Trim();
if (trimmed.StartsWith("/Date(", StringComparison.Ordinal) && trimmed.EndsWith(")/", StringComparison.Ordinal))
{
var epochRaw = trimmed[6..^2];
var separator = epochRaw.IndexOfAny(['+', '-']);
if (separator > 0)
epochRaw = epochRaw[..separator];
if (long.TryParse(epochRaw, out var ms))
{
date = DateTimeOffset.FromUnixTimeMilliseconds(ms).UtcDateTime;
return true;
}
}
return DateTime.TryParse(trimmed, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out date)
|| DateTime.TryParse(trimmed, CultureInfo.GetCultureInfo("de-CH"), DateTimeStyles.AssumeLocal, out date);
}
private static string BuildKey(Dictionary<string, object?> row, IReadOnlyList<string> keys)
=> string.Join("||", keys.Select(k => NormalizeKeyValue(row.TryGetValue(k, out var value) ? value : null)));
private static string BuildKey(Dictionary<string, object?> row, string alias, IReadOnlyList<string> keys)
=> string.Join("||", keys.Select(k =>
{
row.TryGetValue($"{alias}.{k}", out var value);
return NormalizeKeyValue(value);
}));
private static string NormalizeKeyValue(object? value) => value?.ToString()?.Trim() ?? string.Empty;
private static List<string> SplitKeys(string keys)
=> keys.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList();
}
@@ -10,26 +10,32 @@ public class SiteExportService : ISiteExportService
private readonly IDbContextFactory<AppDbContext> _dbFactory;
private readonly IHanaQueryService _hanaService;
private readonly ISapGatewayService _sapGatewayService;
private readonly ISapCompositionService _sapCompositionService;
private readonly IExcelExportService _excelService;
private readonly ISharePointUploadService _sharePointService;
private readonly IRecordTransformationService _transformationService;
private readonly ICentralSalesRecordService _centralSalesRecordService;
private readonly ILogger<SiteExportService> _logger;
public SiteExportService(
IDbContextFactory<AppDbContext> dbFactory,
IHanaQueryService hanaService,
ISapGatewayService sapGatewayService,
ISapCompositionService sapCompositionService,
IExcelExportService excelService,
ISharePointUploadService sharePointService,
IRecordTransformationService transformationService,
ICentralSalesRecordService centralSalesRecordService,
ILogger<SiteExportService> logger)
{
_dbFactory = dbFactory;
_hanaService = hanaService;
_sapGatewayService = sapGatewayService;
_sapCompositionService = sapCompositionService;
_excelService = excelService;
_sharePointService = sharePointService;
_transformationService = transformationService;
_centralSalesRecordService = centralSalesRecordService;
_logger = logger;
}
@@ -59,14 +65,25 @@ public class SiteExportService : ISiteExportService
var credentials = ResolveCredentials(site, settings, sourceSystem);
if (string.IsNullOrWhiteSpace(site.SapServiceUrl))
throw new InvalidOperationException($"Standort '{site.Land}' hat keine SAP Service URL.");
if (string.IsNullOrWhiteSpace(site.SapEntitySet))
throw new InvalidOperationException($"Standort '{site.Land}' hat kein SAP Entity Set ausgewählt.");
var sapSources = await db.SapSourceDefinitions.Where(s => s.SiteId == site.Id).ToListAsync();
var sapJoins = await db.SapJoinDefinitions.Where(j => j.SiteId == site.Id).ToListAsync();
var sapMappings = await db.SapFieldMappings.Where(m => m.SiteId == site.Id).ToListAsync();
if (sapSources.Count == 0)
throw new InvalidOperationException($"Standort '{site.Land}' hat keine SAP-Quellen konfiguriert.");
if (sapMappings.Count == 0)
throw new InvalidOperationException($"Standort '{site.Land}' hat keine SAP-Feldmappings.");
updateStatus?.Invoke("SAP Gateway Abfrage...");
var rows = await _sapGatewayService.GetEntityRowsAsync(site.SapServiceUrl, site.SapEntitySet, credentials.Username, credentials.Password);
updateStatus?.Invoke("SAP Quellen laden...");
records = await _sapCompositionService.BuildSalesRecordsAsync(site, sapSources, sapJoins, sapMappings, credentials.Username, credentials.Password);
updateStatus?.Invoke("Transformationen anwenden...");
var rules = await db.FieldTransformationRules
.Where(r => r.IsActive && r.SourceSystem == sourceSystem)
.OrderBy(r => r.SortOrder)
.ToListAsync();
_transformationService.Apply(records, rules);
updateStatus?.Invoke("Excel erstellen...");
filePath = _excelService.CreateGenericExcelFile(outputDir, $"SAP_{site.TSC}_{site.SapEntitySet}", DateTime.UtcNow.Date, site.SapEntitySet, rows);
log.RowCount = rows.Count;
filePath = _excelService.CreateExcelFile(outputDir, site.TSC, DateTime.UtcNow.Date, records);
log.RowCount = records.Count;
}
else
{
@@ -87,6 +104,9 @@ public class SiteExportService : ISiteExportService
log.RowCount = records.Count;
}
updateStatus?.Invoke("Zentrale Tabelle aktualisieren...");
await _centralSalesRecordService.ReplaceForSiteAsync(site, records);
var fileName = Path.GetFileName(filePath);
if (spConfig is not null &&