From bec76d88f388f4b2d89d702246bfb9cec79e42b1 Mon Sep 17 00:00:00 2001 From: StuartFerguson Date: Wed, 16 Jul 2025 17:25:10 +0100 Subject: [PATCH 1/2] Build in retries to Process Settlement at the domain service --- .../Services/SettlementDomainServiceTests.cs | 100 ++++++++++++++++++ .../Services/SettlementDomainService.cs | 7 ++ 2 files changed, 107 insertions(+) diff --git a/TransactionProcessor.BusinessLogic.Tests/Services/SettlementDomainServiceTests.cs b/TransactionProcessor.BusinessLogic.Tests/Services/SettlementDomainServiceTests.cs index a83c7bfa..f208435e 100644 --- a/TransactionProcessor.BusinessLogic.Tests/Services/SettlementDomainServiceTests.cs +++ b/TransactionProcessor.BusinessLogic.Tests/Services/SettlementDomainServiceTests.cs @@ -82,6 +82,106 @@ public async Task SettlementDomainService_ProcessSettlement_SettlementIsProcesse result.Data.ShouldNotBe(Guid.Empty); } + [Fact] + public async Task SettlementDomainService_ProcessSettlement_RunOutOfRetries_SettlementIsNotProcessed() + { + this.AggregateService.Setup(s => s.GetLatest(It.IsAny(), It.IsAny())) + .ReturnsAsync(Result.Success(TestData.GetSettlementAggregateWithPendingMerchantFees(10))); + this.AggregateService.SetupSequence(s => s.GetLatest(It.IsAny(), It.IsAny())) + .ReturnsAsync(Result.Success(TestData.GetCompletedAuthorisedSaleTransactionAggregateWithPendingFee(TestData.FeeIds.GetValueOrDefault(0)))) + .ReturnsAsync(Result.Success(TestData.GetCompletedAuthorisedSaleTransactionAggregateWithPendingFee(TestData.FeeIds.GetValueOrDefault(1)))) + .ReturnsAsync(Result.Success(TestData.GetCompletedAuthorisedSaleTransactionAggregateWithPendingFee(TestData.FeeIds.GetValueOrDefault(2)))) + .ReturnsAsync(Result.Success(TestData.GetCompletedAuthorisedSaleTransactionAggregateWithPendingFee(TestData.FeeIds.GetValueOrDefault(3)))) + .ReturnsAsync(Result.Success(TestData.GetCompletedAuthorisedSaleTransactionAggregateWithPendingFee(TestData.FeeIds.GetValueOrDefault(4)))) + .ReturnsAsync(Result.Success(TestData.GetCompletedAuthorisedSaleTransactionAggregateWithPendingFee(TestData.FeeIds.GetValueOrDefault(5)))) + .ReturnsAsync(Result.Success(TestData.GetCompletedAuthorisedSaleTransactionAggregateWithPendingFee(TestData.FeeIds.GetValueOrDefault(6)))) + .ReturnsAsync(Result.Success(TestData.GetCompletedAuthorisedSaleTransactionAggregateWithPendingFee(TestData.FeeIds.GetValueOrDefault(7)))) + .ReturnsAsync(Result.Success(TestData.GetCompletedAuthorisedSaleTransactionAggregateWithPendingFee(TestData.FeeIds.GetValueOrDefault(8)))) + .ReturnsAsync(Result.Success(TestData.GetCompletedAuthorisedSaleTransactionAggregateWithPendingFee(TestData.FeeIds.GetValueOrDefault(9)))); + this.AggregateService.SetupSequence(s => s.Save(It.IsAny(), It.IsAny())) + .ReturnsAsync(Result.Success()) + .ReturnsAsync(Result.Failure(new List() { "WrongExpectedVersion" })) + .ReturnsAsync(Result.Success()) + .ReturnsAsync(Result.Failure(new List() { "WrongExpectedVersion" })) + .ReturnsAsync(Result.Success()) + .ReturnsAsync(Result.Failure(new List() { "WrongExpectedVersion" })) + .ReturnsAsync(Result.Success()) + .ReturnsAsync(Result.Failure(new List() { "WrongExpectedVersion" })) + .ReturnsAsync(Result.Success()) + .ReturnsAsync(Result.Failure(new List() { "WrongExpectedVersion" })) + .ReturnsAsync(Result.Failure(new List() { "WrongExpectedVersion" })); + this.AggregateService.SetupSequence(s => s.Save(It.IsAny(), It.IsAny())) + .ReturnsAsync(Result.Success()) + .ReturnsAsync(Result.Success()) + .ReturnsAsync(Result.Success()) + .ReturnsAsync(Result.Success()) + .ReturnsAsync(Result.Success()) + .ReturnsAsync(Result.Success()) + .ReturnsAsync(Result.Success()) + .ReturnsAsync(Result.Success()) + .ReturnsAsync(Result.Success()) + .ReturnsAsync(Result.Success()); + + this.AggregateService.Setup(e => e.Get(It.IsAny(), It.IsAny())) + .ReturnsAsync(TestData.Aggregates.CreatedMerchantAggregate()); + + SettlementCommands.ProcessSettlementCommand command = + new(TestData.SettlementDate, TestData.MerchantId, + TestData.EstateId); + + Result result = await settlementDomainService.ProcessSettlement(command, CancellationToken.None); + + result.IsSuccess.ShouldBeFalse(); + this.AggregateService.Verify(s => s.Save(It.IsAny(), It.IsAny()), Times.Exactly(11)); + } + + [Fact] + public async Task SettlementDomainService_ProcessSettlement_RetryOnWrongExpected_SettlementIsProcessed() + { + this.AggregateService.Setup(s => s.GetLatest(It.IsAny(), It.IsAny())) + .ReturnsAsync(Result.Success(TestData.GetSettlementAggregateWithPendingMerchantFees(10))); + this.AggregateService.SetupSequence(s => s.GetLatest(It.IsAny(), It.IsAny())) + .ReturnsAsync(Result.Success(TestData.GetCompletedAuthorisedSaleTransactionAggregateWithPendingFee(TestData.FeeIds.GetValueOrDefault(0)))) + .ReturnsAsync(Result.Success(TestData.GetCompletedAuthorisedSaleTransactionAggregateWithPendingFee(TestData.FeeIds.GetValueOrDefault(1)))) + .ReturnsAsync(Result.Success(TestData.GetCompletedAuthorisedSaleTransactionAggregateWithPendingFee(TestData.FeeIds.GetValueOrDefault(2)))) + .ReturnsAsync(Result.Success(TestData.GetCompletedAuthorisedSaleTransactionAggregateWithPendingFee(TestData.FeeIds.GetValueOrDefault(3)))) + .ReturnsAsync(Result.Success(TestData.GetCompletedAuthorisedSaleTransactionAggregateWithPendingFee(TestData.FeeIds.GetValueOrDefault(4)))) + .ReturnsAsync(Result.Success(TestData.GetCompletedAuthorisedSaleTransactionAggregateWithPendingFee(TestData.FeeIds.GetValueOrDefault(5)))) + .ReturnsAsync(Result.Success(TestData.GetCompletedAuthorisedSaleTransactionAggregateWithPendingFee(TestData.FeeIds.GetValueOrDefault(6)))) + .ReturnsAsync(Result.Success(TestData.GetCompletedAuthorisedSaleTransactionAggregateWithPendingFee(TestData.FeeIds.GetValueOrDefault(7)))) + .ReturnsAsync(Result.Success(TestData.GetCompletedAuthorisedSaleTransactionAggregateWithPendingFee(TestData.FeeIds.GetValueOrDefault(8)))) + .ReturnsAsync(Result.Success(TestData.GetCompletedAuthorisedSaleTransactionAggregateWithPendingFee(TestData.FeeIds.GetValueOrDefault(9)))); + this.AggregateService.SetupSequence(s => s.Save(It.IsAny(), It.IsAny())) + .ReturnsAsync(Result.Success()) + .ReturnsAsync(Result.Failure(new List(){ "WrongExpectedVersion" })) + .ReturnsAsync(Result.Success()) + .ReturnsAsync(Result.Success()); + this.AggregateService.SetupSequence(s => s.Save(It.IsAny(), It.IsAny())) + .ReturnsAsync(Result.Success()) + .ReturnsAsync(Result.Success()) + .ReturnsAsync(Result.Success()) + .ReturnsAsync(Result.Success()) + .ReturnsAsync(Result.Success()) + .ReturnsAsync(Result.Success()) + .ReturnsAsync(Result.Success()) + .ReturnsAsync(Result.Success()) + .ReturnsAsync(Result.Success()) + .ReturnsAsync(Result.Success()); + + this.AggregateService.Setup(e => e.Get(It.IsAny(), It.IsAny())) + .ReturnsAsync(TestData.Aggregates.CreatedMerchantAggregate()); + + SettlementCommands.ProcessSettlementCommand command = + new(TestData.SettlementDate, TestData.MerchantId, + TestData.EstateId); + + Result result = await settlementDomainService.ProcessSettlement(command, CancellationToken.None); + + result.IsSuccess.ShouldBeTrue(); + result.Data.ShouldNotBe(Guid.Empty); + this.AggregateService.Verify(s => s.Save(It.IsAny(), It.IsAny()), Times.Exactly(4)); + } + [Fact] public async Task SettlementDomainService_ProcessSettlement_MerchantWithImmediateSettlement_SettlementIsProcessed() { diff --git a/TransactionProcessor.BusinessLogic/Services/SettlementDomainService.cs b/TransactionProcessor.BusinessLogic/Services/SettlementDomainService.cs index ffecebaa..7146fa8f 100644 --- a/TransactionProcessor.BusinessLogic/Services/SettlementDomainService.cs +++ b/TransactionProcessor.BusinessLogic/Services/SettlementDomainService.cs @@ -100,6 +100,11 @@ private async Task ApplyTransactionUpdates(Func> ProcessSettlement(SettlementCommands.ProcessSettlementCommand command, CancellationToken cancellationToken) { + + IAsyncPolicy> retryPolicy = PolicyFactory.CreatePolicy(policyTag: "SettlementDomainService - ProcessSettlement"); + + return await PolicyFactory.ExecuteWithPolicyAsync(async () => { + Guid settlementAggregateId = Helpers.CalculateSettlementAggregateId(command.SettlementDate, command.MerchantId, command.EstateId); List<(Guid transactionId, Guid merchantId, CalculatedFee calculatedFee)> feesToBeSettled = new(); @@ -161,6 +166,8 @@ public async Task> ProcessSettlement(SettlementCommands.ProcessSett } return Result.Success(settlementAggregateId); + }, retryPolicy, "SettlementDomainService - ProcessSettlement"); + } public async Task AddMerchantFeePendingSettlement(SettlementCommands.AddMerchantFeePendingSettlementCommand command, From 7904ac570ab8db14dc0b5c2bdb3dc9a07d732201 Mon Sep 17 00:00:00 2001 From: StuartFerguson Date: Wed, 16 Jul 2025 17:31:02 +0100 Subject: [PATCH 2/2] code review changes --- .../Services/SettlementDomainServiceTests.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/TransactionProcessor.BusinessLogic.Tests/Services/SettlementDomainServiceTests.cs b/TransactionProcessor.BusinessLogic.Tests/Services/SettlementDomainServiceTests.cs index f208435e..92b20273 100644 --- a/TransactionProcessor.BusinessLogic.Tests/Services/SettlementDomainServiceTests.cs +++ b/TransactionProcessor.BusinessLogic.Tests/Services/SettlementDomainServiceTests.cs @@ -100,16 +100,16 @@ public async Task SettlementDomainService_ProcessSettlement_RunOutOfRetries_Sett .ReturnsAsync(Result.Success(TestData.GetCompletedAuthorisedSaleTransactionAggregateWithPendingFee(TestData.FeeIds.GetValueOrDefault(9)))); this.AggregateService.SetupSequence(s => s.Save(It.IsAny(), It.IsAny())) .ReturnsAsync(Result.Success()) - .ReturnsAsync(Result.Failure(new List() { "WrongExpectedVersion" })) + .ReturnsAsync(Result.Failure(new List { "WrongExpectedVersion" })) .ReturnsAsync(Result.Success()) - .ReturnsAsync(Result.Failure(new List() { "WrongExpectedVersion" })) + .ReturnsAsync(Result.Failure(new List { "WrongExpectedVersion" })) .ReturnsAsync(Result.Success()) - .ReturnsAsync(Result.Failure(new List() { "WrongExpectedVersion" })) + .ReturnsAsync(Result.Failure(new List { "WrongExpectedVersion" })) .ReturnsAsync(Result.Success()) - .ReturnsAsync(Result.Failure(new List() { "WrongExpectedVersion" })) + .ReturnsAsync(Result.Failure(new List { "WrongExpectedVersion" })) .ReturnsAsync(Result.Success()) - .ReturnsAsync(Result.Failure(new List() { "WrongExpectedVersion" })) - .ReturnsAsync(Result.Failure(new List() { "WrongExpectedVersion" })); + .ReturnsAsync(Result.Failure(new List { "WrongExpectedVersion" })) + .ReturnsAsync(Result.Failure(new List { "WrongExpectedVersion" })); this.AggregateService.SetupSequence(s => s.Save(It.IsAny(), It.IsAny())) .ReturnsAsync(Result.Success()) .ReturnsAsync(Result.Success()) @@ -153,7 +153,7 @@ public async Task SettlementDomainService_ProcessSettlement_RetryOnWrongExpected .ReturnsAsync(Result.Success(TestData.GetCompletedAuthorisedSaleTransactionAggregateWithPendingFee(TestData.FeeIds.GetValueOrDefault(9)))); this.AggregateService.SetupSequence(s => s.Save(It.IsAny(), It.IsAny())) .ReturnsAsync(Result.Success()) - .ReturnsAsync(Result.Failure(new List(){ "WrongExpectedVersion" })) + .ReturnsAsync(Result.Failure(new List{ "WrongExpectedVersion" })) .ReturnsAsync(Result.Success()) .ReturnsAsync(Result.Success()); this.AggregateService.SetupSequence(s => s.Save(It.IsAny(), It.IsAny()))