From 26fdb2d7bfde5d0b1983e23bfafdea832b4d8871 Mon Sep 17 00:00:00 2001 From: StuartFerguson Date: Fri, 16 Oct 2026 17:26:05 +0100 Subject: [PATCH 1/3] Capture profile version when accepting a file --- FIleProcessor.Models/FileDetails.cs | 2 + FIleProcessor.Models/ImportLogFile.cs | 4 +- .../DomainEventHandlerTests.cs | 25 ++++++++++ .../DummyFileProcessorDomainService.cs | 6 +++ .../FileProcessorDomainServiceTests.cs | 6 ++- ...ileProfileDirectoryRecoveryServiceTests.cs | 3 ++ .../EventHandling/FileDomainEventHandler.cs | 3 +- .../Managers/FileProcessorManager.cs | 9 ++++ .../Managers/IFileProcessorManager.cs | 4 ++ .../Requests/FileCommands.cs | 3 +- .../Services/FileProcessorDomainService.cs | 47 +++++++++++++++---- .../FileProfileDirectoryRecoveryService.cs | 6 ++- .../Responses/FileDetails.cs | 2 + .../Responses/FileImportLogFile.cs | 4 +- .../FileAggregateDomainEvents.cs.cs | 3 +- .../FileAggregateTests.cs | 3 +- FileProcessor.FileAggregate/FileAggregate.cs | 8 +++- .../FileImportLogAggregateDomainEvents.cs | 2 +- .../FileImportLogAggregateTests.cs | 19 ++++++++ .../FileImportLogAggregate.cs | 5 +- FileProcessor.Testing/TestData.cs | 8 ++++ FileProcessor.Tests/ModelFactoryTests.cs | 2 + FileProcessor/Common/ModelFactory.cs | 2 + 23 files changed, 154 insertions(+), 22 deletions(-) diff --git a/FIleProcessor.Models/FileDetails.cs b/FIleProcessor.Models/FileDetails.cs index 9bfc668..6586170 100644 --- a/FIleProcessor.Models/FileDetails.cs +++ b/FIleProcessor.Models/FileDetails.cs @@ -64,6 +64,8 @@ public class FileDetails /// public Guid FileProfileId { get; set; } + public Guid FileProfileVersionId { get; set; } + public DateTime FileReceivedDateTime { get; set; } /// diff --git a/FIleProcessor.Models/ImportLogFile.cs b/FIleProcessor.Models/ImportLogFile.cs index 299236a..91920f2 100644 --- a/FIleProcessor.Models/ImportLogFile.cs +++ b/FIleProcessor.Models/ImportLogFile.cs @@ -38,6 +38,8 @@ public class ImportLogFile /// public Guid FileProfileId { get; set; } + public Guid FileProfileVersionId { get; set; } + /// /// Gets or sets the merchant identifier. /// @@ -80,4 +82,4 @@ public class ImportLogFile #endregion } -} \ No newline at end of file +} diff --git a/FileProcessor.BusinessLogic.Tests/DomainEventHandlerTests.cs b/FileProcessor.BusinessLogic.Tests/DomainEventHandlerTests.cs index 2e15aaa..be674c6 100644 --- a/FileProcessor.BusinessLogic.Tests/DomainEventHandlerTests.cs +++ b/FileProcessor.BusinessLogic.Tests/DomainEventHandlerTests.cs @@ -78,5 +78,30 @@ public void FileDomainEventHandler_FileAddedToImportLogEvent_EventIsHandled() await eventHandler.Handle(fileAddedToImportLogEvent, CancellationToken.None); }); } + + [Fact] + public async Task FileDomainEventHandler_FileAddedToImportLogEvent_PropagatesFileProfileVersionId() + { + IMediatorImposter mediator = new IMediatorImposter(); + mediator.Send(Arg>.Any(), Arg.Any()).ReturnsAsync(Result.Success()); + FileDomainEventHandler eventHandler = new FileDomainEventHandler(mediator.Instance()); + FileAddedToImportLogEvent domainEvent = new(TestData.FileImportLogId, + TestData.FileId, + TestData.EstateId, + TestData.MerchantId, + TestData.UserId, + TestData.FileProfileId, + TestData.OriginalFileName, + TestData.FilePath, + TestData.FileUploadedDateTime, + TestData.FileProfileVersionId); + + Result result = await eventHandler.Handle(domainEvent, CancellationToken.None); + + result.IsSuccess.ShouldBeTrue(); + mediator.Send(Arg>.Is(request => request is FileCommands.ProcessUploadedFileCommand command && + command.FileProfileVersionId == TestData.FileProfileVersionId), + Arg.Any()).Called(Count.Once()); + } } } diff --git a/FileProcessor.BusinessLogic.Tests/DummyFileProcessorDomainService.cs b/FileProcessor.BusinessLogic.Tests/DummyFileProcessorDomainService.cs index a0a5323..c5e7972 100644 --- a/FileProcessor.BusinessLogic.Tests/DummyFileProcessorDomainService.cs +++ b/FileProcessor.BusinessLogic.Tests/DummyFileProcessorDomainService.cs @@ -38,6 +38,12 @@ public async Task> GetFileProfile(Guid fileProfileId, return Result.Success(); } + public async Task> GetFileProfileVersion(Guid fileProfileId, + Guid fileProfileVersionId, + CancellationToken cancellationToken) { + return Result.Success(); + } + public async Task> GetFileImportLogs(Guid estateId, DateOnly startDate, DateOnly endDate, diff --git a/FileProcessor.BusinessLogic.Tests/FileProcessorDomainServiceTests.cs b/FileProcessor.BusinessLogic.Tests/FileProcessorDomainServiceTests.cs index 406e8d1..eab6d41 100644 --- a/FileProcessor.BusinessLogic.Tests/FileProcessorDomainServiceTests.cs +++ b/FileProcessor.BusinessLogic.Tests/FileProcessorDomainServiceTests.cs @@ -109,7 +109,10 @@ public async Task FileRequestHandler_UploadFileRequest_RequestIsHandled() [Fact] public async Task UploadFile_DoesNotRequireListeningDirectoryOrMoveTemporaryFile() { - this.FileProcessorManager.GetFileProfile(Arg.Any(), Arg.Any()).ReturnsAsync(TestData.FileProfileSafaricom); + this.FileProcessorManager.GetFileProfile(Arg.Any(), Arg.Any()).ReturnsAsync(TestData.FileProfileSafaricom with + { + FileProfileVersionId = TestData.FileProfileVersionId + }); FileImportLogAggregate importLogAggregate = TestData.GetEmptyFileImportLogAggregate(); this.FileImportLogAggregateRepository.GetLatestVersion(Arg.Any(), Arg.Any()).ReturnsAsync(Result.Success(importLogAggregate)); @@ -131,6 +134,7 @@ public async Task UploadFile_DoesNotRequireListeningDirectoryOrMoveTemporaryFile this.FileSystem.File.Exists(TestData.FilePathWithName).ShouldBeTrue(); importLogAggregate.GetFileImportLog().Files.Single().FilePath.ShouldBe(TestData.FilePathWithName); importLogAggregate.GetFileImportLog().Files.Single().OriginalFileName.ShouldBe("client-name.csv"); + importLogAggregate.GetFileImportLog().Files.Single().FileProfileVersionId.ShouldBe(TestData.FileProfileVersionId); } [Fact] diff --git a/FileProcessor.BusinessLogic.Tests/FileProfileDirectoryRecoveryServiceTests.cs b/FileProcessor.BusinessLogic.Tests/FileProfileDirectoryRecoveryServiceTests.cs index b8cd57b..d586e4c 100644 --- a/FileProcessor.BusinessLogic.Tests/FileProfileDirectoryRecoveryServiceTests.cs +++ b/FileProcessor.BusinessLogic.Tests/FileProfileDirectoryRecoveryServiceTests.cs @@ -64,6 +64,7 @@ public async Task RecoverInProgressFilesAsync_ReplaysFilesFoundInInProgressDirec command.FileId == TestData.FileId && Path.GetFileName(command.FilePath) == Path.GetFileName(inProgressFilePath) && command.FileProfileId == TestData.FileProfileId && + command.FileProfileVersionId == TestData.FileProfileVersionId && command.EstateId == TestData.EstateId), Arg.Any()).Called(Count.Once()); } @@ -106,6 +107,7 @@ public async Task RecoverInProgressFilesAsync_WhenFileAggregateIsMissing_Replays EstateId = TestData.EstateId, FileId = TestData.FileId, FileProfileId = TestData.FileProfileId, + FileProfileVersionId = TestData.FileProfileVersionId, MerchantId = TestData.MerchantId, UserId = TestData.UserId, UploadedDateTime = TestData.FileUploadedDateTime @@ -130,6 +132,7 @@ public async Task RecoverInProgressFilesAsync_WhenFileAggregateIsMissing_Replays command.FileId == TestData.FileId && command.FileImportLogId == TestData.FileImportLogId && command.FileProfileId == TestData.FileProfileId && + command.FileProfileVersionId == TestData.FileProfileVersionId && command.EstateId == TestData.EstateId && command.MerchantId == TestData.MerchantId && command.UserId == TestData.UserId && diff --git a/FileProcessor.BusinessLogic/EventHandling/FileDomainEventHandler.cs b/FileProcessor.BusinessLogic/EventHandling/FileDomainEventHandler.cs index 170b19e..e02855b 100644 --- a/FileProcessor.BusinessLogic/EventHandling/FileDomainEventHandler.cs +++ b/FileProcessor.BusinessLogic/EventHandling/FileDomainEventHandler.cs @@ -116,7 +116,8 @@ private async Task HandleSpecificDomainEvent(FileAddedToImportLogEvent d domainEvent.UserId, domainEvent.FilePath, domainEvent.FileProfileId, - domainEvent.FileUploadedDateTime); + domainEvent.FileUploadedDateTime, + domainEvent.FileProfileVersionId); return await this.Mediator.Send(command, cancellationToken); } diff --git a/FileProcessor.BusinessLogic/Managers/FileProcessorManager.cs b/FileProcessor.BusinessLogic/Managers/FileProcessorManager.cs index 14ec124..f0f5f72 100644 --- a/FileProcessor.BusinessLogic/Managers/FileProcessorManager.cs +++ b/FileProcessor.BusinessLogic/Managers/FileProcessorManager.cs @@ -76,6 +76,15 @@ public async Task> GetFileProfile(Guid fileProfileId, return await this.FileProfileManager.GetFileProfile(fileProfileId, cancellationToken); } + public async Task> GetFileProfileVersion(Guid fileProfileId, + Guid fileProfileVersionId, + CancellationToken cancellationToken) + { + return await this.FileProfileManager.GetFileProfileVersion(fileProfileId, + fileProfileVersionId, + cancellationToken); + } + private async Task GetContext(Guid estateId) { ResolvedDbContext? resolvedContext = this.Resolver.Resolve(EstateManagementDatabaseName, estateId.ToString()); diff --git a/FileProcessor.BusinessLogic/Managers/IFileProcessorManager.cs b/FileProcessor.BusinessLogic/Managers/IFileProcessorManager.cs index 77b194e..62d3d64 100644 --- a/FileProcessor.BusinessLogic/Managers/IFileProcessorManager.cs +++ b/FileProcessor.BusinessLogic/Managers/IFileProcessorManager.cs @@ -20,6 +20,10 @@ public interface IFileProcessorManager Task> GetFileProfile(Guid fileProfileId, CancellationToken cancellationToken); + Task> GetFileProfileVersion(Guid fileProfileId, + Guid fileProfileVersionId, + CancellationToken cancellationToken); + Task> GetFileImportLogs(Guid estateId, DateOnly startDate, DateOnly endDate, Guid? merchantId, Int32 pageNumber, Int32 pageSize, Boolean descending, CancellationToken cancellationToken); Task> GetFileImportLog(Guid fileImportLogId, Guid estateId, Guid? merchantId, CancellationToken cancellationToken); diff --git a/FileProcessor.BusinessLogic/Requests/FileCommands.cs b/FileProcessor.BusinessLogic/Requests/FileCommands.cs index 19cbb2e..38841b0 100644 --- a/FileProcessor.BusinessLogic/Requests/FileCommands.cs +++ b/FileProcessor.BusinessLogic/Requests/FileCommands.cs @@ -17,7 +17,8 @@ public record ProcessUploadedFileCommand(Guid EstateId, Guid UserId, String FilePath, Guid FileProfileId, - DateTime FileUploadedDateTime) : IRequest; + DateTime FileUploadedDateTime, + Guid FileProfileVersionId = default) : IRequest; public record UploadFileCommand(Guid FileId, Guid EstateId, diff --git a/FileProcessor.BusinessLogic/Services/FileProcessorDomainService.cs b/FileProcessor.BusinessLogic/Services/FileProcessorDomainService.cs index cf29e66..d2b91d6 100644 --- a/FileProcessor.BusinessLogic/Services/FileProcessorDomainService.cs +++ b/FileProcessor.BusinessLogic/Services/FileProcessorDomainService.cs @@ -187,6 +187,10 @@ public async Task UploadFile(FileCommands.UploadFileCommand command, if (getFileProfileResult.Data == null) return Result.NotFound($"No file profile found with Id {command.FileProfileId}"); + Guid fileProfileVersionId = getFileProfileResult.Data.FileProfileVersionId == Guid.Empty + ? getFileProfileResult.Data.FileProfileId + : getFileProfileResult.Data.FileProfileVersionId; + IFileInfo file = this.FileSystem.FileInfo.New(command.FilePath); if (file.Exists == false) { return Result.NotFound($"File {file.FullName} not found"); @@ -213,7 +217,7 @@ public async Task UploadFile(FileCommands.UploadFileCommand command, } // Update Import log aggregate - Result stateResult= fileImportLogAggregate.AddImportedFile(command.FileId, command.MerchantId, command.UserId, command.FileProfileId, originalName, command.FilePath, command.FileUploadedDateTime); + Result stateResult= fileImportLogAggregate.AddImportedFile(command.FileId, command.MerchantId, command.UserId, command.FileProfileId, originalName, command.FilePath, command.FileUploadedDateTime, fileProfileVersionId); if (stateResult.IsFailed) return stateResult; @@ -256,12 +260,15 @@ public async Task ProcessUploadedFile(FileCommands.ProcessUploadedFileCo CancellationToken cancellationToken) { Result result = await ApplyFileUpdates(async (FileAggregate fileAggregate) => { Logger.LogWarning("In ProcessUploadedFile action"); - Result operatorIdResult = await GetOperatorIdForFileProfile(command.EstateId, command.FileProfileId, cancellationToken); + Result operatorIdResult = await GetOperatorIdForFileProfile(command.EstateId, + command.FileProfileId, + command.FileProfileVersionId, + cancellationToken); if (operatorIdResult.IsFailed) return ResultHelpers.CreateFailure(operatorIdResult); Logger.LogWarning("About to Create File"); - FileAggregateExtensions.CreateFileRequest request = new(command.FileImportLogId, command.EstateId, command.MerchantId, command.UserId, command.FileProfileId, command.FilePath, command.FileUploadedDateTime, operatorIdResult.Data); + FileAggregateExtensions.CreateFileRequest request = new(command.FileImportLogId, command.EstateId, command.MerchantId, command.UserId, command.FileProfileId, command.FilePath, command.FileUploadedDateTime, operatorIdResult.Data, command.FileProfileVersionId); Result stateResult = fileAggregate.CreateFile(request); if (stateResult.IsFailed) return stateResult; @@ -276,7 +283,11 @@ public async Task ProcessUploadedFile(FileCommands.ProcessUploadedFileCo return result; } - Result processResult = await this.ProcessFile(command.FileId, command.FileProfileId, command.FilePath, cancellationToken); + Result processResult = await this.ProcessFile(command.FileId, + command.FileProfileId, + command.FileProfileVersionId, + command.FilePath, + cancellationToken); return processResult; } @@ -299,9 +310,14 @@ private async Task MoveAcceptedFileToFailedDirectory(Guid fileProfileId, file.MoveTo($"{fileProfileResult.Data.FailedDirectory}/{file.Name}", overwrite: true); } - private async Task> GetOperatorIdForFileProfile(Guid estateId, Guid fileProfileId, CancellationToken cancellationToken){ + private async Task> GetOperatorIdForFileProfile(Guid estateId, + Guid fileProfileId, + Guid fileProfileVersionId, + CancellationToken cancellationToken){ - Result fileProfileResult = await this.FileProcessorManager.GetFileProfile(fileProfileId, cancellationToken); + Result fileProfileResult = await this.GetFileProfileForProcessing(fileProfileId, + fileProfileVersionId, + cancellationToken); if (fileProfileResult.IsFailed){ Logger.LogInformation($"file profile {fileProfileId} not found"); @@ -411,7 +427,9 @@ public async Task ProcessTransactionForFileLine(FileCommands.ProcessTran } processingContext.Stage = FileLineProcessingStage.FileProfileLookup; - Result fileProfileResult = await this.FileProcessorManager.GetFileProfile(fileDetails.FileProfileId, cancellationToken); + Result fileProfileResult = await this.GetFileProfileForProcessing(fileDetails.FileProfileId, + fileDetails.FileProfileVersionId, + cancellationToken); if (fileProfileResult.IsFailed) return (ResultHelpers.CreateFailure(fileProfileResult), null); @@ -724,13 +742,14 @@ private Result ResolveMerchantDevice(MerchantResponse merchant, private async Task ProcessFile(Guid fileId, Guid fileProfileId, + Guid fileProfileVersionId, String fileName, CancellationToken cancellationToken) { IFileInfo inProgressFile = null; FileProfileModel fileProfile = null; Result fileProfileResult = - await this.FileProcessorManager.GetFileProfile(fileProfileId, cancellationToken); + await this.GetFileProfileForProcessing(fileProfileId, fileProfileVersionId, cancellationToken); if (fileProfileResult.IsFailed) return ResultHelpers.CreateFailure(fileProfileResult); @@ -887,6 +906,18 @@ private async Task ProcessFile(Guid fileId, return result; } + private async Task> GetFileProfileForProcessing(Guid fileProfileId, + Guid fileProfileVersionId, + CancellationToken cancellationToken) + { + if (fileProfileVersionId == Guid.Empty || fileProfileVersionId == fileProfileId) + return await this.FileProcessorManager.GetFileProfile(fileProfileId, cancellationToken); + + return await this.FileProcessorManager.GetFileProfileVersion(fileProfileId, + fileProfileVersionId, + cancellationToken); + } + private static Int32 TransactionNumber = 0; private Boolean FileLineCanBeIgnored(String domainEventFileLine, diff --git a/FileProcessor.BusinessLogic/Services/FileProfileDirectoryRecoveryService.cs b/FileProcessor.BusinessLogic/Services/FileProfileDirectoryRecoveryService.cs index bd40135..56896f6 100644 --- a/FileProcessor.BusinessLogic/Services/FileProfileDirectoryRecoveryService.cs +++ b/FileProcessor.BusinessLogic/Services/FileProfileDirectoryRecoveryService.cs @@ -82,7 +82,8 @@ private async Task RecoverFileAsync(FileProfileModel fileProfile, string filePat fileDetails.UserId, filePath, fileDetails.FileProfileId, - fileDetails.FileReceivedDateTime); + fileDetails.FileReceivedDateTime, + fileDetails.FileProfileVersionId); } else { @@ -108,7 +109,8 @@ private async Task RecoverFileAsync(FileProfileModel fileProfile, string filePat importLogFile.UserId, filePath, importLogFile.FileProfileId, - importLogFile.UploadedDateTime == default ? importLog.FileImportLogDateTime : importLogFile.UploadedDateTime); + importLogFile.UploadedDateTime == default ? importLog.FileImportLogDateTime : importLogFile.UploadedDateTime, + importLogFile.FileProfileVersionId); } Result result = await this.Mediator.Send(command, cancellationToken); diff --git a/FileProcessor.DataTransferObjects/Responses/FileDetails.cs b/FileProcessor.DataTransferObjects/Responses/FileDetails.cs index 4b4bf47..3063652 100644 --- a/FileProcessor.DataTransferObjects/Responses/FileDetails.cs +++ b/FileProcessor.DataTransferObjects/Responses/FileDetails.cs @@ -31,6 +31,8 @@ public class FileDetails public Guid FileProfileId { get; set; } + public Guid FileProfileVersionId { get; set; } + public String FileProfileName { get; set; } public Guid FileImportLogId { get; set; } diff --git a/FileProcessor.DataTransferObjects/Responses/FileImportLogFile.cs b/FileProcessor.DataTransferObjects/Responses/FileImportLogFile.cs index b66bd4c..f851692 100644 --- a/FileProcessor.DataTransferObjects/Responses/FileImportLogFile.cs +++ b/FileProcessor.DataTransferObjects/Responses/FileImportLogFile.cs @@ -13,6 +13,8 @@ public class FileImportLogFile public Guid FileProfileId { get; set; } + public Guid FileProfileVersionId { get; set; } + public DateTime FileUploadedDateTime { get; set; } public Guid MerchantId { get; set; } @@ -23,4 +25,4 @@ public class FileImportLogFile #endregion } -} \ No newline at end of file +} diff --git a/FileProcessor.File.DomainEvents/FileAggregateDomainEvents.cs.cs b/FileProcessor.File.DomainEvents/FileAggregateDomainEvents.cs.cs index ea89a7a..35e903a 100644 --- a/FileProcessor.File.DomainEvents/FileAggregateDomainEvents.cs.cs +++ b/FileProcessor.File.DomainEvents/FileAggregateDomainEvents.cs.cs @@ -27,7 +27,8 @@ public record FileCreatedEvent(Guid FileId, Guid FileProfileId, String FileLocation, DateTime FileReceivedDateTime, - Guid OperatorId) : DomainEvent(FileId, Guid.NewGuid()); + Guid OperatorId, + Guid FileProfileVersionId = default) : DomainEvent(FileId, Guid.NewGuid()); public record FileRejectedEvent(Guid FileId, Guid EstateId, diff --git a/FileProcessor.FileAggregate.Tests/FileAggregateTests.cs b/FileProcessor.FileAggregate.Tests/FileAggregateTests.cs index 0f4d06e..e9af557 100644 --- a/FileProcessor.FileAggregate.Tests/FileAggregateTests.cs +++ b/FileProcessor.FileAggregate.Tests/FileAggregateTests.cs @@ -36,7 +36,7 @@ public void FileAggregate_CanBeCreated_InvalidFileId_IsCreated() public void FileAggregate_CreateFile_FileIsCreated() { FileAggregate fileAggregate = FileAggregate.Create(TestData.FileId); - FileAggregateExtensions.CreateFileRequest request = new(TestData.FileImportLogId, TestData.EstateId, TestData.MerchantId, TestData.UserId, TestData.FileProfileId, TestData.FileLocation, TestData.FileUploadedDateTime, TestData.SafaricomOperatorId); + FileAggregateExtensions.CreateFileRequest request = new(TestData.FileImportLogId, TestData.EstateId, TestData.MerchantId, TestData.UserId, TestData.FileProfileId, TestData.FileLocation, TestData.FileUploadedDateTime, TestData.SafaricomOperatorId, TestData.FileProfileVersionId); var result = fileAggregate.CreateFile(request); result.IsSuccess.ShouldBeTrue(); @@ -50,6 +50,7 @@ public void FileAggregate_CreateFile_FileIsCreated() fileDetails.MerchantId.ShouldBe(TestData.MerchantId); fileDetails.UserId.ShouldBe(TestData.UserId); fileDetails.FileProfileId.ShouldBe(TestData.FileProfileId); + fileDetails.FileProfileVersionId.ShouldBe(TestData.FileProfileVersionId); fileDetails.FileLocation.ShouldBe(TestData.FileLocation); fileDetails.FileLines.ShouldBeEmpty(); fileDetails.ProcessingCompleted.ShouldBeFalse(); diff --git a/FileProcessor.FileAggregate/FileAggregate.cs b/FileProcessor.FileAggregate/FileAggregate.cs index b0b7097..b62f704 100644 --- a/FileProcessor.FileAggregate/FileAggregate.cs +++ b/FileProcessor.FileAggregate/FileAggregate.cs @@ -22,6 +22,7 @@ public static void PlayEvent(this FileAggregate aggregate, FileCreatedEvent doma aggregate.EstateId = domainEvent.EstateId; aggregate.MerchantId = domainEvent.MerchantId; aggregate.FileProfileId = domainEvent.FileProfileId; + aggregate.FileProfileVersionId = domainEvent.FileProfileVersionId == Guid.Empty ? domainEvent.FileProfileId : domainEvent.FileProfileVersionId; aggregate.UserId = domainEvent.UserId; aggregate.FileLocation = domainEvent.FileLocation; aggregate.FileImportLogId = domainEvent.FileImportLogId; @@ -43,6 +44,7 @@ public static FileDetails GetFile(this FileAggregate aggregate) EstateId = aggregate.EstateId, MerchantId = aggregate.MerchantId, FileProfileId = aggregate.FileProfileId, + FileProfileVersionId = aggregate.FileProfileVersionId, FileId = aggregate.AggregateId, FileImportLogId = aggregate.FileImportLogId, FileLocation = aggregate.FileLocation, @@ -135,14 +137,14 @@ public static void PlayEvent(this FileAggregate aggregate, FileProcessingComplet aggregate.IsCompleted = true; } - public record CreateFileRequest(Guid fileImportLogId, Guid estateId, Guid merchantId, Guid userId, Guid fileProfileId, String fileLocation, DateTime fileReceivedDateTime, Guid operatorId); + public record CreateFileRequest(Guid fileImportLogId, Guid estateId, Guid merchantId, Guid userId, Guid fileProfileId, String fileLocation, DateTime fileReceivedDateTime, Guid operatorId, Guid fileProfileVersionId = default); public static Result CreateFile(this FileAggregate aggregate, CreateFileRequest createFileRequest) { if (aggregate.IsCreated) return Result.Success(); - FileCreatedEvent fileCreatedEvent = new(aggregate.AggregateId, createFileRequest.fileImportLogId, createFileRequest.estateId, createFileRequest.merchantId, createFileRequest.userId, createFileRequest.fileProfileId, createFileRequest.fileLocation, createFileRequest.fileReceivedDateTime, createFileRequest.operatorId); + FileCreatedEvent fileCreatedEvent = new(aggregate.AggregateId, createFileRequest.fileImportLogId, createFileRequest.estateId, createFileRequest.merchantId, createFileRequest.userId, createFileRequest.fileProfileId, createFileRequest.fileLocation, createFileRequest.fileReceivedDateTime, createFileRequest.operatorId, createFileRequest.fileProfileVersionId); aggregate.ApplyAndAppend(fileCreatedEvent); @@ -343,6 +345,8 @@ public record FileAggregate : Aggregate internal Guid FileProfileId; + internal Guid FileProfileVersionId; + internal Guid FileImportLogId; internal DateTime FileReceivedDateTime; diff --git a/FileProcessor.FileImportLog.DomainEvents/FileImportLogAggregateDomainEvents.cs b/FileProcessor.FileImportLog.DomainEvents/FileImportLogAggregateDomainEvents.cs index e103107..efa2078 100644 --- a/FileProcessor.FileImportLog.DomainEvents/FileImportLogAggregateDomainEvents.cs +++ b/FileProcessor.FileImportLog.DomainEvents/FileImportLogAggregateDomainEvents.cs @@ -5,5 +5,5 @@ namespace FileProcessor.FileImportLog.DomainEvents using Shared.DomainDrivenDesign.EventSourcing; public record ImportLogCreatedEvent(Guid FileImportLogId, Guid EstateId, DateTime ImportLogDateTime) : DomainEvent(FileImportLogId, Guid.NewGuid()); - public record FileAddedToImportLogEvent(Guid FileImportLogId, Guid FileId, Guid EstateId, Guid MerchantId, Guid UserId, Guid FileProfileId, String OriginalFileName, String FilePath, DateTime FileUploadedDateTime) : DomainEvent(FileImportLogId, Guid.NewGuid()); + public record FileAddedToImportLogEvent(Guid FileImportLogId, Guid FileId, Guid EstateId, Guid MerchantId, Guid UserId, Guid FileProfileId, String OriginalFileName, String FilePath, DateTime FileUploadedDateTime, Guid FileProfileVersionId = default) : DomainEvent(FileImportLogId, Guid.NewGuid()); } diff --git a/FileProcessor.FileImportLogAggregate.Tests/FileImportLogAggregateTests.cs b/FileProcessor.FileImportLogAggregate.Tests/FileImportLogAggregateTests.cs index 1828e8c..3ec6045 100644 --- a/FileProcessor.FileImportLogAggregate.Tests/FileImportLogAggregateTests.cs +++ b/FileProcessor.FileImportLogAggregate.Tests/FileImportLogAggregateTests.cs @@ -69,6 +69,25 @@ public void FileImportLogAggregate_AddImportedFile_FileAdded() fileImportLog.Files.Any(f => f.FileId == TestData.FileId).ShouldBeTrue(); } + [Fact] + public void FileImportLogAggregate_AddImportedFile_CapturesFileProfileVersionId() + { + FileImportLogAggregate fileImportLogAggregate = FileImportLogAggregate.Create(TestData.FileImportLogId); + fileImportLogAggregate.CreateImportLog(TestData.EstateId, TestData.ImportLogDateTime); + + Result result = fileImportLogAggregate.AddImportedFile(TestData.FileId, + TestData.MerchantId, + TestData.UserId, + TestData.FileProfileId, + TestData.OriginalFileName, + TestData.FilePath, + TestData.FileUploadedDateTime, + TestData.FileProfileVersionId); + + result.IsSuccess.ShouldBeTrue(); + fileImportLogAggregate.GetFileImportLog().Files.Single().FileProfileVersionId.ShouldBe(TestData.FileProfileVersionId); + } + [Fact] public void FileImportLogAggregate_AddImportedFile_ImportLogNotCreated_ErrorThrown() { FileImportLogAggregate fileImportLogAggregate = FileImportLogAggregate.Create(TestData.FileImportLogId); diff --git a/FileProcessor.FileImportLogAggregate/FileImportLogAggregate.cs b/FileProcessor.FileImportLogAggregate/FileImportLogAggregate.cs index c209446..9956764 100644 --- a/FileProcessor.FileImportLogAggregate/FileImportLogAggregate.cs +++ b/FileProcessor.FileImportLogAggregate/FileImportLogAggregate.cs @@ -27,7 +27,7 @@ public static Result CreateImportLog(this FileImportLogAggregate aggregate, Guid return Result.Success(); } - public static Result AddImportedFile(this FileImportLogAggregate aggregate, Guid fileId, Guid merchantId, Guid userId, Guid fileProfileId, String originalFileName, String filePath, DateTime fileUploadedDateTime) + public static Result AddImportedFile(this FileImportLogAggregate aggregate, Guid fileId, Guid merchantId, Guid userId, Guid fileProfileId, String originalFileName, String filePath, DateTime fileUploadedDateTime, Guid fileProfileVersionId = default) { if (aggregate.IsCreated == false) { @@ -40,7 +40,7 @@ public static Result AddImportedFile(this FileImportLogAggregate aggregate, Guid } FileAddedToImportLogEvent fileAddedToImportLogEvent = - new FileAddedToImportLogEvent(aggregate.AggregateId, fileId, aggregate.EstateId, merchantId, userId, fileProfileId, originalFileName, filePath, fileUploadedDateTime); + new FileAddedToImportLogEvent(aggregate.AggregateId, fileId, aggregate.EstateId, merchantId, userId, fileProfileId, originalFileName, filePath, fileUploadedDateTime, fileProfileVersionId); aggregate.ApplyAndAppend(fileAddedToImportLogEvent); @@ -61,6 +61,7 @@ public static void PlayEvent(this FileImportLogAggregate aggregate, FileAddedToI EstateId = domainEvent.EstateId, FileId = domainEvent.FileId, FileProfileId = domainEvent.FileProfileId, + FileProfileVersionId = domainEvent.FileProfileVersionId == Guid.Empty ? domainEvent.FileProfileId : domainEvent.FileProfileVersionId, MerchantId = domainEvent.MerchantId, FilePath = domainEvent.FilePath, OriginalFileName = domainEvent.OriginalFileName, diff --git a/FileProcessor.Testing/TestData.cs b/FileProcessor.Testing/TestData.cs index 6edd3e6..a701bae 100644 --- a/FileProcessor.Testing/TestData.cs +++ b/FileProcessor.Testing/TestData.cs @@ -37,6 +37,8 @@ public class TestData public static Guid FileProfileId = Guid.Parse("D0D3A4E5-870E-42F6-AD0E-5E24252BC95E"); + public static Guid FileProfileVersionId = Guid.Parse("A0D3A4E5-870E-42F6-AD0E-5E24252BC95E"); + public static String FileName = "testfile1.csv"; public static Int32 LineNumber = 1; @@ -288,6 +290,7 @@ public static FileDetails GetCreatedFileDetails() EstateId = TestData.EstateId, MerchantId = TestData.MerchantId, FileProfileId = TestData.FileProfileId, + FileProfileVersionId = TestData.FileProfileVersionId, FileId = TestData.FileId, FileImportLogId = TestData.FileImportLogId, FileLocation = TestData.FilePathWithName, @@ -586,6 +589,7 @@ public static List GetMerchantContractsResponseNoNullValueProd FileImportLogId = TestData.FileImportLogId, FileLocation = TestData.FileLocation, FileProfileId = TestData.FileProfileId, + FileProfileVersionId = TestData.FileProfileVersionId, MerchantId = TestData.MerchantId, ProcessingCompleted = true, UserId = TestData.UserId, @@ -706,6 +710,7 @@ public static List GetMerchantContractsResponseNoNullValueProd FileId = Guid.NewGuid(), FilePath = "/home/txnproc/file1.csv", FileProfileId = TestData.FileProfileId, + FileProfileVersionId = TestData.FileProfileVersionId, OriginalFileName = "Testfile1.csv", UserId = TestData.UserId, UploadedDateTime = TestData.FileUploadedDateTime @@ -717,6 +722,7 @@ public static List GetMerchantContractsResponseNoNullValueProd FileId = Guid.NewGuid(), FilePath = "/home/txnproc/file2.csv", FileProfileId = TestData.FileProfileId, + FileProfileVersionId = TestData.FileProfileVersionId, OriginalFileName = "Testfile2.csv", UserId = TestData.UserId, UploadedDateTime = TestData.FileUploadedDateTime @@ -742,6 +748,7 @@ public static List GetMerchantContractsResponseNoNullValueProd FileId = Guid.NewGuid(), FilePath = "/home/txnproc/file3.csv", FileProfileId = TestData.FileProfileId, + FileProfileVersionId = TestData.FileProfileVersionId, OriginalFileName = "Testfile3.csv", UserId = TestData.UserId, UploadedDateTime = TestData.FileUploadedDateTime @@ -753,6 +760,7 @@ public static List GetMerchantContractsResponseNoNullValueProd FileId = Guid.NewGuid(), FilePath = "/home/txnproc/file2.csv", FileProfileId = TestData.FileProfileId, + FileProfileVersionId = TestData.FileProfileVersionId, OriginalFileName = "Testfile2.csv", UserId = TestData.UserId, UploadedDateTime = TestData.FileUploadedDateTime diff --git a/FileProcessor.Tests/ModelFactoryTests.cs b/FileProcessor.Tests/ModelFactoryTests.cs index c20b109..b9b1c99 100644 --- a/FileProcessor.Tests/ModelFactoryTests.cs +++ b/FileProcessor.Tests/ModelFactoryTests.cs @@ -112,6 +112,7 @@ private void VerifyFileDetails(FileDetails source, fileDetails.FileId.ShouldBe(source.FileId); fileDetails.FileLocation.ShouldBe(source.FileLocation); fileDetails.FileProfileId.ShouldBe(source.FileProfileId); + fileDetails.FileProfileVersionId.ShouldBe(source.FileProfileVersionId); fileDetails.MerchantId.ShouldBe(source.MerchantId); fileDetails.UserId.ShouldBe(source.UserId); fileDetails.ProcessingCompleted.ShouldBe(source.ProcessingCompleted); @@ -180,6 +181,7 @@ private void VerifyFileImportLog(FileProcessor.Models.FileImportLog source, foundFile.OriginalFileName.ShouldBe(importLogFile.OriginalFileName); foundFile.FilePath.ShouldBe(importLogFile.FilePath); foundFile.FileProfileId.ShouldBe(importLogFile.FileProfileId); + foundFile.FileProfileVersionId.ShouldBe(importLogFile.FileProfileVersionId); foundFile.FileUploadedDateTime.ShouldBe(importLogFile.UploadedDateTime); foundFile.UserId.ShouldBe(importLogFile.UserId); } diff --git a/FileProcessor/Common/ModelFactory.cs b/FileProcessor/Common/ModelFactory.cs index 521c33e..6c9d912 100644 --- a/FileProcessor/Common/ModelFactory.cs +++ b/FileProcessor/Common/ModelFactory.cs @@ -77,6 +77,7 @@ public static FileDetailsResponse ConvertFrom(Models.FileDetails fileDetails) UserId = fileDetails.UserId, MerchantId = fileDetails.MerchantId, FileProfileId = fileDetails.FileProfileId, + FileProfileVersionId = fileDetails.FileProfileVersionId, FileLocation = fileDetails.FileLocation, ProcessingCompleted = fileDetails.ProcessingCompleted, IsRejected = fileDetails.IsRejected, @@ -162,6 +163,7 @@ public static FileImportLogFile ConvertFrom(ImportLogFile importLogFile) FileId = importLogFile.FileId, FilePath = importLogFile.FilePath, FileProfileId = importLogFile.FileProfileId, + FileProfileVersionId = importLogFile.FileProfileVersionId, FileUploadedDateTime = importLogFile.UploadedDateTime, MerchantId = importLogFile.MerchantId, OriginalFileName = importLogFile.OriginalFileName, From 67a7b34ce26351f4c68db53b463ace628121262d Mon Sep 17 00:00:00 2001 From: StuartFerguson Date: Fri, 16 Oct 2026 17:34:47 +0100 Subject: [PATCH 2/3] Address profile version review feedback --- .../FileProcessorDomainServiceTests.cs | 5 +++ .../Services/FileProcessorDomainService.cs | 11 ++++- .../FileImportLogAggregateTests.cs | 16 +++---- .../FileImportLogAggregate.cs | 44 +++++++++++++++++-- 4 files changed, 62 insertions(+), 14 deletions(-) diff --git a/FileProcessor.BusinessLogic.Tests/FileProcessorDomainServiceTests.cs b/FileProcessor.BusinessLogic.Tests/FileProcessorDomainServiceTests.cs index eab6d41..0b0e349 100644 --- a/FileProcessor.BusinessLogic.Tests/FileProcessorDomainServiceTests.cs +++ b/FileProcessor.BusinessLogic.Tests/FileProcessorDomainServiceTests.cs @@ -60,6 +60,9 @@ public FileProcessorDomainServiceTests() Logger.Initialise(NullLogger.Instance); this.FileProcessorManager = new IFileProcessorManagerImposter(); + this.FileProcessorManager.GetFileProfileVersion(Arg.Any(), + Arg.Any(), + Arg.Any()).ReturnsAsync(TestData.FileProfileSafaricom); this.FileImportLogAggregateRepository = new IAggregateRepositoryImposter(); this.FileAggregateRepository = @@ -820,6 +823,8 @@ public async Task FileRequestHandler_ProcessTransactionForFileLineRequest_FilePr this.FileAggregateRepository.GetLatestVersion(Arg.Any(), Arg.Any()).ReturnsAsync(Result.Success(TestData.GetFileAggregateWithLines())); this.FileProcessorManager.GetFileProfile(Arg.Any(), Arg.Any()) .ReturnsAsync(Result.NotFound()); + this.FileProcessorManager.GetFileProfileVersion(Arg.Any(), Arg.Any(), Arg.Any()) + .ReturnsAsync(Result.NotFound()); var result = await this.FileProcessorDomainService.ProcessTransactionForFileLine( TestData.ProcessTransactionForFileLineCommand, CancellationToken.None); result.IsFailed.ShouldBeTrue(); diff --git a/FileProcessor.BusinessLogic/Services/FileProcessorDomainService.cs b/FileProcessor.BusinessLogic/Services/FileProcessorDomainService.cs index d2b91d6..b297229 100644 --- a/FileProcessor.BusinessLogic/Services/FileProcessorDomainService.cs +++ b/FileProcessor.BusinessLogic/Services/FileProcessorDomainService.cs @@ -217,7 +217,14 @@ public async Task UploadFile(FileCommands.UploadFileCommand command, } // Update Import log aggregate - Result stateResult= fileImportLogAggregate.AddImportedFile(command.FileId, command.MerchantId, command.UserId, command.FileProfileId, originalName, command.FilePath, command.FileUploadedDateTime, fileProfileVersionId); + Result stateResult= fileImportLogAggregate.AddImportedFile(new AddImportedFileRequest(command.FileId, + command.MerchantId, + command.UserId, + command.FileProfileId, + originalName, + command.FilePath, + command.FileUploadedDateTime, + fileProfileVersionId)); if (stateResult.IsFailed) return stateResult; @@ -910,7 +917,7 @@ private async Task> GetFileProfileForProcessing(Guid fi Guid fileProfileVersionId, CancellationToken cancellationToken) { - if (fileProfileVersionId == Guid.Empty || fileProfileVersionId == fileProfileId) + if (fileProfileVersionId == Guid.Empty) return await this.FileProcessorManager.GetFileProfile(fileProfileId, cancellationToken); return await this.FileProcessorManager.GetFileProfileVersion(fileProfileId, diff --git a/FileProcessor.FileImportLogAggregate.Tests/FileImportLogAggregateTests.cs b/FileProcessor.FileImportLogAggregate.Tests/FileImportLogAggregateTests.cs index 3ec6045..7f81d6d 100644 --- a/FileProcessor.FileImportLogAggregate.Tests/FileImportLogAggregateTests.cs +++ b/FileProcessor.FileImportLogAggregate.Tests/FileImportLogAggregateTests.cs @@ -75,14 +75,14 @@ public void FileImportLogAggregate_AddImportedFile_CapturesFileProfileVersionId( FileImportLogAggregate fileImportLogAggregate = FileImportLogAggregate.Create(TestData.FileImportLogId); fileImportLogAggregate.CreateImportLog(TestData.EstateId, TestData.ImportLogDateTime); - Result result = fileImportLogAggregate.AddImportedFile(TestData.FileId, - TestData.MerchantId, - TestData.UserId, - TestData.FileProfileId, - TestData.OriginalFileName, - TestData.FilePath, - TestData.FileUploadedDateTime, - TestData.FileProfileVersionId); + Result result = fileImportLogAggregate.AddImportedFile(new AddImportedFileRequest(TestData.FileId, + TestData.MerchantId, + TestData.UserId, + TestData.FileProfileId, + TestData.OriginalFileName, + TestData.FilePath, + TestData.FileUploadedDateTime, + TestData.FileProfileVersionId)); result.IsSuccess.ShouldBeTrue(); fileImportLogAggregate.GetFileImportLog().Files.Single().FileProfileVersionId.ShouldBe(TestData.FileProfileVersionId); diff --git a/FileProcessor.FileImportLogAggregate/FileImportLogAggregate.cs b/FileProcessor.FileImportLogAggregate/FileImportLogAggregate.cs index 9956764..b7267cf 100644 --- a/FileProcessor.FileImportLogAggregate/FileImportLogAggregate.cs +++ b/FileProcessor.FileImportLogAggregate/FileImportLogAggregate.cs @@ -27,20 +27,47 @@ public static Result CreateImportLog(this FileImportLogAggregate aggregate, Guid return Result.Success(); } - public static Result AddImportedFile(this FileImportLogAggregate aggregate, Guid fileId, Guid merchantId, Guid userId, Guid fileProfileId, String originalFileName, String filePath, DateTime fileUploadedDateTime, Guid fileProfileVersionId = default) + public static Result AddImportedFile(this FileImportLogAggregate aggregate, + Guid fileId, + Guid merchantId, + Guid userId, + Guid fileProfileId, + String originalFileName, + String filePath, + DateTime fileUploadedDateTime) + { + return aggregate.AddImportedFile(new AddImportedFileRequest(fileId, + merchantId, + userId, + fileProfileId, + originalFileName, + filePath, + fileUploadedDateTime)); + } + + public static Result AddImportedFile(this FileImportLogAggregate aggregate, AddImportedFileRequest request) { if (aggregate.IsCreated == false) { return Result.Invalid("Import log has not been created"); } - if (aggregate.Files.Any(f => f.FileId == fileId)) + if (aggregate.Files.Any(f => f.FileId == request.FileId)) { - return Result.Invalid($"Duplicate file {originalFileName} detected File Id [{fileId}]"); + return Result.Invalid($"Duplicate file {request.OriginalFileName} detected File Id [{request.FileId}]"); } FileAddedToImportLogEvent fileAddedToImportLogEvent = - new FileAddedToImportLogEvent(aggregate.AggregateId, fileId, aggregate.EstateId, merchantId, userId, fileProfileId, originalFileName, filePath, fileUploadedDateTime, fileProfileVersionId); + new FileAddedToImportLogEvent(aggregate.AggregateId, + request.FileId, + aggregate.EstateId, + request.MerchantId, + request.UserId, + request.FileProfileId, + request.OriginalFileName, + request.FilePath, + request.FileUploadedDateTime, + request.FileProfileVersionId); aggregate.ApplyAndAppend(fileAddedToImportLogEvent); @@ -81,6 +108,15 @@ public static Models.FileImportLog GetFileImportLog(this FileImportLogAggregate } } + public record AddImportedFileRequest(Guid FileId, + Guid MerchantId, + Guid UserId, + Guid FileProfileId, + String OriginalFileName, + String FilePath, + DateTime FileUploadedDateTime, + Guid FileProfileVersionId = default); + public record FileImportLogAggregate : Aggregate { From b4d05e08e9b3079a2a460350e5d0684c50c97123 Mon Sep 17 00:00:00 2001 From: StuartFerguson Date: Fri, 16 Oct 2026 17:42:13 +0100 Subject: [PATCH 3/3] Preserve legacy file profile version marker --- .../FileAggregateTests.cs | 18 ++++++++++++++++++ FileProcessor.FileAggregate/FileAggregate.cs | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/FileProcessor.FileAggregate.Tests/FileAggregateTests.cs b/FileProcessor.FileAggregate.Tests/FileAggregateTests.cs index e9af557..4692329 100644 --- a/FileProcessor.FileAggregate.Tests/FileAggregateTests.cs +++ b/FileProcessor.FileAggregate.Tests/FileAggregateTests.cs @@ -56,6 +56,24 @@ public void FileAggregate_CreateFile_FileIsCreated() fileDetails.ProcessingCompleted.ShouldBeFalse(); } + [Fact] + public void FileAggregate_PlayLegacyFileCreatedEvent_PreservesEmptyFileProfileVersionId() + { + FileAggregate fileAggregate = FileAggregate.Create(TestData.FileId); + + fileAggregate.PlayEvent(new FileCreatedEvent(TestData.FileId, + TestData.FileImportLogId, + TestData.EstateId, + TestData.MerchantId, + TestData.UserId, + TestData.FileProfileId, + TestData.FileLocation, + TestData.FileUploadedDateTime, + TestData.SafaricomOperatorId)); + + fileAggregate.GetFile().FileProfileVersionId.ShouldBe(Guid.Empty); + } + [Fact] public void FileAggregate_CreateFile_FileAlreadyCreated_NoErrorThrown() { diff --git a/FileProcessor.FileAggregate/FileAggregate.cs b/FileProcessor.FileAggregate/FileAggregate.cs index b62f704..d616e3e 100644 --- a/FileProcessor.FileAggregate/FileAggregate.cs +++ b/FileProcessor.FileAggregate/FileAggregate.cs @@ -22,7 +22,7 @@ public static void PlayEvent(this FileAggregate aggregate, FileCreatedEvent doma aggregate.EstateId = domainEvent.EstateId; aggregate.MerchantId = domainEvent.MerchantId; aggregate.FileProfileId = domainEvent.FileProfileId; - aggregate.FileProfileVersionId = domainEvent.FileProfileVersionId == Guid.Empty ? domainEvent.FileProfileId : domainEvent.FileProfileVersionId; + aggregate.FileProfileVersionId = domainEvent.FileProfileVersionId; aggregate.UserId = domainEvent.UserId; aggregate.FileLocation = domainEvent.FileLocation; aggregate.FileImportLogId = domainEvent.FileImportLogId;