diff --git a/TransactionProcessor.BusinessLogic.Tests/DomainEventHandlers/TransactionDomainEventHandlerTests.cs b/TransactionProcessor.BusinessLogic.Tests/DomainEventHandlers/TransactionDomainEventHandlerTests.cs index 23f700a1..ea4dade5 100644 --- a/TransactionProcessor.BusinessLogic.Tests/DomainEventHandlers/TransactionDomainEventHandlerTests.cs +++ b/TransactionProcessor.BusinessLogic.Tests/DomainEventHandlers/TransactionDomainEventHandlerTests.cs @@ -76,7 +76,7 @@ public class TransactionDomainEventHandlerTests private Mock TransactionAggregateManager; - private Mock> PendingSettlementAggregateRepository; + private Mock> SettlementAggregateRepository; private Mock FeeCalculationManager; @@ -90,7 +90,7 @@ public class TransactionDomainEventHandlerTests public TransactionDomainEventHandlerTests() { this.TransactionAggregateManager = new Mock(); - this.PendingSettlementAggregateRepository = new Mock>(); + this.SettlementAggregateRepository = new Mock>(); this.FeeCalculationManager = new Mock(); this.EstateClient = new Mock(); this.SecurityServiceClient = new Mock(); @@ -110,7 +110,7 @@ public async Task TransactionDomainEventHandler_Handle_TransactionHasBeenComplet this.FeeCalculationManager.Setup(f => f.CalculateFees(It.IsAny>(), It.IsAny())).Returns(new List { - TestData.CalculatedFeeMerchantFee, + TestData.CalculatedFeeMerchantFee(), TestData.CalculatedFeeServiceProviderFee }); @@ -135,14 +135,14 @@ public async Task TransactionDomainEventHandler_Handle_TransactionHasBeenComplet this.SecurityServiceClient.Object, this.TransactionReceiptBuilder.Object, this.MessagingServiceClient.Object, - this.PendingSettlementAggregateRepository.Object); + this.SettlementAggregateRepository.Object); await transactionDomainEventHandler.Handle(TestData.TransactionHasBeenCompletedEvent, CancellationToken.None); this.TransactionAggregateManager.Verify(t => t.AddFee(It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny()),Times.Exactly(2)); + It.IsAny()),Times.Once); } [Fact] @@ -150,13 +150,13 @@ public async Task TransactionDomainEventHandler_Handle_TransactionHasBeenComplet { this.TransactionAggregateManager.Setup(t => t.GetAggregate(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(TestData.GetCompletedAuthorisedSaleTransactionAggregate); - PendingSettlementAggregate pendingSettlementAggregate = new PendingSettlementAggregate(); - this.PendingSettlementAggregateRepository.Setup(p => p.GetLatestVersion(It.IsAny(), It.IsAny())) + SettlementAggregate pendingSettlementAggregate = new SettlementAggregate(); + this.SettlementAggregateRepository.Setup(p => p.GetLatestVersion(It.IsAny(), It.IsAny())) .ReturnsAsync(pendingSettlementAggregate); this.FeeCalculationManager.Setup(f => f.CalculateFees(It.IsAny>(), It.IsAny())).Returns(new List { - TestData.CalculatedFeeMerchantFee, + TestData.CalculatedFeeMerchantFee(), TestData.CalculatedFeeServiceProviderFee }); var merchant = new MerchantResponse @@ -182,7 +182,7 @@ public async Task TransactionDomainEventHandler_Handle_TransactionHasBeenComplet this.SecurityServiceClient.Object, this.TransactionReceiptBuilder.Object, this.MessagingServiceClient.Object, - this.PendingSettlementAggregateRepository.Object); + this.SettlementAggregateRepository.Object); await transactionDomainEventHandler.Handle(TestData.TransactionHasBeenCompletedEvent, CancellationToken.None); @@ -190,8 +190,8 @@ public async Task TransactionDomainEventHandler_Handle_TransactionHasBeenComplet It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); - this.PendingSettlementAggregateRepository.Verify(p => p.GetLatestVersion(It.IsAny(), It.IsAny()),Times.Once); - this.PendingSettlementAggregateRepository.Verify(p => p.SaveChanges(It.IsAny(), It.IsAny()), Times.Once); + this.SettlementAggregateRepository.Verify(p => p.GetLatestVersion(It.IsAny(), It.IsAny()),Times.Once); + this.SettlementAggregateRepository.Verify(p => p.SaveChanges(It.IsAny(), It.IsAny()), Times.Once); pendingSettlementAggregate.GetNumberOfFeesPendingSettlement().ShouldBe(1); pendingSettlementAggregate.GetNumberOfFeesSettled().ShouldBe(0); } @@ -201,13 +201,13 @@ public async Task TransactionDomainEventHandler_Handle_TransactionHasBeenComplet { this.TransactionAggregateManager.Setup(t => t.GetAggregate(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(TestData.GetCompletedAuthorisedSaleTransactionAggregate); - PendingSettlementAggregate pendingSettlementAggregate = new PendingSettlementAggregate(); - this.PendingSettlementAggregateRepository.Setup(p => p.GetLatestVersion(It.IsAny(), It.IsAny())) + SettlementAggregate pendingSettlementAggregate = new SettlementAggregate(); + this.SettlementAggregateRepository.Setup(p => p.GetLatestVersion(It.IsAny(), It.IsAny())) .ReturnsAsync(pendingSettlementAggregate); this.FeeCalculationManager.Setup(f => f.CalculateFees(It.IsAny>(), It.IsAny())).Returns(new List { - TestData.CalculatedFeeMerchantFee, + TestData.CalculatedFeeMerchantFee(), TestData.CalculatedFeeServiceProviderFee }); var merchant = new MerchantResponse @@ -233,7 +233,7 @@ public async Task TransactionDomainEventHandler_Handle_TransactionHasBeenComplet this.SecurityServiceClient.Object, this.TransactionReceiptBuilder.Object, this.MessagingServiceClient.Object, - this.PendingSettlementAggregateRepository.Object); + this.SettlementAggregateRepository.Object); await transactionDomainEventHandler.Handle(TestData.TransactionHasBeenCompletedEvent, CancellationToken.None); @@ -241,8 +241,8 @@ public async Task TransactionDomainEventHandler_Handle_TransactionHasBeenComplet It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); - this.PendingSettlementAggregateRepository.Verify(p => p.GetLatestVersion(It.IsAny(), It.IsAny()), Times.Once); - this.PendingSettlementAggregateRepository.Verify(p => p.SaveChanges(It.IsAny(), It.IsAny()), Times.Once); + this.SettlementAggregateRepository.Verify(p => p.GetLatestVersion(It.IsAny(), It.IsAny()), Times.Once); + this.SettlementAggregateRepository.Verify(p => p.SaveChanges(It.IsAny(), It.IsAny()), Times.Once); pendingSettlementAggregate.GetNumberOfFeesPendingSettlement().ShouldBe(1); pendingSettlementAggregate.GetNumberOfFeesSettled().ShouldBe(0); } @@ -259,7 +259,7 @@ public async Task TransactionDomainEventHandler_Handle_TransactionHasBeenComplet this.SecurityServiceClient.Object, this.TransactionReceiptBuilder.Object, this.MessagingServiceClient.Object, - this.PendingSettlementAggregateRepository.Object); + this.SettlementAggregateRepository.Object); await transactionDomainEventHandler.Handle(TestData.TransactionHasBeenCompletedEvent, CancellationToken.None); } @@ -276,7 +276,7 @@ public async Task TransactionDomainEventHandler_Handle_TransactionHasBeenComplet this.SecurityServiceClient.Object, this.TransactionReceiptBuilder.Object, this.MessagingServiceClient.Object, - this.PendingSettlementAggregateRepository.Object); + this.SettlementAggregateRepository.Object); await transactionDomainEventHandler.Handle(TestData.TransactionHasBeenCompletedEvent, CancellationToken.None); } @@ -293,7 +293,7 @@ public async Task TransactionDomainEventHandler_Handle_TransactionHasBeenComplet this.SecurityServiceClient.Object, this.TransactionReceiptBuilder.Object, this.MessagingServiceClient.Object, - this.PendingSettlementAggregateRepository.Object); + this.SettlementAggregateRepository.Object); await transactionDomainEventHandler.Handle(TestData.TransactionHasBeenCompletedEvent, CancellationToken.None); } @@ -310,7 +310,7 @@ public async Task TransactionDomainEventHandler_Handle_TransactionHasBeenComplet this.SecurityServiceClient.Object, this.TransactionReceiptBuilder.Object, this.MessagingServiceClient.Object, - this.PendingSettlementAggregateRepository.Object); + this.SettlementAggregateRepository.Object); await transactionDomainEventHandler.Handle(TestData.TransactionHasBeenCompletedEvent, CancellationToken.None); } @@ -329,9 +329,40 @@ public async Task TransactionDomainEventHandler_Handle_CustomerEmailReceiptReque this.SecurityServiceClient.Object, this.TransactionReceiptBuilder.Object, this.MessagingServiceClient.Object, - this.PendingSettlementAggregateRepository.Object); + this.SettlementAggregateRepository.Object); await transactionDomainEventHandler.Handle(TestData.CustomerEmailReceiptRequestedEvent, CancellationToken.None); } + + [Fact] + public async Task TransactionDomainEventHandler_Handle_MerchantFeeAddedToTransactionEvent_EventIsHandled() + { + this.SettlementAggregateRepository.Setup(s => s.GetLatestVersion(It.IsAny(), It.IsAny())) + .ReturnsAsync(TestData.GetSettlementAggregateWithPendingMerchantFees(1)); + + TransactionDomainEventHandler transactionDomainEventHandler = new TransactionDomainEventHandler(this.TransactionAggregateManager.Object, + this.FeeCalculationManager.Object, + this.EstateClient.Object, + this.SecurityServiceClient.Object, + this.TransactionReceiptBuilder.Object, + this.MessagingServiceClient.Object, + this.SettlementAggregateRepository.Object); + + await transactionDomainEventHandler.Handle(TestData.MerchantFeeAddedToTransactionEvent(TestData.TransactionFeeSettlementDueDate), CancellationToken.None); + } + + [Fact] + public async Task TransactionDomainEventHandler_Handle_MerchantFeeAddedToTransactionEvent_EventHasNoSettlementDueDate_EventIsHandled() + { + TransactionDomainEventHandler transactionDomainEventHandler = new TransactionDomainEventHandler(this.TransactionAggregateManager.Object, + this.FeeCalculationManager.Object, + this.EstateClient.Object, + this.SecurityServiceClient.Object, + this.TransactionReceiptBuilder.Object, + this.MessagingServiceClient.Object, + this.SettlementAggregateRepository.Object); + + await transactionDomainEventHandler.Handle(TestData.MerchantFeeAddedToTransactionEvent(DateTime.MinValue), CancellationToken.None); + } } } diff --git a/TransactionProcessor.BusinessLogic.Tests/RequestHandler/TransactionRequestHandlerTests.cs b/TransactionProcessor.BusinessLogic.Tests/RequestHandler/TransactionRequestHandlerTests.cs index 8dda9bff..4e6fe44f 100644 --- a/TransactionProcessor.BusinessLogic.Tests/RequestHandler/TransactionRequestHandlerTests.cs +++ b/TransactionProcessor.BusinessLogic.Tests/RequestHandler/TransactionRequestHandlerTests.cs @@ -60,4 +60,22 @@ public void TransactionRequestHandler_ProcessReconciliationRequest_IsHandled() } } + + public class SettlementRequestHanslerTests + { + [Fact] + public void TransactionRequestHandler_ProcessLogonTransactionRequest_IsHandled() + { + Mock transactionDomainService = new Mock(); + SettlementRequestHandler handler = new SettlementRequestHandler(transactionDomainService.Object); + + ProcessSettlementRequest command = TestData.ProcessSettlementRequest; + + Should.NotThrow(async () => + { + await handler.Handle(command, CancellationToken.None); + }); + + } + } } \ No newline at end of file diff --git a/TransactionProcessor.BusinessLogic.Tests/Requests/RequestTests.cs b/TransactionProcessor.BusinessLogic.Tests/Requests/RequestTests.cs index 1ff555d5..62715730 100644 --- a/TransactionProcessor.BusinessLogic.Tests/Requests/RequestTests.cs +++ b/TransactionProcessor.BusinessLogic.Tests/Requests/RequestTests.cs @@ -74,5 +74,17 @@ public void ProcessReconciliationRequest_CanBeCreated_IsCreated() processReconciliationRequest.TransactionValue.ShouldBe(TestData.ReconciliationTransactionValue); processReconciliationRequest.TransactionId.ShouldBe(TestData.TransactionId); } + + [Fact] + public void ProcessSettlementRequest_CanBeCreated_IsCreated() + { + ProcessSettlementRequest processSettlementRequest = ProcessSettlementRequest.Create(TestData.SettlementDate, + TestData.EstateId); + + processSettlementRequest.ShouldNotBeNull(); + processSettlementRequest.EstateId.ShouldBe(TestData.EstateId); + processSettlementRequest.SettlementDate.ShouldBe(TestData.SettlementDate); + + } } } diff --git a/TransactionProcessor.BusinessLogic.Tests/Services/TransactionAggregateManagerTests.cs b/TransactionProcessor.BusinessLogic.Tests/Services/TransactionAggregateManagerTests.cs index f4a239ca..773795dc 100644 --- a/TransactionProcessor.BusinessLogic.Tests/Services/TransactionAggregateManagerTests.cs +++ b/TransactionProcessor.BusinessLogic.Tests/Services/TransactionAggregateManagerTests.cs @@ -246,7 +246,22 @@ public async Task TransactionAggregateManager_AddFee_FeeAddedToTransaction() await transactionAggregateManager.AddFee(TestData.EstateId, TestData.TransactionId, - TestData.CalculatedMerchantFees.First(), + TestData.CalculatedFeeServiceProviderFee, + CancellationToken.None); + } + + [Fact] + public async Task TransactionAggregateManager_AddSettledFee_FeeAddedToTransaction() + { + Mock> aggregateRepository = new Mock>(); + aggregateRepository.Setup(a => a.GetLatestVersion(It.IsAny(), It.IsAny())).ReturnsAsync(TestData.GetCompletedAuthorisedSaleTransactionAggregate); + TransactionAggregateManager transactionAggregateManager = new TransactionAggregateManager(aggregateRepository.Object); + + await transactionAggregateManager.AddSettledFee(TestData.EstateId, + TestData.TransactionId, + TestData.CalculatedFeeMerchantFee2, + TestData.TransactionFeeSettlementDueDate, + TestData.TransactionFeeSettledDateTime, CancellationToken.None); } } diff --git a/TransactionProcessor.BusinessLogic.Tests/Services/TransactionDomainServiceTests.cs b/TransactionProcessor.BusinessLogic.Tests/Services/TransactionDomainServiceTests.cs index 453026d6..d5e596a6 100644 --- a/TransactionProcessor.BusinessLogic.Tests/Services/TransactionDomainServiceTests.cs +++ b/TransactionProcessor.BusinessLogic.Tests/Services/TransactionDomainServiceTests.cs @@ -17,6 +17,7 @@ namespace TransactionProcessor.BusinessLogic.Tests.Services using ReconciliationAggregate; using SecurityService.Client; using SecurityService.DataTransferObjects.Responses; + using SettlementAggregates; using Shared.DomainDrivenDesign.EventSourcing; using Shared.EventStore.Aggregate; using Shared.EventStore.EventStore; @@ -53,11 +54,14 @@ public async Task TransactionDomainService_ProcessReconciliationTransaction_Tran new Mock>(); reconciliationAggregateRepository.Setup(r => r.GetLatestVersion(It.IsAny(), It.IsAny())) .ReturnsAsync(new ReconciliationAggregate()); + Mock> settlementAggregateRepository = + new Mock>(); TransactionDomainService transactionDomainService = new TransactionDomainService(transactionAggregateManager.Object, estateClient.Object, securityServiceClient.Object, operatorProxyResolver, - reconciliationAggregateRepository.Object); + reconciliationAggregateRepository.Object, + settlementAggregateRepository.Object); ProcessReconciliationTransactionResponse response = await transactionDomainService.ProcessReconciliationTransaction(TestData.TransactionId, TestData.EstateId, @@ -94,10 +98,13 @@ public async Task TransactionDomainService_ProcessReconciliationTransaction_Inco new Mock>(); reconciliationAggregateRepository.Setup(r => r.GetLatestVersion(It.IsAny(), It.IsAny())) .ReturnsAsync(new ReconciliationAggregate()); + Mock> settlementAggregateRepository = + new Mock>(); TransactionDomainService transactionDomainService = new TransactionDomainService(transactionAggregateManager.Object, estateClient.Object, securityServiceClient.Object, - operatorProxyResolver, reconciliationAggregateRepository.Object); + operatorProxyResolver, reconciliationAggregateRepository.Object, + settlementAggregateRepository.Object); ProcessReconciliationTransactionResponse response = await transactionDomainService.ProcessReconciliationTransaction(TestData.TransactionId, TestData.EstateId, @@ -138,10 +145,13 @@ public async Task TransactionDomainService_ProcessReconciliationTransaction_Merc new Mock>(); reconciliationAggregateRepository.Setup(r => r.GetLatestVersion(It.IsAny(), It.IsAny())) .ReturnsAsync(new ReconciliationAggregate()); + Mock> settlementAggregateRepository = + new Mock>(); TransactionDomainService transactionDomainService = new TransactionDomainService(transactionAggregateManager.Object, estateClient.Object, securityServiceClient.Object, - operatorProxyResolver, reconciliationAggregateRepository.Object); + operatorProxyResolver, reconciliationAggregateRepository.Object, + settlementAggregateRepository.Object); ProcessReconciliationTransactionResponse response = await transactionDomainService.ProcessReconciliationTransaction(TestData.TransactionId, TestData.EstateId, @@ -178,10 +188,13 @@ public async Task TransactionDomainService_ProcessReconciliationTransaction_Inva new Mock>(); reconciliationAggregateRepository.Setup(r => r.GetLatestVersion(It.IsAny(), It.IsAny())) .ReturnsAsync(new ReconciliationAggregate()); + Mock> settlementAggregateRepository = + new Mock>(); TransactionDomainService transactionDomainService = new TransactionDomainService(transactionAggregateManager.Object, estateClient.Object, securityServiceClient.Object, - operatorProxyResolver, reconciliationAggregateRepository.Object); + operatorProxyResolver, reconciliationAggregateRepository.Object, + settlementAggregateRepository.Object); ProcessReconciliationTransactionResponse response = await transactionDomainService.ProcessReconciliationTransaction(TestData.TransactionId, TestData.EstateId, @@ -218,9 +231,13 @@ public async Task TransactionDomainService_ProcessReconciliationTransaction_Inva new Mock>(); reconciliationAggregateRepository.Setup(r => r.GetLatestVersion(It.IsAny(), It.IsAny())) .ReturnsAsync(new ReconciliationAggregate()); + Mock> settlementAggregateRepository = + new Mock>(); + TransactionDomainService transactionDomainService = new TransactionDomainService(transactionAggregateManager.Object, estateClient.Object, securityServiceClient.Object, - operatorProxyResolver, reconciliationAggregateRepository.Object); + operatorProxyResolver, reconciliationAggregateRepository.Object, + settlementAggregateRepository.Object); ProcessReconciliationTransactionResponse response = await transactionDomainService.ProcessReconciliationTransaction(TestData.TransactionId, TestData.EstateId, @@ -258,11 +275,14 @@ public async Task TransactionDomainService_ProcessLogonTransaction_TransactionIs Mock> reconciliationAggregateRepository = new Mock>(); + Mock> settlementAggregateRepository = + new Mock>(); TransactionDomainService transactionDomainService = new TransactionDomainService(transactionAggregateManager.Object, estateClient.Object, securityServiceClient.Object, operatorProxyResolver, - reconciliationAggregateRepository.Object); + reconciliationAggregateRepository.Object, + settlementAggregateRepository.Object); ProcessLogonTransactionResponse response = await transactionDomainService.ProcessLogonTransaction(TestData.TransactionId, TestData.EstateId, @@ -298,10 +318,13 @@ public async Task TransactionDomainService_ProcessLogonTransaction_MerchantWithN Mock> reconciliationAggregateRepository = new Mock>(); + Mock> settlementAggregateRepository = + new Mock>(); TransactionDomainService transactionDomainService = new TransactionDomainService(transactionAggregateManager.Object, estateClient.Object, securityServiceClient.Object, - operatorProxyResolver, reconciliationAggregateRepository.Object); + operatorProxyResolver, reconciliationAggregateRepository.Object, + settlementAggregateRepository.Object); ProcessLogonTransactionResponse response = await transactionDomainService.ProcessLogonTransaction(TestData.TransactionId, TestData.EstateId, @@ -338,10 +361,13 @@ public async Task TransactionDomainService_ProcessLogonTransaction_MerchantWithN Mock> reconciliationAggregateRepository = new Mock>(); - + Mock> settlementAggregateRepository = + new Mock>(); + TransactionDomainService transactionDomainService = new TransactionDomainService(transactionAggregateManager.Object, estateClient.Object, securityServiceClient.Object, - operatorProxyResolver, reconciliationAggregateRepository.Object); + operatorProxyResolver, reconciliationAggregateRepository.Object, + settlementAggregateRepository.Object); ProcessLogonTransactionResponse response = await transactionDomainService.ProcessLogonTransaction(TestData.TransactionId, TestData.EstateId, @@ -377,10 +403,13 @@ public async Task TransactionDomainService_ProcessLogonTransaction_IncorrectDevi Mock> reconciliationAggregateRepository = new Mock>(); + Mock> settlementAggregateRepository = + new Mock>(); TransactionDomainService transactionDomainService = new TransactionDomainService(transactionAggregateManager.Object, estateClient.Object, securityServiceClient.Object, - operatorProxyResolver, reconciliationAggregateRepository.Object); + operatorProxyResolver, reconciliationAggregateRepository.Object, + settlementAggregateRepository.Object); ProcessLogonTransactionResponse response = await transactionDomainService.ProcessLogonTransaction(TestData.TransactionId, TestData.EstateId, @@ -416,10 +445,13 @@ public async Task TransactionDomainService_ProcessLogonTransaction_InvalidEstate Mock> reconciliationAggregateRepository = new Mock>(); + Mock> settlementAggregateRepository = + new Mock>(); TransactionDomainService transactionDomainService = new TransactionDomainService(transactionAggregateManager.Object, estateClient.Object, securityServiceClient.Object, - operatorProxyResolver, reconciliationAggregateRepository.Object); + operatorProxyResolver, reconciliationAggregateRepository.Object, + settlementAggregateRepository.Object); ProcessLogonTransactionResponse response = await transactionDomainService.ProcessLogonTransaction(TestData.TransactionId, TestData.EstateId, @@ -455,10 +487,13 @@ public async Task TransactionDomainService_ProcessLogonTransaction_InvalidMercha Mock> reconciliationAggregateRepository = new Mock>(); + Mock> settlementAggregateRepository = + new Mock>(); TransactionDomainService transactionDomainService = new TransactionDomainService(transactionAggregateManager.Object, estateClient.Object, securityServiceClient.Object, - operatorProxyResolver, reconciliationAggregateRepository.Object); + operatorProxyResolver, reconciliationAggregateRepository.Object, + settlementAggregateRepository.Object); ProcessLogonTransactionResponse response = await transactionDomainService.ProcessLogonTransaction(TestData.TransactionId, TestData.EstateId, @@ -499,21 +534,25 @@ public async Task TransactionDomainService_ProcessSaleTransaction_SuccesfulOpera It.IsAny(), It.IsAny>(), It.IsAny())).ReturnsAsync(new OperatorResponse - { - ResponseMessage = TestData.OperatorResponseMessage, - IsSuccessful = true, - AuthorisationCode = TestData.OperatorAuthorisationCode, - TransactionId = TestData.OperatorTransactionId, - ResponseCode = TestData.ResponseCode - }); + { + ResponseMessage = TestData.OperatorResponseMessage, + IsSuccessful = true, + AuthorisationCode = TestData.OperatorAuthorisationCode, + TransactionId = TestData.OperatorTransactionId, + ResponseCode = TestData.ResponseCode + }); Func operatorProxyResolver = (operatorName) => { return operatorProxy.Object; }; Mock> reconciliationAggregateRepository = new Mock>(); + Mock> settlementAggregateRepository = + new Mock>(); + TransactionDomainService transactionDomainService = new TransactionDomainService(transactionAggregateManager.Object, estateClient.Object, securityServiceClient.Object, - operatorProxyResolver, reconciliationAggregateRepository.Object); + operatorProxyResolver, reconciliationAggregateRepository.Object, + settlementAggregateRepository.Object); ProcessSaleTransactionResponse response = await transactionDomainService.ProcessSaleTransaction(TestData.TransactionId, TestData.EstateId, @@ -531,6 +570,55 @@ public async Task TransactionDomainService_ProcessSaleTransaction_SuccesfulOpera this.ValidateResponse(response, TransactionResponseCode.Success); } + [Fact] + public async Task TransactionDomainService_ProcessSaleTransaction_InvalidTransactionAmountResponse_TransactionIsProcessed() + { + IConfigurationRoot configurationRoot = new ConfigurationBuilder().AddInMemoryCollection(TestData.DefaultAppSettings).Build(); + ConfigurationReader.Initialise(configurationRoot); + + Logger.Initialise(NullLogger.Instance); + + Mock transactionAggregateManager = new Mock(); + Mock estateClient = new Mock(); + Mock securityServiceClient = new Mock(); + + securityServiceClient.Setup(s => s.GetToken(It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(TestData.TokenResponse); + estateClient.Setup(e => e.GetEstate(It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(TestData.GetEstateResponseWithOperator1); + estateClient.Setup(e => e.GetMerchant(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(TestData.GetMerchantResponseWithOperator1); + transactionAggregateManager.Setup(t => t.GetAggregate(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(TestData.GetLocallyDeclinedTransactionAggregate(TransactionResponseCode.InvalidSaleTransactionAmount)); + + Mock operatorProxy = new Mock(); + Func operatorProxyResolver = (operatorName) => { return operatorProxy.Object; }; + + Mock> reconciliationAggregateRepository = + new Mock>(); + + Mock> settlementAggregateRepository = + new Mock>(); + + TransactionDomainService transactionDomainService = + new TransactionDomainService(transactionAggregateManager.Object, estateClient.Object, securityServiceClient.Object, + operatorProxyResolver, reconciliationAggregateRepository.Object, + settlementAggregateRepository.Object); + + ProcessSaleTransactionResponse response = await transactionDomainService.ProcessSaleTransaction(TestData.TransactionId, + TestData.EstateId, + TestData.MerchantId, + TestData.TransactionDateTime, + TestData.TransactionNumber, + TestData.DeviceIdentifier, + TestData.OperatorIdentifier1, + TestData.CustomerEmailAddress, + TestData.AdditionalTransactionMetaData(amount:"0.00"), + TestData.ContractId, + TestData.ProductId, + CancellationToken.None); + + this.ValidateResponse(response, TransactionResponseCode.InvalidSaleTransactionAmount); + } + [Theory] [InlineData("amount")] [InlineData("Amount")] @@ -573,10 +661,13 @@ public async Task TransactionDomainService_ProcessSaleTransaction_MetaDataCaseTe Mock> reconciliationAggregateRepository = new Mock>(); + Mock> settlementAggregateRepository = + new Mock>(); TransactionDomainService transactionDomainService = new TransactionDomainService(transactionAggregateManager.Object, estateClient.Object, securityServiceClient.Object, - operatorProxyResolver, reconciliationAggregateRepository.Object); + operatorProxyResolver, reconciliationAggregateRepository.Object, + settlementAggregateRepository.Object); @@ -638,10 +729,13 @@ public async Task TransactionDomainService_ProcessSaleTransaction_MetaDataCaseTe Mock> reconciliationAggregateRepository = new Mock>(); + Mock> settlementAggregateRepository = + new Mock>(); TransactionDomainService transactionDomainService = new TransactionDomainService(transactionAggregateManager.Object, estateClient.Object, securityServiceClient.Object, - operatorProxyResolver, reconciliationAggregateRepository.Object); + operatorProxyResolver, reconciliationAggregateRepository.Object, + settlementAggregateRepository.Object); @@ -698,10 +792,13 @@ public async Task TransactionDomainService_ProcessSaleTransaction_FailedOperator Mock> reconciliationAggregateRepository = new Mock>(); + Mock> settlementAggregateRepository = + new Mock>(); TransactionDomainService transactionDomainService = new TransactionDomainService(transactionAggregateManager.Object, estateClient.Object, securityServiceClient.Object, - operatorProxyResolver, reconciliationAggregateRepository.Object); + operatorProxyResolver, reconciliationAggregateRepository.Object, + settlementAggregateRepository.Object); ProcessSaleTransactionResponse response = await transactionDomainService.ProcessSaleTransaction(TestData.TransactionId, TestData.EstateId, @@ -742,10 +839,13 @@ public async Task TransactionDomainService_ProcessSaleTransaction_MerchantWithNu Mock> reconciliationAggregateRepository = new Mock>(); + Mock> settlementAggregateRepository = + new Mock>(); TransactionDomainService transactionDomainService = new TransactionDomainService(transactionAggregateManager.Object, estateClient.Object, securityServiceClient.Object, - operatorProxyResolver, reconciliationAggregateRepository.Object); + operatorProxyResolver, reconciliationAggregateRepository.Object, + settlementAggregateRepository.Object); ProcessSaleTransactionResponse response = await transactionDomainService.ProcessSaleTransaction(TestData.TransactionId, TestData.EstateId, @@ -787,10 +887,13 @@ public async Task TransactionDomainService_ProcessSaleTransaction_MerchantWithNo Mock> reconciliationAggregateRepository = new Mock>(); + Mock> settlementAggregateRepository = + new Mock>(); TransactionDomainService transactionDomainService = new TransactionDomainService(transactionAggregateManager.Object, estateClient.Object, securityServiceClient.Object, - operatorProxyResolver, reconciliationAggregateRepository.Object); + operatorProxyResolver, reconciliationAggregateRepository.Object, + settlementAggregateRepository.Object); ProcessSaleTransactionResponse response = await transactionDomainService.ProcessSaleTransaction(TestData.TransactionId, TestData.EstateId, @@ -831,10 +934,13 @@ public async Task TransactionDomainService_ProcessSaleTransaction_IncorrectDevic Mock> reconciliationAggregateRepository = new Mock>(); + Mock> settlementAggregateRepository = + new Mock>(); TransactionDomainService transactionDomainService = new TransactionDomainService(transactionAggregateManager.Object, estateClient.Object, securityServiceClient.Object, - operatorProxyResolver, reconciliationAggregateRepository.Object); + operatorProxyResolver, reconciliationAggregateRepository.Object, + settlementAggregateRepository.Object); ProcessSaleTransactionResponse response = await transactionDomainService.ProcessSaleTransaction(TestData.TransactionId, TestData.EstateId, @@ -875,10 +981,13 @@ public async Task TransactionDomainService_ProcessSaleTransaction_InvalidEstate_ Mock> reconciliationAggregateRepository = new Mock>(); + Mock> settlementAggregateRepository = + new Mock>(); TransactionDomainService transactionDomainService = new TransactionDomainService(transactionAggregateManager.Object, estateClient.Object, securityServiceClient.Object, - operatorProxyResolver, reconciliationAggregateRepository.Object); + operatorProxyResolver, reconciliationAggregateRepository.Object, + settlementAggregateRepository.Object); ProcessSaleTransactionResponse response = await transactionDomainService.ProcessSaleTransaction(TestData.TransactionId, TestData.EstateId, @@ -913,17 +1022,20 @@ public async Task TransactionDomainService_ProcessSaleTransaction_NotEnoughCredi securityServiceClient.Setup(s => s.GetToken(It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(TestData.TokenResponse); estateClient.Setup(e => e.GetEstate(It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(TestData.GetEstateResponseWithOperator1); estateClient.Setup(e => e.GetMerchant(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .ReturnsAsync(TestData.GetMerchantResponseWithZeroAvailableBalance); + .ReturnsAsync(TestData.GetMerchantResponseWithOperator1); transactionAggregateManager.Setup(t => t.GetAggregate(It.IsAny(), It.IsAny(), It.IsAny())) - .ReturnsAsync(TestData.GetLocallyDeclinedTransactionAggregate(TransactionResponseCode.MerchantDoesNotHaveEnoughCredit)); + .ReturnsAsync(TestData.GetDeclinedTransactionAggregate(TransactionResponseCode.MerchantDoesNotHaveEnoughCredit)); Mock> reconciliationAggregateRepository = new Mock>(); + Mock> settlementAggregateRepository = + new Mock>(); TransactionDomainService transactionDomainService = new TransactionDomainService(transactionAggregateManager.Object, estateClient.Object, securityServiceClient.Object, operatorProxyResolver, - reconciliationAggregateRepository.Object); + reconciliationAggregateRepository.Object, + settlementAggregateRepository.Object); ProcessSaleTransactionResponse response = await transactionDomainService.ProcessSaleTransaction(TestData.TransactionId, TestData.EstateId, @@ -963,10 +1075,13 @@ public async Task TransactionDomainService_ProcessSaleTransaction_InvalidMerchan Mock> reconciliationAggregateRepository = new Mock>(); + Mock> settlementAggregateRepository = + new Mock>(); TransactionDomainService transactionDomainService = new TransactionDomainService(transactionAggregateManager.Object, estateClient.Object, securityServiceClient.Object, - operatorProxyResolver, reconciliationAggregateRepository.Object); + operatorProxyResolver, reconciliationAggregateRepository.Object, + settlementAggregateRepository.Object); ProcessSaleTransactionResponse response = await transactionDomainService.ProcessSaleTransaction(TestData.TransactionId, TestData.EstateId, @@ -1007,10 +1122,12 @@ public async Task TransactionDomainService_ProcessSaleTransaction_EstateWithEmpt Mock> reconciliationAggregateRepository = new Mock>(); - + Mock> settlementAggregateRepository = + new Mock>(); TransactionDomainService transactionDomainService = new TransactionDomainService(transactionAggregateManager.Object, estateClient.Object, securityServiceClient.Object, - operatorProxyResolver, reconciliationAggregateRepository.Object); + operatorProxyResolver, reconciliationAggregateRepository.Object, + settlementAggregateRepository.Object); ProcessSaleTransactionResponse response = await transactionDomainService.ProcessSaleTransaction(TestData.TransactionId, TestData.EstateId, @@ -1051,10 +1168,13 @@ public async Task TransactionDomainService_ProcessSaleTransaction_EstateWithNull Mock> reconciliationAggregateRepository = new Mock>(); + Mock> settlementAggregateRepository = + new Mock>(); TransactionDomainService transactionDomainService = new TransactionDomainService(transactionAggregateManager.Object, estateClient.Object, securityServiceClient.Object, - operatorProxyResolver,reconciliationAggregateRepository.Object); + operatorProxyResolver,reconciliationAggregateRepository.Object, + settlementAggregateRepository.Object); ProcessSaleTransactionResponse response = await transactionDomainService.ProcessSaleTransaction(TestData.TransactionId, TestData.EstateId, @@ -1095,10 +1215,13 @@ public async Task TransactionDomainService_ProcessSaleTransaction_OperatorNotSup Mock> reconciliationAggregateRepository = new Mock>(); + Mock> settlementAggregateRepository = + new Mock>(); TransactionDomainService transactionDomainService = new TransactionDomainService(transactionAggregateManager.Object, estateClient.Object, securityServiceClient.Object, - operatorProxyResolver, reconciliationAggregateRepository.Object); + operatorProxyResolver, reconciliationAggregateRepository.Object, + settlementAggregateRepository.Object); ProcessSaleTransactionResponse response = await transactionDomainService.ProcessSaleTransaction(TestData.TransactionId, TestData.EstateId, @@ -1139,11 +1262,14 @@ public async Task TransactionDomainService_ProcessSaleTransaction_MerchantWithEm Mock> reconciliationAggregateRepository = new Mock>(); + Mock> settlementAggregateRepository = + new Mock>(); TransactionDomainService transactionDomainService = new TransactionDomainService(transactionAggregateManager.Object, estateClient.Object, securityServiceClient.Object, operatorProxyResolver, - reconciliationAggregateRepository.Object); + reconciliationAggregateRepository.Object, + settlementAggregateRepository.Object); ProcessSaleTransactionResponse response = await transactionDomainService.ProcessSaleTransaction(TestData.TransactionId, TestData.EstateId, @@ -1184,10 +1310,13 @@ public async Task TransactionDomainService_ProcessSaleTransaction_MerchantWithNu Mock> reconciliationAggregateRepository = new Mock>(); + Mock> settlementAggregateRepository = + new Mock>(); TransactionDomainService transactionDomainService = new TransactionDomainService(transactionAggregateManager.Object, estateClient.Object, securityServiceClient.Object, - operatorProxyResolver,reconciliationAggregateRepository.Object); + operatorProxyResolver,reconciliationAggregateRepository.Object, + settlementAggregateRepository.Object); ProcessSaleTransactionResponse response = await transactionDomainService.ProcessSaleTransaction(TestData.TransactionId, TestData.EstateId, @@ -1228,10 +1357,13 @@ public async Task TransactionDomainService_ProcessSaleTransaction_OperatorNotSup Mock> reconciliationAggregateRepository = new Mock>(); + Mock> settlementAggregateRepository = + new Mock>(); TransactionDomainService transactionDomainService = new TransactionDomainService(transactionAggregateManager.Object, estateClient.Object, securityServiceClient.Object, - operatorProxyResolver, reconciliationAggregateRepository.Object); + operatorProxyResolver, reconciliationAggregateRepository.Object, + settlementAggregateRepository.Object); ProcessSaleTransactionResponse response = await transactionDomainService.ProcessSaleTransaction(TestData.TransactionId, TestData.EstateId, @@ -1281,10 +1413,13 @@ public async Task TransactionDomainService_ProcessSaleTransaction_ErrorInOperato Mock> reconciliationAggregateRepository = new Mock>(); + Mock> settlementAggregateRepository = + new Mock>(); TransactionDomainService transactionDomainService = new TransactionDomainService(transactionAggregateManager.Object, estateClient.Object, securityServiceClient.Object, - operatorProxyResolver, reconciliationAggregateRepository.Object); + operatorProxyResolver, reconciliationAggregateRepository.Object, + settlementAggregateRepository.Object); ProcessSaleTransactionResponse response = await transactionDomainService.ProcessSaleTransaction(TestData.TransactionId, TestData.EstateId, @@ -1339,5 +1474,159 @@ private void ValidateResponse(ProcessReconciliationTransactionResponse response, response.ResponseCode.ShouldBe(TestData.GetResponseCodeAsString(transactionResponseCode)); } + + [Fact] + public async Task TransactionDomainService_ProcessSettlement_SettlementIsProcessed() + { + IConfigurationRoot configurationRoot = new ConfigurationBuilder().AddInMemoryCollection(TestData.DefaultAppSettings).Build(); + ConfigurationReader.Initialise(configurationRoot); + + Logger.Initialise(NullLogger.Instance); + + Mock transactionAggregateManager = new Mock(); + Mock estateClient = new Mock(); + Mock securityServiceClient = new Mock(); + Mock operatorProxy = new Mock(); + Func operatorProxyResolver = (operatorName) => { return operatorProxy.Object; }; + + Mock> reconciliationAggregateRepository = + new Mock>(); + Mock> settlementAggregateRepository = + new Mock>(); + settlementAggregateRepository.Setup(s => s.GetLatestVersion(It.IsAny(), It.IsAny())) + .ReturnsAsync(TestData.GetSettlementAggregateWithPendingMerchantFees(10)); + + TransactionDomainService transactionDomainService = + new TransactionDomainService(transactionAggregateManager.Object, estateClient.Object, securityServiceClient.Object, + operatorProxyResolver, + reconciliationAggregateRepository.Object, + settlementAggregateRepository.Object); + + ProcessSettlementResponse response = await transactionDomainService.ProcessSettlement(TestData.SettlementDate, + TestData.EstateId, + CancellationToken.None); + + response.ShouldNotBeNull(); + response.NumberOfFeesFailedToSettle.ShouldBe(0); + response.NumberOfFeesPendingSettlement.ShouldBe(0); + response.NumberOfFeesSuccessfullySettled.ShouldBe(10); + } + + [Fact] + public async Task TransactionDomainService_ProcessSettlement_SettlementAggregateNotCreated_NothingProcessed() + { + IConfigurationRoot configurationRoot = new ConfigurationBuilder().AddInMemoryCollection(TestData.DefaultAppSettings).Build(); + ConfigurationReader.Initialise(configurationRoot); + + Logger.Initialise(NullLogger.Instance); + + Mock transactionAggregateManager = new Mock(); + Mock estateClient = new Mock(); + Mock securityServiceClient = new Mock(); + Mock operatorProxy = new Mock(); + Func operatorProxyResolver = (operatorName) => { return operatorProxy.Object; }; + + Mock> reconciliationAggregateRepository = + new Mock>(); + Mock> settlementAggregateRepository = + new Mock>(); + settlementAggregateRepository.Setup(s => s.GetLatestVersion(It.IsAny(), It.IsAny())) + .ReturnsAsync(TestData.GetEmptySettlementAggregate); + + TransactionDomainService transactionDomainService = + new TransactionDomainService(transactionAggregateManager.Object, estateClient.Object, securityServiceClient.Object, + operatorProxyResolver, + reconciliationAggregateRepository.Object, + settlementAggregateRepository.Object); + + ProcessSettlementResponse response = await transactionDomainService.ProcessSettlement(TestData.SettlementDate, + TestData.EstateId, + CancellationToken.None); + + response.ShouldNotBeNull(); + response.NumberOfFeesFailedToSettle.ShouldBe(0); + response.NumberOfFeesPendingSettlement.ShouldBe(0); + response.NumberOfFeesSuccessfullySettled.ShouldBe(0); + } + + [Fact] + public async Task TransactionDomainService_ProcessSettlement_SettlementAggregateNoFeesToSettles_NothingProcessed() + { + IConfigurationRoot configurationRoot = new ConfigurationBuilder().AddInMemoryCollection(TestData.DefaultAppSettings).Build(); + ConfigurationReader.Initialise(configurationRoot); + + Logger.Initialise(NullLogger.Instance); + + Mock transactionAggregateManager = new Mock(); + Mock estateClient = new Mock(); + Mock securityServiceClient = new Mock(); + Mock operatorProxy = new Mock(); + Func operatorProxyResolver = (operatorName) => { return operatorProxy.Object; }; + + Mock> reconciliationAggregateRepository = + new Mock>(); + Mock> settlementAggregateRepository = + new Mock>(); + settlementAggregateRepository.Setup(s => s.GetLatestVersion(It.IsAny(), It.IsAny())) + .ReturnsAsync(TestData.GetCreatedSettlementAggregate); + + TransactionDomainService transactionDomainService = + new TransactionDomainService(transactionAggregateManager.Object, estateClient.Object, securityServiceClient.Object, + operatorProxyResolver, + reconciliationAggregateRepository.Object, + settlementAggregateRepository.Object); + + ProcessSettlementResponse response = await transactionDomainService.ProcessSettlement(TestData.SettlementDate, + TestData.EstateId, + CancellationToken.None); + + response.ShouldNotBeNull(); + response.NumberOfFeesFailedToSettle.ShouldBe(0); + response.NumberOfFeesPendingSettlement.ShouldBe(0); + response.NumberOfFeesSuccessfullySettled.ShouldBe(0); + } + + [Fact] + public async Task TransactionDomainService_ProcessSettlement_AddSettledFeeThrownException_SettlementProcessed() + { + IConfigurationRoot configurationRoot = new ConfigurationBuilder().AddInMemoryCollection(TestData.DefaultAppSettings).Build(); + ConfigurationReader.Initialise(configurationRoot); + + Logger.Initialise(NullLogger.Instance); + + Mock transactionAggregateManager = new Mock(); + transactionAggregateManager.Setup(t => t.AddSettledFee(It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())).ThrowsAsync(new Exception()); + Mock estateClient = new Mock(); + Mock securityServiceClient = new Mock(); + Mock operatorProxy = new Mock(); + Func operatorProxyResolver = (operatorName) => { return operatorProxy.Object; }; + + Mock> reconciliationAggregateRepository = + new Mock>(); + Mock> settlementAggregateRepository = + new Mock>(); + settlementAggregateRepository.Setup(s => s.GetLatestVersion(It.IsAny(), It.IsAny())) + .ReturnsAsync(TestData.GetSettlementAggregateWithPendingMerchantFees(10)); + + TransactionDomainService transactionDomainService = + new TransactionDomainService(transactionAggregateManager.Object, estateClient.Object, securityServiceClient.Object, + operatorProxyResolver, + reconciliationAggregateRepository.Object, + settlementAggregateRepository.Object); + + ProcessSettlementResponse response = await transactionDomainService.ProcessSettlement(TestData.SettlementDate, + TestData.EstateId, + CancellationToken.None); + + response.ShouldNotBeNull(); + response.NumberOfFeesFailedToSettle.ShouldBe(10); + response.NumberOfFeesPendingSettlement.ShouldBe(0); + response.NumberOfFeesSuccessfullySettled.ShouldBe(0); + } } } diff --git a/TransactionProcessor.BusinessLogic/EventHandling/TransactionDomainEventHandler.cs b/TransactionProcessor.BusinessLogic/EventHandling/TransactionDomainEventHandler.cs index 090ea2be..19f91de6 100644 --- a/TransactionProcessor.BusinessLogic/EventHandling/TransactionDomainEventHandler.cs +++ b/TransactionProcessor.BusinessLogic/EventHandling/TransactionDomainEventHandler.cs @@ -63,7 +63,7 @@ public class TransactionDomainEventHandler : IDomainEventHandler /// private readonly IMessagingServiceClient MessagingServiceClient; - private readonly IAggregateRepository PendingSettlementAggregateRepository; + private readonly IAggregateRepository SettlementAggregateRepository; /// /// The token response @@ -94,7 +94,7 @@ public TransactionDomainEventHandler(ITransactionAggregateManager transactionAgg ISecurityServiceClient securityServiceClient, ITransactionReceiptBuilder transactionReceiptBuilder, IMessagingServiceClient messagingServiceClient, - IAggregateRepository pendingSettlementAggregateRepository) + IAggregateRepository settlementAggregateRepository) { this.TransactionAggregateManager = transactionAggregateManager; this.FeeCalculationManager = feeCalculationManager; @@ -102,7 +102,7 @@ public TransactionDomainEventHandler(ITransactionAggregateManager transactionAgg this.SecurityServiceClient = securityServiceClient; this.TransactionReceiptBuilder = transactionReceiptBuilder; this.MessagingServiceClient = messagingServiceClient; - this.PendingSettlementAggregateRepository = pendingSettlementAggregateRepository; + this.SettlementAggregateRepository = settlementAggregateRepository; } #endregion @@ -226,7 +226,7 @@ private async Task HandleSpecificDomainEvent(TransactionHasBeenCompletedEvent do foreach (CalculatedFee calculatedFee in merchantFees) { // Add Fee to the Transaction - await this.TransactionAggregateManager.AddFee(transactionAggregate.EstateId, transactionAggregate.AggregateId, calculatedFee, cancellationToken); + await this.TransactionAggregateManager.AddSettledFee(transactionAggregate.EstateId, transactionAggregate.AggregateId, calculatedFee, DateTime.Now.Date, DateTime.Now, cancellationToken); } } else if (merchant.SettlementSchedule == SettlementSchedule.NotSet) @@ -241,7 +241,7 @@ private async Task HandleSpecificDomainEvent(TransactionHasBeenCompletedEvent do Guid aggregateId = merchant.NextSettlementDueDate.ToGuid(); // We need to add the fees to a pending settlement stream (for today) - PendingSettlementAggregate aggregate = await this.PendingSettlementAggregateRepository.GetLatestVersion(aggregateId, cancellationToken); + SettlementAggregate aggregate = await this.SettlementAggregateRepository.GetLatestVersion(aggregateId, cancellationToken); if (aggregate.IsCreated == false) { @@ -250,7 +250,7 @@ private async Task HandleSpecificDomainEvent(TransactionHasBeenCompletedEvent do aggregate.AddFee(transactionAggregate.MerchantId, transactionAggregate.AggregateId, calculatedFee); - await this.PendingSettlementAggregateRepository.SaveChanges(aggregate, cancellationToken); + await this.SettlementAggregateRepository.SaveChanges(aggregate, cancellationToken); } } } @@ -277,6 +277,24 @@ private async Task HandleSpecificDomainEvent(CustomerEmailReceiptRequestedEvent } + private async Task HandleSpecificDomainEvent(MerchantFeeAddedToTransactionEvent domainEvent, + CancellationToken cancellationToken) + { + if (domainEvent.SettlementDueDate == DateTime.MinValue) + { + // Old event format before settlement + return; + } + + Guid aggregateId = domainEvent.SettlementDueDate.ToGuid(); + + SettlementAggregate pendingSettlementAggregate = await this.SettlementAggregateRepository.GetLatestVersion(aggregateId, cancellationToken); + + pendingSettlementAggregate.MarkFeeAsSettled(domainEvent.MerchantId, domainEvent.TransactionId, domainEvent.FeeId); + + await this.SettlementAggregateRepository.SaveChanges(pendingSettlementAggregate, cancellationToken); + } + /// /// Sends the email message. /// diff --git a/TransactionProcessor.BusinessLogic/RequestHandlers/SettlementRequestHandler.cs b/TransactionProcessor.BusinessLogic/RequestHandlers/SettlementRequestHandler.cs new file mode 100644 index 00000000..c2539e21 --- /dev/null +++ b/TransactionProcessor.BusinessLogic/RequestHandlers/SettlementRequestHandler.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TransactionProcessor.BusinessLogic.RequestHandlers +{ + using System.Threading; + using MediatR; + using Models; + using Requests; + using Services; + + public class SettlementRequestHandler : IRequestHandler + { + private readonly ITransactionDomainService TransactionDomainService; + + public SettlementRequestHandler(ITransactionDomainService transactionDomainService) + { + this.TransactionDomainService = transactionDomainService; + } + + public async Task Handle(ProcessSettlementRequest request, + CancellationToken cancellationToken) + { + ProcessSettlementResponse processSettlementResponse = await this.TransactionDomainService.ProcessSettlement(request.SettlementDate, request.EstateId, cancellationToken); + + return processSettlementResponse; + } + } +} diff --git a/TransactionProcessor.BusinessLogic/Requests/ProcessSettlementRequest.cs b/TransactionProcessor.BusinessLogic/Requests/ProcessSettlementRequest.cs new file mode 100644 index 00000000..90ee835e --- /dev/null +++ b/TransactionProcessor.BusinessLogic/Requests/ProcessSettlementRequest.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TransactionProcessor.BusinessLogic.Requests +{ + using MediatR; + using Models; + + public class ProcessSettlementRequest : IRequest + { + #region Constructors + + private ProcessSettlementRequest(DateTime settlementDate, Guid estateId) + { + this.EstateId = estateId; + this.SettlementDate = settlementDate; + } + + #endregion + + #region Properties + + /// + /// Gets the estate identifier. + /// + /// + /// The estate identifier. + /// + public Guid EstateId { get; } + + + public DateTime SettlementDate { get; } + + #endregion + + #region Methods + + /// + /// Creates the specified estate identifier. + /// + /// The transaction identifier. + /// The estate identifier. + /// The merchant identifier. + /// The device identifier. + /// Type of the transaction. + /// The transaction date time. + /// The transaction number. + /// + public static ProcessSettlementRequest Create(DateTime settlementDate, + Guid estateId) + { + return new ProcessSettlementRequest(settlementDate, estateId); + } + + #endregion + } +} diff --git a/TransactionProcessor.BusinessLogic/Services/ITransactionAggregateManager.cs b/TransactionProcessor.BusinessLogic/Services/ITransactionAggregateManager.cs index 455c0f4e..8054d6a8 100644 --- a/TransactionProcessor.BusinessLogic/Services/ITransactionAggregateManager.cs +++ b/TransactionProcessor.BusinessLogic/Services/ITransactionAggregateManager.cs @@ -44,6 +44,14 @@ Task AddFee(Guid estateId, CalculatedFee calculatedFee, CancellationToken cancellationToken); + + Task AddSettledFee(Guid estateId, + Guid transactionId, + CalculatedFee calculatedFee, + DateTime settlementDueDate, + DateTime settledDateTime, + CancellationToken cancellationToken); + /// /// Authorises the transaction. /// diff --git a/TransactionProcessor.BusinessLogic/Services/ITransactionDomainService.cs b/TransactionProcessor.BusinessLogic/Services/ITransactionDomainService.cs index 40fd157d..d87b0a4c 100644 --- a/TransactionProcessor.BusinessLogic/Services/ITransactionDomainService.cs +++ b/TransactionProcessor.BusinessLogic/Services/ITransactionDomainService.cs @@ -82,6 +82,10 @@ Task ProcessReconciliationTransaction( Decimal transactionValue, CancellationToken cancellationToken); + Task ProcessSettlement(DateTime pendingSettlementDate, + Guid estateId, + CancellationToken cancellationToken); + #endregion } } \ No newline at end of file diff --git a/TransactionProcessor.BusinessLogic/Services/TransactionAggregateManager.cs b/TransactionProcessor.BusinessLogic/Services/TransactionAggregateManager.cs index e9ad0fd6..5724330a 100644 --- a/TransactionProcessor.BusinessLogic/Services/TransactionAggregateManager.cs +++ b/TransactionProcessor.BusinessLogic/Services/TransactionAggregateManager.cs @@ -85,6 +85,20 @@ public async Task AddFee(Guid estateId, } + public async Task AddSettledFee(Guid estateId, + Guid transactionId, + CalculatedFee calculatedFee, + DateTime settlementDueDate, + DateTime settledDateTime, + CancellationToken cancellationToken) + { + TransactionAggregate transactionAggregate = await this.TransactionAggregateRepository.GetLatestVersion(transactionId, cancellationToken); + + transactionAggregate.AddSettledFee(calculatedFee, settlementDueDate, settledDateTime); + + await this.TransactionAggregateRepository.SaveChanges(transactionAggregate, cancellationToken); + } + /// /// Authorises the transaction. /// diff --git a/TransactionProcessor.BusinessLogic/Services/TransactionDomainService.cs b/TransactionProcessor.BusinessLogic/Services/TransactionDomainService.cs index 1985b8da..316e9cd4 100644 --- a/TransactionProcessor.BusinessLogic/Services/TransactionDomainService.cs +++ b/TransactionProcessor.BusinessLogic/Services/TransactionDomainService.cs @@ -17,6 +17,7 @@ using ReconciliationAggregate; using SecurityService.Client; using SecurityService.DataTransferObjects.Responses; + using SettlementAggregates; using Shared.DomainDrivenDesign.EventSourcing; using Shared.EventStore.Aggregate; using Shared.EventStore.EventStore; @@ -61,7 +62,9 @@ public class TransactionDomainService : ITransactionDomainService /// The reconciliation aggregate repository /// private readonly IAggregateRepository ReconciliationAggregateRepository; - + + private readonly IAggregateRepository SettlementAggregateRepository; + #endregion #region Constructors @@ -78,13 +81,15 @@ public TransactionDomainService(ITransactionAggregateManager transactionAggregat IEstateClient estateClient, ISecurityServiceClient securityServiceClient, Func operatorProxyResolver, - IAggregateRepository reconciliationAggregateRepository) + IAggregateRepository reconciliationAggregateRepository, + IAggregateRepository settlementAggregateRepository) { this.TransactionAggregateManager = transactionAggregateManager; this.EstateClient = estateClient; this.SecurityServiceClient = securityServiceClient; this.OperatorProxyResolver = operatorProxyResolver; this.ReconciliationAggregateRepository = reconciliationAggregateRepository; + this.SettlementAggregateRepository = settlementAggregateRepository; } #endregion @@ -194,12 +199,7 @@ public async Task ProcessSaleTransaction(Guid tr (String responseMessage, TransactionResponseCode responseCode) validationResult = await this.ValidateSaleTransaction(estateId, merchantId, deviceIdentifier, operatorIdentifier, transactionAmount, cancellationToken); - - if (validationResult.responseCode == TransactionResponseCode.InvalidSaleTransactionAmount) - { - throw new InvalidDataException(validationResult.responseMessage); - } - + await this.TransactionAggregateManager.StartTransaction(transactionId, transactionDateTime, transactionNumber, @@ -396,7 +396,49 @@ public async Task ProcessReconciliatio }; } - + public async Task ProcessSettlement(DateTime settlementDate, + Guid estateId, + CancellationToken cancellationToken) + { + ProcessSettlementResponse response = new ProcessSettlementResponse(); + + Guid aggregateId = settlementDate.ToGuid(); + + SettlementAggregate settlementAggregate = await this.SettlementAggregateRepository.GetLatestVersion(aggregateId, cancellationToken); + + if (settlementAggregate.IsCreated == false) + { + // Not pending settlement for this date + return response; + } + + var feesToBeSettled = settlementAggregate.GetFeesToBeSettled(); + response.NumberOfFeesPendingSettlement = feesToBeSettled.Count; + + foreach ((Guid transactionId, Guid merchantId, CalculatedFee calculatedFee) feeToSettle in feesToBeSettled) + { + try + { + await this.TransactionAggregateManager.AddSettledFee(estateId, + feeToSettle.transactionId, + feeToSettle.calculatedFee, + settlementDate, + DateTime.Now, + cancellationToken); + response.NumberOfFeesSuccessfullySettled++; + response.NumberOfFeesPendingSettlement--; + } + catch(Exception ex) + { + Logger.LogError(ex); + response.NumberOfFeesPendingSettlement--; + response.NumberOfFeesFailedToSettle++; + } + + } + + return response; + } /// /// Adds the device to merchant. diff --git a/TransactionProcessor.Client/ITransactionProcessorClient.cs b/TransactionProcessor.Client/ITransactionProcessorClient.cs index c673f756..e883374d 100644 --- a/TransactionProcessor.Client/ITransactionProcessorClient.cs +++ b/TransactionProcessor.Client/ITransactionProcessorClient.cs @@ -19,11 +19,16 @@ Task PerformTransaction(String accessToken, SerialisedMessage transactionRequest, CancellationToken cancellationToken); - Task GetPendingSettlementByDate(String accessToken, - DateTime pendingSettlementDate, + Task GetSettlementByDate(String accessToken, + DateTime settlementDate, Guid estateId, CancellationToken cancellationToken); + Task ProcessSettlement(String accessToken, + DateTime settlementDate, + Guid estateId, + CancellationToken cancellationToken); + #endregion } } \ No newline at end of file diff --git a/TransactionProcessor.Client/TransactionProcessorClient.cs b/TransactionProcessor.Client/TransactionProcessorClient.cs index e41fa9d9..0464fa87 100644 --- a/TransactionProcessor.Client/TransactionProcessorClient.cs +++ b/TransactionProcessor.Client/TransactionProcessorClient.cs @@ -90,14 +90,14 @@ public async Task PerformTransaction(String accessToken, return response; } - public async Task GetPendingSettlementByDate(String accessToken, - DateTime pendingSettlementDate, + public async Task GetSettlementByDate(String accessToken, + DateTime settlementDate, Guid estateId, CancellationToken cancellationToken) { - PendingSettlementResponse response = null; + SettlementResponse response = null; - String requestUri = $"{this.BaseAddress}/api/settlements/{pendingSettlementDate.Date:yyyy-MM-dd}/estates/{estateId}/pending"; + String requestUri = $"{this.BaseAddress}/api/settlements/{settlementDate.Date:yyyy-MM-dd}/estates/{estateId}/pending"; try { @@ -111,12 +111,12 @@ public async Task GetPendingSettlementByDate(String a String content = await this.HandleResponse(httpResponse, cancellationToken); // call was successful so now deserialise the body to the response object - response = JsonConvert.DeserializeObject(content); + response = JsonConvert.DeserializeObject(content); } catch (Exception ex) { // An exception has occurred, add some additional information to the message - Exception exception = new Exception("Error getting pending settlment.", ex); + Exception exception = new Exception("Error getting settlment.", ex); throw exception; } @@ -124,6 +124,36 @@ public async Task GetPendingSettlementByDate(String a return response; } + public async Task ProcessSettlement(String accessToken, + DateTime settlementDate, + Guid estateId, + CancellationToken cancellationToken) + { + String requestUri = $"{this.BaseAddress}/api/settlements/{settlementDate.Date:yyyy-MM-dd}/estates/{estateId}"; + + try + { + // Add the access token to the client headers + this.HttpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); + + StringContent requestContent = new StringContent(String.Empty); + + // Make the Http Call here + HttpResponseMessage httpResponse = await this.HttpClient.PostAsync(requestUri, requestContent, cancellationToken); + + // Process the response + await this.HandleResponse(httpResponse, cancellationToken); + + } + catch (Exception ex) + { + // An exception has occurred, add some additional information to the message + Exception exception = new Exception("Error processing settlment.", ex); + + throw exception; + } + } + #endregion } } \ No newline at end of file diff --git a/TransactionProcessor.DataTransferObjects/ProcessSettlementResponse.cs b/TransactionProcessor.DataTransferObjects/ProcessSettlementResponse.cs new file mode 100644 index 00000000..de1dcf05 --- /dev/null +++ b/TransactionProcessor.DataTransferObjects/ProcessSettlementResponse.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TransactionProcessor.DataTransferObjects +{ + internal class ProcessSettlementResponse + { + } +} diff --git a/TransactionProcessor.DataTransferObjects/PendingSettlementResponse.cs b/TransactionProcessor.DataTransferObjects/SettlementResponse.cs similarity index 59% rename from TransactionProcessor.DataTransferObjects/PendingSettlementResponse.cs rename to TransactionProcessor.DataTransferObjects/SettlementResponse.cs index 2a32a791..ecb8e54b 100644 --- a/TransactionProcessor.DataTransferObjects/PendingSettlementResponse.cs +++ b/TransactionProcessor.DataTransferObjects/SettlementResponse.cs @@ -1,8 +1,11 @@ namespace TransactionProcessor.DataTransferObjects { using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; - public class PendingSettlementResponse + [ExcludeFromCodeCoverage] + public class SettlementResponse { public Guid EstateId { get; set; } @@ -11,6 +14,7 @@ public class PendingSettlementResponse public Int32 NumberOfFeesPendingSettlement { get; set; } public Int32 NumberOfFeesSettled { get; set; } - + + public Boolean SettlementCompleted { get; set; } } } \ No newline at end of file diff --git a/TransactionProcessor.IntegrationTests/Common/DockerHelper.cs b/TransactionProcessor.IntegrationTests/Common/DockerHelper.cs index 390f1747..628fdc58 100644 --- a/TransactionProcessor.IntegrationTests/Common/DockerHelper.cs +++ b/TransactionProcessor.IntegrationTests/Common/DockerHelper.cs @@ -354,6 +354,7 @@ protected async Task PopulateSubscriptionServiceConfiguration() await client.CreateAsync("$ce-ContractAggregate", "Reporting", settings); await client.CreateAsync("$ce-TransactionAggregate", "Reporting", settings); await client.CreateAsync("$et-TransactionHasBeenCompletedEvent", "TransactionProcessor", settings); + await client.CreateAsync("$et-MerchantFeeAddedToTransactionEvent", "TransactionProcessor", settings); } private static EventStoreClientSettings ConfigureEventStoreSettings(Int32 eventStoreHttpPort) diff --git a/TransactionProcessor.IntegrationTests/Common/SpecflowTableHelper.cs b/TransactionProcessor.IntegrationTests/Common/SpecflowTableHelper.cs index c618d6f5..dc1e1782 100644 --- a/TransactionProcessor.IntegrationTests/Common/SpecflowTableHelper.cs +++ b/TransactionProcessor.IntegrationTests/Common/SpecflowTableHelper.cs @@ -41,6 +41,8 @@ public static DateTime GetDateForDateString(String dateString, return today.AddDays(-1).Date; case "LASTWEEK": return today.AddDays(-7).Date; + case "NEXTWEEK": + return today.AddDays(7).Date; case "LASTMONTH": return today.AddMonths(-1).Date; case "LASTYEAR": diff --git a/TransactionProcessor.IntegrationTests/Settlement/Settlement.feature b/TransactionProcessor.IntegrationTests/Settlement/Settlement.feature index 74767038..d268dfcd 100644 --- a/TransactionProcessor.IntegrationTests/Settlement/Settlement.feature +++ b/TransactionProcessor.IntegrationTests/Settlement/Settlement.feature @@ -46,8 +46,7 @@ Background: | EstateName | OperatorName | ContractDescription | ProductName | CalculationType | FeeDescription | Value | | Test Estate 1 | Safaricom | Safaricom Contract | Variable Topup | Fixed | Merchant Commission | 2.50 | -@PRTest -Scenario: Settlement Processing +Scenario: Get Pending Settlement Given I create the following merchants | MerchantName | AddressLine1 | Town | Region | Country | ContactName | EmailAddress | EstateName | SettlementSchedule | | Test Merchant 1 | Address Line 1 | TestTown | Test Region | United Kingdom | Test Contact 1 | testcontact1@merchant1.co.uk | Test Estate 1 | Immediate | @@ -96,10 +95,56 @@ Scenario: Settlement Processing | Test Estate 1 | Test Merchant 2 | 6 | 0000 | SUCCESS | | Test Estate 1 | Test Merchant 3 | 7 | 0000 | SUCCESS | | Test Estate 1 | Test Merchant 3 | 8 | 0000 | SUCCESS | - + When I get the pending settlements the following information should be returned | NextSettlementDate | EstateName | NumberOfFees | | NextWeek | Test Estate 1 | 1 | | NextMonth | Test Estate 1 | 1 | +@PRTest +Scenario: Process Settlement + Given I create the following merchants + | MerchantName | AddressLine1 | Town | Region | Country | ContactName | EmailAddress | EstateName | SettlementSchedule | + | Test Merchant 1 | Address Line 1 | TestTown | Test Region | United Kingdom | Test Contact 1 | testcontact1@merchant1.co.uk | Test Estate 1 | Immediate | + | Test Merchant 2 | Address Line 1 | TestTown | Test Region | United Kingdom | Test Contact 2 | testcontact2@merchant2.co.uk | Test Estate 1 | Weekly | + + Given I have assigned the following operator to the merchants + | OperatorName | MerchantName | MerchantNumber | TerminalNumber | EstateName | + | Safaricom | Test Merchant 1 | 00000001 | 10000001 | Test Estate 1 | + | Voucher | Test Merchant 1 | 00000001 | 10000001 | Test Estate 1 | + | Safaricom | Test Merchant 2 | 00000002 | 10000002 | Test Estate 1 | + | Voucher | Test Merchant 2 | 00000002 | 10000002 | Test Estate 1 | + + Given I have assigned the following devices to the merchants + | DeviceIdentifier | MerchantName | EstateName | + | 123456780 | Test Merchant 1 | Test Estate 1 | + | 123456781 | Test Merchant 2 | Test Estate 1 | + + Given I make the following manual merchant deposits + | Reference | Amount | DateTime | MerchantName | EstateName | + | Deposit1 | 210.00 | Today | Test Merchant 1 | Test Estate 1 | + | Deposit1 | 110.00 | Today | Test Merchant 2 | Test Estate 1 | + + When I perform the following transactions + | DateTime | TransactionNumber | TransactionType | MerchantName | DeviceIdentifier | EstateName | OperatorName | TransactionAmount | CustomerAccountNumber | CustomerEmailAddress | ContractDescription | ProductName | RecipientEmail | RecipientMobile | + | LastWeek | 1 | Sale | Test Merchant 1 | 123456780 | Test Estate 1 | Safaricom | 100.00 | 123456789 | | Safaricom Contract | Variable Topup | | | + | LastWeek | 2 | Sale | Test Merchant 2 | 123456781 | Test Estate 1 | Safaricom | 100.00 | 123456789 | | Safaricom Contract | Variable Topup | | | + | LastWeek | 4 | Sale | Test Merchant 1 | 123456780 | Test Estate 1 | Safaricom | 100.00 | 123456789 | testcustomer@customer.co.uk | Safaricom Contract | Variable Topup | | | + | LastWeek | 5 | Sale | Test Merchant 1 | 123456780 | Test Estate 1 | Voucher | 10.00 | | | Hospital 1 Contract | 10 KES | test@recipient.co.uk | | + | LastWeek | 6 | Sale | Test Merchant 2 | 123456781 | Test Estate 1 | Voucher | 10.00 | | | Hospital 1 Contract | 10 KES | | 123456789 | + + Then transaction response should contain the following information + | EstateName | MerchantName | TransactionNumber | ResponseCode | ResponseMessage | + | Test Estate 1 | Test Merchant 1 | 1 | 0000 | SUCCESS | + | Test Estate 1 | Test Merchant 2 | 2 | 0000 | SUCCESS | + | Test Estate 1 | Test Merchant 1 | 4 | 0000 | SUCCESS | + | Test Estate 1 | Test Merchant 1 | 5 | 0000 | SUCCESS | + | Test Estate 1 | Test Merchant 2 | 6 | 0000 | SUCCESS | + + When I get the pending settlements the following information should be returned + | NextSettlementDate | EstateName | NumberOfFees | + | NextWeek | Test Estate 1 | 1 | + + When I process the settlement for 'NextWeek' on Estate 'Test Estate 1' then 1 fees are marked as settled and the settlement is completed + diff --git a/TransactionProcessor.IntegrationTests/Settlement/Settlement.feature.cs b/TransactionProcessor.IntegrationTests/Settlement/Settlement.feature.cs index f7bc6a91..fc3e5cbc 100644 --- a/TransactionProcessor.IntegrationTests/Settlement/Settlement.feature.cs +++ b/TransactionProcessor.IntegrationTests/Settlement/Settlement.feature.cs @@ -244,16 +244,16 @@ void System.IDisposable.Dispose() this.TestTearDown(); } - [Xunit.SkippableFactAttribute(DisplayName="Settlement Processing")] + [Xunit.SkippableFactAttribute(DisplayName="Get Pending Settlement")] [Xunit.TraitAttribute("FeatureTitle", "Settlement")] - [Xunit.TraitAttribute("Description", "Settlement Processing")] + [Xunit.TraitAttribute("Description", "Get Pending Settlement")] [Xunit.TraitAttribute("Category", "PRTest")] - public virtual void SettlementProcessing() + public virtual void GetPendingSettlement() { string[] tagsOfScenario = new string[] { "PRTest"}; System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); - TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Settlement Processing", null, tagsOfScenario, argumentsOfScenario); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Get Pending Settlement", null, tagsOfScenario, argumentsOfScenario); #line 50 this.ScenarioInitialize(scenarioInfo); #line hidden @@ -618,13 +618,299 @@ public virtual void SettlementProcessing() "NextMonth", "Test Estate 1", "1"}); -#line 100 +#line 101 testRunner.When("I get the pending settlements the following information should be returned", ((string)(null)), table77, "When "); #line hidden } this.ScenarioCleanup(); } + [Xunit.SkippableFactAttribute(DisplayName="Process Settlement")] + [Xunit.TraitAttribute("FeatureTitle", "Settlement")] + [Xunit.TraitAttribute("Description", "Process Settlement")] + [Xunit.TraitAttribute("Category", "PRTest")] + public virtual void ProcessSettlement() + { + string[] tagsOfScenario = new string[] { + "PRTest"}; + System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Process Settlement", null, tagsOfScenario, argumentsOfScenario); +#line 107 +this.ScenarioInitialize(scenarioInfo); +#line hidden + bool isScenarioIgnored = default(bool); + bool isFeatureIgnored = default(bool); + if ((tagsOfScenario != null)) + { + isScenarioIgnored = tagsOfScenario.Where(__entry => __entry != null).Where(__entry => String.Equals(__entry, "ignore", StringComparison.CurrentCultureIgnoreCase)).Any(); + } + if ((this._featureTags != null)) + { + isFeatureIgnored = this._featureTags.Where(__entry => __entry != null).Where(__entry => String.Equals(__entry, "ignore", StringComparison.CurrentCultureIgnoreCase)).Any(); + } + if ((isScenarioIgnored || isFeatureIgnored)) + { + testRunner.SkipScenario(); + } + else + { + this.ScenarioStart(); +#line 4 +this.FeatureBackground(); +#line hidden + TechTalk.SpecFlow.Table table78 = new TechTalk.SpecFlow.Table(new string[] { + "MerchantName", + "AddressLine1", + "Town", + "Region", + "Country", + "ContactName", + "EmailAddress", + "EstateName", + "SettlementSchedule"}); + table78.AddRow(new string[] { + "Test Merchant 1", + "Address Line 1", + "TestTown", + "Test Region", + "United Kingdom", + "Test Contact 1", + "testcontact1@merchant1.co.uk", + "Test Estate 1", + "Immediate"}); + table78.AddRow(new string[] { + "Test Merchant 2", + "Address Line 1", + "TestTown", + "Test Region", + "United Kingdom", + "Test Contact 2", + "testcontact2@merchant2.co.uk", + "Test Estate 1", + "Weekly"}); +#line 108 + testRunner.Given("I create the following merchants", ((string)(null)), table78, "Given "); +#line hidden + TechTalk.SpecFlow.Table table79 = new TechTalk.SpecFlow.Table(new string[] { + "OperatorName", + "MerchantName", + "MerchantNumber", + "TerminalNumber", + "EstateName"}); + table79.AddRow(new string[] { + "Safaricom", + "Test Merchant 1", + "00000001", + "10000001", + "Test Estate 1"}); + table79.AddRow(new string[] { + "Voucher", + "Test Merchant 1", + "00000001", + "10000001", + "Test Estate 1"}); + table79.AddRow(new string[] { + "Safaricom", + "Test Merchant 2", + "00000002", + "10000002", + "Test Estate 1"}); + table79.AddRow(new string[] { + "Voucher", + "Test Merchant 2", + "00000002", + "10000002", + "Test Estate 1"}); +#line 113 + testRunner.Given("I have assigned the following operator to the merchants", ((string)(null)), table79, "Given "); +#line hidden + TechTalk.SpecFlow.Table table80 = new TechTalk.SpecFlow.Table(new string[] { + "DeviceIdentifier", + "MerchantName", + "EstateName"}); + table80.AddRow(new string[] { + "123456780", + "Test Merchant 1", + "Test Estate 1"}); + table80.AddRow(new string[] { + "123456781", + "Test Merchant 2", + "Test Estate 1"}); +#line 120 + testRunner.Given("I have assigned the following devices to the merchants", ((string)(null)), table80, "Given "); +#line hidden + TechTalk.SpecFlow.Table table81 = new TechTalk.SpecFlow.Table(new string[] { + "Reference", + "Amount", + "DateTime", + "MerchantName", + "EstateName"}); + table81.AddRow(new string[] { + "Deposit1", + "210.00", + "Today", + "Test Merchant 1", + "Test Estate 1"}); + table81.AddRow(new string[] { + "Deposit1", + "110.00", + "Today", + "Test Merchant 2", + "Test Estate 1"}); +#line 125 + testRunner.Given("I make the following manual merchant deposits", ((string)(null)), table81, "Given "); +#line hidden + TechTalk.SpecFlow.Table table82 = new TechTalk.SpecFlow.Table(new string[] { + "DateTime", + "TransactionNumber", + "TransactionType", + "MerchantName", + "DeviceIdentifier", + "EstateName", + "OperatorName", + "TransactionAmount", + "CustomerAccountNumber", + "CustomerEmailAddress", + "ContractDescription", + "ProductName", + "RecipientEmail", + "RecipientMobile"}); + table82.AddRow(new string[] { + "LastWeek", + "1", + "Sale", + "Test Merchant 1", + "123456780", + "Test Estate 1", + "Safaricom", + "100.00", + "123456789", + "", + "Safaricom Contract", + "Variable Topup", + "", + ""}); + table82.AddRow(new string[] { + "LastWeek", + "2", + "Sale", + "Test Merchant 2", + "123456781", + "Test Estate 1", + "Safaricom", + "100.00", + "123456789", + "", + "Safaricom Contract", + "Variable Topup", + "", + ""}); + table82.AddRow(new string[] { + "LastWeek", + "4", + "Sale", + "Test Merchant 1", + "123456780", + "Test Estate 1", + "Safaricom", + "100.00", + "123456789", + "testcustomer@customer.co.uk", + "Safaricom Contract", + "Variable Topup", + "", + ""}); + table82.AddRow(new string[] { + "LastWeek", + "5", + "Sale", + "Test Merchant 1", + "123456780", + "Test Estate 1", + "Voucher", + "10.00", + "", + "", + "Hospital 1 Contract", + "10 KES", + "test@recipient.co.uk", + ""}); + table82.AddRow(new string[] { + "LastWeek", + "6", + "Sale", + "Test Merchant 2", + "123456781", + "Test Estate 1", + "Voucher", + "10.00", + "", + "", + "Hospital 1 Contract", + "10 KES", + "", + "123456789"}); +#line 130 + testRunner.When("I perform the following transactions", ((string)(null)), table82, "When "); +#line hidden + TechTalk.SpecFlow.Table table83 = new TechTalk.SpecFlow.Table(new string[] { + "EstateName", + "MerchantName", + "TransactionNumber", + "ResponseCode", + "ResponseMessage"}); + table83.AddRow(new string[] { + "Test Estate 1", + "Test Merchant 1", + "1", + "0000", + "SUCCESS"}); + table83.AddRow(new string[] { + "Test Estate 1", + "Test Merchant 2", + "2", + "0000", + "SUCCESS"}); + table83.AddRow(new string[] { + "Test Estate 1", + "Test Merchant 1", + "4", + "0000", + "SUCCESS"}); + table83.AddRow(new string[] { + "Test Estate 1", + "Test Merchant 1", + "5", + "0000", + "SUCCESS"}); + table83.AddRow(new string[] { + "Test Estate 1", + "Test Merchant 2", + "6", + "0000", + "SUCCESS"}); +#line 138 + testRunner.Then("transaction response should contain the following information", ((string)(null)), table83, "Then "); +#line hidden + TechTalk.SpecFlow.Table table84 = new TechTalk.SpecFlow.Table(new string[] { + "NextSettlementDate", + "EstateName", + "NumberOfFees"}); + table84.AddRow(new string[] { + "NextWeek", + "Test Estate 1", + "1"}); +#line 146 + testRunner.When("I get the pending settlements the following information should be returned", ((string)(null)), table84, "When "); +#line hidden +#line 150 + testRunner.When("I process the settlement for \'NextWeek\' on Estate \'Test Estate 1\' then 1 fees are" + + " marked as settled and the settlement is completed", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); +#line hidden + } + this.ScenarioCleanup(); + } + [System.CodeDom.Compiler.GeneratedCodeAttribute("TechTalk.SpecFlow", "3.5.0.0")] [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] public class FixtureData : System.IDisposable diff --git a/TransactionProcessor.IntegrationTests/Shared/SharedSteps.cs b/TransactionProcessor.IntegrationTests/Shared/SharedSteps.cs index 20fbfdca..22b82b62 100644 --- a/TransactionProcessor.IntegrationTests/Shared/SharedSteps.cs +++ b/TransactionProcessor.IntegrationTests/Shared/SharedSteps.cs @@ -883,17 +883,41 @@ public async Task WhenIGetThePendingSettlementsTheFollowingInformationShouldBeRe await Retry.For(async () => { - PendingSettlementResponse pendingSettlements = - await this.TestingContext.DockerHelper.TransactionProcessorClient.GetPendingSettlementByDate(this.TestingContext.AccessToken, + SettlementResponse settlements = + await this.TestingContext.DockerHelper.TransactionProcessorClient.GetSettlementByDate(this.TestingContext.AccessToken, nextSettlementDate, estateDetails.EstateId, CancellationToken.None); - pendingSettlements.NumberOfFeesPendingSettlement.ShouldBe(numberOfFees); + settlements.NumberOfFeesPendingSettlement.ShouldBe(numberOfFees); }, TimeSpan.FromMinutes(3)); } } + [When(@"I process the settlement for '([^']*)' on Estate '([^']*)' then (.*) fees are marked as settled and the settlement is completed")] + public async Task WhenIProcessTheSettlementForOnEstateThenFeesAreMarkedAsSettledAndTheSettlementIsCompleted(String dateString, String estateName, Int32 numberOfFeesSettled) + { + var settlementDate = SpecflowTableHelper.GetDateForDateString(dateString, DateTime.Now); + EstateDetails estateDetails = this.TestingContext.GetEstateDetails(estateName); + await this.TestingContext.DockerHelper.TransactionProcessorClient.ProcessSettlement(this.TestingContext.AccessToken, + settlementDate, + estateDetails.EstateId, + CancellationToken.None); + + await Retry.For(async () => + { + SettlementResponse settlement = + await this.TestingContext.DockerHelper.TransactionProcessorClient.GetSettlementByDate(this.TestingContext.AccessToken, + settlementDate, + estateDetails.EstateId, + CancellationToken.None); + + settlement.NumberOfFeesPendingSettlement.ShouldBe(0); + settlement.NumberOfFeesSettled.ShouldBe(numberOfFeesSettled); + settlement.SettlementCompleted.ShouldBeTrue(); + }); + } + private DateTime GetNextSettlementDate(DateTime now, String nextSettlementDate) { diff --git a/TransactionProcessor.Models/ProcessSaleTransactionResponse.cs b/TransactionProcessor.Models/ProcessSaleTransactionResponse.cs index eec217c4..6f8ee362 100644 --- a/TransactionProcessor.Models/ProcessSaleTransactionResponse.cs +++ b/TransactionProcessor.Models/ProcessSaleTransactionResponse.cs @@ -61,4 +61,14 @@ public class ProcessSaleTransactionResponse #endregion } + + [ExcludeFromCodeCoverage] + public class ProcessSettlementResponse + { + public Int32 NumberOfFeesPendingSettlement { get; set; } + + public Int32 NumberOfFeesSuccessfullySettled { get; set; } + + public Int32 NumberOfFeesFailedToSettle { get; set; } + } } diff --git a/TransactionProcessor.Settlement.DomainEvents/MerchantFeeAddedToPendingSettlementEvent.cs b/TransactionProcessor.Settlement.DomainEvents/MerchantFeeAddedToPendingSettlementEvent.cs index e38b05fd..f1b19040 100644 --- a/TransactionProcessor.Settlement.DomainEvents/MerchantFeeAddedToPendingSettlementEvent.cs +++ b/TransactionProcessor.Settlement.DomainEvents/MerchantFeeAddedToPendingSettlementEvent.cs @@ -3,7 +3,7 @@ using System; using Shared.DomainDrivenDesign.EventSourcing; - public record MerchantFeeAddedToPendingSettlementEvent : DomainEventRecord.DomainEvent + public record MerchantFeeAddedPendingSettlementEvent : DomainEventRecord.DomainEvent { #region Properties @@ -71,17 +71,19 @@ public record MerchantFeeAddedToPendingSettlementEvent : DomainEventRecord.Domai /// public Guid TransactionId { get; init; } + public Guid SettlementId { get; init; } + #endregion - public MerchantFeeAddedToPendingSettlementEvent(Guid aggregateId, - Guid estateId, - Guid merchantId, - Guid transactionId, - Decimal calculatedValue, - Int32 feeCalculationType, - Guid feeId, - Decimal feeValue, - DateTime feeCalculatedDateTime) : base(aggregateId, Guid.NewGuid()) + public MerchantFeeAddedPendingSettlementEvent(Guid aggregateId, + Guid estateId, + Guid merchantId, + Guid transactionId, + Decimal calculatedValue, + Int32 feeCalculationType, + Guid feeId, + Decimal feeValue, + DateTime feeCalculatedDateTime) : base(aggregateId, Guid.NewGuid()) { this.EstateId = estateId; this.MerchantId = merchantId; @@ -91,6 +93,7 @@ public MerchantFeeAddedToPendingSettlementEvent(Guid aggregateId, this.FeeId = feeId; this.FeeValue = feeValue; this.FeeCalculatedDateTime = feeCalculatedDateTime; + this.SettlementId = aggregateId; } } } \ No newline at end of file diff --git a/TransactionProcessor.Settlement.DomainEvents/MerchantFeeSettledEvent.cs b/TransactionProcessor.Settlement.DomainEvents/MerchantFeeSettledEvent.cs new file mode 100644 index 00000000..896314bd --- /dev/null +++ b/TransactionProcessor.Settlement.DomainEvents/MerchantFeeSettledEvent.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TransactionProcessor.Settlement.DomainEvents +{ + using Shared.DomainDrivenDesign.EventSourcing; + + public record MerchantFeeSettledEvent : DomainEventRecord.DomainEvent + { + #region Properties + + /// + /// Gets the calculated value. + /// + /// + /// The calculated value. + /// + public Decimal CalculatedValue { get; init; } + + /// + /// Gets or sets the fee calculated date time. + /// + /// + /// The fee calculated date time. + /// + public DateTime FeeCalculatedDateTime { get; init; } + + /// + /// Gets the estate identifier. + /// + /// + /// The estate identifier. + /// + public Guid EstateId { get; init; } + + /// + /// Gets the type of the fee calculation. + /// + /// + /// The type of the fee calculation. + /// + public Int32 FeeCalculationType { get; init; } + + /// + /// Gets the fee identifier. + /// + /// + /// The fee identifier. + /// + public Guid FeeId { get; init; } + + /// + /// Gets the fee value. + /// + /// + /// The fee value. + /// + public Decimal FeeValue { get; init; } + + /// + /// Gets the merchant identifier. + /// + /// + /// The merchant identifier. + /// + public Guid MerchantId { get; init; } + + /// + /// Gets the transaction identifier. + /// + /// + /// The transaction identifier. + /// + public Guid TransactionId { get; init; } + + public Guid SettlementId { get; init; } + + #endregion + + public MerchantFeeSettledEvent(Guid aggregateId, + Guid estateId, + Guid merchantId, + Guid transactionId, + Decimal calculatedValue, + Int32 feeCalculationType, + Guid feeId, + Decimal feeValue, + DateTime feeCalculatedDateTime) : base(aggregateId, Guid.NewGuid()) + { + this.EstateId = estateId; + this.MerchantId = merchantId; + this.TransactionId = transactionId; + this.CalculatedValue = calculatedValue; + this.FeeCalculationType = feeCalculationType; + this.FeeId = feeId; + this.FeeValue = feeValue; + this.FeeCalculatedDateTime = feeCalculatedDateTime; + this.SettlementId = aggregateId; + } + } +} \ No newline at end of file diff --git a/TransactionProcessor.Settlement.DomainEvents/SettlementCompletedEvent.cs b/TransactionProcessor.Settlement.DomainEvents/SettlementCompletedEvent.cs new file mode 100644 index 00000000..da127dba --- /dev/null +++ b/TransactionProcessor.Settlement.DomainEvents/SettlementCompletedEvent.cs @@ -0,0 +1,29 @@ +namespace TransactionProcessor.Settlement.DomainEvents +{ + using System; + using Shared.DomainDrivenDesign.EventSourcing; + + public record SettlementCompletedEvent : DomainEventRecord.DomainEvent + { + #region Properties + + /// + /// Gets the estate identifier. + /// + /// + /// The estate identifier. + /// + public Guid EstateId { get; init; } + + public Guid SettlementId { get; init; } + + #endregion + + public SettlementCompletedEvent(Guid aggregateId, + Guid estateId) : base(aggregateId, Guid.NewGuid()) + { + this.EstateId = estateId; + this.SettlementId = aggregateId; + } + } +} \ No newline at end of file diff --git a/TransactionProcessor.Settlement.DomainEvents/PendingSettlementCreatedForDateEvent.cs b/TransactionProcessor.Settlement.DomainEvents/SettlementCreatedForDateEvent.cs similarity index 75% rename from TransactionProcessor.Settlement.DomainEvents/PendingSettlementCreatedForDateEvent.cs rename to TransactionProcessor.Settlement.DomainEvents/SettlementCreatedForDateEvent.cs index c1155b64..c6ba11c4 100644 --- a/TransactionProcessor.Settlement.DomainEvents/PendingSettlementCreatedForDateEvent.cs +++ b/TransactionProcessor.Settlement.DomainEvents/SettlementCreatedForDateEvent.cs @@ -3,7 +3,7 @@ using System; using Shared.DomainDrivenDesign.EventSourcing; - public record PendingSettlementCreatedForDateEvent : DomainEventRecord.DomainEvent + public record SettlementCreatedForDateEvent : DomainEventRecord.DomainEvent { #region Properties @@ -16,15 +16,18 @@ public record PendingSettlementCreatedForDateEvent : DomainEventRecord.DomainEve /// The estate identifier. /// public Guid EstateId { get; init; } - + + public Guid SettlementId { get; init; } + #endregion - public PendingSettlementCreatedForDateEvent(Guid aggregateId, + public SettlementCreatedForDateEvent(Guid aggregateId, Guid estateId, DateTime settlementDate) : base(aggregateId, Guid.NewGuid()) { this.EstateId = estateId; this.SettlementDate = settlementDate; + this.SettlementId = aggregateId; } } } \ No newline at end of file diff --git a/TransactionProcessor.SettlementAggregates.Tests/DomainEventTests.cs b/TransactionProcessor.SettlementAggregates.Tests/DomainEventTests.cs new file mode 100644 index 00000000..28f6412e --- /dev/null +++ b/TransactionProcessor.SettlementAggregates.Tests/DomainEventTests.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TransactionProcessor.SettlementAggregates.Tests +{ + using Models; + using Settlement.DomainEvents; + using Shouldly; + using Testing; + using Xunit; + + public class DomainEventTests + { + [Fact] + public void SettlementCreatedForDateEvent_CanBeCreated_IsCreated() + { + SettlementCreatedForDateEvent settlementCreatedForDateEvent = + new SettlementCreatedForDateEvent(TestData.SettlementAggregateId, TestData.EstateId, TestData.SettlementDate); + + settlementCreatedForDateEvent.ShouldNotBeNull(); + settlementCreatedForDateEvent.AggregateId.ShouldBe(TestData.SettlementAggregateId); + settlementCreatedForDateEvent.EventId.ShouldNotBe(Guid.Empty); + settlementCreatedForDateEvent.SettlementId.ShouldBe(TestData.SettlementAggregateId); + settlementCreatedForDateEvent.EstateId.ShouldBe(TestData.EstateId); + settlementCreatedForDateEvent.SettlementDate.ShouldBe(TestData.SettlementDate); + } + + [Fact] + public void MerchantFeeAddedPendingSettlementEvent_CanBeCreated_IsCreated() + { + MerchantFeeAddedPendingSettlementEvent merchantFeeAddedPendingSettlementEvent = + new MerchantFeeAddedPendingSettlementEvent(TestData.SettlementAggregateId, + TestData.EstateId, + TestData.MerchantId, + TestData.TransactionId, + TestData.CalculatedFeeValue, + (Int32)CalculationType.Fixed, + TestData.TransactionFeeId, + TestData.TransactionFeeValue, + TestData.TransactionFeeCalculateDateTime); + + merchantFeeAddedPendingSettlementEvent.ShouldNotBeNull(); + merchantFeeAddedPendingSettlementEvent.AggregateId.ShouldBe(TestData.SettlementAggregateId); + merchantFeeAddedPendingSettlementEvent.EventId.ShouldNotBe(Guid.Empty); + merchantFeeAddedPendingSettlementEvent.SettlementId.ShouldBe(TestData.SettlementAggregateId); + merchantFeeAddedPendingSettlementEvent.EstateId.ShouldBe(TestData.EstateId); + merchantFeeAddedPendingSettlementEvent.MerchantId.ShouldBe(TestData.MerchantId); + merchantFeeAddedPendingSettlementEvent.TransactionId.ShouldBe(TestData.TransactionId); + merchantFeeAddedPendingSettlementEvent.CalculatedValue.ShouldBe(TestData.CalculatedFeeValue); + merchantFeeAddedPendingSettlementEvent.FeeCalculationType.ShouldBe((Int32)CalculationType.Fixed); + merchantFeeAddedPendingSettlementEvent.FeeId.ShouldBe(TestData.TransactionFeeId); + merchantFeeAddedPendingSettlementEvent.FeeValue.ShouldBe(TestData.TransactionFeeValue); + merchantFeeAddedPendingSettlementEvent.FeeCalculatedDateTime.ShouldBe(TestData.TransactionFeeCalculateDateTime); + } + + [Fact] + public void MerchantFeeSettledEvent_CanBeCreated_IsCreated() + { + MerchantFeeSettledEvent merchantFeeSettledEvent = + new MerchantFeeSettledEvent(TestData.SettlementAggregateId, TestData.EstateId, TestData.MerchantId, + TestData.TransactionId, TestData.CalculatedFeeValue, + (Int32)CalculationType.Fixed, + TestData.TransactionFeeId, + TestData.TransactionFeeValue, + TestData.TransactionFeeCalculateDateTime); + + merchantFeeSettledEvent.ShouldNotBeNull(); + merchantFeeSettledEvent.AggregateId.ShouldBe(TestData.SettlementAggregateId); + merchantFeeSettledEvent.EventId.ShouldNotBe(Guid.Empty); + merchantFeeSettledEvent.SettlementId.ShouldBe(TestData.SettlementAggregateId); + merchantFeeSettledEvent.EstateId.ShouldBe(TestData.EstateId); + merchantFeeSettledEvent.MerchantId.ShouldBe(TestData.MerchantId); + merchantFeeSettledEvent.TransactionId.ShouldBe(TestData.TransactionId); + merchantFeeSettledEvent.CalculatedValue.ShouldBe(TestData.CalculatedFeeValue); + merchantFeeSettledEvent.FeeCalculationType.ShouldBe((Int32)CalculationType.Fixed); + merchantFeeSettledEvent.FeeId.ShouldBe(TestData.TransactionFeeId); + merchantFeeSettledEvent.FeeValue.ShouldBe(TestData.TransactionFeeValue); + merchantFeeSettledEvent.FeeCalculatedDateTime.ShouldBe(TestData.TransactionFeeCalculateDateTime); + } + + [Fact] + public void SettlementCompletedEvent_CanBeCreated_IsCreated() + { + SettlementCompletedEvent settlementCompletedEvent = + new SettlementCompletedEvent(TestData.SettlementAggregateId, TestData.EstateId); + + settlementCompletedEvent.ShouldNotBeNull(); + settlementCompletedEvent.AggregateId.ShouldBe(TestData.SettlementAggregateId); + settlementCompletedEvent.EventId.ShouldNotBe(Guid.Empty); + settlementCompletedEvent.SettlementId.ShouldBe(TestData.SettlementAggregateId); + settlementCompletedEvent.EstateId.ShouldBe(TestData.EstateId); + } + } +} diff --git a/TransactionProcessor.SettlementAggregates.Tests/PendingSettlementAggregateTests.cs b/TransactionProcessor.SettlementAggregates.Tests/PendingSettlementAggregateTests.cs deleted file mode 100644 index 48aa66ff..00000000 --- a/TransactionProcessor.SettlementAggregates.Tests/PendingSettlementAggregateTests.cs +++ /dev/null @@ -1,104 +0,0 @@ -namespace TransactionProcessor.SettlementAggregates.Tests -{ - using System; - using Shouldly; - using Testing; - using TransactionAggregate; - using Xunit; - - public class PendingSettlementAggregateTests - { - [Fact] - public void PendingSettlementAggregate_CanBeCreated_IsCreated() - { - PendingSettlementAggregate aggregate = PendingSettlementAggregate.Create(TestData.PendingSettlementAggregateId); - - aggregate.AggregateId.ShouldBe(TestData.PendingSettlementAggregateId); - } - - [Fact] - public void PendingSettlementAggregate_Create_AggregateIsCreated() - { - PendingSettlementAggregate aggregate = PendingSettlementAggregate.Create(TestData.PendingSettlementAggregateId); - aggregate.Create(TestData.EstateId, TestData.SettlementDate); - aggregate.EstateId.ShouldBe(TestData.EstateId); - aggregate.SettlmentDate.ShouldBe(TestData.SettlementDate.Date); - aggregate.IsCreated.ShouldBeTrue(); - } - - [Fact] - public void PendingSettlementAggregate_Create_AlreadyCreated_ErrorThrown() - { - PendingSettlementAggregate aggregate = PendingSettlementAggregate.Create(TestData.PendingSettlementAggregateId); - aggregate.Create(TestData.EstateId, TestData.SettlementDate); - - Should.Throw(() => - { - aggregate.Create(TestData.EstateId, TestData.SettlementDate); - }); - } - - [Fact] - public void PendingSettlementAggregate_AddFee_FeeIsAdded() - { - PendingSettlementAggregate aggregate = PendingSettlementAggregate.Create(TestData.PendingSettlementAggregateId); - aggregate.Create(TestData.EstateId, TestData.SettlementDate); - aggregate.AddFee(TestData.MerchantId, TestData.TransactionId, TestData.CalculatedFeeMerchantFee); - - aggregate.AggregateId.ShouldBe(TestData.PendingSettlementAggregateId); - aggregate.GetNumberOfFeesPendingSettlement().ShouldBe(1); - } - - [Fact] - public void PendingSettlementAggregate_AddFee_TwoFeesAdded_FeesAreAdded() - { - PendingSettlementAggregate aggregate = PendingSettlementAggregate.Create(TestData.PendingSettlementAggregateId); - aggregate.Create(TestData.EstateId, TestData.SettlementDate); - aggregate.AddFee(TestData.MerchantId, TestData.TransactionId, TestData.CalculatedFeeMerchantFee); - aggregate.AddFee(TestData.MerchantId, TestData.TransactionId, TestData.CalculatedFeeMerchantFee2); - - aggregate.AggregateId.ShouldBe(TestData.PendingSettlementAggregateId); - aggregate.GetNumberOfFeesPendingSettlement().ShouldBe(2); - } - - [Fact] - public void PendingSettlementAggregate_AddFee_TwoFeesAdded_SameFeeIdDifferentTransaction_FeesAreAdded() - { - PendingSettlementAggregate aggregate = PendingSettlementAggregate.Create(TestData.PendingSettlementAggregateId); - aggregate.Create(TestData.EstateId, TestData.SettlementDate); - aggregate.AddFee(TestData.MerchantId, TestData.TransactionId, TestData.CalculatedFeeMerchantFee); - aggregate.AddFee(TestData.MerchantId, TestData.TransactionId2, TestData.CalculatedFeeMerchantFee); - - aggregate.AggregateId.ShouldBe(TestData.PendingSettlementAggregateId); - aggregate.GetNumberOfFeesPendingSettlement().ShouldBe(2); - } - - [Fact] - public void PendingSettlementAggregate_AddFee_AggregateNotCreated_ErrorThrown() - { - PendingSettlementAggregate aggregate = PendingSettlementAggregate.Create(TestData.PendingSettlementAggregateId); - - Should.Throw(() => - { - aggregate.AddFee(TestData.MerchantId, - TestData.TransactionId, - TestData.CalculatedFeeMerchantFee); - }); - } - - [Fact] - public void PendingSettlementAggregate_AddFee_DuplicateFee_ErrorThrown() - { - PendingSettlementAggregate aggregate = PendingSettlementAggregate.Create(TestData.PendingSettlementAggregateId); - aggregate.Create(TestData.EstateId, TestData.SettlementDate); - aggregate.AddFee(TestData.MerchantId, TestData.TransactionId, TestData.CalculatedFeeMerchantFee); - - Should.Throw(() => - { - aggregate.AddFee(TestData.MerchantId, - TestData.TransactionId, - TestData.CalculatedFeeMerchantFee); - }); - } - } -} \ No newline at end of file diff --git a/TransactionProcessor.SettlementAggregates.Tests/SettlementAggregateTests.cs b/TransactionProcessor.SettlementAggregates.Tests/SettlementAggregateTests.cs new file mode 100644 index 00000000..fe97c930 --- /dev/null +++ b/TransactionProcessor.SettlementAggregates.Tests/SettlementAggregateTests.cs @@ -0,0 +1,179 @@ +namespace TransactionProcessor.SettlementAggregates.Tests +{ + using System; + using Shouldly; + using Testing; + using TransactionAggregate; + using Xunit; + + public class SettlementAggregateTests + { + [Fact] + public void SettlementAggregate_CanBeCreated_IsCreated() + { + SettlementAggregate aggregate = SettlementAggregate.Create(TestData.SettlementAggregateId); + + aggregate.AggregateId.ShouldBe(TestData.SettlementAggregateId); + } + + [Fact] + public void PendingSettlementAggregate_Create_AggregateIsCreated() + { + SettlementAggregate aggregate = SettlementAggregate.Create(TestData.SettlementAggregateId); + aggregate.Create(TestData.EstateId, TestData.SettlementDate); + aggregate.EstateId.ShouldBe(TestData.EstateId); + aggregate.SettlementDate.ShouldBe(TestData.SettlementDate.Date); + aggregate.IsCreated.ShouldBeTrue(); + aggregate.SettlementComplete.ShouldBeFalse(); + } + + [Fact] + public void SettlementAggregate_Create_AlreadyCreated_ErrorThrown() + { + SettlementAggregate aggregate = SettlementAggregate.Create(TestData.SettlementAggregateId); + aggregate.Create(TestData.EstateId, TestData.SettlementDate); + + Should.Throw(() => + { + aggregate.Create(TestData.EstateId, TestData.SettlementDate); + }); + } + + [Fact] + public void SettlementAggregate_AddFee_FeeIsAdded() + { + SettlementAggregate aggregate = SettlementAggregate.Create(TestData.SettlementAggregateId); + aggregate.Create(TestData.EstateId, TestData.SettlementDate); + aggregate.AddFee(TestData.MerchantId, TestData.TransactionId, TestData.CalculatedFeeMerchantFee()); + + aggregate.AggregateId.ShouldBe(TestData.SettlementAggregateId); + aggregate.GetNumberOfFeesPendingSettlement().ShouldBe(1); + } + + [Fact] + public void SettlementAggregate_AddFee_TwoFeesAdded_FeesAreAdded() + { + SettlementAggregate aggregate = SettlementAggregate.Create(TestData.SettlementAggregateId); + aggregate.Create(TestData.EstateId, TestData.SettlementDate); + aggregate.AddFee(TestData.MerchantId, TestData.TransactionId, TestData.CalculatedFeeMerchantFee()); + aggregate.AddFee(TestData.MerchantId, TestData.TransactionId, TestData.CalculatedFeeMerchantFee2); + + aggregate.AggregateId.ShouldBe(TestData.SettlementAggregateId); + aggregate.GetNumberOfFeesPendingSettlement().ShouldBe(2); + } + + [Fact] + public void SettlementAggregate_AddFee_TwoFeesAdded_SameFeeIdDifferentTransaction_FeesAreAdded() + { + SettlementAggregate aggregate = SettlementAggregate.Create(TestData.SettlementAggregateId); + aggregate.Create(TestData.EstateId, TestData.SettlementDate); + aggregate.AddFee(TestData.MerchantId, TestData.TransactionId, TestData.CalculatedFeeMerchantFee()); + aggregate.AddFee(TestData.MerchantId, TestData.TransactionId2, TestData.CalculatedFeeMerchantFee()); + + aggregate.AggregateId.ShouldBe(TestData.SettlementAggregateId); + aggregate.GetNumberOfFeesPendingSettlement().ShouldBe(2); + } + + [Fact] + public void SettlementAggregate_AddFee_AggregateNotCreated_ErrorThrown() + { + SettlementAggregate aggregate = SettlementAggregate.Create(TestData.SettlementAggregateId); + + Should.Throw(() => + { + aggregate.AddFee(TestData.MerchantId, + TestData.TransactionId, + TestData.CalculatedFeeMerchantFee()); + }); + } + + [Fact] + public void SettlementAggregate_AddFee_DuplicateFee_ErrorThrown() + { + SettlementAggregate aggregate = SettlementAggregate.Create(TestData.SettlementAggregateId); + aggregate.Create(TestData.EstateId, TestData.SettlementDate); + aggregate.AddFee(TestData.MerchantId, TestData.TransactionId, TestData.CalculatedFeeMerchantFee()); + + Should.Throw(() => + { + aggregate.AddFee(TestData.MerchantId, + TestData.TransactionId, + TestData.CalculatedFeeMerchantFee()); + }); + } + + [Fact] + public void SettlementAggregate_AddFee_InvalidFeeType_ErrorThrown() + { + SettlementAggregate aggregate = SettlementAggregate.Create(TestData.SettlementAggregateId); + aggregate.Create(TestData.EstateId, TestData.SettlementDate); + Should.Throw(() => + { + aggregate.AddFee(TestData.MerchantId, TestData.TransactionId, TestData.CalculatedFeeServiceProviderFee); + }); + } + + [Fact] + public void SettlementAggregate_MarkFeeAsSettled_FeeIsSettledAndSettlementCompleted() + { + SettlementAggregate aggregate = SettlementAggregate.Create(TestData.SettlementAggregateId); + aggregate.Create(TestData.EstateId, TestData.SettlementDate); + aggregate.AddFee(TestData.MerchantId, TestData.TransactionId, TestData.CalculatedFeeMerchantFee()); + + aggregate.GetNumberOfFeesPendingSettlement().ShouldBe(1); + + aggregate.MarkFeeAsSettled(TestData.MerchantId, TestData.TransactionId, TestData.CalculatedFeeMerchantFee().FeeId); + + aggregate.GetNumberOfFeesPendingSettlement().ShouldBe(0); + aggregate.GetNumberOfFeesSettled().ShouldBe(1); + aggregate.SettlementComplete.ShouldBeTrue(); + } + + [Fact] + public void SettlementAggregate_MarkFeeAsSettled_FeeIsSettled_SettlementNotCompleted() + { + SettlementAggregate aggregate = SettlementAggregate.Create(TestData.SettlementAggregateId); + aggregate.Create(TestData.EstateId, TestData.SettlementDate); + aggregate.AddFee(TestData.MerchantId, TestData.TransactionId, TestData.CalculatedFeeMerchantFee()); + aggregate.AddFee(TestData.MerchantId, TestData.TransactionId, TestData.CalculatedFeeMerchantFee2); + + aggregate.GetNumberOfFeesPendingSettlement().ShouldBe(2); + + aggregate.MarkFeeAsSettled(TestData.MerchantId, TestData.TransactionId, TestData.CalculatedFeeMerchantFee().FeeId); + + aggregate.GetNumberOfFeesPendingSettlement().ShouldBe(1); + aggregate.GetNumberOfFeesSettled().ShouldBe(1); + aggregate.SettlementComplete.ShouldBeFalse(); + } + + [Fact] + public void SettlementAggregate_MarkFeeAsSettled_PendingFeeNotFound_NoErrorThrown() + { + SettlementAggregate aggregate = SettlementAggregate.Create(TestData.SettlementAggregateId); + aggregate.Create(TestData.EstateId, TestData.SettlementDate); + + aggregate.MarkFeeAsSettled(TestData.MerchantId, TestData.TransactionId, TestData.CalculatedFeeMerchantFee().FeeId); + + aggregate.GetNumberOfFeesPendingSettlement().ShouldBe(0); + aggregate.GetNumberOfFeesSettled().ShouldBe(0); + } + + [Fact] + public void SettlementAggregate_MarkFeeAsSettled_FeeAlreadySettled_NoErrorThrown() + { + SettlementAggregate aggregate = SettlementAggregate.Create(TestData.SettlementAggregateId); + aggregate.Create(TestData.EstateId, TestData.SettlementDate); + aggregate.AddFee(TestData.MerchantId, TestData.TransactionId, TestData.CalculatedFeeMerchantFee()); + + aggregate.GetNumberOfFeesPendingSettlement().ShouldBe(1); + + aggregate.MarkFeeAsSettled(TestData.MerchantId, TestData.TransactionId, TestData.CalculatedFeeMerchantFee().FeeId); + + aggregate.GetNumberOfFeesPendingSettlement().ShouldBe(0); + aggregate.GetNumberOfFeesSettled().ShouldBe(1); + + aggregate.MarkFeeAsSettled(TestData.MerchantId, TestData.TransactionId, TestData.CalculatedFeeMerchantFee().FeeId); + } + + } +} \ No newline at end of file diff --git a/TransactionProcessor.SettlementAggregates.Tests/TransactionProcessor.SettlementAggregates.Tests.csproj b/TransactionProcessor.SettlementAggregates.Tests/TransactionProcessor.SettlementAggregates.Tests.csproj index 115026b2..d8ea5677 100644 --- a/TransactionProcessor.SettlementAggregates.Tests/TransactionProcessor.SettlementAggregates.Tests.csproj +++ b/TransactionProcessor.SettlementAggregates.Tests/TransactionProcessor.SettlementAggregates.Tests.csproj @@ -2,7 +2,7 @@ net5.0 - + None false diff --git a/TransactionProcessor.SettlementAggregates/PendingSettlementAggregate.cs b/TransactionProcessor.SettlementAggregates/PendingSettlementAggregate.cs deleted file mode 100644 index dda0150b..00000000 --- a/TransactionProcessor.SettlementAggregates/PendingSettlementAggregate.cs +++ /dev/null @@ -1,186 +0,0 @@ -namespace TransactionProcessor.SettlementAggregates -{ - using System; - using System.Collections.Generic; - using System.Diagnostics.CodeAnalysis; - using System.Linq; - using Models; - using Settlement.DomainEvents; - using Shared.DomainDrivenDesign.EventSourcing; - using Shared.EventStore.Aggregate; - using Shared.General; - - public class PendingSettlementAggregate : Aggregate - { - #region Fields - - /// - /// The calculated fees - /// - private readonly List<(Guid transactionId, CalculatedFee calculatedFee)> CalculatedFeesPendingSettlement; - - private readonly List<(Guid transactionId, CalculatedFee calculatedFee)> SettledCalculatedFees; - - #endregion - - #region Constructors - - /// - /// Initializes a new instance of the class. - /// - [ExcludeFromCodeCoverage] - public PendingSettlementAggregate() - { - this.CalculatedFeesPendingSettlement = new List<(Guid transactionId, CalculatedFee calculatedFee)>(); - this.SettledCalculatedFees = new List<(Guid transactionId, CalculatedFee calculatedFee)>(); - } - - /// - /// Initializes a new instance of the class. - /// - /// The aggregate identifier. - private PendingSettlementAggregate(Guid aggregateId) - { - Guard.ThrowIfInvalidGuid(aggregateId, "Aggregate Id cannot be an Empty Guid"); - - this.AggregateId = aggregateId; - this.CalculatedFeesPendingSettlement = new List<(Guid transactionId, CalculatedFee calculatedFee)>(); - this.SettledCalculatedFees = new List<(Guid transactionId, CalculatedFee calculatedFee)>(); - } - - #endregion - - #region Properties - - public Guid EstateId { get; private set; } - - public Boolean IsCreated { get; private set; } - - public DateTime SettlmentDate { get; private set; } - - #endregion - - #region Methods - - public void AddFee(Guid merchantId, - Guid transactionId, - CalculatedFee calculatedFee) - { - if (calculatedFee == null) - { - throw new ArgumentNullException(nameof(calculatedFee)); - } - - this.CheckHasBeenCreated(); - this.CheckFeeHasNotAlreadyBeenAdded(transactionId, calculatedFee); - - DomainEventRecord.DomainEvent @event = null; - if (calculatedFee.FeeType == FeeType.Merchant) - { - // This is a merchant fee - @event = new MerchantFeeAddedToPendingSettlementEvent(this.AggregateId, - this.EstateId, - merchantId, - transactionId, - calculatedFee.CalculatedValue, - (Int32)calculatedFee.FeeCalculationType, - calculatedFee.FeeId, - calculatedFee.FeeValue, - calculatedFee.FeeCalculatedDateTime); - } - else - { - throw new InvalidOperationException("Unsupported Fee Type"); - } - - this.ApplyAndAppend(@event); - } - - /// - /// Creates the specified aggregate identifier. - /// - /// The aggregate identifier. - /// - public static PendingSettlementAggregate Create(Guid aggregateId) - { - return new PendingSettlementAggregate(aggregateId); - } - - public void Create(Guid estateId, - DateTime settlementDate) - { - this.CheckHasNotAlreadyBeenCreated(); - - PendingSettlementCreatedForDateEvent pendingSettlementCreatedForDateEvent = - new PendingSettlementCreatedForDateEvent(this.AggregateId, estateId, settlementDate.Date); - - this.ApplyAndAppend(pendingSettlementCreatedForDateEvent); - } - - public Int32 GetNumberOfFeesPendingSettlement() - { - return this.CalculatedFeesPendingSettlement.Count; - } - - public Int32 GetNumberOfFeesSettled() - { - return this.SettledCalculatedFees.Count; - } - - public override void PlayEvent(IDomainEvent domainEvent) - { - this.PlayEvent((dynamic)domainEvent); - } - - protected override Object GetMetadata() - { - return null; - } - - private void CheckFeeHasNotAlreadyBeenAdded(Guid transactionId, CalculatedFee calculatedFee) - { - if (this.CalculatedFeesPendingSettlement.Any(c => c.calculatedFee.FeeId == calculatedFee.FeeId && c.transactionId == transactionId)) - { - throw new InvalidOperationException($"Fee with Id [{calculatedFee.FeeId}] for Transaction Id [{transactionId}] has already been added to this days pending settlement"); - } - } - - private void CheckHasBeenCreated() - { - if (this.IsCreated == false) - { - throw new InvalidOperationException($"Pending Settlement not created for this date {this.SettlmentDate}"); - } - } - - private void CheckHasNotAlreadyBeenCreated() - { - if (this.IsCreated) - { - throw new InvalidOperationException($"Pending Settlement already created for this date {this.SettlmentDate}"); - } - } - - private void PlayEvent(MerchantFeeAddedToPendingSettlementEvent domainEvent) - { - this.CalculatedFeesPendingSettlement.Add(new(domainEvent.TransactionId, - new CalculatedFee - { - CalculatedValue = domainEvent.CalculatedValue, - FeeId = domainEvent.FeeId, - FeeType = FeeType.Merchant, - FeeValue = domainEvent.FeeValue, - FeeCalculationType = (CalculationType)domainEvent.FeeCalculationType - })); - } - - private void PlayEvent(PendingSettlementCreatedForDateEvent domainEvent) - { - this.EstateId = domainEvent.EstateId; - this.SettlmentDate = domainEvent.SettlementDate; - this.IsCreated = true; - } - - #endregion - } -} \ No newline at end of file diff --git a/TransactionProcessor.SettlementAggregates/SettlementAggregate.cs b/TransactionProcessor.SettlementAggregates/SettlementAggregate.cs new file mode 100644 index 00000000..997a5880 --- /dev/null +++ b/TransactionProcessor.SettlementAggregates/SettlementAggregate.cs @@ -0,0 +1,260 @@ +namespace TransactionProcessor.SettlementAggregates +{ + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Linq; + using Models; + using Settlement.DomainEvents; + using Shared.DomainDrivenDesign.EventSourcing; + using Shared.EventStore.Aggregate; + using Shared.General; + + public class SettlementAggregate : Aggregate + { + #region Fields + + /// + /// The calculated fees + /// + private readonly List<(Guid transactionId, Guid merchantId, CalculatedFee calculatedFee)> CalculatedFeesPendingSettlement; + + private readonly List<(Guid transactionId, Guid merchantId, CalculatedFee calculatedFee)> SettledCalculatedFees; + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + [ExcludeFromCodeCoverage] + public SettlementAggregate() + { + this.CalculatedFeesPendingSettlement = new List<(Guid transactionId, Guid merchantId, CalculatedFee calculatedFee)>(); + this.SettledCalculatedFees = new List<(Guid transactionId, Guid merchantId, CalculatedFee calculatedFee)>(); + } + + /// + /// Initializes a new instance of the class. + /// + /// The aggregate identifier. + private SettlementAggregate(Guid aggregateId) + { + Guard.ThrowIfInvalidGuid(aggregateId, "Aggregate Id cannot be an Empty Guid"); + + this.AggregateId = aggregateId; + this.CalculatedFeesPendingSettlement = new List<(Guid transactionId, Guid merchantId, CalculatedFee calculatedFee)>(); + this.SettledCalculatedFees = new List<(Guid transactionId, Guid merchantId, CalculatedFee calculatedFee)>(); + } + + #endregion + + #region Properties + + public Guid EstateId { get; private set; } + + public Boolean IsCreated { get; private set; } + + public DateTime SettlementDate { get; private set; } + + public Boolean SettlementComplete { get; private set; } + + #endregion + + #region Methods + + public void MarkFeeAsSettled(Guid merchantId, Guid transactionId, Guid feeId) + { + var pendingFee = this.CalculatedFeesPendingSettlement.Where(c => c.merchantId == merchantId && c.transactionId == transactionId && c.calculatedFee.FeeId == feeId) + .SingleOrDefault(); + + var settledFee = this.SettledCalculatedFees.Where(c => c.merchantId == merchantId && c.transactionId == transactionId && c.calculatedFee.FeeId == feeId) + .SingleOrDefault(); + + if (settledFee!= default((Guid, Guid, CalculatedFee))) + { + // Fee already settled.... + return; + } + + if (pendingFee == default((Guid, Guid, CalculatedFee))) + { + // Fee not found.... + return; + } + + MerchantFeeSettledEvent merchantFeeSettledEvent = new MerchantFeeSettledEvent(this.AggregateId, + this.EstateId, + pendingFee.merchantId, + pendingFee.transactionId, + pendingFee.calculatedFee.CalculatedValue, + (Int32)pendingFee.calculatedFee.FeeCalculationType, + pendingFee.calculatedFee.FeeId, + pendingFee.calculatedFee.FeeValue, + pendingFee.calculatedFee.FeeCalculatedDateTime); + + this.ApplyAndAppend(merchantFeeSettledEvent); + + if (this.CalculatedFeesPendingSettlement.Any() == false) + { + // Settlement is completed + SettlementCompletedEvent pendingSettlementCompletedEvent = new SettlementCompletedEvent(this.AggregateId, this.EstateId); + this.ApplyAndAppend(pendingSettlementCompletedEvent); + } + } + + public void AddFee(Guid merchantId, + Guid transactionId, + CalculatedFee calculatedFee) + { + Guard.ThrowIfInvalidGuid(merchantId, nameof(merchantId)); + Guard.ThrowIfInvalidGuid(transactionId, nameof(merchantId)); + Guard.ThrowIfNull(calculatedFee, nameof(calculatedFee)); + + this.CheckHasBeenCreated(); + this.CheckFeeHasNotAlreadyBeenAdded(transactionId, calculatedFee); + + DomainEventRecord.DomainEvent @event = null; + if (calculatedFee.FeeType == FeeType.Merchant) + { + // This is a merchant fee + @event = new MerchantFeeAddedPendingSettlementEvent(this.AggregateId, + this.EstateId, + merchantId, + transactionId, + calculatedFee.CalculatedValue, + (Int32)calculatedFee.FeeCalculationType, + calculatedFee.FeeId, + calculatedFee.FeeValue, + calculatedFee.FeeCalculatedDateTime); + } + else + { + throw new InvalidOperationException("Unsupported Fee Type"); + } + + this.ApplyAndAppend(@event); + } + + public List<(Guid transactionId, Guid merchantId, CalculatedFee calculatedFee)> GetFeesToBeSettled() + { + return this.CalculatedFeesPendingSettlement; + } + + /// + /// Creates the specified aggregate identifier. + /// + /// The aggregate identifier. + /// + public static SettlementAggregate Create(Guid aggregateId) + { + return new SettlementAggregate(aggregateId); + } + + public void Create(Guid estateId, + DateTime settlementDate) + { + this.CheckHasNotAlreadyBeenCreated(); + + SettlementCreatedForDateEvent pendingSettlementCreatedForDateEvent = + new SettlementCreatedForDateEvent(this.AggregateId, estateId, settlementDate.Date); + + this.ApplyAndAppend(pendingSettlementCreatedForDateEvent); + } + + public Int32 GetNumberOfFeesPendingSettlement() + { + return this.CalculatedFeesPendingSettlement.Count; + } + + public Int32 GetNumberOfFeesSettled() + { + return this.SettledCalculatedFees.Count; + } + + public override void PlayEvent(IDomainEvent domainEvent) + { + this.PlayEvent((dynamic)domainEvent); + } + + protected override Object GetMetadata() + { + return null; + } + + private void CheckFeeHasNotAlreadyBeenAdded(Guid transactionId, CalculatedFee calculatedFee) + { + if (this.CalculatedFeesPendingSettlement.Any(c => c.calculatedFee.FeeId == calculatedFee.FeeId && c.transactionId == transactionId)) + { + throw new InvalidOperationException($"Fee with Id [{calculatedFee.FeeId}] for Transaction Id [{transactionId}] has already been added to this days pending settlement"); + } + } + + private void CheckHasBeenCreated() + { + if (this.IsCreated == false) + { + throw new InvalidOperationException($"Pending Settlement not created for this date {this.SettlementDate}"); + } + } + + private void CheckHasNotAlreadyBeenCreated() + { + if (this.IsCreated) + { + throw new InvalidOperationException($"Pending Settlement already created for this date {this.SettlementDate}"); + } + } + + private void PlayEvent(MerchantFeeSettledEvent domainEvent) + { + // Add to the settled fees list + this.SettledCalculatedFees.Add(new(domainEvent.TransactionId, + domainEvent.MerchantId, + new CalculatedFee + { + CalculatedValue = domainEvent.CalculatedValue, + FeeId = domainEvent.FeeId, + FeeType = FeeType.Merchant, + FeeValue = domainEvent.FeeValue, + FeeCalculationType = (CalculationType)domainEvent.FeeCalculationType + })); + + // Remove from the pending list + var feeToRemove = this.CalculatedFeesPendingSettlement + .Single(f => f.transactionId == domainEvent.TransactionId && + f.merchantId == domainEvent.MerchantId && f.calculatedFee.FeeId == domainEvent.FeeId); + + this.CalculatedFeesPendingSettlement.Remove(feeToRemove); + } + + private void PlayEvent(MerchantFeeAddedPendingSettlementEvent domainEvent) + { + this.CalculatedFeesPendingSettlement.Add(new(domainEvent.TransactionId, + domainEvent.MerchantId, + new CalculatedFee + { + CalculatedValue = domainEvent.CalculatedValue, + FeeId = domainEvent.FeeId, + FeeType = FeeType.Merchant, + FeeValue = domainEvent.FeeValue, + FeeCalculationType = (CalculationType)domainEvent.FeeCalculationType + })); + } + + private void PlayEvent(SettlementCreatedForDateEvent domainEvent) + { + this.EstateId = domainEvent.EstateId; + this.SettlementDate = domainEvent.SettlementDate; + this.IsCreated = true; + } + + private void PlayEvent(SettlementCompletedEvent domainEvent) + { + this.SettlementComplete = true; + } + + #endregion + } +} \ No newline at end of file diff --git a/TransactionProcessor.Testing/TestData.cs b/TransactionProcessor.Testing/TestData.cs index 49a2c382..cfbdb7ad 100644 --- a/TransactionProcessor.Testing/TestData.cs +++ b/TransactionProcessor.Testing/TestData.cs @@ -12,6 +12,7 @@ using Models; using ReconciliationAggregate; using SecurityService.DataTransferObjects.Responses; + using SettlementAggregates; using Transaction.DomainEvents; using TransactionAggregate; using VoucherManagement.DataTransferObjects; @@ -74,7 +75,7 @@ public class TestData public static Guid TransactionId2 = Guid.Parse("760E702C-682E-41B1-A582-3D4ECA0F38C3"); - public static Guid PendingSettlementAggregateId = Guid.Parse("BAEBA232-CD7F-46F5-AE2E-3204FE69A441"); + public static Guid SettlementAggregateId = Guid.Parse("BAEBA232-CD7F-46F5-AE2E-3204FE69A441"); public static String TransactionNumber = "0001"; @@ -145,18 +146,18 @@ public class TestData /// /// The additional transaction meta data. /// - public static Dictionary AdditionalTransactionMetaData(String amountName = "Amount", String customerAccountNumberName = "CustomerAccountNumber") => + public static Dictionary AdditionalTransactionMetaData(String amountName = "Amount", String customerAccountNumberName = "CustomerAccountNumber", String amount="100.00", String customerAccountNumber = "123456789") => new Dictionary { - {amountName, "100.00"}, - {customerAccountNumberName, "123456789" } + {amountName, amount}, + {customerAccountNumberName, customerAccountNumber } }; - public static Dictionary AdditionalTransactionMetaDataForVoucher(String amountName = "Amount", String recipientEmailName = "RecipientEmail") => + public static Dictionary AdditionalTransactionMetaDataForVoucher(String amountName = "Amount", String recipientEmailName = "RecipientEmail", String amount = "100.00", String recipientEmail = "test@testvoucher.co.uk") => new Dictionary { - {amountName, "100.00"}, - {recipientEmailName, "test@testvoucher.co.uk" } + {amountName, amount}, + {recipientEmailName, recipientEmail } }; @@ -388,6 +389,10 @@ public static Dictionary AdditionalTransactionMetaDataForVoucher TestData.TransactionDateTime, TestData.TransactionNumber); + public static ProcessSettlementRequest ProcessSettlementRequest => + ProcessSettlementRequest.Create(TestData.SettlementDate, + TestData.EstateId); + public static ProcessLogonTransactionResponse ProcessLogonTransactionResponseModel => new ProcessLogonTransactionResponse { @@ -689,6 +694,17 @@ public static TokenResponse TokenResponse() public static CustomerEmailReceiptRequestedEvent CustomerEmailReceiptRequestedEvent = new CustomerEmailReceiptRequestedEvent(TestData.TransactionId, TestData.EstateId, TestData.MerchantId, TestData.CustomerEmailAddress); + public static MerchantFeeAddedToTransactionEvent MerchantFeeAddedToTransactionEvent(DateTime settlementDueDate) => new MerchantFeeAddedToTransactionEvent(TestData.SettlementAggregateId, + TestData.EstateId, + TestData.MerchantId, + TestData.CalculatedFeeValue, + (Int32)CalculationType.Fixed, + TestData.TransactionFeeId, + TestData.CalculatedFeeValue, + TestData.TransactionFeeCalculateDateTime, + settlementDueDate, + TestData.SettlementDate); + public static TransactionHasBeenCompletedEvent TransactionHasBeenCompletedEvent = new TransactionHasBeenCompletedEvent(TestData.TransactionId, TestData.EstateId, TestData.MerchantId, @@ -707,6 +723,11 @@ public static TokenResponse TokenResponse() public static Decimal TransactionFeeValue = 0.5m; public static DateTime TransactionFeeCalculateDateTime = new DateTime(2021, 3, 18); + + public static DateTime TransactionFeeSettlementDueDate = new DateTime(2021, 3, 19); + + public static DateTime TransactionFeeSettledDateTime = new DateTime(2021, 3, 19,1,2,3); + public static Decimal CalculatedFeeValue = 0.5m; public static Int32 ReconciliationTransactionCount = 1; @@ -742,7 +763,7 @@ public static TokenResponse TokenResponse() } }; - public static CalculatedFee CalculatedFeeMerchantFee => + public static CalculatedFee CalculatedFeeMerchantFee() => new CalculatedFee { CalculatedValue = TestData.CalculatedFeeValue, @@ -752,6 +773,16 @@ public static TokenResponse TokenResponse() FeeType = FeeType.Merchant }; + public static CalculatedFee CalculatedFeeMerchantFee(Guid transactionFeeId) => + new CalculatedFee + { + CalculatedValue = TestData.CalculatedFeeValue, + FeeCalculationType = CalculationType.Fixed, + FeeId = transactionFeeId, + FeeValue = TestData.TransactionFeeValue, + FeeType = FeeType.Merchant + }; + public static CalculatedFee CalculatedFeeMerchantFee2 => new CalculatedFee { @@ -785,7 +816,7 @@ public static TokenResponse TokenResponse() public static List CalculatedMerchantFees => new List { - TestData.CalculatedFeeMerchantFee + TestData.CalculatedFeeMerchantFee() }; public static List CalculatedServiceProviderFees => @@ -794,6 +825,66 @@ public static TokenResponse TokenResponse() TestData.CalculatedFeeServiceProviderFee }; + public static SettlementAggregate GetEmptySettlementAggregate() + { + return SettlementAggregate.Create(TestData.SettlementAggregateId); + } + + public static SettlementAggregate GetCreatedSettlementAggregate() + { + var aggregate = SettlementAggregate.Create(TestData.SettlementAggregateId); + aggregate.Create(TestData.EstateId, TestData.SettlementDate); + return aggregate; + } + + public static SettlementAggregate GetSettlementAggregateWithPendingMerchantFees(Int32 numberOfFees) + { + var aggregate = SettlementAggregate.Create(TestData.SettlementAggregateId); + aggregate.Create(TestData.EstateId, TestData.SettlementDate); + + for (int i = 0; i < numberOfFees; i++) + { + aggregate.AddFee(TestData.MerchantId, Guid.NewGuid(), CalculatedFeeMerchantFee(Guid.NewGuid())); + } + + return aggregate; + } + + public static SettlementAggregate GetSettlementAggregateWithAllFeesSettled(Int32 numberOfFees) + { + var aggregate = SettlementAggregate.Create(TestData.SettlementAggregateId); + aggregate.Create(TestData.EstateId, TestData.SettlementDate); + + for (int i = 0; i < numberOfFees; i++) + { + Guid transactionId = Guid.NewGuid(); + Guid transactionFeeId = Guid.NewGuid(); + aggregate.AddFee(TestData.MerchantId, transactionId, CalculatedFeeMerchantFee(transactionFeeId)); + aggregate.MarkFeeAsSettled(TestData.MerchantId, transactionId, transactionFeeId); + } + + return aggregate; + } + + public static SettlementAggregate GetSettlementAggregateWithNotAllFeesSettled(Int32 numberOfFees) + { + var aggregate = SettlementAggregate.Create(TestData.SettlementAggregateId); + aggregate.Create(TestData.EstateId, TestData.SettlementDate); + + for (int i = 0; i <= numberOfFees; i++) + { + Guid transactionId = Guid.NewGuid(); + Guid transactionFeeId = Guid.NewGuid(); + aggregate.AddFee(TestData.MerchantId, transactionId, CalculatedFeeMerchantFee(transactionFeeId)); + if (i < numberOfFees) + { + aggregate.MarkFeeAsSettled(TestData.MerchantId, transactionId, transactionFeeId); + } + } + + return aggregate; + } + #endregion } } \ No newline at end of file diff --git a/TransactionProcessor.Transaction.DomainEvents/MerchantFeeAddedToTransactionEvent.cs b/TransactionProcessor.Transaction.DomainEvents/MerchantFeeAddedToTransactionEvent.cs index 1b24a6c9..6db2730a 100644 --- a/TransactionProcessor.Transaction.DomainEvents/MerchantFeeAddedToTransactionEvent.cs +++ b/TransactionProcessor.Transaction.DomainEvents/MerchantFeeAddedToTransactionEvent.cs @@ -29,7 +29,9 @@ public MerchantFeeAddedToTransactionEvent(Guid aggregateId, Int32 feeCalculationType, Guid feeId, Decimal feeValue, - DateTime feeCalculatedDateTime) : base(aggregateId, Guid.NewGuid()) + DateTime feeCalculatedDateTime, + DateTime settlementDueDate, + DateTime settledDateTime) : base(aggregateId, Guid.NewGuid()) { this.TransactionId = aggregateId; this.EstateId = estateId; @@ -39,6 +41,8 @@ public MerchantFeeAddedToTransactionEvent(Guid aggregateId, this.FeeId = feeId; this.FeeValue = feeValue; this.FeeCalculatedDateTime = feeCalculatedDateTime; + this.SettlementDueDate = settlementDueDate; + this.SettledDateTime = settledDateTime; } #endregion @@ -61,6 +65,10 @@ public MerchantFeeAddedToTransactionEvent(Guid aggregateId, /// public DateTime FeeCalculatedDateTime { get; init; } + public DateTime SettlementDueDate { get; init; } + + public DateTime SettledDateTime { get; init; } + /// /// Gets the estate identifier. /// diff --git a/TransactionProcessor.TransactionAggregate.Tests/DomainEventTests.cs b/TransactionProcessor.TransactionAggregate.Tests/DomainEventTests.cs index 68662b7e..a83c7e78 100644 --- a/TransactionProcessor.TransactionAggregate.Tests/DomainEventTests.cs +++ b/TransactionProcessor.TransactionAggregate.Tests/DomainEventTests.cs @@ -247,13 +247,15 @@ public void ProductDetailsAddedToTransactionEvent_CanBeCreated_IsCreated() public void MerchantFeeAddedToTransactionEvent_CanBeCreated_IsCreated() { MerchantFeeAddedToTransactionEvent merchantFeeAddedToTransactionEvent = new MerchantFeeAddedToTransactionEvent(TestData.TransactionId, - TestData.EstateId, - TestData.MerchantId, - TestData.CalculatedFeeValue, - (Int32)CalculationType.Fixed, - TestData.TransactionFeeId, - TestData.TransactionFeeValue, - TestData.TransactionFeeCalculateDateTime); + TestData.EstateId, + TestData.MerchantId, + TestData.CalculatedFeeValue, + (Int32)CalculationType.Fixed, + TestData.TransactionFeeId, + TestData.TransactionFeeValue, + TestData.TransactionFeeCalculateDateTime, + TestData.TransactionFeeSettlementDueDate, + TestData.TransactionFeeSettledDateTime); merchantFeeAddedToTransactionEvent.ShouldNotBeNull(); merchantFeeAddedToTransactionEvent.AggregateId.ShouldBe(TestData.TransactionId); @@ -266,6 +268,8 @@ public void MerchantFeeAddedToTransactionEvent_CanBeCreated_IsCreated() merchantFeeAddedToTransactionEvent.FeeId.ShouldBe(TestData.TransactionFeeId); merchantFeeAddedToTransactionEvent.FeeValue.ShouldBe(TestData.TransactionFeeValue); merchantFeeAddedToTransactionEvent.FeeCalculatedDateTime.ShouldBe(TestData.TransactionFeeCalculateDateTime); + merchantFeeAddedToTransactionEvent.SettlementDueDate.ShouldBe(TestData.TransactionFeeSettlementDueDate); + merchantFeeAddedToTransactionEvent.SettledDateTime.ShouldBe(TestData.TransactionFeeSettledDateTime); } diff --git a/TransactionProcessor.TransactionAggregate.Tests/TransactionAggregateTests.cs b/TransactionProcessor.TransactionAggregate.Tests/TransactionAggregateTests.cs index 4877dd69..058429c9 100644 --- a/TransactionProcessor.TransactionAggregate.Tests/TransactionAggregateTests.cs +++ b/TransactionProcessor.TransactionAggregate.Tests/TransactionAggregateTests.cs @@ -1003,7 +1003,6 @@ public void TransactionAggregate_RequestEmailReceipt_EmailReceiptAlreadyRequeste [Theory] [InlineData(TransactionType.Sale, FeeType.ServiceProvider)] - [InlineData(TransactionType.Sale, FeeType.Merchant)] public void TransactionAggregate_AddFee_FeeDetailsAdded(TransactionType transactionType, FeeType feeType) { TransactionAggregate transactionAggregate = TransactionAggregate.Create(TestData.TransactionId); @@ -1072,7 +1071,7 @@ private CalculatedFee GetCalculatedFeeToAdd(FeeType feeType) CalculatedFee calculatedFee = null; if (feeType == FeeType.Merchant) { - calculatedFee = TestData.CalculatedFeeMerchantFee; + calculatedFee = TestData.CalculatedFeeMerchantFee(); } else if (feeType == FeeType.ServiceProvider) { @@ -1084,7 +1083,6 @@ private CalculatedFee GetCalculatedFeeToAdd(FeeType feeType) [Theory] [InlineData(TransactionType.Sale, FeeType.ServiceProvider)] - [InlineData(TransactionType.Sale, FeeType.Merchant)] public void TransactionAggregate_AddFee_TransactionNotCompleted_ErrorThrown(TransactionType transactionType, FeeType feeType) { TransactionAggregate transactionAggregate = TransactionAggregate.Create(TestData.TransactionId); @@ -1102,7 +1100,6 @@ public void TransactionAggregate_AddFee_TransactionNotCompleted_ErrorThrown(Tran [Theory] [InlineData(TransactionType.Sale, FeeType.ServiceProvider)] - [InlineData(TransactionType.Sale, FeeType.Merchant)] public void TransactionAggregate_AddFee_FeeAlreadyAdded_ErrorThrown(TransactionType transactionType, FeeType feeType) { TransactionAggregate transactionAggregate = TransactionAggregate.Create(TestData.TransactionId); @@ -1155,5 +1152,143 @@ public void TransactionAggregate_AddFee_LogonTransaction_ErrorThrown(FeeType fee transactionAggregate.AddFee(this.GetCalculatedFeeToAdd(feeType)); }); } + + + //############### + + [Theory] + [InlineData(TransactionType.Sale, FeeType.Merchant)] + public void TransactionAggregate_AddSettledFee_FeeDetailsAdded(TransactionType transactionType, FeeType feeType) + { + TransactionAggregate transactionAggregate = TransactionAggregate.Create(TestData.TransactionId); + transactionAggregate.StartTransaction(TestData.TransactionDateTime, TestData.TransactionNumber, transactionType, TestData.TransactionReference, TestData.EstateId, TestData.MerchantId, TestData.DeviceIdentifier, + TestData.TransactionAmount); + + transactionAggregate.AddProductDetails(TestData.ContractId, TestData.ProductId); + transactionAggregate.AuthoriseTransaction(TestData.OperatorIdentifier1, TestData.OperatorAuthorisationCode, TestData.OperatorResponseCode, TestData.OperatorResponseMessage, TestData.OperatorTransactionId, TestData.ResponseCode, TestData.ResponseMessage); + transactionAggregate.CompleteTransaction(); + + CalculatedFee calculatedFee = this.GetCalculatedFeeToAdd(feeType); + + transactionAggregate.AddSettledFee(calculatedFee, TestData.TransactionFeeSettlementDueDate, TestData.SettlementDate); + + List fees = transactionAggregate.GetFees(); + + fees.ShouldHaveSingleItem(); + CalculatedFee calculatedFeeAdded = fees.Single(); + calculatedFeeAdded.FeeId.ShouldBe(calculatedFee.FeeId); + calculatedFeeAdded.CalculatedValue.ShouldBe(calculatedFee.CalculatedValue); + calculatedFeeAdded.FeeCalculationType.ShouldBe(calculatedFee.FeeCalculationType); + calculatedFeeAdded.FeeType.ShouldBe(calculatedFee.FeeType); + calculatedFeeAdded.FeeValue.ShouldBe(calculatedFee.FeeValue); + + } + + [Theory] + [InlineData(TransactionType.Sale)] + public void TransactionAggregate_AddSettledFee_NullFee_ErrorThrown(TransactionType transactionType) + { + TransactionAggregate transactionAggregate = TransactionAggregate.Create(TestData.TransactionId); + transactionAggregate.StartTransaction(TestData.TransactionDateTime, TestData.TransactionNumber, transactionType, TestData.TransactionReference, TestData.EstateId, TestData.MerchantId, TestData.DeviceIdentifier, + TestData.TransactionAmount); + + transactionAggregate.AddProductDetails(TestData.ContractId, TestData.ProductId); + transactionAggregate.AuthoriseTransaction(TestData.OperatorIdentifier1, TestData.OperatorAuthorisationCode, TestData.OperatorResponseCode, TestData.OperatorResponseMessage, TestData.OperatorTransactionId, TestData.ResponseCode, TestData.ResponseMessage); + transactionAggregate.CompleteTransaction(); + + Should.Throw(() => + { + transactionAggregate.AddSettledFee(null, TestData.TransactionFeeSettlementDueDate, TestData.SettlementDate); + }); + } + + [Theory] + [InlineData(TransactionType.Sale, FeeType.Merchant)] + public void TransactionAggregate_AddSettledFee_TransactionNotAuthorised_ErrorThrown(TransactionType transactionType, FeeType feeType) + { + TransactionAggregate transactionAggregate = TransactionAggregate.Create(TestData.TransactionId); + transactionAggregate.StartTransaction(TestData.TransactionDateTime, TestData.TransactionNumber, transactionType, TestData.TransactionReference, TestData.EstateId, TestData.MerchantId, TestData.DeviceIdentifier, + TestData.TransactionAmount); + + transactionAggregate.AddProductDetails(TestData.ContractId, TestData.ProductId); + transactionAggregate.DeclineTransaction(TestData.OperatorIdentifier1, TestData.OperatorResponseCode, TestData.OperatorResponseMessage, TestData.ResponseCode, TestData.ResponseMessage); + transactionAggregate.CompleteTransaction(); + + Should.Throw(() => + { + transactionAggregate.AddSettledFee(this.GetCalculatedFeeToAdd(feeType), TestData.TransactionFeeSettlementDueDate, TestData.SettlementDate); + }); + } + + [Theory] + [InlineData(TransactionType.Sale, FeeType.Merchant)] + public void TransactionAggregate_AddSettledFee_TransactionNotCompleted_ErrorThrown(TransactionType transactionType, FeeType feeType) + { + TransactionAggregate transactionAggregate = TransactionAggregate.Create(TestData.TransactionId); + transactionAggregate.StartTransaction(TestData.TransactionDateTime, TestData.TransactionNumber, transactionType, TestData.TransactionReference, TestData.EstateId, TestData.MerchantId, TestData.DeviceIdentifier, + TestData.TransactionAmount); + + transactionAggregate.AddProductDetails(TestData.ContractId, TestData.ProductId); + transactionAggregate.AuthoriseTransaction(TestData.OperatorIdentifier1, TestData.OperatorAuthorisationCode, TestData.OperatorResponseCode, TestData.OperatorResponseMessage, TestData.OperatorTransactionId, TestData.ResponseCode, TestData.ResponseMessage); + + Should.Throw(() => + { + transactionAggregate.AddSettledFee(this.GetCalculatedFeeToAdd(feeType), TestData.TransactionFeeSettlementDueDate, TestData.SettlementDate); + }); + } + + [Theory] + [InlineData(TransactionType.Sale, FeeType.Merchant)] + public void TransactionAggregate_AddSettledFee_FeeAlreadyAdded_ErrorThrown(TransactionType transactionType, FeeType feeType) + { + TransactionAggregate transactionAggregate = TransactionAggregate.Create(TestData.TransactionId); + transactionAggregate.StartTransaction(TestData.TransactionDateTime, TestData.TransactionNumber, transactionType, TestData.TransactionReference, TestData.EstateId, TestData.MerchantId, TestData.DeviceIdentifier, + TestData.TransactionAmount); + + transactionAggregate.AddProductDetails(TestData.ContractId, TestData.ProductId); + transactionAggregate.AuthoriseTransaction(TestData.OperatorIdentifier1, TestData.OperatorAuthorisationCode, TestData.OperatorResponseCode, TestData.OperatorResponseMessage, TestData.OperatorTransactionId, TestData.ResponseCode, TestData.ResponseMessage); + transactionAggregate.CompleteTransaction(); + transactionAggregate.AddSettledFee(this.GetCalculatedFeeToAdd(feeType), TestData.TransactionFeeSettlementDueDate, TestData.SettlementDate); + + Should.Throw(() => + { + transactionAggregate.AddSettledFee(this.GetCalculatedFeeToAdd(feeType), TestData.TransactionFeeSettlementDueDate, TestData.SettlementDate); + }); + } + + [Theory] + [InlineData(TransactionType.Sale)] + public void TransactionAggregate_AddSettledFee_UnsupportedFeeType_ErrorThrown(TransactionType transactionType) + { + TransactionAggregate transactionAggregate = TransactionAggregate.Create(TestData.TransactionId); + transactionAggregate.StartTransaction(TestData.TransactionDateTime, TestData.TransactionNumber, transactionType, TestData.TransactionReference, TestData.EstateId, TestData.MerchantId, TestData.DeviceIdentifier, + TestData.TransactionAmount); + + transactionAggregate.AddProductDetails(TestData.ContractId, TestData.ProductId); + transactionAggregate.AuthoriseTransaction(TestData.OperatorIdentifier1, TestData.OperatorAuthorisationCode, TestData.OperatorResponseCode, TestData.OperatorResponseMessage, TestData.OperatorTransactionId, TestData.ResponseCode, TestData.ResponseMessage); + transactionAggregate.CompleteTransaction(); + + Should.Throw(() => + { + transactionAggregate.AddSettledFee(TestData.CalculatedFeeUnsupportedFee, TestData.TransactionFeeSettlementDueDate, TestData.SettlementDate); + }); + } + + [Theory] + [InlineData(FeeType.Merchant)] + public void TransactionAggregate_AddSettledFee_LogonTransaction_ErrorThrown(FeeType feeType) + { + TransactionAggregate transactionAggregate = TransactionAggregate.Create(TestData.TransactionId); + transactionAggregate.StartTransaction(TestData.TransactionDateTime, TestData.TransactionNumber, TransactionType.Logon, TestData.TransactionReference, TestData.EstateId, TestData.MerchantId, TestData.DeviceIdentifier, + TestData.TransactionAmount); + + transactionAggregate.AuthoriseTransactionLocally(TestData.AuthorisationCode, TestData.ResponseCode, TestData.ResponseMessage); + transactionAggregate.CompleteTransaction(); + + Should.Throw(() => + { + transactionAggregate.AddSettledFee(this.GetCalculatedFeeToAdd(feeType), TestData.TransactionFeeSettlementDueDate, TestData.SettlementDate); + }); + } } } diff --git a/TransactionProcessor.TransactionAgrgegate/TransactionAggregate.cs b/TransactionProcessor.TransactionAgrgegate/TransactionAggregate.cs index 4ac6188f..91bed829 100644 --- a/TransactionProcessor.TransactionAgrgegate/TransactionAggregate.cs +++ b/TransactionProcessor.TransactionAgrgegate/TransactionAggregate.cs @@ -278,6 +278,39 @@ private TransactionAggregate(Guid aggregateId) /// calculatedFee /// Unsupported Fee Type public void AddFee(CalculatedFee calculatedFee) + { + Guard.ThrowIfNull(calculatedFee, nameof(calculatedFee)); + + this.CheckFeeHasNotAlreadyBeenAdded(calculatedFee); + this.CheckTransactionHasBeenAuthorised(); + this.CheckTransactionHasBeenCompleted(); + this.CheckTransactionCanAttractFees(); + + DomainEventRecord.DomainEvent @event = null; + if (calculatedFee.FeeType == FeeType.ServiceProvider) + { + // This is an operational (service provider) fee + @event = new ServiceProviderFeeAddedToTransactionEvent(this.AggregateId, + this.EstateId, + this.MerchantId, + calculatedFee.CalculatedValue, + (Int32)calculatedFee.FeeCalculationType, + calculatedFee.FeeId, + calculatedFee.FeeValue, + calculatedFee.FeeCalculatedDateTime); + } + else + { + throw new InvalidOperationException("Unsupported Fee Type"); + } + + if (@event != null) + { + this.ApplyAndAppend(@event); + } + } + + public void AddSettledFee(CalculatedFee calculatedFee, DateTime settlementDueDate, DateTime settledDateTime) { if (calculatedFee == null) { @@ -300,19 +333,9 @@ public void AddFee(CalculatedFee calculatedFee) (Int32)calculatedFee.FeeCalculationType, calculatedFee.FeeId, calculatedFee.FeeValue, - calculatedFee.FeeCalculatedDateTime); - } - else if (calculatedFee.FeeType == FeeType.ServiceProvider) - { - // This is an operational (service provider) fee - @event = new ServiceProviderFeeAddedToTransactionEvent(this.AggregateId, - this.EstateId, - this.MerchantId, - calculatedFee.CalculatedValue, - (Int32)calculatedFee.FeeCalculationType, - calculatedFee.FeeId, - calculatedFee.FeeValue, - calculatedFee.FeeCalculatedDateTime); + calculatedFee.FeeCalculatedDateTime, + settlementDueDate, + settledDateTime); } else { diff --git a/TransactionProcessor/Controllers/SettlementController.cs b/TransactionProcessor/Controllers/SettlementController.cs index 0c3388c4..c44ee5e4 100644 --- a/TransactionProcessor/Controllers/SettlementController.cs +++ b/TransactionProcessor/Controllers/SettlementController.cs @@ -8,7 +8,10 @@ namespace TransactionProcessor.Controllers using System.Threading; using System.Threading.Tasks; using BusinessLogic.Common; + using BusinessLogic.Requests; using DataTransferObjects; + using Factories; + using MediatR; using Microsoft.AspNetCore.Authorization; using SettlementAggregates; using Shared.DomainDrivenDesign.EventSourcing; @@ -20,7 +23,11 @@ namespace TransactionProcessor.Controllers [Authorize] public class SettlementController : ControllerBase { - private readonly IAggregateRepository PendingSettlmentAggregateRepository; + private readonly IAggregateRepository SettlmentAggregateRepository; + + private readonly IMediator Mediator; + + private readonly IModelFactory ModelFactory; #region Others @@ -34,34 +41,53 @@ public class SettlementController : ControllerBase /// private const String ControllerRoute = "api/" + SettlementController.ControllerName; - public SettlementController(IAggregateRepository pendingSettlmentAggregateRepository) + public SettlementController(IAggregateRepository settlmentAggregateRepository, + IMediator mediator, + IModelFactory modelFactory) { - this.PendingSettlmentAggregateRepository = pendingSettlmentAggregateRepository; + this.SettlmentAggregateRepository = settlmentAggregateRepository; + this.Mediator = mediator; + this.ModelFactory = modelFactory; } [HttpGet] - [Route("{pendingSettlementDate}/estates/{estateId}/pending")] - public async Task GetPendingSettlement([FromRoute] DateTime pendingSettlementDate, + [Route("{settlementDate}/estates/{estateId}/pending")] + public async Task GetPendingSettlement([FromRoute] DateTime settlementDate, [FromRoute] Guid estateId, CancellationToken cancellationToken) { // TODO: Convert to using a manager/model/factory // Convert the date passed in to a guid - var aggregateId = pendingSettlementDate.Date.ToGuid(); + var aggregateId = settlementDate.Date.ToGuid(); - var pendingSettlementAggregate = await this.PendingSettlmentAggregateRepository.GetLatestVersion(aggregateId, cancellationToken); + var settlementAggregate = await this.SettlmentAggregateRepository.GetLatestVersion(aggregateId, cancellationToken); - var pendingSettlementResponse = new PendingSettlementResponse + var settlementResponse = new SettlementResponse { - EstateId = pendingSettlementAggregate.EstateId, - NumberOfFeesPendingSettlement = pendingSettlementAggregate.GetNumberOfFeesPendingSettlement(), - NumberOfFeesSettled = pendingSettlementAggregate.GetNumberOfFeesSettled(), - SettlementDate = pendingSettlementAggregate.SettlmentDate, + EstateId = settlementAggregate.EstateId, + NumberOfFeesPendingSettlement = settlementAggregate.GetNumberOfFeesPendingSettlement(), + NumberOfFeesSettled = settlementAggregate.GetNumberOfFeesSettled(), + SettlementDate = settlementAggregate.SettlementDate, + SettlementCompleted = settlementAggregate.SettlementComplete }; - return this.Ok(pendingSettlementResponse); + return this.Ok(settlementResponse); + + } + + [HttpPost] + [Route("{settlementDate}/estates/{estateId}")] + public async Task ProcessSettlement([FromRoute] DateTime settlementDate, + [FromRoute] Guid estateId, + CancellationToken cancellationToken) + { + ProcessSettlementRequest command = ProcessSettlementRequest.Create(settlementDate, estateId); + + var processSettlementResponse = await this.Mediator.Send(command, cancellationToken); + return this.Ok(); } + #endregion } diff --git a/TransactionProcessor/Program.cs b/TransactionProcessor/Program.cs index aa0a32c6..a507823c 100644 --- a/TransactionProcessor/Program.cs +++ b/TransactionProcessor/Program.cs @@ -49,8 +49,8 @@ public static IHostBuilder CreateHostBuilder(string[] args) }) .ConfigureServices(services => { - PendingSettlementCreatedForDateEvent s = - new PendingSettlementCreatedForDateEvent(Guid.Parse("62CA5BF0-D138-4A19-9970-A4F7D52DE292"), + SettlementCreatedForDateEvent s = + new SettlementCreatedForDateEvent(Guid.Parse("62CA5BF0-D138-4A19-9970-A4F7D52DE292"), Guid.Parse("3E42516B-6C6F-4F86-BF08-3EF0ACDDDD55"), DateTime.Now); diff --git a/TransactionProcessor/Startup.cs b/TransactionProcessor/Startup.cs index 50167eae..f653ac62 100644 --- a/TransactionProcessor/Startup.cs +++ b/TransactionProcessor/Startup.cs @@ -157,7 +157,7 @@ public void ConfigureServices(IServiceCollection services) services.AddSingleton(); services.AddSingleton, AggregateRepository>(); services.AddSingleton, AggregateRepository>(); - services.AddSingleton, AggregateRepository>(); + services.AddSingleton, AggregateRepository>(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); @@ -196,6 +196,8 @@ public void ConfigureServices(IServiceCollection services) services.AddSingleton, TransactionRequestHandler>(); services.AddSingleton, TransactionRequestHandler>(); + services.AddSingleton, SettlementRequestHandler>(); + services.AddTransient>(context => (operatorIdentifier) => { if (String.Compare(operatorIdentifier, "Safaricom", StringComparison.CurrentCultureIgnoreCase) == 0) @@ -254,23 +256,21 @@ private static void ConfigureEventStoreSettings(EventStoreClientSettings setting } settings.CreateHttpMessageHandler = () => new SocketsHttpHandler - { - SslOptions = + { + SslOptions = { RemoteCertificateValidationCallback = (sender, certificate, chain, errors) => true, } - }; + }; settings.ConnectionName = Startup.Configuration.GetValue("EventStoreSettings:ConnectionName"); settings.ConnectivitySettings = new EventStoreClientConnectivitySettings - { - Address = new Uri(Startup.Configuration.GetValue("EventStoreSettings:ConnectionString")), - }; - - settings.DefaultCredentials = new UserCredentials(Startup.Configuration.GetValue("EventStoreSettings:UserName"), - Startup.Configuration.GetValue("EventStoreSettings:Password")); + { + Address = new Uri(Startup.Configuration.GetValue("EventStoreSettings:ConnectionString")), + }; + Startup.EventStoreClientSettings = settings; } diff --git a/TransactionProcessor/appsettings.json b/TransactionProcessor/appsettings.json index 1b36c3f0..826158a6 100644 --- a/TransactionProcessor/appsettings.json +++ b/TransactionProcessor/appsettings.json @@ -9,6 +9,9 @@ ], "CustomerEmailReceiptRequestedEvent": [ "TransactionProcessor.BusinessLogic.EventHandling.TransactionDomainEventHandler,TransactionProcessor.BusinessLogic" + ], + "MerchantFeeAddedToTransactionEvent": [ + "TransactionProcessor.BusinessLogic.EventHandling.TransactionDomainEventHandler,TransactionProcessor.BusinessLogic" ] } },