diff --git a/EstateReportingAPI.BusinessLogic/Queries/FileImportLogQueries.cs b/EstateReportingAPI.BusinessLogic/Queries/FileImportLogQueries.cs new file mode 100644 index 0000000..360d553 --- /dev/null +++ b/EstateReportingAPI.BusinessLogic/Queries/FileImportLogQueries.cs @@ -0,0 +1,13 @@ +using System.Diagnostics.CodeAnalysis; +using EstateReportingAPI.Models; +using MediatR; +using SimpleResults; + +namespace EstateReportingAPI.BusinessLogic.Queries; + +[ExcludeFromCodeCoverage] +public record FileImportLogQueries +{ + public record GetFileImportLogListQuery(Guid EstateId, Guid? MerchantId, DateTime StartDate, DateTime EndDate) : IRequest>>; + public record GetFileImportLogQuery(Guid EstateId, Guid? MerchantId, Guid FileImportLogId) : IRequest>; +} \ No newline at end of file diff --git a/EstateReportingAPI.BusinessLogic/Queries/SettlementQueries.cs b/EstateReportingAPI.BusinessLogic/Queries/SettlementQueries.cs index 97527b6..04269f1 100644 --- a/EstateReportingAPI.BusinessLogic/Queries/SettlementQueries.cs +++ b/EstateReportingAPI.BusinessLogic/Queries/SettlementQueries.cs @@ -8,10 +8,4 @@ namespace EstateReportingAPI.BusinessLogic.Queries; [ExcludeFromCodeCoverage] public record SettlementQueries { public record TodaysSettlementQuery(Guid EstateId, DateTime ComparisonDate) : IRequest>; -} - -[ExcludeFromCodeCoverage] -public record FileImportLogQueries -{ - public record GetFileImportLogListQuery(Guid EstateId, Guid? MerchantId, DateTime StartDate, DateTime EndDate) : IRequest>>; } \ No newline at end of file diff --git a/EstateReportingAPI.BusinessLogic/ReportingManager.cs b/EstateReportingAPI.BusinessLogic/ReportingManager.cs index b444865..8f5e392 100644 --- a/EstateReportingAPI.BusinessLogic/ReportingManager.cs +++ b/EstateReportingAPI.BusinessLogic/ReportingManager.cs @@ -63,6 +63,8 @@ Task> GetMerchantSchedule(MerchantQueries.GetMe Task>> GetFileImportLogList(FileImportLogQueries.GetFileImportLogListQuery request, CancellationToken cancellationToken); + Task> GetFileImportLog(FileImportLogQueries.GetFileImportLogQuery request, + CancellationToken cancellationToken); } public class ReportingManager : IReportingManager { @@ -1449,6 +1451,74 @@ join fl in context.FileLines on f.FileId equals fl.FileId return Result.Success(fileImportLogs); } + public async Task> GetFileImportLog(FileImportLogQueries.GetFileImportLogQuery request, + CancellationToken cancellationToken) + { + using ResolvedDbContext? resolvedContext = this.Resolver.Resolve(EstateManagementDatabaseName, request.EstateId.ToString()); + await using EstateManagementContext context = resolvedContext.Context; + // Build flat projection of the required data then assemble into hierarchical models in-memory + var flatQuery = from fil in context.FileImportLogs + join f in context.Files on fil.FileImportLogId equals f.FileImportLogId + join esu in context.EstateSecurityUsers on f.UserId equals esu.SecurityUserId into esuJoin + from esu in esuJoin.DefaultIfEmpty() + join m in context.Merchants on f.MerchantId equals m.MerchantId into mJoin + from m in mJoin.DefaultIfEmpty() + join fl in context.FileLines on f.FileId equals fl.FileId + where fil.FileImportLogId == request.FileImportLogId + && (request.MerchantId == null || f.MerchantId == request.MerchantId) + select new FileImportFlatData + { + FileImportLogId = fil.FileImportLogId, + ImportLogDateTime = fil.ImportLogDateTime, + FileId = f.FileId, + FileName = f.FileLocation, + FileProfileId = f.FileProfileId, + DateTimeUploaded = f.FileReceivedDateTime, + UserId = f.UserId, + UploadedBy = esu != null ? esu.EmailAddress : null, + MerchantId = f.MerchantId, + MerchantName = m != null ? m.Name : null, + LineNumber = fl.LineNumber, + LineContents = fl.FileLineData, + LineStatus = fl.Status + }; + + var flatResult = await ExecuteQuerySafeToList(flatQuery, cancellationToken, "Error retrieving file import logs"); + + if (flatResult.IsFailed) + return ResultHelpers.CreateFailure(flatResult); + + var flatItems = flatResult.Data; + + var fileImportLogs = flatItems + .GroupBy(x => new { x.FileImportLogId, x.ImportLogDateTime }) + .Select(g => new FileImportLog + { + FileImportLogId = g.Key.FileImportLogId, + ImportLogDateTime = g.Key.ImportLogDateTime, + FileDetailsList = g.GroupBy(f => new { f.FileId, f.FileName, f.FileProfileId, f.DateTimeUploaded, f.UserId, f.UploadedBy, f.MerchantId, f.MerchantName }) + .Select(fg => new FileDetails + { + FileId = fg.Key.FileId, + FileName = fg.Key.FileName, + FileProfile = fg.Key.FileProfileId != null ? fg.Key.FileProfileId.ToString() : null, + DateTimeUploaded = fg.Key.DateTimeUploaded, + UserId = fg.Key.UserId, + UploadedBy = fg.Key.UploadedBy, + MerchantId = fg.Key.MerchantId, + MerchantName = fg.Key.MerchantName, + FileLines = fg.Select(fl => new FileLine + { + LineNumber = fl.LineNumber, + LineContents = fl.LineContents, + LineStatus = fl.LineStatus + }).ToList() + }).ToList() + }).SingleOrDefault(); + + return Result.Success(fileImportLogs); + } + private async Task GetSettlementSummary(IQueryable query, CancellationToken cancellationToken) { // Get the settleed fees summary diff --git a/EstateReportingAPI.BusinessLogic/RequestHandlers/FileImportLogRequestHandler.cs b/EstateReportingAPI.BusinessLogic/RequestHandlers/FileImportLogRequestHandler.cs new file mode 100644 index 0000000..888c723 --- /dev/null +++ b/EstateReportingAPI.BusinessLogic/RequestHandlers/FileImportLogRequestHandler.cs @@ -0,0 +1,26 @@ +using EstateReportingAPI.BusinessLogic.Queries; +using EstateReportingAPI.Models; +using MediatR; +using SimpleResults; + +namespace EstateReportingAPI.BusinessLogic.RequestHandlers; + +public class FileImportLogRequestHandler : IRequestHandler>>, + IRequestHandler> { + private readonly IReportingManager Manager; + public FileImportLogRequestHandler(IReportingManager manager) + { + this.Manager = manager; + } + + public async Task>> Handle(FileImportLogQueries.GetFileImportLogListQuery request, + CancellationToken cancellationToken) { + return await this.Manager.GetFileImportLogList(request, cancellationToken); + } + + public async Task> Handle(FileImportLogQueries.GetFileImportLogQuery request, + CancellationToken cancellationToken) + { + return await this.Manager.GetFileImportLog(request, cancellationToken); + } +} \ No newline at end of file diff --git a/EstateReportingAPI.BusinessLogic/RequestHandlers/SettlementRequestHandler.cs b/EstateReportingAPI.BusinessLogic/RequestHandlers/SettlementRequestHandler.cs index d813c19..2bd87d6 100644 --- a/EstateReportingAPI.BusinessLogic/RequestHandlers/SettlementRequestHandler.cs +++ b/EstateReportingAPI.BusinessLogic/RequestHandlers/SettlementRequestHandler.cs @@ -15,18 +15,4 @@ public async Task> Handle(SettlementQueries.TodaysSettl CancellationToken cancellationToken) { return await this.Manager.GetTodaysSettlement(request, cancellationToken); } -} - -public class FileImportLogRequestHandler : IRequestHandler>> -{ - private readonly IReportingManager Manager; - public FileImportLogRequestHandler(IReportingManager manager) - { - this.Manager = manager; - } - - public async Task>> Handle(FileImportLogQueries.GetFileImportLogListQuery request, - CancellationToken cancellationToken) { - return await this.Manager.GetFileImportLogList(request, cancellationToken); - } } \ No newline at end of file diff --git a/EstateReportingAPI.IntegrationTests/FileImportLogsEndpointTests.cs b/EstateReportingAPI.IntegrationTests/FileImportLogsEndpointTests.cs index 6be3fea..321483c 100644 --- a/EstateReportingAPI.IntegrationTests/FileImportLogsEndpointTests.cs +++ b/EstateReportingAPI.IntegrationTests/FileImportLogsEndpointTests.cs @@ -3,6 +3,8 @@ using System.Diagnostics; using System.Linq; using System.Text; +using System.Net; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; using EstateReportingAPI.DataTransferObjects; @@ -36,6 +38,108 @@ public async Task FileImportEndpoint_GetFileImportLogs_NoData_ReturnsEmptyList() list.Count.ShouldBe(0); } + [Fact] + public async Task FileImportEndpoint_GetFileImportLogs_WithMerchantFilter_ReturnsData() + { + // create a user + var userId = await this.helper.AddEstateUser("Test Estate", "Api User", "apiuser@example.com"); + + // create a merchant and use it for the file + var merchant = await this.helper.AddMerchant("Test Estate", "List Filter Merchant", 10, DateTime.MinValue, DateTime.MinValue, default, default); + + // create file import log, file and a line associated to the merchant + var filId = await this.helper.AddFileImportLog(this.TestId, DateTime.Now); + var fileId = await this.helper.AddFile(filId, merchant, userId, "test/location/file-filter-list.csv"); + await this.helper.AddFileLine(fileId, 1, "filterlistline", "OK"); + + DateTime start = DateTime.Today.AddDays(-1); + DateTime end = DateTime.Today.AddDays(1); + + Result> result = await this.CreateAndSendHttpRequestMessage>($"{this.BaseRoute}?merchantId={merchant}&startDate={start:yyyy-MM-dd}&endDate={end:yyyy-MM-dd}", CancellationToken.None); + result.IsSuccess.ShouldBeTrue(); + var list = result.Data; + list.ShouldNotBeNull(); + list.Count.ShouldBeGreaterThan(0); + + var match = list.SelectMany(x => x.FileDetailsList).SelectMany(fd => fd.FileLines).SingleOrDefault(fl => fl.LineContents == "filterlistline"); + match.ShouldNotBeNull(); + match.LineStatus.ShouldBe("OK"); + } + + [Fact] + public async Task FileImportEndpoint_GetFileImportLog_WithMerchantFilter_ReturnsData() + { + // create a user + var userId = await this.helper.AddEstateUser("Test Estate", "Api User", "apiuser@example.com"); + + // create a merchant and use it for the file + var merchant = await this.helper.AddMerchant("Test Estate", "Filter Merchant", 10, DateTime.MinValue, DateTime.MinValue, default, default); + + // create file import log, file and a line associated to the merchant + var filId = await this.helper.AddFileImportLog(this.TestId, DateTime.Now); + var fileId = await this.helper.AddFile(filId, merchant, userId, "test/location/file-filter.csv"); + await this.helper.AddFileLine(fileId, 1, "filterline", "OK"); + + Result result = await this.CreateAndSendHttpRequestMessage($"{this.BaseRoute}/{filId}?merchantId={merchant}", CancellationToken.None); + result.IsSuccess.ShouldBeTrue(); + var item = result.Data; + item.ShouldNotBeNull(); + item.FileImportLogId.ShouldBe(filId); + + var match = item.FileDetailsList.SelectMany(fd => fd.FileLines).SingleOrDefault(fl => fl.LineContents == "filterline"); + match.ShouldNotBeNull(); + match.LineStatus.ShouldBe("OK"); + } + + [Fact] + public async Task FileImportEndpoint_GetFileImportLog_ReturnsInsertedData() + { + // create a user + var userId = await this.helper.AddEstateUser("Test Estate", "Api User", "apiuser@example.com"); + + // pick a merchant + var merchant = await this.context.Merchants.FirstAsync(); + + // create file import log, file and a line + var filId = await this.helper.AddFileImportLog(this.TestId, DateTime.Now); + var fileId = await this.helper.AddFile(filId, merchant.MerchantId, userId, "test/location/file1.csv"); + await this.helper.AddFileLine(fileId, 1, "line1data", "OK"); + + Result result = await this.CreateAndSendHttpRequestMessage($"{this.BaseRoute}/{filId}", CancellationToken.None); + result.IsSuccess.ShouldBeTrue(); + var item = result.Data; + item.ShouldNotBeNull(); + item.FileImportLogId.ShouldBe(filId); + + var match = item.FileDetailsList.SelectMany(fd => fd.FileLines).SingleOrDefault(fl => fl.LineContents == "line1data"); + match.ShouldNotBeNull(); + match.LineStatus.ShouldBe("OK"); + } + + [Fact] + public async Task FileImportEndpoint_GetFileImportLog_WithMerchantFilter_ReturnsNotFound() + { + // create a user + var userId = await this.helper.AddEstateUser("Test Estate", "Api User", "apiuser@example.com"); + + // pick a merchant and create another merchant to use as a mismatched filter + var merchant1 = await this.context.Merchants.FirstAsync(); + var merchant2 = await this.helper.AddMerchant("Test Estate", "Other Merchant", 50, DateTime.MinValue, DateTime.MinValue, default, default); + + // create file import log, file and a line associated to merchant1 + var filId = await this.helper.AddFileImportLog(this.TestId, DateTime.Now); + var fileId = await this.helper.AddFile(filId, merchant1.MerchantId, userId, "test/location/file1.csv"); + await this.helper.AddFileLine(fileId, 1, "line1data", "OK"); + + var url = $"{this.BaseRoute}/{filId}?merchantId={merchant2}"; + var requestMessage = new HttpRequestMessage(HttpMethod.Get, url); + requestMessage.Headers.Add("estateId", this.TestId.ToString()); + requestMessage.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Test"); + + var response = await this.Client.SendAsync(requestMessage, CancellationToken.None); + response.StatusCode.ShouldBe(HttpStatusCode.NotFound); + } + [Fact] public async Task FileImportEndpoint_GetFileImportLogs_ReturnsInsertedData() { diff --git a/EstateReportingAPI/Endpoints/FileImportLogEndpoints.cs b/EstateReportingAPI/Endpoints/FileImportLogEndpoints.cs index 84c78f2..5a7b086 100644 --- a/EstateReportingAPI/Endpoints/FileImportLogEndpoints.cs +++ b/EstateReportingAPI/Endpoints/FileImportLogEndpoints.cs @@ -19,6 +19,8 @@ public static void MapFileImportLogEndpoints(this IEndpointRouteBuilder app) group = group.RequireAuthorization(); } - group.MapGet("", FileImportHandler.GetFileImportLogList).WithStandardProduces>(); + group.MapGet("/", FileImportHandler.GetFileImportLogList).WithStandardProduces>(); + + group.MapGet("/{fileimportlogid}", FileImportHandler.GetFileImportLog).WithStandardProduces(); } } \ No newline at end of file diff --git a/EstateReportingAPI/Handlers/FileImportHandler.cs b/EstateReportingAPI/Handlers/FileImportHandler.cs index 4d79bbe..d36faf4 100644 --- a/EstateReportingAPI/Handlers/FileImportHandler.cs +++ b/EstateReportingAPI/Handlers/FileImportHandler.cs @@ -40,4 +40,42 @@ public static async Task GetFileImportLogList([FromHeader] Guid estateI }).ToList() }).ToList()); } + + public static async Task GetFileImportLog([FromHeader] Guid estateId, + [FromRoute] Guid fileImportLogId, + [FromQuery] Guid? merchantId, + IMediator mediator, + CancellationToken cancellationToken) + { + FileImportLogQueries.GetFileImportLogQuery query = new(estateId, merchantId, fileImportLogId); + Result result = await mediator.Send(query, cancellationToken); + + if (result.IsSuccess && result.Data == null) + { + return Results.NotFound(); + } + + return ResponseFactory.FromResult(result, r => new DataTransferObjects.FileImportLog + { + FileImportLogId = r.FileImportLogId, + ImportLogDateTime = r.ImportLogDateTime, + FileDetailsList = r.FileDetailsList.Select(fd => new DataTransferObjects.FileDetails + { + DateTimeUploaded = fd.DateTimeUploaded, + FileId = fd.FileId, + FileName = fd.FileName, + FileProfile = fd.FileProfile, + MerchantId = fd.MerchantId, + MerchantName = fd.MerchantName, + UploadedBy = fd.UploadedBy, + UserId = fd.UserId, + FileLines = fd.FileLines.Select(fl => new DataTransferObjects.FileLine + { + LineContents = fl.LineContents, + LineNumber = fl.LineNumber, + LineStatus = fl.LineStatus + }).ToList() + }).ToList() + }); + } } \ No newline at end of file