diff --git a/Directory.Packages.props b/Directory.Packages.props
index 593e495..c585016 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -5,7 +5,7 @@
-
+
diff --git a/FIleProcessor.Models/FileLine.cs b/FIleProcessor.Models/FileLine.cs
index 2e5a4b3..db06d06 100644
--- a/FIleProcessor.Models/FileLine.cs
+++ b/FIleProcessor.Models/FileLine.cs
@@ -3,58 +3,22 @@
using System;
using System.Collections.Generic;
- ///
- ///
- ///
public class FileLine
{
- #region Properties
-
- ///
- /// Gets or sets the line data.
- ///
- ///
- /// The line data.
- ///
public String LineData { get; set; }
- ///
- /// Gets or sets the line number.
- ///
- ///
- /// The line number.
- ///
public Int32 LineNumber { get; set; }
- ///
- /// Gets or sets a value indicating whether this is ignored.
- ///
- ///
- /// true if ignored; otherwise, false.
- ///
public ProcessingResult ProcessingResult { get; set; }
- ///
- /// Gets or sets the transaction identifier.
- ///
- ///
- /// The transaction identifier.
- /// 1011934
- ///
public Guid TransactionId { get; set; }
- ///
- /// Gets or sets the rejected reason.
- ///
- ///
- /// The rejected reason.
- ///
+ public Guid EventId { get; set; }
+
public String RejectedReason { get; set; }
public Int32 DispatchAttemptCount { get; set; }
public List FailedDispatchAttempts { get; set; } = new();
-
- #endregion
}
}
diff --git a/FileProcessor.BusinessLogic.Tests/DummyFileProcessorDomainService.cs b/FileProcessor.BusinessLogic.Tests/DummyFileProcessorDomainService.cs
index 74f49e0..ab7de3c 100644
--- a/FileProcessor.BusinessLogic.Tests/DummyFileProcessorDomainService.cs
+++ b/FileProcessor.BusinessLogic.Tests/DummyFileProcessorDomainService.cs
@@ -24,6 +24,8 @@ public async Task ProcessUploadedFile(FileCommands.ProcessUploadedFileCo
public async Task ProcessTransactionForFileLine(FileCommands.ProcessTransactionForFileLineCommand command,
CancellationToken cancellationToken) => Result.Success();
+
+ public async Task ReplayFileLine(FileCommands.ReplayFileLineCommand command, CancellationToken cancellationToken) => Result.Success();
}
public class DummyFileProcessorManager : IFileProcessorManager {
diff --git a/FileProcessor.BusinessLogic.Tests/FileHandlersTests.cs b/FileProcessor.BusinessLogic.Tests/FileHandlersTests.cs
new file mode 100644
index 0000000..d021bd6
--- /dev/null
+++ b/FileProcessor.BusinessLogic.Tests/FileHandlersTests.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using FileProcessor.BusinessLogic.Requests;
+using FileProcessor.Handlers;
+using Imposter.Abstractions;
+using MediatR;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.HttpResults;
+using Shouldly;
+using SimpleResults;
+using FileProcessor.Testing;
+using Xunit;
+
+namespace FileProcessor.BusinessLogic.Tests;
+
+public class FileHandlersTests
+{
+ [Fact]
+ public async Task ReplayLine_SendsReplayCommand_AndReturnsOk()
+ {
+ IMediatorImposter mediator = new();
+ mediator.Send(Arg>.Any(), Arg.Any()).ReturnsAsync(Result.Success());
+
+ IResult result = await FileHandlers.ReplayLine(
+ mediator.Instance(), TestData.FileId, TestData.EstateId, TestData.LineNumber, CancellationToken.None);
+
+ StatusCodeHttpResult response = result.ShouldBeOfType();
+ response.StatusCode.ShouldBe(StatusCodes.Status204NoContent);
+ mediator.Send(Arg>.Any(), Arg.Any()).Called(Count.Once());
+ }
+}
diff --git a/FileProcessor.BusinessLogic.Tests/FileProcessorDomainServiceTests.cs b/FileProcessor.BusinessLogic.Tests/FileProcessorDomainServiceTests.cs
index aff8156..021ba88 100644
--- a/FileProcessor.BusinessLogic.Tests/FileProcessorDomainServiceTests.cs
+++ b/FileProcessor.BusinessLogic.Tests/FileProcessorDomainServiceTests.cs
@@ -1092,6 +1092,67 @@ public async Task FileRequestHandler_ProcessTransactionForFileLineRequest_Transa
message.Contains(TestData.FileId.ToString()));
fileAggregate.GetFile().FileLines.Single().DispatchAttemptCount.ShouldBe(1);
}
+
+ [Fact]
+ public async Task FileRequestHandler_ReplayFileLineRequest_UnprocessedLineIsProcessed()
+ {
+ FileAggregate fileAggregate = TestData.GetFileAggregateWithLines();
+ FileLine fileLine = fileAggregate.GetFile().FileLines.Single();
+ this.FileAggregateRepository.GetLatestVersion(Arg.Any(), Arg.Any()).ReturnsAsync(Result.Success(fileAggregate));
+ this.FileAggregateRepository.SaveChanges(Arg.Any(), Arg.Any()).ReturnsAsync(Result.Success());
+ this.FileProcessorManager.GetFileProfile(Arg.Any(), Arg.Any()).ReturnsAsync(TestData.FileProfileSafaricom);
+ this.TransactionProcessorClient.GetMerchant(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()).ReturnsAsync(TestData.GetMerchantResponseWithOperator);
+ this.TransactionProcessorClient.GetMerchantContracts(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()).ReturnsAsync(Result.Success(TestData.GetMerchantContractsResponse()));
+ this.SecurityServiceClient.GetToken(Arg.Any(), Arg.Any(), Arg.Any()).ReturnsAsync(Result.Success(TestData.TokenResponse()));
+ this.TransactionProcessorClient.PerformTransaction(Arg.Any(), Arg.Any(), Arg.Any()).ReturnsAsync(TestData.ClientSaleTransactionResponse);
+ this.FileFormatHandler.FileLineCanBeIgnored(Arg.Any()).Returns(false);
+ this.FileFormatHandler.ParseFileLine(Arg.Any()).Returns(TestData.TransactionMetadata);
+
+ FileCommands.ReplayFileLineCommand command = new(TestData.FileId, TestData.EstateId, fileLine.LineNumber);
+
+ Result result = await this.FileProcessorDomainService.ReplayFileLine(command, CancellationToken.None);
+
+ result.IsSuccess.ShouldBeTrue();
+ fileLine.ProcessingResult.ShouldBe(ProcessingResult.Successful);
+ this.FileAggregateRepository.SaveChanges(Arg.Any(), Arg.Any()).Called(Count.Twice());
+ }
+
+ [Fact]
+ public async Task FileRequestHandler_ReplayFileLineRequest_AlreadyProcessedLineIsIgnored()
+ {
+ FileAggregate fileAggregate = TestData.GetFileAggregateWithLinesAlreadyProcessed();
+ this.FileAggregateRepository.GetLatestVersion(Arg.Any(), Arg.Any()).ReturnsAsync(Result.Success(fileAggregate));
+ this.FileAggregateRepository.SaveChanges(Arg.Any(), Arg.Any()).ReturnsAsync(Result.Success());
+
+ Result result = await this.FileProcessorDomainService.ReplayFileLine(
+ new FileCommands.ReplayFileLineCommand(TestData.FileId, TestData.EstateId, 1), CancellationToken.None);
+
+ result.IsSuccess.ShouldBeTrue();
+ this.FileAggregateRepository.SaveChanges(Arg.Any(), Arg.Any()).Called(Count.Once());
+ }
+
+ [Fact]
+ public async Task FileRequestHandler_ReplayFileLineRequest_LineNotFound_ReturnsFailure()
+ {
+ this.FileAggregateRepository.GetLatestVersion(Arg.Any(), Arg.Any()).ReturnsAsync(Result.Success(TestData.GetFileAggregateWithLines()));
+
+ Result result = await this.FileProcessorDomainService.ReplayFileLine(
+ new FileCommands.ReplayFileLineCommand(TestData.FileId, TestData.EstateId, TestData.NotFoundLineNumber), CancellationToken.None);
+
+ result.IsFailed.ShouldBeTrue();
+ }
+
+ [Fact]
+ public async Task FileRequestHandler_ReplayFileLineRequest_FileAggregateNotFound_ReturnsNotFound()
+ {
+ this.FileAggregateRepository.GetLatestVersion(Arg.Any(), Arg.Any()).ReturnsAsync(Result.NotFound());
+
+ Result result = await this.FileProcessorDomainService.ReplayFileLine(
+ new FileCommands.ReplayFileLineCommand(TestData.FileId, TestData.EstateId, TestData.LineNumber), CancellationToken.None);
+
+ result.IsFailed.ShouldBeTrue();
+ result.Status.ShouldBe(ResultStatus.NotFound);
+ }
private void VerifyFileProcessing(String filePath)
diff --git a/FileProcessor.BusinessLogic.Tests/FileRequestHandlerTests.cs b/FileProcessor.BusinessLogic.Tests/FileRequestHandlerTests.cs
index 6e4851d..6249dc5 100644
--- a/FileProcessor.BusinessLogic.Tests/FileRequestHandlerTests.cs
+++ b/FileProcessor.BusinessLogic.Tests/FileRequestHandlerTests.cs
@@ -4,15 +4,18 @@
using System.Threading.Tasks;
using FileProcessor.BusinessLogic.Managers;
using MediatR;
+using SimpleResults;
namespace FileProcessor.BusinessLogic.Tests
{
using System.Threading;
using Imposter.Abstractions;
using RequestHandlers;
+ using Requests;
using Services;
using Shouldly;
using Testing;
+ using Xunit;
public class FileRequestHandlerTests
{
@@ -45,5 +48,18 @@ public async Task FileRequestHandler_ProcessTransactionForFileLineRequest_Reques
await this.FileRequestHandler.Handle(TestData.ProcessTransactionForFileLineCommand, CancellationToken.None);
});
}
+
+ [Fact]
+ public async Task FileRequestHandler_ReplayFileLineRequest_RequestIsDelegated()
+ {
+ FileCommands.ReplayFileLineCommand command = new(TestData.FileId, TestData.EstateId, TestData.LineNumber);
+ this.FileProcessorDomainService.ReplayFileLine(Arg.Any(), Arg.Any())
+ .ReturnsAsync(Result.Success());
+
+ Result result = await this.FileRequestHandler.Handle(command, CancellationToken.None);
+
+ result.IsSuccess.ShouldBeTrue();
+ this.FileProcessorDomainService.ReplayFileLine(command, CancellationToken.None).Called(Count.Once());
+ }
}
}
diff --git a/FileProcessor.BusinessLogic/RequestHandlers/FileRequestHandler.cs b/FileProcessor.BusinessLogic/RequestHandlers/FileRequestHandler.cs
index 08704d5..a7a29fe 100644
--- a/FileProcessor.BusinessLogic/RequestHandlers/FileRequestHandler.cs
+++ b/FileProcessor.BusinessLogic/RequestHandlers/FileRequestHandler.cs
@@ -17,7 +17,8 @@ public class FileRequestHandler : IRequestHandler,
IRequestHandler>,
IRequestHandler>>,
- IRequestHandler>
+ IRequestHandler>,
+ IRequestHandler
{
private readonly IFileProcessorDomainService DomainService;
private readonly IFileProcessorManager Manager;
@@ -56,5 +57,10 @@ public async Task> Handle(FileQueries.GetFileQuery query,
CancellationToken cancellationToken) {
return await this.Manager.GetFileImportLog(query.FileImportLogId, query.EstateId, query.MerchantId, cancellationToken);
}
+
+ public async Task Handle(FileCommands.ReplayFileLineCommand request, CancellationToken cancellationToken)
+ {
+ return await this.DomainService.ReplayFileLine(request, cancellationToken);
+ }
}
}
diff --git a/FileProcessor.BusinessLogic/Requests/FileCommands.cs b/FileProcessor.BusinessLogic/Requests/FileCommands.cs
index 500ae85..da4364f 100644
--- a/FileProcessor.BusinessLogic/Requests/FileCommands.cs
+++ b/FileProcessor.BusinessLogic/Requests/FileCommands.cs
@@ -26,4 +26,8 @@ public record UploadFileCommand(Guid FileId,
String FilePath,
Guid FileProfileId,
DateTime FileUploadedDateTime) : IRequest;
+
+ public record ReplayFileLineCommand(Guid FileId,
+ Guid EstateId,
+ Int32 LineNumber) : IRequest;
}
\ No newline at end of file
diff --git a/FileProcessor.BusinessLogic/Services/FileProcessorDomainService.cs b/FileProcessor.BusinessLogic/Services/FileProcessorDomainService.cs
index 00190a5..4fdc0ec 100644
--- a/FileProcessor.BusinessLogic/Services/FileProcessorDomainService.cs
+++ b/FileProcessor.BusinessLogic/Services/FileProcessorDomainService.cs
@@ -47,6 +47,8 @@ Task ProcessUploadedFile(FileCommands.ProcessUploadedFileCommand command
Task ProcessTransactionForFileLine(FileCommands.ProcessTransactionForFileLineCommand command,
CancellationToken cancellationToken);
+
+ Task ReplayFileLine(FileCommands.ReplayFileLineCommand command, CancellationToken cancellationToken);
}
public class FileProcessorDomainService : IFileProcessorDomainService
@@ -298,6 +300,34 @@ private async Task> GetOperatorIdForFileProfile(Guid estateId, Guid
return Result.Success(@operator.OperatorId);
}
+ public async Task ReplayFileLine(FileCommands.ReplayFileLineCommand command, CancellationToken cancellationToken)
+ {
+ Result result = await ApplyFileUpdates(async (FileAggregate fileAggregate) =>
+ {
+ FileDetails fileDetails = fileAggregate.GetFile();
+ // Now find the line to be resumbitted
+ FileLine fileLine = fileDetails.FileLines.SingleOrDefault(l => l.LineNumber == command.LineNumber);
+ if (fileLine == null)
+ {
+ return Result.NotFound($"Line number {command.LineNumber} not found in file Id {command.FileId}");
+ }
+
+ if (fileLine.ProcessingResult != ProcessingResult.NotProcessed)
+ {
+ return Result.Success();
+ }
+
+ // Build the command to reprocess the line
+ FileCommands.ProcessTransactionForFileLineCommand processTransactionForFileLineCommand = new FileCommands.ProcessTransactionForFileLineCommand(command.FileId, fileLine.EventId,
+ command.LineNumber, fileLine.LineData);
+ Result result = await this.ProcessTransactionForFileLine(processTransactionForFileLineCommand, cancellationToken);
+ return result;
+ }, command.FileId, cancellationToken);
+
+ return result;
+ }
+
+
public async Task ProcessTransactionForFileLine(FileCommands.ProcessTransactionForFileLineCommand command,
CancellationToken cancellationToken) {
FileLineProcessingContext processingContext = new(command);
diff --git a/FileProcessor.FileAggregate.Tests/FileAggregateTests.cs b/FileProcessor.FileAggregate.Tests/FileAggregateTests.cs
index 9bce7f5..d339740 100644
--- a/FileProcessor.FileAggregate.Tests/FileAggregateTests.cs
+++ b/FileProcessor.FileAggregate.Tests/FileAggregateTests.cs
@@ -5,6 +5,7 @@
namespace FileProcessor.FileAggregate.Tests
{
using System.Linq;
+ using File.DomainEvents;
using FileProcessor.Models;
using Shared.Exceptions;
using Shouldly;
@@ -88,6 +89,17 @@ public void FileAggregate_AddFileLine_FileLineAdded()
fileDetails.ProcessingSummary.IgnoredLines.ShouldBe(0);
}
+ [Fact]
+ public void FileAggregate_PlayFileLineAddedEvent_PreservesEventIdOnFileLine()
+ {
+ FileAggregate fileAggregate = FileAggregate.Create(TestData.FileId);
+ FileLineAddedEvent domainEvent = TestData.FileLineAddedEvent;
+
+ fileAggregate.PlayEvent(domainEvent);
+
+ fileAggregate.GetFile().FileLines.Single().EventId.ShouldBe(domainEvent.EventId);
+ }
+
[Fact]
public void FileAggregate_AddFileLine_AddDuplicateLine_FileLineIsNotAddedAdded()
{
diff --git a/FileProcessor.FileAggregate/FileAggregate.cs b/FileProcessor.FileAggregate/FileAggregate.cs
index 8373a5f..200a147 100644
--- a/FileProcessor.FileAggregate/FileAggregate.cs
+++ b/FileProcessor.FileAggregate/FileAggregate.cs
@@ -60,7 +60,8 @@ public static void PlayEvent(this FileAggregate aggregate, FileLineAddedEvent do
{
LineData = domainEvent.FileLine,
LineNumber = domainEvent.LineNumber,
- FailedDispatchAttempts = new List()
+ FailedDispatchAttempts = new List(),
+ EventId= domainEvent.EventId
});
}
diff --git a/FileProcessor/Endpoints/FileEndpoints.cs b/FileProcessor/Endpoints/FileEndpoints.cs
index 63b3d32..a160268 100644
--- a/FileProcessor/Endpoints/FileEndpoints.cs
+++ b/FileProcessor/Endpoints/FileEndpoints.cs
@@ -16,12 +16,15 @@ public static void MapFileEndpoints(this IEndpointRouteBuilder app)
.WithTags("Files")
.RequireAuthorization();
- group.MapPost("/", FileHandlers.UploadFileAsync)
+ group.MapPost("/", FileHandlers.UploadFile)
.DisableAntiforgery()
.Accepts("multipart/form-data")
.WithName("UploadFile");
- group.MapGet("/{fileId:guid}", FileHandlers.GetFileAsync)
+ group.MapGet("/{fileId:guid}", FileHandlers.GetFile)
.WithName("GetFile");
+
+ group.MapPost("/{fileId:guid}/lines/{lineNumber:int}/retry", FileHandlers.ReplayLine)
+ .WithName("ReplayLine");
}
-}
\ No newline at end of file
+}
diff --git a/FileProcessor/Handlers/FileHandlers.cs b/FileProcessor/Handlers/FileHandlers.cs
index 272af2b..b5a640c 100644
--- a/FileProcessor/Handlers/FileHandlers.cs
+++ b/FileProcessor/Handlers/FileHandlers.cs
@@ -20,7 +20,7 @@ namespace FileProcessor.Handlers;
public static class FileHandlers
{
- public static async Task UploadFileAsync(IMediator mediator,
+ public static async Task UploadFile(IMediator mediator,
[FromForm] UploadFileRequest request,
[FromForm] IFormCollection formCollection,
CancellationToken cancellationToken)
@@ -56,7 +56,7 @@ public static async Task UploadFileAsync(IMediator mediator,
return ResponseFactory.FromResult(result, r => r);
}
- public static async Task GetFileAsync(IMediator mediator,
+ public static async Task GetFile(IMediator mediator,
[FromRoute] Guid fileId,
[FromQuery] Guid estateId,
CancellationToken cancellationToken)
@@ -66,4 +66,16 @@ public static async Task GetFileAsync(IMediator mediator,
return ResponseFactory.FromResult(result, ModelFactory.ConvertFrom);
}
+
+ public static async Task ReplayLine(IMediator mediator,
+ [FromRoute] Guid fileId,
+ [FromQuery] Guid estateId,
+ [FromRoute] Int32 lineNumber,
+ CancellationToken cancellationToken)
+ {
+ FileCommands.ReplayFileLineCommand command = new(fileId, estateId, lineNumber);
+ Result result = await mediator.Send(command, cancellationToken);
+
+ return ResponseFactory.FromResult(result);
+ }
}
\ No newline at end of file