Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion FIleProcessor.Models/FileLine.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
namespace FileProcessor.Models
{
using System;
using System.Collections.Generic;

/// <summary>
///
Expand Down Expand Up @@ -50,6 +51,10 @@ public class FileLine
/// </value>
public String RejectedReason { get; set; }

public Int32 DispatchAttemptCount { get; set; }

public List<TransactionDispatchAttempt> FailedDispatchAttempts { get; set; } = new();

#endregion
}
}
}
13 changes: 13 additions & 0 deletions FIleProcessor.Models/TransactionDispatchAttempt.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace FileProcessor.Models
{
using System;

public class TransactionDispatchAttempt
{
public Int32 TransactionNumber { get; set; }

public String FailureType { get; set; }

public DateTime AttemptedAt { get; set; }
}
}
107 changes: 106 additions & 1 deletion FileProcessor.BusinessLogic.Tests/FileProcessorDomainServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ namespace FileProcessor.BusinessLogic.Tests;
using System.IO;
using System.IO.Abstractions;
using System.IO.Abstractions.TestingHelpers;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using FileAggregate;
using FileFormatHandlers;
using FileImportLogAggregate;
using Models;
using Managers;
using Microsoft.Extensions.Configuration;
using Imposter.Abstractions;
Expand Down Expand Up @@ -959,12 +961,114 @@ public async Task FileRequestHandler_ProcessTransactionForFileLineRequest_Transa
message.Contains(TestData.FileId.ToString()));
}

[Fact]
public async Task FileRequestHandler_ProcessTransactionForFileLineRequest_SendFailure_LeavesLineEligibleForRetry()
{
this.FileProcessorManager.GetFileProfile(Arg<Guid>.Any(), Arg<CancellationToken>.Any()).ReturnsAsync(TestData.FileProfileSafaricom);
FileAggregate fileAggregate = TestData.GetFileAggregateWithLines();
this.FileAggregateRepository.GetLatestVersion(Arg<Guid>.Any(), Arg<CancellationToken>.Any()).ReturnsAsync(Result.Success(fileAggregate));
this.FileAggregateRepository.SaveChanges(Arg<FileAggregate>.Any(), Arg<CancellationToken>.Any()).ReturnsAsync(Result.Success());
this.TransactionProcessorClient.PerformTransaction(Arg<String>.Any(), Arg<SaleTransactionRequest>.Any(), Arg<CancellationToken>.Any())
.ReturnsAsync(Result.Failure());
this.TransactionProcessorClient.GetMerchant(Arg<String>.Any(), Arg<Guid>.Any(), Arg<Guid>.Any(), Arg<CancellationToken>.Any())
.ReturnsAsync(TestData.GetMerchantResponseWithOperator);
this.TransactionProcessorClient.GetMerchantContracts(Arg<String>.Any(), Arg<Guid>.Any(), Arg<Guid>.Any(), Arg<CancellationToken>.Any())
.ReturnsAsync(Result.Success(TestData.GetMerchantContractsResponse()));
this.SecurityServiceClient.GetToken(Arg<String>.Any(), Arg<String>.Any(), Arg<CancellationToken>.Any()).ReturnsAsync(Result.Success(TestData.TokenResponse()));
this.FileFormatHandler.FileLineCanBeIgnored(Arg<String>.Any()).Returns(false);
this.FileFormatHandler.ParseFileLine(Arg<String>.Any()).Returns(TestData.TransactionMetadata);

Result result = await this.FileProcessorDomainService.ProcessTransactionForFileLine(TestData.ProcessTransactionForFileLineCommand, CancellationToken.None);

result.IsFailed.ShouldBeTrue();
this.FileAggregateRepository.SaveChanges(Arg<FileAggregate>.Any(), Arg<CancellationToken>.Any()).Called(Count.Once());
fileAggregate.GetFile().FileLines.Single().ProcessingResult.ShouldBe(ProcessingResult.NotProcessed);
fileAggregate.GetFile().FileLines.Single().FailedDispatchAttempts.ShouldHaveSingleItem().FailureType.ShouldBe("SendFailure");
fileAggregate.GetFile().FileLines.Single().DispatchAttemptCount.ShouldBe(1);
}

[Fact]
public async Task FileRequestHandler_ProcessTransactionForFileLineRequest_RetryCarriesSameFileLineIdentity()
{
Int32 aggregateReadCount = 0;
Int32 transactionAttemptCount = 0;
FileAggregate fileAggregate = TestData.GetFileAggregateWithLines();
List<String> transactionNumbers = new();
List<String> retryKeys = new();
this.FileProcessorManager.GetFileProfile(Arg<Guid>.Any(), Arg<CancellationToken>.Any()).ReturnsAsync(TestData.FileProfileSafaricom);
this.FileAggregateRepository.GetLatestVersion(Arg<Guid>.Any(), Arg<CancellationToken>.Any())
.Returns((Guid fileId, CancellationToken cancellationToken) =>
{
aggregateReadCount++;
return Task.FromResult(Result.Success(fileAggregate));
});
this.FileAggregateRepository.SaveChanges(Arg<FileAggregate>.Any(), Arg<CancellationToken>.Any()).ReturnsAsync(Result.Success());
this.TransactionProcessorClient.PerformTransaction(Arg<String>.Any(), Arg<SaleTransactionRequest>.Any(), Arg<CancellationToken>.Any())
.Returns(async (String accessToken, SaleTransactionRequest request, CancellationToken cancellationToken) =>
{
transactionAttemptCount++;
transactionNumbers.Add(request.TransactionNumber);
retryKeys.Add($"{request.AdditionalTransactionMetadata["FileId"]}:{request.AdditionalTransactionMetadata["FileLineNumber"]}");
if (transactionAttemptCount == 1)
return Result.Failure();

return TestData.ClientSaleTransactionResponse;
});
this.TransactionProcessorClient.GetMerchant(Arg<String>.Any(), Arg<Guid>.Any(), Arg<Guid>.Any(), Arg<CancellationToken>.Any())
.ReturnsAsync(TestData.GetMerchantResponseWithOperator);
this.TransactionProcessorClient.GetMerchantContracts(Arg<String>.Any(), Arg<Guid>.Any(), Arg<Guid>.Any(), Arg<CancellationToken>.Any())
.ReturnsAsync(Result.Success(TestData.GetMerchantContractsResponse()));
this.SecurityServiceClient.GetToken(Arg<String>.Any(), Arg<String>.Any(), Arg<CancellationToken>.Any()).ReturnsAsync(Result.Success(TestData.TokenResponse()));
this.FileFormatHandler.FileLineCanBeIgnored(Arg<String>.Any()).Returns(false);
this.FileFormatHandler.ParseFileLine(Arg<String>.Any()).Returns((String line) => new Dictionary<String, String>(TestData.TransactionMetadata));

Result firstAttempt = await this.FileProcessorDomainService.ProcessTransactionForFileLine(TestData.ProcessTransactionForFileLineCommand, CancellationToken.None);
Result retry = await this.FileProcessorDomainService.ProcessTransactionForFileLine(TestData.ProcessTransactionForFileLineCommand, CancellationToken.None);

firstAttempt.IsFailed.ShouldBeTrue();
retry.IsSuccess.ShouldBeTrue();
aggregateReadCount.ShouldBe(2);
transactionAttemptCount.ShouldBe(2);
transactionNumbers.Count.ShouldBe(2);
Int32.TryParse(transactionNumbers[0], out _).ShouldBeTrue();
Int32.TryParse(transactionNumbers[1], out _).ShouldBeTrue();
retryKeys[0].ShouldBe(retryKeys[1]);
fileAggregate.GetFile().FileLines.Single().DispatchAttemptCount.ShouldBe(2);
}

[Fact]
public async Task FileRequestHandler_ProcessTransactionForFileLineRequest_TransactionDispatchTimeout_RecordsAttemptAndLeavesLineEligibleForRetry()
{
this.FileProcessorManager.GetFileProfile(Arg<Guid>.Any(), Arg<CancellationToken>.Any()).ReturnsAsync(TestData.FileProfileSafaricom);
FileAggregate fileAggregate = TestData.GetFileAggregateWithLines();
this.FileAggregateRepository.GetLatestVersion(Arg<Guid>.Any(), Arg<CancellationToken>.Any()).ReturnsAsync(Result.Success(fileAggregate));
this.FileAggregateRepository.SaveChanges(Arg<FileAggregate>.Any(), Arg<CancellationToken>.Any()).ReturnsAsync(Result.Success());
this.TransactionProcessorClient.PerformTransaction(Arg<String>.Any(), Arg<SaleTransactionRequest>.Any(), Arg<CancellationToken>.Any())
.ThrowsAsync(new TimeoutException("Transaction processor request timed out"));
this.TransactionProcessorClient.GetMerchant(Arg<String>.Any(), Arg<Guid>.Any(), Arg<Guid>.Any(), Arg<CancellationToken>.Any())
.ReturnsAsync(TestData.GetMerchantResponseWithOperator);
this.TransactionProcessorClient.GetMerchantContracts(Arg<String>.Any(), Arg<Guid>.Any(), Arg<Guid>.Any(), Arg<CancellationToken>.Any())
.ReturnsAsync(Result.Success(TestData.GetMerchantContractsResponse()));
this.SecurityServiceClient.GetToken(Arg<String>.Any(), Arg<String>.Any(), Arg<CancellationToken>.Any()).ReturnsAsync(Result.Success(TestData.TokenResponse()));
this.FileFormatHandler.FileLineCanBeIgnored(Arg<String>.Any()).Returns(false);
this.FileFormatHandler.ParseFileLine(Arg<String>.Any()).Returns(TestData.TransactionMetadata);

Result result = await this.FileProcessorDomainService.ProcessTransactionForFileLine(TestData.ProcessTransactionForFileLineCommand, CancellationToken.None);

result.IsFailed.ShouldBeTrue();
this.FileAggregateRepository.SaveChanges(Arg<FileAggregate>.Any(), Arg<CancellationToken>.Any()).Called(Count.Once());
fileAggregate.GetFile().FileLines.Single().ProcessingResult.ShouldBe(ProcessingResult.NotProcessed);
fileAggregate.GetFile().FileLines.Single().FailedDispatchAttempts.ShouldHaveSingleItem().FailureType.ShouldBe("Timeout");
fileAggregate.GetFile().FileLines.Single().DispatchAttemptCount.ShouldBe(1);
}

[Fact]
public async Task FileRequestHandler_ProcessTransactionForFileLineRequest_TransactionNotSuccessful_RequestIsHandled()
{
this.FileProcessorManager.GetFileProfile(Arg<Guid>.Any(), Arg<CancellationToken>.Any()).ReturnsAsync(TestData.FileProfileSafaricom);

this.FileAggregateRepository.GetLatestVersion(Arg<Guid>.Any(), Arg<CancellationToken>.Any()).ReturnsAsync(Result.Success(TestData.GetFileAggregateWithLines()));
FileAggregate fileAggregate = TestData.GetFileAggregateWithLines();
this.FileAggregateRepository.GetLatestVersion(Arg<Guid>.Any(), Arg<CancellationToken>.Any()).ReturnsAsync(Result.Success(fileAggregate));

this.TransactionProcessorClient.PerformTransaction(Arg<String>.Any(), Arg<SaleTransactionRequest>.Any(), Arg<CancellationToken>.Any())
.ReturnsAsync(TestData.ClientSaleTransactionFailedResponse);
Expand All @@ -986,6 +1090,7 @@ public async Task FileRequestHandler_ProcessTransactionForFileLineRequest_Transa
this.DiagnosticLogger.Messages.ShouldContain(message => message.Contains("transaction-dispatch-completed") &&
message.Contains(TestData.ResponseCodeFailed) &&
message.Contains(TestData.FileId.ToString()));
fileAggregate.GetFile().FileLines.Single().DispatchAttemptCount.ShouldBe(1);
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ internal enum FileLineProcessingStage
TransactionDetailsResolved,
TransactionDispatchStarted,
TransactionDispatchCompleted,
TransactionDispatchAttemptPersisted,
FileLineStateUpdate,
FileLineStatePersisted
}
Expand All @@ -44,6 +45,8 @@ public FileLineProcessingContext(FileCommands.ProcessTransactionForFileLineComma

public Boolean TransactionDispatchSucceeded { get; set; }

public String TransactionDispatchFailureType { get; set; }

public Guid EstateId { get; set; }

public Guid MerchantId { get; set; }
Expand Down Expand Up @@ -141,6 +144,7 @@ public static void EventHandlerDispatchThrew(FileLineAddedEvent domainEvent, Exc
FileLineProcessingStage.TransactionDetailsResolved => "transaction-details-resolved",
FileLineProcessingStage.TransactionDispatchStarted => "transaction-dispatch-started",
FileLineProcessingStage.TransactionDispatchCompleted => "transaction-dispatch-completed",
FileLineProcessingStage.TransactionDispatchAttemptPersisted => "transaction-dispatch-attempt-persisted",
FileLineProcessingStage.FileLineStateUpdate => "file-line-state-update",
FileLineProcessingStage.FileLineStatePersisted => "file-line-state-persisted",
_ => throw new ArgumentOutOfRangeException(nameof(stage), stage, "Unknown file-line processing stage")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,15 @@ private async Task<Result> HandleSpecificDomainEvent(FileLineAddedEvent domainEv
}
}

private Task<Result> HandleSpecificDomainEvent(FileLineTransactionDispatchFailedEvent domainEvent,
CancellationToken cancellationToken)
{
_ = domainEvent;
_ = cancellationToken;
// The event records an attempt for audit purposes; it must not enqueue the line as completed.
return Task.FromResult(Result.Success());
}

/// <summary>
/// Handles the specific domain event.
/// </summary>
Expand Down
Loading
Loading