diff --git a/TransactionProcessor.BusinessLogic.Tests/RequestHandler/TransactionRequestHandlerTests.cs b/TransactionProcessor.BusinessLogic.Tests/RequestHandler/TransactionRequestHandlerTests.cs index 964b1c89..b8662a04 100644 --- a/TransactionProcessor.BusinessLogic.Tests/RequestHandler/TransactionRequestHandlerTests.cs +++ b/TransactionProcessor.BusinessLogic.Tests/RequestHandler/TransactionRequestHandlerTests.cs @@ -29,5 +29,20 @@ public void TransactionRequestHandler_ProcessLogonTransactionRequest_IsHandled() }); } + + [Fact] + public void TransactionRequestHandler_ProcessSaleTransactionRequest_IsHandled() + { + Mock transactionDomainService = new Mock(); + TransactionRequestHandler handler = new TransactionRequestHandler(transactionDomainService.Object); + + ProcessSaleTransactionRequest command = TestData.ProcessSaleTransactionRequest; + + 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 ee6c12bf..3de4e355 100644 --- a/TransactionProcessor.BusinessLogic.Tests/Requests/RequestTests.cs +++ b/TransactionProcessor.BusinessLogic.Tests/Requests/RequestTests.cs @@ -14,16 +14,38 @@ public class RequestTests [Fact] public void ProcessLogonTransactionRequest_CanBeCreated_IsCreated() { - ProcessLogonTransactionRequest processLogonTransactionRequest = ProcessLogonTransactionRequest.Create(TestData.TransactionId, TestData.EstateId, TestData.MerchantId, TestData.DeviceIdentifier,TestData.TransactionType, TestData.TransactionDateTime, + ProcessLogonTransactionRequest processLogonTransactionRequest = ProcessLogonTransactionRequest.Create(TestData.TransactionId, TestData.EstateId, TestData.MerchantId, TestData.DeviceIdentifier,TestData.TransactionTypeLogon.ToString(), TestData.TransactionDateTime, TestData.TransactionNumber); processLogonTransactionRequest.ShouldNotBeNull(); processLogonTransactionRequest.EstateId.ShouldBe(TestData.EstateId); processLogonTransactionRequest.MerchantId.ShouldBe(TestData.MerchantId); processLogonTransactionRequest.DeviceIdentifier.ShouldBe(TestData.DeviceIdentifier); - processLogonTransactionRequest.TransactionType.ShouldBe(TestData.TransactionType); + processLogonTransactionRequest.TransactionType.ShouldBe(TestData.TransactionTypeLogon.ToString()); processLogonTransactionRequest.TransactionDateTime.ShouldBe(TestData.TransactionDateTime); processLogonTransactionRequest.TransactionNumber.ShouldBe(TestData.TransactionNumber); + processLogonTransactionRequest.TransactionId.ShouldBe(processLogonTransactionRequest.TransactionId); + } + + [Fact] + public void ProcessSaleTransactionRequest_CanBeCreated_IsCreated() + { + ProcessSaleTransactionRequest processSaleTransactionRequest = ProcessSaleTransactionRequest.Create(TestData.TransactionId, TestData.EstateId, TestData.MerchantId, TestData.DeviceIdentifier, TestData.TransactionTypeLogon.ToString(), TestData.TransactionDateTime, + TestData.TransactionNumber, + TestData.OperatorIdentifier, + TestData.AdditionalTransactionMetaData); + + processSaleTransactionRequest.ShouldNotBeNull(); + processSaleTransactionRequest.EstateId.ShouldBe(TestData.EstateId); + processSaleTransactionRequest.MerchantId.ShouldBe(TestData.MerchantId); + processSaleTransactionRequest.DeviceIdentifier.ShouldBe(TestData.DeviceIdentifier); + processSaleTransactionRequest.TransactionType.ShouldBe(TestData.TransactionTypeLogon.ToString()); + processSaleTransactionRequest.TransactionDateTime.ShouldBe(TestData.TransactionDateTime); + processSaleTransactionRequest.TransactionNumber.ShouldBe(TestData.TransactionNumber); + processSaleTransactionRequest.TransactionId.ShouldBe(TestData.TransactionId); + processSaleTransactionRequest.OperatorIdentifier.ShouldBe(TestData.OperatorIdentifier); + processSaleTransactionRequest.AdditionalTransactionMetadata.ShouldNotBeNull(); + processSaleTransactionRequest.AdditionalTransactionMetadata.Count.ShouldBe(TestData.AdditionalTransactionMetaData.Count); } } } diff --git a/TransactionProcessor.BusinessLogic.Tests/Services/TransactionDomainServiceTests.cs b/TransactionProcessor.BusinessLogic.Tests/Services/TransactionDomainServiceTests.cs index 4834fd84..ed1b3a78 100644 --- a/TransactionProcessor.BusinessLogic.Tests/Services/TransactionDomainServiceTests.cs +++ b/TransactionProcessor.BusinessLogic.Tests/Services/TransactionDomainServiceTests.cs @@ -11,6 +11,7 @@ namespace TransactionProcessor.BusinessLogic.Tests.Services using Microsoft.Extensions.Configuration; using Models; using Moq; + using OperatorInterfaces; using SecurityService.Client; using SecurityService.DataTransferObjects.Responses; using Shared.DomainDrivenDesign.EventStore; @@ -51,8 +52,12 @@ public async Task TransactionDomainService_ProcessLogonTransaction_TransactionIs estateClient.Setup(e => e.GetMerchant(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(TestData.GetMerchantResponse); + Mock operatorProxy = new Mock(); + Func operatorProxyResolver = (operatorName) => { return operatorProxy.Object; }; + TransactionDomainService transactionDomainService = - new TransactionDomainService(aggregateRepositoryManager.Object, estateClient.Object, securityServiceClient.Object); + new TransactionDomainService(aggregateRepositoryManager.Object, estateClient.Object, securityServiceClient.Object, + operatorProxyResolver); ProcessLogonTransactionResponse response = await transactionDomainService.ProcessLogonTransaction(TestData.TransactionId, TestData.EstateId, @@ -62,7 +67,7 @@ public async Task TransactionDomainService_ProcessLogonTransaction_TransactionIs TestData.DeviceIdentifier, CancellationToken.None); - this.ValidateLogonResponse(response, TransactionResponseCode.Success); + this.ValidateResponse(response, TransactionResponseCode.Success); } [Fact] @@ -86,6 +91,8 @@ public async Task TransactionDomainService_ProcessLogonTransaction_MerchantWithN 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.GetEstateResponse); @@ -93,7 +100,8 @@ public async Task TransactionDomainService_ProcessLogonTransaction_MerchantWithN .ReturnsAsync(TestData.GetMerchantResponseWithNullDevices); TransactionDomainService transactionDomainService = - new TransactionDomainService(aggregateRepositoryManager.Object, estateClient.Object, securityServiceClient.Object); + new TransactionDomainService(aggregateRepositoryManager.Object, estateClient.Object, securityServiceClient.Object, + operatorProxyResolver); ProcessLogonTransactionResponse response = await transactionDomainService.ProcessLogonTransaction(TestData.TransactionId, TestData.EstateId, @@ -103,7 +111,7 @@ public async Task TransactionDomainService_ProcessLogonTransaction_MerchantWithN TestData.DeviceIdentifier, CancellationToken.None); - this.ValidateLogonResponse(response, TransactionResponseCode.Success); + this.ValidateResponse(response, TransactionResponseCode.Success); } [Fact] @@ -128,13 +136,17 @@ public async Task TransactionDomainService_ProcessLogonTransaction_MerchantWithN 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.GetEstateResponse); estateClient.Setup(e => e.GetMerchant(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(TestData.GetMerchantResponseWithNoDevices); TransactionDomainService transactionDomainService = - new TransactionDomainService(aggregateRepositoryManager.Object, estateClient.Object, securityServiceClient.Object); + new TransactionDomainService(aggregateRepositoryManager.Object, estateClient.Object, securityServiceClient.Object, + operatorProxyResolver); ProcessLogonTransactionResponse response = await transactionDomainService.ProcessLogonTransaction(TestData.TransactionId, TestData.EstateId, @@ -144,7 +156,7 @@ public async Task TransactionDomainService_ProcessLogonTransaction_MerchantWithN TestData.DeviceIdentifier, CancellationToken.None); - this.ValidateLogonResponse(response, TransactionResponseCode.Success); + this.ValidateResponse(response, TransactionResponseCode.Success); } [Fact] @@ -168,6 +180,8 @@ public async Task TransactionDomainService_ProcessLogonTransaction_IncorrectDevi 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.GetEstateResponse); @@ -175,7 +189,8 @@ public async Task TransactionDomainService_ProcessLogonTransaction_IncorrectDevi .ReturnsAsync(TestData.GetMerchantResponse); TransactionDomainService transactionDomainService = - new TransactionDomainService(aggregateRepositoryManager.Object, estateClient.Object, securityServiceClient.Object); + new TransactionDomainService(aggregateRepositoryManager.Object, estateClient.Object, securityServiceClient.Object, + operatorProxyResolver); ProcessLogonTransactionResponse response = await transactionDomainService.ProcessLogonTransaction(TestData.TransactionId, TestData.EstateId, @@ -185,7 +200,7 @@ public async Task TransactionDomainService_ProcessLogonTransaction_IncorrectDevi TestData.DeviceIdentifier1, CancellationToken.None); - this.ValidateLogonResponse(response, TransactionResponseCode.InvalidDeviceIdentifier); + this.ValidateResponse(response, TransactionResponseCode.InvalidDeviceIdentifier); } [Fact] @@ -209,14 +224,17 @@ public async Task TransactionDomainService_ProcessLogonTransaction_InvalidEstate 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.GetEmptyEstateResponse); + 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.GetMerchantResponse); TransactionDomainService transactionDomainService = - new TransactionDomainService(aggregateRepositoryManager.Object, estateClient.Object, securityServiceClient.Object); + new TransactionDomainService(aggregateRepositoryManager.Object, estateClient.Object, securityServiceClient.Object, + operatorProxyResolver); ProcessLogonTransactionResponse response = await transactionDomainService.ProcessLogonTransaction(TestData.TransactionId, TestData.EstateId, @@ -226,7 +244,7 @@ public async Task TransactionDomainService_ProcessLogonTransaction_InvalidEstate TestData.DeviceIdentifier1, CancellationToken.None); - this.ValidateLogonResponse(response, TransactionResponseCode.InvalidEstateId); + this.ValidateResponse(response, TransactionResponseCode.InvalidEstateId); } [Fact] @@ -250,6 +268,8 @@ public async Task TransactionDomainService_ProcessLogonTransaction_InvalidMercha 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.GetEstateResponse); @@ -257,7 +277,8 @@ public async Task TransactionDomainService_ProcessLogonTransaction_InvalidMercha .ReturnsAsync(TestData.GetEmptyMerchantResponse); TransactionDomainService transactionDomainService = - new TransactionDomainService(aggregateRepositoryManager.Object, estateClient.Object, securityServiceClient.Object); + new TransactionDomainService(aggregateRepositoryManager.Object, estateClient.Object, securityServiceClient.Object, + operatorProxyResolver); ProcessLogonTransactionResponse response = await transactionDomainService.ProcessLogonTransaction(TestData.TransactionId, TestData.EstateId, @@ -267,10 +288,288 @@ public async Task TransactionDomainService_ProcessLogonTransaction_InvalidMercha TestData.DeviceIdentifier1, CancellationToken.None); - this.ValidateLogonResponse(response, TransactionResponseCode.InvalidMerchantId); + this.ValidateResponse(response, TransactionResponseCode.InvalidMerchantId); + } + + [Fact] + public async Task TransactionDomainService_ProcessSaleTransaction_TransactionIsProcessed() + { + IConfigurationRoot configurationRoot = new ConfigurationBuilder().AddInMemoryCollection(TestData.DefaultAppSettings).Build(); + ConfigurationReader.Initialise(configurationRoot); + + Logger.Initialise(NullLogger.Instance); + + Mock aggregateRepositoryManager = new Mock(); + Mock> transactionAggregateRepository = new Mock>(); + + aggregateRepositoryManager.Setup(x => x.GetAggregateRepository(It.IsAny())).Returns(transactionAggregateRepository.Object); + transactionAggregateRepository.SetupSequence(t => t.GetLatestVersion(It.IsAny(), It.IsAny())) + .ReturnsAsync(TestData.GetEmptyTransactionAggregate) + .ReturnsAsync(TestData.GetStartedTransactionAggregate) + .ReturnsAsync(TestData.GetLocallyAuthorisedTransactionAggregate) + .ReturnsAsync(TestData.GetCompletedTransactionAggregate); + transactionAggregateRepository.Setup(t => t.SaveChanges(It.IsAny(), It.IsAny())).Returns(Task.CompletedTask); + + 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.GetEstateResponse); + estateClient.Setup(e => e.GetMerchant(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(TestData.GetMerchantResponse); + + Mock operatorProxy = new Mock(); + Func operatorProxyResolver = (operatorName) => { return operatorProxy.Object; }; + + TransactionDomainService transactionDomainService = + new TransactionDomainService(aggregateRepositoryManager.Object, estateClient.Object, securityServiceClient.Object, + operatorProxyResolver); + + ProcessSaleTransactionResponse response = await transactionDomainService.ProcessSaleTransaction(TestData.TransactionId, + TestData.EstateId, + TestData.MerchantId, + TestData.TransactionDateTime, + TestData.TransactionNumber, + TestData.DeviceIdentifier, + TestData.OperatorIdentifier, + TestData.AdditionalTransactionMetaData, + CancellationToken.None); + + this.ValidateResponse(response, TransactionResponseCode.Success); + } + + [Fact] + public async Task TransactionDomainService_ProcessSaleTransaction_MerchantWithNullDevices_TransactionIsProcessed() + { + IConfigurationRoot configurationRoot = new ConfigurationBuilder().AddInMemoryCollection(TestData.DefaultAppSettings).Build(); + ConfigurationReader.Initialise(configurationRoot); + + Logger.Initialise(NullLogger.Instance); + + Mock aggregateRepositoryManager = new Mock(); + Mock> transactionAggregateRepository = new Mock>(); + + aggregateRepositoryManager.Setup(x => x.GetAggregateRepository(It.IsAny())).Returns(transactionAggregateRepository.Object); + transactionAggregateRepository.SetupSequence(t => t.GetLatestVersion(It.IsAny(), It.IsAny())) + .ReturnsAsync(TestData.GetEmptyTransactionAggregate) + .ReturnsAsync(TestData.GetStartedTransactionAggregate) + .ReturnsAsync(TestData.GetLocallyDeclinedTransactionAggregate(TransactionResponseCode.NoValidDevices)) + .ReturnsAsync(TestData.GetCompletedTransactionAggregate); + transactionAggregateRepository.Setup(t => t.SaveChanges(It.IsAny(), It.IsAny())).Returns(Task.CompletedTask); + + 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.GetEstateResponse); + estateClient.Setup(e => e.GetMerchant(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(TestData.GetMerchantResponseWithNullDevices); + + TransactionDomainService transactionDomainService = + new TransactionDomainService(aggregateRepositoryManager.Object, estateClient.Object, securityServiceClient.Object, + operatorProxyResolver); + + ProcessSaleTransactionResponse response = await transactionDomainService.ProcessSaleTransaction(TestData.TransactionId, + TestData.EstateId, + TestData.MerchantId, + TestData.TransactionDateTime, + TestData.TransactionNumber, + TestData.DeviceIdentifier, + TestData.OperatorIdentifier, + TestData.AdditionalTransactionMetaData, + CancellationToken.None); + + this.ValidateResponse(response, TransactionResponseCode.NoValidDevices); + } + + [Fact] + public async Task TransactionDomainService_ProcessSaleTransaction_MerchantWithNoDevices_TransactionIsProcessed() + { + IConfigurationRoot configurationRoot = new ConfigurationBuilder().AddInMemoryCollection(TestData.DefaultAppSettings).Build(); + ConfigurationReader.Initialise(configurationRoot); + + Logger.Initialise(NullLogger.Instance); + + Mock aggregateRepositoryManager = new Mock(); + Mock> transactionAggregateRepository = new Mock>(); + + aggregateRepositoryManager.Setup(x => x.GetAggregateRepository(It.IsAny())).Returns(transactionAggregateRepository.Object); + transactionAggregateRepository.SetupSequence(t => t.GetLatestVersion(It.IsAny(), It.IsAny())) + .ReturnsAsync(TestData.GetEmptyTransactionAggregate) + .ReturnsAsync(TestData.GetStartedTransactionAggregate) + .ReturnsAsync(TestData.GetLocallyDeclinedTransactionAggregate(TransactionResponseCode.NoValidDevices)) + .ReturnsAsync(TestData.GetCompletedTransactionAggregate); + transactionAggregateRepository.Setup(t => t.SaveChanges(It.IsAny(), It.IsAny())).Returns(Task.CompletedTask); + + 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.GetEstateResponse); + estateClient.Setup(e => e.GetMerchant(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(TestData.GetMerchantResponseWithNoDevices); + + TransactionDomainService transactionDomainService = + new TransactionDomainService(aggregateRepositoryManager.Object, estateClient.Object, securityServiceClient.Object, + operatorProxyResolver); + + ProcessSaleTransactionResponse response = await transactionDomainService.ProcessSaleTransaction(TestData.TransactionId, + TestData.EstateId, + TestData.MerchantId, + TestData.TransactionDateTime, + TestData.TransactionNumber, + TestData.DeviceIdentifier, + TestData.OperatorIdentifier, + TestData.AdditionalTransactionMetaData, + CancellationToken.None); + + this.ValidateResponse(response, TransactionResponseCode.NoValidDevices); + } + + [Fact] + public async Task TransactionDomainService_ProcessSaleTransaction_IncorrectDevice_TransactionIsProcessed() + { + IConfigurationRoot configurationRoot = new ConfigurationBuilder().AddInMemoryCollection(TestData.DefaultAppSettings).Build(); + ConfigurationReader.Initialise(configurationRoot); + + Logger.Initialise(NullLogger.Instance); + + Mock aggregateRepositoryManager = new Mock(); + Mock> transactionAggregateRepository = new Mock>(); + + aggregateRepositoryManager.Setup(x => x.GetAggregateRepository(It.IsAny())).Returns(transactionAggregateRepository.Object); + transactionAggregateRepository.SetupSequence(t => t.GetLatestVersion(It.IsAny(), It.IsAny())) + .ReturnsAsync(TestData.GetEmptyTransactionAggregate) + .ReturnsAsync(TestData.GetStartedTransactionAggregate) + .ReturnsAsync(TestData.GetLocallyDeclinedTransactionAggregate(TransactionResponseCode.InvalidDeviceIdentifier)) + .ReturnsAsync(TestData.GetCompletedTransactionAggregate); + transactionAggregateRepository.Setup(t => t.SaveChanges(It.IsAny(), It.IsAny())).Returns(Task.CompletedTask); + + 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.GetEstateResponse); + estateClient.Setup(e => e.GetMerchant(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(TestData.GetMerchantResponse); + + TransactionDomainService transactionDomainService = + new TransactionDomainService(aggregateRepositoryManager.Object, estateClient.Object, securityServiceClient.Object, + operatorProxyResolver); + + ProcessSaleTransactionResponse response = await transactionDomainService.ProcessSaleTransaction(TestData.TransactionId, + TestData.EstateId, + TestData.MerchantId, + TestData.TransactionDateTime, + TestData.TransactionNumber, + TestData.DeviceIdentifier1, + TestData.OperatorIdentifier, + TestData.AdditionalTransactionMetaData, + CancellationToken.None); + + this.ValidateResponse(response, TransactionResponseCode.InvalidDeviceIdentifier); + } + + [Fact] + public async Task TransactionDomainService_ProcessSaleTransaction_InvalidEstate_TransactionIsProcessed() + { + IConfigurationRoot configurationRoot = new ConfigurationBuilder().AddInMemoryCollection(TestData.DefaultAppSettings).Build(); + ConfigurationReader.Initialise(configurationRoot); + + Logger.Initialise(NullLogger.Instance); + + Mock aggregateRepositoryManager = new Mock(); + Mock> transactionAggregateRepository = new Mock>(); + + aggregateRepositoryManager.Setup(x => x.GetAggregateRepository(It.IsAny())).Returns(transactionAggregateRepository.Object); + transactionAggregateRepository.SetupSequence(t => t.GetLatestVersion(It.IsAny(), It.IsAny())) + .ReturnsAsync(TestData.GetEmptyTransactionAggregate) + .ReturnsAsync(TestData.GetStartedTransactionAggregate) + .ReturnsAsync(TestData.GetLocallyDeclinedTransactionAggregate(TransactionResponseCode.InvalidEstateId)) + .ReturnsAsync(TestData.GetCompletedTransactionAggregate); + transactionAggregateRepository.Setup(t => t.SaveChanges(It.IsAny(), It.IsAny())).Returns(Task.CompletedTask); + + 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.GetMerchantResponse); + + TransactionDomainService transactionDomainService = + new TransactionDomainService(aggregateRepositoryManager.Object, estateClient.Object, securityServiceClient.Object, + operatorProxyResolver); + + ProcessSaleTransactionResponse response = await transactionDomainService.ProcessSaleTransaction(TestData.TransactionId, + TestData.EstateId, + TestData.MerchantId, + TestData.TransactionDateTime, + TestData.TransactionNumber, + TestData.DeviceIdentifier1, + TestData.OperatorIdentifier, + TestData.AdditionalTransactionMetaData, + CancellationToken.None); + + this.ValidateResponse(response, TransactionResponseCode.InvalidEstateId); } - private void ValidateLogonResponse(ProcessLogonTransactionResponse response, + [Fact] + public async Task TransactionDomainService_ProcessSaleTransaction_InvalidMerchant_TransactionIsProcessed() + { + IConfigurationRoot configurationRoot = new ConfigurationBuilder().AddInMemoryCollection(TestData.DefaultAppSettings).Build(); + ConfigurationReader.Initialise(configurationRoot); + + Logger.Initialise(NullLogger.Instance); + + Mock aggregateRepositoryManager = new Mock(); + Mock> transactionAggregateRepository = new Mock>(); + + aggregateRepositoryManager.Setup(x => x.GetAggregateRepository(It.IsAny())).Returns(transactionAggregateRepository.Object); + transactionAggregateRepository.SetupSequence(t => t.GetLatestVersion(It.IsAny(), It.IsAny())) + .ReturnsAsync(TestData.GetEmptyTransactionAggregate) + .ReturnsAsync(TestData.GetStartedTransactionAggregate) + .ReturnsAsync(TestData.GetLocallyDeclinedTransactionAggregate(TransactionResponseCode.InvalidMerchantId)) + .ReturnsAsync(TestData.GetCompletedTransactionAggregate); + transactionAggregateRepository.Setup(t => t.SaveChanges(It.IsAny(), It.IsAny())).Returns(Task.CompletedTask); + + 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.GetEstateResponse); + estateClient.Setup(e => e.GetMerchant(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(TestData.GetEmptyMerchantResponse); + + TransactionDomainService transactionDomainService = + new TransactionDomainService(aggregateRepositoryManager.Object, estateClient.Object, securityServiceClient.Object, + operatorProxyResolver); + + ProcessSaleTransactionResponse response = await transactionDomainService.ProcessSaleTransaction(TestData.TransactionId, + TestData.EstateId, + TestData.MerchantId, + TestData.TransactionDateTime, + TestData.TransactionNumber, + TestData.DeviceIdentifier1, + TestData.OperatorIdentifier, + TestData.AdditionalTransactionMetaData, + CancellationToken.None); + + this.ValidateResponse(response, TransactionResponseCode.InvalidMerchantId); + } + + private void ValidateResponse(ProcessLogonTransactionResponse response, TransactionResponseCode transactionResponseCode) { response.ShouldNotBeNull(); @@ -284,5 +583,20 @@ private void ValidateLogonResponse(ProcessLogonTransactionResponse response, response.ResponseMessage.ShouldBe(messageToValidate); } + + private void ValidateResponse(ProcessSaleTransactionResponse response, + TransactionResponseCode transactionResponseCode) + { + response.ShouldNotBeNull(); + response.ResponseCode.ShouldBe(TestData.GetResponseCodeAsString(transactionResponseCode)); + + String messageToValidate = TestData.GetResponseCodeMessage(transactionResponseCode); + if (transactionResponseCode == TransactionResponseCode.Success) + { + messageToValidate = messageToValidate.ToUpper(); + } + + response.ResponseMessage.ShouldBe(messageToValidate); + } } } diff --git a/TransactionProcessor.BusinessLogic.Tests/TransactionProcessor.BusinessLogic.Tests.csproj b/TransactionProcessor.BusinessLogic.Tests/TransactionProcessor.BusinessLogic.Tests.csproj index 57578ef1..d88ee50a 100644 --- a/TransactionProcessor.BusinessLogic.Tests/TransactionProcessor.BusinessLogic.Tests.csproj +++ b/TransactionProcessor.BusinessLogic.Tests/TransactionProcessor.BusinessLogic.Tests.csproj @@ -2,7 +2,7 @@ netcoreapp3.1 - Full + None false diff --git a/TransactionProcessor.BusinessLogic/OperatorInterfaces/ISafaricomProxy.cs b/TransactionProcessor.BusinessLogic/OperatorInterfaces/ISafaricomProxy.cs new file mode 100644 index 00000000..6f3eb4a5 --- /dev/null +++ b/TransactionProcessor.BusinessLogic/OperatorInterfaces/ISafaricomProxy.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace TransactionProcessor.BusinessLogic.OperatorInterfaces +{ + using System.Diagnostics.CodeAnalysis; + using System.Net; + using System.Threading; + using System.Threading.Tasks; + + public interface IOperatorProxy + { + Task ProcessMessage(CancellationToken cancellationToken); + } + + [ExcludeFromCodeCoverage] + public class SafaricomPinlessProxy : IOperatorProxy + { + public async Task ProcessMessage(CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + } + + /*http://{{linuxservername}}:8000/api/safaricom? + VENDOR=D-S136 - Fixed + &REQTYPE=EXRCTRFREQ - Fixed + &DATA= + + + EXRCTRFREQ - Fixed + 02-JUL-2018 - From Sales Transaction + SA - Fixed + 700945625 - Config + 0322 - Config + D-S136 - Config + @SafePay33 - Config + D-S136 - Config ? + 100022814 - From Sales Transaction + 0723581504 - From Sales Transaction + 65000 - From Sales Transaction + 0 - Fixed + 0 - Fixed + 1 - Fixed + */ +} diff --git a/TransactionProcessor.BusinessLogic/RequestHandlers/TransactionRequestHandler.cs b/TransactionProcessor.BusinessLogic/RequestHandlers/TransactionRequestHandler.cs index fbe26d73..0466071f 100644 --- a/TransactionProcessor.BusinessLogic/RequestHandlers/TransactionRequestHandler.cs +++ b/TransactionProcessor.BusinessLogic/RequestHandlers/TransactionRequestHandler.cs @@ -12,7 +12,8 @@ /// /// /// - public class TransactionRequestHandler : IRequestHandler + public class TransactionRequestHandler : IRequestHandler, + IRequestHandler { #region Fields @@ -34,6 +35,12 @@ public TransactionRequestHandler(ITransactionDomainService transactionDomainServ #region Methods + /// + /// Handles the specified request. + /// + /// The request. + /// The cancellation token. + /// public async Task Handle(ProcessLogonTransactionRequest request, CancellationToken cancellationToken) { @@ -50,5 +57,27 @@ await this.TransactionDomainService.ProcessLogonTransaction(request.TransactionI } #endregion + + /// + /// Handles the specified request. + /// + /// The request. + /// The cancellation token. + /// + 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.AdditionalTransactionMetadata, + cancellationToken); + + return saleResponse; + } } } \ No newline at end of file diff --git a/TransactionProcessor.BusinessLogic/Requests/ProcessSaleTransactionRequest.cs b/TransactionProcessor.BusinessLogic/Requests/ProcessSaleTransactionRequest.cs new file mode 100644 index 00000000..c661da07 --- /dev/null +++ b/TransactionProcessor.BusinessLogic/Requests/ProcessSaleTransactionRequest.cs @@ -0,0 +1,164 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace TransactionProcessor.BusinessLogic.Requests +{ + using MediatR; + using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + using Models; + + public class ProcessSaleTransactionRequest : IRequest + { + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The transaction identifier. + /// The estate identifier. + /// The merchant identifier. + /// The device identifier. + /// Type of the transaction. + /// The transaction date time. + /// The transaction number. + /// The operator identifier. + /// The additional transaction metadata. + private ProcessSaleTransactionRequest(Guid transactionId, + Guid estateId, + Guid merchantId, + String deviceIdentifier, + String transactionType, + DateTime transactionDateTime, + String transactionNumber, + String operatorIdentifier, + Dictionary additionalTransactionMetadata) + { + this.TransactionId = transactionId; + this.EstateId = estateId; + this.DeviceIdentifier = deviceIdentifier; + this.MerchantId = merchantId; + this.TransactionDateTime = transactionDateTime; + this.TransactionNumber = transactionNumber; + this.OperatorIdentifier = operatorIdentifier; + this.AdditionalTransactionMetadata = additionalTransactionMetadata; + this.TransactionType = transactionType; + } + + #endregion + + #region Properties + + /// + /// Gets the device identifier. + /// + /// + /// The device identifier. + /// + public String DeviceIdentifier { get; } + + /// + /// Gets the estate identifier. + /// + /// + /// The estate identifier. + /// + public Guid EstateId { get; } + + /// + /// Gets the merchant identifier. + /// + /// + /// The merchant identifier. + /// + public Guid MerchantId { get; } + + /// + /// Gets the transaction date time. + /// + /// + /// The transaction date time. + /// + public DateTime TransactionDateTime { get; } + + /// + /// Gets the transaction identifier. + /// + /// + /// The transaction identifier. + /// + public Guid TransactionId { get; } + + /// + /// Gets the transaction number. + /// + /// + /// The transaction number. + /// + public String TransactionNumber { get; } + + /// + /// Gets the type of the transaction. + /// + /// + /// The type of the transaction. + /// + public String TransactionType { get; } + + /// + /// Gets or sets the operator identifier. + /// + /// + /// The operator identifier. + /// + public String OperatorIdentifier { get; } + + /// + /// Gets or sets the additional transaction metadata. + /// + /// + /// The additional transaction metadata. + /// + public Dictionary AdditionalTransactionMetadata { get; } + + #endregion + + #region Methods + + /// + /// Creates the specified estate identifier. + /// + /// The transaction identifier. + /// The estate identifier. + /// The merchant identifier. + /// The device identifier. + /// Type of the transaction. + /// The transaction date time. + /// The transaction number. + /// The operator identifier. + /// The additional transaction metadata. + /// + public static ProcessSaleTransactionRequest Create(Guid transactionId, + Guid estateId, + Guid merchantId, + String deviceIdentifier, + String transactionType, + DateTime transactionDateTime, + String transactionNumber, + String operatorIdentifier, + Dictionary additionalTransactionMetadata) + { + return new ProcessSaleTransactionRequest(transactionId, + estateId, + merchantId, + deviceIdentifier, + transactionType, + transactionDateTime, + transactionNumber, + operatorIdentifier, + additionalTransactionMetadata); + } + + #endregion + } +} diff --git a/TransactionProcessor.BusinessLogic/Services/ITransactionDomainService.cs b/TransactionProcessor.BusinessLogic/Services/ITransactionDomainService.cs index 91a78f1b..649193ca 100644 --- a/TransactionProcessor.BusinessLogic/Services/ITransactionDomainService.cs +++ b/TransactionProcessor.BusinessLogic/Services/ITransactionDomainService.cs @@ -1,6 +1,7 @@ namespace TransactionProcessor.BusinessLogic.Services { using System; + using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Models; @@ -23,8 +24,36 @@ public interface ITransactionDomainService /// The device identifier. /// The cancellation token. /// - Task ProcessLogonTransaction(Guid transactionId, Guid estateId, Guid merchantId, DateTime transactionDateTime, - String transactionNumber, String deviceIdentifier, CancellationToken cancellationToken); + Task ProcessLogonTransaction(Guid transactionId, + Guid estateId, + Guid merchantId, + DateTime transactionDateTime, + String transactionNumber, + String deviceIdentifier, + CancellationToken cancellationToken); + + /// + /// Processes the sale transaction. + /// + /// The transaction identifier. + /// The estate identifier. + /// The merchant identifier. + /// The transaction date time. + /// The transaction number. + /// The device identifier. + /// The operator identifier. + /// The additional transaction metadata. + /// The cancellation token. + /// + Task ProcessSaleTransaction(Guid transactionId, + Guid estateId, + Guid merchantId, + DateTime transactionDateTime, + String transactionNumber, + String deviceIdentifier, + String operatorId, + Dictionary additionalTransactionMetadata, + CancellationToken cancellationToken); #endregion } diff --git a/TransactionProcessor.BusinessLogic/Services/TransactionDomainService.cs b/TransactionProcessor.BusinessLogic/Services/TransactionDomainService.cs index 54e42e64..c5c03e0a 100644 --- a/TransactionProcessor.BusinessLogic/Services/TransactionDomainService.cs +++ b/TransactionProcessor.BusinessLogic/Services/TransactionDomainService.cs @@ -9,6 +9,7 @@ using EstateManagement.DataTransferObjects.Requests; using EstateManagement.DataTransferObjects.Responses; using Models; + using OperatorInterfaces; using SecurityService.Client; using SecurityService.DataTransferObjects.Responses; using Shared.DomainDrivenDesign.EventStore; @@ -41,6 +42,8 @@ public class TransactionDomainService : ITransactionDomainService /// private readonly ISecurityServiceClient SecurityServiceClient; + private readonly Func OperatorProxyResolver; + #endregion #region Constructors @@ -51,13 +54,16 @@ public class TransactionDomainService : ITransactionDomainService /// The aggregate repository manager. /// The estate client. /// The security service client. + /// The operator proxy resolver. public TransactionDomainService(IAggregateRepositoryManager aggregateRepositoryManager, IEstateClient estateClient, - ISecurityServiceClient securityServiceClient) + ISecurityServiceClient securityServiceClient, + Func operatorProxyResolver) { this.AggregateRepositoryManager = aggregateRepositoryManager; this.EstateClient = estateClient; this.SecurityServiceClient = securityServiceClient; + this.OperatorProxyResolver = operatorProxyResolver; } #endregion @@ -83,14 +89,16 @@ public async Task ProcessLogonTransaction(Guid String deviceIdentifier, CancellationToken cancellationToken) { + TransactionType transactionType = TransactionType.Logon; + IAggregateRepository transactionAggregateRepository = this.AggregateRepositoryManager.GetAggregateRepository(estateId); TransactionAggregate transactionAggregate = await transactionAggregateRepository.GetLatestVersion(transactionId, cancellationToken); - transactionAggregate.StartTransaction(transactionDateTime, transactionNumber, "Logon", estateId, merchantId, deviceIdentifier); + transactionAggregate.StartTransaction(transactionDateTime, transactionNumber, transactionType, estateId, merchantId, deviceIdentifier); await transactionAggregateRepository.SaveChanges(transactionAggregate, cancellationToken); - (String responseMessage, TransactionResponseCode responseCode) validationResult = await this.ValidateTransaction(estateId, merchantId, deviceIdentifier, cancellationToken); + (String responseMessage, TransactionResponseCode responseCode) validationResult = await this.ValidateTransaction(estateId, merchantId, deviceIdentifier, transactionType, cancellationToken); if (validationResult.responseCode == TransactionResponseCode.Success) { @@ -121,18 +129,85 @@ public async Task ProcessLogonTransaction(Guid }; } + /// + /// Processes the sale transaction. + /// + /// The transaction identifier. + /// The estate identifier. + /// The merchant identifier. + /// The transaction date time. + /// The transaction number. + /// The device identifier. + /// The operator identifier. + /// The additional transaction metadata. + /// The cancellation token. + /// + public async Task ProcessSaleTransaction(Guid transactionId, + Guid estateId, + Guid merchantId, + DateTime transactionDateTime, + String transactionNumber, + String deviceIdentifier, + String operatorId, + Dictionary additionalTransactionMetadata, + CancellationToken cancellationToken) + { + TransactionType transactionType = TransactionType.Sale; + + IAggregateRepository transactionAggregateRepository = + this.AggregateRepositoryManager.GetAggregateRepository(estateId); + + TransactionAggregate transactionAggregate = await transactionAggregateRepository.GetLatestVersion(transactionId, cancellationToken); + transactionAggregate.StartTransaction(transactionDateTime, transactionNumber, transactionType, estateId, merchantId, deviceIdentifier); + await transactionAggregateRepository.SaveChanges(transactionAggregate, cancellationToken); + + (String responseMessage, TransactionResponseCode responseCode) validationResult = await this.ValidateTransaction(estateId, merchantId, deviceIdentifier, transactionType, cancellationToken); + + if (validationResult.responseCode == TransactionResponseCode.Success) + { + // TODO: Do the online processing with the operator here + // Record the successful validation + transactionAggregate = await transactionAggregateRepository.GetLatestVersion(transactionId, cancellationToken); + // TODO: Generate local authcode + transactionAggregate.AuthoriseTransactionLocally("ABCD1234", ((Int32)validationResult.responseCode).ToString().PadLeft(4, '0'), validationResult.responseMessage); + await transactionAggregateRepository.SaveChanges(transactionAggregate, cancellationToken); + } + else + { + // Record the failure + transactionAggregate = await transactionAggregateRepository.GetLatestVersion(transactionId, cancellationToken); + transactionAggregate.DeclineTransactionLocally(((Int32)validationResult.responseCode).ToString().PadLeft(4, '0'), validationResult.responseMessage); + await transactionAggregateRepository.SaveChanges(transactionAggregate, cancellationToken); + } + + transactionAggregate = await transactionAggregateRepository.GetLatestVersion(transactionId, cancellationToken); + transactionAggregate.CompleteTransaction(); + await transactionAggregateRepository.SaveChanges(transactionAggregate, cancellationToken); + + return new ProcessSaleTransactionResponse + { + ResponseMessage = transactionAggregate.ResponseMessage, + ResponseCode = transactionAggregate.ResponseCode, + EstateId = estateId, + MerchantId = merchantId, + AdditionalTransactionMetadata = new Dictionary() + }; + } + /// /// Validates the transaction. /// /// The estate identifier. /// The merchant identifier. /// The device identifier. + /// Type of the transaction. /// The cancellation token. /// /// Device Identifier {deviceIdentifier} not valid for Merchant {merchant.MerchantName} private async Task<(String responseMessage, TransactionResponseCode responseCode)> ValidateTransaction(Guid estateId, Guid merchantId, String deviceIdentifier, + TransactionType transactionType, CancellationToken cancellationToken) { try @@ -149,12 +224,12 @@ public async Task ProcessLogonTransaction(Guid Logger.LogInformation($"Token is {token.AccessToken}"); EstateResponse estate = null; + // Validate the Estate Record is a valid estate try { - // Validate the Estate Record is a valid estate estate = await this.EstateClient.GetEstate(token.AccessToken, estateId, cancellationToken); } - catch(Exception e) when (e.InnerException is KeyNotFoundException) + catch (Exception ex) when (ex.InnerException != null && ex.InnerException.GetType() == typeof(KeyNotFoundException)) { throw new TransactionValidationException($"Estate Id [{estateId}] is not a valid estate", TransactionResponseCode.InvalidEstateId); } @@ -166,20 +241,29 @@ public async Task ProcessLogonTransaction(Guid // TODO: Remove this once GetMerchant returns correct response when merchant not found if (merchant.MerchantName == null) { - throw new TransactionValidationException($"Merchant Id [{merchantId}] is not a valid merchant for estate [{estate.EstateName}]", TransactionResponseCode.InvalidMerchantId); + throw new TransactionValidationException($"Merchant Id [{merchantId}] is not a valid merchant for estate [{estate.EstateName}]", + TransactionResponseCode.InvalidMerchantId); } if (merchant.Devices == null || merchant.Devices.Any() == false) { - // Add the device to the merchant - await this.EstateClient.AddDeviceToMerchant(token.AccessToken, - estateId, - merchantId, - new AddMerchantDeviceRequest - { - DeviceIdentifier = deviceIdentifier - }, - cancellationToken); + if (transactionType == TransactionType.Logon) + { + // Add the device to the merchant + await this.EstateClient.AddDeviceToMerchant(token.AccessToken, + estateId, + merchantId, + new AddMerchantDeviceRequest + { + DeviceIdentifier = deviceIdentifier + }, + cancellationToken); + } + else + { + throw new TransactionValidationException($"Merchant {merchant.MerchantName} has no valid Devices for this transaction.", + TransactionResponseCode.NoValidDevices); + } } else { @@ -189,7 +273,8 @@ await this.EstateClient.AddDeviceToMerchant(token.AccessToken, if (device.Key == Guid.Empty) { // Device not found,throw error - throw new TransactionValidationException($"Device Identifier {deviceIdentifier} not valid for Merchant {merchant.MerchantName}", TransactionResponseCode.InvalidDeviceIdentifier); + throw new TransactionValidationException($"Device Identifier {deviceIdentifier} not valid for Merchant {merchant.MerchantName}", + TransactionResponseCode.InvalidDeviceIdentifier); } } @@ -200,6 +285,11 @@ await this.EstateClient.AddDeviceToMerchant(token.AccessToken, { return (tvex.Message, tvex.ResponseCode); } + catch (Exception ex) + { + Logger.LogError(ex); + return ("Unspecified Processing Error", TransactionResponseCode.UnknownFailure); + } } diff --git a/TransactionProcessor.BusinessLogic/Services/TransactionResponseCode.cs b/TransactionProcessor.BusinessLogic/Services/TransactionResponseCode.cs index ed8c098c..498ababb 100644 --- a/TransactionProcessor.BusinessLogic/Services/TransactionResponseCode.cs +++ b/TransactionProcessor.BusinessLogic/Services/TransactionResponseCode.cs @@ -3,9 +3,13 @@ public enum TransactionResponseCode { Success = 0, - + InvalidDeviceIdentifier = 1000, InvalidEstateId = 1001, - InvalidMerchantId = 1002 + InvalidMerchantId = 1002, + NoValidDevices = 1003, + + // A Catch All generic Error where reason has not been identified + UnknownFailure = 9999 } } \ No newline at end of file diff --git a/TransactionProcessor.DataTransferObjects/SaleTransactionRequest.cs b/TransactionProcessor.DataTransferObjects/SaleTransactionRequest.cs new file mode 100644 index 00000000..6497b4ba --- /dev/null +++ b/TransactionProcessor.DataTransferObjects/SaleTransactionRequest.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace TransactionProcessor.DataTransferObjects +{ + using System.Diagnostics.CodeAnalysis; + + [ExcludeFromCodeCoverage] + public class SaleTransactionRequest : DataTransferObject + { + /// + /// Gets or sets the device identifier. + /// + /// + /// The device identifier. + /// + public String DeviceIdentifier { get; set; } + + /// + /// Gets or sets the transaction date time. + /// + /// + /// The transaction date time. + /// + public DateTime TransactionDateTime { get; set; } + + /// + /// Gets or sets the transaction number. + /// + /// + /// The transaction number. + /// + public String TransactionNumber { get; set; } + + /// + /// Gets or sets the type of the transaction. + /// + /// + /// The type of the transaction. + /// + public String TransactionType { get; set; } + + /// + /// Gets or sets the operator identifier. + /// + /// + /// The operator identifier. + /// + public String OperatorIdentifier { get; set; } + + /// + /// Gets or sets the additional transaction metadata. + /// + /// + /// The additional transaction metadata. + /// + public Dictionary AdditionalTransactionMetadata { get; set; } + } +} diff --git a/TransactionProcessor.DataTransferObjects/SaleTransactionResponse.cs b/TransactionProcessor.DataTransferObjects/SaleTransactionResponse.cs new file mode 100644 index 00000000..00ab7658 --- /dev/null +++ b/TransactionProcessor.DataTransferObjects/SaleTransactionResponse.cs @@ -0,0 +1,54 @@ +namespace TransactionProcessor.DataTransferObjects +{ + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + + [ExcludeFromCodeCoverage] + public class SaleTransactionResponse + { + #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; } + + /// + /// Gets or sets the additional transaction metadata. + /// + /// + /// The additional transaction metadata. + /// + public Dictionary AdditionalTransactionMetadata { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/TransactionProcessor.IntegrationTests/LogonTransaction/LogonTransaction.feature b/TransactionProcessor.IntegrationTests/LogonTransaction/LogonTransaction.feature index 7f172d4f..9c9e9330 100644 --- a/TransactionProcessor.IntegrationTests/LogonTransaction/LogonTransaction.feature +++ b/TransactionProcessor.IntegrationTests/LogonTransaction/LogonTransaction.feature @@ -36,7 +36,7 @@ Background: | Test Operator 1 | Test Merchant 1 | 00000001 | 10000001 | Test Estate 1 | @PRTest -Scenario: Logon Transaction +Scenario: Logon Transactions When I perform the following transactions | DateTime | TransactionNumber | TransactionType | MerchantName | DeviceIdentifier | EstateName | diff --git a/TransactionProcessor.IntegrationTests/LogonTransaction/LogonTransaction.feature.cs b/TransactionProcessor.IntegrationTests/LogonTransaction/LogonTransaction.feature.cs index 14adeafa..d4f6a31a 100644 --- a/TransactionProcessor.IntegrationTests/LogonTransaction/LogonTransaction.feature.cs +++ b/TransactionProcessor.IntegrationTests/LogonTransaction/LogonTransaction.feature.cs @@ -212,15 +212,15 @@ void System.IDisposable.Dispose() this.TestTearDown(); } - [Xunit.SkippableFactAttribute(DisplayName="Logon Transaction")] + [Xunit.SkippableFactAttribute(DisplayName="Logon Transactions")] [Xunit.TraitAttribute("FeatureTitle", "LogonTransaction")] - [Xunit.TraitAttribute("Description", "Logon Transaction")] + [Xunit.TraitAttribute("Description", "Logon Transactions")] [Xunit.TraitAttribute("Category", "PRTest")] - public virtual void LogonTransaction() + public virtual void LogonTransactions() { string[] tagsOfScenario = new string[] { "PRTest"}; - TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Logon Transaction", null, new string[] { + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Logon Transactions", null, new string[] { "PRTest"}); #line 39 this.ScenarioInitialize(scenarioInfo); diff --git a/TransactionProcessor.IntegrationTests/SaleTransaction/SaleTransactionFeature.feature b/TransactionProcessor.IntegrationTests/SaleTransaction/SaleTransactionFeature.feature new file mode 100644 index 00000000..0857a168 --- /dev/null +++ b/TransactionProcessor.IntegrationTests/SaleTransaction/SaleTransactionFeature.feature @@ -0,0 +1,89 @@ +@base @shared +Feature: SaleTransaction + +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 | + + 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 | + + 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 | + +@PRTest +Scenario: Sale Transactions + + When I perform the following transactions + | DateTime | TransactionNumber | TransactionType | MerchantName | DeviceIdentifier | EstateName | OperatorName | TransactionAmount | + | Today | 1 | Sale | Test Merchant 1 | 123456780 | Test Estate 1 | Safaricom | 100.00 | + | Today | 2 | Sale | Test Merchant 2 | 123456781 | Test Estate 1 | Safaricom | 100.00 | + | Today | 3 | Sale | Test Merchant 3 | 123456782 | Test Estate 2 | Safaricom | 100.00 | + + Then transaction response should contain the following information + | EstateName | MerchantName | TransactionNumber | ResponseCode | ResponseMessage | + | Test Estate 1 | Test Merchant 1 | 1 | 0000 | SUCCESS | + | Test Estate 1 | Test Merchant 2 | 2 | 0000 | SUCCESS | + | Test Estate 2 | Test Merchant 3 | 3 | 0000 | SUCCESS | + +@PRTest +Scenario: Sale Transaction with Invalid Device + + When I perform the following transactions + | DateTime | TransactionNumber | TransactionType | MerchantName | DeviceIdentifier | EstateName | OperatorName | TransactionAmount | + | Today | 1 | Sale | Test Merchant 1 | 123456781 | Test Estate 1 | Safaricom | 100.00 | + + Then transaction response should contain the following information + | EstateName | MerchantName | TransactionNumber | ResponseCode | ResponseMessage | + | Test Estate 1 | Test Merchant 1 | 1 | 1000 | Device Identifier 123456781 not valid for Merchant Test Merchant 1 | + +Scenario: Sale Transaction with Invalid Estate + + When I perform the following transactions + | DateTime | TransactionNumber | TransactionType | MerchantName | DeviceIdentifier | EstateName |OperatorName |TransactionAmount | + | Today | 1 | Sale | Test Merchant 1 | 123456780 | InvalidEstate |Safaricom | 100.00 | + + Then transaction response should contain the following information + | EstateName | MerchantName | TransactionNumber | ResponseCode | ResponseMessage | + | InvalidEstate | Test Merchant 1 | 1 | 1001 | Estate Id [79902550-64df-4491-b0c1-4e78943928a3] is not a valid estate | + +Scenario: Sale Transaction with Invalid Merchant + + When I perform the following transactions + | DateTime | TransactionNumber | TransactionType | MerchantName | DeviceIdentifier | EstateName |OperatorName |TransactionAmount | + | Today | 1 | Sale | InvalidMerchant | 123456780 | Test Estate 1 |Safaricom | 100.00 | + + Then transaction response should contain the following information + | EstateName | MerchantName | TransactionNumber | ResponseCode | ResponseMessage | + | Test Estate 1 | InvalidMerchant | 1 | 1002 | Merchant Id [d59320fa-4c3e-4900-a999-483f6a10c69a] is not a valid merchant for estate [Test Estate 1] | + diff --git a/TransactionProcessor.IntegrationTests/SaleTransaction/SaleTransactionFeature.feature.cs b/TransactionProcessor.IntegrationTests/SaleTransaction/SaleTransactionFeature.feature.cs new file mode 100644 index 00000000..9f071a31 --- /dev/null +++ b/TransactionProcessor.IntegrationTests/SaleTransaction/SaleTransactionFeature.feature.cs @@ -0,0 +1,569 @@ +// ------------------------------------------------------------------------------ +// +// This code was generated by SpecFlow (http://www.specflow.org/). +// SpecFlow Version:3.1.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.SaleTransaction +{ + using TechTalk.SpecFlow; + using System; + using System.Linq; + + + [System.CodeDom.Compiler.GeneratedCodeAttribute("TechTalk.SpecFlow", "3.1.0.0")] + [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [Xunit.TraitAttribute("Category", "base")] + [Xunit.TraitAttribute("Category", "shared")] + public partial class SaleTransactionFeature : 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 "SaleTransactionFeature.feature" +#line hidden + + public SaleTransactionFeature(SaleTransactionFeature.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"), "SaleTransaction", 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"}); +#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[] { + "MerchantName", + "AddressLine1", + "Town", + "Region", + "Country", + "ContactName", + "EmailAddress", + "EstateName"}); + table27.AddRow(new string[] { + "Test Merchant 1", + "Address Line 1", + "TestTown", + "Test Region", + "United Kingdom", + "Test Contact 1", + "testcontact1@merchant1.co.uk", + "Test Estate 1"}); + table27.AddRow(new string[] { + "Test Merchant 2", + "Address Line 1", + "TestTown", + "Test Region", + "United Kingdom", + "Test Contact 2", + "testcontact2@merchant2.co.uk", + "Test Estate 1"}); + table27.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 28 + testRunner.Given("I create the following merchants", ((string)(null)), table27, "Given "); +#line hidden + TechTalk.SpecFlow.Table table28 = new TechTalk.SpecFlow.Table(new string[] { + "OperatorName", + "MerchantName", + "MerchantNumber", + "TerminalNumber", + "EstateName"}); + table28.AddRow(new string[] { + "Safaricom", + "Test Merchant 1", + "00000001", + "10000001", + "Test Estate 1"}); +#line 34 + testRunner.Given("I have assigned the following operator to the merchants", ((string)(null)), table28, "Given "); +#line hidden + TechTalk.SpecFlow.Table table29 = new TechTalk.SpecFlow.Table(new string[] { + "DeviceIdentifier", + "MerchantName", + "EstateName"}); + table29.AddRow(new string[] { + "123456780", + "Test Merchant 1", + "Test Estate 1"}); + table29.AddRow(new string[] { + "123456781", + "Test Merchant 2", + "Test Estate 1"}); + table29.AddRow(new string[] { + "123456782", + "Test Merchant 3", + "Test Estate 2"}); +#line 38 + testRunner.Given("I have assigned the following devices to the merchants", ((string)(null)), table29, "Given "); +#line hidden + } + + void System.IDisposable.Dispose() + { + this.TestTearDown(); + } + + [Xunit.SkippableFactAttribute(DisplayName="Sale Transactions")] + [Xunit.TraitAttribute("FeatureTitle", "SaleTransaction")] + [Xunit.TraitAttribute("Description", "Sale Transactions")] + [Xunit.TraitAttribute("Category", "PRTest")] + public virtual void SaleTransactions() + { + string[] tagsOfScenario = new string[] { + "PRTest"}; + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Sale Transactions", null, new string[] { + "PRTest"}); +#line 45 +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 table30 = new TechTalk.SpecFlow.Table(new string[] { + "DateTime", + "TransactionNumber", + "TransactionType", + "MerchantName", + "DeviceIdentifier", + "EstateName", + "OperatorName", + "TransactionAmount"}); + table30.AddRow(new string[] { + "Today", + "1", + "Sale", + "Test Merchant 1", + "123456780", + "Test Estate 1", + "Safaricom", + "100.00"}); + table30.AddRow(new string[] { + "Today", + "2", + "Sale", + "Test Merchant 2", + "123456781", + "Test Estate 1", + "Safaricom", + "100.00"}); + table30.AddRow(new string[] { + "Today", + "3", + "Sale", + "Test Merchant 3", + "123456782", + "Test Estate 2", + "Safaricom", + "100.00"}); +#line 47 + testRunner.When("I perform the following transactions", ((string)(null)), table30, "When "); +#line hidden + TechTalk.SpecFlow.Table table31 = new TechTalk.SpecFlow.Table(new string[] { + "EstateName", + "MerchantName", + "TransactionNumber", + "ResponseCode", + "ResponseMessage"}); + table31.AddRow(new string[] { + "Test Estate 1", + "Test Merchant 1", + "1", + "0000", + "SUCCESS"}); + table31.AddRow(new string[] { + "Test Estate 1", + "Test Merchant 2", + "2", + "0000", + "SUCCESS"}); + table31.AddRow(new string[] { + "Test Estate 2", + "Test Merchant 3", + "3", + "0000", + "SUCCESS"}); +#line 53 + testRunner.Then("transaction response should contain the following information", ((string)(null)), table31, "Then "); +#line hidden + } + this.ScenarioCleanup(); + } + + [Xunit.SkippableFactAttribute(DisplayName="Sale Transaction with Invalid Device")] + [Xunit.TraitAttribute("FeatureTitle", "SaleTransaction")] + [Xunit.TraitAttribute("Description", "Sale Transaction with Invalid Device")] + [Xunit.TraitAttribute("Category", "PRTest")] + public virtual void SaleTransactionWithInvalidDevice() + { + string[] tagsOfScenario = new string[] { + "PRTest"}; + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Sale Transaction with Invalid Device", null, new string[] { + "PRTest"}); +#line 60 +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 table32 = new TechTalk.SpecFlow.Table(new string[] { + "DateTime", + "TransactionNumber", + "TransactionType", + "MerchantName", + "DeviceIdentifier", + "EstateName", + "OperatorName", + "TransactionAmount"}); + table32.AddRow(new string[] { + "Today", + "1", + "Sale", + "Test Merchant 1", + "123456781", + "Test Estate 1", + "Safaricom", + "100.00"}); +#line 62 + testRunner.When("I perform the following transactions", ((string)(null)), table32, "When "); +#line hidden + TechTalk.SpecFlow.Table table33 = new TechTalk.SpecFlow.Table(new string[] { + "EstateName", + "MerchantName", + "TransactionNumber", + "ResponseCode", + "ResponseMessage"}); + table33.AddRow(new string[] { + "Test Estate 1", + "Test Merchant 1", + "1", + "1000", + "Device Identifier 123456781 not valid for Merchant Test Merchant 1"}); +#line 66 + testRunner.Then("transaction response should contain the following information", ((string)(null)), table33, "Then "); +#line hidden + } + this.ScenarioCleanup(); + } + + [Xunit.SkippableFactAttribute(DisplayName="Sale Transaction with Invalid Estate")] + [Xunit.TraitAttribute("FeatureTitle", "SaleTransaction")] + [Xunit.TraitAttribute("Description", "Sale Transaction with Invalid Estate")] + public virtual void SaleTransactionWithInvalidEstate() + { + string[] tagsOfScenario = ((string[])(null)); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Sale Transaction with Invalid Estate", null, ((string[])(null))); +#line 70 +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", + "TransactionNumber", + "TransactionType", + "MerchantName", + "DeviceIdentifier", + "EstateName", + "OperatorName", + "TransactionAmount"}); + table34.AddRow(new string[] { + "Today", + "1", + "Sale", + "Test Merchant 1", + "123456780", + "InvalidEstate", + "Safaricom", + "100.00"}); +#line 72 + testRunner.When("I perform the following transactions", ((string)(null)), table34, "When "); +#line hidden + TechTalk.SpecFlow.Table table35 = new TechTalk.SpecFlow.Table(new string[] { + "EstateName", + "MerchantName", + "TransactionNumber", + "ResponseCode", + "ResponseMessage"}); + table35.AddRow(new string[] { + "InvalidEstate", + "Test Merchant 1", + "1", + "1001", + "Estate Id [79902550-64df-4491-b0c1-4e78943928a3] is not a valid estate"}); +#line 76 + testRunner.Then("transaction response should contain the following information", ((string)(null)), table35, "Then "); +#line hidden + } + this.ScenarioCleanup(); + } + + [Xunit.SkippableFactAttribute(DisplayName="Sale Transaction with Invalid Merchant")] + [Xunit.TraitAttribute("FeatureTitle", "SaleTransaction")] + [Xunit.TraitAttribute("Description", "Sale Transaction with Invalid Merchant")] + public virtual void SaleTransactionWithInvalidMerchant() + { + string[] tagsOfScenario = ((string[])(null)); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Sale Transaction with Invalid Merchant", null, ((string[])(null))); +#line 80 +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 table36 = new TechTalk.SpecFlow.Table(new string[] { + "DateTime", + "TransactionNumber", + "TransactionType", + "MerchantName", + "DeviceIdentifier", + "EstateName", + "OperatorName", + "TransactionAmount"}); + table36.AddRow(new string[] { + "Today", + "1", + "Sale", + "InvalidMerchant", + "123456780", + "Test Estate 1", + "Safaricom", + "100.00"}); +#line 82 + testRunner.When("I perform the following transactions", ((string)(null)), table36, "When "); +#line hidden + TechTalk.SpecFlow.Table table37 = new TechTalk.SpecFlow.Table(new string[] { + "EstateName", + "MerchantName", + "TransactionNumber", + "ResponseCode", + "ResponseMessage"}); + table37.AddRow(new string[] { + "Test Estate 1", + "InvalidMerchant", + "1", + "1002", + "Merchant Id [d59320fa-4c3e-4900-a999-483f6a10c69a] is not a valid merchant for es" + + "tate [Test Estate 1]"}); +#line 86 + testRunner.Then("transaction response should contain the following information", ((string)(null)), table37, "Then "); +#line hidden + } + this.ScenarioCleanup(); + } + + [System.CodeDom.Compiler.GeneratedCodeAttribute("TechTalk.SpecFlow", "3.1.0.0")] + [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public class FixtureData : System.IDisposable + { + + public FixtureData() + { + SaleTransactionFeature.FeatureSetup(); + } + + void System.IDisposable.Dispose() + { + SaleTransactionFeature.FeatureTearDown(); + } + } + } +} +#pragma warning restore +#endregion diff --git a/TransactionProcessor.IntegrationTests/Shared/SharedSteps.cs b/TransactionProcessor.IntegrationTests/Shared/SharedSteps.cs index b79468e1..d79aca20 100644 --- a/TransactionProcessor.IntegrationTests/Shared/SharedSteps.cs +++ b/TransactionProcessor.IntegrationTests/Shared/SharedSteps.cs @@ -228,7 +228,7 @@ public async Task WhenIPerformTheFollowingTransactions(Table table) String transactionNumber = SpecflowTableHelper.GetStringRowValue(tableRow, "TransactionNumber"); String transactionType = SpecflowTableHelper.GetStringRowValue(tableRow, "TransactionType"); String deviceIdentifier = SpecflowTableHelper.GetStringRowValue(tableRow, "DeviceIdentifier"); - + EstateDetails estateDetails = this.TestingContext.GetEstateDetails(tableRow); // Lookup the merchant id @@ -245,6 +245,22 @@ public async Task WhenIPerformTheFollowingTransactions(Table table) deviceIdentifier, CancellationToken.None); break; + case "Sale": + + // Get specific sale fields + String operatorName = SpecflowTableHelper.GetStringRowValue(tableRow, "OperatorName"); + Decimal transactionAmount = SpecflowTableHelper.GetDecimalValue(tableRow, "TransactionAmount"); + + transactionResponse = await this.PerformSaleTransaction(estateDetails.EstateId, + merchantId, + transactionDateTime, + transactionType, + transactionNumber, + deviceIdentifier, + operatorName, + transactionAmount, + CancellationToken.None); + break; } @@ -252,6 +268,17 @@ public async Task WhenIPerformTheFollowingTransactions(Table table) } } + /// + /// Performs the logon transaction. + /// + /// The estate identifier. + /// The merchant identifier. + /// The transaction date time. + /// Type of the transaction. + /// The transaction number. + /// The device identifier. + /// The cancellation token. + /// private async Task PerformLogonTransaction(Guid estateId, Guid merchantId, DateTime transactionDateTime, String transactionType, String transactionNumber, String deviceIdentifier, CancellationToken cancellationToken) { LogonTransactionRequest logonTransactionRequest = new LogonTransactionRequest @@ -280,6 +307,39 @@ await this.TestingContext.DockerHelper.TransactionProcessorClient.PerformTransac return responseSerialisedMessage; } + private async Task PerformSaleTransaction(Guid estateId, Guid merchantId, DateTime transactionDateTime, String transactionType, String transactionNumber, String deviceIdentifier, String operatorIdentifier, Decimal transactionAmount, CancellationToken cancellationToken) + { + SaleTransactionRequest saleTransactionRequest = new SaleTransactionRequest + { + MerchantId = merchantId, + EstateId = estateId, + TransactionDateTime = transactionDateTime, + TransactionNumber = transactionNumber, + DeviceIdentifier = deviceIdentifier, + TransactionType = transactionType, + OperatorIdentifier = operatorIdentifier, + AdditionalTransactionMetadata = new Dictionary + { + { "Amount", transactionAmount.ToString() } + } + }; + + SerialisedMessage serialisedMessage = new SerialisedMessage(); + serialisedMessage.Metadata.Add(MetadataContants.KeyNameEstateId, estateId.ToString()); + serialisedMessage.Metadata.Add(MetadataContants.KeyNameMerchantId, merchantId.ToString()); + serialisedMessage.SerialisedData = JsonConvert.SerializeObject(saleTransactionRequest, 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) { @@ -312,6 +372,16 @@ private void ValidateTransactionResponse(LogonTransactionResponse logonTransacti logonTransactionResponse.ResponseMessage.ShouldBe(expectedResponseMessage); } + private void ValidateTransactionResponse(SaleTransactionResponse saleTransactionResponse, + TableRow tableRow) + { + String expectedResponseCode = SpecflowTableHelper.GetStringRowValue(tableRow, "ResponseCode"); + String expectedResponseMessage = SpecflowTableHelper.GetStringRowValue(tableRow, "ResponseMessage"); + + saleTransactionResponse.ResponseCode.ShouldBe(expectedResponseCode); + saleTransactionResponse.ResponseMessage.ShouldBe(expectedResponseMessage); + } + [Given(@"the following api resources exist")] public async Task GivenTheFollowingApiResourcesExist(Table table) { diff --git a/TransactionProcessor.IntegrationTests/TransactionProcessor.IntegrationTests.csproj b/TransactionProcessor.IntegrationTests/TransactionProcessor.IntegrationTests.csproj index 9d12ed35..318130e5 100644 --- a/TransactionProcessor.IntegrationTests/TransactionProcessor.IntegrationTests.csproj +++ b/TransactionProcessor.IntegrationTests/TransactionProcessor.IntegrationTests.csproj @@ -44,11 +44,20 @@ + + + Always + + + SpecFlowSingleFileGenerator LogonTransaction.feature.cs + + SpecFlowSingleFileGenerator + diff --git a/TransactionProcessor.IntegrationTests/nlog.config b/TransactionProcessor.IntegrationTests/nlog.config new file mode 100644 index 00000000..f94fa5fb --- /dev/null +++ b/TransactionProcessor.IntegrationTests/nlog.config @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TransactionProcessor.Models/ProcessLogonTransactionResponse.cs b/TransactionProcessor.Models/ProcessLogonTransactionResponse.cs index 174bd346..ca3c516f 100644 --- a/TransactionProcessor.Models/ProcessLogonTransactionResponse.cs +++ b/TransactionProcessor.Models/ProcessLogonTransactionResponse.cs @@ -27,8 +27,20 @@ public class ProcessLogonTransactionResponse /// 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 diff --git a/TransactionProcessor.Models/ProcessSaleTransactionResponse.cs b/TransactionProcessor.Models/ProcessSaleTransactionResponse.cs new file mode 100644 index 00000000..c65c7765 --- /dev/null +++ b/TransactionProcessor.Models/ProcessSaleTransactionResponse.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace TransactionProcessor.Models +{ + using System.Diagnostics.CodeAnalysis; + + [ExcludeFromCodeCoverage] + public class ProcessSaleTransactionResponse + { + #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; } + + /// + /// Gets or sets the additional transaction metadata. + /// + /// + /// The additional transaction metadata. + /// + public Dictionary AdditionalTransactionMetadata { get; set; } + + #endregion + } +} diff --git a/TransactionProcessor.Models/TransactionType.cs b/TransactionProcessor.Models/TransactionType.cs new file mode 100644 index 00000000..a91b1f5b --- /dev/null +++ b/TransactionProcessor.Models/TransactionType.cs @@ -0,0 +1,10 @@ +namespace TransactionProcessor.Models +{ + public enum TransactionType + { + Logon, + Sale, + Refund, + Reversal + } +} \ No newline at end of file diff --git a/TransactionProcessor.Testing/TestData.cs b/TransactionProcessor.Testing/TestData.cs index b9fc4432..c2103d38 100644 --- a/TransactionProcessor.Testing/TestData.cs +++ b/TransactionProcessor.Testing/TestData.cs @@ -16,7 +16,7 @@ public class TestData public static ProcessLogonTransactionResponse ProcessLogonTransactionResponseModel = new ProcessLogonTransactionResponse { ResponseMessage = TestData.ResponseMessage, - ResponseCode = TestData.ResponseCode + ResponseCode = TestData.ResponseCode, }; public static String ResponseMessage = "SUCCESS"; @@ -32,7 +32,7 @@ public class TestData public static Guid TransactionId = Guid.Parse("AE89B2F6-307B-46F4-A8E7-CEF27097D766"); public static ProcessLogonTransactionRequest ProcessLogonTransactionRequest = ProcessLogonTransactionRequest.Create( TestData.TransactionId, TestData.EstateId, TestData.MerchantId, - TestData.DeviceIdentifier, TestData.TransactionType, + TestData.DeviceIdentifier, TestData.TransactionTypeLogon.ToString(), TestData.TransactionDateTime, TestData.TransactionNumber); @@ -40,7 +40,8 @@ public class TestData public static String DeviceIdentifier1 = "1234567891"; - public static String TransactionType = "Logon"; + public static TransactionType TransactionTypeLogon = TransactionType.Logon; + public static TransactionType TransactionTypeSale = TransactionType.Sale; public static DateTime TransactionDateTime = DateTime.Now; @@ -59,7 +60,7 @@ public static TransactionAggregate GetStartedTransactionAggregate() { TransactionAggregate transactionAggregate = TransactionAggregate.Create(TestData.TransactionId); - transactionAggregate.StartTransaction(TestData.TransactionDateTime,TestData.TransactionNumber, TestData.TransactionType, TestData.EstateId, TestData.MerchantId, + transactionAggregate.StartTransaction(TestData.TransactionDateTime,TestData.TransactionNumber, TestData.TransactionTypeLogon, TestData.EstateId, TestData.MerchantId, TestData.DeviceIdentifier); return transactionAggregate; @@ -69,7 +70,7 @@ public static TransactionAggregate GetLocallyAuthorisedTransactionAggregate() { TransactionAggregate transactionAggregate = TransactionAggregate.Create(TestData.TransactionId); - transactionAggregate.StartTransaction(TestData.TransactionDateTime, TestData.TransactionNumber, TestData.TransactionType, TestData.EstateId, TestData.MerchantId, + transactionAggregate.StartTransaction(TestData.TransactionDateTime, TestData.TransactionNumber, TestData.TransactionTypeLogon, TestData.EstateId, TestData.MerchantId, TestData.DeviceIdentifier); transactionAggregate.AuthoriseTransactionLocally(TestData.AuthorisationCode, TestData.ResponseCode, TestData.ResponseMessage); @@ -81,7 +82,7 @@ public static TransactionAggregate GetLocallyDeclinedTransactionAggregate(Transa { TransactionAggregate transactionAggregate = TransactionAggregate.Create(TestData.TransactionId); - transactionAggregate.StartTransaction(TestData.TransactionDateTime, TestData.TransactionNumber, TestData.TransactionType, TestData.EstateId, TestData.MerchantId, + transactionAggregate.StartTransaction(TestData.TransactionDateTime, TestData.TransactionNumber, TestData.TransactionTypeLogon, TestData.EstateId, TestData.MerchantId, TestData.DeviceIdentifier); transactionAggregate.DeclineTransactionLocally(TestData.GetResponseCodeAsString(transactionResponseCode), TestData.GetResponseCodeMessage(transactionResponseCode)); @@ -93,7 +94,7 @@ public static TransactionAggregate GetCompletedTransactionAggregate() { TransactionAggregate transactionAggregate = TransactionAggregate.Create(TestData.TransactionId); - transactionAggregate.StartTransaction(TestData.TransactionDateTime, TestData.TransactionNumber, TestData.TransactionType, TestData.EstateId, TestData.MerchantId, + transactionAggregate.StartTransaction(TestData.TransactionDateTime, TestData.TransactionNumber, TestData.TransactionTypeLogon, TestData.EstateId, TestData.MerchantId, TestData.DeviceIdentifier); transactionAggregate.AuthoriseTransactionLocally(TestData.AuthorisationCode, TestData.ResponseCode, TestData.ResponseMessage); @@ -163,7 +164,29 @@ public static TokenResponse TokenResponse() MerchantName = null }; - + public static ProcessSaleTransactionResponse ProcessSaleTransactionResponseModel = new ProcessSaleTransactionResponse + { + ResponseMessage = TestData.ResponseMessage, + ResponseCode = TestData.ResponseCode, + AdditionalTransactionMetadata = new Dictionary + { + {"OperatorResponseCode", "1000"} + } + }; + + public static String OperatorIdentifier = "Safaricom"; + + public static Dictionary AdditionalTransactionMetaData = new Dictionary + { + {"Amount", "100.00"} + }; + + public static ProcessSaleTransactionRequest ProcessSaleTransactionRequest = ProcessSaleTransactionRequest.Create(TestData.TransactionId, TestData.EstateId, TestData.MerchantId, + TestData.DeviceIdentifier, TestData.TransactionTypeLogon.ToString(), + TestData.TransactionDateTime, + TestData.TransactionNumber, + TestData.OperatorIdentifier, + TestData.AdditionalTransactionMetaData); public static String GetResponseCodeAsString(TransactionResponseCode transactionResponseCode) { diff --git a/TransactionProcessor.Tests/Factories/ModelFactoryTests.cs b/TransactionProcessor.Tests/Factories/ModelFactoryTests.cs index 3b0e4a1a..0f919242 100644 --- a/TransactionProcessor.Tests/Factories/ModelFactoryTests.cs +++ b/TransactionProcessor.Tests/Factories/ModelFactoryTests.cs @@ -42,5 +42,35 @@ public void ModelFactory_ProcessLogonTransactionResponseModel_NullInput_IsConver logonTransactionResponse.ShouldBeNull(); } + + [Fact] + public void ModelFactory_ProcessSaleTransactionResponseModel_IsConverted() + { + ProcessSaleTransactionResponse processSaleTransactionResponseModel = TestData.ProcessSaleTransactionResponseModel; + + ModelFactory modelFactory = new ModelFactory(); + + SerialisedMessage saleTransactionResponse = modelFactory.ConvertFrom(processSaleTransactionResponseModel); + + saleTransactionResponse.ShouldNotBeNull(); + saleTransactionResponse.Metadata.ShouldContainKey(MetadataContants.KeyNameEstateId); + saleTransactionResponse.Metadata.ShouldContainKey(MetadataContants.KeyNameMerchantId); + String estateId = saleTransactionResponse.Metadata[MetadataContants.KeyNameEstateId]; + String merchantId = saleTransactionResponse.Metadata[MetadataContants.KeyNameMerchantId]; + estateId.ShouldBe(TestData.ProcessSaleTransactionResponseModel.EstateId.ToString()); + merchantId.ShouldBe(TestData.ProcessSaleTransactionResponseModel.MerchantId.ToString()); + } + + [Fact] + public void ModelFactory_ProcessSaleTransactionResponseModel_NullInput_IsConverted() + { + ProcessSaleTransactionResponse processSaleTransactionResponseModel = null; + + ModelFactory modelFactory = new ModelFactory(); + + SerialisedMessage saleTransactionResponse = modelFactory.ConvertFrom(processSaleTransactionResponseModel); + + saleTransactionResponse.ShouldBeNull(); + } } } diff --git a/TransactionProcessor.TransactionAggregate.Tests/DomainEventTests.cs b/TransactionProcessor.TransactionAggregate.Tests/DomainEventTests.cs index b437bdd9..1e68d9b7 100644 --- a/TransactionProcessor.TransactionAggregate.Tests/DomainEventTests.cs +++ b/TransactionProcessor.TransactionAggregate.Tests/DomainEventTests.cs @@ -4,6 +4,7 @@ namespace TransactionProcessor.TransactionAggregate.Tests { + using Models; using Shouldly; using Testing; using Transaction.DomainEvents; @@ -11,15 +12,17 @@ namespace TransactionProcessor.TransactionAggregate.Tests public class DomainEventTests { - [Fact] - public void TransactionHasStartedEvent_CanBeCreated_IsCreated() + [Theory] + [InlineData(TransactionType.Logon)] + [InlineData(TransactionType.Sale)] + public void TransactionHasStartedEvent_CanBeCreated_IsCreated(TransactionType transactionType) { TransactionHasStartedEvent transactionHasStartedEvent = TransactionHasStartedEvent.Create(TestData.TransactionId, TestData.EstateId, TestData.MerchantId, TestData.TransactionDateTime, TestData.TransactionNumber, - TestData.TransactionType, + transactionType.ToString(), TestData.DeviceIdentifier); transactionHasStartedEvent.ShouldNotBeNull(); transactionHasStartedEvent.AggregateId.ShouldBe(TestData.TransactionId); @@ -31,7 +34,7 @@ public void TransactionHasStartedEvent_CanBeCreated_IsCreated() transactionHasStartedEvent.MerchantId.ShouldBe(TestData.MerchantId); transactionHasStartedEvent.TransactionDateTime.ShouldBe(TestData.TransactionDateTime); transactionHasStartedEvent.TransactionNumber.ShouldBe(TestData.TransactionNumber); - transactionHasStartedEvent.TransactionType.ShouldBe(TestData.TransactionType); + transactionHasStartedEvent.TransactionType.ShouldBe(transactionType.ToString()); } [Fact] diff --git a/TransactionProcessor.TransactionAggregate.Tests/TransactionAggregateTests.cs b/TransactionProcessor.TransactionAggregate.Tests/TransactionAggregateTests.cs index f6bbac18..c38a46d1 100644 --- a/TransactionProcessor.TransactionAggregate.Tests/TransactionAggregateTests.cs +++ b/TransactionProcessor.TransactionAggregate.Tests/TransactionAggregateTests.cs @@ -4,6 +4,7 @@ namespace TransactionProcessor.TransactionAggregate.Tests { + using Models; using Shouldly; public class TransactionAggregateTests @@ -16,43 +17,49 @@ public void TransactionAggregate_CanBeCreated_IsCreated() aggregate.AggregateId.ShouldBe(TestData.TransactionId); } - [Fact] - public void TransactionAggregate_StartTransaction_TransactionIsStarted() + [Theory] + [InlineData(TransactionType.Logon)] + [InlineData(TransactionType.Sale)] + public void TransactionAggregate_StartTransaction_TransactionIsStarted(TransactionType transactionType) { TransactionAggregate transactionAggregate = TransactionAggregate.Create(TestData.TransactionId); - transactionAggregate.StartTransaction(TestData.TransactionDateTime, TestData.TransactionNumber, TestData.TransactionType, TestData.EstateId, TestData.MerchantId, TestData.DeviceIdentifier); + transactionAggregate.StartTransaction(TestData.TransactionDateTime, TestData.TransactionNumber, transactionType, TestData.EstateId, TestData.MerchantId, TestData.DeviceIdentifier); transactionAggregate.IsStarted.ShouldBeTrue(); transactionAggregate.TransactionDateTime.ShouldBe(TestData.TransactionDateTime); transactionAggregate.TransactionNumber.ShouldBe(TestData.TransactionNumber); - transactionAggregate.TransactionType.ShouldBe(TestData.TransactionType); + transactionAggregate.TransactionType.ShouldBe(transactionType); transactionAggregate.EstateId.ShouldBe(TestData.EstateId); transactionAggregate.MerchantId.ShouldBe(TestData.MerchantId); transactionAggregate.DeviceIdentifier.ShouldBe(TestData.DeviceIdentifier); } - [Fact] - public void TransactionAggregate_StartTransaction_TransactionAlreadyStarted_ErrorThrown() + [Theory] + [InlineData(TransactionType.Logon)] + [InlineData(TransactionType.Sale)] + public void TransactionAggregate_StartTransaction_TransactionAlreadyStarted_ErrorThrown(TransactionType transactionType) { TransactionAggregate transactionAggregate = TransactionAggregate.Create(TestData.TransactionId); - transactionAggregate.StartTransaction(TestData.TransactionDateTime, TestData.TransactionNumber, TestData.TransactionType, TestData.EstateId, TestData.MerchantId, TestData.DeviceIdentifier); + transactionAggregate.StartTransaction(TestData.TransactionDateTime, TestData.TransactionNumber, transactionType, TestData.EstateId, TestData.MerchantId, TestData.DeviceIdentifier); Should.Throw(() => { transactionAggregate.StartTransaction(TestData.TransactionDateTime, TestData.TransactionNumber, - TestData.TransactionType, + transactionType, TestData.EstateId, TestData.MerchantId, TestData.DeviceIdentifier); }); } - [Fact] - public void TransactionAggregate_StartTransaction_TransactionAlreadyCompleted_ErrorThrown() + [Theory] + [InlineData(TransactionType.Logon)] + [InlineData(TransactionType.Sale)] + public void TransactionAggregate_StartTransaction_TransactionAlreadyCompleted_ErrorThrown(TransactionType transactionType) { TransactionAggregate transactionAggregate = TransactionAggregate.Create(TestData.TransactionId); - transactionAggregate.StartTransaction(TestData.TransactionDateTime, TestData.TransactionNumber, TestData.TransactionType, TestData.EstateId, TestData.MerchantId, TestData.DeviceIdentifier); + transactionAggregate.StartTransaction(TestData.TransactionDateTime, TestData.TransactionNumber, transactionType, TestData.EstateId, TestData.MerchantId, TestData.DeviceIdentifier); transactionAggregate.AuthoriseTransactionLocally(TestData.AuthorisationCode, TestData.ResponseCode, TestData.ResponseMessage); transactionAggregate.CompleteTransaction(); @@ -60,7 +67,7 @@ public void TransactionAggregate_StartTransaction_TransactionAlreadyCompleted_Er { transactionAggregate.StartTransaction(TestData.TransactionDateTime, TestData.TransactionNumber, - TestData.TransactionType, + transactionType, TestData.EstateId, TestData.MerchantId, TestData.DeviceIdentifier); @@ -68,19 +75,26 @@ public void TransactionAggregate_StartTransaction_TransactionAlreadyCompleted_Er } [Theory] - [InlineData(false, "0001", "Logon", true, true, "A1234567890" )] - [InlineData(true, "", "Logon", true, true, "A1234567890")] - [InlineData(true, null, "Logon", true, true, "A1234567890")] - [InlineData(true, "ABCD", "Logon", true, true, "A1234567890")] - [InlineData(true, "0001", "", true, true, "A1234567890")] - [InlineData(true, "0001", null, true, true, "A1234567890")] - [InlineData(true, "0001", "Invalid", true, true, "A1234567890")] - [InlineData(true, "0001", "Logon", false, true, "A1234567890")] - [InlineData(true, "0001", "Logon", true, false, "A1234567890")] - [InlineData(true, "0001", "Logon", true, true, "")] - [InlineData(true, "0001", "Logon", true, true, null)] - [InlineData(true, "0001", "Logon", true, true, "A!234567890")] - public void TransactionAggregate_StartTransaction_InvalidData_ErrorThrown(Boolean validTransactionDateTime, String transactionNumber, String transactionType, Boolean validEstateId, Boolean validMerchantId, String deviceIdentifier) + [InlineData(false, "0001", TransactionType.Logon, true, true, "A1234567890" )] + [InlineData(true, "", TransactionType.Logon, true, true, "A1234567890")] + [InlineData(true, null, TransactionType.Logon, true, true, "A1234567890")] + [InlineData(true, "ABCD", TransactionType.Logon, true, true, "A1234567890")] + [InlineData(true, "0001", TransactionType.Logon, false, true, "A1234567890")] + [InlineData(true, "0001", TransactionType.Logon, true, false, "A1234567890")] + [InlineData(true, "0001", TransactionType.Logon, true, true, "")] + [InlineData(true, "0001", TransactionType.Logon, true, true, null)] + [InlineData(true, "0001", TransactionType.Logon, true, true, "A!234567890")] + [InlineData(true, "0001", (TransactionType)99, true, true, "A1234567890")] + [InlineData(false, "0001", TransactionType.Sale, true, true, "A1234567890")] + [InlineData(true, "", TransactionType.Sale, true, true, "A1234567890")] + [InlineData(true, null, TransactionType.Sale, true, true, "A1234567890")] + [InlineData(true, "ABCD", TransactionType.Sale, true, true, "A1234567890")] + [InlineData(true, "0001", TransactionType.Sale, false, true, "A1234567890")] + [InlineData(true, "0001", TransactionType.Sale, true, false, "A1234567890")] + [InlineData(true, "0001", TransactionType.Sale, true, true, "")] + [InlineData(true, "0001", TransactionType.Sale, true, true, null)] + [InlineData(true, "0001", TransactionType.Sale, true, true, "A!234567890")] + public void TransactionAggregate_StartTransaction_InvalidData_ErrorThrown(Boolean validTransactionDateTime, String transactionNumber, TransactionType transactionType, Boolean validEstateId, Boolean validMerchantId, String deviceIdentifier) { DateTime transactionDateTime = validTransactionDateTime ? TestData.TransactionDateTime : DateTime.MinValue; Guid estateId = validEstateId ? TestData.EstateId : Guid.Empty; @@ -99,11 +113,12 @@ public void TransactionAggregate_StartTransaction_InvalidData_ErrorThrown(Boolea }); } - [Fact] - public void TransactionAggregate_AuthoriseTransactionLocally_TransactionIsAuthorised() + [Theory] + [InlineData(TransactionType.Logon)] + public void TransactionAggregate_AuthoriseTransactionLocally_TransactionIsAuthorised(TransactionType transactionType) { TransactionAggregate transactionAggregate = TransactionAggregate.Create(TestData.TransactionId); - transactionAggregate.StartTransaction(TestData.TransactionDateTime, TestData.TransactionNumber, TestData.TransactionType, TestData.EstateId, TestData.MerchantId, TestData.DeviceIdentifier); + transactionAggregate.StartTransaction(TestData.TransactionDateTime, TestData.TransactionNumber, transactionType, TestData.EstateId, TestData.MerchantId, TestData.DeviceIdentifier); transactionAggregate.AuthoriseTransactionLocally(TestData.AuthorisationCode, TestData.ResponseCode, TestData.ResponseMessage); @@ -127,11 +142,12 @@ public void TransactionAggregate_AuthoriseTransactionLocally_TransactionNotStart }); } - [Fact] - public void TransactionAggregate_AuthoriseTransactionLocally_TransactionAlreadyAuthorised_ErrorThrown() + [Theory] + [InlineData(TransactionType.Logon)] + public void TransactionAggregate_AuthoriseTransactionLocally_TransactionAlreadyAuthorised_ErrorThrown(TransactionType transactionType) { TransactionAggregate transactionAggregate = TransactionAggregate.Create(TestData.TransactionId); - transactionAggregate.StartTransaction(TestData.TransactionDateTime, TestData.TransactionNumber, TestData.TransactionType, TestData.EstateId, TestData.MerchantId, TestData.DeviceIdentifier); + transactionAggregate.StartTransaction(TestData.TransactionDateTime, TestData.TransactionNumber, transactionType, TestData.EstateId, TestData.MerchantId, TestData.DeviceIdentifier); transactionAggregate.AuthoriseTransactionLocally(TestData.AuthorisationCode, TestData.ResponseCode, TestData.ResponseMessage); Should.Throw(() => @@ -142,11 +158,13 @@ public void TransactionAggregate_AuthoriseTransactionLocally_TransactionAlreadyA }); } - [Fact] - public void TransactionAggregate_DeclineTransactionLocally_TransactionIsDeclined() + [Theory] + [InlineData(TransactionType.Logon)] + [InlineData(TransactionType.Sale)] + public void TransactionAggregate_DeclineTransactionLocally_TransactionIsDeclined(TransactionType transactionType) { TransactionAggregate transactionAggregate = TransactionAggregate.Create(TestData.TransactionId); - transactionAggregate.StartTransaction(TestData.TransactionDateTime, TestData.TransactionNumber, TestData.TransactionType, TestData.EstateId, TestData.MerchantId, TestData.DeviceIdentifier); + transactionAggregate.StartTransaction(TestData.TransactionDateTime, TestData.TransactionNumber, transactionType, TestData.EstateId, TestData.MerchantId, TestData.DeviceIdentifier); transactionAggregate.DeclineTransactionLocally(TestData.DeclinedResponseCode, TestData.DeclinedResponseMessage); @@ -170,11 +188,13 @@ public void TransactionAggregate_DeclineTransactionLocally_TransactionNotStarted }); } - [Fact] - public void TransactionAggregate_DeclineTransactionLocally_TransactionAlreadyAuthorised_ErrorThrown() + [Theory] + [InlineData(TransactionType.Logon)] + [InlineData(TransactionType.Sale)] + public void TransactionAggregate_DeclineTransactionLocally_TransactionAlreadyAuthorised_ErrorThrown(TransactionType transactionType) { TransactionAggregate transactionAggregate = TransactionAggregate.Create(TestData.TransactionId); - transactionAggregate.StartTransaction(TestData.TransactionDateTime, TestData.TransactionNumber, TestData.TransactionType, TestData.EstateId, TestData.MerchantId, TestData.DeviceIdentifier); + transactionAggregate.StartTransaction(TestData.TransactionDateTime, TestData.TransactionNumber, transactionType, TestData.EstateId, TestData.MerchantId, TestData.DeviceIdentifier); transactionAggregate.AuthoriseTransactionLocally(TestData.AuthorisationCode, TestData.ResponseCode, TestData.ResponseMessage); Should.Throw(() => @@ -183,11 +203,13 @@ public void TransactionAggregate_DeclineTransactionLocally_TransactionAlreadyAut }); } - [Fact] - public void TransactionAggregate_DeclineTransactionLocally_TransactionAlreadyDeclined_ErrorThrown() + [Theory] + [InlineData(TransactionType.Logon)] + [InlineData(TransactionType.Sale)] + public void TransactionAggregate_DeclineTransactionLocally_TransactionAlreadyDeclined_ErrorThrown(TransactionType transactionType) { TransactionAggregate transactionAggregate = TransactionAggregate.Create(TestData.TransactionId); - transactionAggregate.StartTransaction(TestData.TransactionDateTime, TestData.TransactionNumber, TestData.TransactionType, TestData.EstateId, TestData.MerchantId, TestData.DeviceIdentifier); + transactionAggregate.StartTransaction(TestData.TransactionDateTime, TestData.TransactionNumber, transactionType, TestData.EstateId, TestData.MerchantId, TestData.DeviceIdentifier); transactionAggregate.DeclineTransactionLocally(TestData.ResponseCode, TestData.ResponseMessage); Should.Throw(() => @@ -196,11 +218,13 @@ public void TransactionAggregate_DeclineTransactionLocally_TransactionAlreadyDec }); } - [Fact] - public void TransactionAggregate_CompleteTransaction_TransactionIsCompleted() + [Theory] + [InlineData(TransactionType.Logon)] + [InlineData(TransactionType.Sale)] + public void TransactionAggregate_CompleteTransaction_TransactionIsCompleted(TransactionType transactionType) { TransactionAggregate transactionAggregate = TransactionAggregate.Create(TestData.TransactionId); - transactionAggregate.StartTransaction(TestData.TransactionDateTime, TestData.TransactionNumber, TestData.TransactionType, TestData.EstateId, TestData.MerchantId, TestData.DeviceIdentifier); + transactionAggregate.StartTransaction(TestData.TransactionDateTime, TestData.TransactionNumber, transactionType, TestData.EstateId, TestData.MerchantId, TestData.DeviceIdentifier); transactionAggregate.AuthoriseTransactionLocally(TestData.AuthorisationCode, TestData.ResponseCode, TestData.ResponseMessage); transactionAggregate.CompleteTransaction(); @@ -219,22 +243,27 @@ public void TransactionAggregate_CompleteTransaction_TransactionNotStarted_Error }); } - [Fact] - public void TransactionAggregate_CompleteTransaction_TransactionNotAuthorised_ErrorThrown() + [Theory] + [InlineData(TransactionType.Logon)] + [InlineData(TransactionType.Sale)] + public void TransactionAggregate_CompleteTransaction_TransactionNotAuthorised_ErrorThrown(TransactionType transactionType) { TransactionAggregate transactionAggregate = TransactionAggregate.Create(TestData.TransactionId); - transactionAggregate.StartTransaction(TestData.TransactionDateTime, TestData.TransactionNumber, TestData.TransactionType, TestData.EstateId, TestData.MerchantId, TestData.DeviceIdentifier); + transactionAggregate.StartTransaction(TestData.TransactionDateTime, TestData.TransactionNumber, transactionType, TestData.EstateId, TestData.MerchantId, TestData.DeviceIdentifier); Should.Throw(() => { transactionAggregate.CompleteTransaction(); }); } - [Fact] - public void TransactionAggregate_CompleteTransaction_TransactionAlreadyCompleted_ErrorThrown() + + [Theory] + [InlineData(TransactionType.Logon)] + [InlineData(TransactionType.Sale)] + public void TransactionAggregate_CompleteTransaction_TransactionAlreadyCompleted_ErrorThrown(TransactionType transactionType) { TransactionAggregate transactionAggregate = TransactionAggregate.Create(TestData.TransactionId); - transactionAggregate.StartTransaction(TestData.TransactionDateTime, TestData.TransactionNumber, TestData.TransactionType, TestData.EstateId, TestData.MerchantId, TestData.DeviceIdentifier); + transactionAggregate.StartTransaction(TestData.TransactionDateTime, TestData.TransactionNumber, transactionType, TestData.EstateId, TestData.MerchantId, TestData.DeviceIdentifier); transactionAggregate.AuthoriseTransactionLocally(TestData.AuthorisationCode, TestData.ResponseCode, TestData.ResponseMessage); transactionAggregate.CompleteTransaction(); diff --git a/TransactionProcessor.TransactionAgrgegate/TransactionAggregate.cs b/TransactionProcessor.TransactionAgrgegate/TransactionAggregate.cs index 45e11d9d..415728ba 100644 --- a/TransactionProcessor.TransactionAgrgegate/TransactionAggregate.cs +++ b/TransactionProcessor.TransactionAgrgegate/TransactionAggregate.cs @@ -3,6 +3,7 @@ using System; using System.Diagnostics.CodeAnalysis; using System.Text.RegularExpressions; + using Models; using Shared.DomainDrivenDesign.EventSourcing; using Shared.DomainDrivenDesign.EventStore; using Shared.General; @@ -158,7 +159,7 @@ private TransactionAggregate(Guid aggregateId) /// /// The type of the transaction. /// - public String TransactionType { get; private set; } + public TransactionType TransactionType { get; private set; } #endregion @@ -181,7 +182,7 @@ public void AuthoriseTransactionLocally(String authorisationCode, this.ApplyAndPend(transactionHasBeenLocallyAuthorisedEvent); } - + /// /// Completes the transaction. /// @@ -253,7 +254,7 @@ private void CheckTransactionNotAlreadyDeclined() /// public void StartTransaction(DateTime transactionDateTime, String transactionNumber, - String transactionType, + TransactionType transactionType, Guid estateId, Guid merchantId, String deviceIdentifier) @@ -265,13 +266,8 @@ public void StartTransaction(DateTime transactionDateTime, throw new ArgumentException("Transaction Number must be numeric"); } - Guard.ThrowIfNullOrEmpty(transactionType, typeof(ArgumentException), "Transaction Type must not be null or empty"); - - // Temporary validation until using enum - if (transactionType != "Logon") - { - throw new ArgumentException($"Invalid Transaction Type [{transactionType}]"); - } + // Validate the transaction Type + Guard.ThrowIfInvalidEnum(typeof(TransactionType), transactionType, typeof(ArgumentOutOfRangeException), $"Invalid Transaction Type [{transactionType}]"); Guard.ThrowIfInvalidGuid(estateId, typeof(ArgumentException), $"Estate Id must not be [{Guid.Empty}]"); Guard.ThrowIfInvalidGuid(merchantId, typeof(ArgumentException), $"Merchant Id must not be [{Guid.Empty}]"); @@ -286,7 +282,7 @@ public void StartTransaction(DateTime transactionDateTime, this.CheckTransactionNotAlreadyStarted(); this.CheckTransactionNotAlreadyCompleted(); TransactionHasStartedEvent transactionHasStartedEvent = - TransactionHasStartedEvent.Create(this.AggregateId, estateId, merchantId, transactionDateTime, transactionNumber, transactionType, deviceIdentifier); + TransactionHasStartedEvent.Create(this.AggregateId, estateId, merchantId, transactionDateTime, transactionNumber, transactionType.ToString(), deviceIdentifier); this.ApplyAndPend(transactionHasStartedEvent); } @@ -387,7 +383,7 @@ private void PlayEvent(TransactionHasStartedEvent domainEvent) this.IsStarted = true; this.TransactionDateTime = domainEvent.TransactionDateTime; this.TransactionNumber = domainEvent.TransactionNumber; - this.TransactionType = domainEvent.TransactionType; + this.TransactionType = Enum.Parse(domainEvent.TransactionType); this.IsLocallyDeclined = false; this.IsDeclined = false; this.IsLocallyAuthorised = false; diff --git a/TransactionProcessor.TransactionAgrgegate/TransactionProcessor.TransactionAggregate.csproj b/TransactionProcessor.TransactionAgrgegate/TransactionProcessor.TransactionAggregate.csproj index a9f3842c..9b5cb053 100644 --- a/TransactionProcessor.TransactionAgrgegate/TransactionProcessor.TransactionAggregate.csproj +++ b/TransactionProcessor.TransactionAgrgegate/TransactionProcessor.TransactionAggregate.csproj @@ -9,6 +9,7 @@ + diff --git a/TransactionProcessor/Controllers/TransactionController.cs b/TransactionProcessor/Controllers/TransactionController.cs index 82c113aa..7ac06a05 100644 --- a/TransactionProcessor/Controllers/TransactionController.cs +++ b/TransactionProcessor/Controllers/TransactionController.cs @@ -116,6 +116,32 @@ private async Task ProcessSpecificMessage(LogonTransactionReq return this.ModelFactory.ConvertFrom(response); } + /// + /// Processes the specific message. + /// + /// The sale transaction request. + /// The cancellation token. + /// + private async Task ProcessSpecificMessage(SaleTransactionRequest saleTransactionRequest, + CancellationToken cancellationToken) + { + Guid transactionId = Guid.NewGuid(); + + ProcessSaleTransactionRequest request = ProcessSaleTransactionRequest.Create(transactionId, + saleTransactionRequest.EstateId, + saleTransactionRequest.MerchantId, + saleTransactionRequest.DeviceIdentifier, + saleTransactionRequest.TransactionType, + saleTransactionRequest.TransactionDateTime, + saleTransactionRequest.TransactionNumber, + saleTransactionRequest.OperatorIdentifier, + saleTransactionRequest.AdditionalTransactionMetadata); + + ProcessSaleTransactionResponse 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 70bc7d8b..ddfea764 100644 --- a/TransactionProcessor/Factories/IModelFactory.cs +++ b/TransactionProcessor/Factories/IModelFactory.cs @@ -17,6 +17,13 @@ public interface IModelFactory /// SerialisedMessage ConvertFrom(ProcessLogonTransactionResponse processLogonTransactionResponse); + /// + /// Converts from. + /// + /// The process sale transaction response. + /// + SerialisedMessage ConvertFrom(ProcessSaleTransactionResponse processSaleTransactionResponse); + #endregion } } \ No newline at end of file diff --git a/TransactionProcessor/Factories/ModelFactory.cs b/TransactionProcessor/Factories/ModelFactory.cs index 572c3d11..93863bfa 100644 --- a/TransactionProcessor/Factories/ModelFactory.cs +++ b/TransactionProcessor/Factories/ModelFactory.cs @@ -48,6 +48,41 @@ public SerialisedMessage ConvertFrom(ProcessLogonTransactionResponse processLogo }; } + /// + /// Converts from. + /// + /// The process sale transaction response. + /// + public SerialisedMessage ConvertFrom(ProcessSaleTransactionResponse processSaleTransactionResponse) + { + if (processSaleTransactionResponse == null) + { + return null; + } + + SaleTransactionResponse saleTransactionResponse = new SaleTransactionResponse + { + ResponseMessage = processSaleTransactionResponse.ResponseMessage, + ResponseCode = processSaleTransactionResponse.ResponseCode, + MerchantId = processSaleTransactionResponse.MerchantId, + EstateId = processSaleTransactionResponse.EstateId, + AdditionalTransactionMetadata = processSaleTransactionResponse.AdditionalTransactionMetadata + }; + + return new SerialisedMessage + { + Metadata = new Dictionary() + { + {MetadataContants.KeyNameEstateId, processSaleTransactionResponse.EstateId.ToString()}, + {MetadataContants.KeyNameMerchantId, processSaleTransactionResponse.MerchantId.ToString()} + }, + SerialisedData = JsonConvert.SerializeObject(saleTransactionResponse, new JsonSerializerSettings + { + TypeNameHandling = TypeNameHandling.All + }) + }; + } + #endregion } } \ No newline at end of file diff --git a/TransactionProcessor/Startup.cs b/TransactionProcessor/Startup.cs index abfcc67c..f15a17a8 100644 --- a/TransactionProcessor/Startup.cs +++ b/TransactionProcessor/Startup.cs @@ -17,6 +17,7 @@ namespace TransactionProcessor using System.Net.Http; using System.Reflection; using Autofac; + using BusinessLogic.OperatorInterfaces; using BusinessLogic.RequestHandlers; using BusinessLogic.Requests; using BusinessLogic.Services; @@ -127,6 +128,8 @@ public void ConfigureContainer(ContainerBuilder builder) Uri uri = ConfigurationReader.GetBaseServerUri(api); return uri.AbsoluteUri.Substring(0, uri.AbsoluteUri.Length - 1); }); + builder.Register>(c => (operatorIdentifier) => { return new SafaricomPinlessProxy();}); + // request & notification handlers builder.Register(context => { @@ -135,6 +138,7 @@ public void ConfigureContainer(ContainerBuilder builder) }); builder.RegisterType().As>().SingleInstance(); + builder.RegisterType().As>().SingleInstance(); } private void ConfigureMiddlewareServices(IServiceCollection services)