diff --git a/TransactionProcessor.BusinessLogic.Tests/RequestHandler/TransactionRequestHandlerTests.cs b/TransactionProcessor.BusinessLogic.Tests/RequestHandler/TransactionRequestHandlerTests.cs index b8662a04..8dda9bff 100644 --- a/TransactionProcessor.BusinessLogic.Tests/RequestHandler/TransactionRequestHandlerTests.cs +++ b/TransactionProcessor.BusinessLogic.Tests/RequestHandler/TransactionRequestHandlerTests.cs @@ -44,5 +44,20 @@ public void TransactionRequestHandler_ProcessSaleTransactionRequest_IsHandled() }); } + + [Fact] + public void TransactionRequestHandler_ProcessReconciliationRequest_IsHandled() + { + Mock transactionDomainService = new Mock(); + TransactionRequestHandler handler = new TransactionRequestHandler(transactionDomainService.Object); + + ProcessReconciliationRequest command = TestData.ProcessReconciliationRequest; + + Should.NotThrow(async () => + { + await handler.Handle(command, CancellationToken.None); + }); + + } } } \ No newline at end of file diff --git a/TransactionProcessor.BusinessLogic.Tests/Requests/RequestTests.cs b/TransactionProcessor.BusinessLogic.Tests/Requests/RequestTests.cs index 25899284..b846c2b9 100644 --- a/TransactionProcessor.BusinessLogic.Tests/Requests/RequestTests.cs +++ b/TransactionProcessor.BusinessLogic.Tests/Requests/RequestTests.cs @@ -53,5 +53,26 @@ public void ProcessSaleTransactionRequest_CanBeCreated_IsCreated() processSaleTransactionRequest.ContractId.ShouldBe(TestData.ContractId); processSaleTransactionRequest.ProductId.ShouldBe(TestData.ProductId); } + + [Fact] + public void ProcessReconciliationRequest_CanBeCreated_IsCreated() + { + ProcessReconciliationRequest processReconciliationRequest = ProcessReconciliationRequest.Create(TestData.TransactionId, + TestData.EstateId, + TestData.MerchantId, + TestData.DeviceIdentifier, + TestData.TransactionDateTime, + TestData.ReconciliationTransactionCount, + TestData.ReconciliationTransactionValue); + + processReconciliationRequest.ShouldNotBeNull(); + processReconciliationRequest.EstateId.ShouldBe(TestData.EstateId); + processReconciliationRequest.MerchantId.ShouldBe(TestData.MerchantId); + processReconciliationRequest.DeviceIdentifier.ShouldBe(TestData.DeviceIdentifier); + processReconciliationRequest.TransactionDateTime.ShouldBe(TestData.TransactionDateTime); + processReconciliationRequest.TransactionCount.ShouldBe(TestData.ReconciliationTransactionCount); + processReconciliationRequest.TransactionValue.ShouldBe(TestData.ReconciliationTransactionValue); + processReconciliationRequest.TransactionId.ShouldBe(TestData.TransactionId); + } } } diff --git a/TransactionProcessor.BusinessLogic.Tests/Services/TransactionDomainServiceTests.cs b/TransactionProcessor.BusinessLogic.Tests/Services/TransactionDomainServiceTests.cs index b82df017..d14e1309 100644 --- a/TransactionProcessor.BusinessLogic.Tests/Services/TransactionDomainServiceTests.cs +++ b/TransactionProcessor.BusinessLogic.Tests/Services/TransactionDomainServiceTests.cs @@ -14,6 +14,7 @@ namespace TransactionProcessor.BusinessLogic.Tests.Services using Models; using Moq; using OperatorInterfaces; + using ReconciliationAggregate; using SecurityService.Client; using SecurityService.DataTransferObjects.Responses; using Shared.EventStore.EventStore; @@ -26,6 +27,213 @@ namespace TransactionProcessor.BusinessLogic.Tests.Services public class TransactionDomainServiceTests { + [Fact] + public async Task TransactionDomainService_ProcessReconciliationTransaction_TransactionIsProcessed() + { + IConfigurationRoot configurationRoot = new ConfigurationBuilder().AddInMemoryCollection(TestData.DefaultAppSettings).Build(); + ConfigurationReader.Initialise(configurationRoot); + + Logger.Initialise(NullLogger.Instance); + + Mock transactionAggregateManager = new Mock(); + Mock estateClient = new Mock(); + Mock securityServiceClient = new Mock(); + + securityServiceClient.Setup(s => s.GetToken(It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(TestData.TokenResponse); + estateClient.Setup(e => e.GetEstate(It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(TestData.GetEstateResponseWithOperator1); + estateClient.Setup(e => e.GetMerchant(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(TestData.GetMerchantResponseWithOperator1); + + Mock operatorProxy = new Mock(); + Func operatorProxyResolver = (operatorName) => { return operatorProxy.Object; }; + + Mock> reconciliationAggregateRepository = + new Mock>(); + reconciliationAggregateRepository.Setup(r => r.GetLatestVersion(It.IsAny(), It.IsAny())) + .ReturnsAsync(new ReconciliationAggregate()); + + TransactionDomainService transactionDomainService = + new TransactionDomainService(transactionAggregateManager.Object, estateClient.Object, securityServiceClient.Object, + operatorProxyResolver, + reconciliationAggregateRepository.Object); + + ProcessReconciliationTransactionResponse response = await transactionDomainService.ProcessReconciliationTransaction(TestData.TransactionId, + TestData.EstateId, + TestData.MerchantId, + TestData.DeviceIdentifier, + TestData.TransactionDateTime, + TestData.ReconciliationTransactionCount, + TestData.ReconciliationTransactionValue, + CancellationToken.None); + + this.ValidateResponse(response, TransactionResponseCode.Success); + } + + [Fact] + public async Task TransactionDomainService_ProcessReconciliationTransaction_IncorrectDevice_TransactionIsProcessed() + { + IConfigurationRoot configurationRoot = new ConfigurationBuilder().AddInMemoryCollection(TestData.DefaultAppSettings).Build(); + ConfigurationReader.Initialise(configurationRoot); + + Logger.Initialise(NullLogger.Instance); + + Mock transactionAggregateManager = new Mock(); + Mock estateClient = new Mock(); + Mock securityServiceClient = new Mock(); + Mock operatorProxy = new Mock(); + Func operatorProxyResolver = (operatorName) => { return operatorProxy.Object; }; + + securityServiceClient.Setup(s => s.GetToken(It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(TestData.TokenResponse); + estateClient.Setup(e => e.GetEstate(It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(TestData.GetEstateResponseWithOperator1); + estateClient.Setup(e => e.GetMerchant(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(TestData.GetMerchantResponseWithOperator1); + + Mock> reconciliationAggregateRepository = + new Mock>(); + reconciliationAggregateRepository.Setup(r => r.GetLatestVersion(It.IsAny(), It.IsAny())) + .ReturnsAsync(new ReconciliationAggregate()); + + TransactionDomainService transactionDomainService = + new TransactionDomainService(transactionAggregateManager.Object, estateClient.Object, securityServiceClient.Object, + operatorProxyResolver, reconciliationAggregateRepository.Object); + + ProcessReconciliationTransactionResponse response = await transactionDomainService.ProcessReconciliationTransaction(TestData.TransactionId, + TestData.EstateId, + TestData.MerchantId, + TestData.DeviceIdentifier1, + TestData.TransactionDateTime, + TestData.ReconciliationTransactionCount, + TestData.ReconciliationTransactionValue, + CancellationToken.None); + + this.ValidateResponse(response, TransactionResponseCode.InvalidDeviceIdentifier); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task TransactionDomainService_ProcessReconciliationTransaction_MerchantHasNoDevices_TransactionIsProcessed(Boolean deviceListIsNull) + { + IConfigurationRoot configurationRoot = new ConfigurationBuilder().AddInMemoryCollection(TestData.DefaultAppSettings).Build(); + ConfigurationReader.Initialise(configurationRoot); + + Logger.Initialise(NullLogger.Instance); + + Mock transactionAggregateManager = new Mock(); + Mock estateClient = new Mock(); + Mock securityServiceClient = new Mock(); + Mock operatorProxy = new Mock(); + Func operatorProxyResolver = (operatorName) => { return operatorProxy.Object; }; + + securityServiceClient.Setup(s => s.GetToken(It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(TestData.TokenResponse); + estateClient.Setup(e => e.GetEstate(It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(TestData.GetEstateResponseWithOperator1); + + MerchantResponse merchantResponse = deviceListIsNull ? TestData.GetMerchantResponseWithNullDevices : TestData.GetMerchantResponseWithNoDevices; + estateClient.Setup(e => e.GetMerchant(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(merchantResponse); + + Mock> reconciliationAggregateRepository = + new Mock>(); + reconciliationAggregateRepository.Setup(r => r.GetLatestVersion(It.IsAny(), It.IsAny())) + .ReturnsAsync(new ReconciliationAggregate()); + + TransactionDomainService transactionDomainService = + new TransactionDomainService(transactionAggregateManager.Object, estateClient.Object, securityServiceClient.Object, + operatorProxyResolver, reconciliationAggregateRepository.Object); + + ProcessReconciliationTransactionResponse response = await transactionDomainService.ProcessReconciliationTransaction(TestData.TransactionId, + TestData.EstateId, + TestData.MerchantId, + TestData.DeviceIdentifier1, + TestData.TransactionDateTime, + TestData.ReconciliationTransactionCount, + TestData.ReconciliationTransactionValue, + CancellationToken.None); + + this.ValidateResponse(response, TransactionResponseCode.NoValidDevices); + } + + [Fact] + public async Task TransactionDomainService_ProcessReconciliationTransaction_InvalidEstate_TransactionIsProcessed() + { + IConfigurationRoot configurationRoot = new ConfigurationBuilder().AddInMemoryCollection(TestData.DefaultAppSettings).Build(); + ConfigurationReader.Initialise(configurationRoot); + + Logger.Initialise(NullLogger.Instance); + + Mock transactionAggregateManager = new Mock(); + Mock estateClient = new Mock(); + Mock securityServiceClient = new Mock(); + Mock operatorProxy = new Mock(); + Func operatorProxyResolver = (operatorName) => { return operatorProxy.Object; }; + + securityServiceClient.Setup(s => s.GetToken(It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(TestData.TokenResponse); + estateClient.Setup(e => e.GetEstate(It.IsAny(), It.IsAny(), It.IsAny())).ThrowsAsync(new Exception("Exception", new KeyNotFoundException("Invalid Estate"))); + estateClient.Setup(e => e.GetMerchant(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(TestData.GetMerchantResponseWithOperator1); + + Mock> reconciliationAggregateRepository = + new Mock>(); + reconciliationAggregateRepository.Setup(r => r.GetLatestVersion(It.IsAny(), It.IsAny())) + .ReturnsAsync(new ReconciliationAggregate()); + + TransactionDomainService transactionDomainService = + new TransactionDomainService(transactionAggregateManager.Object, estateClient.Object, securityServiceClient.Object, + operatorProxyResolver, reconciliationAggregateRepository.Object); + + ProcessReconciliationTransactionResponse response = await transactionDomainService.ProcessReconciliationTransaction(TestData.TransactionId, + TestData.EstateId, + TestData.MerchantId, + TestData.DeviceIdentifier, + TestData.TransactionDateTime, + TestData.ReconciliationTransactionCount, + TestData.ReconciliationTransactionValue, + CancellationToken.None); + + this.ValidateResponse(response, TransactionResponseCode.InvalidEstateId); + } + + [Fact] + public async Task TransactionDomainService_ProcessReconciliationTransaction_InvalidMerchant_TransactionIsProcessed() + { + IConfigurationRoot configurationRoot = new ConfigurationBuilder().AddInMemoryCollection(TestData.DefaultAppSettings).Build(); + ConfigurationReader.Initialise(configurationRoot); + + Logger.Initialise(NullLogger.Instance); + + Mock transactionAggregateManager = new Mock(); + Mock estateClient = new Mock(); + Mock securityServiceClient = new Mock(); + Mock operatorProxy = new Mock(); + Func operatorProxyResolver = (operatorName) => { return operatorProxy.Object; }; + + securityServiceClient.Setup(s => s.GetToken(It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(TestData.TokenResponse); + estateClient.Setup(e => e.GetEstate(It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(TestData.GetEstateResponseWithOperator1); + estateClient.Setup(e => e.GetMerchant(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ThrowsAsync(new Exception("Exception", new KeyNotFoundException("Invalid Merchant"))); + + Mock> reconciliationAggregateRepository = + new Mock>(); + reconciliationAggregateRepository.Setup(r => r.GetLatestVersion(It.IsAny(), It.IsAny())) + .ReturnsAsync(new ReconciliationAggregate()); + TransactionDomainService transactionDomainService = + new TransactionDomainService(transactionAggregateManager.Object, estateClient.Object, securityServiceClient.Object, + operatorProxyResolver, reconciliationAggregateRepository.Object); + + ProcessReconciliationTransactionResponse response = await transactionDomainService.ProcessReconciliationTransaction(TestData.TransactionId, + TestData.EstateId, + TestData.MerchantId, + TestData.DeviceIdentifier, + TestData.TransactionDateTime, + TestData.ReconciliationTransactionCount, + TestData.ReconciliationTransactionValue, + CancellationToken.None); + + this.ValidateResponse(response, TransactionResponseCode.InvalidMerchantId); + } + + //###### + [Fact] public async Task TransactionDomainService_ProcessLogonTransaction_TransactionIsProcessed() { @@ -48,9 +256,13 @@ public async Task TransactionDomainService_ProcessLogonTransaction_TransactionIs Mock operatorProxy = new Mock(); Func operatorProxyResolver = (operatorName) => { return operatorProxy.Object; }; + Mock> reconciliationAggregateRepository = + new Mock>(); + TransactionDomainService transactionDomainService = new TransactionDomainService(transactionAggregateManager.Object, estateClient.Object, securityServiceClient.Object, - operatorProxyResolver); + operatorProxyResolver, + reconciliationAggregateRepository.Object); ProcessLogonTransactionResponse response = await transactionDomainService.ProcessLogonTransaction(TestData.TransactionId, TestData.EstateId, @@ -84,9 +296,12 @@ public async Task TransactionDomainService_ProcessLogonTransaction_MerchantWithN transactionAggregateManager.Setup(t => t.GetAggregate(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(TestData.GetCompletedLogonTransactionAggregate); + Mock> reconciliationAggregateRepository = + new Mock>(); + TransactionDomainService transactionDomainService = new TransactionDomainService(transactionAggregateManager.Object, estateClient.Object, securityServiceClient.Object, - operatorProxyResolver); + operatorProxyResolver, reconciliationAggregateRepository.Object); ProcessLogonTransactionResponse response = await transactionDomainService.ProcessLogonTransaction(TestData.TransactionId, TestData.EstateId, @@ -121,9 +336,12 @@ public async Task TransactionDomainService_ProcessLogonTransaction_MerchantWithN transactionAggregateManager.Setup(t => t.GetAggregate(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(TestData.GetCompletedLogonTransactionAggregate); + Mock> reconciliationAggregateRepository = + new Mock>(); + TransactionDomainService transactionDomainService = new TransactionDomainService(transactionAggregateManager.Object, estateClient.Object, securityServiceClient.Object, - operatorProxyResolver); + operatorProxyResolver, reconciliationAggregateRepository.Object); ProcessLogonTransactionResponse response = await transactionDomainService.ProcessLogonTransaction(TestData.TransactionId, TestData.EstateId, @@ -157,9 +375,12 @@ public async Task TransactionDomainService_ProcessLogonTransaction_IncorrectDevi transactionAggregateManager.Setup(t => t.GetAggregate(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(TestData.GetLocallyDeclinedTransactionAggregate(TransactionResponseCode.InvalidDeviceIdentifier)); + Mock> reconciliationAggregateRepository = + new Mock>(); + TransactionDomainService transactionDomainService = new TransactionDomainService(transactionAggregateManager.Object, estateClient.Object, securityServiceClient.Object, - operatorProxyResolver); + operatorProxyResolver, reconciliationAggregateRepository.Object); ProcessLogonTransactionResponse response = await transactionDomainService.ProcessLogonTransaction(TestData.TransactionId, TestData.EstateId, @@ -193,9 +414,12 @@ public async Task TransactionDomainService_ProcessLogonTransaction_InvalidEstate transactionAggregateManager.Setup(t => t.GetAggregate(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(TestData.GetLocallyDeclinedTransactionAggregate(TransactionResponseCode.InvalidEstateId)); + Mock> reconciliationAggregateRepository = + new Mock>(); + TransactionDomainService transactionDomainService = new TransactionDomainService(transactionAggregateManager.Object, estateClient.Object, securityServiceClient.Object, - operatorProxyResolver); + operatorProxyResolver, reconciliationAggregateRepository.Object); ProcessLogonTransactionResponse response = await transactionDomainService.ProcessLogonTransaction(TestData.TransactionId, TestData.EstateId, @@ -229,9 +453,12 @@ public async Task TransactionDomainService_ProcessLogonTransaction_InvalidMercha transactionAggregateManager.Setup(t => t.GetAggregate(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(TestData.GetLocallyDeclinedTransactionAggregate(TransactionResponseCode.InvalidMerchantId)); + Mock> reconciliationAggregateRepository = + new Mock>(); + TransactionDomainService transactionDomainService = new TransactionDomainService(transactionAggregateManager.Object, estateClient.Object, securityServiceClient.Object, - operatorProxyResolver); + operatorProxyResolver, reconciliationAggregateRepository.Object); ProcessLogonTransactionResponse response = await transactionDomainService.ProcessLogonTransaction(TestData.TransactionId, TestData.EstateId, @@ -279,9 +506,12 @@ public async Task TransactionDomainService_ProcessSaleTransaction_SuccesfulOpera }); Func operatorProxyResolver = (operatorName) => { return operatorProxy.Object; }; + Mock> reconciliationAggregateRepository = + new Mock>(); + TransactionDomainService transactionDomainService = new TransactionDomainService(transactionAggregateManager.Object, estateClient.Object, securityServiceClient.Object, - operatorProxyResolver); + operatorProxyResolver, reconciliationAggregateRepository.Object); ProcessSaleTransactionResponse response = await transactionDomainService.ProcessSaleTransaction(TestData.TransactionId, TestData.EstateId, @@ -332,9 +562,12 @@ public async Task TransactionDomainService_ProcessSaleTransaction_FailedOperator }); Func operatorProxyResolver = (operatorName) => { return operatorProxy.Object; }; + Mock> reconciliationAggregateRepository = + new Mock>(); + TransactionDomainService transactionDomainService = new TransactionDomainService(transactionAggregateManager.Object, estateClient.Object, securityServiceClient.Object, - operatorProxyResolver); + operatorProxyResolver, reconciliationAggregateRepository.Object); ProcessSaleTransactionResponse response = await transactionDomainService.ProcessSaleTransaction(TestData.TransactionId, TestData.EstateId, @@ -373,9 +606,12 @@ public async Task TransactionDomainService_ProcessSaleTransaction_MerchantWithNu transactionAggregateManager.Setup(t => t.GetAggregate(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(TestData.GetLocallyDeclinedTransactionAggregate(TransactionResponseCode.NoValidDevices)); + Mock> reconciliationAggregateRepository = + new Mock>(); + TransactionDomainService transactionDomainService = new TransactionDomainService(transactionAggregateManager.Object, estateClient.Object, securityServiceClient.Object, - operatorProxyResolver); + operatorProxyResolver, reconciliationAggregateRepository.Object); ProcessSaleTransactionResponse response = await transactionDomainService.ProcessSaleTransaction(TestData.TransactionId, TestData.EstateId, @@ -415,9 +651,12 @@ public async Task TransactionDomainService_ProcessSaleTransaction_MerchantWithNo transactionAggregateManager.Setup(t => t.GetAggregate(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(TestData.GetDeclinedTransactionAggregate(TransactionResponseCode.NoValidDevices)); + Mock> reconciliationAggregateRepository = + new Mock>(); + TransactionDomainService transactionDomainService = new TransactionDomainService(transactionAggregateManager.Object, estateClient.Object, securityServiceClient.Object, - operatorProxyResolver); + operatorProxyResolver, reconciliationAggregateRepository.Object); ProcessSaleTransactionResponse response = await transactionDomainService.ProcessSaleTransaction(TestData.TransactionId, TestData.EstateId, @@ -456,9 +695,12 @@ public async Task TransactionDomainService_ProcessSaleTransaction_IncorrectDevic transactionAggregateManager.Setup(t => t.GetAggregate(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(TestData.GetLocallyDeclinedTransactionAggregate(TransactionResponseCode.InvalidDeviceIdentifier)); + Mock> reconciliationAggregateRepository = + new Mock>(); + TransactionDomainService transactionDomainService = new TransactionDomainService(transactionAggregateManager.Object, estateClient.Object, securityServiceClient.Object, - operatorProxyResolver); + operatorProxyResolver, reconciliationAggregateRepository.Object); ProcessSaleTransactionResponse response = await transactionDomainService.ProcessSaleTransaction(TestData.TransactionId, TestData.EstateId, @@ -497,9 +739,12 @@ public async Task TransactionDomainService_ProcessSaleTransaction_InvalidEstate_ transactionAggregateManager.Setup(t => t.GetAggregate(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(TestData.GetLocallyDeclinedTransactionAggregate(TransactionResponseCode.InvalidEstateId)); + Mock> reconciliationAggregateRepository = + new Mock>(); + TransactionDomainService transactionDomainService = new TransactionDomainService(transactionAggregateManager.Object, estateClient.Object, securityServiceClient.Object, - operatorProxyResolver); + operatorProxyResolver, reconciliationAggregateRepository.Object); ProcessSaleTransactionResponse response = await transactionDomainService.ProcessSaleTransaction(TestData.TransactionId, TestData.EstateId, @@ -538,9 +783,13 @@ public async Task TransactionDomainService_ProcessSaleTransaction_NotEnoughCredi transactionAggregateManager.Setup(t => t.GetAggregate(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(TestData.GetLocallyDeclinedTransactionAggregate(TransactionResponseCode.MerchantDoesNotHaveEnoughCredit)); + Mock> reconciliationAggregateRepository = + new Mock>(); + TransactionDomainService transactionDomainService = new TransactionDomainService(transactionAggregateManager.Object, estateClient.Object, securityServiceClient.Object, - operatorProxyResolver); + operatorProxyResolver, + reconciliationAggregateRepository.Object); ProcessSaleTransactionResponse response = await transactionDomainService.ProcessSaleTransaction(TestData.TransactionId, TestData.EstateId, @@ -578,9 +827,12 @@ public async Task TransactionDomainService_ProcessSaleTransaction_InvalidMerchan transactionAggregateManager.Setup(t => t.GetAggregate(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(TestData.GetLocallyDeclinedTransactionAggregate(TransactionResponseCode.InvalidMerchantId)); + Mock> reconciliationAggregateRepository = + new Mock>(); + TransactionDomainService transactionDomainService = new TransactionDomainService(transactionAggregateManager.Object, estateClient.Object, securityServiceClient.Object, - operatorProxyResolver); + operatorProxyResolver, reconciliationAggregateRepository.Object); ProcessSaleTransactionResponse response = await transactionDomainService.ProcessSaleTransaction(TestData.TransactionId, TestData.EstateId, @@ -619,9 +871,12 @@ public async Task TransactionDomainService_ProcessSaleTransaction_EstateWithEmpt transactionAggregateManager.Setup(t => t.GetAggregate(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(TestData.GetLocallyDeclinedTransactionAggregate(TransactionResponseCode.NoEstateOperators)); + Mock> reconciliationAggregateRepository = + new Mock>(); + TransactionDomainService transactionDomainService = new TransactionDomainService(transactionAggregateManager.Object, estateClient.Object, securityServiceClient.Object, - operatorProxyResolver); + operatorProxyResolver, reconciliationAggregateRepository.Object); ProcessSaleTransactionResponse response = await transactionDomainService.ProcessSaleTransaction(TestData.TransactionId, TestData.EstateId, @@ -660,9 +915,12 @@ public async Task TransactionDomainService_ProcessSaleTransaction_EstateWithNull transactionAggregateManager.Setup(t => t.GetAggregate(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(TestData.GetLocallyDeclinedTransactionAggregate(TransactionResponseCode.NoEstateOperators)); + Mock> reconciliationAggregateRepository = + new Mock>(); + TransactionDomainService transactionDomainService = new TransactionDomainService(transactionAggregateManager.Object, estateClient.Object, securityServiceClient.Object, - operatorProxyResolver); + operatorProxyResolver,reconciliationAggregateRepository.Object); ProcessSaleTransactionResponse response = await transactionDomainService.ProcessSaleTransaction(TestData.TransactionId, TestData.EstateId, @@ -701,9 +959,12 @@ public async Task TransactionDomainService_ProcessSaleTransaction_OperatorNotSup transactionAggregateManager.Setup(t => t.GetAggregate(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(TestData.GetDeclinedTransactionAggregate(TransactionResponseCode.OperatorNotValidForEstate)); + Mock> reconciliationAggregateRepository = + new Mock>(); + TransactionDomainService transactionDomainService = new TransactionDomainService(transactionAggregateManager.Object, estateClient.Object, securityServiceClient.Object, - operatorProxyResolver); + operatorProxyResolver, reconciliationAggregateRepository.Object); ProcessSaleTransactionResponse response = await transactionDomainService.ProcessSaleTransaction(TestData.TransactionId, TestData.EstateId, @@ -742,9 +1003,13 @@ public async Task TransactionDomainService_ProcessSaleTransaction_MerchantWithEm transactionAggregateManager.Setup(t => t.GetAggregate(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(TestData.GetDeclinedTransactionAggregate(TransactionResponseCode.NoMerchantOperators)); + Mock> reconciliationAggregateRepository = + new Mock>(); + TransactionDomainService transactionDomainService = new TransactionDomainService(transactionAggregateManager.Object, estateClient.Object, securityServiceClient.Object, - operatorProxyResolver); + operatorProxyResolver, + reconciliationAggregateRepository.Object); ProcessSaleTransactionResponse response = await transactionDomainService.ProcessSaleTransaction(TestData.TransactionId, TestData.EstateId, @@ -783,9 +1048,12 @@ public async Task TransactionDomainService_ProcessSaleTransaction_MerchantWithNu transactionAggregateManager.Setup(t => t.GetAggregate(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(TestData.GetDeclinedTransactionAggregate(TransactionResponseCode.NoMerchantOperators)); + Mock> reconciliationAggregateRepository = + new Mock>(); + TransactionDomainService transactionDomainService = new TransactionDomainService(transactionAggregateManager.Object, estateClient.Object, securityServiceClient.Object, - operatorProxyResolver); + operatorProxyResolver,reconciliationAggregateRepository.Object); ProcessSaleTransactionResponse response = await transactionDomainService.ProcessSaleTransaction(TestData.TransactionId, TestData.EstateId, @@ -824,9 +1092,12 @@ public async Task TransactionDomainService_ProcessSaleTransaction_OperatorNotSup transactionAggregateManager.Setup(t => t.GetAggregate(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(TestData.GetDeclinedTransactionAggregate(TransactionResponseCode.OperatorNotValidForMerchant)); + Mock> reconciliationAggregateRepository = + new Mock>(); + TransactionDomainService transactionDomainService = new TransactionDomainService(transactionAggregateManager.Object, estateClient.Object, securityServiceClient.Object, - operatorProxyResolver); + operatorProxyResolver, reconciliationAggregateRepository.Object); ProcessSaleTransactionResponse response = await transactionDomainService.ProcessSaleTransaction(TestData.TransactionId, TestData.EstateId, @@ -873,5 +1144,13 @@ private void ValidateResponse(ProcessSaleTransactionResponse response, response.ResponseMessage.ShouldBe(messageToValidate); } + + private void ValidateResponse(ProcessReconciliationTransactionResponse response, + TransactionResponseCode transactionResponseCode) + { + response.ShouldNotBeNull(); + response.ResponseCode.ShouldBe(TestData.GetResponseCodeAsString(transactionResponseCode)); + + } } } diff --git a/TransactionProcessor.BusinessLogic/RequestHandlers/TransactionRequestHandler.cs b/TransactionProcessor.BusinessLogic/RequestHandlers/TransactionRequestHandler.cs index ed244832..b8e8c39d 100644 --- a/TransactionProcessor.BusinessLogic/RequestHandlers/TransactionRequestHandler.cs +++ b/TransactionProcessor.BusinessLogic/RequestHandlers/TransactionRequestHandler.cs @@ -10,10 +10,14 @@ /// /// /// + /// + /// + /// /// /// public class TransactionRequestHandler : IRequestHandler, - IRequestHandler + IRequestHandler, + IRequestHandler { #region Fields @@ -26,6 +30,10 @@ public class TransactionRequestHandler : IRequestHandler + /// Initializes a new instance of the class. + /// + /// The transaction domain service. public TransactionRequestHandler(ITransactionDomainService transactionDomainService) { this.TransactionDomainService = transactionDomainService; @@ -40,7 +48,9 @@ public TransactionRequestHandler(ITransactionDomainService transactionDomainServ /// /// The request. /// The cancellation token. - /// + /// + /// Response from the request + /// public async Task Handle(ProcessLogonTransactionRequest request, CancellationToken cancellationToken) { @@ -56,31 +66,56 @@ await this.TransactionDomainService.ProcessLogonTransaction(request.TransactionI return logonResponse; } - #endregion - /// /// Handles the specified request. /// /// The request. /// The cancellation token. - /// + /// + /// Response from the request + /// public async Task Handle(ProcessSaleTransactionRequest request, CancellationToken cancellationToken) { ProcessSaleTransactionResponse saleResponse = await this.TransactionDomainService.ProcessSaleTransaction(request.TransactionId, - request.EstateId, - request.MerchantId, - request.TransactionDateTime, - request.TransactionNumber, - request.DeviceIdentifier, - request.OperatorIdentifier, - request.CustomerEmailAddress, - request.AdditionalTransactionMetadata, - request.ContractId, - request.ProductId, - cancellationToken); + request.EstateId, + request.MerchantId, + request.TransactionDateTime, + request.TransactionNumber, + request.DeviceIdentifier, + request.OperatorIdentifier, + request.CustomerEmailAddress, + request.AdditionalTransactionMetadata, + request.ContractId, + request.ProductId, + cancellationToken); return saleResponse; } + + /// + /// Handles a request + /// + /// The request + /// Cancellation token + /// + /// Response from the request + /// + public async Task Handle(ProcessReconciliationRequest request, + CancellationToken cancellationToken) + { + ProcessReconciliationTransactionResponse reconciliationResponse= await this.TransactionDomainService.ProcessReconciliationTransaction(request.TransactionId, + request.EstateId, + request.MerchantId, + request.DeviceIdentifier, + request.TransactionDateTime, + request.TransactionCount, + request.TransactionValue, + cancellationToken); + + return reconciliationResponse; + } + + #endregion } } \ No newline at end of file diff --git a/TransactionProcessor.BusinessLogic/Requests/ProcessReconciliationRequest.cs b/TransactionProcessor.BusinessLogic/Requests/ProcessReconciliationRequest.cs new file mode 100644 index 00000000..272dd925 --- /dev/null +++ b/TransactionProcessor.BusinessLogic/Requests/ProcessReconciliationRequest.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace TransactionProcessor.BusinessLogic.Requests +{ + using System.Diagnostics.CodeAnalysis; + using MediatR; + using Models; + + public class ProcessReconciliationRequest : IRequest + { + /// + /// Initializes a new instance of the class. + /// + /// The transaction identifier. + /// The estate identifier. + /// The merchant identifier. + /// The device identifier. + /// The transaction date time. + /// The transaction count. + /// The transaction value. + private ProcessReconciliationRequest(Guid transactionId, + Guid estateId, + Guid merchantId, + String deviceIdentifier, + DateTime transactionDateTime, + Int32 transactionCount, + Decimal transactionValue) + { + this.TransactionId = transactionId; + this.EstateId = estateId; + this.MerchantId = merchantId; + this.DeviceIdentifier = deviceIdentifier; + this.TransactionDateTime = transactionDateTime; + this.TransactionCount = transactionCount; + this.TransactionValue = transactionValue; + } + + /// + /// Gets the transaction identifier. + /// + /// + /// The transaction identifier. + /// + public Guid TransactionId { get; } + + /// + /// Gets the estate identifier. + /// + /// + /// The estate identifier. + /// + public Guid EstateId { get; } + + /// + /// Gets the merchant identifier. + /// + /// + /// The merchant identifier. + /// + public Guid MerchantId { get; } + + public String DeviceIdentifier { get; } + + public DateTime TransactionDateTime { get; } + + public Int32 TransactionCount { get; } + + public Decimal TransactionValue { get; } + + public static ProcessReconciliationRequest Create(Guid transactionId, + Guid estateId, + Guid merchantId, + String deviceIdentifier, + DateTime transactionDateTime, + Int32 transactionCount, + Decimal transactionValue) + { + return new ProcessReconciliationRequest(transactionId, estateId, merchantId, deviceIdentifier, transactionDateTime, transactionCount, transactionValue); + } + } +} diff --git a/TransactionProcessor.BusinessLogic/Services/ITransactionDomainService.cs b/TransactionProcessor.BusinessLogic/Services/ITransactionDomainService.cs index 17e5b5fc..40fd157d 100644 --- a/TransactionProcessor.BusinessLogic/Services/ITransactionDomainService.cs +++ b/TransactionProcessor.BusinessLogic/Services/ITransactionDomainService.cs @@ -61,6 +61,27 @@ Task ProcessSaleTransaction(Guid transactionId, Guid productId, CancellationToken cancellationToken); + /// + /// Processes the reconciliation transaction. + /// + /// The transaction identifier. + /// The estate identifier. + /// The merchant identifier. + /// The device identifier. + /// The transaction date time. + /// The transaction count. + /// The transaction value. + /// The cancellation token. + /// + Task ProcessReconciliationTransaction(Guid transactionId, + Guid estateId, + Guid merchantId, + String deviceIdentifier, + DateTime transactionDateTime, + Int32 transactionCount, + Decimal transactionValue, + CancellationToken cancellationToken); + #endregion } } \ No newline at end of file diff --git a/TransactionProcessor.BusinessLogic/Services/TransactionDomainService.cs b/TransactionProcessor.BusinessLogic/Services/TransactionDomainService.cs index 865dc6d9..546c3c7c 100644 --- a/TransactionProcessor.BusinessLogic/Services/TransactionDomainService.cs +++ b/TransactionProcessor.BusinessLogic/Services/TransactionDomainService.cs @@ -12,8 +12,10 @@ using EstateManagement.DataTransferObjects.Responses; using Models; using OperatorInterfaces; + using ReconciliationAggregate; using SecurityService.Client; using SecurityService.DataTransferObjects.Responses; + using Shared.EventStore.EventStore; using Shared.General; using Shared.Logger; using TransactionAggregate; @@ -31,6 +33,9 @@ public class TransactionDomainService : ITransactionDomainService /// private readonly IEstateClient EstateClient; + /// + /// The operator proxy resolver + /// private readonly Func OperatorProxyResolver; /// @@ -38,6 +43,9 @@ public class TransactionDomainService : ITransactionDomainService /// private readonly ISecurityServiceClient SecurityServiceClient; + /// + /// The token response + /// private TokenResponse TokenResponse; /// @@ -45,6 +53,11 @@ public class TransactionDomainService : ITransactionDomainService /// private readonly ITransactionAggregateManager TransactionAggregateManager; + /// + /// The reconciliation aggregate repository + /// + private readonly IAggregateRepository ReconciliationAggregateRepository; + #endregion #region Constructors @@ -56,15 +69,18 @@ public class TransactionDomainService : ITransactionDomainService /// The estate client. /// The security service client. /// The operator proxy resolver. + /// The reconciliation aggregate repository. public TransactionDomainService(ITransactionAggregateManager transactionAggregateManager, IEstateClient estateClient, ISecurityServiceClient securityServiceClient, - Func operatorProxyResolver) + Func operatorProxyResolver, + IAggregateRepository reconciliationAggregateRepository) { this.TransactionAggregateManager = transactionAggregateManager; this.EstateClient = estateClient; this.SecurityServiceClient = securityServiceClient; this.OperatorProxyResolver = operatorProxyResolver; + this.ReconciliationAggregateRepository = reconciliationAggregateRepository; } #endregion @@ -173,7 +189,7 @@ public async Task ProcessSaleTransaction(Guid tr (String responseMessage, TransactionResponseCode responseCode) validationResult = await this.ValidateSaleTransaction(estateId, merchantId, deviceIdentifier, operatorIdentifier, transactionAmount, cancellationToken); - + await this.TransactionAggregateManager.StartTransaction(transactionId, transactionDateTime, transactionNumber, @@ -190,7 +206,7 @@ await this.TransactionAggregateManager.StartTransaction(transactionId, { await this.TransactionAggregateManager.AddProductDetails(estateId, transactionId, contractId, productId, cancellationToken); } - + if (validationResult.responseCode == TransactionResponseCode.Success) { // Record any additional request metadata @@ -271,6 +287,60 @@ await this.TransactionAggregateManager.RecordAdditionalResponseData(estateId, }; } + /// + /// Processes the reconciliation transaction. + /// + /// The transaction identifier. + /// The estate identifier. + /// The merchant identifier. + /// The device identifier. + /// The transaction date time. + /// The transaction count. + /// The transaction value. + /// The cancellation token. + /// + public async Task ProcessReconciliationTransaction(Guid transactionId, + Guid estateId, + Guid merchantId, + String deviceIdentifier, + DateTime transactionDateTime, + Int32 transactionCount, + Decimal transactionValue, + CancellationToken cancellationToken) + { + (String responseMessage, TransactionResponseCode responseCode) validationResult = + await this.ValidateReconciliationTransaction(estateId, merchantId, deviceIdentifier, cancellationToken); + + ReconciliationAggregate reconciliationAggregate = await this.ReconciliationAggregateRepository.GetLatestVersion(transactionId, cancellationToken); + + reconciliationAggregate.StartReconciliation(transactionDateTime, estateId, merchantId); + + reconciliationAggregate.RecordOverallTotals(transactionCount, transactionValue); + + if (validationResult.responseCode == TransactionResponseCode.Success) + { + // Record the successful validation + reconciliationAggregate.Authorise(((Int32)validationResult.responseCode).ToString().PadLeft(4, '0'), validationResult.responseMessage); + } + else + { + // Record the failure + reconciliationAggregate.Decline(((Int32)validationResult.responseCode).ToString().PadLeft(4, '0'), validationResult.responseMessage); + } + + reconciliationAggregate.CompleteReconciliation(); + + await this.ReconciliationAggregateRepository.SaveChanges(reconciliationAggregate, cancellationToken); + + return new ProcessReconciliationTransactionResponse + { + EstateId = reconciliationAggregate.EstateId, + MerchantId = reconciliationAggregate.MerchantId, + ResponseCode = reconciliationAggregate.ResponseCode, + ResponseMessage = reconciliationAggregate.ResponseMessage + }; + } + /// /// Extracts the field from metadata. /// @@ -280,7 +350,7 @@ await this.TransactionAggregateManager.RecordAdditionalResponseData(estateId, /// [ExcludeFromCodeCoverage] private T ExtractFieldFromMetadata(String fieldName, - Dictionary additionalTransactionMetadata) + Dictionary additionalTransactionMetadata) { if (additionalTransactionMetadata.ContainsKey(fieldName)) { @@ -294,6 +364,13 @@ private T ExtractFieldFromMetadata(String fieldName, } + /// + /// Adds the device to merchant. + /// + /// The estate identifier. + /// The merchant identifier. + /// The device identifier. + /// The cancellation token. private async Task AddDeviceToMerchant(Guid estateId, Guid merchantId, String deviceIdentifier, @@ -328,6 +405,12 @@ private String GenerateTransactionReference() return $"{i - DateTime.Now.Ticks:x}"; } + /// + /// Gets the estate. + /// + /// The estate identifier. + /// The cancellation token. + /// private async Task GetEstate(Guid estateId, CancellationToken cancellationToken) { @@ -338,6 +421,13 @@ private async Task GetEstate(Guid estateId, return estate; } + /// + /// Gets the merchant. + /// + /// The estate identifier. + /// The merchant identifier. + /// The cancellation token. + /// private async Task GetMerchant(Guid estateId, Guid merchantId, CancellationToken cancellationToken) @@ -388,11 +478,12 @@ private async Task GetToken(CancellationToken cancellationToken) /// The device identifier. /// The cancellation token. /// + /// Device Identifier {deviceIdentifier} not valid for Merchant {merchant.MerchantName} /// Device Identifier {deviceIdentifier} not valid for Merchant {merchant.MerchantName} private async Task<(String responseMessage, TransactionResponseCode responseCode)> ValidateLogonTransaction(Guid estateId, - Guid merchantId, - String deviceIdentifier, - CancellationToken cancellationToken) + Guid merchantId, + String deviceIdentifier, + CancellationToken cancellationToken) { try { @@ -448,11 +539,11 @@ private async Task GetToken(CancellationToken cancellationToken) /// or /// Operator {operatorIdentifier} not configured for Merchant [{merchant.MerchantName}] private async Task<(String responseMessage, TransactionResponseCode responseCode)> ValidateSaleTransaction(Guid estateId, - Guid merchantId, - String deviceIdentifier, - String operatorIdentifier, - Decimal transactionAmount, - CancellationToken cancellationToken) + Guid merchantId, + String deviceIdentifier, + String operatorIdentifier, + Decimal transactionAmount, + CancellationToken cancellationToken) { try { @@ -512,8 +603,9 @@ private async Task GetToken(CancellationToken cancellationToken) // Check the merchant has enough balance to perform the sale if (merchant.AvailableBalance < transactionAmount) { - throw new TransactionValidationException($"Merchant [{merchant.MerchantName}] does not have enough credit available [{merchant.AvailableBalance}] to perform transaction amount [{transactionAmount}]", - TransactionResponseCode.MerchantDoesNotHaveEnoughCredit); + throw new + TransactionValidationException($"Merchant [{merchant.MerchantName}] does not have enough credit available [{merchant.AvailableBalance}] to perform transaction amount [{transactionAmount}]", + TransactionResponseCode.MerchantDoesNotHaveEnoughCredit); } // If we get here everything is good @@ -532,11 +624,9 @@ private async Task GetToken(CancellationToken cancellationToken) /// The merchant identifier. /// The cancellation token. /// - /// - /// Estate Id [{estateId}] is not a valid estate + /// Estate Id [{estateId}] is not a valid estate /// or - /// Merchant Id [{merchantId}] is not a valid merchant for estate [{estate.EstateName}] - /// + /// Merchant Id [{merchantId}] is not a valid merchant for estate [{estate.EstateName}] private async Task<(EstateResponse estate, MerchantResponse merchant)> ValidateTransaction(Guid estateId, Guid merchantId, CancellationToken cancellationToken) @@ -559,7 +649,7 @@ private async Task GetToken(CancellationToken cancellationToken) { merchant = await this.GetMerchant(estateId, merchantId, cancellationToken); } - catch (Exception ex) when (ex.InnerException != null && ex.InnerException.GetType() == typeof(KeyNotFoundException)) + catch(Exception ex) when(ex.InnerException != null && ex.InnerException.GetType() == typeof(KeyNotFoundException)) { throw new TransactionValidationException($"Merchant Id [{merchantId}] is not a valid merchant for estate [{estate.EstateName}]", TransactionResponseCode.InvalidMerchantId); @@ -569,5 +659,52 @@ private async Task GetToken(CancellationToken cancellationToken) } #endregion + + /// + /// Validates the reconciliation transaction. + /// + /// The estate identifier. + /// The merchant identifier. + /// The device identifier. + /// The cancellation token. + /// + /// Merchant {merchant.MerchantName} has no valid Devices for this transaction. + /// or + /// Device Identifier {deviceIdentifier} not valid for Merchant {merchant.MerchantName} + private async Task<(String responseMessage, TransactionResponseCode responseCode)> ValidateReconciliationTransaction(Guid estateId, + Guid merchantId, + String deviceIdentifier, + CancellationToken cancellationToken) + { + try + { + (EstateResponse estate, MerchantResponse merchant) validateTransactionResponse = await this.ValidateTransaction(estateId, merchantId, cancellationToken); + MerchantResponse merchant = validateTransactionResponse.merchant; + + // Device Validation + if (merchant.Devices == null || merchant.Devices.Any() == false) + { + throw new TransactionValidationException($"Merchant {merchant.MerchantName} has no valid Devices for this transaction.", + TransactionResponseCode.NoValidDevices); + } + + // Validate the device + KeyValuePair device = merchant.Devices.SingleOrDefault(d => d.Value == deviceIdentifier); + + if (device.Key == Guid.Empty) + { + // Device not found,throw error + throw new TransactionValidationException($"Device Identifier {deviceIdentifier} not valid for Merchant {merchant.MerchantName}", + TransactionResponseCode.InvalidDeviceIdentifier); + } + + // If we get here everything is good + return ("SUCCESS", TransactionResponseCode.Success); + } + catch(TransactionValidationException tvex) + { + return (tvex.Message, tvex.ResponseCode); + } + } } } \ No newline at end of file diff --git a/TransactionProcessor.BusinessLogic/TransactionProcessor.BusinessLogic.csproj b/TransactionProcessor.BusinessLogic/TransactionProcessor.BusinessLogic.csproj index 2b5d2e9f..a2d983a8 100644 --- a/TransactionProcessor.BusinessLogic/TransactionProcessor.BusinessLogic.csproj +++ b/TransactionProcessor.BusinessLogic/TransactionProcessor.BusinessLogic.csproj @@ -17,6 +17,7 @@ + diff --git a/TransactionProcessor.DataTransferObjects/OperatorTotalRequest.cs b/TransactionProcessor.DataTransferObjects/OperatorTotalRequest.cs new file mode 100644 index 00000000..06eedf76 --- /dev/null +++ b/TransactionProcessor.DataTransferObjects/OperatorTotalRequest.cs @@ -0,0 +1,48 @@ +namespace TransactionProcessor.DataTransferObjects +{ + using System; + using System.Diagnostics.CodeAnalysis; + + /// + /// + /// + [ExcludeFromCodeCoverage] + public class OperatorTotalRequest + { + #region Properties + + /// + /// Gets or sets the contract identifier. + /// + /// + /// The contract identifier. + /// + public Guid ContractId { get; set; } + + /// + /// Gets or sets the operator identifier. + /// + /// + /// The operator identifier. + /// + public String OperatorIdentifier { get; set; } + + /// + /// Gets or sets the transaction count. + /// + /// + /// The transaction count. + /// + public Int32 TransactionCount { get; set; } + + /// + /// Gets or sets the transaction value. + /// + /// + /// The transaction value. + /// + public Decimal TransactionValue { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/TransactionProcessor.DataTransferObjects/ReconciliationRequest.cs b/TransactionProcessor.DataTransferObjects/ReconciliationRequest.cs new file mode 100644 index 00000000..d736561e --- /dev/null +++ b/TransactionProcessor.DataTransferObjects/ReconciliationRequest.cs @@ -0,0 +1,58 @@ +namespace TransactionProcessor.DataTransferObjects +{ + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + + /// + /// + /// + /// + [ExcludeFromCodeCoverage] + public class ReconciliationRequest : DataTransferObject + { + #region Properties + + /// + /// Gets or sets the device identifier. + /// + /// + /// The device identifier. + /// + public String DeviceIdentifier { get; set; } + + /// + /// Gets or sets the operator totals. + /// + /// + /// The operator totals. + /// + public List OperatorTotals { get; set; } + + /// + /// Gets or sets the transaction count. + /// + /// + /// The transaction count. + /// + public Int32 TransactionCount { get; set; } + + /// + /// Gets or sets the transaction date time. + /// + /// + /// The transaction date time. + /// + public DateTime TransactionDateTime { get; set; } + + /// + /// Gets or sets the transaction value. + /// + /// + /// The transaction value. + /// + public Decimal TransactionValue { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/TransactionProcessor.DataTransferObjects/ReconciliationResponse.cs b/TransactionProcessor.DataTransferObjects/ReconciliationResponse.cs new file mode 100644 index 00000000..49d654af --- /dev/null +++ b/TransactionProcessor.DataTransferObjects/ReconciliationResponse.cs @@ -0,0 +1,45 @@ +namespace TransactionProcessor.DataTransferObjects +{ + using System; + using System.Diagnostics.CodeAnalysis; + + [ExcludeFromCodeCoverage] + public class ReconciliationResponse + { + #region Properties + + /// + /// Gets or sets the estate identifier. + /// + /// + /// The estate identifier. + /// + public Guid EstateId { get; set; } + + /// + /// Gets or sets the merchant identifier. + /// + /// + /// The merchant identifier. + /// + public Guid MerchantId { get; set; } + + /// + /// Gets or sets the response code. + /// + /// + /// The response code. + /// + public String ResponseCode { get; set; } + + /// + /// Gets or sets the response message. + /// + /// + /// The response message. + /// + public String ResponseMessage { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/TransactionProcessor.IntegrationTests/Common/EstateDetails.cs b/TransactionProcessor.IntegrationTests/Common/EstateDetails.cs index 2970fd77..ba0164cb 100644 --- a/TransactionProcessor.IntegrationTests/Common/EstateDetails.cs +++ b/TransactionProcessor.IntegrationTests/Common/EstateDetails.cs @@ -55,6 +55,7 @@ private EstateDetails(Guid estateId, this.Operators = new Dictionary(); this.MerchantUsers = new Dictionary>(); this.TransactionResponses = new Dictionary<(Guid merchantId, String transactionNumber), SerialisedMessage>(); + this.ReconciliationResponses = new Dictionary(); this.Contracts = new List(); } @@ -110,6 +111,14 @@ private EstateDetails(Guid estateId, /// private Dictionary<(Guid merchantId, String transactionNumber), SerialisedMessage> TransactionResponses { get; } + /// + /// Gets the reconciliation responses. + /// + /// + /// The reconciliation responses. + /// + private Dictionary ReconciliationResponses { get; } + #endregion #region Methods @@ -219,6 +228,12 @@ public void AddTransactionResponse(Guid merchantId, this.TransactionResponses.Add((merchantId, transactionNumber), transactionResponse); } + public void AddReconciliationResponse(Guid merchantId, + SerialisedMessage reconciliationResponse) + { + this.ReconciliationResponses.Add(merchantId, reconciliationResponse); + } + /// /// Creates the specified estate identifier. /// @@ -295,6 +310,14 @@ public SerialisedMessage GetTransactionResponse(Guid merchantId, return transactionResponse.Value; } + public SerialisedMessage GetReconciliationResponse(Guid merchantId) + { + KeyValuePair reconciliationResponse = + this.ReconciliationResponses.Where(t => t.Key == merchantId).SingleOrDefault(); + + return reconciliationResponse.Value; + } + /// /// Sets the estate user. /// diff --git a/TransactionProcessor.IntegrationTests/Reconciliation/ReconciliationFeature.feature b/TransactionProcessor.IntegrationTests/Reconciliation/ReconciliationFeature.feature new file mode 100644 index 00000000..e88e3c52 --- /dev/null +++ b/TransactionProcessor.IntegrationTests/Reconciliation/ReconciliationFeature.feature @@ -0,0 +1,81 @@ +@base @shared +Feature: Reconciliation + +Background: + + Given the following api resources exist + | ResourceName | DisplayName | Secret | Scopes | UserClaims | + | estateManagement | Estate Managememt REST | Secret1 | estateManagement | MerchantId, EstateId, role | + | transactionProcessor | Transaction Processor REST | Secret1 | transactionProcessor | | + + Given the following clients exist + | ClientId | ClientName | Secret | AllowedScopes | AllowedGrantTypes | + | serviceClient | Service Client | Secret1 | estateManagement,transactionProcessor | client_credentials | + + Given I have a token to access the estate management and transaction processor resources + | ClientId | + | serviceClient | + + Given I have created the following estates + | EstateName | + | Test Estate 1 | + | Test Estate 2 | + + Given I have created the following operators + | EstateName | OperatorName | RequireCustomMerchantNumber | RequireCustomTerminalNumber | + | Test Estate 1 | Safaricom | True | True | + | Test Estate 2 | Safaricom | True | True | + + Given I create a contract with the following values + | EstateName | OperatorName | ContractDescription | + | Test Estate 1 | Safaricom | Safaricom Contract | + | Test Estate 2 | Safaricom | Safaricom Contract | + + When I create the following Products + | EstateName | OperatorName | ContractDescription | ProductName | DisplayText | Value | + | Test Estate 1 | Safaricom | Safaricom Contract | Variable Topup | Custom | | + | Test Estate 2 | Safaricom | Safaricom Contract | Variable Topup | Custom | | + + When I add the following Transaction Fees + | EstateName | OperatorName | ContractDescription | ProductName | CalculationType | FeeDescription | Value | + | Test Estate 1 | Safaricom | Safaricom Contract | Variable Topup | Fixed | Merchant Commission | 2.50 | + | Test Estate 2 | Safaricom | Safaricom Contract | Variable Topup | Percentage | Merchant Commission | 0.85 | + + Given I create the following merchants + | MerchantName | AddressLine1 | Town | Region | Country | ContactName | EmailAddress | EstateName | + | Test Merchant 1 | Address Line 1 | TestTown | Test Region | United Kingdom | Test Contact 1 | testcontact1@merchant1.co.uk | Test Estate 1 | + | Test Merchant 2 | Address Line 1 | TestTown | Test Region | United Kingdom | Test Contact 2 | testcontact2@merchant2.co.uk | Test Estate 1 | + | Test Merchant 3 | Address Line 1 | TestTown | Test Region | United Kingdom | Test Contact 3 | testcontact3@merchant2.co.uk | Test Estate 2 | + + Given I have assigned the following operator to the merchants + | OperatorName | MerchantName | MerchantNumber | TerminalNumber | EstateName | + | Safaricom | Test Merchant 1 | 00000001 | 10000001 | Test Estate 1 | + | Safaricom | Test Merchant 2 | 00000002 | 10000002 | Test Estate 1 | + | Safaricom | Test Merchant 3 | 00000003 | 10000003 | Test Estate 2 | + + Given I have assigned the following devices to the merchants + | DeviceIdentifier | MerchantName | EstateName | + | 123456780 | Test Merchant 1 | Test Estate 1 | + | 123456781 | Test Merchant 2 | Test Estate 1 | + | 123456782 | Test Merchant 3 | Test Estate 2 | + + Given I make the following manual merchant deposits + | Reference | Amount | DateTime | MerchantName | EstateName | + | Deposit1 | 200.00 | Today | Test Merchant 1 | Test Estate 1 | + | Deposit1 | 100.00 | Today | Test Merchant 2 | Test Estate 1 | + | Deposit1 | 100.00 | Today | Test Merchant 3 | Test Estate 2 | + +@PRTest +Scenario: Sale Transactions + + When I perform the following reconciliations + | DateTime | MerchantName | DeviceIdentifier | EstateName | TransactionCount | TransactionValue | + | Today | Test Merchant 1 | 123456780 | Test Estate 1 | 1 | 100.00 | + | Today | Test Merchant 2 | 123456781 | Test Estate 1 | 2 | 200.00 | + | Today | Test Merchant 3 | 123456782 | Test Estate 2 | 3 | 300.00 | + + Then reconciliation response should contain the following information + | EstateName | MerchantName | ResponseCode | ResponseMessage | + | Test Estate 1 | Test Merchant 1 | 0000 | SUCCESS | + | Test Estate 1 | Test Merchant 2 | 0000 | SUCCESS | + | Test Estate 2 | Test Merchant 3 | 0000 | SUCCESS | \ No newline at end of file diff --git a/TransactionProcessor.IntegrationTests/Reconciliation/ReconciliationFeature.feature.cs b/TransactionProcessor.IntegrationTests/Reconciliation/ReconciliationFeature.feature.cs new file mode 100644 index 00000000..c6e571ef --- /dev/null +++ b/TransactionProcessor.IntegrationTests/Reconciliation/ReconciliationFeature.feature.cs @@ -0,0 +1,453 @@ +// ------------------------------------------------------------------------------ +// +// This code was generated by SpecFlow (https://www.specflow.org/). +// SpecFlow Version:3.3.0.0 +// SpecFlow Generator Version:3.1.0.0 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// ------------------------------------------------------------------------------ +#region Designer generated code +#pragma warning disable +namespace TransactionProcessor.IntegrationTests.Reconciliation +{ + using TechTalk.SpecFlow; + using System; + using System.Linq; + + + [System.CodeDom.Compiler.GeneratedCodeAttribute("TechTalk.SpecFlow", "3.3.0.0")] + [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [Xunit.TraitAttribute("Category", "base")] + [Xunit.TraitAttribute("Category", "shared")] + public partial class ReconciliationFeature : object, Xunit.IClassFixture, System.IDisposable + { + + private static TechTalk.SpecFlow.ITestRunner testRunner; + + private string[] _featureTags = new string[] { + "base", + "shared"}; + + private Xunit.Abstractions.ITestOutputHelper _testOutputHelper; + +#line 1 "ReconciliationFeature.feature" +#line hidden + + public ReconciliationFeature(ReconciliationFeature.FixtureData fixtureData, TransactionProcessor_IntegrationTests_XUnitAssemblyFixture assemblyFixture, Xunit.Abstractions.ITestOutputHelper testOutputHelper) + { + this._testOutputHelper = testOutputHelper; + this.TestInitialize(); + } + + public static void FeatureSetup() + { + testRunner = TechTalk.SpecFlow.TestRunnerManager.GetTestRunner(); + TechTalk.SpecFlow.FeatureInfo featureInfo = new TechTalk.SpecFlow.FeatureInfo(new System.Globalization.CultureInfo("en-US"), "Reconciliation", null, ProgrammingLanguage.CSharp, new string[] { + "base", + "shared"}); + testRunner.OnFeatureStart(featureInfo); + } + + public static void FeatureTearDown() + { + testRunner.OnFeatureEnd(); + testRunner = null; + } + + public virtual void TestInitialize() + { + } + + public virtual void TestTearDown() + { + testRunner.OnScenarioEnd(); + } + + public virtual void ScenarioInitialize(TechTalk.SpecFlow.ScenarioInfo scenarioInfo) + { + testRunner.OnScenarioInitialize(scenarioInfo); + testRunner.ScenarioContext.ScenarioContainer.RegisterInstanceAs(_testOutputHelper); + } + + public virtual void ScenarioStart() + { + testRunner.OnScenarioStart(); + } + + public virtual void ScenarioCleanup() + { + testRunner.CollectScenarioErrors(); + } + + public virtual void FeatureBackground() + { +#line 4 +#line hidden + TechTalk.SpecFlow.Table table22 = new TechTalk.SpecFlow.Table(new string[] { + "ResourceName", + "DisplayName", + "Secret", + "Scopes", + "UserClaims"}); + table22.AddRow(new string[] { + "estateManagement", + "Estate Managememt REST", + "Secret1", + "estateManagement", + "MerchantId, EstateId, role"}); + table22.AddRow(new string[] { + "transactionProcessor", + "Transaction Processor REST", + "Secret1", + "transactionProcessor", + ""}); +#line 6 + testRunner.Given("the following api resources exist", ((string)(null)), table22, "Given "); +#line hidden + TechTalk.SpecFlow.Table table23 = new TechTalk.SpecFlow.Table(new string[] { + "ClientId", + "ClientName", + "Secret", + "AllowedScopes", + "AllowedGrantTypes"}); + table23.AddRow(new string[] { + "serviceClient", + "Service Client", + "Secret1", + "estateManagement,transactionProcessor", + "client_credentials"}); +#line 11 + testRunner.Given("the following clients exist", ((string)(null)), table23, "Given "); +#line hidden + TechTalk.SpecFlow.Table table24 = new TechTalk.SpecFlow.Table(new string[] { + "ClientId"}); + table24.AddRow(new string[] { + "serviceClient"}); +#line 15 + testRunner.Given("I have a token to access the estate management and transaction processor resource" + + "s", ((string)(null)), table24, "Given "); +#line hidden + TechTalk.SpecFlow.Table table25 = new TechTalk.SpecFlow.Table(new string[] { + "EstateName"}); + table25.AddRow(new string[] { + "Test Estate 1"}); + table25.AddRow(new string[] { + "Test Estate 2"}); +#line 19 + testRunner.Given("I have created the following estates", ((string)(null)), table25, "Given "); +#line hidden + TechTalk.SpecFlow.Table table26 = new TechTalk.SpecFlow.Table(new string[] { + "EstateName", + "OperatorName", + "RequireCustomMerchantNumber", + "RequireCustomTerminalNumber"}); + table26.AddRow(new string[] { + "Test Estate 1", + "Safaricom", + "True", + "True"}); + table26.AddRow(new string[] { + "Test Estate 2", + "Safaricom", + "True", + "True"}); +#line 24 + testRunner.Given("I have created the following operators", ((string)(null)), table26, "Given "); +#line hidden + TechTalk.SpecFlow.Table table27 = new TechTalk.SpecFlow.Table(new string[] { + "EstateName", + "OperatorName", + "ContractDescription"}); + table27.AddRow(new string[] { + "Test Estate 1", + "Safaricom", + "Safaricom Contract"}); + table27.AddRow(new string[] { + "Test Estate 2", + "Safaricom", + "Safaricom Contract"}); +#line 29 + testRunner.Given("I create a contract with the following values", ((string)(null)), table27, "Given "); +#line hidden + TechTalk.SpecFlow.Table table28 = new TechTalk.SpecFlow.Table(new string[] { + "EstateName", + "OperatorName", + "ContractDescription", + "ProductName", + "DisplayText", + "Value"}); + table28.AddRow(new string[] { + "Test Estate 1", + "Safaricom", + "Safaricom Contract", + "Variable Topup", + "Custom", + ""}); + table28.AddRow(new string[] { + "Test Estate 2", + "Safaricom", + "Safaricom Contract", + "Variable Topup", + "Custom", + ""}); +#line 34 + testRunner.When("I create the following Products", ((string)(null)), table28, "When "); +#line hidden + TechTalk.SpecFlow.Table table29 = new TechTalk.SpecFlow.Table(new string[] { + "EstateName", + "OperatorName", + "ContractDescription", + "ProductName", + "CalculationType", + "FeeDescription", + "Value"}); + table29.AddRow(new string[] { + "Test Estate 1", + "Safaricom", + "Safaricom Contract", + "Variable Topup", + "Fixed", + "Merchant Commission", + "2.50"}); + table29.AddRow(new string[] { + "Test Estate 2", + "Safaricom", + "Safaricom Contract", + "Variable Topup", + "Percentage", + "Merchant Commission", + "0.85"}); +#line 39 + testRunner.When("I add the following Transaction Fees", ((string)(null)), table29, "When "); +#line hidden + TechTalk.SpecFlow.Table table30 = new TechTalk.SpecFlow.Table(new string[] { + "MerchantName", + "AddressLine1", + "Town", + "Region", + "Country", + "ContactName", + "EmailAddress", + "EstateName"}); + table30.AddRow(new string[] { + "Test Merchant 1", + "Address Line 1", + "TestTown", + "Test Region", + "United Kingdom", + "Test Contact 1", + "testcontact1@merchant1.co.uk", + "Test Estate 1"}); + table30.AddRow(new string[] { + "Test Merchant 2", + "Address Line 1", + "TestTown", + "Test Region", + "United Kingdom", + "Test Contact 2", + "testcontact2@merchant2.co.uk", + "Test Estate 1"}); + table30.AddRow(new string[] { + "Test Merchant 3", + "Address Line 1", + "TestTown", + "Test Region", + "United Kingdom", + "Test Contact 3", + "testcontact3@merchant2.co.uk", + "Test Estate 2"}); +#line 44 + testRunner.Given("I create the following merchants", ((string)(null)), table30, "Given "); +#line hidden + TechTalk.SpecFlow.Table table31 = new TechTalk.SpecFlow.Table(new string[] { + "OperatorName", + "MerchantName", + "MerchantNumber", + "TerminalNumber", + "EstateName"}); + table31.AddRow(new string[] { + "Safaricom", + "Test Merchant 1", + "00000001", + "10000001", + "Test Estate 1"}); + table31.AddRow(new string[] { + "Safaricom", + "Test Merchant 2", + "00000002", + "10000002", + "Test Estate 1"}); + table31.AddRow(new string[] { + "Safaricom", + "Test Merchant 3", + "00000003", + "10000003", + "Test Estate 2"}); +#line 50 + testRunner.Given("I have assigned the following operator to the merchants", ((string)(null)), table31, "Given "); +#line hidden + TechTalk.SpecFlow.Table table32 = new TechTalk.SpecFlow.Table(new string[] { + "DeviceIdentifier", + "MerchantName", + "EstateName"}); + table32.AddRow(new string[] { + "123456780", + "Test Merchant 1", + "Test Estate 1"}); + table32.AddRow(new string[] { + "123456781", + "Test Merchant 2", + "Test Estate 1"}); + table32.AddRow(new string[] { + "123456782", + "Test Merchant 3", + "Test Estate 2"}); +#line 56 + testRunner.Given("I have assigned the following devices to the merchants", ((string)(null)), table32, "Given "); +#line hidden + TechTalk.SpecFlow.Table table33 = new TechTalk.SpecFlow.Table(new string[] { + "Reference", + "Amount", + "DateTime", + "MerchantName", + "EstateName"}); + table33.AddRow(new string[] { + "Deposit1", + "200.00", + "Today", + "Test Merchant 1", + "Test Estate 1"}); + table33.AddRow(new string[] { + "Deposit1", + "100.00", + "Today", + "Test Merchant 2", + "Test Estate 1"}); + table33.AddRow(new string[] { + "Deposit1", + "100.00", + "Today", + "Test Merchant 3", + "Test Estate 2"}); +#line 62 + testRunner.Given("I make the following manual merchant deposits", ((string)(null)), table33, "Given "); +#line hidden + } + + void System.IDisposable.Dispose() + { + this.TestTearDown(); + } + + [Xunit.SkippableFactAttribute(DisplayName="Sale Transactions")] + [Xunit.TraitAttribute("FeatureTitle", "Reconciliation")] + [Xunit.TraitAttribute("Description", "Sale Transactions")] + [Xunit.TraitAttribute("Category", "PRTest")] + public virtual void SaleTransactions() + { + string[] tagsOfScenario = new string[] { + "PRTest"}; + System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Sale Transactions", null, tagsOfScenario, argumentsOfScenario); +#line 69 +this.ScenarioInitialize(scenarioInfo); +#line hidden + bool isScenarioIgnored = default(bool); + bool isFeatureIgnored = default(bool); + if ((tagsOfScenario != null)) + { + isScenarioIgnored = tagsOfScenario.Where(__entry => __entry != null).Where(__entry => String.Equals(__entry, "ignore", StringComparison.CurrentCultureIgnoreCase)).Any(); + } + if ((this._featureTags != null)) + { + isFeatureIgnored = this._featureTags.Where(__entry => __entry != null).Where(__entry => String.Equals(__entry, "ignore", StringComparison.CurrentCultureIgnoreCase)).Any(); + } + if ((isScenarioIgnored || isFeatureIgnored)) + { + testRunner.SkipScenario(); + } + else + { + this.ScenarioStart(); +#line 4 +this.FeatureBackground(); +#line hidden + TechTalk.SpecFlow.Table table34 = new TechTalk.SpecFlow.Table(new string[] { + "DateTime", + "MerchantName", + "DeviceIdentifier", + "EstateName", + "TransactionCount", + "TransactionValue"}); + table34.AddRow(new string[] { + "Today", + "Test Merchant 1", + "123456780", + "Test Estate 1", + "1", + "100.00"}); + table34.AddRow(new string[] { + "Today", + "Test Merchant 2", + "123456781", + "Test Estate 1", + "2", + "200.00"}); + table34.AddRow(new string[] { + "Today", + "Test Merchant 3", + "123456782", + "Test Estate 2", + "3", + "300.00"}); +#line 71 + testRunner.When("I perform the following reconciliations", ((string)(null)), table34, "When "); +#line hidden + TechTalk.SpecFlow.Table table35 = new TechTalk.SpecFlow.Table(new string[] { + "EstateName", + "MerchantName", + "ResponseCode", + "ResponseMessage"}); + table35.AddRow(new string[] { + "Test Estate 1", + "Test Merchant 1", + "0000", + "SUCCESS"}); + table35.AddRow(new string[] { + "Test Estate 1", + "Test Merchant 2", + "0000", + "SUCCESS"}); + table35.AddRow(new string[] { + "Test Estate 2", + "Test Merchant 3", + "0000", + "SUCCESS"}); +#line 77 + testRunner.Then("reconciliation response should contain the following information", ((string)(null)), table35, "Then "); +#line hidden + } + this.ScenarioCleanup(); + } + + [System.CodeDom.Compiler.GeneratedCodeAttribute("TechTalk.SpecFlow", "3.3.0.0")] + [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public class FixtureData : System.IDisposable + { + + public FixtureData() + { + ReconciliationFeature.FeatureSetup(); + } + + void System.IDisposable.Dispose() + { + ReconciliationFeature.FeatureTearDown(); + } + } + } +} +#pragma warning restore +#endregion diff --git a/TransactionProcessor.IntegrationTests/SaleTransaction/SaleTransactionFeature.feature.cs b/TransactionProcessor.IntegrationTests/SaleTransaction/SaleTransactionFeature.feature.cs index fef900df..23df45bb 100644 --- a/TransactionProcessor.IntegrationTests/SaleTransaction/SaleTransactionFeature.feature.cs +++ b/TransactionProcessor.IntegrationTests/SaleTransaction/SaleTransactionFeature.feature.cs @@ -85,107 +85,107 @@ public virtual void FeatureBackground() { #line 4 #line hidden - TechTalk.SpecFlow.Table table22 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table36 = new TechTalk.SpecFlow.Table(new string[] { "ResourceName", "DisplayName", "Secret", "Scopes", "UserClaims"}); - table22.AddRow(new string[] { + table36.AddRow(new string[] { "estateManagement", "Estate Managememt REST", "Secret1", "estateManagement", "MerchantId, EstateId, role"}); - table22.AddRow(new string[] { + table36.AddRow(new string[] { "transactionProcessor", "Transaction Processor REST", "Secret1", "transactionProcessor", ""}); #line 6 - testRunner.Given("the following api resources exist", ((string)(null)), table22, "Given "); + testRunner.Given("the following api resources exist", ((string)(null)), table36, "Given "); #line hidden - TechTalk.SpecFlow.Table table23 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table37 = new TechTalk.SpecFlow.Table(new string[] { "ClientId", "ClientName", "Secret", "AllowedScopes", "AllowedGrantTypes"}); - table23.AddRow(new string[] { + table37.AddRow(new string[] { "serviceClient", "Service Client", "Secret1", "estateManagement,transactionProcessor", "client_credentials"}); #line 11 - testRunner.Given("the following clients exist", ((string)(null)), table23, "Given "); + testRunner.Given("the following clients exist", ((string)(null)), table37, "Given "); #line hidden - TechTalk.SpecFlow.Table table24 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table38 = new TechTalk.SpecFlow.Table(new string[] { "ClientId"}); - table24.AddRow(new string[] { + table38.AddRow(new string[] { "serviceClient"}); #line 15 testRunner.Given("I have a token to access the estate management and transaction processor resource" + - "s", ((string)(null)), table24, "Given "); + "s", ((string)(null)), table38, "Given "); #line hidden - TechTalk.SpecFlow.Table table25 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table39 = new TechTalk.SpecFlow.Table(new string[] { "EstateName"}); - table25.AddRow(new string[] { + table39.AddRow(new string[] { "Test Estate 1"}); - table25.AddRow(new string[] { + table39.AddRow(new string[] { "Test Estate 2"}); #line 19 - testRunner.Given("I have created the following estates", ((string)(null)), table25, "Given "); + testRunner.Given("I have created the following estates", ((string)(null)), table39, "Given "); #line hidden - TechTalk.SpecFlow.Table table26 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table40 = new TechTalk.SpecFlow.Table(new string[] { "EstateName", "OperatorName", "RequireCustomMerchantNumber", "RequireCustomTerminalNumber"}); - table26.AddRow(new string[] { + table40.AddRow(new string[] { "Test Estate 1", "Safaricom", "True", "True"}); - table26.AddRow(new string[] { + table40.AddRow(new string[] { "Test Estate 2", "Safaricom", "True", "True"}); #line 24 - testRunner.Given("I have created the following operators", ((string)(null)), table26, "Given "); + testRunner.Given("I have created the following operators", ((string)(null)), table40, "Given "); #line hidden - TechTalk.SpecFlow.Table table27 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table41 = new TechTalk.SpecFlow.Table(new string[] { "EstateName", "OperatorName", "ContractDescription"}); - table27.AddRow(new string[] { + table41.AddRow(new string[] { "Test Estate 1", "Safaricom", "Safaricom Contract"}); - table27.AddRow(new string[] { + table41.AddRow(new string[] { "Test Estate 2", "Safaricom", "Safaricom Contract"}); #line 29 - testRunner.Given("I create a contract with the following values", ((string)(null)), table27, "Given "); + testRunner.Given("I create a contract with the following values", ((string)(null)), table41, "Given "); #line hidden - TechTalk.SpecFlow.Table table28 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table42 = new TechTalk.SpecFlow.Table(new string[] { "EstateName", "OperatorName", "ContractDescription", "ProductName", "DisplayText", "Value"}); - table28.AddRow(new string[] { + table42.AddRow(new string[] { "Test Estate 1", "Safaricom", "Safaricom Contract", "Variable Topup", "Custom", ""}); - table28.AddRow(new string[] { + table42.AddRow(new string[] { "Test Estate 2", "Safaricom", "Safaricom Contract", @@ -193,9 +193,9 @@ public virtual void FeatureBackground() "Custom", ""}); #line 34 - testRunner.When("I create the following Products", ((string)(null)), table28, "When "); + testRunner.When("I create the following Products", ((string)(null)), table42, "When "); #line hidden - TechTalk.SpecFlow.Table table29 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table43 = new TechTalk.SpecFlow.Table(new string[] { "EstateName", "OperatorName", "ContractDescription", @@ -203,7 +203,7 @@ public virtual void FeatureBackground() "CalculationType", "FeeDescription", "Value"}); - table29.AddRow(new string[] { + table43.AddRow(new string[] { "Test Estate 1", "Safaricom", "Safaricom Contract", @@ -211,7 +211,7 @@ public virtual void FeatureBackground() "Fixed", "Merchant Commission", "2.50"}); - table29.AddRow(new string[] { + table43.AddRow(new string[] { "Test Estate 2", "Safaricom", "Safaricom Contract", @@ -220,9 +220,9 @@ public virtual void FeatureBackground() "Merchant Commission", "0.85"}); #line 39 - testRunner.When("I add the following Transaction Fees", ((string)(null)), table29, "When "); + testRunner.When("I add the following Transaction Fees", ((string)(null)), table43, "When "); #line hidden - TechTalk.SpecFlow.Table table30 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table44 = new TechTalk.SpecFlow.Table(new string[] { "MerchantName", "AddressLine1", "Town", @@ -231,7 +231,7 @@ public virtual void FeatureBackground() "ContactName", "EmailAddress", "EstateName"}); - table30.AddRow(new string[] { + table44.AddRow(new string[] { "Test Merchant 1", "Address Line 1", "TestTown", @@ -240,7 +240,7 @@ public virtual void FeatureBackground() "Test Contact 1", "testcontact1@merchant1.co.uk", "Test Estate 1"}); - table30.AddRow(new string[] { + table44.AddRow(new string[] { "Test Merchant 2", "Address Line 1", "TestTown", @@ -249,7 +249,7 @@ public virtual void FeatureBackground() "Test Contact 2", "testcontact2@merchant2.co.uk", "Test Estate 1"}); - table30.AddRow(new string[] { + table44.AddRow(new string[] { "Test Merchant 3", "Address Line 1", "TestTown", @@ -259,80 +259,80 @@ public virtual void FeatureBackground() "testcontact3@merchant2.co.uk", "Test Estate 2"}); #line 44 - testRunner.Given("I create the following merchants", ((string)(null)), table30, "Given "); + testRunner.Given("I create the following merchants", ((string)(null)), table44, "Given "); #line hidden - TechTalk.SpecFlow.Table table31 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table45 = new TechTalk.SpecFlow.Table(new string[] { "OperatorName", "MerchantName", "MerchantNumber", "TerminalNumber", "EstateName"}); - table31.AddRow(new string[] { + table45.AddRow(new string[] { "Safaricom", "Test Merchant 1", "00000001", "10000001", "Test Estate 1"}); - table31.AddRow(new string[] { + table45.AddRow(new string[] { "Safaricom", "Test Merchant 2", "00000002", "10000002", "Test Estate 1"}); - table31.AddRow(new string[] { + table45.AddRow(new string[] { "Safaricom", "Test Merchant 3", "00000003", "10000003", "Test Estate 2"}); #line 50 - testRunner.Given("I have assigned the following operator to the merchants", ((string)(null)), table31, "Given "); + testRunner.Given("I have assigned the following operator to the merchants", ((string)(null)), table45, "Given "); #line hidden - TechTalk.SpecFlow.Table table32 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table46 = new TechTalk.SpecFlow.Table(new string[] { "DeviceIdentifier", "MerchantName", "EstateName"}); - table32.AddRow(new string[] { + table46.AddRow(new string[] { "123456780", "Test Merchant 1", "Test Estate 1"}); - table32.AddRow(new string[] { + table46.AddRow(new string[] { "123456781", "Test Merchant 2", "Test Estate 1"}); - table32.AddRow(new string[] { + table46.AddRow(new string[] { "123456782", "Test Merchant 3", "Test Estate 2"}); #line 56 - testRunner.Given("I have assigned the following devices to the merchants", ((string)(null)), table32, "Given "); + testRunner.Given("I have assigned the following devices to the merchants", ((string)(null)), table46, "Given "); #line hidden - TechTalk.SpecFlow.Table table33 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table47 = new TechTalk.SpecFlow.Table(new string[] { "Reference", "Amount", "DateTime", "MerchantName", "EstateName"}); - table33.AddRow(new string[] { + table47.AddRow(new string[] { "Deposit1", "200.00", "Today", "Test Merchant 1", "Test Estate 1"}); - table33.AddRow(new string[] { + table47.AddRow(new string[] { "Deposit1", "100.00", "Today", "Test Merchant 2", "Test Estate 1"}); - table33.AddRow(new string[] { + table47.AddRow(new string[] { "Deposit1", "100.00", "Today", "Test Merchant 3", "Test Estate 2"}); #line 62 - testRunner.Given("I make the following manual merchant deposits", ((string)(null)), table33, "Given "); + testRunner.Given("I make the following manual merchant deposits", ((string)(null)), table47, "Given "); #line hidden } @@ -374,7 +374,7 @@ public virtual void SaleTransactions() #line 4 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table34 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table48 = new TechTalk.SpecFlow.Table(new string[] { "DateTime", "TransactionNumber", "TransactionType", @@ -387,7 +387,7 @@ public virtual void SaleTransactions() "CustomerEmailAddress", "ContractDescription", "ProductName"}); - table34.AddRow(new string[] { + table48.AddRow(new string[] { "Today", "1", "Sale", @@ -400,7 +400,7 @@ public virtual void SaleTransactions() "", "Safaricom Contract", "Variable Topup"}); - table34.AddRow(new string[] { + table48.AddRow(new string[] { "Today", "2", "Sale", @@ -413,7 +413,7 @@ public virtual void SaleTransactions() "", "Safaricom Contract", "Variable Topup"}); - table34.AddRow(new string[] { + table48.AddRow(new string[] { "Today", "3", "Sale", @@ -426,7 +426,7 @@ public virtual void SaleTransactions() "", "Safaricom Contract", "Variable Topup"}); - table34.AddRow(new string[] { + table48.AddRow(new string[] { "Today", "4", "Sale", @@ -440,40 +440,40 @@ public virtual void SaleTransactions() "Safaricom Contract", "Variable Topup"}); #line 71 - testRunner.When("I perform the following transactions", ((string)(null)), table34, "When "); + testRunner.When("I perform the following transactions", ((string)(null)), table48, "When "); #line hidden - TechTalk.SpecFlow.Table table35 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table49 = new TechTalk.SpecFlow.Table(new string[] { "EstateName", "MerchantName", "TransactionNumber", "ResponseCode", "ResponseMessage"}); - table35.AddRow(new string[] { + table49.AddRow(new string[] { "Test Estate 1", "Test Merchant 1", "1", "0000", "SUCCESS"}); - table35.AddRow(new string[] { + table49.AddRow(new string[] { "Test Estate 1", "Test Merchant 2", "2", "0000", "SUCCESS"}); - table35.AddRow(new string[] { + table49.AddRow(new string[] { "Test Estate 2", "Test Merchant 3", "3", "0000", "SUCCESS"}); - table35.AddRow(new string[] { + table49.AddRow(new string[] { "Test Estate 1", "Test Merchant 1", "4", "0000", "SUCCESS"}); #line 78 - testRunner.Then("transaction response should contain the following information", ((string)(null)), table35, "Then "); + testRunner.Then("transaction response should contain the following information", ((string)(null)), table49, "Then "); #line hidden } this.ScenarioCleanup(); @@ -512,7 +512,7 @@ public virtual void SaleTransactionWithInvalidDevice() #line 4 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table36 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table50 = new TechTalk.SpecFlow.Table(new string[] { "DateTime", "TransactionNumber", "TransactionType", @@ -525,7 +525,7 @@ public virtual void SaleTransactionWithInvalidDevice() "CustomerEmailAddress", "ContractDescription", "ProductName"}); - table36.AddRow(new string[] { + table50.AddRow(new string[] { "Today", "1", "Sale", @@ -539,22 +539,22 @@ public virtual void SaleTransactionWithInvalidDevice() "Safaricom Contract", "Variable Topup"}); #line 88 - testRunner.When("I perform the following transactions", ((string)(null)), table36, "When "); + testRunner.When("I perform the following transactions", ((string)(null)), table50, "When "); #line hidden - TechTalk.SpecFlow.Table table37 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table51 = new TechTalk.SpecFlow.Table(new string[] { "EstateName", "MerchantName", "TransactionNumber", "ResponseCode", "ResponseMessage"}); - table37.AddRow(new string[] { + table51.AddRow(new string[] { "Test Estate 1", "Test Merchant 1", "1", "1000", "Device Identifier 123456781 not valid for Merchant Test Merchant 1"}); #line 92 - testRunner.Then("transaction response should contain the following information", ((string)(null)), table37, "Then "); + testRunner.Then("transaction response should contain the following information", ((string)(null)), table51, "Then "); #line hidden } this.ScenarioCleanup(); @@ -591,7 +591,7 @@ public virtual void SaleTransactionWithInvalidEstate() #line 4 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table38 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table52 = new TechTalk.SpecFlow.Table(new string[] { "DateTime", "TransactionNumber", "TransactionType", @@ -604,7 +604,7 @@ public virtual void SaleTransactionWithInvalidEstate() "CustomerEmailAddress", "ContractDescription", "ProductName"}); - table38.AddRow(new string[] { + table52.AddRow(new string[] { "Today", "1", "Sale", @@ -618,22 +618,22 @@ public virtual void SaleTransactionWithInvalidEstate() "Safaricom Contract", "Variable Topup"}); #line 98 - testRunner.When("I perform the following transactions", ((string)(null)), table38, "When "); + testRunner.When("I perform the following transactions", ((string)(null)), table52, "When "); #line hidden - TechTalk.SpecFlow.Table table39 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table53 = new TechTalk.SpecFlow.Table(new string[] { "EstateName", "MerchantName", "TransactionNumber", "ResponseCode", "ResponseMessage"}); - table39.AddRow(new string[] { + table53.AddRow(new string[] { "InvalidEstate", "Test Merchant 1", "1", "1001", "Estate Id [79902550-64df-4491-b0c1-4e78943928a3] is not a valid estate"}); #line 102 - testRunner.Then("transaction response should contain the following information", ((string)(null)), table39, "Then "); + testRunner.Then("transaction response should contain the following information", ((string)(null)), table53, "Then "); #line hidden } this.ScenarioCleanup(); @@ -670,7 +670,7 @@ public virtual void SaleTransactionWithInvalidMerchant() #line 4 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table40 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table54 = new TechTalk.SpecFlow.Table(new string[] { "DateTime", "TransactionNumber", "TransactionType", @@ -683,7 +683,7 @@ public virtual void SaleTransactionWithInvalidMerchant() "CustomerEmailAddress", "ContractDescription", "ProductName"}); - table40.AddRow(new string[] { + table54.AddRow(new string[] { "Today", "1", "Sale", @@ -697,15 +697,15 @@ public virtual void SaleTransactionWithInvalidMerchant() "Safaricom Contract", "Variable Topup"}); #line 108 - testRunner.When("I perform the following transactions", ((string)(null)), table40, "When "); + testRunner.When("I perform the following transactions", ((string)(null)), table54, "When "); #line hidden - TechTalk.SpecFlow.Table table41 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table55 = new TechTalk.SpecFlow.Table(new string[] { "EstateName", "MerchantName", "TransactionNumber", "ResponseCode", "ResponseMessage"}); - table41.AddRow(new string[] { + table55.AddRow(new string[] { "Test Estate 1", "InvalidMerchant", "1", @@ -713,7 +713,7 @@ public virtual void SaleTransactionWithInvalidMerchant() "Merchant Id [d59320fa-4c3e-4900-a999-483f6a10c69a] is not a valid merchant for es" + "tate [Test Estate 1]"}); #line 112 - testRunner.Then("transaction response should contain the following information", ((string)(null)), table41, "Then "); + testRunner.Then("transaction response should contain the following information", ((string)(null)), table55, "Then "); #line hidden } this.ScenarioCleanup(); @@ -752,7 +752,7 @@ public virtual void SaleTransactionWithNotEnoughCreditAvailable() #line 4 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table42 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table56 = new TechTalk.SpecFlow.Table(new string[] { "DateTime", "TransactionNumber", "TransactionType", @@ -765,7 +765,7 @@ public virtual void SaleTransactionWithNotEnoughCreditAvailable() "CustomerEmailAddress", "ContractDescription", "ProductName"}); - table42.AddRow(new string[] { + table56.AddRow(new string[] { "Today", "1", "Sale", @@ -779,15 +779,15 @@ public virtual void SaleTransactionWithNotEnoughCreditAvailable() "Safaricom Contract", "Variable Topup"}); #line 119 - testRunner.When("I perform the following transactions", ((string)(null)), table42, "When "); + testRunner.When("I perform the following transactions", ((string)(null)), table56, "When "); #line hidden - TechTalk.SpecFlow.Table table43 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table57 = new TechTalk.SpecFlow.Table(new string[] { "EstateName", "MerchantName", "TransactionNumber", "ResponseCode", "ResponseMessage"}); - table43.AddRow(new string[] { + table57.AddRow(new string[] { "Test Estate 1", "Test Merchant 1", "1", @@ -795,7 +795,7 @@ public virtual void SaleTransactionWithNotEnoughCreditAvailable() "Merchant [Test Merchant 1] does not have enough credit available [200.0] to perfo" + "rm transaction amount [300.00]"}); #line 124 - testRunner.Then("transaction response should contain the following information", ((string)(null)), table43, "Then "); + testRunner.Then("transaction response should contain the following information", ((string)(null)), table57, "Then "); #line hidden } this.ScenarioCleanup(); diff --git a/TransactionProcessor.IntegrationTests/Shared/SharedSteps.cs b/TransactionProcessor.IntegrationTests/Shared/SharedSteps.cs index 4b569218..c48ec39b 100644 --- a/TransactionProcessor.IntegrationTests/Shared/SharedSteps.cs +++ b/TransactionProcessor.IntegrationTests/Shared/SharedSteps.cs @@ -488,6 +488,34 @@ await this.TestingContext.DockerHelper.TransactionProcessorClient.PerformTransac return responseSerialisedMessage; } + private async Task PerformReconciliationTransaction(Guid estateId, Guid merchantId, DateTime transactionDateTime, String deviceIdentifier, Int32 transactionCount, Decimal transactionValue, CancellationToken cancellationToken) + { + ReconciliationRequest reconciliationRequest = new ReconciliationRequest + { + MerchantId = merchantId, + EstateId = estateId, + TransactionDateTime = transactionDateTime, + DeviceIdentifier = deviceIdentifier, + TransactionValue = transactionValue, + TransactionCount = transactionCount, + }; + + SerialisedMessage serialisedMessage = new SerialisedMessage(); + serialisedMessage.Metadata.Add(MetadataContants.KeyNameEstateId, estateId.ToString()); + serialisedMessage.Metadata.Add(MetadataContants.KeyNameMerchantId, merchantId.ToString()); + serialisedMessage.SerialisedData = JsonConvert.SerializeObject(reconciliationRequest, new JsonSerializerSettings + { + TypeNameHandling = TypeNameHandling.All + }); + + SerialisedMessage responseSerialisedMessage = + await this.TestingContext.DockerHelper.TransactionProcessorClient.PerformTransaction(this.TestingContext.AccessToken, + serialisedMessage, + cancellationToken); + + return responseSerialisedMessage; + } + [Then(@"transaction response should contain the following information")] public void ThenTransactionResponseShouldContainTheFollowingInformation(Table table) { @@ -530,6 +558,16 @@ private void ValidateTransactionResponse(SaleTransactionResponse saleTransaction saleTransactionResponse.ResponseMessage.ShouldBe(expectedResponseMessage); } + private void ValidateTransactionResponse(ReconciliationResponse reconciliationResponse, + TableRow tableRow) + { + String expectedResponseCode = SpecflowTableHelper.GetStringRowValue(tableRow, "ResponseCode"); + String expectedResponseMessage = SpecflowTableHelper.GetStringRowValue(tableRow, "ResponseMessage"); + + reconciliationResponse.ResponseCode.ShouldBe(expectedResponseCode); + reconciliationResponse.ResponseMessage.ShouldBe(expectedResponseMessage); + } + [Given(@"the following api resources exist")] public async Task GivenTheFollowingApiResourcesExist(Table table) { @@ -706,5 +744,55 @@ public async Task GivenIMakeTheFollowingManualMerchantDeposits(Table table) } } + [When(@"I perform the following reconciliations")] + public async Task WhenIPerformTheFollowingReconciliations(Table table) + { + foreach (TableRow tableRow in table.Rows) + { + String merchantName = SpecflowTableHelper.GetStringRowValue(tableRow, "MerchantName"); + String dateString = SpecflowTableHelper.GetStringRowValue(tableRow, "DateTime"); + DateTime transactionDateTime = SpecflowTableHelper.GetDateForDateString(dateString, DateTime.Today); + String deviceIdentifier = SpecflowTableHelper.GetStringRowValue(tableRow, "DeviceIdentifier"); + Int32 transactionCount = SpecflowTableHelper.GetIntValue(tableRow, "TransactionCount"); + Decimal transactionValue = SpecflowTableHelper.GetDecimalValue(tableRow, "TransactionValue"); + + EstateDetails estateDetails = this.TestingContext.GetEstateDetails(tableRow); + + // Lookup the merchant id + Guid merchantId = estateDetails.GetMerchantId(merchantName); + SerialisedMessage reconciliationResponse = await this.PerformReconciliationTransaction(estateDetails.EstateId, + merchantId, + transactionDateTime, + deviceIdentifier, + transactionCount, + transactionValue, + CancellationToken.None); + + estateDetails.AddReconciliationResponse(merchantId, reconciliationResponse); + } + } + + [Then(@"reconciliation response should contain the following information")] + public void ThenReconciliationResponseShouldContainTheFollowingInformation(Table table) + { + foreach (TableRow tableRow in table.Rows) + { + // Get the merchant name + EstateDetails estateDetails = this.TestingContext.GetEstateDetails(tableRow); + + String merchantName = SpecflowTableHelper.GetStringRowValue(tableRow, "MerchantName"); + Guid merchantId = estateDetails.GetMerchantId(merchantName); + + SerialisedMessage serialisedMessage = estateDetails.GetReconciliationResponse(merchantId); + Object transactionResponse = JsonConvert.DeserializeObject(serialisedMessage.SerialisedData, + new JsonSerializerSettings + { + TypeNameHandling = TypeNameHandling.All + }); + this.ValidateTransactionResponse((dynamic)transactionResponse, tableRow); + } + } + + } } diff --git a/TransactionProcessor.IntegrationTests/TransactionProcessor.IntegrationTests.csproj b/TransactionProcessor.IntegrationTests/TransactionProcessor.IntegrationTests.csproj index 3a6b2982..c31d2010 100644 --- a/TransactionProcessor.IntegrationTests/TransactionProcessor.IntegrationTests.csproj +++ b/TransactionProcessor.IntegrationTests/TransactionProcessor.IntegrationTests.csproj @@ -43,6 +43,9 @@ True LogonTransaction.feature + + True + True @@ -59,6 +62,10 @@ SpecFlowSingleFileGenerator LogonTransaction.feature.cs + + SpecFlowSingleFileGenerator + ReconciliationFeature.feature.cs + SpecFlowSingleFileGenerator SaleTransactionFeature.feature.cs diff --git a/TransactionProcessor.Models/ProcessReconciliationResponse.cs b/TransactionProcessor.Models/ProcessReconciliationResponse.cs new file mode 100644 index 00000000..ac074ac5 --- /dev/null +++ b/TransactionProcessor.Models/ProcessReconciliationResponse.cs @@ -0,0 +1,45 @@ +namespace TransactionProcessor.Models +{ + using System; + using System.Diagnostics.CodeAnalysis; + + [ExcludeFromCodeCoverage] + public class ProcessReconciliationTransactionResponse + { + #region Properties + + /// + /// Gets or sets the response code. + /// + /// + /// The response code. + /// + public String ResponseCode { get; set; } + + /// + /// Gets or sets the response message. + /// + /// + /// The response message. + /// + public String ResponseMessage { get; set; } + + /// + /// Gets or sets the estate identifier. + /// + /// + /// The estate identifier. + /// + public Guid EstateId { get; set; } + + /// + /// Gets or sets the merchant identifier. + /// + /// + /// The merchant identifier. + /// + public Guid MerchantId { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/TransactionProcessor.Reconciliation.DomainEvents/OverallTotalsRecordedEvent.cs b/TransactionProcessor.Reconciliation.DomainEvents/OverallTotalsRecordedEvent.cs new file mode 100644 index 00000000..dbb4f20f --- /dev/null +++ b/TransactionProcessor.Reconciliation.DomainEvents/OverallTotalsRecordedEvent.cs @@ -0,0 +1,121 @@ +namespace TransactionProcessor.Reconciliation.DomainEvents +{ + using System; + using System.Diagnostics.CodeAnalysis; + using Newtonsoft.Json; + using Shared.DomainDrivenDesign.EventSourcing; + + /// + /// + /// + /// + [JsonObject] + public class OverallTotalsRecordedEvent : DomainEvent + { + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + [ExcludeFromCodeCoverage] + public OverallTotalsRecordedEvent() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The aggregate identifier. + /// The event identifier. + /// The estate identifier. + /// The merchant identifier. + /// The transaction count. + /// The transaction value. + private OverallTotalsRecordedEvent(Guid aggregateId, + Guid eventId, + Guid estateId, + Guid merchantId, + Int32 transactionCount, + Decimal transactionValue) : base(aggregateId, eventId) + { + this.TransactionId = aggregateId; + this.EstateId = estateId; + this.MerchantId = merchantId; + this.TransactionCount = transactionCount; + this.TransactionValue = transactionValue; + } + + #endregion + + #region Properties + + /// + /// Gets the estate identifier. + /// + /// + /// The estate identifier. + /// + [JsonProperty] + public Guid EstateId { get; private set; } + + /// + /// Gets the merchant identifier. + /// + /// + /// The merchant identifier. + /// + [JsonProperty] + public Guid MerchantId { get; private set; } + + /// + /// Gets the transaction count. + /// + /// + /// The transaction count. + /// + [JsonProperty] + public Int32 TransactionCount { get; private set; } + + /// + /// Gets the transaction value. + /// + /// + /// The transaction value. + /// + [JsonProperty] + public Decimal TransactionValue { 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 transaction count. + /// The transaction value. + /// + public static OverallTotalsRecordedEvent Create(Guid aggregateId, + Guid estateId, + Guid merchantId, + Int32 transactionCount, + Decimal transactionValue) + { + return new OverallTotalsRecordedEvent(aggregateId, Guid.NewGuid(), estateId, merchantId, transactionCount, transactionValue); + } + + #endregion + } +} \ No newline at end of file diff --git a/TransactionProcessor.Reconciliation.DomainEvents/ReconciliationHasBeenLocallyAuthorisedEvent.cs b/TransactionProcessor.Reconciliation.DomainEvents/ReconciliationHasBeenLocallyAuthorisedEvent.cs new file mode 100644 index 00000000..29b56062 --- /dev/null +++ b/TransactionProcessor.Reconciliation.DomainEvents/ReconciliationHasBeenLocallyAuthorisedEvent.cs @@ -0,0 +1,121 @@ +namespace TransactionProcessor.Transaction.DomainEvents +{ + using System; + using System.Diagnostics.CodeAnalysis; + using Newtonsoft.Json; + using Shared.DomainDrivenDesign.EventSourcing; + + /// + /// + /// + /// + [JsonObject] + public class ReconciliationHasBeenLocallyAuthorisedEvent : DomainEvent + { + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The aggregate identifier. + /// The event identifier. + /// The estate identifier. + /// The merchant identifier. + /// The response code. + /// The response message. + private ReconciliationHasBeenLocallyAuthorisedEvent(Guid aggregateId, + Guid eventId, + Guid estateId, + Guid merchantId, + String responseCode, + String responseMessage) : base(aggregateId, eventId) + { + this.TransactionId = aggregateId; + this.EstateId = estateId; + this.MerchantId = merchantId; + this.ResponseCode = responseCode; + this.ResponseMessage = responseMessage; + } + + /// + /// Initializes a new instance of the class. + /// + [ExcludeFromCodeCoverage] + public ReconciliationHasBeenLocallyAuthorisedEvent() + { + } + + #endregion + + #region Properties + + /// + /// Gets the estate identifier. + /// + /// + /// The estate identifier. + /// + [JsonProperty] + public Guid EstateId { get; private set; } + + /// + /// Gets the merchant identifier. + /// + /// + /// The merchant identifier. + /// + [JsonProperty] + public Guid MerchantId { get; private set; } + + /// + /// Gets the response code. + /// + /// + /// The response code. + /// + [JsonProperty] + public String ResponseCode { get; private set; } + + /// + /// Gets the response message. + /// + /// + /// The response message. + /// + [JsonProperty] + public String ResponseMessage { 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 response code. + /// The response message. + /// + public static ReconciliationHasBeenLocallyAuthorisedEvent Create(Guid aggregateId, + Guid estateId, + Guid merchantId, + String responseCode, + String responseMessage) + { + return new ReconciliationHasBeenLocallyAuthorisedEvent(aggregateId, Guid.NewGuid(), estateId, merchantId, responseCode, responseMessage); + } + + #endregion + } +} \ No newline at end of file diff --git a/TransactionProcessor.Reconciliation.DomainEvents/ReconciliationHasBeenLocallyDeclinedEvent.cs b/TransactionProcessor.Reconciliation.DomainEvents/ReconciliationHasBeenLocallyDeclinedEvent.cs new file mode 100644 index 00000000..23eed486 --- /dev/null +++ b/TransactionProcessor.Reconciliation.DomainEvents/ReconciliationHasBeenLocallyDeclinedEvent.cs @@ -0,0 +1,117 @@ +namespace TransactionProcessor.Transaction.DomainEvents +{ + using System; + using System.Diagnostics.CodeAnalysis; + using Newtonsoft.Json; + using Shared.DomainDrivenDesign.EventSourcing; + + [JsonObject] + public class ReconciliationHasBeenLocallyDeclinedEvent : DomainEvent + { + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The aggregate identifier. + /// The event identifier. + /// The estate identifier. + /// The merchant identifier. + /// The response code. + /// The response message. + private ReconciliationHasBeenLocallyDeclinedEvent(Guid aggregateId, + Guid eventId, + Guid estateId, + Guid merchantId, + String responseCode, + String responseMessage) : base(aggregateId, eventId) + { + this.TransactionId = aggregateId; + this.EstateId = estateId; + this.MerchantId = merchantId; + this.ResponseCode = responseCode; + this.ResponseMessage = responseMessage; + } + + /// + /// Initializes a new instance of the class. + /// + [ExcludeFromCodeCoverage] + public ReconciliationHasBeenLocallyDeclinedEvent() + { + } + + #endregion + + #region Properties + + /// + /// Gets the estate identifier. + /// + /// + /// The estate identifier. + /// + [JsonProperty] + public Guid EstateId { get; private set; } + + /// + /// Gets the merchant identifier. + /// + /// + /// The merchant identifier. + /// + [JsonProperty] + public Guid MerchantId { get; private set; } + + /// + /// Gets the response code. + /// + /// + /// The response code. + /// + [JsonProperty] + public String ResponseCode { get; private set; } + + /// + /// Gets the response message. + /// + /// + /// The response message. + /// + [JsonProperty] + public String ResponseMessage { 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 response code. + /// The response message. + /// + public static ReconciliationHasBeenLocallyDeclinedEvent Create(Guid aggregateId, + Guid estateId, + Guid merchantId, + String responseCode, + String responseMessage) + { + return new ReconciliationHasBeenLocallyDeclinedEvent(aggregateId, Guid.NewGuid(), estateId, merchantId, responseCode, responseMessage); + } + + #endregion + } +} \ No newline at end of file diff --git a/TransactionProcessor.Reconciliation.DomainEvents/ReconciliationHasCompletedEvent.cs b/TransactionProcessor.Reconciliation.DomainEvents/ReconciliationHasCompletedEvent.cs new file mode 100644 index 00000000..4d3f3b68 --- /dev/null +++ b/TransactionProcessor.Reconciliation.DomainEvents/ReconciliationHasCompletedEvent.cs @@ -0,0 +1,94 @@ +namespace TransactionProcessor.Reconciliation.DomainEvents +{ + using System; + using System.Diagnostics.CodeAnalysis; + using Newtonsoft.Json; + using Shared.DomainDrivenDesign.EventSourcing; + + /// + /// + /// + /// + [JsonObject] + public class ReconciliationHasCompletedEvent : DomainEvent + { + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + [ExcludeFromCodeCoverage] + public ReconciliationHasCompletedEvent() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The aggregate identifier. + /// The event identifier. + /// The estate identifier. + /// The merchant identifier. + private ReconciliationHasCompletedEvent(Guid aggregateId, + Guid eventId, + Guid estateId, + Guid merchantId) : base(aggregateId, eventId) + { + this.TransactionId = aggregateId; + this.EstateId = estateId; + this.MerchantId = merchantId; + } + + #endregion + + #region Properties + + /// + /// Gets the estate identifier. + /// + /// + /// The estate identifier. + /// + [JsonProperty] + public Guid EstateId { 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. + /// + public static ReconciliationHasCompletedEvent Create(Guid aggregateId, + Guid estateId, + Guid merchantId) + { + return new ReconciliationHasCompletedEvent(aggregateId, Guid.NewGuid(), estateId, merchantId); + } + + #endregion + + } +} \ No newline at end of file diff --git a/TransactionProcessor.Reconciliation.DomainEvents/ReconciliationHasStartedEvent.cs b/TransactionProcessor.Reconciliation.DomainEvents/ReconciliationHasStartedEvent.cs new file mode 100644 index 00000000..02cff8e7 --- /dev/null +++ b/TransactionProcessor.Reconciliation.DomainEvents/ReconciliationHasStartedEvent.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace TransactionProcessor.Reconciliation.DomainEvents +{ + using System.Diagnostics.CodeAnalysis; + using Newtonsoft.Json; + using Shared.DomainDrivenDesign.EventSourcing; + + /// + /// + /// + /// + [JsonObject] + public class ReconciliationHasStartedEvent : DomainEvent + { + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + [ExcludeFromCodeCoverage] + public ReconciliationHasStartedEvent() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The aggregate identifier. + /// The event identifier. + /// The estate identifier. + /// The merchant identifier. + /// The transaction date time. + private ReconciliationHasStartedEvent(Guid aggregateId, + Guid eventId, + Guid estateId, + Guid merchantId, + DateTime transactionDateTime) : base(aggregateId, eventId) + { + this.TransactionId = aggregateId; + this.EstateId = estateId; + this.MerchantId = merchantId; + this.TransactionDateTime = transactionDateTime; + } + + #endregion + + #region Properties + + /// + /// Gets the estate identifier. + /// + /// + /// The estate identifier. + /// + [JsonProperty] + public Guid EstateId { get; private set; } + + /// + /// Gets the merchant identifier. + /// + /// + /// The merchant identifier. + /// + [JsonProperty] + public Guid MerchantId { get; private set; } + + /// + /// Gets the transaction date time. + /// + /// + /// The transaction date time. + /// + [JsonProperty] + public DateTime TransactionDateTime { 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 transaction date time. + /// + public static ReconciliationHasStartedEvent Create(Guid aggregateId, + Guid estateId, + Guid merchantId, + DateTime transactionDateTime) + { + return new ReconciliationHasStartedEvent(aggregateId, Guid.NewGuid(), estateId, merchantId, transactionDateTime); + } + + #endregion + } +} diff --git a/TransactionProcessor.Reconciliation.DomainEvents/TransactionProcessor.Reconciliation.DomainEvents.csproj b/TransactionProcessor.Reconciliation.DomainEvents/TransactionProcessor.Reconciliation.DomainEvents.csproj new file mode 100644 index 00000000..a91298ed --- /dev/null +++ b/TransactionProcessor.Reconciliation.DomainEvents/TransactionProcessor.Reconciliation.DomainEvents.csproj @@ -0,0 +1,10 @@ + + + + netstandard2.1 + + + + + + diff --git a/TransactionProcessor.ReconciliationAggregate.Tests/DomainEventTests.cs b/TransactionProcessor.ReconciliationAggregate.Tests/DomainEventTests.cs new file mode 100644 index 00000000..405a933a --- /dev/null +++ b/TransactionProcessor.ReconciliationAggregate.Tests/DomainEventTests.cs @@ -0,0 +1,127 @@ +namespace TransactionProcessor.ReconciliationAggregate.Tests +{ + using System; + using Reconciliation.DomainEvents; + using Shouldly; + using Testing; + using Transaction.DomainEvents; + using Xunit; + + /// + /// + /// + public class DomainEventTests + { + #region Methods + + /// + /// Overalls the totals recorded event can be created is created. + /// + [Fact] + public void OverallTotalsRecordedEvent_CanBeCreated_IsCreated() + { + OverallTotalsRecordedEvent overallTotalsRecordedEvent = OverallTotalsRecordedEvent.Create(TestData.TransactionId, + TestData.EstateId, + TestData.MerchantId, + TestData.ReconciliationTransactionCount, + TestData.ReconciliationTransactionValue); + + overallTotalsRecordedEvent.ShouldNotBeNull(); + overallTotalsRecordedEvent.AggregateId.ShouldBe(TestData.TransactionId); + overallTotalsRecordedEvent.EventCreatedDateTime.ShouldNotBe(DateTime.MinValue); + overallTotalsRecordedEvent.EventId.ShouldNotBe(Guid.Empty); + overallTotalsRecordedEvent.TransactionId.ShouldBe(TestData.TransactionId); + overallTotalsRecordedEvent.EstateId.ShouldBe(TestData.EstateId); + overallTotalsRecordedEvent.MerchantId.ShouldBe(TestData.MerchantId); + overallTotalsRecordedEvent.TransactionCount.ShouldBe(TestData.ReconciliationTransactionCount); + overallTotalsRecordedEvent.TransactionValue.ShouldBe(TestData.ReconciliationTransactionValue); + } + + /// + /// Reconciliations the has been locally authorised event can be created is created. + /// + [Fact] + public void ReconciliationHasBeenLocallyAuthorisedEvent_CanBeCreated_IsCreated() + { + ReconciliationHasBeenLocallyAuthorisedEvent reconciliationHasBeenLocallyAuthorisedEvent = + ReconciliationHasBeenLocallyAuthorisedEvent.Create(TestData.TransactionId, + TestData.EstateId, + TestData.MerchantId, + TestData.ResponseCode, + TestData.ResponseMessage); + + reconciliationHasBeenLocallyAuthorisedEvent.ShouldNotBeNull(); + reconciliationHasBeenLocallyAuthorisedEvent.AggregateId.ShouldBe(TestData.TransactionId); + reconciliationHasBeenLocallyAuthorisedEvent.EventCreatedDateTime.ShouldNotBe(DateTime.MinValue); + reconciliationHasBeenLocallyAuthorisedEvent.EventId.ShouldNotBe(Guid.Empty); + reconciliationHasBeenLocallyAuthorisedEvent.TransactionId.ShouldBe(TestData.TransactionId); + reconciliationHasBeenLocallyAuthorisedEvent.EstateId.ShouldBe(TestData.EstateId); + reconciliationHasBeenLocallyAuthorisedEvent.MerchantId.ShouldBe(TestData.MerchantId); + reconciliationHasBeenLocallyAuthorisedEvent.ResponseCode.ShouldBe(TestData.ResponseCode); + reconciliationHasBeenLocallyAuthorisedEvent.ResponseMessage.ShouldBe(TestData.ResponseMessage); + } + + /// + /// Reconciliations the has completed event can be created is created. + /// + [Fact] + public void ReconciliationHasCompletedEvent_CanBeCreated_IsCreated() + { + ReconciliationHasCompletedEvent reconciliationHasCompletedEvent = + ReconciliationHasCompletedEvent.Create(TestData.TransactionId, TestData.EstateId, TestData.MerchantId); + + reconciliationHasCompletedEvent.ShouldNotBeNull(); + reconciliationHasCompletedEvent.AggregateId.ShouldBe(TestData.TransactionId); + reconciliationHasCompletedEvent.EventCreatedDateTime.ShouldNotBe(DateTime.MinValue); + reconciliationHasCompletedEvent.EventId.ShouldNotBe(Guid.Empty); + reconciliationHasCompletedEvent.TransactionId.ShouldBe(TestData.TransactionId); + reconciliationHasCompletedEvent.EstateId.ShouldBe(TestData.EstateId); + reconciliationHasCompletedEvent.MerchantId.ShouldBe(TestData.MerchantId); + } + + /// + /// Reconciliations the has started event can be created is created. + /// + [Fact] + public void ReconciliationHasStartedEvent_CanBeCreated_IsCreated() + { + ReconciliationHasStartedEvent reconciliationHasStartedEvent = ReconciliationHasStartedEvent.Create(TestData.TransactionId, + TestData.EstateId, + TestData.MerchantId, + TestData.TransactionDateTime); + reconciliationHasStartedEvent.ShouldNotBeNull(); + reconciliationHasStartedEvent.AggregateId.ShouldBe(TestData.TransactionId); + reconciliationHasStartedEvent.EventCreatedDateTime.ShouldNotBe(DateTime.MinValue); + reconciliationHasStartedEvent.EventId.ShouldNotBe(Guid.Empty); + reconciliationHasStartedEvent.TransactionId.ShouldBe(TestData.TransactionId); + reconciliationHasStartedEvent.EstateId.ShouldBe(TestData.EstateId); + reconciliationHasStartedEvent.MerchantId.ShouldBe(TestData.MerchantId); + reconciliationHasStartedEvent.TransactionDateTime.ShouldBe(TestData.TransactionDateTime); + } + + /// + /// Transactions the has been locally declined event can be created is created. + /// + [Fact] + public void TransactionHasBeenLocallyDeclinedEvent_CanBeCreated_IsCreated() + { + ReconciliationHasBeenLocallyDeclinedEvent reconciliationHasBeenLocallyDeclinedEvent = ReconciliationHasBeenLocallyDeclinedEvent.Create(TestData.TransactionId, + TestData.EstateId, + TestData.MerchantId, + TestData.DeclinedResponseCode, + TestData.DeclinedResponseMessage); + + reconciliationHasBeenLocallyDeclinedEvent.ShouldNotBeNull(); + reconciliationHasBeenLocallyDeclinedEvent.AggregateId.ShouldBe(TestData.TransactionId); + reconciliationHasBeenLocallyDeclinedEvent.EventCreatedDateTime.ShouldNotBe(DateTime.MinValue); + reconciliationHasBeenLocallyDeclinedEvent.EventId.ShouldNotBe(Guid.Empty); + reconciliationHasBeenLocallyDeclinedEvent.TransactionId.ShouldBe(TestData.TransactionId); + reconciliationHasBeenLocallyDeclinedEvent.EstateId.ShouldBe(TestData.EstateId); + reconciliationHasBeenLocallyDeclinedEvent.MerchantId.ShouldBe(TestData.MerchantId); + reconciliationHasBeenLocallyDeclinedEvent.ResponseCode.ShouldBe(TestData.DeclinedResponseCode); + reconciliationHasBeenLocallyDeclinedEvent.ResponseMessage.ShouldBe(TestData.DeclinedResponseMessage); + } + + #endregion + } +} \ No newline at end of file diff --git a/TransactionProcessor.ReconciliationAggregate.Tests/ReconciliationAggregateTests.cs b/TransactionProcessor.ReconciliationAggregate.Tests/ReconciliationAggregateTests.cs new file mode 100644 index 00000000..393155be --- /dev/null +++ b/TransactionProcessor.ReconciliationAggregate.Tests/ReconciliationAggregateTests.cs @@ -0,0 +1,340 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace TransactionProcessor.ReconciliationAggregate.Tests +{ + using Shouldly; + using Testing; + using TransactionAggregate; + using Xunit; + + public class ReconciliationAggregateTests + { + [Fact] + public void ReconciliationAggregate_CanBeCreated_IsCreated() + { + ReconciliationAggregate aggregate = ReconciliationAggregate.Create(TestData.TransactionId); + + aggregate.AggregateId.ShouldBe(TestData.TransactionId); + } + + [Fact] + public void ReconciliationAggregate_StartReconciliation_ReconciliationIsStarted() + { + ReconciliationAggregate reconciliationAggregate = ReconciliationAggregate.Create(TestData.TransactionId); + + reconciliationAggregate.StartReconciliation(TestData.TransactionDateTime, TestData.EstateId, TestData.MerchantId); + + reconciliationAggregate.IsStarted.ShouldBeTrue(); + reconciliationAggregate.EstateId.ShouldBe(TestData.EstateId); + reconciliationAggregate.MerchantId.ShouldBe(TestData.MerchantId); + } + + [Theory] + [InlineData(false, true, true)] + [InlineData(true, false, true)] + [InlineData(true, true, false)] + public void ReconciliationAggregate_StartReconciliation_InvalidData_ErrorThrown(Boolean validDateTime, + Boolean validEstateId, + Boolean validMerchantId) + { + ReconciliationAggregate reconciliationAggregate = ReconciliationAggregate.Create(TestData.TransactionId); + + DateTime transactionDateTime = validDateTime ? TestData.TransactionDateTime : DateTime.MinValue; + Guid estateId = validEstateId ? TestData.EstateId : Guid.Empty; + Guid merchantId = validMerchantId ? TestData.MerchantId : Guid.Empty; + + Should.Throw(() => { reconciliationAggregate.StartReconciliation(transactionDateTime, estateId, merchantId); }); + } + + [Fact] + public void ReconciliationAggregate_StartReconciliation_ReconciliationAlreadyStarted_ErrorThrown() + { + ReconciliationAggregate reconciliationAggregate = ReconciliationAggregate.Create(TestData.TransactionId); + + reconciliationAggregate.StartReconciliation(TestData.TransactionDateTime, TestData.EstateId, TestData.MerchantId); + + Should.Throw(() => + { + reconciliationAggregate.StartReconciliation(TestData.TransactionDateTime, TestData.EstateId, TestData.MerchantId); + }); + } + + [Fact] + public void ReconciliationAggregate_StartReconciliation_ReconciliationAlreadyCompleted_ErrorThrown() + { + ReconciliationAggregate reconciliationAggregate = ReconciliationAggregate.Create(TestData.TransactionId); + + reconciliationAggregate.StartReconciliation(TestData.TransactionDateTime, TestData.EstateId, TestData.MerchantId); + reconciliationAggregate.RecordOverallTotals(TestData.ReconciliationTransactionCount, TestData.ReconciliationTransactionValue); + reconciliationAggregate.Authorise(TestData.ResponseCode, TestData.ResponseMessage); + reconciliationAggregate.CompleteReconciliation(); + + Should.Throw(() => + { + reconciliationAggregate.StartReconciliation(TestData.TransactionDateTime, TestData.EstateId, TestData.MerchantId); + }); + } + + + [Fact] + public void ReconciliationAggregate_RecordOverallTotals_OverallTotalsAreRecorded() + { + ReconciliationAggregate reconciliationAggregate = ReconciliationAggregate.Create(TestData.TransactionId); + + reconciliationAggregate.StartReconciliation(TestData.TransactionDateTime, TestData.EstateId, TestData.MerchantId); + reconciliationAggregate.RecordOverallTotals(TestData.ReconciliationTransactionCount, TestData.ReconciliationTransactionValue); + + reconciliationAggregate.TransactionCount.ShouldBe(TestData.ReconciliationTransactionCount); + reconciliationAggregate.TransactionValue.ShouldBe(TestData.ReconciliationTransactionValue); + } + + [Fact] + public void ReconciliationAggregate_RecordOverallTotals_ReconciliationNotStarted_ErrorThrown() + { + ReconciliationAggregate reconciliationAggregate = ReconciliationAggregate.Create(TestData.TransactionId); + + Should.Throw(() => + { + reconciliationAggregate.RecordOverallTotals(TestData.ReconciliationTransactionCount, + TestData.ReconciliationTransactionValue); + }); + } + + [Fact] + public void ReconciliationAggregate_RecordOverallTotals_ReconciliationAlreadyCompleted_ErrorThrown() + { + ReconciliationAggregate reconciliationAggregate = ReconciliationAggregate.Create(TestData.TransactionId); + + reconciliationAggregate.StartReconciliation(TestData.TransactionDateTime, TestData.EstateId, TestData.MerchantId); + reconciliationAggregate.RecordOverallTotals(TestData.ReconciliationTransactionCount, TestData.ReconciliationTransactionValue); + reconciliationAggregate.Authorise(TestData.ResponseCode, TestData.ResponseMessage); + reconciliationAggregate.CompleteReconciliation(); + + Should.Throw(() => + { + reconciliationAggregate.RecordOverallTotals(TestData.ReconciliationTransactionCount, + TestData.ReconciliationTransactionValue); + }); + } + + [Fact] + public void ReconciliationAggregate_Authorise_ReconciliationIsAuthorised() + { + ReconciliationAggregate reconciliationAggregate = ReconciliationAggregate.Create(TestData.TransactionId); + + reconciliationAggregate.StartReconciliation(TestData.TransactionDateTime, TestData.EstateId, TestData.MerchantId); + reconciliationAggregate.RecordOverallTotals(TestData.ReconciliationTransactionCount, TestData.ReconciliationTransactionValue); + reconciliationAggregate.Authorise(TestData.ResponseCode, TestData.ResponseMessage); + + reconciliationAggregate.ResponseCode.ShouldBe(TestData.ResponseCode); + reconciliationAggregate.ResponseMessage.ShouldBe(TestData.ResponseMessage); + reconciliationAggregate.IsAuthorised.ShouldBeTrue(); + } + + [Fact] + public void ReconciliationAggregate_Authorise_ReconciliationNotStarted_ErrorThrown() + { + ReconciliationAggregate reconciliationAggregate = ReconciliationAggregate.Create(TestData.TransactionId); + + Should.Throw(() => + { + reconciliationAggregate.Authorise(TestData.ResponseCode, TestData.ResponseMessage); + }); + } + + [Fact] + public void ReconciliationAggregate_Authorise_ReconciliationAlreadyAuthorised_ErrorThrown() + { + ReconciliationAggregate reconciliationAggregate = ReconciliationAggregate.Create(TestData.TransactionId); + + reconciliationAggregate.StartReconciliation(TestData.TransactionDateTime, TestData.EstateId, TestData.MerchantId); + reconciliationAggregate.RecordOverallTotals(TestData.ReconciliationTransactionCount, TestData.ReconciliationTransactionValue); + reconciliationAggregate.Authorise(TestData.ResponseCode, TestData.ResponseMessage); + + Should.Throw(() => + { + reconciliationAggregate.Authorise(TestData.ResponseCode, TestData.ResponseMessage); + }); + } + + [Fact] + public void ReconciliationAggregate_Authorise_ReconciliationAlreadyDeclined_ErrorThrown() + { + ReconciliationAggregate reconciliationAggregate = ReconciliationAggregate.Create(TestData.TransactionId); + + reconciliationAggregate.StartReconciliation(TestData.TransactionDateTime, TestData.EstateId, TestData.MerchantId); + reconciliationAggregate.RecordOverallTotals(TestData.ReconciliationTransactionCount, TestData.ReconciliationTransactionValue); + reconciliationAggregate.Decline(TestData.ResponseCode, TestData.ResponseMessage); + + Should.Throw(() => + { + reconciliationAggregate.Authorise(TestData.ResponseCode, TestData.ResponseMessage); + }); + } + + [Fact] + public void ReconciliationAggregate_Authorise_ReconciliationAlreadyCompleted_ErrorThrown() + { + ReconciliationAggregate reconciliationAggregate = ReconciliationAggregate.Create(TestData.TransactionId); + + reconciliationAggregate.StartReconciliation(TestData.TransactionDateTime, TestData.EstateId, TestData.MerchantId); + reconciliationAggregate.RecordOverallTotals(TestData.ReconciliationTransactionCount, TestData.ReconciliationTransactionValue); + reconciliationAggregate.Authorise(TestData.ResponseCode, TestData.ResponseMessage); + reconciliationAggregate.CompleteReconciliation(); + + Should.Throw(() => + { + reconciliationAggregate.Authorise(TestData.ResponseCode, TestData.ResponseMessage); + }); + } + + [Fact] + public void ReconciliationAggregate_Decline_ReconciliationIsDeclined() + { + ReconciliationAggregate reconciliationAggregate = ReconciliationAggregate.Create(TestData.TransactionId); + + reconciliationAggregate.StartReconciliation(TestData.TransactionDateTime, TestData.EstateId, TestData.MerchantId); + reconciliationAggregate.RecordOverallTotals(TestData.ReconciliationTransactionCount, TestData.ReconciliationTransactionValue); + reconciliationAggregate.Decline(TestData.ResponseCode, TestData.ResponseMessage); + + reconciliationAggregate.ResponseCode.ShouldBe(TestData.ResponseCode); + reconciliationAggregate.ResponseMessage.ShouldBe(TestData.ResponseMessage); + reconciliationAggregate.IsAuthorised.ShouldBeFalse(); + } + + [Fact] + public void ReconciliationAggregate_Decline_ReconciliationNotStarted_ErrorThrown() + { + ReconciliationAggregate reconciliationAggregate = ReconciliationAggregate.Create(TestData.TransactionId); + + Should.Throw(() => + { + reconciliationAggregate.Decline(TestData.ResponseCode, TestData.ResponseMessage); + }); + } + + [Fact] + public void ReconciliationAggregate_Decline_ReconciliationAlreadyAuthorised_ErrorThrown() + { + ReconciliationAggregate reconciliationAggregate = ReconciliationAggregate.Create(TestData.TransactionId); + + reconciliationAggregate.StartReconciliation(TestData.TransactionDateTime, TestData.EstateId, TestData.MerchantId); + reconciliationAggregate.RecordOverallTotals(TestData.ReconciliationTransactionCount, TestData.ReconciliationTransactionValue); + reconciliationAggregate.Authorise(TestData.ResponseCode, TestData.ResponseMessage); + + Should.Throw(() => + { + reconciliationAggregate.Decline(TestData.ResponseCode, TestData.ResponseMessage); + }); + } + + [Fact] + public void ReconciliationAggregate_Decline_ReconciliationAlreadyDecline_ErrorThrown() + { + ReconciliationAggregate reconciliationAggregate = ReconciliationAggregate.Create(TestData.TransactionId); + + reconciliationAggregate.StartReconciliation(TestData.TransactionDateTime, TestData.EstateId, TestData.MerchantId); + reconciliationAggregate.RecordOverallTotals(TestData.ReconciliationTransactionCount, TestData.ReconciliationTransactionValue); + reconciliationAggregate.Decline(TestData.ResponseCode, TestData.ResponseMessage); + + Should.Throw(() => + { + reconciliationAggregate.Decline(TestData.ResponseCode, TestData.ResponseMessage); + }); + } + + [Fact] + public void ReconciliationAggregate_Decline_ReconciliationAlreadyCompleted_ErrorThrown() + { + ReconciliationAggregate reconciliationAggregate = ReconciliationAggregate.Create(TestData.TransactionId); + + reconciliationAggregate.StartReconciliation(TestData.TransactionDateTime, TestData.EstateId, TestData.MerchantId); + reconciliationAggregate.RecordOverallTotals(TestData.ReconciliationTransactionCount, TestData.ReconciliationTransactionValue); + reconciliationAggregate.Decline(TestData.ResponseCode, TestData.ResponseMessage); + reconciliationAggregate.CompleteReconciliation(); + + Should.Throw(() => + { + reconciliationAggregate.Decline(TestData.ResponseCode, TestData.ResponseMessage); + }); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ReconciliationAggregate_CompleteReconciliation_ReconciliationIsCompleted(Boolean isAuthorised) + { + ReconciliationAggregate reconciliationAggregate = ReconciliationAggregate.Create(TestData.TransactionId); + + reconciliationAggregate.StartReconciliation(TestData.TransactionDateTime, TestData.EstateId, TestData.MerchantId); + reconciliationAggregate.RecordOverallTotals(TestData.ReconciliationTransactionCount, TestData.ReconciliationTransactionValue); + if (isAuthorised) + { + reconciliationAggregate.Authorise(TestData.ResponseCode, TestData.ResponseMessage); + } + else + { + reconciliationAggregate.Decline(TestData.ResponseCode, TestData.ResponseMessage); + } + + reconciliationAggregate.CompleteReconciliation(); + + reconciliationAggregate.IsAuthorised.ShouldBe(isAuthorised); + reconciliationAggregate.IsCompleted.ShouldBeTrue(); + reconciliationAggregate.IsStarted.ShouldBeFalse(); + } + + [Fact] + public void ReconciliationAggregate_CompleteReconciliation_ReconciliationNotStarted_ErrorThrown() + { + ReconciliationAggregate reconciliationAggregate = ReconciliationAggregate.Create(TestData.TransactionId); + + Should.Throw(() => + { + reconciliationAggregate.CompleteReconciliation(); + }); + } + + [Fact] + public void ReconciliationAggregate_CompleteReconciliation_NotAuthorisedOrDeclined_ErrorThrown() + { + ReconciliationAggregate reconciliationAggregate = ReconciliationAggregate.Create(TestData.TransactionId); + + reconciliationAggregate.StartReconciliation(TestData.TransactionDateTime, TestData.EstateId, TestData.MerchantId); + reconciliationAggregate.RecordOverallTotals(TestData.ReconciliationTransactionCount, TestData.ReconciliationTransactionValue); + + Should.Throw(() => + { + reconciliationAggregate.CompleteReconciliation(); + }); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ReconciliationAggregate_CompleteReconciliation_ReconciliationAlreadyCompleted(Boolean isAuthorised) + { + ReconciliationAggregate reconciliationAggregate = ReconciliationAggregate.Create(TestData.TransactionId); + + reconciliationAggregate.StartReconciliation(TestData.TransactionDateTime, TestData.EstateId, TestData.MerchantId); + reconciliationAggregate.RecordOverallTotals(TestData.ReconciliationTransactionCount, TestData.ReconciliationTransactionValue); + if (isAuthorised) + { + reconciliationAggregate.Authorise(TestData.ResponseCode, TestData.ResponseMessage); + } + else + { + reconciliationAggregate.Decline(TestData.ResponseCode, TestData.ResponseMessage); + } + + reconciliationAggregate.CompleteReconciliation(); + + Should.Throw(() => + { + reconciliationAggregate.CompleteReconciliation(); + }); + } + } +} diff --git a/TransactionProcessor.ReconciliationAggregate.Tests/TransactionProcessor.ReconciliationAggregate.Tests.csproj b/TransactionProcessor.ReconciliationAggregate.Tests/TransactionProcessor.ReconciliationAggregate.Tests.csproj new file mode 100644 index 00000000..9e675606 --- /dev/null +++ b/TransactionProcessor.ReconciliationAggregate.Tests/TransactionProcessor.ReconciliationAggregate.Tests.csproj @@ -0,0 +1,29 @@ + + + + netcoreapp3.1 + None + false + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + diff --git a/TransactionProcessor.ReconciliationAggregate/ReconciliationAggregate.cs b/TransactionProcessor.ReconciliationAggregate/ReconciliationAggregate.cs new file mode 100644 index 00000000..08a40de6 --- /dev/null +++ b/TransactionProcessor.ReconciliationAggregate/ReconciliationAggregate.cs @@ -0,0 +1,228 @@ +using System; + +namespace TransactionProcessor.ReconciliationAggregate +{ + using System.Diagnostics.CodeAnalysis; + using Reconciliation.DomainEvents; + using Shared.DomainDrivenDesign.EventSourcing; + using Shared.EventStore.EventStore; + using Shared.General; + using Transaction.DomainEvents; + + public class ReconciliationAggregate : Aggregate + { + [ExcludeFromCodeCoverage] + protected override Object GetMetadata() + { + return new + { + this.EstateId + }; + } + + protected override void PlayEvent(DomainEvent domainEvent) + { + this.PlayEvent((dynamic)domainEvent); + } + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + [ExcludeFromCodeCoverage] + public ReconciliationAggregate() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The aggregate identifier. + private ReconciliationAggregate(Guid aggregateId) + { + Guard.ThrowIfInvalidGuid(aggregateId, "Aggregate Id cannot be an Empty Guid"); + + this.AggregateId = aggregateId; + } + + public Guid EstateId { get; private set; } + public Boolean IsStarted { get; private set; } + public Guid MerchantId { get; private set; } + public String ResponseCode { get; private set; } + public Boolean IsCompleted { get; private set; } + public Boolean IsAuthorised { get; private set; } + public Boolean IsDeclined { get; private set; } + + /// + /// Gets the response message. + /// + /// + /// The response message. + /// + public String ResponseMessage { get; private set; } + + public Int32 TransactionCount { get; private set; } + + public Decimal TransactionValue { get; private set; } + + /// + /// Creates the specified aggregate identifier. + /// + /// The aggregate identifier. + /// + public static ReconciliationAggregate Create(Guid aggregateId) + { + return new ReconciliationAggregate(aggregateId); + } + + private void CheckReconciliationNotAlreadyStarted() + { + if (this.IsStarted) + { + throw new InvalidOperationException($"Reconciliation Id [{this.AggregateId}] has already been started"); + } + } + + private void CheckReconciliationNotAlreadyCompleted() + { + if (this.IsCompleted) + { + throw new InvalidOperationException($"Reconciliation Id [{this.AggregateId}] has already been completed"); + } + } + + public void StartReconciliation(DateTime transactionDateTime, + Guid estateId, + Guid merchantId) + { + Guard.ThrowIfInvalidGuid(estateId, typeof(ArgumentException), $"Estate Id must not be [{Guid.Empty}]"); + Guard.ThrowIfInvalidGuid(merchantId, typeof(ArgumentException), $"Merchant Id must not be [{Guid.Empty}]"); + Guard.ThrowIfInvalidDate(transactionDateTime, typeof(ArgumentException), $"Transaction Date Time must not be [{DateTime.MinValue}]"); + + // TODO: Some rules here + this.CheckReconciliationNotAlreadyStarted(); + this.CheckReconciliationNotAlreadyCompleted(); + + ReconciliationHasStartedEvent reconciliationHasStartedEvent = + ReconciliationHasStartedEvent.Create(this.AggregateId, estateId, merchantId, transactionDateTime); + + this.ApplyAndPend(reconciliationHasStartedEvent); + } + + private void CheckReconciliationHasBeenStarted() + { + if (this.IsStarted == false) + { + throw new InvalidOperationException($"Reconciliation [{this.AggregateId}] has not been started"); + } + } + + public void RecordOverallTotals(Int32 totalCount, Decimal totalValue) + { + // TODO: Rules + this.CheckReconciliationHasBeenStarted(); + this.CheckReconciliationNotAlreadyCompleted(); + + OverallTotalsRecordedEvent overallTotalsRecordedEvent = OverallTotalsRecordedEvent.Create(this.AggregateId, this.EstateId,this.MerchantId, totalCount, totalValue); + this.ApplyAndPend(overallTotalsRecordedEvent); + } + + public void Authorise(String responseCode, String responseMessage) + { + this.CheckReconciliationHasBeenStarted(); + this.CheckReconciliationNotAlreadyCompleted(); + this.CheckReconciliationNotAlreadyAuthorised(); + this.CheckReconciliationNotAlreadyDeclined(); + + ReconciliationHasBeenLocallyAuthorisedEvent reconciliationHasBeenLocallyAuthorisedEvent = ReconciliationHasBeenLocallyAuthorisedEvent.Create(this.AggregateId, this.EstateId, this.MerchantId, + responseCode, responseMessage); + + this.ApplyAndPend(reconciliationHasBeenLocallyAuthorisedEvent); + } + + private void CheckReconciliationNotAlreadyAuthorised() + { + if (this.IsAuthorised) + { + throw new InvalidOperationException($"Reconciliation [{this.AggregateId}] has already been authorised"); + } + } + + private void CheckReconciliationNotAlreadyDeclined() + { + if (this.IsDeclined) + { + throw new InvalidOperationException($"Reconciliation [{this.AggregateId}] has already been declined"); + } + } + + private void CheckReconciliationHasBeenAuthorisedOrDeclined() + { + if (this.IsAuthorised == false && this.IsDeclined == false) + { + throw new InvalidOperationException($"Reconciliation [{this.AggregateId}] has not been authorised or declined"); + } + } + + public void Decline(String responseCode, String responseMessage) + { + this.CheckReconciliationHasBeenStarted(); + this.CheckReconciliationNotAlreadyCompleted(); + this.CheckReconciliationNotAlreadyAuthorised(); + this.CheckReconciliationNotAlreadyDeclined(); + + ReconciliationHasBeenLocallyDeclinedEvent reconciliationHasBeenLocallyDeclinedEvent = ReconciliationHasBeenLocallyDeclinedEvent.Create(this.AggregateId, this.EstateId, this.MerchantId, + responseCode, responseMessage); + + this.ApplyAndPend(reconciliationHasBeenLocallyDeclinedEvent); + } + + public void CompleteReconciliation() + { + this.CheckReconciliationHasBeenStarted(); + this.CheckReconciliationNotAlreadyCompleted(); + this.CheckReconciliationHasBeenAuthorisedOrDeclined(); + + ReconciliationHasCompletedEvent reconciliationHasCompletedEvent = + ReconciliationHasCompletedEvent.Create(this.AggregateId, this.EstateId, this.MerchantId); + + this.ApplyAndPend(reconciliationHasCompletedEvent); + + } + #endregion + + private void PlayEvent(ReconciliationHasStartedEvent domainEvent) + { + this.EstateId = domainEvent.EstateId; + this.MerchantId = domainEvent.MerchantId; + this.IsStarted = true; + } + + private void PlayEvent(OverallTotalsRecordedEvent domainEvent) + { + this.TransactionCount = domainEvent.TransactionCount; + this.TransactionValue = domainEvent.TransactionValue; + } + + private void PlayEvent(ReconciliationHasBeenLocallyAuthorisedEvent domainEvent) + { + this.ResponseCode = domainEvent.ResponseCode; + this.ResponseMessage = domainEvent.ResponseMessage; + this.IsAuthorised = true; + } + + private void PlayEvent(ReconciliationHasBeenLocallyDeclinedEvent domainEvent) + { + this.ResponseCode = domainEvent.ResponseCode; + this.ResponseMessage = domainEvent.ResponseMessage; + this.IsDeclined = true; + } + + private void PlayEvent(ReconciliationHasCompletedEvent domainEvent) + { + this.IsStarted = false; + this.IsCompleted = true; + } + } +} diff --git a/TransactionProcessor.ReconciliationAggregate/TransactionProcessor.ReconciliationAggregate.csproj b/TransactionProcessor.ReconciliationAggregate/TransactionProcessor.ReconciliationAggregate.csproj new file mode 100644 index 00000000..1324c053 --- /dev/null +++ b/TransactionProcessor.ReconciliationAggregate/TransactionProcessor.ReconciliationAggregate.csproj @@ -0,0 +1,15 @@ + + + + netcoreapp3.1 + + + + + + + + + + + diff --git a/TransactionProcessor.Testing/TestData.cs b/TransactionProcessor.Testing/TestData.cs index 624e88f3..0ca34494 100644 --- a/TransactionProcessor.Testing/TestData.cs +++ b/TransactionProcessor.Testing/TestData.cs @@ -9,6 +9,7 @@ using EstateManagement.DataTransferObjects; using EstateManagement.DataTransferObjects.Responses; using Models; + using ReconciliationAggregate; using SecurityService.DataTransferObjects.Responses; using Transaction.DomainEvents; using TransactionAggregate; @@ -416,6 +417,16 @@ public class TestData Pin = "1234" }; + public static ProcessReconciliationRequest ProcessReconciliationRequest => + ProcessReconciliationRequest.Create(TestData.TransactionId, TestData.EstateId, TestData.MerchantId, TestData.DeviceIdentifier, TestData.TransactionDateTime, TestData.ReconciliationTransactionCount, TestData.ReconciliationTransactionValue); + + public static ProcessReconciliationTransactionResponse ProcessReconciliationTransactionResponseModel => + new ProcessReconciliationTransactionResponse + { + ResponseMessage = TestData.ResponseMessage, + ResponseCode = TestData.ResponseCode + }; + #endregion #region Methods @@ -570,7 +581,7 @@ public static TransactionAggregate GetEmptyTransactionAggregate() { return TransactionAggregate.Create(TestData.TransactionId); } - + public static TransactionAggregate GetLocallyAuthorisedTransactionAggregate() { TransactionAggregate transactionAggregate = TransactionAggregate.Create(TestData.TransactionId); @@ -679,6 +690,10 @@ public static TokenResponse TokenResponse() public static Decimal TransactionFeeValue = 0.5m; public static Decimal CalculatedFeeValue = 0.5m; + public static Int32 ReconciliationTransactionCount = 1; + + public static Decimal ReconciliationTransactionValue = 100; + public static List ContractProductTransactionFees => new List { diff --git a/TransactionProcessor.Tests/Factories/ModelFactoryTests.cs b/TransactionProcessor.Tests/Factories/ModelFactoryTests.cs index 0f919242..fb56b7ab 100644 --- a/TransactionProcessor.Tests/Factories/ModelFactoryTests.cs +++ b/TransactionProcessor.Tests/Factories/ModelFactoryTests.cs @@ -72,5 +72,35 @@ public void ModelFactory_ProcessSaleTransactionResponseModel_NullInput_IsConvert saleTransactionResponse.ShouldBeNull(); } + + [Fact] + public void ModelFactory_ProcessReconciliationTransactionResponse_IsConverted() + { + ProcessReconciliationTransactionResponse processReconciliationTransactionResponseModel = TestData.ProcessReconciliationTransactionResponseModel; + + ModelFactory modelFactory = new ModelFactory(); + + SerialisedMessage processReconciliationTransactionResponse = modelFactory.ConvertFrom(processReconciliationTransactionResponseModel); + + processReconciliationTransactionResponse.ShouldNotBeNull(); + processReconciliationTransactionResponse.Metadata.ShouldContainKey(MetadataContants.KeyNameEstateId); + processReconciliationTransactionResponse.Metadata.ShouldContainKey(MetadataContants.KeyNameMerchantId); + String estateId = processReconciliationTransactionResponse.Metadata[MetadataContants.KeyNameEstateId]; + String merchantId = processReconciliationTransactionResponse.Metadata[MetadataContants.KeyNameMerchantId]; + estateId.ShouldBe(TestData.ProcessSaleTransactionResponseModel.EstateId.ToString()); + merchantId.ShouldBe(TestData.ProcessSaleTransactionResponseModel.MerchantId.ToString()); + } + + [Fact] + public void ModelFactory_ProcessReconciliationTransactionResponse_NullInput_IsConverted() + { + ProcessReconciliationTransactionResponse processReconciliationTransactionResponseModel = null; + + ModelFactory modelFactory = new ModelFactory(); + + SerialisedMessage processReconciliationTransactionResponse = modelFactory.ConvertFrom(processReconciliationTransactionResponseModel); + + processReconciliationTransactionResponse.ShouldBeNull(); + } } } diff --git a/TransactionProcessor.TransactionAgrgegate/TransactionAggregate.cs b/TransactionProcessor.TransactionAgrgegate/TransactionAggregate.cs index 07063683..7f529c42 100644 --- a/TransactionProcessor.TransactionAgrgegate/TransactionAggregate.cs +++ b/TransactionProcessor.TransactionAgrgegate/TransactionAggregate.cs @@ -272,6 +272,8 @@ private TransactionAggregate(Guid aggregateId) /// Adds the fee. /// /// The calculated fee. + /// calculatedFee + /// Unsupported Fee Type /// calculatedFee /// Unsupported Fee Type public void AddFee(CalculatedFee calculatedFee) @@ -544,6 +546,11 @@ public void RequestEmailReceipt(String customerEmailAddress) /// The merchant identifier. /// The device identifier. /// The transaction amount. + /// + /// Transaction Number must be numeric + /// or + /// Device Identifier must be alphanumeric + /// /// Transaction Number must be numeric /// or /// Invalid Transaction Type [{transactionType}] @@ -619,6 +626,7 @@ protected override void PlayEvent(DomainEvent domainEvent) /// /// Checks the additional request data not already recorded. /// + /// Additional Request Data already recorded /// Additional Request Data already recorded private void CheckAdditionalRequestDataNotAlreadyRecorded() { @@ -631,6 +639,7 @@ private void CheckAdditionalRequestDataNotAlreadyRecorded() /// /// Checks the additional response data not already recorded. /// + /// Additional Response Data already recorded /// Additional Response Data already recorded private void CheckAdditionalResponseDataNotAlreadyRecorded() { @@ -643,6 +652,7 @@ private void CheckAdditionalResponseDataNotAlreadyRecorded() /// /// Checks the customer has not already requested email receipt. /// + /// Customer Email Receipt already requested for Transaction [{this.AggregateId}] /// Customer Email Receipt already requested for Transaction [{this.AggregateId}] private void CheckCustomerHasNotAlreadyRequestedEmailReceipt() { @@ -656,6 +666,7 @@ 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 /// Fee with Id [{calculatedFee.FeeId}] has already been added to this transaction private void CheckFeeHasNotAlreadyBeenAdded(CalculatedFee calculatedFee) { @@ -668,6 +679,7 @@ private void CheckFeeHasNotAlreadyBeenAdded(CalculatedFee calculatedFee) /// /// Checks the product details not already added. /// + /// Product details already added /// Product details already added private void CheckProductDetailsNotAlreadyAdded() { @@ -680,6 +692,7 @@ private void CheckProductDetailsNotAlreadyAdded() /// /// Checks the transaction can attract fees. /// + /// Transactions of type {this.TransactionType} cannot attract fees /// Transactions of type {this.TransactionType} cannot attract fees private void CheckTransactionCanAttractFees() { @@ -692,6 +705,7 @@ private void CheckTransactionCanAttractFees() /// /// Checks the transaction can be locally authorised. /// + /// Sales cannot be locally authorised /// Sales cannot be locally authorised /// Sales cannot be locally authorised private void CheckTransactionCanBeLocallyAuthorised() @@ -705,6 +719,7 @@ private void CheckTransactionCanBeLocallyAuthorised() /// /// Checks the transaction has been authorised. /// + /// Transaction [{this.AggregateId}] has not been authorised /// Transaction [{this.AggregateId}] has not been authorised private void CheckTransactionHasBeenAuthorised() { @@ -717,6 +732,7 @@ private void CheckTransactionHasBeenAuthorised() /// /// Checks the transaction has been authorised. /// + /// Transaction [{this.AggregateId}] has not been authorised or declined /// Transaction [{this.AggregateId}] has not been authorised private void CheckTransactionHasBeenAuthorisedOrDeclined() { @@ -729,6 +745,7 @@ private void CheckTransactionHasBeenAuthorisedOrDeclined() /// /// Checks the transaction has been completed. /// + /// Transaction [{this.AggregateId}] has not been completed /// Transaction [{this.AggregateId}] has not been completed private void CheckTransactionHasBeenCompleted() { @@ -741,6 +758,7 @@ private void CheckTransactionHasBeenCompleted() /// /// Checks the transaction has been started. /// + /// Transaction [{this.AggregateId}] has not been started /// Transaction [{this.AggregateId}] has not been started private void CheckTransactionHasBeenStarted() { @@ -753,6 +771,7 @@ private void CheckTransactionHasBeenStarted() /// /// Checks the transaction not already authorised. /// + /// Transaction [{this.AggregateId}] has already been{authtype}authorised /// Transaction [{this.AggregateId}] has already been{authtype}authorised private void CheckTransactionNotAlreadyAuthorised() { @@ -766,6 +785,7 @@ private void CheckTransactionNotAlreadyAuthorised() /// /// Checks the transaction not already completed. /// + /// Transaction Id [{this.AggregateId}] has already been completed /// Transaction Id [{this.AggregateId}] has already been completed private void CheckTransactionNotAlreadyCompleted() { @@ -778,6 +798,7 @@ private void CheckTransactionNotAlreadyCompleted() /// /// Checks the transaction not already declined. /// + /// Transaction [{this.AggregateId}] has already been{authtype}declined /// Transaction [{this.AggregateId}] has already been{authtype}declined private void CheckTransactionNotAlreadyDeclined() { @@ -791,6 +812,7 @@ private void CheckTransactionNotAlreadyDeclined() /// /// Checks the transaction not already started. /// + /// Transaction Id [{this.AggregateId}] has already been started /// Transaction Id [{this.AggregateId}] has already been started private void CheckTransactionNotAlreadyStarted() { @@ -955,6 +977,11 @@ private void PlayEvent(ServiceProviderFeeAddedToTransactionEvent domainEvent) #endregion + + /// + /// Gets the transaction. + /// + /// public Transaction GetTransaction() { return new Transaction diff --git a/TransactionProcessor.sln b/TransactionProcessor.sln index 297e1530..1057d36c 100644 --- a/TransactionProcessor.sln +++ b/TransactionProcessor.sln @@ -29,7 +29,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TransactionProcessor.Transa EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TransactionProcessor.IntegrationTests", "TransactionProcessor.IntegrationTests\TransactionProcessor.IntegrationTests.csproj", "{3C40D27B-66B6-4C4A-839C-1E2BD7B4994C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TransactionProcessor.Client", "TransactionProcessor.Client\TransactionProcessor.Client.csproj", "{DAF20F25-8BD1-4FA5-ADAD-71B068CDF393}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TransactionProcessor.Client", "TransactionProcessor.Client\TransactionProcessor.Client.csproj", "{DAF20F25-8BD1-4FA5-ADAD-71B068CDF393}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TransactionProcessor.ReconciliationAggregate", "TransactionProcessor.ReconciliationAggregate\TransactionProcessor.ReconciliationAggregate.csproj", "{74CA5F89-A99B-49AE-B690-376900DF488E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TransactionProcessor.Reconciliation.DomainEvents", "TransactionProcessor.Reconciliation.DomainEvents\TransactionProcessor.Reconciliation.DomainEvents.csproj", "{E079DF81-BA19-4F71-85D6-AE36463F2B7F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TransactionProcessor.ReconciliationAggregate.Tests", "TransactionProcessor.ReconciliationAggregate.Tests\TransactionProcessor.ReconciliationAggregate.Tests.csproj", "{E71A7E6D-B320-40A1-BF86-C2857B472313}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -85,6 +91,18 @@ Global {DAF20F25-8BD1-4FA5-ADAD-71B068CDF393}.Debug|Any CPU.Build.0 = Debug|Any CPU {DAF20F25-8BD1-4FA5-ADAD-71B068CDF393}.Release|Any CPU.ActiveCfg = Release|Any CPU {DAF20F25-8BD1-4FA5-ADAD-71B068CDF393}.Release|Any CPU.Build.0 = Release|Any CPU + {74CA5F89-A99B-49AE-B690-376900DF488E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {74CA5F89-A99B-49AE-B690-376900DF488E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {74CA5F89-A99B-49AE-B690-376900DF488E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {74CA5F89-A99B-49AE-B690-376900DF488E}.Release|Any CPU.Build.0 = Release|Any CPU + {E079DF81-BA19-4F71-85D6-AE36463F2B7F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E079DF81-BA19-4F71-85D6-AE36463F2B7F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E079DF81-BA19-4F71-85D6-AE36463F2B7F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E079DF81-BA19-4F71-85D6-AE36463F2B7F}.Release|Any CPU.Build.0 = Release|Any CPU + {E71A7E6D-B320-40A1-BF86-C2857B472313}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E71A7E6D-B320-40A1-BF86-C2857B472313}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E71A7E6D-B320-40A1-BF86-C2857B472313}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E71A7E6D-B320-40A1-BF86-C2857B472313}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -102,6 +120,9 @@ Global {69BE1042-5AB9-420B-9A27-E2F1ADFC4E65} = {71B30DC4-AB27-4D30-8481-B4C326D074CB} {3C40D27B-66B6-4C4A-839C-1E2BD7B4994C} = {71B30DC4-AB27-4D30-8481-B4C326D074CB} {DAF20F25-8BD1-4FA5-ADAD-71B068CDF393} = {749ADE74-A6F0-4469-A638-8FD7E82A8667} + {74CA5F89-A99B-49AE-B690-376900DF488E} = {749ADE74-A6F0-4469-A638-8FD7E82A8667} + {E079DF81-BA19-4F71-85D6-AE36463F2B7F} = {749ADE74-A6F0-4469-A638-8FD7E82A8667} + {E71A7E6D-B320-40A1-BF86-C2857B472313} = {71B30DC4-AB27-4D30-8481-B4C326D074CB} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {193D13DE-424B-4D50-B674-01F9E4CC2CA9} diff --git a/TransactionProcessor/Controllers/TransactionController.cs b/TransactionProcessor/Controllers/TransactionController.cs index 98ff8cc5..011c8b29 100644 --- a/TransactionProcessor/Controllers/TransactionController.cs +++ b/TransactionProcessor/Controllers/TransactionController.cs @@ -2,6 +2,7 @@ { using System; using System.Diagnostics.CodeAnalysis; + using System.Net; using System.Threading; using System.Threading.Tasks; using BusinessLogic.Requests; @@ -45,7 +46,7 @@ public class TransactionController : ControllerBase /// /// Initializes a new instance of the class. /// - /// The command router. + /// The mediator. /// The model factory. public TransactionController(IMediator mediator, IModelFactory modelFactory) @@ -145,6 +146,30 @@ private async Task ProcessSpecificMessage(SaleTransactionRequ return this.ModelFactory.ConvertFrom(response); } + /// + /// Processes the specific message. + /// + /// The reconciliation request. + /// The cancellation token. + /// + private async Task ProcessSpecificMessage(ReconciliationRequest reconciliationRequest, + CancellationToken cancellationToken) + { + Guid transactionId = Guid.NewGuid(); + + ProcessReconciliationRequest request = ProcessReconciliationRequest.Create(transactionId, + reconciliationRequest.EstateId, + reconciliationRequest.MerchantId, + reconciliationRequest.DeviceIdentifier, + reconciliationRequest.TransactionDateTime, + reconciliationRequest.TransactionCount, + reconciliationRequest.TransactionValue); + + ProcessReconciliationTransactionResponse response = await this.Mediator.Send(request, cancellationToken); + + return this.ModelFactory.ConvertFrom(response); + } + #endregion #region Others diff --git a/TransactionProcessor/Factories/IModelFactory.cs b/TransactionProcessor/Factories/IModelFactory.cs index ddfea764..5be78a22 100644 --- a/TransactionProcessor/Factories/IModelFactory.cs +++ b/TransactionProcessor/Factories/IModelFactory.cs @@ -1,5 +1,6 @@ namespace TransactionProcessor.Factories { + using BusinessLogic.Requests; using DataTransferObjects; using Models; @@ -24,6 +25,13 @@ public interface IModelFactory /// SerialisedMessage ConvertFrom(ProcessSaleTransactionResponse processSaleTransactionResponse); + /// + /// Converts from. + /// + /// The process reconciliation transaction response. + /// + SerialisedMessage ConvertFrom(ProcessReconciliationTransactionResponse processReconciliationTransactionResponse); + #endregion } } \ No newline at end of file diff --git a/TransactionProcessor/Factories/ModelFactory.cs b/TransactionProcessor/Factories/ModelFactory.cs index 93863bfa..1077049f 100644 --- a/TransactionProcessor/Factories/ModelFactory.cs +++ b/TransactionProcessor/Factories/ModelFactory.cs @@ -2,6 +2,7 @@ { using System; using System.Collections.Generic; + using BusinessLogic.Requests; using DataTransferObjects; using Models; using Newtonsoft.Json; @@ -83,6 +84,41 @@ public SerialisedMessage ConvertFrom(ProcessSaleTransactionResponse processSaleT }; } + + /// + /// Converts from. + /// + /// The process reconciliation transaction response. + /// + public SerialisedMessage ConvertFrom(ProcessReconciliationTransactionResponse processReconciliationTransactionResponse) + { + if (processReconciliationTransactionResponse == null) + { + return null; + } + + ReconciliationResponse reconciliationTransactionResponse = new ReconciliationResponse + { + ResponseMessage = processReconciliationTransactionResponse.ResponseMessage, + ResponseCode = processReconciliationTransactionResponse.ResponseCode, + MerchantId = processReconciliationTransactionResponse.MerchantId, + EstateId = processReconciliationTransactionResponse.EstateId + }; + + return new SerialisedMessage + { + Metadata = new Dictionary() + { + {MetadataContants.KeyNameEstateId, processReconciliationTransactionResponse.EstateId.ToString()}, + {MetadataContants.KeyNameMerchantId, processReconciliationTransactionResponse.MerchantId.ToString()} + }, + SerialisedData = JsonConvert.SerializeObject(reconciliationTransactionResponse, new JsonSerializerSettings + { + TypeNameHandling = TypeNameHandling.All + }) + }; + } + #endregion } } \ No newline at end of file diff --git a/TransactionProcessor/Startup.cs b/TransactionProcessor/Startup.cs index f4d5ac4f..27227410 100644 --- a/TransactionProcessor/Startup.cs +++ b/TransactionProcessor/Startup.cs @@ -54,9 +54,16 @@ namespace TransactionProcessor using Swashbuckle.AspNetCore.SwaggerGen; using ILogger = Microsoft.Extensions.Logging.ILogger; + /// + /// + /// [ExcludeFromCodeCoverage] public class Startup { + /// + /// Initializes a new instance of the class. + /// + /// The web host environment. public Startup(IWebHostEnvironment webHostEnvironment) { IConfigurationBuilder builder = new ConfigurationBuilder().SetBasePath(webHostEnvironment.ContentRootPath) @@ -67,11 +74,27 @@ public Startup(IWebHostEnvironment webHostEnvironment) Startup.WebHostEnvironment = webHostEnvironment; } + /// + /// Gets or sets the configuration. + /// + /// + /// The configuration. + /// public static IConfigurationRoot Configuration { get; set; } + /// + /// Gets or sets the web host environment. + /// + /// + /// The web host environment. + /// public static IWebHostEnvironment WebHostEnvironment { get; set; } // This method gets called by the runtime. Use this method to add services to the container. + /// + /// Configures the services. + /// + /// The services. public void ConfigureServices(IServiceCollection services) { ConfigurationReader.Initialise(Startup.Configuration); @@ -123,6 +146,7 @@ public void ConfigureServices(IServiceCollection services) services.AddTransient(); services.AddSingleton(); services.AddSingleton, AggregateRepository>(); + services.AddSingleton, AggregateRepository>(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); @@ -144,6 +168,7 @@ public void ConfigureServices(IServiceCollection services) services.AddSingleton, TransactionRequestHandler>(); services.AddSingleton, TransactionRequestHandler>(); + services.AddSingleton, TransactionRequestHandler>(); services.AddTransient>(context => (operatorIdentifier) => { @@ -176,8 +201,15 @@ public void ConfigureServices(IServiceCollection services) services.AddSingleton(); } + /// + /// The event store client settings + /// private static EventStoreClientSettings EventStoreClientSettings; + /// + /// Configures the event store settings. + /// + /// The settings. private static void ConfigureEventStoreSettings(EventStoreClientSettings settings = null) { if (settings == null) @@ -206,6 +238,10 @@ private static void ConfigureEventStoreSettings(EventStoreClientSettings setting Startup.EventStoreClientSettings = settings; } + /// + /// Configures the middleware services. + /// + /// The services. private void ConfigureMiddlewareServices(IServiceCollection services) { services.AddHealthChecks() @@ -296,6 +332,13 @@ private void ConfigureMiddlewareServices(IServiceCollection services) } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + /// + /// Configures the specified application. + /// + /// The application. + /// The env. + /// The logger factory. + /// The provider. public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory, IApiVersionDescriptionProvider provider) {