From d4d8d0b8bf9ceb623cc292b518773f42beca3e8c Mon Sep 17 00:00:00 2001 From: StuartFerguson Date: Tue, 13 Oct 2026 14:20:57 +0100 Subject: [PATCH] fixes for file processing scenarios --- .../TransactionAggregateTests.cs | 50 ++++++++++++++++-- .../TransactionAggregate.cs | 3 +- .../Services/TransactionDomainServiceTests.cs | 6 +++ .../TransactionValidationServiceTests.cs | 52 +++++++++++++++++++ .../Services/TransactionDomainService.cs | 8 +-- .../Services/TransactionValidationService.cs | 12 +++-- 6 files changed, 120 insertions(+), 11 deletions(-) diff --git a/TransactionProcessor.Aggregates.Tests/TransactionAggregateTests.cs b/TransactionProcessor.Aggregates.Tests/TransactionAggregateTests.cs index fe738d1a..570d3c79 100644 --- a/TransactionProcessor.Aggregates.Tests/TransactionAggregateTests.cs +++ b/TransactionProcessor.Aggregates.Tests/TransactionAggregateTests.cs @@ -109,8 +109,6 @@ public void TransactionAggregate_StartTransaction_TransactionAlreadyCompleted_Er [InlineData(true, "ABCD", TransactionType.Logon, "ABCDEFG", true, true, "A1234567890")] [InlineData(true, "0001", TransactionType.Logon, "ABCDEFG", false, true, "A1234567890")] [InlineData(true, "0001", TransactionType.Logon, "ABCDEFG", true, false, "A1234567890")] - [InlineData(true, "0001", TransactionType.Logon, "ABCDEFG", true, true, "")] - [InlineData(true, "0001", TransactionType.Logon, "ABCDEFG", true, true, null)] [InlineData(true, "0001", (TransactionType)99, "ABCDEFG", true, true, "A1234567890")] [InlineData(true, "0001", TransactionType.Logon, "", true, true, "A1234567890")] [InlineData(true, "0001", TransactionType.Logon, null, true, true, "A1234567890")] @@ -120,8 +118,6 @@ public void TransactionAggregate_StartTransaction_TransactionAlreadyCompleted_Er [InlineData(true, "ABCD", TransactionType.Sale, "ABCDEFG", true, true, "A1234567890")] [InlineData(true, "0001", TransactionType.Sale, "ABCDEFG", false, true, "A1234567890")] [InlineData(true, "0001", TransactionType.Sale, "ABCDEFG", true, false, "A1234567890")] - [InlineData(true, "0001", TransactionType.Sale, "ABCDEFG", true, true, "")] - [InlineData(true, "0001", TransactionType.Sale, "ABCDEFG", true, true, null)] [InlineData(true, "0001", TransactionType.Sale, "", true, true, "A1234567890")] [InlineData(true, "0001", TransactionType.Sale, null, true, true, "A1234567890")] public void TransactionAggregate_StartTransaction_InvalidData_ErrorThrown(Boolean validTransactionDateTime, @@ -151,6 +147,52 @@ public void TransactionAggregate_StartTransaction_NullContext_ErrorThrown() { result.Status.ShouldBe(ResultStatus.Invalid); } + [Theory] + [InlineData(TransactionSource.NotSet, "")] + [InlineData(TransactionSource.NotSet, null)] + [InlineData(TransactionSource.FileImport, "")] + [InlineData(TransactionSource.FileImport, null)] + public void TransactionAggregate_StartTransaction_NonOnlineSaleWithoutDeviceIdentifier_TransactionIsStarted(TransactionSource transactionSource, + String deviceIdentifier) { + Aggregates.TransactionAggregate transactionAggregate = Aggregates.TransactionAggregate.Create(TestData.TransactionId); + + Result result = transactionAggregate.StartTransaction(TestData.TransactionDateTime, + TestData.TransactionNumber, + TransactionType.Sale, + TestData.TransactionReference, + new TransactionStartContext { + EstateId = TestData.EstateId, + MerchantId = TestData.MerchantId, + DeviceIdentifier = deviceIdentifier, + TransactionSource = transactionSource + }, + TestData.TransactionAmount); + + result.IsSuccess.ShouldBeTrue(); + } + + [Theory] + [InlineData("")] + [InlineData(null)] + public void TransactionAggregate_StartTransaction_OnlineSaleWithoutDeviceIdentifier_ErrorThrown(String deviceIdentifier) { + Aggregates.TransactionAggregate transactionAggregate = Aggregates.TransactionAggregate.Create(TestData.TransactionId); + + Result result = transactionAggregate.StartTransaction(TestData.TransactionDateTime, + TestData.TransactionNumber, + TransactionType.Sale, + TestData.TransactionReference, + new TransactionStartContext { + EstateId = TestData.EstateId, + MerchantId = TestData.MerchantId, + DeviceIdentifier = deviceIdentifier, + TransactionSource = TransactionSource.OnlineSale + }, + TestData.TransactionAmount); + + result.IsFailed.ShouldBeTrue(); + result.Status.ShouldBe(ResultStatus.Invalid); + } + [Theory] [InlineData(TransactionType.Sale)] public void TransactionAggregate_AddProductDetails_ProductDetailsAdded(TransactionType transactionType){ diff --git a/TransactionProcessor.Aggregates/TransactionAggregate.cs b/TransactionProcessor.Aggregates/TransactionAggregate.cs index 974127c9..25269a4f 100644 --- a/TransactionProcessor.Aggregates/TransactionAggregate.cs +++ b/TransactionProcessor.Aggregates/TransactionAggregate.cs @@ -55,6 +55,7 @@ public class TransactionStartContext public Guid EstateId { get; init; } public Guid MerchantId { get; init; } public String DeviceIdentifier { get; init; } + public TransactionSource TransactionSource { get; set; } } public static class TransactionAggregateExtensions{ @@ -556,7 +557,7 @@ private static Result ValidateContext(TransactionStartContext transactionStartCo return Result.Invalid($"Estate Id must not be [{Guid.Empty}]"); if (transactionStartContext.MerchantId == Guid.Empty) return Result.Invalid($"Merchant Id must not be [{Guid.Empty}]"); - if (String.IsNullOrEmpty(transactionStartContext.DeviceIdentifier)) + if (String.IsNullOrEmpty(transactionStartContext.DeviceIdentifier) && transactionStartContext.TransactionSource == TransactionSource.OnlineSale) return Result.Invalid("Device Identifier must not be null or empty"); return Result.Success(); diff --git a/TransactionProcessor.BusinessLogic.Tests/Services/TransactionDomainServiceTests.cs b/TransactionProcessor.BusinessLogic.Tests/Services/TransactionDomainServiceTests.cs index d8fbbdc2..62744aef 100644 --- a/TransactionProcessor.BusinessLogic.Tests/Services/TransactionDomainServiceTests.cs +++ b/TransactionProcessor.BusinessLogic.Tests/Services/TransactionDomainServiceTests.cs @@ -246,6 +246,7 @@ public async Task TransactionDomainService_ProcessSaleTransaction_DeclinedByOper Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any()).ReturnsAsync(Result.Success(new TransactionValidationResult(TransactionResponseCode.Success, "SUCCESS"))); this.OperatorProxy.ProcessSaleMessage(Arg.Any(), @@ -303,6 +304,7 @@ public async Task TransactionDomainService_ProcessSaleTransaction_OperatorProxyT Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any()).ReturnsAsync(Result.Success(new TransactionValidationResult(TransactionResponseCode.Success, "SUCCESS"))); this.OperatorProxy.ProcessSaleMessage(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg>.Any(), Arg.Any()).Throws(new Exception()); @@ -349,6 +351,7 @@ public async Task TransactionDomainService_ProcessSaleTransaction_TransactionIsP Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any()).ReturnsAsync(Result.Success(new TransactionValidationResult(TransactionResponseCode.Success, "SUCCESS"))); this.OperatorProxy.ProcessSaleMessage(Arg.Any(), @@ -412,6 +415,7 @@ public async Task TransactionDomainService_ProcessSaleTransaction_NoFloatFound_T Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any()).ReturnsAsync(Result.Success(new TransactionValidationResult(TransactionResponseCode.Success, "SUCCESS"))); this.OperatorProxy.ProcessSaleMessage(Arg.Any(), @@ -470,6 +474,7 @@ public async Task TransactionDomainService_ProcessSaleTransaction_ValidationFail Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any()).ReturnsAsync(Result.Success(new TransactionValidationResult(responseCode, responseCode.ToString()))); this.AggregateService.GetLatest(Arg.Any(), Arg.Any()).ReturnsAsync(Result.Success(TestData.GetEmptyFloatAggregate())); @@ -883,6 +888,7 @@ public async Task TransactionDomainService_ProcessSaleTransaction_SaveFailed_Res Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any()).ReturnsAsync(Result.Success(new TransactionValidationResult(TransactionResponseCode.Success, "SUCCESS"))); this.OperatorProxy.ProcessSaleMessage(Arg.Any(), Arg.Any(), diff --git a/TransactionProcessor.BusinessLogic.Tests/Services/TransactionValidationServiceTests.cs b/TransactionProcessor.BusinessLogic.Tests/Services/TransactionValidationServiceTests.cs index b4edad6a..46efed4f 100644 --- a/TransactionProcessor.BusinessLogic.Tests/Services/TransactionValidationServiceTests.cs +++ b/TransactionProcessor.BusinessLogic.Tests/Services/TransactionValidationServiceTests.cs @@ -1,6 +1,7 @@ using Shared.EventStore.Aggregate; using SimpleResults; using TransactionProcessor.Aggregates; +using TransactionProcessor.Models; using TransactionProcessor.Models.Merchant; namespace TransactionProcessor.BusinessLogic.Tests.Services; @@ -261,6 +262,7 @@ public async Task TransactionValidationService_ValidateSaleTransaction_DeviceNot TestData.DeviceIdentifier1, TestData.OperatorId, TestData.TransactionAmount, + TransactionSource.OnlineSale, TestContext.Current.CancellationToken); result.IsFailed.ShouldBeTrue(); @@ -282,6 +284,7 @@ public async Task TransactionValidationService_ValidateSaleTransaction_EstateFou TestData.DeviceIdentifier, TestData.OperatorId, TestData.TransactionAmount, + TransactionSource.OnlineSale, TestContext.Current.CancellationToken); result.IsFailed.ShouldBeTrue(); @@ -303,6 +306,7 @@ public async Task TransactionValidationService_ValidateSaleTransaction_EstateFou TestData.DeviceIdentifier, TestData.OperatorId, TestData.TransactionAmount, + TransactionSource.OnlineSale, TestContext.Current.CancellationToken); result.IsFailed.ShouldBeTrue(); @@ -324,6 +328,7 @@ public async Task TransactionValidationService_ValidateSaleTransaction_EstateFou TestData.DeviceIdentifier, TestData.OperatorId, TestData.TransactionAmount, + TransactionSource.OnlineSale, TestContext.Current.CancellationToken); result.IsFailed.ShouldBeTrue(); @@ -345,6 +350,7 @@ public async Task TransactionValidationService_ValidateSaleTransaction_EstateFou TestData.DeviceIdentifier, TestData.OperatorId2, TestData.TransactionAmount, + TransactionSource.OnlineSale, TestContext.Current.CancellationToken); result.IsFailed.ShouldBeTrue(); @@ -366,6 +372,7 @@ public async Task TransactionValidationService_ValidateSaleTransaction_EstateNot TestData.DeviceIdentifier, TestData.OperatorId, TestData.TransactionAmount, + TransactionSource.OnlineSale, TestContext.Current.CancellationToken); result.IsFailed.ShouldBeTrue(); @@ -387,6 +394,7 @@ public async Task TransactionValidationService_ValidateSaleTransaction_GetEstate TestData.DeviceIdentifier, TestData.OperatorId, TestData.TransactionAmount, + TransactionSource.OnlineSale, TestContext.Current.CancellationToken); result.IsFailed.ShouldBeTrue(); @@ -413,6 +421,7 @@ public async Task TransactionValidationService_ValidateSaleTransaction_InvalidCo TestData.DeviceIdentifier, TestData.OperatorId, TestData.TransactionAmount, + TransactionSource.OnlineSale, TestContext.Current.CancellationToken); result.IsFailed.ShouldBeTrue(); @@ -438,6 +447,7 @@ public async Task TransactionValidationService_ValidateSaleTransaction_InvalidPr TestData.DeviceIdentifier, TestData.OperatorId, TestData.TransactionAmount, + TransactionSource.OnlineSale, TestContext.Current.CancellationToken); result.IsFailed.ShouldBeTrue(); @@ -462,6 +472,7 @@ public async Task TransactionValidationService_ValidateSaleTransaction_InvalidTr TestData.DeviceIdentifier, TestData.OperatorId, transactionAmount, + TransactionSource.OnlineSale, TestContext.Current.CancellationToken); result.IsFailed.ShouldBeTrue(); @@ -485,6 +496,7 @@ public async Task TransactionValidationService_ValidateSaleTransaction_MerchantD TestData.DeviceIdentifier1, TestData.OperatorId, TestData.TransactionAmount, + TransactionSource.OnlineSale, TestContext.Current.CancellationToken); result.IsFailed.ShouldBeTrue(); @@ -510,6 +522,7 @@ public async Task TransactionValidationService_ValidateSaleTransaction_MerchantD TestData.DeviceIdentifier, TestData.OperatorId, TestData.TransactionAmount, + TransactionSource.OnlineSale, TestContext.Current.CancellationToken); result.IsFailed.ShouldBeTrue(); @@ -537,6 +550,7 @@ public async Task TransactionValidationService_ValidateSaleTransaction_MerchantH TestData.DeviceIdentifier, TestData.OperatorId, TestData.TransactionAmount, + TransactionSource.OnlineSale, TestContext.Current.CancellationToken); result.IsFailed.ShouldBeTrue(); @@ -561,6 +575,7 @@ public async Task TransactionValidationService_ValidateSaleTransaction_MerchantN TestData.DeviceIdentifier, TestData.OperatorId, TestData.TransactionAmount, + TransactionSource.OnlineSale, TestContext.Current.CancellationToken); result.IsFailed.ShouldBeTrue(); @@ -585,6 +600,7 @@ public async Task TransactionValidationService_ValidateSaleTransaction_MerchantN TestData.DeviceIdentifier, TestData.OperatorId, TestData.TransactionAmount, + TransactionSource.OnlineSale, TestContext.Current.CancellationToken); result.IsFailed.ShouldBeTrue(); @@ -608,6 +624,7 @@ public async Task TransactionValidationService_ValidateSaleTransaction_FailedGet TestData.DeviceIdentifier, TestData.OperatorId, TestData.TransactionAmount, + TransactionSource.OnlineSale, TestContext.Current.CancellationToken); result.IsFailed.ShouldBeTrue(); @@ -636,6 +653,7 @@ public async Task TransactionValidationService_ValidateSaleTransaction_MerchantN TestData.DeviceIdentifier, TestData.OperatorId, TestData.TransactionAmount, + TransactionSource.OnlineSale, TestContext.Current.CancellationToken); result.IsFailed.ShouldBeTrue(); @@ -663,6 +681,7 @@ public async Task TransactionValidationService_ValidateSaleTransaction_FailedGet TestData.DeviceIdentifier, TestData.OperatorId, TestData.TransactionAmount, + TransactionSource.OnlineSale, TestContext.Current.CancellationToken); result.IsFailed.ShouldBeTrue(); @@ -687,6 +706,7 @@ public async Task TransactionValidationService_ValidateSaleTransaction_MerchantO TestData.DeviceIdentifier, TestData.OperatorId, TestData.TransactionAmount, + TransactionSource.OnlineSale, TestContext.Current.CancellationToken); result.IsFailed.ShouldBeTrue(); @@ -710,6 +730,7 @@ public async Task TransactionValidationService_ValidateSaleTransaction_MerchantO TestData.DeviceIdentifier, TestData.OperatorId, TestData.TransactionAmount, + TransactionSource.OnlineSale, TestContext.Current.CancellationToken); result.IsFailed.ShouldBeTrue(); @@ -733,6 +754,7 @@ public async Task TransactionValidationService_ValidateSaleTransaction_OperatorN TestData.DeviceIdentifier, TestData.OperatorId2, TestData.TransactionAmount, + TransactionSource.OnlineSale, TestContext.Current.CancellationToken); result.IsFailed.ShouldBeTrue(); result.Data.ResponseCode.ShouldBe(TransactionResponseCode.OperatorNotValidForMerchant); @@ -758,6 +780,7 @@ public async Task TransactionValidationService_ValidateSaleTransaction_ProductId TestData.DeviceIdentifier, TestData.OperatorId, TestData.TransactionAmount, + TransactionSource.OnlineSale, TestContext.Current.CancellationToken); result.IsFailed.ShouldBeTrue(); @@ -783,11 +806,39 @@ public async Task TransactionValidationService_ValidateSaleTransaction_Successfu TestData.DeviceIdentifier, TestData.OperatorId, TestData.TransactionAmount, + TransactionSource.OnlineSale, TestContext.Current.CancellationToken); result.IsSuccess.ShouldBeTrue(); result.Data.ResponseCode.ShouldBe(TransactionResponseCode.Success); } + [Theory] + [InlineData(TransactionSource.NotSet)] + [InlineData(TransactionSource.FileImport)] + public async Task TransactionValidationService_ValidateSaleTransaction_NonOnlineSaleWithMissingDevice_ResponseIsSuccess(TransactionSource transactionSource) + { + this.AggregateService.Get(Arg.Any(), Arg.Any()) + .ReturnsAsync(Result.Success(TestData.Aggregates.EstateAggregateWithOperator())); + this.AggregateService.Get(Arg.Any(), Arg.Any()) + .ReturnsAsync(TestData.Aggregates.MerchantAggregateWithEverything(SettlementSchedule.Immediate)); + + this.EventStoreContext.GetPartitionStateFromProjection(Arg.Any(), Arg.Any(), Arg.Any()) + .ReturnsAsync(StringSerialiser.Serialise(TestData.MerchantBalanceProjectionState)); + + var result = await this.TransactionValidationService.ValidateSaleTransaction(TestData.EstateId, + TestData.MerchantId, + TestData.ContractId, + TestData.ProductId, + null, + TestData.OperatorId, + TestData.TransactionAmount, + transactionSource, + TestContext.Current.CancellationToken); + + result.IsSuccess.ShouldBeTrue(); + result.Data.ResponseCode.ShouldBe(TransactionResponseCode.Success); + } + [Fact] public async Task TransactionValidationService_ValidateSaleTransaction_FailedGettingMerchantBalance_ResponseIsInvalidMerchantId() { @@ -808,6 +859,7 @@ public async Task TransactionValidationService_ValidateSaleTransaction_FailedGet TestData.DeviceIdentifier, TestData.OperatorId, TestData.TransactionAmount, + TransactionSource.OnlineSale, TestContext.Current.CancellationToken); result.IsFailed.ShouldBeTrue(); diff --git a/TransactionProcessor.BusinessLogic/Services/TransactionDomainService.cs b/TransactionProcessor.BusinessLogic/Services/TransactionDomainService.cs index 856cc40b..731caebc 100644 --- a/TransactionProcessor.BusinessLogic/Services/TransactionDomainService.cs +++ b/TransactionProcessor.BusinessLogic/Services/TransactionDomainService.cs @@ -101,7 +101,7 @@ public async Task> ProcessLogonTransacti // Generate a transaction reference String transactionReference = TransactionHelpers.GenerateTransactionReference(); - Result stateResult = transactionAggregate.StartTransaction(command.TransactionDateTime, command.TransactionNumber, transactionType, transactionReference, new TransactionStartContext { EstateId = command.EstateId, MerchantId = command.MerchantId, DeviceIdentifier = command.DeviceIdentifier }, null); // Logon transaction has no amount + Result stateResult = transactionAggregate.StartTransaction(command.TransactionDateTime, command.TransactionNumber, transactionType, transactionReference, new TransactionStartContext { EstateId = command.EstateId, MerchantId = command.MerchantId, DeviceIdentifier = command.DeviceIdentifier, TransactionSource = TransactionSource.NotSet }, null); // Logon transaction has no amount if (stateResult.IsFailed) return ResultHelpers.CreateFailure(stateResult); @@ -614,8 +614,9 @@ private async Task> ValidateTransaction(Tran CancellationToken cancellationToken) { Decimal? amount = command.AdditionalTransactionMetadata.ExtractFieldFromMetadata("Amount"); Logger.LogInformation($"Validating Sale Transaction for Merchant {command.MerchantId}, Product {command.ProductId}"); + TransactionSource transactionSourceValue = (TransactionSource)command.TransactionSource; - Result result = await TransactionValidationService.ValidateSaleTransaction(command.EstateId, command.MerchantId, command.ContractId, command.ProductId, command.DeviceIdentifier, command.OperatorId, amount, cancellationToken); + Result result = await TransactionValidationService.ValidateSaleTransaction(command.EstateId, command.MerchantId, command.ContractId, command.ProductId, command.DeviceIdentifier, command.OperatorId, amount, transactionSourceValue, cancellationToken); Logger.LogInformation($"Validation completed with ResponseCode {result.Data?.ResponseCode}, Message {result.Data?.ResponseMessage}"); @@ -654,7 +655,8 @@ private Result StartTransaction(TransactionAggregate transactionAggregate, String transactionReference = TransactionHelpers.GenerateTransactionReference(); Decimal? amount = command.AdditionalTransactionMetadata.ExtractFieldFromMetadata("Amount"); - Result result = transactionAggregate.StartTransaction(command.TransactionDateTime, command.TransactionNumber, transactionType, transactionReference, new TransactionStartContext { EstateId = command.EstateId, MerchantId = command.MerchantId, DeviceIdentifier = command.DeviceIdentifier }, amount); + Result result = transactionAggregate.StartTransaction(command.TransactionDateTime, command.TransactionNumber, transactionType, transactionReference, new TransactionStartContext { EstateId = command.EstateId, MerchantId = command.MerchantId, DeviceIdentifier = command.DeviceIdentifier, + TransactionSource = transactionSourceValue}, amount); if (result.IsFailed) return result; diff --git a/TransactionProcessor.BusinessLogic/Services/TransactionValidationService.cs b/TransactionProcessor.BusinessLogic/Services/TransactionValidationService.cs index 51f4368b..3221bd68 100644 --- a/TransactionProcessor.BusinessLogic/Services/TransactionValidationService.cs +++ b/TransactionProcessor.BusinessLogic/Services/TransactionValidationService.cs @@ -15,6 +15,7 @@ namespace TransactionProcessor.BusinessLogic.Services; using System.Linq; using System.Threading; using System.Threading.Tasks; +using TransactionProcessor.Models; public interface ITransactionValidationService { @@ -36,6 +37,7 @@ Task> ValidateSaleTransaction(Guid estateId, String deviceIdentifier, Guid operatorId, Decimal? transactionAmount, + TransactionSource transactionSource, CancellationToken cancellationToken); #endregion @@ -123,6 +125,7 @@ public async Task> ValidateSaleTransaction(G String deviceIdentifier, Guid operatorId, Decimal? transactionAmount, + TransactionSource transactionSource, CancellationToken cancellationToken) { // Validate Estate @@ -141,9 +144,12 @@ public async Task> ValidateSaleTransaction(G Models.Merchant.Merchant merchant = merchantValidationResult.Data.additionalData.GetMerchant(); - // Validate Device - Result deviceValidationResult = ValidateDevice(merchant, deviceIdentifier); - if (deviceValidationResult.IsFailed) return deviceValidationResult; + if (transactionSource == TransactionSource.OnlineSale) + { + // Validate Device + Result deviceValidationResult = ValidateDevice(merchant, deviceIdentifier); + if (deviceValidationResult.IsFailed) return deviceValidationResult; + } // Validate Operator for Merchant Result merchantOperatorValidationResult = ValidateMerchantOperator(merchant, operatorId);