diff --git a/TransactionProcessor.BusinessLogic.Tests/Services/TransactionDomainServiceTests.cs b/TransactionProcessor.BusinessLogic.Tests/Services/TransactionDomainServiceTests.cs index f2d7dda8..5487b2d4 100644 --- a/TransactionProcessor.BusinessLogic.Tests/Services/TransactionDomainServiceTests.cs +++ b/TransactionProcessor.BusinessLogic.Tests/Services/TransactionDomainServiceTests.cs @@ -506,6 +506,45 @@ public async Task TransactionDomainService_ProcessSaleTransaction_InvalidEstate_ this.ValidateResponse(response, TransactionResponseCode.InvalidEstateId); } + [Fact] + public async Task TransactionDomainService_ProcessSaleTransaction_NotEnoughCredit_TransactionIsProcessed() + { + IConfigurationRoot configurationRoot = new ConfigurationBuilder().AddInMemoryCollection(TestData.DefaultAppSettings).Build(); + ConfigurationReader.Initialise(configurationRoot); + + Logger.Initialise(NullLogger.Instance); + + Mock transactionAggregateManager = new Mock(); + Mock estateClient = new Mock(); + Mock securityServiceClient = new Mock(); + Mock operatorProxy = new Mock(); + Func operatorProxyResolver = (operatorName) => { return operatorProxy.Object; }; + + securityServiceClient.Setup(s => s.GetToken(It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(TestData.TokenResponse); + estateClient.Setup(e => e.GetEstate(It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(TestData.GetEstateResponseWithOperator1); + estateClient.Setup(e => e.GetMerchant(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(TestData.GetMerchantResponseWithZeroAvailableBalance); + transactionAggregateManager.Setup(t => t.GetAggregate(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(TestData.GetLocallyDeclinedTransactionAggregate(TransactionResponseCode.MerchantDoesNotHaveEnoughCredit)); + + TransactionDomainService transactionDomainService = + new TransactionDomainService(transactionAggregateManager.Object, estateClient.Object, securityServiceClient.Object, + operatorProxyResolver); + + ProcessSaleTransactionResponse response = await transactionDomainService.ProcessSaleTransaction(TestData.TransactionId, + TestData.EstateId, + TestData.MerchantId, + TestData.TransactionDateTime, + TestData.TransactionNumber, + TestData.DeviceIdentifier, + TestData.OperatorIdentifier1, + TestData.CustomerEmailAddress, + TestData.AdditionalTransactionMetaData, + CancellationToken.None); + + this.ValidateResponse(response, TransactionResponseCode.MerchantDoesNotHaveEnoughCredit); + } + [Fact] public async Task TransactionDomainService_ProcessSaleTransaction_InvalidMerchant_TransactionIsProcessed() { diff --git a/TransactionProcessor.BusinessLogic.Tests/TransactionProcessor.BusinessLogic.Tests.csproj b/TransactionProcessor.BusinessLogic.Tests/TransactionProcessor.BusinessLogic.Tests.csproj index d88ee50a..d9c90ae5 100644 --- a/TransactionProcessor.BusinessLogic.Tests/TransactionProcessor.BusinessLogic.Tests.csproj +++ b/TransactionProcessor.BusinessLogic.Tests/TransactionProcessor.BusinessLogic.Tests.csproj @@ -7,15 +7,15 @@ - - + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/TransactionProcessor.BusinessLogic/Services/TransactionDomainService.cs b/TransactionProcessor.BusinessLogic/Services/TransactionDomainService.cs index d5305f9b..0e89c9c7 100644 --- a/TransactionProcessor.BusinessLogic/Services/TransactionDomainService.cs +++ b/TransactionProcessor.BusinessLogic/Services/TransactionDomainService.cs @@ -166,6 +166,9 @@ public async Task ProcessSaleTransaction(Guid tr // Extract the transaction amount from the metadata Decimal transactionAmount = this.ExtractFieldFromMetadata("Amount", additionalTransactionMetadata); + (String responseMessage, TransactionResponseCode responseCode) validationResult = + await this.ValidateSaleTransaction(estateId, merchantId, deviceIdentifier, operatorIdentifier, transactionAmount, cancellationToken); + await this.TransactionAggregateManager.StartTransaction(transactionId, transactionDateTime, transactionNumber, @@ -176,10 +179,7 @@ await this.TransactionAggregateManager.StartTransaction(transactionId, deviceIdentifier, transactionAmount, cancellationToken); - - (String responseMessage, TransactionResponseCode responseCode) validationResult = - await this.ValidateSaleTransaction(estateId, merchantId, deviceIdentifier, operatorIdentifier, cancellationToken); - + if (validationResult.responseCode == TransactionResponseCode.Success) { // Record any additional request metadata @@ -420,10 +420,10 @@ private async Task GetToken(CancellationToken cancellationToken) /// The merchant identifier. /// The device identifier. /// The operator identifier. + /// The transaction amount. /// The cancellation token. /// - /// - /// Merchant {merchant.MerchantName} has no valid Devices for this transaction. + /// Merchant {merchant.MerchantName} has no valid Devices for this transaction. /// or /// Device Identifier {deviceIdentifier} not valid for Merchant {merchant.MerchantName} /// or @@ -433,12 +433,12 @@ private async Task GetToken(CancellationToken cancellationToken) /// or /// Merchant {merchant.MerchantName} has no operators defined /// or - /// Operator {operatorIdentifier} not configured for Merchant [{merchant.MerchantName}] - /// + /// Operator {operatorIdentifier} not configured for Merchant [{merchant.MerchantName}] private async Task<(String responseMessage, TransactionResponseCode responseCode)> ValidateSaleTransaction(Guid estateId, Guid merchantId, String deviceIdentifier, String operatorIdentifier, + Decimal transactionAmount, CancellationToken cancellationToken) { try @@ -496,6 +496,13 @@ private async Task GetToken(CancellationToken cancellationToken) } } + // Check the merchant has enough balance to perform the sale + if (merchant.AvailableBalance < transactionAmount) + { + throw new TransactionValidationException($"Merchant [{merchant.MerchantName}] does not have enough credit available [{merchant.AvailableBalance}] to perform transaction amount [{transactionAmount}]", + TransactionResponseCode.MerchantDoesNotHaveEnoughCredit); + } + // If we get here everything is good return ("SUCCESS", TransactionResponseCode.Success); } diff --git a/TransactionProcessor.BusinessLogic/Services/TransactionResponseCode.cs b/TransactionProcessor.BusinessLogic/Services/TransactionResponseCode.cs index 5bdacefa..5c38e778 100644 --- a/TransactionProcessor.BusinessLogic/Services/TransactionResponseCode.cs +++ b/TransactionProcessor.BusinessLogic/Services/TransactionResponseCode.cs @@ -13,6 +13,7 @@ public enum TransactionResponseCode NoMerchantOperators = 1006, OperatorNotValidForMerchant = 1007, TransactionDeclinedByOperator = 1008, + MerchantDoesNotHaveEnoughCredit = 1009, // A Catch All generic Error where reason has not been identified UnknownFailure = 9999 diff --git a/TransactionProcessor.BusinessLogic/TransactionProcessor.BusinessLogic.csproj b/TransactionProcessor.BusinessLogic/TransactionProcessor.BusinessLogic.csproj index 4334670d..35e92685 100644 --- a/TransactionProcessor.BusinessLogic/TransactionProcessor.BusinessLogic.csproj +++ b/TransactionProcessor.BusinessLogic/TransactionProcessor.BusinessLogic.csproj @@ -5,12 +5,12 @@ - - - - - - + + + + + + diff --git a/TransactionProcessor.Client/TransactionProcessor.Client.csproj b/TransactionProcessor.Client/TransactionProcessor.Client.csproj index 63b91939..b449a109 100644 --- a/TransactionProcessor.Client/TransactionProcessor.Client.csproj +++ b/TransactionProcessor.Client/TransactionProcessor.Client.csproj @@ -6,7 +6,7 @@ - + diff --git a/TransactionProcessor.IntegrationTests/Common/DockerHelper.cs b/TransactionProcessor.IntegrationTests/Common/DockerHelper.cs index 345666f3..818013bf 100644 --- a/TransactionProcessor.IntegrationTests/Common/DockerHelper.cs +++ b/TransactionProcessor.IntegrationTests/Common/DockerHelper.cs @@ -3,7 +3,9 @@ using System; using System.Collections.Generic; using System.Data; + using System.IO; using System.Linq; + using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; @@ -15,6 +17,9 @@ using Ductus.FluentDocker.Services.Extensions; using EstateManagement.Client; using EstateReporting.Database; + using EventStore.ClientAPI.Common.Log; + using EventStore.ClientAPI.Projections; + using EventStore.ClientAPI.SystemData; using global::Shared.Logger; using Microsoft.Data.SqlClient; using SecurityService.Client; @@ -144,6 +149,45 @@ public static IContainerService SetupTestHostContainer(String containerName, ILo return builtContainer; } + private async Task LoadEventStoreProjections() + { + //Start our Continous Projections - we might decide to do this at a different stage, but now lets try here + String projectionsFolder = "../../../projections/continuous"; + IPAddress[] ipAddresses = Dns.GetHostAddresses("127.0.0.1"); + IPEndPoint endpoint = new IPEndPoint(ipAddresses.First(), this.EventStoreHttpPort); + + if (!String.IsNullOrWhiteSpace(projectionsFolder)) + { + DirectoryInfo di = new DirectoryInfo(projectionsFolder); + + if (di.Exists) + { + FileInfo[] files = di.GetFiles(); + + // TODO: possibly need to change timeout and logger here + ProjectionsManager projectionManager = new ProjectionsManager(new ConsoleLogger(), endpoint, TimeSpan.FromSeconds(30)); + + foreach (FileInfo file in files) + { + String projection = File.ReadAllText(file.FullName); + String projectionName = file.Name.Replace(".js", String.Empty); + + try + { + Logger.LogInformation($"Creating projection [{projectionName}]"); + await projectionManager.CreateContinuousAsync(projectionName, projection, new UserCredentials("admin", "changeit")).ConfigureAwait(false); + } + catch (Exception e) + { + Logger.LogError(new Exception($"Projection [{projectionName}] error", e)); + } + } + } + } + + Logger.LogInformation("Loaded projections"); + } + #region Methods /// @@ -185,9 +229,9 @@ public override async Task StartContainersForScenarioRun(String scenarioName) }, traceFolder, null, this.SecurityServiceContainerName, this.EventStoreContainerName, - Setup.SqlServerContainerName, + (Setup.SqlServerContainerName, "sa", - "thisisalongpassword123!", + "thisisalongpassword123!"), ("serviceClient", "Secret1")); IContainerService securityServiceContainer = DockerHelper.SetupSecurityServiceContainer(this.SecurityServiceContainerName, @@ -224,9 +268,9 @@ public override async Task StartContainersForScenarioRun(String scenarioName) traceFolder, dockerCredentials, this.SecurityServiceContainerName, - Setup.SqlServerContainerName, + (Setup.SqlServerContainerName, "sa", - "thisisalongpassword123!", + "thisisalongpassword123!"), ("serviceClient", "Secret1"), true); @@ -267,6 +311,8 @@ public override async Task StartContainersForScenarioRun(String scenarioName) this.SecurityServiceClient = new SecurityServiceClient(SecurityServiceBaseAddressResolver, httpClient); this.TransactionProcessorClient = new TransactionProcessorClient(TransactionProcessorBaseAddressResolver, httpClient); + await this.LoadEventStoreProjections().ConfigureAwait(false); + await PopulateSubscriptionServiceConfiguration().ConfigureAwait(false); IContainerService subscriptionServiceContainer = DockerHelper.SetupSubscriptionServiceContainer(this.SubscriptionServiceContainerName, @@ -280,9 +326,9 @@ public override async Task StartContainersForScenarioRun(String scenarioName) traceFolder, dockerCredentials, this.SecurityServiceContainerName, - Setup.SqlServerContainerName, + (Setup.SqlServerContainerName, "sa", - "thisisalongpassword123!", + "thisisalongpassword123!"), this.TestId, ("serviceClient", "Secret1"), true); diff --git a/TransactionProcessor.IntegrationTests/SaleTransaction/SaleTransactionFeature.feature b/TransactionProcessor.IntegrationTests/SaleTransaction/SaleTransactionFeature.feature index 637641c1..50c9e058 100644 --- a/TransactionProcessor.IntegrationTests/SaleTransaction/SaleTransactionFeature.feature +++ b/TransactionProcessor.IntegrationTests/SaleTransaction/SaleTransactionFeature.feature @@ -44,6 +44,12 @@ Background: | 123456781 | Test Merchant 2 | Test Estate 1 | | 123456782 | Test Merchant 3 | Test Estate 2 | + Given I make the following manual merchant deposits + | Reference | Amount | DateTime | MerchantName | EstateName | + | Deposit1 | 2000.00 | Today | Test Merchant 1 | Test Estate 1 | + | Deposit1 | 1000.00 | Today | Test Merchant 2 | Test Estate 1 | + | Deposit1 | 1000.00 | Today | Test Merchant 3 | Test Estate 2 | + @PRTest Scenario: Sale Transactions @@ -52,7 +58,7 @@ Scenario: Sale Transactions | Today | 1 | Sale | Test Merchant 1 | 123456780 | Test Estate 1 | Safaricom | 1000.00 | 123456789 | | | Today | 2 | Sale | Test Merchant 2 | 123456781 | Test Estate 1 | Safaricom | 1000.00 | 123456789 | | | Today | 3 | Sale | Test Merchant 3 | 123456782 | Test Estate 2 | Safaricom | 1000.00 | 123456789 | | - | Today | 4 | Sale | Test Merchant 1 | 123456780 | Test Estate 1 | Safaricom | 1000.00 | 123456789 | testcustomer@vustomer.co.uk | + | Today | 4 | Sale | Test Merchant 1 | 123456780 | Test Estate 1 | Safaricom | 1000.00 | 123456789 | testcustomer@customer.co.uk | Then transaction response should contain the following information | EstateName | MerchantName | TransactionNumber | ResponseCode | ResponseMessage | @@ -65,8 +71,8 @@ Scenario: Sale Transactions 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 | 1000.00 | + | DateTime | TransactionNumber | TransactionType | MerchantName | DeviceIdentifier | EstateName | OperatorName | TransactionAmount | CustomerAccountNumber | CustomerEmailAddress | + | Today | 1 | Sale | Test Merchant 1 | 123456781 | Test Estate 1 | Safaricom | 1000.00 | 123456789 | testcustomer@customer.co.uk | Then transaction response should contain the following information | EstateName | MerchantName | TransactionNumber | ResponseCode | ResponseMessage | @@ -75,8 +81,8 @@ Scenario: Sale Transaction with Invalid Device 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 | 1000.00 | + | DateTime | TransactionNumber | TransactionType | MerchantName | DeviceIdentifier | EstateName | OperatorName | TransactionAmount | CustomerAccountNumber | CustomerEmailAddress | + | Today | 1 | Sale | Test Merchant 1 | 123456780 | InvalidEstate | Safaricom | 1000.00 | 123456789 | testcustomer@customer.co.uk | Then transaction response should contain the following information | EstateName | MerchantName | TransactionNumber | ResponseCode | ResponseMessage | @@ -85,10 +91,21 @@ Scenario: Sale Transaction with Invalid 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 | 1000.00 | + | DateTime | TransactionNumber | TransactionType | MerchantName | DeviceIdentifier | EstateName | OperatorName | TransactionAmount | CustomerAccountNumber | CustomerEmailAddress | + | Today | 1 | Sale | InvalidMerchant | 123456780 | Test Estate 1 | Safaricom | 1000.00 | 123456789 | testcustomer@customer.co.uk | 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] | +@PRTest +Scenario: Sale Transaction with Not Enough Credit Available + + When I perform the following transactions + | DateTime | TransactionNumber | TransactionType | MerchantName | DeviceIdentifier | EstateName | OperatorName | TransactionAmount | CustomerAccountNumber | CustomerEmailAddress | + | Today | 1 | Sale | Test Merchant 1 | 123456780 | Test Estate 1 | Safaricom | 3000.00 | 123456789 | testcustomer@customer.co.uk | + + + Then transaction response should contain the following information + | EstateName | MerchantName | TransactionNumber | ResponseCode | ResponseMessage | + | Test Estate 1 | Test Merchant 1 | 1 | 1009 | Merchant [Test Merchant 1] does not have enough credit available [2000.0] to perform transaction amount [3000.00] | diff --git a/TransactionProcessor.IntegrationTests/SaleTransaction/SaleTransactionFeature.feature.cs b/TransactionProcessor.IntegrationTests/SaleTransaction/SaleTransactionFeature.feature.cs index 8273d42d..44ffe951 100644 --- a/TransactionProcessor.IntegrationTests/SaleTransaction/SaleTransactionFeature.feature.cs +++ b/TransactionProcessor.IntegrationTests/SaleTransaction/SaleTransactionFeature.feature.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------------ +// ------------------------------------------------------------------------------ // // This code was generated by SpecFlow (http://www.specflow.org/). // SpecFlow Version:3.1.0.0 @@ -240,6 +240,33 @@ public virtual void FeatureBackground() "Test Estate 2"}); #line 41 testRunner.Given("I have assigned the following devices to the merchants", ((string)(null)), table29, "Given "); +#line hidden + TechTalk.SpecFlow.Table table30 = new TechTalk.SpecFlow.Table(new string[] { + "Reference", + "Amount", + "DateTime", + "MerchantName", + "EstateName"}); + table30.AddRow(new string[] { + "Deposit1", + "2000.00", + "Today", + "Test Merchant 1", + "Test Estate 1"}); + table30.AddRow(new string[] { + "Deposit1", + "1000.00", + "Today", + "Test Merchant 2", + "Test Estate 1"}); + table30.AddRow(new string[] { + "Deposit1", + "1000.00", + "Today", + "Test Merchant 3", + "Test Estate 2"}); +#line 47 + testRunner.Given("I make the following manual merchant deposits", ((string)(null)), table30, "Given "); #line hidden } @@ -258,7 +285,7 @@ public virtual void SaleTransactions() "PRTest"}; TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Sale Transactions", null, new string[] { "PRTest"}); -#line 48 +#line 54 this.ScenarioInitialize(scenarioInfo); #line hidden bool isScenarioIgnored = default(bool); @@ -281,7 +308,7 @@ public virtual void SaleTransactions() #line 4 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table30 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table31 = new TechTalk.SpecFlow.Table(new string[] { "DateTime", "TransactionNumber", "TransactionType", @@ -292,7 +319,7 @@ public virtual void SaleTransactions() "TransactionAmount", "CustomerAccountNumber", "CustomerEmailAddress"}); - table30.AddRow(new string[] { + table31.AddRow(new string[] { "Today", "1", "Sale", @@ -303,7 +330,7 @@ public virtual void SaleTransactions() "1000.00", "123456789", ""}); - table30.AddRow(new string[] { + table31.AddRow(new string[] { "Today", "2", "Sale", @@ -314,7 +341,7 @@ public virtual void SaleTransactions() "1000.00", "123456789", ""}); - table30.AddRow(new string[] { + table31.AddRow(new string[] { "Today", "3", "Sale", @@ -325,7 +352,7 @@ public virtual void SaleTransactions() "1000.00", "123456789", ""}); - table30.AddRow(new string[] { + table31.AddRow(new string[] { "Today", "4", "Sale", @@ -335,42 +362,42 @@ public virtual void SaleTransactions() "Safaricom", "1000.00", "123456789", - "testcustomer@vustomer.co.uk"}); -#line 50 - testRunner.When("I perform the following transactions", ((string)(null)), table30, "When "); + "testcustomer@customer.co.uk"}); +#line 56 + testRunner.When("I perform the following transactions", ((string)(null)), table31, "When "); #line hidden - TechTalk.SpecFlow.Table table31 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table32 = new TechTalk.SpecFlow.Table(new string[] { "EstateName", "MerchantName", "TransactionNumber", "ResponseCode", "ResponseMessage"}); - table31.AddRow(new string[] { + table32.AddRow(new string[] { "Test Estate 1", "Test Merchant 1", "1", "0000", "SUCCESS"}); - table31.AddRow(new string[] { + table32.AddRow(new string[] { "Test Estate 1", "Test Merchant 2", "2", "0000", "SUCCESS"}); - table31.AddRow(new string[] { + table32.AddRow(new string[] { "Test Estate 2", "Test Merchant 3", "3", "0000", "SUCCESS"}); - table31.AddRow(new string[] { + table32.AddRow(new string[] { "Test Estate 1", "Test Merchant 1", "4", "0000", "SUCCESS"}); -#line 57 - testRunner.Then("transaction response should contain the following information", ((string)(null)), table31, "Then "); +#line 63 + testRunner.Then("transaction response should contain the following information", ((string)(null)), table32, "Then "); #line hidden } this.ScenarioCleanup(); @@ -386,7 +413,7 @@ public virtual void SaleTransactionWithInvalidDevice() "PRTest"}; TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Sale Transaction with Invalid Device", null, new string[] { "PRTest"}); -#line 65 +#line 71 this.ScenarioInitialize(scenarioInfo); #line hidden bool isScenarioIgnored = default(bool); @@ -409,7 +436,7 @@ public virtual void SaleTransactionWithInvalidDevice() #line 4 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table32 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table33 = new TechTalk.SpecFlow.Table(new string[] { "DateTime", "TransactionNumber", "TransactionType", @@ -417,8 +444,10 @@ public virtual void SaleTransactionWithInvalidDevice() "DeviceIdentifier", "EstateName", "OperatorName", - "TransactionAmount"}); - table32.AddRow(new string[] { + "TransactionAmount", + "CustomerAccountNumber", + "CustomerEmailAddress"}); + table33.AddRow(new string[] { "Today", "1", "Sale", @@ -426,24 +455,26 @@ public virtual void SaleTransactionWithInvalidDevice() "123456781", "Test Estate 1", "Safaricom", - "1000.00"}); -#line 67 - testRunner.When("I perform the following transactions", ((string)(null)), table32, "When "); + "1000.00", + "123456789", + "testcustomer@customer.co.uk"}); +#line 73 + testRunner.When("I perform the following transactions", ((string)(null)), table33, "When "); #line hidden - TechTalk.SpecFlow.Table table33 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table34 = new TechTalk.SpecFlow.Table(new string[] { "EstateName", "MerchantName", "TransactionNumber", "ResponseCode", "ResponseMessage"}); - table33.AddRow(new string[] { + table34.AddRow(new string[] { "Test Estate 1", "Test Merchant 1", "1", "1000", "Device Identifier 123456781 not valid for Merchant Test Merchant 1"}); -#line 71 - testRunner.Then("transaction response should contain the following information", ((string)(null)), table33, "Then "); +#line 77 + testRunner.Then("transaction response should contain the following information", ((string)(null)), table34, "Then "); #line hidden } this.ScenarioCleanup(); @@ -456,7 +487,7 @@ 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 75 +#line 81 this.ScenarioInitialize(scenarioInfo); #line hidden bool isScenarioIgnored = default(bool); @@ -479,7 +510,7 @@ public virtual void SaleTransactionWithInvalidEstate() #line 4 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table34 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table35 = new TechTalk.SpecFlow.Table(new string[] { "DateTime", "TransactionNumber", "TransactionType", @@ -487,8 +518,10 @@ public virtual void SaleTransactionWithInvalidEstate() "DeviceIdentifier", "EstateName", "OperatorName", - "TransactionAmount"}); - table34.AddRow(new string[] { + "TransactionAmount", + "CustomerAccountNumber", + "CustomerEmailAddress"}); + table35.AddRow(new string[] { "Today", "1", "Sale", @@ -496,24 +529,26 @@ public virtual void SaleTransactionWithInvalidEstate() "123456780", "InvalidEstate", "Safaricom", - "1000.00"}); -#line 77 - testRunner.When("I perform the following transactions", ((string)(null)), table34, "When "); + "1000.00", + "123456789", + "testcustomer@customer.co.uk"}); +#line 83 + testRunner.When("I perform the following transactions", ((string)(null)), table35, "When "); #line hidden - TechTalk.SpecFlow.Table table35 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table36 = new TechTalk.SpecFlow.Table(new string[] { "EstateName", "MerchantName", "TransactionNumber", "ResponseCode", "ResponseMessage"}); - table35.AddRow(new string[] { + table36.AddRow(new string[] { "InvalidEstate", "Test Merchant 1", "1", "1001", "Estate Id [79902550-64df-4491-b0c1-4e78943928a3] is not a valid estate"}); -#line 81 - testRunner.Then("transaction response should contain the following information", ((string)(null)), table35, "Then "); +#line 87 + testRunner.Then("transaction response should contain the following information", ((string)(null)), table36, "Then "); #line hidden } this.ScenarioCleanup(); @@ -526,7 +561,7 @@ 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 85 +#line 91 this.ScenarioInitialize(scenarioInfo); #line hidden bool isScenarioIgnored = default(bool); @@ -549,7 +584,7 @@ public virtual void SaleTransactionWithInvalidMerchant() #line 4 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table36 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table37 = new TechTalk.SpecFlow.Table(new string[] { "DateTime", "TransactionNumber", "TransactionType", @@ -557,8 +592,10 @@ public virtual void SaleTransactionWithInvalidMerchant() "DeviceIdentifier", "EstateName", "OperatorName", - "TransactionAmount"}); - table36.AddRow(new string[] { + "TransactionAmount", + "CustomerAccountNumber", + "CustomerEmailAddress"}); + table37.AddRow(new string[] { "Today", "1", "Sale", @@ -566,25 +603,105 @@ public virtual void SaleTransactionWithInvalidMerchant() "123456780", "Test Estate 1", "Safaricom", - "1000.00"}); -#line 87 - testRunner.When("I perform the following transactions", ((string)(null)), table36, "When "); + "1000.00", + "123456789", + "testcustomer@customer.co.uk"}); +#line 93 + testRunner.When("I perform the following transactions", ((string)(null)), table37, "When "); #line hidden - TechTalk.SpecFlow.Table table37 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table38 = new TechTalk.SpecFlow.Table(new string[] { "EstateName", "MerchantName", "TransactionNumber", "ResponseCode", "ResponseMessage"}); - table37.AddRow(new string[] { + table38.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 91 - testRunner.Then("transaction response should contain the following information", ((string)(null)), table37, "Then "); +#line 97 + testRunner.Then("transaction response should contain the following information", ((string)(null)), table38, "Then "); +#line hidden + } + this.ScenarioCleanup(); + } + + [Xunit.SkippableFactAttribute(DisplayName="Sale Transaction with Not Enough Credit Available")] + [Xunit.TraitAttribute("FeatureTitle", "SaleTransaction")] + [Xunit.TraitAttribute("Description", "Sale Transaction with Not Enough Credit Available")] + [Xunit.TraitAttribute("Category", "PRTest")] + public virtual void SaleTransactionWithNotEnoughCreditAvailable() + { + string[] tagsOfScenario = new string[] { + "PRTest"}; + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Sale Transaction with Not Enough Credit Available", null, new string[] { + "PRTest"}); +#line 102 +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 table39 = new TechTalk.SpecFlow.Table(new string[] { + "DateTime", + "TransactionNumber", + "TransactionType", + "MerchantName", + "DeviceIdentifier", + "EstateName", + "OperatorName", + "TransactionAmount", + "CustomerAccountNumber", + "CustomerEmailAddress"}); + table39.AddRow(new string[] { + "Today", + "1", + "Sale", + "Test Merchant 1", + "123456780", + "Test Estate 1", + "Safaricom", + "3000.00", + "123456789", + "testcustomer@customer.co.uk"}); +#line 104 + testRunner.When("I perform the following transactions", ((string)(null)), table39, "When "); +#line hidden + TechTalk.SpecFlow.Table table40 = new TechTalk.SpecFlow.Table(new string[] { + "EstateName", + "MerchantName", + "TransactionNumber", + "ResponseCode", + "ResponseMessage"}); + table40.AddRow(new string[] { + "Test Estate 1", + "Test Merchant 1", + "1", + "1009", + "Merchant [Test Merchant 1] does not have enough credit available [2000.0] to perf" + + "orm transaction amount [3000.00]"}); +#line 109 + testRunner.Then("transaction response should contain the following information", ((string)(null)), table40, "Then "); #line hidden } this.ScenarioCleanup(); diff --git a/TransactionProcessor.IntegrationTests/Shared/SharedSteps.cs b/TransactionProcessor.IntegrationTests/Shared/SharedSteps.cs index 09262a80..52f9fc22 100644 --- a/TransactionProcessor.IntegrationTests/Shared/SharedSteps.cs +++ b/TransactionProcessor.IntegrationTests/Shared/SharedSteps.cs @@ -6,6 +6,7 @@ namespace TransactionProcessor.IntegrationTests.Shared { using System.Linq; using System.Net.Http; + using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using Common; @@ -519,5 +520,50 @@ public async Task GivenIHaveAssignedTheFollowingDevicesToTheMerchants(Table tabl this.TestingContext.Logger.LogInformation($"Device {deviceIdentifier} assigned to Merchant {merchantName} Estate {estateDetails.EstateName}"); } } + + [Given(@"I make the following manual merchant deposits")] + public async Task GivenIMakeTheFollowingManualMerchantDeposits(Table table) + { + foreach (TableRow tableRow in table.Rows) + { + EstateDetails estateDetails = this.TestingContext.GetEstateDetails(tableRow); + + String token = this.TestingContext.AccessToken; + if (String.IsNullOrEmpty(estateDetails.AccessToken) == false) + { + token = estateDetails.AccessToken; + } + + // Lookup the merchant id + String merchantName = SpecflowTableHelper.GetStringRowValue(tableRow, "MerchantName"); + Guid merchantId = estateDetails.GetMerchantId(merchantName); + + // Get current balance + MerchantBalanceResponse previousMerchantBalance = await this.TestingContext.DockerHelper.EstateClient.GetMerchantBalance(token, estateDetails.EstateId, merchantId, CancellationToken.None); + + MakeMerchantDepositRequest makeMerchantDepositRequest = new MakeMerchantDepositRequest + { + DepositDateTime = SpecflowTableHelper.GetDateForDateString(SpecflowTableHelper.GetStringRowValue(tableRow, "DateTime"), DateTime.Now), + Source = MerchantDepositSource.Manual, + Reference = SpecflowTableHelper.GetStringRowValue(tableRow, "Reference"), + Amount = SpecflowTableHelper.GetDecimalValue(tableRow, "Amount") + }; + + MakeMerchantDepositResponse makeMerchantDepositResponse = await this.TestingContext.DockerHelper.EstateClient.MakeMerchantDeposit(token, estateDetails.EstateId, merchantId, makeMerchantDepositRequest, CancellationToken.None).ConfigureAwait(false); + + makeMerchantDepositResponse.EstateId.ShouldBe(estateDetails.EstateId); + makeMerchantDepositResponse.MerchantId.ShouldBe(merchantId); + makeMerchantDepositResponse.DepositId.ShouldNotBe(Guid.Empty); + + this.TestingContext.Logger.LogInformation($"Deposit Reference {makeMerchantDepositRequest.Reference} made for Merchant {merchantName}"); + + // Check the merchant balance + MerchantBalanceResponse currentMerchantBalance = await this.TestingContext.DockerHelper.EstateClient.GetMerchantBalance(token, estateDetails.EstateId, merchantId, CancellationToken.None); + + currentMerchantBalance.AvailableBalance.ShouldBe(previousMerchantBalance.AvailableBalance + makeMerchantDepositRequest.Amount); + + } + } + } } diff --git a/TransactionProcessor.IntegrationTests/TransactionProcessor.IntegrationTests.csproj b/TransactionProcessor.IntegrationTests/TransactionProcessor.IntegrationTests.csproj index 0ed715b8..f39c4091 100644 --- a/TransactionProcessor.IntegrationTests/TransactionProcessor.IntegrationTests.csproj +++ b/TransactionProcessor.IntegrationTests/TransactionProcessor.IntegrationTests.csproj @@ -7,25 +7,26 @@ - + - - - - - - - + + + + + + + + - - - + + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/TransactionProcessor.IntegrationTests/libman.json b/TransactionProcessor.IntegrationTests/libman.json new file mode 100644 index 00000000..e7322b80 --- /dev/null +++ b/TransactionProcessor.IntegrationTests/libman.json @@ -0,0 +1,11 @@ +{ + "version": "1.0", + "defaultProvider": "cdnjs", + "libraries": [ + { + "provider": "filesystem", + "library": "https://github.com/StuartFerguson/EventStoreProjections/releases/download/0.0.1/MerchantBalanceCalculator.js", + "destination": "projections/continuous" + } + ] +} \ No newline at end of file diff --git a/TransactionProcessor.IntegrationTests/projections/continuous/MerchantBalanceCalculator.js b/TransactionProcessor.IntegrationTests/projections/continuous/MerchantBalanceCalculator.js new file mode 100644 index 00000000..138998a4 --- /dev/null +++ b/TransactionProcessor.IntegrationTests/projections/continuous/MerchantBalanceCalculator.js @@ -0,0 +1,148 @@ +var fromStreams = fromStreams || require('../../node_modules/event-store-projection-testing').scope.fromStreams; +var partitionBy = partitionBy !== null ? partitionBy : require('../../node_modules/event-store-projection-testing').scope.partitionBy; +var emit = emit || require('../../node_modules/event-store-projection-testing').scope.emit; + +var incrementBalanceFromDeposit = function(s, merchantId, amount, dateTime) { + var merchant = s.merchants[merchantId]; + merchant.Balance += amount; + merchant.AvailableBalance += amount; + // protect against events coming in out of order + if (merchant.LastDepositDateTime === null || dateTime > merchant.LastDepositDateTime) { + merchant.LastDepositDateTime = dateTime; + } + } + +var addPendingBalanceUpdate = function(s, merchantId, amount, transactionId, dateTime) +{ + var merchant = s.merchants[merchantId]; + merchant.AvailableBalance -= amount; + merchant.PendingBalanceUpdates[transactionId] = { + Amount: amount, + TransactionId: transactionId + }; + // protect against events coming in out of order + if (merchant.LastSaleDateTime === null || dateTime > merchant.LastSaleDateTime) + { + merchant.LastSaleDateTime = dateTime; + } +} + +var decrementBalanceForSale = function(s, merchantId, transactionId, isAuthorised) +{ + var merchant = s.merchants[merchantId]; + // lookup the balance update + var balanceUpdate = merchant.PendingBalanceUpdates[transactionId]; + + if (balanceUpdate !== undefined) + { + if (isAuthorised) + { + merchant.Balance -= balanceUpdate.Amount; + } + else + { + merchant.AvailableBalance += balanceUpdate.Amount; + } + + delete merchant.PendingBalanceUpdates[transactionId]; + } +} + + +var eventbus = { + dispatch: function (s, e) { + + if (e.eventType === 'EstateManagement.Merchant.DomainEvents.MerchantCreatedEvent') { + merchantCreatedEventHandler(s, e); + return; + } + + if (e.eventType === 'EstateManagement.Merchant.DomainEvents.ManualDepositMadeEvent') { + depositMadeEventHandler(s, e); + return; + } + + if (e.eventType === 'TransactionProcessor.Transaction.DomainEvents.TransactionHasStartedEvent') { + transactionHasStartedEventHandler(s, e); + return; + } + + if (e.eventType === 'TransactionProcessor.Transaction.DomainEvents.TransactionHasBeenCompletedEvent') { + transactionHasCompletedEventHandler(s, e); + return; + } + } +} + +var merchantCreatedEventHandler = function (s, e) +{ + var merchantId = e.data.MerchantId; + + if (s.merchants[merchantId] === undefined) { + s.merchants[merchantId] = { + MerchantId: e.data.MerchantId, + MerchantName: e.data.MerchantName, + AvailableBalance: 0, + Balance: 0, + LastDepositDateTime: null, + LastSaleDateTime: null, + PendingBalanceUpdates: [] + }; + } +} + +var depositMadeEventHandler = function (s, e) { + var merchantId = e.data.MerchantId; + var merchant = s.merchants[merchantId]; + + incrementBalanceFromDeposit(s, merchantId,e.data.Amount, e.data.DepositDateTime); +} + +var transactionHasStartedEventHandler = function(s, e) +{ + // Add this to a pending balance update list + var merchantId = e.data.MerchantId; + var merchant = s.merchants[merchantId]; + + var amount = e.data.TransactionAmount; + if (amount === undefined) + { + amount = 0; + } + + addPendingBalanceUpdate(s, merchantId,amount, e.data.TransactionId, e.data.TransactionDateTime); +} + +var transactionHasCompletedEventHandler = function(s, e) +{ + // Add this to a pending balance update list + var merchantId = e.data.MerchantId; + var merchant = s.merchants[merchantId]; + + decrementBalanceForSale(s, merchantId, e.data.TransactionId, e.data.IsAuthorised); +} + +fromStreams('$et-EstateManagement.Merchant.DomainEvents.MerchantCreatedEvent', + '$et-EstateManagement.Merchant.DomainEvents.ManualDepositMadeEvent', + '$et-TransactionProcessor.Transaction.DomainEvents.TransactionHasStartedEvent', + '$et-TransactionProcessor.Transaction.DomainEvents.TransactionHasBeenCompletedEvent') + .partitionBy(function(e) + { + return "MerchantBalanceHistory-" + e.data.MerchantId.replace(/-/gi, ""); + }) + .when({ + $init: function (s, e) { + return { + merchants: {}, + debug: [] + }; + }, + + $any: function (s, e) { + + if (e === null || e.data === null || e.data.IsJson === false) + return; + + eventbus.dispatch(s, e); + } + }); \ No newline at end of file diff --git a/TransactionProcessor.Testing/TestData.cs b/TransactionProcessor.Testing/TestData.cs index 96d90aa2..8bddd476 100644 --- a/TransactionProcessor.Testing/TestData.cs +++ b/TransactionProcessor.Testing/TestData.cs @@ -158,6 +158,9 @@ public class TestData MerchantName = null }; + public static Decimal AvailableBalance = 1000.00m; + public static Decimal ZeroAvailableBalance = 0.00m; + public static EstateResponse GetEstateResponseWithOperator1 => new EstateResponse { @@ -211,6 +214,30 @@ public class TestData public static MerchantResponse GetMerchantResponseWithOperator1 => new MerchantResponse { + AvailableBalance = TestData.AvailableBalance, + EstateId = TestData.EstateId, + MerchantId = TestData.MerchantId, + MerchantName = TestData.MerchantName, + Devices = new Dictionary + { + {TestData.DeviceId, TestData.DeviceIdentifier} + }, + Operators = new List + { + new MerchantOperatorResponse + { + Name = TestData.OperatorIdentifier1, + OperatorId = TestData.OperatorId, + MerchantNumber = TestData.MerchantNumber, + TerminalNumber = TestData.TerminalNumber + } + } + }; + + public static MerchantResponse GetMerchantResponseWithZeroAvailableBalance => + new MerchantResponse + { + AvailableBalance = TestData.ZeroAvailableBalance, EstateId = TestData.EstateId, MerchantId = TestData.MerchantId, MerchantName = TestData.MerchantName, @@ -233,6 +260,7 @@ public class TestData public static MerchantResponse GetMerchantResponseWithOperator2 => new MerchantResponse { + AvailableBalance = TestData.AvailableBalance, EstateId = TestData.EstateId, MerchantId = TestData.MerchantId, MerchantName = TestData.MerchantName, @@ -255,6 +283,7 @@ public class TestData public static MerchantResponse GetMerchantResponseWithNoDevices => new MerchantResponse { + AvailableBalance = TestData.AvailableBalance, EstateId = TestData.EstateId, MerchantId = TestData.MerchantId, MerchantName = TestData.MerchantName, @@ -274,6 +303,7 @@ public class TestData public static MerchantResponse GetMerchantResponseWithNullDevices => new MerchantResponse { + AvailableBalance = TestData.AvailableBalance, EstateId = TestData.EstateId, MerchantId = TestData.MerchantId, MerchantName = TestData.MerchantName, @@ -293,6 +323,7 @@ public class TestData public static MerchantResponse GetMerchantResponseWithEmptyOperators => new MerchantResponse { + AvailableBalance = TestData.AvailableBalance, EstateId = TestData.EstateId, MerchantId = TestData.MerchantId, MerchantName = TestData.MerchantName, @@ -306,6 +337,7 @@ public class TestData public static MerchantResponse GetMerchantResponseWithNullOperators => new MerchantResponse { + AvailableBalance = TestData.AvailableBalance, EstateId = TestData.EstateId, MerchantId = TestData.MerchantId, MerchantName = TestData.MerchantName, diff --git a/TransactionProcessor.Tests/TransactionProcessor.Tests.csproj b/TransactionProcessor.Tests/TransactionProcessor.Tests.csproj index 38b01c07..1f7a599b 100644 --- a/TransactionProcessor.Tests/TransactionProcessor.Tests.csproj +++ b/TransactionProcessor.Tests/TransactionProcessor.Tests.csproj @@ -7,16 +7,16 @@ - - - + + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/TransactionProcessor.Transaction.DomainEvents/TransactionProcessor.Transaction.DomainEvents.csproj b/TransactionProcessor.Transaction.DomainEvents/TransactionProcessor.Transaction.DomainEvents.csproj index 40985bc9..62d84c01 100644 --- a/TransactionProcessor.Transaction.DomainEvents/TransactionProcessor.Transaction.DomainEvents.csproj +++ b/TransactionProcessor.Transaction.DomainEvents/TransactionProcessor.Transaction.DomainEvents.csproj @@ -5,7 +5,7 @@ - + diff --git a/TransactionProcessor.TransactionAggregate.Tests/TransactionProcessor.TransactionAggregate.Tests.csproj b/TransactionProcessor.TransactionAggregate.Tests/TransactionProcessor.TransactionAggregate.Tests.csproj index 5ce42fef..1563578a 100644 --- a/TransactionProcessor.TransactionAggregate.Tests/TransactionProcessor.TransactionAggregate.Tests.csproj +++ b/TransactionProcessor.TransactionAggregate.Tests/TransactionProcessor.TransactionAggregate.Tests.csproj @@ -7,15 +7,15 @@ - - + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/TransactionProcessor.TransactionAgrgegate/TransactionProcessor.TransactionAggregate.csproj b/TransactionProcessor.TransactionAgrgegate/TransactionProcessor.TransactionAggregate.csproj index 25aeb51f..3d2d639e 100644 --- a/TransactionProcessor.TransactionAgrgegate/TransactionProcessor.TransactionAggregate.csproj +++ b/TransactionProcessor.TransactionAgrgegate/TransactionProcessor.TransactionAggregate.csproj @@ -5,7 +5,7 @@ - + diff --git a/TransactionProcessor/TransactionProcessor.csproj b/TransactionProcessor/TransactionProcessor.csproj index d07c84a3..0d30d107 100644 --- a/TransactionProcessor/TransactionProcessor.csproj +++ b/TransactionProcessor/TransactionProcessor.csproj @@ -9,23 +9,23 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - + + + + + + + + + + + +