Files

342 lines
15 KiB
C#

using Microsoft.EntityFrameworkCore;
using TrafagSalesExporter.Data;
using TrafagSalesExporter.Models;
namespace TrafagSalesExporter.Services;
public interface ISettingsPageService
{
Task<SettingsPageState> LoadAsync();
Task SaveSharePointAsync(SharePointConfig config);
Task<string> BuildSharePointTestPreviewAsync(SharePointConfig config);
Task SaveExportSettingsAsync(ExportSettings settings);
Task<List<SourceSystemDefinition>> SaveSourceSystemsAsync(List<SourceSystemDefinition> sourceSystems);
Task<List<CurrencyExchangeRate>> SaveExchangeRatesAsync(List<CurrencyExchangeRate> exchangeRates);
Task<SettingsExchangeRateRefreshResult> RefreshEcbRatesAsync();
Task<string> ExportConfigurationAsync(bool includeSecrets);
Task<SettingsPageState> ImportConfigurationAsync(string json);
Task<PageActionResult> TestCentralCredentialsAsync(SourceSystemDefinition definition);
}
public sealed class SettingsPageService : ISettingsPageService
{
private readonly IDbContextFactory<AppDbContext> _dbFactory;
private readonly ISharePointUploadService _sharePointService;
private readonly TimerBackgroundService _timerService;
private readonly IHanaQueryService _hanaService;
private readonly ISapGatewayService _sapGatewayService;
private readonly IConfigTransferService _configTransferService;
private readonly IExchangeRateImportService _exchangeRateImportService;
public SettingsPageService(
IDbContextFactory<AppDbContext> dbFactory,
ISharePointUploadService sharePointService,
TimerBackgroundService timerService,
IHanaQueryService hanaService,
ISapGatewayService sapGatewayService,
IConfigTransferService configTransferService,
IExchangeRateImportService exchangeRateImportService)
{
_dbFactory = dbFactory;
_sharePointService = sharePointService;
_timerService = timerService;
_hanaService = hanaService;
_sapGatewayService = sapGatewayService;
_configTransferService = configTransferService;
_exchangeRateImportService = exchangeRateImportService;
}
public async Task<SettingsPageState> LoadAsync()
{
await using var db = await _dbFactory.CreateDbContextAsync();
return new SettingsPageState
{
SharePointConfig = await db.SharePointConfigs.FirstOrDefaultAsync() ?? new SharePointConfig(),
ExportSettings = await db.ExportSettings.FirstOrDefaultAsync() ?? new ExportSettings(),
SourceSystems = await db.SourceSystemDefinitions.OrderBy(x => x.Code).ToListAsync(),
ExchangeRates = await LoadExchangeRatesAsync(db)
};
}
public async Task SaveSharePointAsync(SharePointConfig config)
{
await using var db = await _dbFactory.CreateDbContextAsync();
var existing = await db.SharePointConfigs.FirstOrDefaultAsync();
if (existing is null)
{
db.SharePointConfigs.Add(config);
}
else
{
existing.SiteUrl = config.SiteUrl;
existing.ExportFolder = config.ExportFolder;
existing.CentralExportFolder = config.CentralExportFolder;
existing.TenantId = config.TenantId;
existing.ClientId = config.ClientId;
existing.ClientSecret = config.ClientSecret;
}
await db.SaveChangesAsync();
}
public async Task<string> BuildSharePointTestPreviewAsync(SharePointConfig config)
{
var tenantId = NormalizeConfigValue(config.TenantId);
var clientId = NormalizeConfigValue(config.ClientId);
var clientSecret = NormalizeConfigValue(config.ClientSecret);
var siteUrl = NormalizeConfigValue(config.SiteUrl);
await _sharePointService.TestConnectionAsync(tenantId, clientId, clientSecret, siteUrl);
return BuildSharePointTestPreview(tenantId, clientId, clientSecret, siteUrl);
}
public async Task SaveExportSettingsAsync(ExportSettings settings)
{
await using var db = await _dbFactory.CreateDbContextAsync();
var existing = await db.ExportSettings.FirstOrDefaultAsync();
if (existing is null)
{
settings.ExchangeRateDateField = NormalizeExchangeRateDateField(settings.ExchangeRateDateField);
db.ExportSettings.Add(settings);
}
else
{
existing.DateFilter = settings.DateFilter;
existing.TimerHour = settings.TimerHour;
existing.TimerMinute = settings.TimerMinute;
existing.TimerEnabled = settings.TimerEnabled;
existing.DebugLoggingEnabled = settings.DebugLoggingEnabled;
existing.LocalSiteExportFolder = settings.LocalSiteExportFolder;
existing.LocalConsolidatedExportFolder = settings.LocalConsolidatedExportFolder;
existing.AuditCsvEnabled = settings.AuditCsvEnabled;
existing.UseAuditCsvAsCentralSource = settings.UseAuditCsvAsCentralSource;
existing.LocalAuditCsvFolder = settings.LocalAuditCsvFolder;
existing.ExchangeRateDateField = NormalizeExchangeRateDateField(settings.ExchangeRateDateField);
}
await db.SaveChangesAsync();
_timerService.Recalculate();
}
public async Task<List<SourceSystemDefinition>> SaveSourceSystemsAsync(List<SourceSystemDefinition> sourceSystems)
{
var normalized = sourceSystems
.Select(x => new SourceSystemDefinition
{
Id = x.Id,
Code = NormalizeSourceSystemCode(x.Code),
DisplayName = NormalizeConfigValue(x.DisplayName),
ConnectionKind = NormalizeConnectionKind(x.ConnectionKind),
IsActive = x.IsActive,
CentralServiceUrl = NormalizeConfigValue(x.CentralServiceUrl),
CentralUsername = NormalizeConfigValue(x.CentralUsername),
CentralPassword = x.CentralPassword ?? string.Empty
})
.Where(x => !string.IsNullOrWhiteSpace(x.Code))
.ToList();
if (normalized.Any(x => string.IsNullOrWhiteSpace(x.DisplayName)))
throw new InvalidOperationException("Jedes Quellsystem braucht einen Anzeigenamen.");
var duplicates = normalized.GroupBy(x => x.Code).FirstOrDefault(g => g.Count() > 1);
if (duplicates is not null)
throw new InvalidOperationException($"Quellsystem-Code doppelt vorhanden: {duplicates.Key}");
await using var db = await _dbFactory.CreateDbContextAsync();
var existing = await db.SourceSystemDefinitions.ToListAsync();
if (existing.Count > 0)
db.SourceSystemDefinitions.RemoveRange(existing);
db.SourceSystemDefinitions.AddRange(normalized);
await db.SaveChangesAsync();
return await db.SourceSystemDefinitions.OrderBy(x => x.Code).ToListAsync();
}
public async Task<List<CurrencyExchangeRate>> SaveExchangeRatesAsync(List<CurrencyExchangeRate> exchangeRates)
{
await using var db = await _dbFactory.CreateDbContextAsync();
var existingRates = await db.CurrencyExchangeRates.ToListAsync();
if (existingRates.Count > 0)
db.CurrencyExchangeRates.RemoveRange(existingRates);
db.CurrencyExchangeRates.AddRange(exchangeRates.Select(rate => new CurrencyExchangeRate
{
FromCurrency = NormalizeConfigValue(rate.FromCurrency).ToUpperInvariant(),
ToCurrency = NormalizeConfigValue(rate.ToCurrency).ToUpperInvariant(),
Rate = rate.Rate,
ValidFrom = rate.ValidFrom.Date,
ValidTo = rate.ValidTo?.Date,
Notes = NormalizeConfigValue(rate.Notes),
IsActive = rate.IsActive
}).Where(rate => !string.IsNullOrWhiteSpace(rate.FromCurrency)
&& !string.IsNullOrWhiteSpace(rate.ToCurrency)
&& rate.Rate > 0m));
await db.SaveChangesAsync();
return await LoadExchangeRatesAsync(db);
}
public async Task<SettingsExchangeRateRefreshResult> RefreshEcbRatesAsync()
{
var result = await _exchangeRateImportService.RefreshEcbRatesAsync();
await using var db = await _dbFactory.CreateDbContextAsync();
return new SettingsExchangeRateRefreshResult
{
ImportedCount = result.ImportedCount,
RateDate = result.RateDate,
ExchangeRates = await LoadExchangeRatesAsync(db)
};
}
public Task<string> ExportConfigurationAsync(bool includeSecrets)
=> _configTransferService.ExportJsonAsync(includeSecrets);
public async Task<SettingsPageState> ImportConfigurationAsync(string json)
{
await _configTransferService.ImportJsonAsync(json);
_timerService.Recalculate();
return await LoadAsync();
}
public async Task<PageActionResult> TestCentralCredentialsAsync(SourceSystemDefinition definition)
{
if (string.Equals(definition.ConnectionKind, SourceSystemConnectionKinds.SapGateway, StringComparison.OrdinalIgnoreCase))
return await TestCentralSapCredentialsAsync(definition);
if (string.Equals(definition.ConnectionKind, SourceSystemConnectionKinds.Hana, StringComparison.OrdinalIgnoreCase))
return await TestCentralHanaCredentialsAsync(definition);
return PageActionResult.WarningResult($"Quellsystem '{definition.Code}' hat keinen testbaren Verbindungstyp.");
}
private async Task<PageActionResult> TestCentralHanaCredentialsAsync(SourceSystemDefinition definition)
{
var sourceSystem = definition.Code;
var username = definition.CentralUsername;
var password = definition.CentralPassword;
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
return PageActionResult.WarningResult($"Fuer {sourceSystem} sind keine zentralen Zugangsdaten gepflegt.");
await using var db = await _dbFactory.CreateDbContextAsync();
var centralServer = await db.HanaServers
.Where(s => s.SourceSystem == sourceSystem)
.OrderBy(s => s.Id)
.FirstOrDefaultAsync();
if (centralServer is null || string.IsNullOrWhiteSpace(centralServer.Host))
return PageActionResult.WarningResult($"Keine zentrale HANA-Konfiguration fuer {sourceSystem} gefunden.");
var testServer = new HanaServer
{
SourceSystem = sourceSystem,
Name = $"{sourceSystem} Central Test",
Host = centralServer.Host,
Port = centralServer.Port,
Username = username.Trim(),
Password = password.Trim(),
DatabaseName = centralServer.DatabaseName,
UseSsl = centralServer.UseSsl,
ValidateCertificate = centralServer.ValidateCertificate,
AdditionalParams = centralServer.AdditionalParams
};
var result = await _hanaService.TestConnectionDetailedAsync(testServer);
return result.Success
? PageActionResult.SuccessResult($"{sourceSystem}: Zentrale HANA-Verbindung erfolgreich.")
: PageActionResult.ErrorResult($"{sourceSystem}: {result.ExceptionType} - {result.ErrorMessage}");
}
private async Task<PageActionResult> TestCentralSapCredentialsAsync(SourceSystemDefinition definition)
{
var sourceSystem = definition.Code;
var username = definition.CentralUsername;
var password = definition.CentralPassword;
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
return PageActionResult.WarningResult("Fuer SAP sind keine zentralen Gateway-Zugangsdaten gepflegt.");
if (string.IsNullOrWhiteSpace(definition.CentralServiceUrl))
return PageActionResult.WarningResult($"Fuer {sourceSystem} ist keine zentrale SAP Service URL gepflegt.");
try
{
await _sapGatewayService.TestConnectionAsync(definition.CentralServiceUrl, username.Trim(), password.Trim());
return PageActionResult.SuccessResult($"{sourceSystem}: Zentrale SAP Gateway-Verbindung erfolgreich.");
}
catch (Exception ex)
{
return PageActionResult.ErrorResult($"{sourceSystem}: {ex.Message}");
}
}
private static async Task<List<CurrencyExchangeRate>> LoadExchangeRatesAsync(AppDbContext db)
=> await db.CurrencyExchangeRates
.OrderBy(x => x.FromCurrency)
.ThenBy(x => x.ToCurrency)
.ThenByDescending(x => x.ValidFrom)
.ToListAsync();
public static string NormalizeSourceSystemCode(string? code) => NormalizeConfigValue(code).ToUpperInvariant();
public static string NormalizeConnectionKind(string? connectionKind)
=> SourceSystemConnectionKinds.All.Contains(connectionKind ?? string.Empty, StringComparer.OrdinalIgnoreCase)
? (connectionKind ?? string.Empty).Trim().ToUpperInvariant()
: SourceSystemConnectionKinds.Hana;
public static string NormalizeConfigValue(string? value) => value?.Trim() ?? string.Empty;
public static string NormalizeExchangeRateDateField(string? value)
{
var normalized = NormalizeConfigValue(value);
return normalized switch
{
ExchangeRateDateFields.PostingDate => ExchangeRateDateFields.PostingDate,
ExchangeRateDateFields.InvoiceDate => ExchangeRateDateFields.InvoiceDate,
ExchangeRateDateFields.ExtractionDate => ExchangeRateDateFields.ExtractionDate,
_ => ExchangeRateDateFields.PostingDate
};
}
public static string BuildSharePointTestPreview(string tenantId, string clientId, string clientSecret, string siteUrl)
{
var maskedSecret = string.IsNullOrEmpty(clientSecret)
? "<leer>"
: $"{new string('*', Math.Min(clientSecret.Length, 8))} (len={clientSecret.Length})";
return string.Join(Environment.NewLine,
[
$"Tenant ID: {tenantId}",
$"Client ID: {clientId}",
$"Client Secret: {maskedSecret}",
$"Site URL: {siteUrl}"
]);
}
}
public sealed class SettingsPageState
{
public SharePointConfig SharePointConfig { get; set; } = new();
public ExportSettings ExportSettings { get; set; } = new();
public List<SourceSystemDefinition> SourceSystems { get; set; } = [];
public List<CurrencyExchangeRate> ExchangeRates { get; set; } = [];
}
public sealed class SettingsExchangeRateRefreshResult
{
public int ImportedCount { get; set; }
public DateTime RateDate { get; set; }
public List<CurrencyExchangeRate> ExchangeRates { get; set; } = [];
}
public sealed class PageActionResult
{
public bool Success { get; init; }
public bool Warning { get; init; }
public string Message { get; init; } = string.Empty;
public static PageActionResult SuccessResult(string message) => new() { Success = true, Message = message };
public static PageActionResult WarningResult(string message) => new() { Warning = true, Message = message };
public static PageActionResult ErrorResult(string message) => new() { Message = message };
}