From 957fdb7dc886c88d1850bbcc58feaef657b3fec1 Mon Sep 17 00:00:00 2001 From: metacube Date: Thu, 11 Jun 2026 10:38:15 +0200 Subject: [PATCH] Upload audit CSV with site exports --- .../Services/SiteExportService.cs | 14 +- .../SiteExportServiceTests.cs | 225 ++++++++++++++++++ 2 files changed, 238 insertions(+), 1 deletion(-) create mode 100644 TrafagSalesExporter/TrafagSalesExporter.Tests/SiteExportServiceTests.cs diff --git a/TrafagSalesExporter/Services/SiteExportService.cs b/TrafagSalesExporter/Services/SiteExportService.cs index ace38c4..8737d92 100644 --- a/TrafagSalesExporter/Services/SiteExportService.cs +++ b/TrafagSalesExporter/Services/SiteExportService.cs @@ -106,7 +106,7 @@ public class SiteExportService : ISiteExportService details: $"Records={records.Count}"); await _centralSalesRecordService.ReplaceForSiteAsync(site, records, updateStatus); - await UploadToSharePointIfConfiguredAsync(site, spConfig, filePath, updateStatus, fetchResult); + await UploadToSharePointIfConfiguredAsync(site, spConfig, filePath, auditCsvPath, updateStatus, fetchResult); sw.Stop(); log.Status = "OK"; @@ -171,6 +171,7 @@ public class SiteExportService : ISiteExportService Site site, SharePointConfig? spConfig, string filePath, + string? auditCsvPath, Action? updateStatus, DataSourceFetchResult fetchResult) { @@ -191,6 +192,17 @@ public class SiteExportService : ISiteExportService await _sharePointService.UploadAsync( spConfig.TenantId, spConfig.ClientId, spConfig.ClientSecret, spConfig.SiteUrl, uploadFolder, uploadLand, filePath); + + if (string.IsNullOrWhiteSpace(auditCsvPath) || !File.Exists(auditCsvPath)) + return; + + updateStatus?.Invoke("Audit-CSV SharePoint Upload..."); + await _appEventLogService.WriteAsync("Export", "Audit-CSV SharePoint Upload gestartet", + siteId: site.Id, land: site.Land, + details: $"{spConfig.SiteUrl} | {uploadFolder}"); + await _sharePointService.UploadAsync( + spConfig.TenantId, spConfig.ClientId, spConfig.ClientSecret, + spConfig.SiteUrl, uploadFolder, uploadLand, auditCsvPath); } private static string NormalizeSourceSystem(string? sourceSystem) diff --git a/TrafagSalesExporter/TrafagSalesExporter.Tests/SiteExportServiceTests.cs b/TrafagSalesExporter/TrafagSalesExporter.Tests/SiteExportServiceTests.cs new file mode 100644 index 0000000..112c569 --- /dev/null +++ b/TrafagSalesExporter/TrafagSalesExporter.Tests/SiteExportServiceTests.cs @@ -0,0 +1,225 @@ +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging.Abstractions; +using TrafagSalesExporter.Data; +using TrafagSalesExporter.Models; +using TrafagSalesExporter.Services; +using TrafagSalesExporter.Services.DataSources; + +namespace TrafagSalesExporter.Tests; + +public sealed class SiteExportServiceTests : IDisposable +{ + private readonly string _tempDirectory; + + public SiteExportServiceTests() + { + _tempDirectory = Path.Combine("C:\\TMP", $"trafag-site-export-tests-{Guid.NewGuid():N}"); + Directory.CreateDirectory(_tempDirectory); + } + + public void Dispose() + { + if (Directory.Exists(_tempDirectory)) + Directory.Delete(_tempDirectory, recursive: true); + } + + [Fact] + public async Task ExportAsync_Uploads_AuditCsv_To_Same_SharePoint_Target_As_Excel() + { + await using var connection = new SqliteConnection("DataSource=:memory:"); + await connection.OpenAsync(); + var options = new DbContextOptionsBuilder() + .UseSqlite(connection) + .Options; + + await using (var db = new AppDbContext(options)) + { + await db.Database.EnsureCreatedAsync(); + db.ExportSettings.Add(new ExportSettings + { + AuditCsvEnabled = true, + LocalSiteExportFolder = _tempDirectory + }); + db.SharePointConfigs.Add(new SharePointConfig + { + TenantId = "tenant", + ClientId = "client", + ClientSecret = "secret", + SiteUrl = "https://trafagag.sharepoint.com/sites/WorldwideBIPlatform", + ExportFolder = "Import/Finance" + }); + db.SourceSystemDefinitions.Add(new SourceSystemDefinition + { + Code = "MANUAL_EXCEL", + DisplayName = "Manual Excel", + ConnectionKind = SourceSystemConnectionKinds.ManualExcel, + IsActive = true + }); + await db.SaveChangesAsync(); + } + + var sharePoint = new RecordingSharePointUploadService(); + var service = new SiteExportService( + new TestDbContextFactory(options), + new FixedDataSourceAdapterResolver(new FixedDataSourceAdapter(new DataSourceFetchResult + { + Records = + [ + new SalesRecord + { + SourceSystem = "MANUAL_EXCEL", + ExtractionDate = new DateTime(2026, 6, 11, 8, 0, 0, DateTimeKind.Utc), + Tsc = "TRSE", + Land = "Spanien", + InvoiceNumber = "ES-1", + SalesPriceValue = 100m, + SalesCurrency = "EUR", + InvoiceDate = new DateTime(2026, 6, 10), + DocumentType = "Invoice" + } + ], + SharePointUploadFolderOverride = "Import/Finance/Spanien", + SharePointUploadLandOverride = string.Empty + })), + new FileWritingExcelExportService(), + sharePoint, + new NoopRecordTransformationService(), + new NoopCentralSalesRecordService(), + new ExportAuditCsvService(), + new NoopAppEventLogService(), + NullLogger.Instance); + + var result = await service.ExportAsync(new Site + { + Id = 7, + TSC = "TRSE", + Land = "Spanien", + SourceSystem = "MANUAL_EXCEL", + IsActive = true + }); + + Assert.NotNull(result.FilePath); + Assert.True(File.Exists(result.FilePath)); + var auditCsv = Directory.GetFiles(_tempDirectory, "Sales_TRSE_*.csv").Single(); + Assert.True(File.Exists(auditCsv)); + + Assert.Equal(2, sharePoint.Uploads.Count); + Assert.EndsWith(".xlsx", sharePoint.Uploads[0].FileName, StringComparison.OrdinalIgnoreCase); + Assert.EndsWith(".csv", sharePoint.Uploads[1].FileName, StringComparison.OrdinalIgnoreCase); + Assert.All(sharePoint.Uploads, upload => + { + Assert.Equal("Import/Finance/Spanien", upload.ExportFolder); + Assert.Equal(string.Empty, upload.Land); + }); + } + + private sealed class TestDbContextFactory : IDbContextFactory + { + private readonly DbContextOptions _options; + + public TestDbContextFactory(DbContextOptions options) + { + _options = options; + } + + public AppDbContext CreateDbContext() => new(_options); + + public Task CreateDbContextAsync(CancellationToken cancellationToken = default) + => Task.FromResult(new AppDbContext(_options)); + } + + private sealed class FixedDataSourceAdapterResolver : IDataSourceAdapterResolver + { + private readonly IDataSourceAdapter _adapter; + + public FixedDataSourceAdapterResolver(IDataSourceAdapter adapter) + { + _adapter = adapter; + } + + public IDataSourceAdapter Resolve(string connectionKind) => _adapter; + } + + private sealed class FixedDataSourceAdapter : IDataSourceAdapter + { + private readonly DataSourceFetchResult _result; + + public FixedDataSourceAdapter(DataSourceFetchResult result) + { + _result = result; + } + + public string ConnectionKind => SourceSystemConnectionKinds.ManualExcel; + + public Task FetchAsync(DataSourceFetchContext context) + => Task.FromResult(_result); + } + + private sealed class FileWritingExcelExportService : IExcelExportService + { + public string CreateExcelFile(string outputDirectory, string tsc, DateTime fileDate, List records) + { + Directory.CreateDirectory(outputDirectory); + var path = Path.Combine(outputDirectory, $"Sales_{tsc}_{fileDate:yyyy-MM-dd}.xlsx"); + File.WriteAllText(path, "excel"); + return path; + } + + public string CreateConsolidatedExcelFile(string outputDirectory, DateTime fileDate, List records) + => throw new NotSupportedException(); + + public string CreateGenericExcelFile(string outputDirectory, string filePrefix, DateTime fileDate, string worksheetName, IReadOnlyList> rows) + => throw new NotSupportedException(); + } + + private sealed class RecordingSharePointUploadService : ISharePointUploadService + { + public List Uploads { get; } = []; + + public Task UploadAsync(string tenantId, string clientId, string clientSecret, string siteUrl, string exportFolder, string land, string localFilePath, bool uploadTimestampedCopyIfLocked = false) + { + Uploads.Add(new UploadCall(exportFolder, land, Path.GetFileName(localFilePath))); + return Task.CompletedTask; + } + + public Task DownloadToTempFileAsync(string tenantId, string clientId, string clientSecret, string siteUrl, string fileReference) + => throw new NotSupportedException(); + + public Task ResolveLatestFileInFolderAsync(string tenantId, string clientId, string clientSecret, string siteUrl, string folderReference, string siteTsc, int? preferredYear = null) + => throw new NotSupportedException(); + + public Task> ResolveManualImportFilesInFolderAsync(string tenantId, string clientId, string clientSecret, string siteUrl, string folderReference, string siteTsc, int? preferredYear = null) + => throw new NotSupportedException(); + + public Task TestConnectionAsync(string tenantId, string clientId, string clientSecret, string siteUrl) + => Task.CompletedTask; + } + + private sealed record UploadCall(string ExportFolder, string Land, string FileName); + + private sealed class NoopRecordTransformationService : IRecordTransformationService + { + public void Apply(List records, IEnumerable rules) + { + } + } + + private sealed class NoopCentralSalesRecordService : ICentralSalesRecordService + { + public Task ReplaceForSiteAsync(Site site, IEnumerable records, Action? updateStatus = null) + => Task.CompletedTask; + + public Task> GetAllAsync() + => Task.FromResult(new List()); + } + + private sealed class NoopAppEventLogService : IAppEventLogService + { + public Task WriteAsync(string category, string message, string level = "Info", int? siteId = null, string? land = null, string? details = null) + => Task.CompletedTask; + + public Task WriteDebugAsync(string category, string message, int? siteId = null, string? land = null, string? details = null) + => Task.CompletedTask; + } +}