manometer

This commit is contained in:
2026-04-17 12:00:03 +02:00
parent bec0410ef4
commit eb187cdc15
15 changed files with 1817 additions and 43 deletions
@@ -158,19 +158,21 @@ public class ConfigTransferService : IConfigTransferService
{
var package = JsonSerializer.Deserialize<ConfigTransferPackage>(json, JsonOptions)
?? throw new InvalidOperationException("Konfigurationsdatei konnte nicht gelesen werden.");
var importedSourceSystems = ResolveImportedSourceSystems(json, package);
using var db = await _dbFactory.CreateDbContextAsync();
await using var transaction = await db.Database.BeginTransactionAsync();
var existingSharePoint = await db.SharePointConfigs.FirstOrDefaultAsync();
var existingSettings = await db.ExportSettings.FirstOrDefaultAsync();
var existingSourceSystems = await db.SourceSystemDefinitions.ToListAsync();
var existingServers = await db.HanaServers.ToListAsync();
var existingExchangeRates = await db.CurrencyExchangeRates.ToListAsync();
var existingSites = await db.Sites.ToListAsync();
var existingCentralRecords = await db.CentralSalesRecords.AsNoTracking().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 preservedSourceSystemSecrets = existingSourceSystems.ToDictionary(
@@ -180,13 +182,15 @@ public class ConfigTransferService : IConfigTransferService
var preservedSiteSecrets = existingSites.ToDictionary(
x => BuildSiteSignature(x.Land, x.TSC, x.Schema, x.SourceSystem),
x => (x.UsernameOverride, x.PasswordOverride));
var existingSiteSignaturesById = existingSites.ToDictionary(
x => x.Id,
x => BuildSiteSignature(x.Land, x.TSC, x.Schema, x.SourceSystem));
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 (existingExchangeRates.Count > 0) db.CurrencyExchangeRates.RemoveRange(existingExchangeRates);
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 (existingSourceSystems.Count > 0) db.SourceSystemDefinitions.RemoveRange(existingSourceSystems);
@@ -217,10 +221,6 @@ public class ConfigTransferService : IConfigTransferService
LocalConsolidatedExportFolder = importedSettings.LocalConsolidatedExportFolder
});
var importedSourceSystems = package.SourceSystemDefinitions.Count > 0
? package.SourceSystemDefinitions
: BuildDefaultSourceSystems();
foreach (var sourceSystem in importedSourceSystems)
{
preservedSourceSystemSecrets.TryGetValue(sourceSystem.Code, out var preserved);
@@ -272,6 +272,7 @@ public class ConfigTransferService : IConfigTransferService
}
var siteIdMap = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
var importedSiteIdBySignature = 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);
@@ -298,8 +299,52 @@ public class ConfigTransferService : IConfigTransferService
db.Sites.Add(entity);
await db.SaveChangesAsync();
siteIdMap[site.Key] = entity.Id;
importedSiteIdBySignature[BuildSiteSignature(site.Land, site.TSC, site.Schema, site.SourceSystem)] = entity.Id;
}
var centralRecordsToPreserve = existingCentralRecords
.Where(record => existingSiteSignaturesById.TryGetValue(record.SiteId, out var signature) && importedSiteIdBySignature.ContainsKey(signature))
.Select(record =>
{
var signature = existingSiteSignaturesById[record.SiteId];
return new CentralSalesRecord
{
StoredAtUtc = record.StoredAtUtc,
SiteId = importedSiteIdBySignature[signature],
SourceSystem = record.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
};
})
.ToList();
if (centralRecordsToPreserve.Count > 0)
db.CentralSalesRecords.AddRange(centralRecordsToPreserve);
if (package.FieldTransformationRules.Count > 0)
{
db.FieldTransformationRules.AddRange(package.FieldTransformationRules.Select(r => new FieldTransformationRule
@@ -363,10 +408,53 @@ public class ConfigTransferService : IConfigTransferService
}
await db.SaveChangesAsync();
await transaction.CommitAsync();
}
private static string BuildSiteSignature(string land, string tsc, string schema, string sourceSystem)
=> $"{land}|{tsc}|{schema}|{sourceSystem}".ToUpperInvariant();
private static List<ConfigTransferSourceSystemDefinition> ResolveImportedSourceSystems(string json, ConfigTransferPackage package)
{
if (package.SourceSystemDefinitions.Count == 0)
return BuildDefaultSourceSystems();
using var document = JsonDocument.Parse(json);
if (!document.RootElement.TryGetProperty(nameof(ConfigTransferPackage.SourceSystemDefinitions), out var sourceSystemsElement) ||
sourceSystemsElement.ValueKind != JsonValueKind.Array)
{
return package.SourceSystemDefinitions;
}
var imported = package.SourceSystemDefinitions
.Select((sourceSystem, index) =>
{
var hasExplicitConnectionKind =
index < sourceSystemsElement.GetArrayLength() &&
sourceSystemsElement[index].TryGetProperty(nameof(ConfigTransferSourceSystemDefinition.ConnectionKind), out _);
if (hasExplicitConnectionKind)
return sourceSystem;
sourceSystem.ConnectionKind = InferLegacyConnectionKind(sourceSystem.Code);
return sourceSystem;
})
.ToList();
return imported;
}
private static string InferLegacyConnectionKind(string code)
{
if (string.Equals(code, "SAP", StringComparison.OrdinalIgnoreCase))
return SourceSystemConnectionKinds.SapGateway;
if (string.Equals(code, "MANUAL_EXCEL", StringComparison.OrdinalIgnoreCase))
return SourceSystemConnectionKinds.ManualExcel;
return SourceSystemConnectionKinds.Hana;
}
private static List<ConfigTransferSourceSystemDefinition> BuildDefaultSourceSystems()
{
return
@@ -48,7 +48,7 @@ public class DatabaseInitializationService : IDatabaseInitializationService
EnsureSitesTableSupportsOptionalHanaServer(db);
EnsureExportSettingsTableSupportsCurrentSchema(db);
EnsureHanaServersTableSupportsCurrentSchema(db);
RepairBrokenSiteForeignKeys(db);
RepairBrokenForeignKeys(db);
AddColumnIfMissing(db, "HanaServers", "SourceSystem", "TEXT NOT NULL DEFAULT ''");
AddColumnIfMissing(db, "HanaServers", "DatabaseName", "TEXT NOT NULL DEFAULT ''");
AddColumnIfMissing(db, "HanaServers", "UseSsl", "INTEGER NOT NULL DEFAULT 0");
@@ -166,26 +166,7 @@ public class DatabaseInitializationService : IDatabaseInitializationService
using (var create = conn.CreateCommand())
{
create.Transaction = transaction;
create.CommandText = @"
CREATE TABLE Sites (
Id INTEGER NOT NULL CONSTRAINT PK_Sites PRIMARY KEY AUTOINCREMENT,
HanaServerId INTEGER NULL,
Schema TEXT NOT NULL,
TSC TEXT NOT NULL,
Land TEXT NOT NULL,
SourceSystem TEXT NOT NULL DEFAULT 'SAP',
UsernameOverride TEXT NOT NULL DEFAULT '',
PasswordOverride TEXT NOT NULL DEFAULT '',
LocalExportFolderOverride TEXT NOT NULL DEFAULT '',
ManualImportFilePath TEXT NOT NULL DEFAULT '',
ManualImportLastUploadedAtUtc TEXT NULL,
SapServiceUrl TEXT NOT NULL DEFAULT '',
SapEntitySet TEXT NOT NULL DEFAULT '',
SapEntitySetsCache TEXT NOT NULL DEFAULT '',
SapEntitySetsRefreshedAtUtc TEXT NULL,
IsActive INTEGER NOT NULL,
CONSTRAINT FK_Sites_HanaServers_HanaServerId FOREIGN KEY (HanaServerId) REFERENCES HanaServers (Id)
);";
create.CommandText = GetSitesCreateSql();
create.ExecuteNonQuery();
}
@@ -195,8 +176,9 @@ CREATE TABLE Sites (
copy.CommandText = @"
INSERT INTO Sites (
Id, HanaServerId, Schema, TSC, Land, SourceSystem,
UsernameOverride, PasswordOverride, LocalExportFolderOverride, SapServiceUrl, SapEntitySet,
ManualImportFilePath, ManualImportLastUploadedAtUtc, SapEntitySetsCache, SapEntitySetsRefreshedAtUtc, IsActive
UsernameOverride, PasswordOverride, LocalExportFolderOverride, ManualImportFilePath,
ManualImportLastUploadedAtUtc, SapServiceUrl, SapEntitySet, SapEntitySetsCache,
SapEntitySetsRefreshedAtUtc, IsActive
)
SELECT
Id, HanaServerId, Schema, TSC, Land,
@@ -229,13 +211,13 @@ FROM Sites_old;";
enableFk.ExecuteNonQuery();
}
private static void RepairBrokenSiteForeignKeys(AppDbContext db)
private static void RepairBrokenForeignKeys(AppDbContext db)
{
var conn = db.Database.GetDbConnection();
if (conn.State != ConnectionState.Open)
conn.Open();
var tablesToRepair = new[]
var siteDependentTables = new[]
{
("ExportLogs", GetExportLogsCreateSql()),
("AppEventLogs", GetAppEventLogsCreateSql()),
@@ -245,14 +227,17 @@ FROM Sites_old;";
("SapFieldMappings", GetSapFieldMappingsCreateSql())
};
foreach (var (tableName, createSql) in tablesToRepair)
foreach (var (tableName, createSql) in siteDependentTables)
{
if (TableReferencesSitesOld(conn, tableName))
if (TableReferences(conn, tableName, "Sites_old"))
RebuildTable(conn, tableName, createSql);
}
if (TableReferences(conn, "Sites", "HanaServers_repair_old"))
RebuildTable(conn, "Sites", GetSitesCreateSql());
}
private static bool TableReferencesSitesOld(System.Data.Common.DbConnection connection, string tableName)
private static bool TableReferences(System.Data.Common.DbConnection connection, string tableName, string referencedTableName)
{
using var command = connection.CreateCommand();
command.CommandText = "SELECT sql FROM sqlite_master WHERE type = 'table' AND name = $tableName;";
@@ -263,7 +248,7 @@ FROM Sites_old;";
command.Parameters.Add(parameter);
var sql = command.ExecuteScalar()?.ToString() ?? string.Empty;
return sql.Contains("Sites_old", StringComparison.OrdinalIgnoreCase);
return sql.Contains(referencedTableName, StringComparison.OrdinalIgnoreCase);
}
private static void RebuildTable(System.Data.Common.DbConnection connection, string tableName, string createSql)
@@ -383,6 +368,27 @@ CREATE TABLE HanaServers (
AdditionalParams TEXT NOT NULL DEFAULT ''
);";
private static string GetSitesCreateSql() => @"
CREATE TABLE Sites (
Id INTEGER NOT NULL CONSTRAINT PK_Sites PRIMARY KEY AUTOINCREMENT,
HanaServerId INTEGER NULL,
Schema TEXT NOT NULL,
TSC TEXT NOT NULL,
Land TEXT NOT NULL,
SourceSystem TEXT NOT NULL DEFAULT 'SAP',
UsernameOverride TEXT NOT NULL DEFAULT '',
PasswordOverride TEXT NOT NULL DEFAULT '',
LocalExportFolderOverride TEXT NOT NULL DEFAULT '',
ManualImportFilePath TEXT NOT NULL DEFAULT '',
ManualImportLastUploadedAtUtc TEXT NULL,
SapServiceUrl TEXT NOT NULL DEFAULT '',
SapEntitySet TEXT NOT NULL DEFAULT '',
SapEntitySetsCache TEXT NOT NULL DEFAULT '',
SapEntitySetsRefreshedAtUtc TEXT NULL,
IsActive INTEGER NOT NULL,
CONSTRAINT FK_Sites_HanaServers_HanaServerId FOREIGN KEY (HanaServerId) REFERENCES HanaServers (Id)
);";
private static string GetAppEventLogsCreateSql() => @"
CREATE TABLE AppEventLogs (
Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
@@ -3,5 +3,6 @@ namespace TrafagSalesExporter.Services;
public interface ISharePointUploadService
{
Task UploadAsync(string tenantId, string clientId, string clientSecret, string siteUrl, string exportFolder, string land, string localFilePath);
Task<string> DownloadToTempFileAsync(string tenantId, string clientId, string clientSecret, string siteUrl, string fileReference);
Task TestConnectionAsync(string tenantId, string clientId, string clientSecret, string siteUrl);
}
@@ -43,6 +43,45 @@ public class SharePointUploadService : ISharePointUploadService
await graphClient.Drives[drive.Id].Root.ItemWithPath(remotePath).Content.PutAsync(stream);
}
public async Task<string> DownloadToTempFileAsync(string tenantId, string clientId, string clientSecret, string siteUrl, string fileReference)
{
var normalizedTenantId = Normalize(tenantId);
var normalizedClientId = Normalize(clientId);
var normalizedClientSecret = Normalize(clientSecret);
var normalizedSiteUrl = Normalize(siteUrl);
var normalizedReference = Normalize(fileReference);
if (string.IsNullOrWhiteSpace(normalizedReference))
throw new InvalidOperationException("SharePoint-Dateireferenz fehlt.");
var credential = new ClientSecretCredential(normalizedTenantId, normalizedClientId, normalizedClientSecret);
var graphClient = new GraphServiceClient(credential, ["https://graph.microsoft.com/.default"]);
var siteUri = new Uri(normalizedSiteUrl);
var sitePath = siteUri.AbsolutePath.TrimEnd('/');
var site = await graphClient.Sites[$"{siteUri.Host}:{sitePath}"].GetAsync();
if (site?.Id is null)
throw new InvalidOperationException("SharePoint Site konnte nicht gefunden werden.");
var drive = await graphClient.Sites[site.Id].Drive.GetAsync();
if (drive?.Id is null)
throw new InvalidOperationException("SharePoint Dokumentenbibliothek konnte nicht gefunden werden.");
var remotePath = ResolveRemotePath(normalizedReference, siteUri);
var fileName = Path.GetFileName(remotePath);
if (string.IsNullOrWhiteSpace(fileName))
throw new InvalidOperationException("Aus der SharePoint-Dateireferenz konnte kein Dateiname gelesen werden.");
await using var contentStream = await graphClient.Drives[drive.Id].Root.ItemWithPath(remotePath).Content.GetAsync()
?? throw new InvalidOperationException("SharePoint-Datei konnte nicht gelesen werden.");
var tempPath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}_{fileName}");
await using var targetStream = File.Create(tempPath);
await contentStream.CopyToAsync(targetStream);
return tempPath;
}
public async Task TestConnectionAsync(string tenantId, string clientId, string clientSecret, string siteUrl)
{
var normalizedTenantId = Normalize(tenantId);
@@ -86,6 +125,24 @@ public class SharePointUploadService : ISharePointUploadService
private static string Normalize(string value) => value?.Trim() ?? string.Empty;
private static string ResolveRemotePath(string fileReference, Uri siteUri)
{
if (Uri.TryCreate(fileReference, UriKind.Absolute, out var fileUri))
{
if (!string.Equals(fileUri.Host, siteUri.Host, StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException("Die SharePoint-Datei muss auf derselben SharePoint-Site liegen wie die zentrale Konfiguration.");
var sitePath = siteUri.AbsolutePath.TrimEnd('/');
var absolutePath = Uri.UnescapeDataString(fileUri.AbsolutePath);
if (absolutePath.StartsWith(sitePath, StringComparison.OrdinalIgnoreCase))
absolutePath = absolutePath[sitePath.Length..];
return absolutePath.Trim('/').Trim();
}
return fileReference.Trim('/').Trim();
}
private static string BuildInputPreview(string tenantId, string clientId, string clientSecret, string siteUrl)
{
var maskedSecret = string.IsNullOrEmpty(clientSecret)
@@ -110,13 +110,48 @@ public class SiteExportService : ISiteExportService
{
if (string.IsNullOrWhiteSpace(site.ManualImportFilePath))
throw new InvalidOperationException($"Standort '{site.Land}' hat keine manuelle Excel-Datei.");
if (!File.Exists(site.ManualImportFilePath))
throw new InvalidOperationException($"Die manuelle Excel-Datei wurde nicht gefunden: {site.ManualImportFilePath}");
string? tempManualImportPath = null;
try
{
var manualImportPath = site.ManualImportFilePath.Trim();
if (File.Exists(manualImportPath))
{
filePath = manualImportPath;
}
else if (LooksLikeSharePointReference(manualImportPath))
{
if (spConfig is null ||
string.IsNullOrWhiteSpace(spConfig.TenantId) ||
string.IsNullOrWhiteSpace(spConfig.ClientId) ||
string.IsNullOrWhiteSpace(spConfig.ClientSecret) ||
string.IsNullOrWhiteSpace(spConfig.SiteUrl))
{
throw new InvalidOperationException("Fuer SharePoint-Manuellimport fehlt eine vollstaendige SharePoint-Konfiguration in Settings.");
}
updateStatus?.Invoke("Manuelle Excel lesen...");
await _appEventLogService.WriteAsync("Export", "Manuelle Excel lesen", siteId: site.Id, land: site.Land,
details: site.ManualImportFilePath);
records = await _manualExcelImportService.ReadSalesRecordsAsync(site.ManualImportFilePath, site);
updateStatus?.Invoke("Manuelle Excel von SharePoint laden...");
await _appEventLogService.WriteAsync("Export", "Manuelle Excel von SharePoint laden", siteId: site.Id, land: site.Land,
details: manualImportPath);
tempManualImportPath = await _sharePointService.DownloadToTempFileAsync(
spConfig.TenantId, spConfig.ClientId, spConfig.ClientSecret, spConfig.SiteUrl, manualImportPath);
filePath = manualImportPath;
}
else
{
throw new InvalidOperationException($"Die manuelle Excel-Datei wurde nicht gefunden: {manualImportPath}");
}
var readPath = tempManualImportPath ?? filePath;
updateStatus?.Invoke("Manuelle Excel lesen...");
await _appEventLogService.WriteAsync("Export", "Manuelle Excel lesen", siteId: site.Id, land: site.Land,
details: filePath);
records = await _manualExcelImportService.ReadSalesRecordsAsync(readPath, site);
}
finally
{
if (!string.IsNullOrWhiteSpace(tempManualImportPath) && File.Exists(tempManualImportPath))
File.Delete(tempManualImportPath);
}
updateStatus?.Invoke("Transformationen anwenden...");
await _appEventLogService.WriteAsync("Export", "Transformationen anwenden", siteId: site.Id, land: site.Land,
@@ -127,7 +162,6 @@ public class SiteExportService : ISiteExportService
.ToListAsync();
_transformationService.Apply(records, rules);
filePath = site.ManualImportFilePath;
log.RowCount = records.Count;
}
else
@@ -272,6 +306,12 @@ public class SiteExportService : ISiteExportService
: configured;
}
private static bool LooksLikeSharePointReference(string path)
=> path.StartsWith("http://", StringComparison.OrdinalIgnoreCase) ||
path.StartsWith("https://", StringComparison.OrdinalIgnoreCase) ||
path.StartsWith("/Shared Documents/", StringComparison.OrdinalIgnoreCase) ||
path.StartsWith("Shared Documents/", StringComparison.OrdinalIgnoreCase);
private static Site CloneSiteWithSapServiceUrl(Site site, string sapServiceUrl)
{
return new Site