From 9b140d13716119db4e03d77c24db905fa1fa61f9 Mon Sep 17 00:00:00 2001 From: Stuart Ferguson Date: Wed, 12 Aug 2020 07:28:46 +0100 Subject: [PATCH] Add calculated fees to a sale transaction --- .../DomainEventHandlerResolverTests.cs | 100 ++++++ .../TransactionDomainEventHandlerTests.cs | 147 ++++++++ .../Manager/FeeCalculationManagerTests.cs | 330 ++++++++++++++++++ .../TransactionAggregateManagerTests.cs | 18 +- .../Services/TransactionDomainServiceTests.cs | 8 +- ...actionProcessor.BusinessLogic.Tests.csproj | 4 - .../DomainEventHandlerResolver.cs | 99 ++++++ .../EventHandling/IDomainEventHandler.cs | 22 ++ .../IDomainEventHandlerResolver.cs | 21 ++ .../TransactionDomainEventHandler.cs | 184 ++++++++++ .../Manager/FeeCalculationManager.cs | 62 ++++ .../Manager/IFeeCalculationManager.cs | 26 ++ .../Manager/TransactionFeeToCalculate.cs | 49 +++ .../Services/ITransactionAggregateManager.cs | 14 + .../Services/TransactionAggregateManager.cs | 21 ++ .../TransactionProcessor.BusinessLogic.csproj | 6 +- .../Common/DockerHelper.cs | 15 +- ...ansactionProcessor.IntegrationTests.csproj | 4 +- TransactionProcessor.Models/CalculatedFee.cs | 56 +++ .../CalculationType.cs | 18 + TransactionProcessor.Models/FeeType.cs | 18 + .../TransactionType.cs | 18 + TransactionProcessor.Testing/TestData.cs | 200 ++++++++++- .../MerchantFeeAddedToTransactionEvent.cs | 149 ++++++++ .../ProductDetailsAddedToTransactionEvent.cs | 19 +- ...rviceProviderFeeAddedToTransactionEvent.cs | 149 ++++++++ .../DomainEventTests.cs | 54 ++- .../TransactionAggregateTests.cs | 156 +++++++++ .../TransactionAggregate.cs | 249 ++++++++++--- .../Controllers/DomainEventController.cs | 104 ++++++ TransactionProcessor/Startup.cs | 26 ++ TransactionProcessor/appsettings.json | 7 +- 32 files changed, 2277 insertions(+), 76 deletions(-) create mode 100644 TransactionProcessor.BusinessLogic.Tests/DomainEventHandlers/DomainEventHandlerResolverTests.cs create mode 100644 TransactionProcessor.BusinessLogic.Tests/DomainEventHandlers/TransactionDomainEventHandlerTests.cs create mode 100644 TransactionProcessor.BusinessLogic.Tests/Manager/FeeCalculationManagerTests.cs create mode 100644 TransactionProcessor.BusinessLogic/EventHandling/DomainEventHandlerResolver.cs create mode 100644 TransactionProcessor.BusinessLogic/EventHandling/IDomainEventHandler.cs create mode 100644 TransactionProcessor.BusinessLogic/EventHandling/IDomainEventHandlerResolver.cs create mode 100644 TransactionProcessor.BusinessLogic/EventHandling/TransactionDomainEventHandler.cs create mode 100644 TransactionProcessor.BusinessLogic/Manager/FeeCalculationManager.cs create mode 100644 TransactionProcessor.BusinessLogic/Manager/IFeeCalculationManager.cs create mode 100644 TransactionProcessor.BusinessLogic/Manager/TransactionFeeToCalculate.cs create mode 100644 TransactionProcessor.Models/CalculatedFee.cs create mode 100644 TransactionProcessor.Models/CalculationType.cs create mode 100644 TransactionProcessor.Models/FeeType.cs create mode 100644 TransactionProcessor.Transaction.DomainEvents/MerchantFeeAddedToTransactionEvent.cs create mode 100644 TransactionProcessor.Transaction.DomainEvents/ServiceProviderFeeAddedToTransactionEvent.cs create mode 100644 TransactionProcessor/Controllers/DomainEventController.cs diff --git a/TransactionProcessor.BusinessLogic.Tests/DomainEventHandlers/DomainEventHandlerResolverTests.cs b/TransactionProcessor.BusinessLogic.Tests/DomainEventHandlers/DomainEventHandlerResolverTests.cs new file mode 100644 index 00000000..488ea0fe --- /dev/null +++ b/TransactionProcessor.BusinessLogic.Tests/DomainEventHandlers/DomainEventHandlerResolverTests.cs @@ -0,0 +1,100 @@ +namespace TransactionProcessor.BusinessLogic.Tests.DomainEventHanders +{ + using System; + using System.Collections.Generic; + using System.Linq; + using MessagingService.BusinessLogic.EventHandling; + using Moq; + using Shouldly; + using Testing; + using Transaction.DomainEvents; + using Xunit; + + public class DomainEventHandlerResolverTests + { + [Fact] + public void DomainEventHandlerResolver_CanBeCreated_IsCreated() + { + Dictionary eventHandlerConfiguration = new Dictionary(); + + eventHandlerConfiguration.Add("TestEventType1", new String[] { "TransactionProcessor.BusinessLogic.EventHandling.TransactionDomainEventHandler" }); + + Mock domainEventHandler = new Mock(); + Func createDomainEventHandlerFunc = (type) => { return domainEventHandler.Object; }; + DomainEventHandlerResolver resolver = new DomainEventHandlerResolver(eventHandlerConfiguration, createDomainEventHandlerFunc); + + resolver.ShouldNotBeNull(); + } + + [Fact] + public void DomainEventHandlerResolver_CanBeCreated_InvalidEventHandlerType_ErrorThrown() + { + Dictionary eventHandlerConfiguration = new Dictionary(); + + eventHandlerConfiguration.Add("TestEventType1", new String[] { "TransactionProcessor.BusinessLogic.EventHandling.NonExistantDomainEventHandler" }); + + Mock domainEventHandler = new Mock(); + Func createDomainEventHandlerFunc = (type) => { return domainEventHandler.Object; }; + + Should.Throw(() => new DomainEventHandlerResolver(eventHandlerConfiguration, createDomainEventHandlerFunc)); + } + + [Fact] + public void DomainEventHandlerResolver_GetDomainEventHandlers_TransactionHasBeenCompletedEvent_EventHandlersReturned() + { + String handlerTypeName = "TransactionProcessor.BusinessLogic.EventHandling.TransactionDomainEventHandler"; + Dictionary eventHandlerConfiguration = new Dictionary(); + + TransactionHasBeenCompletedEvent transactionHasBeenCompletedEvent = TestData.TransactionHasBeenCompletedEvent; + + eventHandlerConfiguration.Add(transactionHasBeenCompletedEvent.GetType().FullName, new String[] { handlerTypeName }); + + Mock domainEventHandler = new Mock(); + Func createDomainEventHandlerFunc = (type) => { return domainEventHandler.Object; }; + + DomainEventHandlerResolver resolver = new DomainEventHandlerResolver(eventHandlerConfiguration, createDomainEventHandlerFunc); + + List handlers = resolver.GetDomainEventHandlers(transactionHasBeenCompletedEvent); + + handlers.ShouldNotBeNull(); + handlers.Any().ShouldBeTrue(); + handlers.Count.ShouldBe(1); + } + + [Fact] + public void DomainEventHandlerResolver_GetDomainEventHandlers_TransactionHasBeenCompletedEvent_EventNotConfigured_EventHandlersReturned() + { + String handlerTypeName = "TransactionProcessor.BusinessLogic.EventHandling.TransactionDomainEventHandler"; + Dictionary eventHandlerConfiguration = new Dictionary(); + + TransactionHasBeenCompletedEvent transactionHasBeenCompletedEvent = TestData.TransactionHasBeenCompletedEvent; + + eventHandlerConfiguration.Add("RandomEvent", new String[] { handlerTypeName }); + Mock domainEventHandler = new Mock(); + Func createDomainEventHandlerFunc = (type) => { return domainEventHandler.Object; }; + + DomainEventHandlerResolver resolver = new DomainEventHandlerResolver(eventHandlerConfiguration, createDomainEventHandlerFunc); + + List handlers = resolver.GetDomainEventHandlers(transactionHasBeenCompletedEvent); + + handlers.ShouldBeNull(); + } + + [Fact] + public void DomainEventHandlerResolver_GetDomainEventHandlers_TransactionHasBeenCompletedEvent_NoHandlersConfigured_EventHandlersReturned() + { + Dictionary eventHandlerConfiguration = new Dictionary(); + + TransactionHasBeenCompletedEvent transactionHasBeenCompletedEvent = TestData.TransactionHasBeenCompletedEvent; + Mock domainEventHandler = new Mock(); + + Func createDomainEventHandlerFunc = (type) => { return domainEventHandler.Object; }; + + DomainEventHandlerResolver resolver = new DomainEventHandlerResolver(eventHandlerConfiguration, createDomainEventHandlerFunc); + + List handlers = resolver.GetDomainEventHandlers(transactionHasBeenCompletedEvent); + + handlers.ShouldBeNull(); + } + } +} \ No newline at end of file diff --git a/TransactionProcessor.BusinessLogic.Tests/DomainEventHandlers/TransactionDomainEventHandlerTests.cs b/TransactionProcessor.BusinessLogic.Tests/DomainEventHandlers/TransactionDomainEventHandlerTests.cs new file mode 100644 index 00000000..24ffbbcd --- /dev/null +++ b/TransactionProcessor.BusinessLogic.Tests/DomainEventHandlers/TransactionDomainEventHandlerTests.cs @@ -0,0 +1,147 @@ +namespace TransactionProcessor.BusinessLogic.Tests.DomainEventHandlers +{ + using System; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using BusinessLogic.Manager; + using BusinessLogic.Services; + using EstateManagement.Client; + using EventHandling; + using Microsoft.Extensions.Configuration; + using Moq; + using SecurityService.Client; + using Shared.EventStore.EventStore; + using Shared.General; + using Shared.Logger; + using Testing; + using TransactionAggregate; + using Xunit; + + public class TransactionDomainEventHandlerTests + { + [Fact] + public async Task TransactionDomainEventHandler_Handle_TransactionHasBeenCompletedEvent_SuccessfulSale_EventIsHandled() + { + Mock transactionAggregateManager = new Mock(); + transactionAggregateManager.Setup(t => t.GetAggregate(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(TestData.GetCompletedAuthorisedSaleTransactionAggregate); + Mock feeCalculationManager = new Mock(); + feeCalculationManager.Setup(f => f.CalculateFees(It.IsAny>(), It.IsAny())).Returns(TestData.CalculatedMerchantFees); + Mock estateClient = new Mock(); + estateClient.Setup(e => e.GetTransactionFeesForProduct(It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())).ReturnsAsync(TestData.ContractProductTransactionFees); + + Mock securityServiceClient = new Mock(); + securityServiceClient.Setup(s => s.GetToken(It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(TestData.TokenResponse); + + + IConfigurationRoot configurationRoot = new ConfigurationBuilder().AddInMemoryCollection(TestData.DefaultAppSettings).Build(); + ConfigurationReader.Initialise(configurationRoot); + Logger.Initialise(NullLogger.Instance); + + TransactionDomainEventHandler transactionDomainEventHandler = new TransactionDomainEventHandler(transactionAggregateManager.Object, + feeCalculationManager.Object, + estateClient.Object, + securityServiceClient.Object); + + await transactionDomainEventHandler.Handle(TestData.TransactionHasBeenCompletedEvent, CancellationToken.None); + } + + [Fact] + public async Task TransactionDomainEventHandler_Handle_TransactionHasBeenCompletedEvent_UnsuccessfulSale_EventIsHandled() + { + Mock transactionAggregateManager = new Mock(); + transactionAggregateManager.Setup(t => t.GetAggregate(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(TestData.GetCompletedDeclinedSaleTransactionAggregate); + Mock feeCalculationManager = new Mock(); + Mock estateClient = new Mock(); + + Mock securityServiceClient = new Mock(); + + IConfigurationRoot configurationRoot = new ConfigurationBuilder().AddInMemoryCollection(TestData.DefaultAppSettings).Build(); + ConfigurationReader.Initialise(configurationRoot); + Logger.Initialise(NullLogger.Instance); + + TransactionDomainEventHandler transactionDomainEventHandler = new TransactionDomainEventHandler(transactionAggregateManager.Object, + feeCalculationManager.Object, + estateClient.Object, + securityServiceClient.Object); + + await transactionDomainEventHandler.Handle(TestData.TransactionHasBeenCompletedEvent, CancellationToken.None); + } + + [Fact] + public async Task TransactionDomainEventHandler_Handle_TransactionHasBeenCompletedEvent_IncompleteSale_EventIsHandled() + { + Mock transactionAggregateManager = new Mock(); + transactionAggregateManager.Setup(t => t.GetAggregate(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(TestData.GetIncompleteAuthorisedSaleTransactionAggregate); + Mock feeCalculationManager = new Mock(); + Mock estateClient = new Mock(); + + Mock securityServiceClient = new Mock(); + + IConfigurationRoot configurationRoot = new ConfigurationBuilder().AddInMemoryCollection(TestData.DefaultAppSettings).Build(); + ConfigurationReader.Initialise(configurationRoot); + Logger.Initialise(NullLogger.Instance); + + TransactionDomainEventHandler transactionDomainEventHandler = new TransactionDomainEventHandler(transactionAggregateManager.Object, + feeCalculationManager.Object, + estateClient.Object, + securityServiceClient.Object); + + await transactionDomainEventHandler.Handle(TestData.TransactionHasBeenCompletedEvent, CancellationToken.None); + } + + [Fact] + public async Task TransactionDomainEventHandler_Handle_TransactionHasBeenCompletedEvent_SaleWithNoProductDetails_EventIsHandled() + { + Mock transactionAggregateManager = new Mock(); + transactionAggregateManager.Setup(t => t.GetAggregate(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(TestData.GetCompletedAuthorisedSaleWithNoProductDetailsTransactionAggregate); + Mock feeCalculationManager = new Mock(); + Mock estateClient = new Mock(); + + Mock securityServiceClient = new Mock(); + + IConfigurationRoot configurationRoot = new ConfigurationBuilder().AddInMemoryCollection(TestData.DefaultAppSettings).Build(); + ConfigurationReader.Initialise(configurationRoot); + Logger.Initialise(NullLogger.Instance); + + TransactionDomainEventHandler transactionDomainEventHandler = new TransactionDomainEventHandler(transactionAggregateManager.Object, + feeCalculationManager.Object, + estateClient.Object, + securityServiceClient.Object); + + await transactionDomainEventHandler.Handle(TestData.TransactionHasBeenCompletedEvent, CancellationToken.None); + } + + [Fact] + public async Task TransactionDomainEventHandler_Handle_TransactionHasBeenCompletedEvent_AuthorisedLogon_EventIsHandled() + { + Mock transactionAggregateManager = new Mock(); + transactionAggregateManager.Setup(t => t.GetAggregate(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(TestData.GetCompletedAuthorisedLogonTransactionAggregate); + Mock feeCalculationManager = new Mock(); + Mock estateClient = new Mock(); + + Mock securityServiceClient = new Mock(); + + IConfigurationRoot configurationRoot = new ConfigurationBuilder().AddInMemoryCollection(TestData.DefaultAppSettings).Build(); + ConfigurationReader.Initialise(configurationRoot); + Logger.Initialise(NullLogger.Instance); + + TransactionDomainEventHandler transactionDomainEventHandler = new TransactionDomainEventHandler(transactionAggregateManager.Object, + feeCalculationManager.Object, + estateClient.Object, + securityServiceClient.Object); + + await transactionDomainEventHandler.Handle(TestData.TransactionHasBeenCompletedEvent, CancellationToken.None); + } + } +} diff --git a/TransactionProcessor.BusinessLogic.Tests/Manager/FeeCalculationManagerTests.cs b/TransactionProcessor.BusinessLogic.Tests/Manager/FeeCalculationManagerTests.cs new file mode 100644 index 00000000..ef8d0a93 --- /dev/null +++ b/TransactionProcessor.BusinessLogic.Tests/Manager/FeeCalculationManagerTests.cs @@ -0,0 +1,330 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace TransactionProcessor.BusinessLogic.Tests.Manager +{ + using System.Linq; + using BusinessLogic.Manager; + using EventHandling; + using Models; + using Shouldly; + using Xunit; + + public class FeeCalculationManagerTests + { + [Fact] + public void FeeCalculationManager_CalculateFees_SingleFixedFee_ServiceFee_FeesAreCalculated() + { + IFeeCalculationManager manager = new FeeCalculationManager(); + + List feesList = new List + { + FeeCalculationManagerTestData.FixedServiceFee5 + }; + + List calculatedFees = manager.CalculateFees(feesList, FeeCalculationManagerTestData.TransactionAmount100); + + calculatedFees.ShouldHaveSingleItem(); + CalculatedFee calculatedFee = calculatedFees.Single(); + calculatedFee.CalculatedValue.ShouldBe(5.0m); + calculatedFee.FeeType.ShouldBe(FeeCalculationManagerTestData.FixedServiceFee5.FeeType); + calculatedFee.FeeCalculationType.ShouldBe(FeeCalculationManagerTestData.FixedServiceFee5.CalculationType); + calculatedFee.FeeId.ShouldBe(FeeCalculationManagerTestData.FixedServiceFee5.FeeId); + calculatedFee.FeeValue.ShouldBe(FeeCalculationManagerTestData.FixedServiceFee5.Value); + } + + [Fact] + public void FeeCalculationManager_CalculateFees_MultipleFixedFees_ServiceFee_FeesAreCalculated() + { + IFeeCalculationManager manager = new FeeCalculationManager(); + + List feesList = new List + { + FeeCalculationManagerTestData.FixedServiceFee5, + FeeCalculationManagerTestData.FixedServiceFee2 + }; + + List calculatedFees = manager.CalculateFees(feesList, FeeCalculationManagerTestData.TransactionAmount100); + + calculatedFees.Count.ShouldBe(2); + calculatedFees[0].CalculatedValue.ShouldBe(5.0m); + calculatedFees[0].FeeType.ShouldBe(FeeType.ServiceProvider); + calculatedFees[0].FeeType.ShouldBe(FeeCalculationManagerTestData.FixedServiceFee5.FeeType); + calculatedFees[0].FeeCalculationType.ShouldBe(FeeCalculationManagerTestData.FixedServiceFee5.CalculationType); + calculatedFees[0].FeeId.ShouldBe(FeeCalculationManagerTestData.FixedServiceFee5.FeeId); + calculatedFees[0].FeeValue.ShouldBe(FeeCalculationManagerTestData.FixedServiceFee5.Value); + calculatedFees[1].CalculatedValue.ShouldBe(2.0m); + calculatedFees[1].FeeType.ShouldBe(FeeType.ServiceProvider); + calculatedFees[1].FeeType.ShouldBe(FeeCalculationManagerTestData.FixedServiceFee2.FeeType); + calculatedFees[1].FeeCalculationType.ShouldBe(FeeCalculationManagerTestData.FixedServiceFee2.CalculationType); + calculatedFees[1].FeeId.ShouldBe(FeeCalculationManagerTestData.FixedServiceFee2.FeeId); + calculatedFees[1].FeeValue.ShouldBe(FeeCalculationManagerTestData.FixedServiceFee2.Value); + } + + [Fact] + public void FeeCalculationManager_CalculateFees_SinglePercentageFee_ServiceFee_FeesAreCalculated() + { + IFeeCalculationManager manager = new FeeCalculationManager(); + + List feesList = new List + { + FeeCalculationManagerTestData.PercentageServiceFeeQuarterPercent + }; + + List calculatedFees = manager.CalculateFees(feesList, FeeCalculationManagerTestData.TransactionAmount100); + + calculatedFees.ShouldHaveSingleItem(); + CalculatedFee calculatedFee = calculatedFees.Single(); + calculatedFee.CalculatedValue.ShouldBe(0.25m); + calculatedFee.FeeType.ShouldBe(FeeCalculationManagerTestData.PercentageServiceFeeQuarterPercent.FeeType); + calculatedFee.FeeCalculationType.ShouldBe(FeeCalculationManagerTestData.PercentageServiceFeeQuarterPercent.CalculationType); + calculatedFee.FeeId.ShouldBe(FeeCalculationManagerTestData.PercentageServiceFeeQuarterPercent.FeeId); + calculatedFee.FeeValue.ShouldBe(FeeCalculationManagerTestData.PercentageServiceFeeQuarterPercent.Value); + } + + [Fact] + public void FeeCalculationManager_CalculateFees_MultiplePercentageFees_ServiceFee_FeesAreCalculated() + { + IFeeCalculationManager manager = new FeeCalculationManager(); + + List feesList = new List + { + FeeCalculationManagerTestData.PercentageServiceFeeQuarterPercent, + FeeCalculationManagerTestData.PercentageServiceFeeThreeQuarterPercent + }; + + List calculatedFees = manager.CalculateFees(feesList, FeeCalculationManagerTestData.TransactionAmount100); + + calculatedFees[0].CalculatedValue.ShouldBe(0.25m); + calculatedFees[0].FeeType.ShouldBe(FeeType.ServiceProvider); + calculatedFees[0].FeeType.ShouldBe(FeeCalculationManagerTestData.PercentageServiceFeeQuarterPercent.FeeType); + calculatedFees[0].FeeCalculationType.ShouldBe(FeeCalculationManagerTestData.PercentageServiceFeeQuarterPercent.CalculationType); + calculatedFees[0].FeeId.ShouldBe(FeeCalculationManagerTestData.PercentageServiceFeeQuarterPercent.FeeId); + calculatedFees[0].FeeValue.ShouldBe(FeeCalculationManagerTestData.PercentageServiceFeeQuarterPercent.Value); + calculatedFees[1].CalculatedValue.ShouldBe(0.75m); + calculatedFees[1].FeeType.ShouldBe(FeeType.ServiceProvider); + calculatedFees[1].FeeType.ShouldBe(FeeCalculationManagerTestData.PercentageServiceFeeThreeQuarterPercent.FeeType); + calculatedFees[1].FeeCalculationType.ShouldBe(FeeCalculationManagerTestData.PercentageServiceFeeThreeQuarterPercent.CalculationType); + calculatedFees[1].FeeId.ShouldBe(FeeCalculationManagerTestData.PercentageServiceFeeThreeQuarterPercent.FeeId); + calculatedFees[1].FeeValue.ShouldBe(FeeCalculationManagerTestData.PercentageServiceFeeThreeQuarterPercent.Value); + } + + [Fact] + public void FeeCalculationManager_CalculateFees_MultipleFeesFixedAndPercentage_ServiceFee_FeesAreCalculated() + { + IFeeCalculationManager manager = new FeeCalculationManager(); + + List feesList = new List + { + FeeCalculationManagerTestData.PercentageServiceFeeQuarterPercent, + FeeCalculationManagerTestData.FixedServiceFee2 + }; + + List calculatedFees = manager.CalculateFees(feesList, FeeCalculationManagerTestData.TransactionAmount100); + + calculatedFees[0].CalculatedValue.ShouldBe(0.25m); + calculatedFees[0].FeeType.ShouldBe(FeeType.ServiceProvider); + calculatedFees[0].FeeType.ShouldBe(FeeCalculationManagerTestData.PercentageServiceFeeQuarterPercent.FeeType); + calculatedFees[0].FeeCalculationType.ShouldBe(FeeCalculationManagerTestData.PercentageServiceFeeQuarterPercent.CalculationType); + calculatedFees[0].FeeId.ShouldBe(FeeCalculationManagerTestData.PercentageServiceFeeQuarterPercent.FeeId); + calculatedFees[0].FeeValue.ShouldBe(FeeCalculationManagerTestData.PercentageServiceFeeQuarterPercent.Value); + calculatedFees[1].CalculatedValue.ShouldBe(2.0m); + calculatedFees[1].FeeType.ShouldBe(FeeType.ServiceProvider); + calculatedFees[1].FeeType.ShouldBe(FeeCalculationManagerTestData.FixedServiceFee2.FeeType); + calculatedFees[1].FeeCalculationType.ShouldBe(FeeCalculationManagerTestData.FixedServiceFee2.CalculationType); + calculatedFees[1].FeeId.ShouldBe(FeeCalculationManagerTestData.FixedServiceFee2.FeeId); + calculatedFees[1].FeeValue.ShouldBe(FeeCalculationManagerTestData.FixedServiceFee2.Value); + } + + [Fact] + public void FeeCalculationManager_CalculateFees_SingleFixedFee_MerchantFee_FeesAreCalculated() + { + IFeeCalculationManager manager = new FeeCalculationManager(); + + List feesList = new List + { + FeeCalculationManagerTestData.FixedMerchantFee5 + }; + + List calculatedFees = manager.CalculateFees(feesList, FeeCalculationManagerTestData.TransactionAmount100); + + calculatedFees.ShouldHaveSingleItem(); + CalculatedFee calculatedFee = calculatedFees.Single(); + calculatedFee.CalculatedValue.ShouldBe(5.0m); + calculatedFee.FeeType.ShouldBe(FeeCalculationManagerTestData.FixedMerchantFee5.FeeType); + calculatedFee.FeeCalculationType.ShouldBe(FeeCalculationManagerTestData.FixedMerchantFee5.CalculationType); + calculatedFee.FeeId.ShouldBe(FeeCalculationManagerTestData.FixedMerchantFee5.FeeId); + calculatedFee.FeeValue.ShouldBe(FeeCalculationManagerTestData.FixedMerchantFee5.Value); + } + + [Fact] + public void FeeCalculationManager_CalculateFees_MultipleFixedFees_MerchantFee_FeesAreCalculated() + { + IFeeCalculationManager manager = new FeeCalculationManager(); + + List feesList = new List + { + FeeCalculationManagerTestData.FixedMerchantFee5, + FeeCalculationManagerTestData.FixedMerchantFee2 + }; + + List calculatedFees = manager.CalculateFees(feesList, FeeCalculationManagerTestData.TransactionAmount100); + + calculatedFees.Count.ShouldBe(2); + calculatedFees[0].CalculatedValue.ShouldBe(5.0m); + calculatedFees[0].FeeType.ShouldBe(FeeCalculationManagerTestData.FixedMerchantFee5.FeeType); + calculatedFees[0].FeeCalculationType.ShouldBe(FeeCalculationManagerTestData.FixedMerchantFee5.CalculationType); + calculatedFees[0].FeeId.ShouldBe(FeeCalculationManagerTestData.FixedMerchantFee5.FeeId); + calculatedFees[0].FeeValue.ShouldBe(FeeCalculationManagerTestData.FixedMerchantFee5.Value); + calculatedFees[1].CalculatedValue.ShouldBe(2.0m); + calculatedFees[1].FeeType.ShouldBe(FeeCalculationManagerTestData.FixedMerchantFee2.FeeType); + calculatedFees[1].FeeCalculationType.ShouldBe(FeeCalculationManagerTestData.FixedMerchantFee2.CalculationType); + calculatedFees[1].FeeId.ShouldBe(FeeCalculationManagerTestData.FixedMerchantFee2.FeeId); + calculatedFees[1].FeeValue.ShouldBe(FeeCalculationManagerTestData.FixedMerchantFee2.Value); + } + + [Fact] + public void FeeCalculationManager_CalculateFees_SinglePercentageFee_MerchantFee_FeesAreCalculated() + { + IFeeCalculationManager manager = new FeeCalculationManager(); + + List feesList = new List + { + FeeCalculationManagerTestData.PercentageMerchantFeeQuarterPercent + }; + + List calculatedFees = manager.CalculateFees(feesList, FeeCalculationManagerTestData.TransactionAmount100); + + calculatedFees.ShouldHaveSingleItem(); + CalculatedFee calculatedFee = calculatedFees.Single(); + calculatedFee.CalculatedValue.ShouldBe(0.25m); + calculatedFee.FeeType.ShouldBe(FeeCalculationManagerTestData.PercentageMerchantFeeQuarterPercent.FeeType); + calculatedFee.FeeCalculationType.ShouldBe(FeeCalculationManagerTestData.PercentageMerchantFeeQuarterPercent.CalculationType); + calculatedFee.FeeId.ShouldBe(FeeCalculationManagerTestData.PercentageMerchantFeeQuarterPercent.FeeId); + calculatedFee.FeeValue.ShouldBe(FeeCalculationManagerTestData.PercentageMerchantFeeQuarterPercent.Value); + } + + [Fact] + public void FeeCalculationManager_CalculateFees_MultiplePercentageFees_MerchantFee_FeesAreCalculated() + { + IFeeCalculationManager manager = new FeeCalculationManager(); + + List feesList = new List + { + FeeCalculationManagerTestData.PercentageMerchantFeeQuarterPercent, + FeeCalculationManagerTestData.PercentageMerchantFeeThreeQuarterPercent + }; + + List calculatedFees = manager.CalculateFees(feesList, FeeCalculationManagerTestData.TransactionAmount100); + + calculatedFees.Count.ShouldBe(2); + calculatedFees[0].CalculatedValue.ShouldBe(0.25m); + calculatedFees[0].FeeType.ShouldBe(FeeCalculationManagerTestData.PercentageMerchantFeeQuarterPercent.FeeType); + calculatedFees[0].FeeCalculationType.ShouldBe(FeeCalculationManagerTestData.PercentageMerchantFeeQuarterPercent.CalculationType); + calculatedFees[0].FeeId.ShouldBe(FeeCalculationManagerTestData.PercentageMerchantFeeQuarterPercent.FeeId); + calculatedFees[0].FeeValue.ShouldBe(FeeCalculationManagerTestData.PercentageMerchantFeeQuarterPercent.Value); + calculatedFees[1].CalculatedValue.ShouldBe(0.75m); + calculatedFees[1].FeeType.ShouldBe(FeeCalculationManagerTestData.PercentageMerchantFeeThreeQuarterPercent.FeeType); + calculatedFees[1].FeeCalculationType.ShouldBe(FeeCalculationManagerTestData.PercentageMerchantFeeThreeQuarterPercent.CalculationType); + calculatedFees[1].FeeId.ShouldBe(FeeCalculationManagerTestData.PercentageMerchantFeeThreeQuarterPercent.FeeId); + calculatedFees[1].FeeValue.ShouldBe(FeeCalculationManagerTestData.PercentageMerchantFeeThreeQuarterPercent.Value); + } + + [Fact] + public void FeeCalculationManager_CalculateFees_MultipleFeesFixedAndPercentage_MerchantFee_FeesAreCalculated() + { + IFeeCalculationManager manager = new FeeCalculationManager(); + + List feesList = new List + { + FeeCalculationManagerTestData.PercentageMerchantFeeQuarterPercent, + FeeCalculationManagerTestData.FixedMerchantFee2 + }; + + List calculatedFees = manager.CalculateFees(feesList, FeeCalculationManagerTestData.TransactionAmount100); + + calculatedFees.Count.ShouldBe(2); + calculatedFees[0].CalculatedValue.ShouldBe(0.25m); + calculatedFees[0].FeeType.ShouldBe(FeeCalculationManagerTestData.PercentageMerchantFeeQuarterPercent.FeeType); + calculatedFees[0].FeeCalculationType.ShouldBe(FeeCalculationManagerTestData.PercentageMerchantFeeQuarterPercent.CalculationType); + calculatedFees[0].FeeId.ShouldBe(FeeCalculationManagerTestData.PercentageMerchantFeeQuarterPercent.FeeId); + calculatedFees[0].FeeValue.ShouldBe(FeeCalculationManagerTestData.PercentageMerchantFeeQuarterPercent.Value); + calculatedFees[1].CalculatedValue.ShouldBe(2.0m); + calculatedFees[1].FeeType.ShouldBe(FeeCalculationManagerTestData.FixedMerchantFee2.FeeType); + calculatedFees[1].FeeCalculationType.ShouldBe(FeeCalculationManagerTestData.FixedMerchantFee2.CalculationType); + calculatedFees[1].FeeId.ShouldBe(FeeCalculationManagerTestData.FixedMerchantFee2.FeeId); + calculatedFees[1].FeeValue.ShouldBe(FeeCalculationManagerTestData.FixedMerchantFee2.Value); + } + } + + public static class FeeCalculationManagerTestData + { + public static Decimal TransactionAmount100 = 100.00m; + + public static TransactionFeeToCalculate FixedServiceFee5 = new TransactionFeeToCalculate + { + Value = 5.0m, + CalculationType = CalculationType.Fixed, + FeeType = FeeType.ServiceProvider, + FeeId = Guid.Parse("BB321B3A-CA36-40DD-8E75-DD5C3B0A02E7"), + }; + + public static TransactionFeeToCalculate FixedMerchantFee5 = new TransactionFeeToCalculate + { + Value = 5.0m, + CalculationType = CalculationType.Fixed, + FeeType = FeeType.Merchant, + FeeId = Guid.Parse("D24C1645-447F-433B-8CB4-10F03532211A") + }; + + public static TransactionFeeToCalculate FixedServiceFee2 = new TransactionFeeToCalculate + { + Value = 2.0m, + CalculationType = CalculationType.Fixed, + FeeType = FeeType.ServiceProvider, + FeeId =Guid.Parse("905F6DF5-0F00-45F4-B139-AA5965BE522A") + }; + + public static TransactionFeeToCalculate FixedMerchantFee2 = new TransactionFeeToCalculate + { + Value = 2.0m, + CalculationType = CalculationType.Fixed, + FeeType = FeeType.Merchant, + FeeId = Guid.Parse("12DF5128-8DDA-40A2-B34E-A20353D53EE8") + }; + + public static TransactionFeeToCalculate PercentageServiceFeeQuarterPercent = new TransactionFeeToCalculate + { + Value = 0.25m, + CalculationType = CalculationType.Percentage, + FeeType = FeeType.ServiceProvider, + FeeId = Guid.Parse("C3CA3208-BBE1-4155-A64D-465DD94A1C9B") + }; + + public static TransactionFeeToCalculate PercentageMerchantFeeQuarterPercent = new TransactionFeeToCalculate + { + Value = 0.25m, + CalculationType = CalculationType.Percentage, + FeeType = FeeType.Merchant, + FeeId = Guid.Parse("DD737732-3B94-480F-A98C-58AF60BCD4AF") + }; + + public static TransactionFeeToCalculate PercentageServiceFeeThreeQuarterPercent = new TransactionFeeToCalculate + { + Value = 0.75m, + CalculationType = CalculationType.Percentage, + FeeType = FeeType.ServiceProvider, + FeeId = Guid.Parse("2CD69A2A-E04A-42E2-B01C-706AEE172B80") + }; + + public static TransactionFeeToCalculate PercentageMerchantFeeThreeQuarterPercent = new TransactionFeeToCalculate + { + Value = 0.75m, + CalculationType = CalculationType.Percentage, + FeeType = FeeType.Merchant, + FeeId = Guid.Parse("25E12CD8-5F1D-4A78-80D9-7CC4B0D64E50") + }; + + } +} diff --git a/TransactionProcessor.BusinessLogic.Tests/Services/TransactionAggregateManagerTests.cs b/TransactionProcessor.BusinessLogic.Tests/Services/TransactionAggregateManagerTests.cs index 36287b96..ce5d0d88 100644 --- a/TransactionProcessor.BusinessLogic.Tests/Services/TransactionAggregateManagerTests.cs +++ b/TransactionProcessor.BusinessLogic.Tests/Services/TransactionAggregateManagerTests.cs @@ -4,6 +4,7 @@ namespace TransactionProcessor.BusinessLogic.Tests.Services { + using System.Linq; using System.Threading; using System.Threading.Tasks; using BusinessLogic.Services; @@ -80,7 +81,7 @@ await transactionAggregateManager.DeclineTransactionLocally(TestData.EstateId, public async Task TransactionAggregateManager_GetAggregate_AggregateReturned() { Mock> aggregateRepository = new Mock>(); - aggregateRepository.Setup(a => a.GetLatestVersion(It.IsAny(), It.IsAny())).ReturnsAsync(TestData.GetCompletedTransactionAggregate); + aggregateRepository.Setup(a => a.GetLatestVersion(It.IsAny(), It.IsAny())).ReturnsAsync(TestData.GetCompletedLogonTransactionAggregate); TransactionAggregateManager transactionAggregateManager = new TransactionAggregateManager(aggregateRepository.Object); TransactionAggregate result = await transactionAggregateManager.GetAggregate(TestData.EstateId, @@ -211,7 +212,7 @@ await transactionAggregateManager.CompleteTransaction(TestData.EstateId, public async Task TransactionAggregateManager_RequestEmailReceipt_EmailRecieptRequested() { Mock> aggregateRepository = new Mock>(); - aggregateRepository.Setup(a => a.GetLatestVersion(It.IsAny(), It.IsAny())).ReturnsAsync(TestData.GetCompletedTransactionAggregate); + aggregateRepository.Setup(a => a.GetLatestVersion(It.IsAny(), It.IsAny())).ReturnsAsync(TestData.GetCompletedLogonTransactionAggregate); TransactionAggregateManager transactionAggregateManager = new TransactionAggregateManager(aggregateRepository.Object); await transactionAggregateManager.RequestEmailReceipt(TestData.EstateId, @@ -233,5 +234,18 @@ await transactionAggregateManager.AddProductDetails(TestData.EstateId, TestData.ProductId, CancellationToken.None); } + + [Fact] + public async Task TransactionAggregateManager_AddFee_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.AddFee(TestData.EstateId, + TestData.TransactionId, + TestData.CalculatedMerchantFees.First(), + CancellationToken.None); + } } } diff --git a/TransactionProcessor.BusinessLogic.Tests/Services/TransactionDomainServiceTests.cs b/TransactionProcessor.BusinessLogic.Tests/Services/TransactionDomainServiceTests.cs index fbaf245a..b82df017 100644 --- a/TransactionProcessor.BusinessLogic.Tests/Services/TransactionDomainServiceTests.cs +++ b/TransactionProcessor.BusinessLogic.Tests/Services/TransactionDomainServiceTests.cs @@ -43,7 +43,7 @@ public async Task TransactionDomainService_ProcessLogonTransaction_TransactionIs 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.GetCompletedTransactionAggregate); + .ReturnsAsync(TestData.GetCompletedLogonTransactionAggregate); Mock operatorProxy = new Mock(); Func operatorProxyResolver = (operatorName) => { return operatorProxy.Object; }; @@ -82,7 +82,7 @@ public async Task TransactionDomainService_ProcessLogonTransaction_MerchantWithN estateClient.Setup(e => e.GetMerchant(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(TestData.GetMerchantResponseWithNullDevices); transactionAggregateManager.Setup(t => t.GetAggregate(It.IsAny(), It.IsAny(), It.IsAny())) - .ReturnsAsync(TestData.GetCompletedTransactionAggregate); + .ReturnsAsync(TestData.GetCompletedLogonTransactionAggregate); TransactionDomainService transactionDomainService = new TransactionDomainService(transactionAggregateManager.Object, estateClient.Object, securityServiceClient.Object, @@ -119,7 +119,7 @@ public async Task TransactionDomainService_ProcessLogonTransaction_MerchantWithN estateClient.Setup(e => e.GetMerchant(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(TestData.GetMerchantResponseWithNoDevices); transactionAggregateManager.Setup(t => t.GetAggregate(It.IsAny(), It.IsAny(), It.IsAny())) - .ReturnsAsync(TestData.GetCompletedTransactionAggregate); + .ReturnsAsync(TestData.GetCompletedLogonTransactionAggregate); TransactionDomainService transactionDomainService = new TransactionDomainService(transactionAggregateManager.Object, estateClient.Object, securityServiceClient.Object, @@ -261,7 +261,7 @@ public async Task TransactionDomainService_ProcessSaleTransaction_SuccesfulOpera 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.GetCompletedTransactionAggregate); + .ReturnsAsync(TestData.GetCompletedAuthorisedSaleTransactionAggregate); Mock operatorProxy = new Mock(); operatorProxy.Setup(o => o.ProcessSaleMessage(It.IsAny(), diff --git a/TransactionProcessor.BusinessLogic.Tests/TransactionProcessor.BusinessLogic.Tests.csproj b/TransactionProcessor.BusinessLogic.Tests/TransactionProcessor.BusinessLogic.Tests.csproj index bc07a8c4..ca9e1665 100644 --- a/TransactionProcessor.BusinessLogic.Tests/TransactionProcessor.BusinessLogic.Tests.csproj +++ b/TransactionProcessor.BusinessLogic.Tests/TransactionProcessor.BusinessLogic.Tests.csproj @@ -22,10 +22,6 @@ - - - - diff --git a/TransactionProcessor.BusinessLogic/EventHandling/DomainEventHandlerResolver.cs b/TransactionProcessor.BusinessLogic/EventHandling/DomainEventHandlerResolver.cs new file mode 100644 index 00000000..393f014c --- /dev/null +++ b/TransactionProcessor.BusinessLogic/EventHandling/DomainEventHandlerResolver.cs @@ -0,0 +1,99 @@ +namespace MessagingService.BusinessLogic.EventHandling +{ + using System; + using System.Collections.Generic; + using System.Linq; + using Shared.DomainDrivenDesign.EventSourcing; + + public class DomainEventHandlerResolver : IDomainEventHandlerResolver + { + #region Fields + + /// + /// The domain event handlers + /// + private readonly Dictionary DomainEventHandlers; + + /// + /// The event handler configuration + /// + private readonly Dictionary EventHandlerConfiguration; + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The event handler configuration. + public DomainEventHandlerResolver(Dictionary eventHandlerConfiguration, Func createEventHandlerResolver) + { + this.EventHandlerConfiguration = eventHandlerConfiguration; + + this.DomainEventHandlers = new Dictionary(); + + List handlers = new List(); + + // Precreate the Event Handlers here + foreach (KeyValuePair handlerConfig in eventHandlerConfiguration) + { + handlers.AddRange(handlerConfig.Value); + } + + IEnumerable distinctHandlers = handlers.Distinct(); + + foreach (String handlerTypeString in distinctHandlers) + { + Type handlerType = Type.GetType(handlerTypeString); + + if (handlerType == null) + { + throw new NotSupportedException("Event handler configuration is not for a valid type"); + } + + IDomainEventHandler eventHandler = createEventHandlerResolver(handlerType); + this.DomainEventHandlers.Add(handlerTypeString, eventHandler); + } + } + + #endregion + + #region Methods + + /// + /// Gets the domain event handlers. + /// + /// The domain event. + /// + public List GetDomainEventHandlers(DomainEvent domainEvent) + { + // Get the type of the event passed in + String typeString = domainEvent.GetType().FullName; + + // Lookup the list + Boolean eventIsConfigured = this.EventHandlerConfiguration.ContainsKey(typeString); + + if (!eventIsConfigured) + { + // No handlers setup, return null and let the caller decide what to do next + return null; + } + + String[] handlers = this.EventHandlerConfiguration[typeString]; + + List handlersToReturn = new List(); + + foreach (String handler in handlers) + { + List> foundHandlers = this.DomainEventHandlers.Where(h => h.Key == handler).ToList(); + + handlersToReturn.AddRange(foundHandlers.Select(x => x.Value)); + } + + return handlersToReturn; + } + + #endregion + } +} \ No newline at end of file diff --git a/TransactionProcessor.BusinessLogic/EventHandling/IDomainEventHandler.cs b/TransactionProcessor.BusinessLogic/EventHandling/IDomainEventHandler.cs new file mode 100644 index 00000000..494f396e --- /dev/null +++ b/TransactionProcessor.BusinessLogic/EventHandling/IDomainEventHandler.cs @@ -0,0 +1,22 @@ +namespace MessagingService.BusinessLogic.EventHandling +{ + using System.Threading; + using System.Threading.Tasks; + using Shared.DomainDrivenDesign.EventSourcing; + + public interface IDomainEventHandler + { + #region Methods + + /// + /// Handles the specified domain event. + /// + /// The domain event. + /// The cancellation token. + /// + Task Handle(DomainEvent domainEvent, + CancellationToken cancellationToken); + + #endregion + } +} \ No newline at end of file diff --git a/TransactionProcessor.BusinessLogic/EventHandling/IDomainEventHandlerResolver.cs b/TransactionProcessor.BusinessLogic/EventHandling/IDomainEventHandlerResolver.cs new file mode 100644 index 00000000..62db6f39 --- /dev/null +++ b/TransactionProcessor.BusinessLogic/EventHandling/IDomainEventHandlerResolver.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.Text; + +namespace MessagingService.BusinessLogic.EventHandling +{ + using Shared.DomainDrivenDesign.EventSourcing; + + public interface IDomainEventHandlerResolver + { + #region Methods + + /// + /// Gets the domain event handlers. + /// + /// The domain event. + /// + List GetDomainEventHandlers(DomainEvent domainEvent); + + #endregion + } +} diff --git a/TransactionProcessor.BusinessLogic/EventHandling/TransactionDomainEventHandler.cs b/TransactionProcessor.BusinessLogic/EventHandling/TransactionDomainEventHandler.cs new file mode 100644 index 00000000..416b375c --- /dev/null +++ b/TransactionProcessor.BusinessLogic/EventHandling/TransactionDomainEventHandler.cs @@ -0,0 +1,184 @@ +namespace TransactionProcessor.BusinessLogic.EventHandling +{ + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Threading; + using System.Threading.Tasks; + using EstateManagement.Client; + using EstateManagement.DataTransferObjects.Responses; + using Manager; + using MessagingService.BusinessLogic.EventHandling; + using Models; + using SecurityService.Client; + using SecurityService.DataTransferObjects.Responses; + using Services; + using Shared.DomainDrivenDesign.EventSourcing; + using Shared.General; + using Shared.Logger; + using Transaction.DomainEvents; + using TransactionAggregate; + + /// + /// + /// + /// + public class TransactionDomainEventHandler : IDomainEventHandler + { + #region Fields + + /// + /// The estate client + /// + private readonly IEstateClient EstateClient; + + /// + /// The fee calculation manager + /// + private readonly IFeeCalculationManager FeeCalculationManager; + + /// + /// The security service client + /// + private readonly ISecurityServiceClient SecurityServiceClient; + + /// + /// The token response + /// + private TokenResponse TokenResponse; + + /// + /// The transaction aggregate manager + /// + private readonly ITransactionAggregateManager TransactionAggregateManager; + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The transaction aggregate manager. + /// The fee calculation manager. + /// The estate client. + /// The security service client. + public TransactionDomainEventHandler(ITransactionAggregateManager transactionAggregateManager, + IFeeCalculationManager feeCalculationManager, + IEstateClient estateClient, + ISecurityServiceClient securityServiceClient) + { + this.TransactionAggregateManager = transactionAggregateManager; + this.FeeCalculationManager = feeCalculationManager; + this.EstateClient = estateClient; + this.SecurityServiceClient = securityServiceClient; + } + + #endregion + + #region Methods + + /// + /// Handles the specified domain event. + /// + /// The domain event. + /// The cancellation token. + public async Task Handle(DomainEvent domainEvent, + CancellationToken cancellationToken) + { + await this.HandleSpecificDomainEvent((dynamic)domainEvent, cancellationToken); + } + + /// + /// Gets the token. + /// + /// The cancellation token. + /// + [ExcludeFromCodeCoverage] + private async Task GetToken(CancellationToken cancellationToken) + { + // Get a token to talk to the estate service + String clientId = ConfigurationReader.GetValue("AppSettings", "ClientId"); + String clientSecret = ConfigurationReader.GetValue("AppSettings", "ClientSecret"); + Logger.LogInformation($"Client Id is {clientId}"); + Logger.LogInformation($"Client Secret is {clientSecret}"); + + if (this.TokenResponse == null) + { + TokenResponse token = await this.SecurityServiceClient.GetToken(clientId, clientSecret, cancellationToken); + Logger.LogInformation($"Token is {token.AccessToken}"); + return token; + } + + if (this.TokenResponse.Expires.UtcDateTime.Subtract(DateTime.UtcNow) < TimeSpan.FromMinutes(2)) + { + Logger.LogInformation($"Token is about to expire at {this.TokenResponse.Expires.DateTime:O}"); + TokenResponse token = await this.SecurityServiceClient.GetToken(clientId, clientSecret, cancellationToken); + Logger.LogInformation($"Token is {token.AccessToken}"); + return token; + } + + return this.TokenResponse; + } + + /// + /// Handles the specific domain event. + /// + /// The domain event. + /// The cancellation token. + private async Task HandleSpecificDomainEvent(TransactionHasBeenCompletedEvent domainEvent, + CancellationToken cancellationToken) + { + TransactionAggregate transactionAggregate = + await this.TransactionAggregateManager.GetAggregate(domainEvent.EstateId, domainEvent.TransactionId, cancellationToken); + + if (transactionAggregate.IsAuthorised == false) + { + // Ignore not successful transactions + return; + } + + if (transactionAggregate.IsCompleted == false || transactionAggregate.TransactionType == TransactionType.Logon || + (transactionAggregate.ContractId == Guid.Empty || transactionAggregate.ProductId == Guid.Empty)) + { + // These transactions cannot have fee values calculated so skip + return; + } + + this.TokenResponse = await this.GetToken(cancellationToken); + // Ok we should have filtered out the not applicable transactions + // Get the fees to be calculated + List feesForProduct = await this.EstateClient.GetTransactionFeesForProduct(this.TokenResponse.AccessToken, + transactionAggregate.EstateId, + transactionAggregate.MerchantId, + transactionAggregate.ContractId, + transactionAggregate.ProductId, + cancellationToken); + List feesForCalculation = new List(); + + foreach (ContractProductTransactionFee contractProductTransactionFee in feesForProduct) + { + TransactionFeeToCalculate transactionFeeToCalculate = new TransactionFeeToCalculate + { + FeeId = contractProductTransactionFee.TransactionFeeId, + Value = contractProductTransactionFee.Value, + FeeType = (FeeType)contractProductTransactionFee.FeeType, + CalculationType = (CalculationType)contractProductTransactionFee.CalculationType + }; + + feesForCalculation.Add(transactionFeeToCalculate); + } + + // Do the fee calculation + List resultFees = this.FeeCalculationManager.CalculateFees(feesForCalculation, transactionAggregate.TransactionAmount.Value); + + foreach (CalculatedFee calculatedFee in resultFees) + { + // Add Fee to the Transaction + await this.TransactionAggregateManager.AddFee(transactionAggregate.EstateId, transactionAggregate.AggregateId, calculatedFee, cancellationToken); + } + } + + #endregion + } +} \ No newline at end of file diff --git a/TransactionProcessor.BusinessLogic/Manager/FeeCalculationManager.cs b/TransactionProcessor.BusinessLogic/Manager/FeeCalculationManager.cs new file mode 100644 index 00000000..4463c2f9 --- /dev/null +++ b/TransactionProcessor.BusinessLogic/Manager/FeeCalculationManager.cs @@ -0,0 +1,62 @@ +namespace TransactionProcessor.BusinessLogic.Manager +{ + using System; + using System.Collections.Generic; + using EventHandling; + using Models; + + /// + /// + /// + /// + public class FeeCalculationManager : IFeeCalculationManager + { + #region Methods + + /// + /// Calculates the fees. + /// + /// The fee list. + /// The transaction amount. + /// + public List CalculateFees(List feeList, + Decimal transactionAmount) + { + List calculatedFees = new List(); + + foreach (TransactionFeeToCalculate transactionFeeToCalculate in feeList) + { + if (transactionFeeToCalculate.CalculationType == CalculationType.Percentage) + { + // percentage fee + Decimal feeValue = (transactionFeeToCalculate.Value / 100) * transactionAmount; + calculatedFees.Add(new CalculatedFee + { + CalculatedValue = feeValue, + FeeType = transactionFeeToCalculate.FeeType, + FeeCalculationType = transactionFeeToCalculate.CalculationType, + FeeId = transactionFeeToCalculate.FeeId, + FeeValue = transactionFeeToCalculate.Value + }); + } + + if (transactionFeeToCalculate.CalculationType == CalculationType.Fixed) + { + // fixed value fee + calculatedFees.Add(new CalculatedFee + { + CalculatedValue = transactionFeeToCalculate.Value, + FeeType = transactionFeeToCalculate.FeeType, + FeeCalculationType = transactionFeeToCalculate.CalculationType, + FeeId = transactionFeeToCalculate.FeeId, + FeeValue = transactionFeeToCalculate.Value + }); + } + } + + return calculatedFees; + } + + #endregion + } +} \ No newline at end of file diff --git a/TransactionProcessor.BusinessLogic/Manager/IFeeCalculationManager.cs b/TransactionProcessor.BusinessLogic/Manager/IFeeCalculationManager.cs new file mode 100644 index 00000000..d97ffbe9 --- /dev/null +++ b/TransactionProcessor.BusinessLogic/Manager/IFeeCalculationManager.cs @@ -0,0 +1,26 @@ +namespace TransactionProcessor.BusinessLogic.Manager +{ + using System; + using System.Collections.Generic; + using EventHandling; + using Models; + + /// + /// + /// + public interface IFeeCalculationManager + { + #region Methods + + /// + /// Calculates the fees. + /// + /// The fee list. + /// The transaction amount. + /// + List CalculateFees(List feeList, + Decimal transactionAmount); + + #endregion + } +} \ No newline at end of file diff --git a/TransactionProcessor.BusinessLogic/Manager/TransactionFeeToCalculate.cs b/TransactionProcessor.BusinessLogic/Manager/TransactionFeeToCalculate.cs new file mode 100644 index 00000000..b4d7aa94 --- /dev/null +++ b/TransactionProcessor.BusinessLogic/Manager/TransactionFeeToCalculate.cs @@ -0,0 +1,49 @@ +namespace TransactionProcessor.BusinessLogic.Manager +{ + using System; + using System.Diagnostics.CodeAnalysis; + using Models; + + /// + /// + /// + [ExcludeFromCodeCoverage] + public class TransactionFeeToCalculate + { + #region Properties + + /// + /// Gets or sets the type of the calculation. + /// + /// + /// The type of the calculation. + /// + public CalculationType CalculationType { get; set; } + + /// + /// Gets or sets the fee identifier. + /// + /// + /// The fee identifier. + /// + public Guid FeeId { get; set; } + + /// + /// Gets or sets the type of the fee. + /// + /// + /// The type of the fee. + /// + public FeeType FeeType { get; set; } + + /// + /// Gets or sets the value. + /// + /// + /// The value. + /// + public Decimal Value { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/TransactionProcessor.BusinessLogic/Services/ITransactionAggregateManager.cs b/TransactionProcessor.BusinessLogic/Services/ITransactionAggregateManager.cs index ecbbce9f..455c0f4e 100644 --- a/TransactionProcessor.BusinessLogic/Services/ITransactionAggregateManager.cs +++ b/TransactionProcessor.BusinessLogic/Services/ITransactionAggregateManager.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; + using EventHandling; using Models; using OperatorInterfaces; using TransactionAggregate; @@ -30,6 +31,19 @@ Task AddProductDetails(Guid estateId, Guid productId, CancellationToken cancellationToken); + /// + /// Adds the fee. + /// + /// The estate identifier. + /// The transaction identifier. + /// The calculated fee. + /// The cancellation token. + /// + Task AddFee(Guid estateId, + Guid transactionId, + CalculatedFee calculatedFee, + CancellationToken cancellationToken); + /// /// Authorises the transaction. /// diff --git a/TransactionProcessor.BusinessLogic/Services/TransactionAggregateManager.cs b/TransactionProcessor.BusinessLogic/Services/TransactionAggregateManager.cs index f32fa01b..4be47eb7 100644 --- a/TransactionProcessor.BusinessLogic/Services/TransactionAggregateManager.cs +++ b/TransactionProcessor.BusinessLogic/Services/TransactionAggregateManager.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; + using EventHandling; using Microsoft.EntityFrameworkCore.Internal; using Models; using OperatorInterfaces; @@ -61,6 +62,26 @@ public async Task AddProductDetails(Guid estateId, await this.TransactionAggregateRepository.SaveChanges(transactionAggregate, cancellationToken); } + /// + /// Adds the fee. + /// + /// The estate identifier. + /// The transaction identifier. + /// The calculated fee. + /// The cancellation token. + public async Task AddFee(Guid estateId, + Guid transactionId, + CalculatedFee calculatedFee, + CancellationToken cancellationToken) + { + TransactionAggregate transactionAggregate = await this.TransactionAggregateRepository.GetLatestVersion(transactionId, cancellationToken); + + transactionAggregate.AddFee(calculatedFee); + + await this.TransactionAggregateRepository.SaveChanges(transactionAggregate, cancellationToken); + + } + /// /// Authorises the transaction. /// diff --git a/TransactionProcessor.BusinessLogic/TransactionProcessor.BusinessLogic.csproj b/TransactionProcessor.BusinessLogic/TransactionProcessor.BusinessLogic.csproj index dd39f933..e09f289b 100644 --- a/TransactionProcessor.BusinessLogic/TransactionProcessor.BusinessLogic.csproj +++ b/TransactionProcessor.BusinessLogic/TransactionProcessor.BusinessLogic.csproj @@ -5,7 +5,7 @@ - + @@ -13,10 +13,6 @@ - - - - diff --git a/TransactionProcessor.IntegrationTests/Common/DockerHelper.cs b/TransactionProcessor.IntegrationTests/Common/DockerHelper.cs index a6008c03..6f250593 100644 --- a/TransactionProcessor.IntegrationTests/Common/DockerHelper.cs +++ b/TransactionProcessor.IntegrationTests/Common/DockerHelper.cs @@ -364,12 +364,21 @@ protected async Task PopulateSubscriptionServiceConfiguration() // Create an Event Store Server await this.InsertEventStoreServer(connection, this.EventStoreContainerName).ConfigureAwait(false); - String endPointUri = $"http://{this.EstateReportingContainerName}:5005/api/domainevents"; + String reportingEndPointUri = $"http://{this.EstateReportingContainerName}:5005/api/domainevents"; + String transactionProcessorEndPointUri = $"http://{this.TransactionProcessorContainerName}:5002/api/domainevents"; + // Add Route for Estate Aggregate Events - await this.InsertSubscription(connection, "$ce-EstateAggregate", "Reporting", endPointUri).ConfigureAwait(false); + await this.InsertSubscription(connection, "$ce-EstateAggregate", "Reporting", reportingEndPointUri).ConfigureAwait(false); // Add Route for Merchant Aggregate Events - await this.InsertSubscription(connection, "$ce-MerchantAggregate", "Reporting", endPointUri).ConfigureAwait(false); + await this.InsertSubscription(connection, "$ce-MerchantAggregate", "Reporting", reportingEndPointUri).ConfigureAwait(false); + + // Add Route for Contract Aggregate Events + await this.InsertSubscription(connection, "$ce-ContractAggregate", "Reporting", reportingEndPointUri).ConfigureAwait(false); + + // Add Route for Transaction Aggregate Events + await this.InsertSubscription(connection, "$ce-TransactionAggregate", "Reporting", reportingEndPointUri).ConfigureAwait(false); + await this.InsertSubscription(connection, "$et-TransactionProcessor.Transaction.DomainEvents.TransactionHasBeenCompletedEvent", "Transaction Processor", transactionProcessorEndPointUri).ConfigureAwait(false); await connection.CloseAsync().ConfigureAwait(false); } diff --git a/TransactionProcessor.IntegrationTests/TransactionProcessor.IntegrationTests.csproj b/TransactionProcessor.IntegrationTests/TransactionProcessor.IntegrationTests.csproj index 165b8e83..3a6b2982 100644 --- a/TransactionProcessor.IntegrationTests/TransactionProcessor.IntegrationTests.csproj +++ b/TransactionProcessor.IntegrationTests/TransactionProcessor.IntegrationTests.csproj @@ -9,8 +9,8 @@ - - + + diff --git a/TransactionProcessor.Models/CalculatedFee.cs b/TransactionProcessor.Models/CalculatedFee.cs new file mode 100644 index 00000000..a7a9a976 --- /dev/null +++ b/TransactionProcessor.Models/CalculatedFee.cs @@ -0,0 +1,56 @@ +namespace TransactionProcessor.Models +{ + using System; + using System.Diagnostics.CodeAnalysis; + + /// + /// + /// + [ExcludeFromCodeCoverage] + public class CalculatedFee + { + #region Properties + + /// + /// Gets or sets the calculated value. + /// + /// + /// The calculated value. + /// + public Decimal CalculatedValue { get; set; } + + /// + /// Gets or sets the type of the fee calculation. + /// + /// + /// The type of the fee calculation. + /// + public CalculationType FeeCalculationType { get; set; } + + /// + /// Gets or sets the fee identifier. + /// + /// + /// The fee identifier. + /// + public Guid FeeId { get; set; } + + /// + /// Gets or sets the type of the fee. + /// + /// + /// The type of the fee. + /// + public FeeType FeeType { get; set; } + + /// + /// Gets or sets the fee value. + /// + /// + /// The fee value. + /// + public Decimal FeeValue { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/TransactionProcessor.Models/CalculationType.cs b/TransactionProcessor.Models/CalculationType.cs new file mode 100644 index 00000000..336ec006 --- /dev/null +++ b/TransactionProcessor.Models/CalculationType.cs @@ -0,0 +1,18 @@ +namespace TransactionProcessor.Models +{ + /// + /// + /// + public enum CalculationType + { + /// + /// The percentage + /// + Percentage, + + /// + /// The fixed + /// + Fixed + } +} \ No newline at end of file diff --git a/TransactionProcessor.Models/FeeType.cs b/TransactionProcessor.Models/FeeType.cs new file mode 100644 index 00000000..00d11116 --- /dev/null +++ b/TransactionProcessor.Models/FeeType.cs @@ -0,0 +1,18 @@ +namespace TransactionProcessor.Models +{ + /// + /// + /// + public enum FeeType + { + /// + /// The merchant + /// + Merchant, + + /// + /// The service provider + /// + ServiceProvider + } +} \ No newline at end of file diff --git a/TransactionProcessor.Models/TransactionType.cs b/TransactionProcessor.Models/TransactionType.cs index a91b1f5b..bf746b07 100644 --- a/TransactionProcessor.Models/TransactionType.cs +++ b/TransactionProcessor.Models/TransactionType.cs @@ -1,10 +1,28 @@ namespace TransactionProcessor.Models { + /// + /// + /// public enum TransactionType { + /// + /// The logon + /// Logon, + + /// + /// The sale + /// Sale, + + /// + /// The refund + /// Refund, + + /// + /// The reversal + /// Reversal } } \ No newline at end of file diff --git a/TransactionProcessor.Testing/TestData.cs b/TransactionProcessor.Testing/TestData.cs index 737d4a75..70712bdf 100644 --- a/TransactionProcessor.Testing/TestData.cs +++ b/TransactionProcessor.Testing/TestData.cs @@ -6,10 +6,14 @@ using BusinessLogic.OperatorInterfaces.SafaricomPinless; using BusinessLogic.Requests; using BusinessLogic.Services; + using EstateManagement.DataTransferObjects; using EstateManagement.DataTransferObjects.Responses; using Models; using SecurityService.DataTransferObjects.Responses; + using Transaction.DomainEvents; using TransactionAggregate; + using CalculationType = Models.CalculationType; + using FeeType = Models.FeeType; public class TestData { @@ -416,7 +420,7 @@ public class TestData #region Methods - public static TransactionAggregate GetCompletedTransactionAggregate() + public static TransactionAggregate GetCompletedLogonTransactionAggregate() { TransactionAggregate transactionAggregate = TransactionAggregate.Create(TestData.TransactionId); @@ -436,6 +440,132 @@ public static TransactionAggregate GetCompletedTransactionAggregate() return transactionAggregate; } + public static TransactionAggregate GetCompletedAuthorisedSaleTransactionAggregate() + { + TransactionAggregate transactionAggregate = TransactionAggregate.Create(TestData.TransactionId); + + transactionAggregate.StartTransaction(TestData.TransactionDateTime, + TestData.TransactionNumber, + TestData.TransactionTypeSale, + TestData.TransactionReference, + TestData.EstateId, + TestData.MerchantId, + TestData.DeviceIdentifier, + TestData.TransactionAmount); + + transactionAggregate.AddProductDetails(TestData.ContractId, TestData.ProductId); + + transactionAggregate.AuthoriseTransaction(TestData.OperatorIdentifier1, + TestData.AuthorisationCode, + TestData.OperatorResponseCode, + TestData.OperatorResponseMessage, + TestData.OperatorTransactionId, + TestData.ResponseCode, + TestData.ResponseMessage); + + transactionAggregate.CompleteTransaction(); + + return transactionAggregate; + } + + public static TransactionAggregate GetCompletedDeclinedSaleTransactionAggregate() + { + TransactionAggregate transactionAggregate = TransactionAggregate.Create(TestData.TransactionId); + + transactionAggregate.StartTransaction(TestData.TransactionDateTime, + TestData.TransactionNumber, + TestData.TransactionTypeSale, + 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(); + + return transactionAggregate; + } + + public static TransactionAggregate GetIncompleteAuthorisedSaleTransactionAggregate() + { + TransactionAggregate transactionAggregate = TransactionAggregate.Create(TestData.TransactionId); + + transactionAggregate.StartTransaction(TestData.TransactionDateTime, + TestData.TransactionNumber, + TestData.TransactionTypeSale, + 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); + + return transactionAggregate; + } + + public static TransactionAggregate GetCompletedAuthorisedLogonTransactionAggregate() + { + TransactionAggregate transactionAggregate = TransactionAggregate.Create(TestData.TransactionId); + + transactionAggregate.StartTransaction(TestData.TransactionDateTime, + TestData.TransactionNumber, + TestData.TransactionTypeLogon, + TestData.TransactionReference, + TestData.EstateId, + TestData.MerchantId, + TestData.DeviceIdentifier, + TestData.TransactionAmount); + + transactionAggregate.AuthoriseTransactionLocally(TestData.AuthorisationCode, + TestData.ResponseCode, + TestData.ResponseMessage); + + transactionAggregate.CompleteTransaction(); + + return transactionAggregate; + } + + public static TransactionAggregate GetCompletedAuthorisedSaleWithNoProductDetailsTransactionAggregate() + { + TransactionAggregate transactionAggregate = TransactionAggregate.Create(TestData.TransactionId); + + transactionAggregate.StartTransaction(TestData.TransactionDateTime, + TestData.TransactionNumber, + TestData.TransactionTypeSale, + TestData.TransactionReference, + TestData.EstateId, + TestData.MerchantId, + TestData.DeviceIdentifier, + TestData.TransactionAmount); + + transactionAggregate.AuthoriseTransaction(TestData.OperatorIdentifier1, + TestData.AuthorisationCode, + TestData.OperatorResponseCode, + TestData.OperatorResponseMessage, + TestData.OperatorTransactionId, + TestData.ResponseCode, + TestData.ResponseMessage); + + transactionAggregate.CompleteTransaction(); + + return transactionAggregate; + } + public static TransactionAggregate GetEmptyTransactionAggregate() { return TransactionAggregate.Create(TestData.TransactionId); @@ -531,6 +661,74 @@ public static TokenResponse TokenResponse() return SecurityService.DataTransferObjects.Responses.TokenResponse.Create("AccessToken", string.Empty, 100); } + public static TransactionHasBeenCompletedEvent TransactionHasBeenCompletedEvent = TransactionHasBeenCompletedEvent.Create(TestData.TransactionId, + TestData.EstateId, + TestData.MerchantId, + TestData.ResponseCode, + TestData.ResponseMessage, + TestData.IsAuthorised); + + public static Guid TransactionFeeId = Guid.Parse("B83FCCCE-0D45-4FC2-8952-ED277A124BDB"); + + public static String TransactionFeeDescription = "Commission for Merchant"; + + public static Decimal TransactionFeeValue = 0.5m; + public static Decimal CalculatedFeeValue = 0.5m; + + public static List ContractProductTransactionFees => + new List + { + new ContractProductTransactionFee + { + Value = TestData.TransactionFeeValue, + TransactionFeeId = TestData.TransactionFeeId, + Description = TestData.TransactionFeeDescription, + CalculationType = (EstateManagement.DataTransferObjects.CalculationType)CalculationType.Fixed + } + }; + + public static CalculatedFee CalculatedFeeMerchantFee => + new CalculatedFee + { + CalculatedValue = TestData.CalculatedFeeValue, + FeeCalculationType = CalculationType.Fixed, + FeeId = TestData.TransactionFeeId, + FeeValue = TestData.TransactionFeeValue, + FeeType = FeeType.Merchant + }; + + public static CalculatedFee CalculatedFeeServiceProviderFee => + new CalculatedFee + { + CalculatedValue = TestData.CalculatedFeeValue, + FeeCalculationType = CalculationType.Fixed, + FeeId = TestData.TransactionFeeId, + FeeValue = TestData.TransactionFeeValue, + FeeType = FeeType.ServiceProvider + }; + + public static CalculatedFee CalculatedFeeUnsupportedFee => + new CalculatedFee + { + CalculatedValue = TestData.CalculatedFeeValue, + FeeCalculationType = CalculationType.Fixed, + FeeId = TestData.TransactionFeeId, + FeeValue = TestData.TransactionFeeValue, + FeeType = (FeeType)99 + }; + + public static List CalculatedMerchantFees => + new List + { + TestData.CalculatedFeeMerchantFee + }; + + public static List CalculatedServiceProviderFees => + new List + { + TestData.CalculatedFeeServiceProviderFee + }; + #endregion } } \ No newline at end of file diff --git a/TransactionProcessor.Transaction.DomainEvents/MerchantFeeAddedToTransactionEvent.cs b/TransactionProcessor.Transaction.DomainEvents/MerchantFeeAddedToTransactionEvent.cs new file mode 100644 index 00000000..95740d29 --- /dev/null +++ b/TransactionProcessor.Transaction.DomainEvents/MerchantFeeAddedToTransactionEvent.cs @@ -0,0 +1,149 @@ +namespace TransactionProcessor.Transaction.DomainEvents +{ + using System; + using System.Diagnostics.CodeAnalysis; + using Newtonsoft.Json; + using Shared.DomainDrivenDesign.EventSourcing; + + /// + /// + /// + /// + [JsonObject] + public class MerchantFeeAddedToTransactionEvent : DomainEvent + { + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + [ExcludeFromCodeCoverage] + public MerchantFeeAddedToTransactionEvent() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The aggregate identifier. + /// The event identifier. + /// The estate identifier. + /// The merchant identifier. + /// The calculated value. + /// Type of the fee calculation. + /// The fee identifier. + /// The fee value. + private MerchantFeeAddedToTransactionEvent(Guid aggregateId, + Guid eventId, + Guid estateId, + Guid merchantId, + Decimal calculatedValue, + Int32 feeCalculationType, + Guid feeId, + Decimal feeValue) : base(aggregateId, eventId) + { + this.TransactionId = aggregateId; + this.EstateId = estateId; + this.MerchantId = merchantId; + this.CalculatedValue = calculatedValue; + this.FeeCalculationType = feeCalculationType; + this.FeeId = feeId; + this.FeeValue = feeValue; + } + + #endregion + + #region Properties + + /// + /// Gets the calculated value. + /// + /// + /// The calculated value. + /// + [JsonProperty] + public Decimal CalculatedValue { get; private set; } + + /// + /// Gets the estate identifier. + /// + /// + /// The estate identifier. + /// + [JsonProperty] + public Guid EstateId { get; private set; } + + /// + /// Gets the type of the fee calculation. + /// + /// + /// The type of the fee calculation. + /// + [JsonProperty] + public Int32 FeeCalculationType { get; private set; } + + /// + /// Gets the fee identifier. + /// + /// + /// The fee identifier. + /// + [JsonProperty] + public Guid FeeId { get; private set; } + + /// + /// Gets the fee value. + /// + /// + /// The fee value. + /// + [JsonProperty] + public Decimal FeeValue { get; private set; } + + /// + /// Gets the merchant identifier. + /// + /// + /// The merchant identifier. + /// + [JsonProperty] + public Guid MerchantId { get; private set; } + + /// + /// Gets the transaction identifier. + /// + /// + /// The transaction identifier. + /// + [JsonProperty] + public Guid TransactionId { get; private set; } + + #endregion + + #region Methods + + /// + /// Creates the specified aggregate identifier. + /// + /// The aggregate identifier. + /// The estate identifier. + /// The merchant identifier. + /// The calculated value. + /// Type of the fee calculation. + /// The fee identifier. + /// The fee value. + /// + public static MerchantFeeAddedToTransactionEvent Create(Guid aggregateId, + Guid estateId, + Guid merchantId, + Decimal calculatedValue, + Int32 feeCalculationType, + Guid feeId, + Decimal feeValue) + { + return new MerchantFeeAddedToTransactionEvent(aggregateId, Guid.NewGuid(), estateId, merchantId, calculatedValue, feeCalculationType, feeId, feeValue); + } + + #endregion + } +} \ No newline at end of file diff --git a/TransactionProcessor.Transaction.DomainEvents/ProductDetailsAddedToTransactionEvent.cs b/TransactionProcessor.Transaction.DomainEvents/ProductDetailsAddedToTransactionEvent.cs index ee415fc8..32a7f3ca 100644 --- a/TransactionProcessor.Transaction.DomainEvents/ProductDetailsAddedToTransactionEvent.cs +++ b/TransactionProcessor.Transaction.DomainEvents/ProductDetailsAddedToTransactionEvent.cs @@ -29,16 +29,20 @@ public ProductDetailsAddedToTransactionEvent() /// The aggregate identifier. /// The event identifier. /// The estate identifier. + /// The merchant identifier. /// The contract identifier. /// The product identifier. private ProductDetailsAddedToTransactionEvent(Guid aggregateId, Guid eventId, Guid estateId, + Guid merchantId, Guid contractId, Guid productId) : base(aggregateId, eventId) { this.TransactionId = aggregateId; + this.EventId = eventId; this.EstateId = estateId; + this.MerchantId = merchantId; this.ContractId = contractId; this.ProductId = productId; } @@ -65,6 +69,15 @@ private ProductDetailsAddedToTransactionEvent(Guid aggregateId, [JsonProperty] public Guid EstateId { get; private set; } + /// + /// Gets the merchant identifier. + /// + /// + /// The merchant identifier. + /// + [JsonProperty] + public Guid MerchantId { get; private set; } + /// /// Gets the product identifier. /// @@ -83,6 +96,8 @@ private ProductDetailsAddedToTransactionEvent(Guid aggregateId, [JsonProperty] public Guid TransactionId { get; private set; } + public Guid EventId { get; private set; } + #endregion #region Methods @@ -92,15 +107,17 @@ private ProductDetailsAddedToTransactionEvent(Guid aggregateId, /// /// The aggregate identifier. /// The estate identifier. + /// The merchant identifier. /// The contract identifier. /// The product identifier. /// public static ProductDetailsAddedToTransactionEvent Create(Guid aggregateId, Guid estateId, + Guid merchantId, Guid contractId, Guid productId) { - return new ProductDetailsAddedToTransactionEvent(aggregateId, Guid.NewGuid(), estateId, contractId, productId); + return new ProductDetailsAddedToTransactionEvent(aggregateId, Guid.NewGuid(), estateId, merchantId, contractId, productId); } #endregion diff --git a/TransactionProcessor.Transaction.DomainEvents/ServiceProviderFeeAddedToTransactionEvent.cs b/TransactionProcessor.Transaction.DomainEvents/ServiceProviderFeeAddedToTransactionEvent.cs new file mode 100644 index 00000000..e27e58ae --- /dev/null +++ b/TransactionProcessor.Transaction.DomainEvents/ServiceProviderFeeAddedToTransactionEvent.cs @@ -0,0 +1,149 @@ +namespace TransactionProcessor.Transaction.DomainEvents +{ + using System; + using System.Diagnostics.CodeAnalysis; + using Newtonsoft.Json; + using Shared.DomainDrivenDesign.EventSourcing; + + /// + /// + /// + /// + [JsonObject] + public class ServiceProviderFeeAddedToTransactionEvent : DomainEvent + { + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + [ExcludeFromCodeCoverage] + public ServiceProviderFeeAddedToTransactionEvent() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The aggregate identifier. + /// The event identifier. + /// The estate identifier. + /// The merchant identifier. + /// The calculated value. + /// Type of the fee calculation. + /// The fee identifier. + /// The fee value. + private ServiceProviderFeeAddedToTransactionEvent(Guid aggregateId, + Guid eventId, + Guid estateId, + Guid merchantId, + Decimal calculatedValue, + Int32 feeCalculationType, + Guid feeId, + Decimal feeValue) : base(aggregateId, eventId) + { + this.TransactionId = aggregateId; + this.EstateId = estateId; + this.MerchantId = merchantId; + this.CalculatedValue = calculatedValue; + this.FeeCalculationType = feeCalculationType; + this.FeeId = feeId; + this.FeeValue = feeValue; + } + + #endregion + + #region Properties + + /// + /// Gets the calculated value. + /// + /// + /// The calculated value. + /// + [JsonProperty] + public Decimal CalculatedValue { get; private set; } + + /// + /// Gets the estate identifier. + /// + /// + /// The estate identifier. + /// + [JsonProperty] + public Guid EstateId { get; private set; } + + /// + /// Gets the type of the fee calculation. + /// + /// + /// The type of the fee calculation. + /// + [JsonProperty] + public Int32 FeeCalculationType { get; private set; } + + /// + /// Gets the fee identifier. + /// + /// + /// The fee identifier. + /// + [JsonProperty] + public Guid FeeId { get; private set; } + + /// + /// Gets the fee value. + /// + /// + /// The fee value. + /// + [JsonProperty] + public Decimal FeeValue { get; private set; } + + /// + /// Gets the merchant identifier. + /// + /// + /// The merchant identifier. + /// + [JsonProperty] + public Guid MerchantId { get; private set; } + + /// + /// Gets the transaction identifier. + /// + /// + /// The transaction identifier. + /// + [JsonProperty] + public Guid TransactionId { get; private set; } + + #endregion + + #region Methods + + /// + /// Creates the specified aggregate identifier. + /// + /// The aggregate identifier. + /// The estate identifier. + /// The merchant identifier. + /// The calculated value. + /// Type of the fee calculation. + /// The fee identifier. + /// The fee value. + /// + public static ServiceProviderFeeAddedToTransactionEvent Create(Guid aggregateId, + Guid estateId, + Guid merchantId, + Decimal calculatedValue, + Int32 feeCalculationType, + Guid feeId, + Decimal feeValue) + { + return new ServiceProviderFeeAddedToTransactionEvent(aggregateId, Guid.NewGuid(), estateId, merchantId, calculatedValue, feeCalculationType, feeId, feeValue); + } + + #endregion + } +} \ No newline at end of file diff --git a/TransactionProcessor.TransactionAggregate.Tests/DomainEventTests.cs b/TransactionProcessor.TransactionAggregate.Tests/DomainEventTests.cs index ab646489..6ef3564c 100644 --- a/TransactionProcessor.TransactionAggregate.Tests/DomainEventTests.cs +++ b/TransactionProcessor.TransactionAggregate.Tests/DomainEventTests.cs @@ -4,11 +4,13 @@ namespace TransactionProcessor.TransactionAggregate.Tests { + using EstateManagement.DataTransferObjects; using Models; using Shouldly; using Testing; using Transaction.DomainEvents; using Xunit; + using CalculationType = Models.CalculationType; public class DomainEventTests { @@ -234,7 +236,7 @@ public void CustomerEmailReceiptRequestedEvent_CanBeCreated_IsCreated() [Fact] public void ProductDetailsAddedToTransactionEvent_CanBeCreated_IsCreated() { - ProductDetailsAddedToTransactionEvent productDetailsAddedToTransactionEvent = ProductDetailsAddedToTransactionEvent.Create(TestData.TransactionId, TestData.EstateId,TestData.ContractId, TestData.ProductId); + ProductDetailsAddedToTransactionEvent productDetailsAddedToTransactionEvent = ProductDetailsAddedToTransactionEvent.Create(TestData.TransactionId, TestData.EstateId,TestData.MerchantId, TestData.ContractId, TestData.ProductId); productDetailsAddedToTransactionEvent.ShouldNotBeNull(); productDetailsAddedToTransactionEvent.AggregateId.ShouldBe(TestData.TransactionId); @@ -242,8 +244,58 @@ public void ProductDetailsAddedToTransactionEvent_CanBeCreated_IsCreated() productDetailsAddedToTransactionEvent.EventId.ShouldNotBe(Guid.Empty); productDetailsAddedToTransactionEvent.TransactionId.ShouldBe(TestData.TransactionId); productDetailsAddedToTransactionEvent.EstateId.ShouldBe(TestData.EstateId); + productDetailsAddedToTransactionEvent.MerchantId.ShouldBe(TestData.MerchantId); productDetailsAddedToTransactionEvent.ProductId.ShouldBe(TestData.ProductId); productDetailsAddedToTransactionEvent.ContractId.ShouldBe(TestData.ContractId); + } + + [Fact] + public void MerchantFeeAddedToTransactionEvent_CanBeCreated_IsCreated() + { + MerchantFeeAddedToTransactionEvent merchantFeeAddedToTransactionEvent = MerchantFeeAddedToTransactionEvent.Create(TestData.TransactionId, + TestData.EstateId, + TestData.MerchantId, + TestData.CalculatedFeeValue, + (Int32)CalculationType.Fixed, + TestData.TransactionFeeId, + TestData.TransactionFeeValue); + + merchantFeeAddedToTransactionEvent.ShouldNotBeNull(); + merchantFeeAddedToTransactionEvent.AggregateId.ShouldBe(TestData.TransactionId); + merchantFeeAddedToTransactionEvent.EventCreatedDateTime.ShouldNotBe(DateTime.MinValue); + merchantFeeAddedToTransactionEvent.EventId.ShouldNotBe(Guid.Empty); + merchantFeeAddedToTransactionEvent.TransactionId.ShouldBe(TestData.TransactionId); + merchantFeeAddedToTransactionEvent.EstateId.ShouldBe(TestData.EstateId); + merchantFeeAddedToTransactionEvent.MerchantId.ShouldBe(TestData.MerchantId); + merchantFeeAddedToTransactionEvent.CalculatedValue.ShouldBe(TestData.CalculatedFeeValue); + merchantFeeAddedToTransactionEvent.FeeCalculationType.ShouldBe((Int32)CalculationType.Fixed); + merchantFeeAddedToTransactionEvent.FeeId.ShouldBe(TestData.TransactionFeeId); + merchantFeeAddedToTransactionEvent.FeeValue.ShouldBe(TestData.TransactionFeeValue); + + } + + [Fact] + public void ServiceProviderFeeAddedToTransactionEvent_CanBeCreated_IsCreated() + { + ServiceProviderFeeAddedToTransactionEvent serviceProviderFeeAddedToTransactionEvent = ServiceProviderFeeAddedToTransactionEvent.Create(TestData.TransactionId, + TestData.EstateId, + TestData.MerchantId, + TestData.CalculatedFeeValue, + (Int32)CalculationType.Fixed, + TestData.TransactionFeeId, + TestData.TransactionFeeValue); + + serviceProviderFeeAddedToTransactionEvent.ShouldNotBeNull(); + serviceProviderFeeAddedToTransactionEvent.AggregateId.ShouldBe(TestData.TransactionId); + serviceProviderFeeAddedToTransactionEvent.EventCreatedDateTime.ShouldNotBe(DateTime.MinValue); + serviceProviderFeeAddedToTransactionEvent.EventId.ShouldNotBe(Guid.Empty); + serviceProviderFeeAddedToTransactionEvent.TransactionId.ShouldBe(TestData.TransactionId); + serviceProviderFeeAddedToTransactionEvent.EstateId.ShouldBe(TestData.EstateId); + serviceProviderFeeAddedToTransactionEvent.MerchantId.ShouldBe(TestData.MerchantId); + serviceProviderFeeAddedToTransactionEvent.CalculatedValue.ShouldBe(TestData.CalculatedFeeValue); + serviceProviderFeeAddedToTransactionEvent.FeeCalculationType.ShouldBe((Int32)CalculationType.Fixed); + serviceProviderFeeAddedToTransactionEvent.FeeId.ShouldBe(TestData.TransactionFeeId); + serviceProviderFeeAddedToTransactionEvent.FeeValue.ShouldBe(TestData.TransactionFeeValue); } } diff --git a/TransactionProcessor.TransactionAggregate.Tests/TransactionAggregateTests.cs b/TransactionProcessor.TransactionAggregate.Tests/TransactionAggregateTests.cs index f18c33b2..18527b77 100644 --- a/TransactionProcessor.TransactionAggregate.Tests/TransactionAggregateTests.cs +++ b/TransactionProcessor.TransactionAggregate.Tests/TransactionAggregateTests.cs @@ -4,6 +4,8 @@ namespace TransactionProcessor.TransactionAggregate.Tests { + using System.Collections.Generic; + using System.Linq; using Models; using Shouldly; @@ -993,5 +995,159 @@ public void TransactionAggregate_RequestEmailReceipt_EmailReceiptAlreadyRequeste Should.Throw(() => { transactionAggregate.RequestEmailReceipt(TestData.CustomerEmailAddress); }); } + [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); + 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.AddFee(calculatedFee); + + 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_AddFee_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.AddFee(null); + }); + } + + [Theory] + [InlineData(TransactionType.Sale, FeeType.ServiceProvider)] + [InlineData(TransactionType.Sale, FeeType.Merchant)] + public void TransactionAggregate_AddFee_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.AddFee(this.GetCalculatedFeeToAdd(feeType)); + }); + } + + private CalculatedFee GetCalculatedFeeToAdd(FeeType feeType) + { + CalculatedFee calculatedFee = null; + if (feeType == FeeType.Merchant) + { + calculatedFee = TestData.CalculatedFeeMerchantFee; + } + else if (feeType == FeeType.ServiceProvider) + { + calculatedFee = TestData.CalculatedFeeServiceProviderFee; + } + + return calculatedFee; + } + + [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); + 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.AddFee(this.GetCalculatedFeeToAdd(feeType)); + }); + } + + [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); + 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.AddFee(this.GetCalculatedFeeToAdd(feeType)); + + Should.Throw(() => + { + transactionAggregate.AddFee(this.GetCalculatedFeeToAdd(feeType)); + }); + } + + [Theory] + [InlineData(TransactionType.Sale)] + public void TransactionAggregate_AddFee_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.AddFee(TestData.CalculatedFeeUnsupportedFee); + }); + } + + [Theory] + [InlineData(FeeType.ServiceProvider)] + [InlineData(FeeType.Merchant)] + public void TransactionAggregate_AddFee_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.AddFee(this.GetCalculatedFeeToAdd(feeType)); + }); + } } } diff --git a/TransactionProcessor.TransactionAgrgegate/TransactionAggregate.cs b/TransactionProcessor.TransactionAgrgegate/TransactionAggregate.cs index e81f9c42..0bff21d2 100644 --- a/TransactionProcessor.TransactionAgrgegate/TransactionAggregate.cs +++ b/TransactionProcessor.TransactionAgrgegate/TransactionAggregate.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; + using System.Linq; using System.Text.RegularExpressions; using Models; using Shared.DomainDrivenDesign.EventSourcing; @@ -13,6 +14,7 @@ /// /// /// + /// /// public class TransactionAggregate : Aggregate { @@ -28,6 +30,11 @@ public class TransactionAggregate : Aggregate /// private Dictionary AdditionalTransactionResponseMetadata; + /// + /// The calculated fees + /// + private readonly List CalculatedFees; + #endregion #region Constructors @@ -38,7 +45,7 @@ public class TransactionAggregate : Aggregate [ExcludeFromCodeCoverage] public TransactionAggregate() { - // Nothing here + this.CalculatedFees = new List(); } /// @@ -50,6 +57,7 @@ private TransactionAggregate(Guid aggregateId) Guard.ThrowIfInvalidGuid(aggregateId, "Aggregate Id cannot be an Empty Guid"); this.AggregateId = aggregateId; + this.CalculatedFees = new List(); } #endregion @@ -64,6 +72,14 @@ private TransactionAggregate(Guid aggregateId) /// public String AuthorisationCode { get; private set; } + /// + /// Gets the contract identifier. + /// + /// + /// The contract identifier. + /// + public Guid ContractId { get; private set; } + /// /// Gets a value indicating whether [customer email receipt has been requested]. /// @@ -88,21 +104,6 @@ private TransactionAggregate(Guid aggregateId) /// public Guid EstateId { get; private set; } - /// - /// Gets the contract identifier. - /// - /// - /// The contract identifier. - /// - public Guid ContractId { get; private set; } - /// - /// Gets the product identifier. - /// - /// - /// The product identifier. - /// - public Guid ProductId { get; private set; } - /// /// Gets a value indicating whether this instance is authorised. /// @@ -143,6 +144,14 @@ private TransactionAggregate(Guid aggregateId) /// public Boolean IsLocallyDeclined { get; private set; } + /// + /// Gets a value indicating whether this instance is product details added. + /// + /// + /// true if this instance is product details added; otherwise, false. + /// + public Boolean IsProductDetailsAdded { get; private set; } + /// /// Gets a value indicating whether this instance is started. /// @@ -183,6 +192,14 @@ private TransactionAggregate(Guid aggregateId) /// public String OperatorTransactionId { get; private set; } + /// + /// Gets the product identifier. + /// + /// + /// The product identifier. + /// + public Guid ProductId { get; private set; } + /// /// Gets the response code. /// @@ -243,6 +260,79 @@ private TransactionAggregate(Guid aggregateId) #region Methods + /// + /// Adds the fee. + /// + /// The calculated fee. + /// calculatedFee + /// Unsupported Fee Type + public void AddFee(CalculatedFee calculatedFee) + { + if (calculatedFee == null) + { + throw new ArgumentNullException(nameof(calculatedFee)); + } + + this.CheckFeeHasNotAlreadyBeenAdded(calculatedFee); + this.CheckTransactionHasBeenAuthorised(); + this.CheckTransactionHasBeenCompleted(); + this.CheckTransactionCanAttractFees(); + + DomainEvent @event = null; + if (calculatedFee.FeeType == FeeType.Merchant) + { + // This is a merchant fee + @event = MerchantFeeAddedToTransactionEvent.Create(this.AggregateId, + this.EstateId, + this.MerchantId, + calculatedFee.CalculatedValue, + (Int32)calculatedFee.FeeCalculationType, + calculatedFee.FeeId, + calculatedFee.FeeValue); + } + else if (calculatedFee.FeeType == FeeType.ServiceProvider) + { + // This is an operational (service provider) fee + @event = ServiceProviderFeeAddedToTransactionEvent.Create(this.AggregateId, + this.EstateId, + this.MerchantId, + calculatedFee.CalculatedValue, + (Int32)calculatedFee.FeeCalculationType, + calculatedFee.FeeId, + calculatedFee.FeeValue); + } + else + { + throw new InvalidOperationException("Unsupported Fee Type"); + } + + if (@event != null) + { + this.ApplyAndPend(@event); + } + } + + /// + /// Adds the product details. + /// + /// The contract identifier. + /// The product identifier. + public void AddProductDetails(Guid contractId, + Guid productId) + { + Guard.ThrowIfInvalidGuid(contractId, typeof(ArgumentException), $"Contract Id must not be [{Guid.Empty}]"); + Guard.ThrowIfInvalidGuid(productId, typeof(ArgumentException), $"Product Id must not be [{Guid.Empty}]"); + + this.CheckTransactionHasBeenStarted(); + this.CheckTransactionNotAlreadyCompleted(); + this.CheckProductDetailsNotAlreadyAdded(); + + ProductDetailsAddedToTransactionEvent productDetailsAddedToTransactionEvent = + ProductDetailsAddedToTransactionEvent.Create(this.AggregateId, this.EstateId, this.MerchantId, contractId, productId); + + this.ApplyAndPend(productDetailsAddedToTransactionEvent); + } + /// /// Authorises the transaction. /// @@ -373,6 +463,15 @@ public void DeclineTransactionLocally(String responseCode, this.ApplyAndPend(transactionHasBeenLocallyDeclinedEvent); } + /// + /// Gets the fees. + /// + /// + public List GetFees() + { + return this.CalculatedFees; + } + /// /// Records the additional request data. /// @@ -486,29 +585,6 @@ public void StartTransaction(DateTime transactionDateTime, this.ApplyAndPend(transactionHasStartedEvent); } - /// - /// Adds the product details. - /// - /// The contract identifier. - /// The product identifier. - public void AddProductDetails(Guid contractId, - Guid productId) - { - Guard.ThrowIfInvalidGuid(contractId, typeof(ArgumentException), $"Contract Id must not be [{Guid.Empty}]"); - Guard.ThrowIfInvalidGuid(productId, typeof(ArgumentException), $"Product Id must not be [{Guid.Empty}]"); - - this.CheckTransactionHasBeenStarted(); - this.CheckTransactionNotAlreadyCompleted(); - this.CheckProductDetailsNotAlreadyAdded(); - - ProductDetailsAddedToTransactionEvent productDetailsAddedToTransactionEvent = ProductDetailsAddedToTransactionEvent.Create(this.AggregateId, - this.EstateId, - contractId, - productId); - - this.ApplyAndPend(productDetailsAddedToTransactionEvent); - } - /// /// Gets the metadata. /// @@ -531,14 +607,6 @@ protected override void PlayEvent(DomainEvent domainEvent) this.PlayEvent((dynamic)domainEvent); } - private void CheckProductDetailsNotAlreadyAdded() - { - if (this.IsProductDetailsAdded) - { - throw new InvalidOperationException("Product details already added"); - } - } - /// /// Checks the additional request data not already recorded. /// @@ -575,6 +643,43 @@ private void CheckCustomerHasNotAlreadyRequestedEmailReceipt() } } + /// + /// Checks the fee has not already been added. + /// + /// The calculated fee. + /// Fee with Id [{calculatedFee.FeeId}] has already been added to this transaction + private void CheckFeeHasNotAlreadyBeenAdded(CalculatedFee calculatedFee) + { + if (this.CalculatedFees.Any(c => c.FeeId == calculatedFee.FeeId)) + { + throw new InvalidOperationException($"Fee with Id [{calculatedFee.FeeId}] has already been added to this transaction"); + } + } + + /// + /// Checks the product details not already added. + /// + /// Product details already added + private void CheckProductDetailsNotAlreadyAdded() + { + if (this.IsProductDetailsAdded) + { + throw new InvalidOperationException("Product details already added"); + } + } + + /// + /// Checks the transaction can attract fees. + /// + /// Transactions of type {this.TransactionType} cannot attract fees + private void CheckTransactionCanAttractFees() + { + if (this.TransactionType != TransactionType.Sale) + { + throw new NotSupportedException($"Transactions of type {this.TransactionType} cannot attract fees"); + } + } + /// /// Checks the transaction can be locally authorised. /// @@ -588,6 +693,18 @@ private void CheckTransactionCanBeLocallyAuthorised() } } + /// + /// Checks the transaction has been authorised. + /// + /// Transaction [{this.AggregateId}] has not been authorised + private void CheckTransactionHasBeenAuthorised() + { + if (this.IsLocallyAuthorised == false && this.IsAuthorised == false) + { + throw new InvalidOperationException($"Transaction [{this.AggregateId}] has not been authorised"); + } + } + /// /// Checks the transaction has been authorised. /// @@ -783,6 +900,10 @@ private void PlayEvent(TransactionDeclinedByOperatorEvent domainEvent) this.ResponseMessage = domainEvent.ResponseMessage; } + /// + /// Plays the event. + /// + /// The domain event. private void PlayEvent(ProductDetailsAddedToTransactionEvent domainEvent) { this.IsProductDetailsAdded = true; @@ -791,12 +912,36 @@ private void PlayEvent(ProductDetailsAddedToTransactionEvent domainEvent) } /// - /// Gets a value indicating whether this instance is product details added. + /// Plays the event. /// - /// - /// true if this instance is product details added; otherwise, false. - /// - public Boolean IsProductDetailsAdded { get; private set; } + /// The domain event. + private void PlayEvent(MerchantFeeAddedToTransactionEvent domainEvent) + { + this.CalculatedFees.Add(new CalculatedFee + { + CalculatedValue = domainEvent.CalculatedValue, + FeeId = domainEvent.FeeId, + FeeType = FeeType.Merchant, + FeeValue = domainEvent.FeeValue, + FeeCalculationType = (CalculationType)domainEvent.FeeCalculationType + }); + } + + /// + /// Plays the event. + /// + /// The domain event. + private void PlayEvent(ServiceProviderFeeAddedToTransactionEvent domainEvent) + { + this.CalculatedFees.Add(new CalculatedFee + { + CalculatedValue = domainEvent.CalculatedValue, + FeeId = domainEvent.FeeId, + FeeType = FeeType.ServiceProvider, + FeeValue = domainEvent.FeeValue, + FeeCalculationType = (CalculationType)domainEvent.FeeCalculationType + }); + } #endregion } diff --git a/TransactionProcessor/Controllers/DomainEventController.cs b/TransactionProcessor/Controllers/DomainEventController.cs new file mode 100644 index 00000000..ac2e8ca8 --- /dev/null +++ b/TransactionProcessor/Controllers/DomainEventController.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace TransactionProcessor.Controllers +{ + using System.Diagnostics.CodeAnalysis; + using System.Threading; + using MessagingService.BusinessLogic.EventHandling; + using Microsoft.AspNetCore.Mvc; + using Newtonsoft.Json; + using Shared.DomainDrivenDesign.EventSourcing; + using Shared.Logger; + + [Route(DomainEventController.ControllerRoute)] + [ApiController] + [ExcludeFromCodeCoverage] + public class DomainEventController : ControllerBase + { + private readonly IDomainEventHandlerResolver DomainEventHandlerResolver; + + public DomainEventController(IDomainEventHandlerResolver domainEventHandlerResolver) + { + this.DomainEventHandlerResolver = domainEventHandlerResolver; + } + + /// + /// Posts the event asynchronous. + /// + /// The domain event. + /// The cancellation token. + /// + [HttpPost] + public async Task PostEventAsync([FromBody] DomainEvent domainEvent, + CancellationToken cancellationToken) + { + cancellationToken.Register(() => this.Callback(cancellationToken, domainEvent.EventId)); + + try + { + Logger.LogInformation($"Processing event - ID [{domainEvent.EventId}], Type[{domainEvent.GetType().Name}]"); + + List eventHandlers = this.DomainEventHandlerResolver.GetDomainEventHandlers(domainEvent); + + if (eventHandlers == null || eventHandlers.Any() == false) + { + // Log a warning out + Logger.LogWarning($"No event handlers configured for Event Type [{domainEvent.GetType().Name}]"); + return this.Ok(); + } + + List tasks = new List(); + foreach (IDomainEventHandler domainEventHandler in eventHandlers) + { + tasks.Add(domainEventHandler.Handle(domainEvent, cancellationToken)); + } + + Task.WaitAll(tasks.ToArray()); + + Logger.LogInformation($"Finished processing event - ID [{domainEvent.EventId}]"); + + return this.Ok(); + } + catch (Exception ex) + { + String domainEventData = JsonConvert.SerializeObject(domainEvent); + Logger.LogError(new Exception($" Failed to Process Event, Event Data received [{domainEventData}]", ex)); + + throw; + } + } + + /// + /// Callbacks the specified cancellation token. + /// + /// The cancellation token. + /// The event identifier. + private void Callback(CancellationToken cancellationToken, + Guid eventId) + { + if (cancellationToken.IsCancellationRequested) //I think this would always be true anyway + { + Logger.LogInformation($"Cancel request for EventId {eventId}"); + cancellationToken.ThrowIfCancellationRequested(); + } + } + + #region Others + + /// + /// The controller name + /// + public const String ControllerName = "domainevents"; + + /// + /// The controller route + /// + private const String ControllerRoute = "api/" + DomainEventController.ControllerName; + + #endregion + + } +} diff --git a/TransactionProcessor/Startup.cs b/TransactionProcessor/Startup.cs index b67a5783..196dfb38 100644 --- a/TransactionProcessor/Startup.cs +++ b/TransactionProcessor/Startup.cs @@ -16,6 +16,8 @@ namespace TransactionProcessor using System.IO; using System.Net.Http; using System.Reflection; + using BusinessLogic.EventHandling; + using BusinessLogic.Manager; using BusinessLogic.OperatorInterfaces; using BusinessLogic.OperatorInterfaces.SafaricomPinless; using BusinessLogic.RequestHandlers; @@ -25,6 +27,7 @@ namespace TransactionProcessor using EstateManagement.Client; using EventStore.Client; using MediatR; + using MessagingService.BusinessLogic.EventHandling; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.AspNetCore.Mvc.Versioning; @@ -178,6 +181,29 @@ public void ConfigureServices(IServiceCollection services) HttpClient client = context.GetRequiredService(); return new SafaricomPinlessProxy(configuration, client); }); + + Dictionary eventHandlersConfiguration = new Dictionary(); + + if (Startup.Configuration != null) + { + IConfigurationSection section = Startup.Configuration.GetSection("AppSettings:EventHandlerConfiguration"); + + if (section != null) + { + Startup.Configuration.GetSection("AppSettings:EventHandlerConfiguration").Bind(eventHandlersConfiguration); + } + } + services.AddSingleton>(eventHandlersConfiguration); + + services.AddSingleton>(container => (type) => + { + IDomainEventHandler handler = container.GetService(type) as IDomainEventHandler; + return handler; + }); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); } diff --git a/TransactionProcessor/appsettings.json b/TransactionProcessor/appsettings.json index c6c7a21f..4808d659 100644 --- a/TransactionProcessor/appsettings.json +++ b/TransactionProcessor/appsettings.json @@ -24,7 +24,12 @@ "SecurityService": "http://192.168.1.133:5001", "EstateManagementApi": "http://192.168.1.133:5000", "ClientId": "serviceClient", - "ClientSecret": "d192cbc46d834d0da90e8a9d50ded543" + "ClientSecret": "d192cbc46d834d0da90e8a9d50ded543", + "EventHandlerConfiguration": { + "TransactionProcessor.Transaction.DomainEvents.TransactionHasBeenCompletedEvent": [ + "TransactionProcessor.BusinessLogic.EventHandling.TransactionDomainEventHandler" + ] + } }, "SecurityConfiguration": { "ApiName": "transactionProcessor",