diff --git a/TransactionProcessor.BusinessLogic.Tests/OperatorInterfaces/AgencyBankingProxyTests.cs b/TransactionProcessor.BusinessLogic.Tests/OperatorInterfaces/AgencyBankingProxyTests.cs index bd66d9dc..e94466cd 100644 --- a/TransactionProcessor.BusinessLogic.Tests/OperatorInterfaces/AgencyBankingProxyTests.cs +++ b/TransactionProcessor.BusinessLogic.Tests/OperatorInterfaces/AgencyBankingProxyTests.cs @@ -152,6 +152,38 @@ public async Task AgencyBankingProxy_ProcessSaleMessage_BalanceEnquiryFailure_Re capturedRequest.RequestUri.ShouldBe(new Uri("http://localhost/transactions/balance-enquiry")); } + [Fact] + public async Task AgencyBankingProxy_ProcessSaleMessage_BalanceEnquiryTimeout_ReturnsFailureResult() + { + Mock handlerMock = new Mock(MockBehavior.Strict); + handlerMock.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ThrowsAsync(new TimeoutException("Execution Timeout Expired")); + + HttpClient httpClient = new HttpClient(handlerMock.Object); + AgencyBankingConfiguration configuration = CreateConfiguration(); + IOperatorProxy proxy = new AgencyBankingProxy(httpClient, configuration); + + Dictionary metadata = new Dictionary + { + { "AgencyBankingMessageType", "balanceenquiry" }, + { "AgencyBankingAccountNumber", "123456789" } + }; + + var result = await proxy.ProcessSaleMessage(TestData.TransactionId, + TestData.OperatorId, + TestData.Merchant, + TestData.TransactionDateTime, + TestData.TransactionReference, + metadata, + CancellationToken.None); + + result.IsFailed.ShouldBeTrue(); + result.Status.ShouldBe(ResultStatus.Failure); + result.Message.ShouldContain("AgencyBanking request failed"); + result.Message.ShouldContain("Execution Timeout Expired"); + } + [Fact] public async Task AgencyBankingProxy_ProcessSaleMessage_BalanceEnquirySuccess_ReturnsMappedOperatorResponse() { diff --git a/TransactionProcessor.BusinessLogic.Tests/OperatorInterfaces/PataPawaPostPayProxyTests.cs b/TransactionProcessor.BusinessLogic.Tests/OperatorInterfaces/PataPawaPostPayProxyTests.cs index 77cc10fc..de8c3a26 100644 --- a/TransactionProcessor.BusinessLogic.Tests/OperatorInterfaces/PataPawaPostPayProxyTests.cs +++ b/TransactionProcessor.BusinessLogic.Tests/OperatorInterfaces/PataPawaPostPayProxyTests.cs @@ -203,6 +203,49 @@ public async Task PataPawaPostPayProxy_ProcessSaleMessage_VerifyAccount_RequestF result.Status.ShouldBe(ResultStatus.NotFound); result.Message.Contains($"Error verifying account number {TestData.PataPawaPostPaidAccountNumber}"); } + + [Fact] + public async Task PataPawaPostPayProxy_ProcessSaleMessage_VerifyAccount_Timeout_ErrorIsThrown() + { + this.PataPawaPostPayService.Setup(s => s.getVerifyRequestAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ThrowsAsync(new TimeoutException("Execution Timeout Expired")); + MemoryCache.Set("PataPawaPostPayLogon", TestData.PataPawaPostPaidSuccessfulLoginOperatorResponse); + + var result = await this.PataPawaPostPayProxy.ProcessSaleMessage(TestData.TransactionId, + TestData.OperatorId, + TestData.Merchant, + TestData.TransactionDateTime, + TestData.TransactionReference, + TestData.AdditionalTransactionMetaDataForPataPawaVerifyAccount(customerAccountNumber: TestData.PataPawaPostPaidAccountNumber), + CancellationToken.None); + + result.IsFailed.ShouldBeTrue(); + result.Status.ShouldBe(ResultStatus.Failure); + result.Message.ShouldContain($"Error verifying account number {TestData.PataPawaPostPaidAccountNumber}"); + result.Message.ShouldContain("Execution Timeout Expired"); + } + + [Fact] + public async Task PataPawaPostPayProxy_ProcessSaleMessage_ProcessBill_Timeout_ErrorIsThrown() + { + this.PataPawaPostPayService.Setup(s => s.getPayBillRequestAsync(It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny())) + .ThrowsAsync(new TimeoutException("Execution Timeout Expired")); + MemoryCache.Set("PataPawaPostPayLogon", TestData.PataPawaPostPaidSuccessfulLoginOperatorResponse); + + var result = await this.PataPawaPostPayProxy.ProcessSaleMessage(TestData.TransactionId, + TestData.OperatorId, + TestData.Merchant, + TestData.TransactionDateTime, + TestData.TransactionReference, + TestData.AdditionalTransactionMetaDataForPataPawaProcessBill(customerAccountNumber: TestData.PataPawaPostPaidAccountNumber), + CancellationToken.None); + + result.IsFailed.ShouldBeTrue(); + result.Status.ShouldBe(ResultStatus.Failure); + result.Message.ShouldContain($"Error paying bill for account number {TestData.PataPawaPostPaidAccountNumber}"); + result.Message.ShouldContain("Execution Timeout Expired"); + } [Fact] public async Task PataPawaPostPayProxy_ProcessSaleMessage_ProcessBill_SuccessfulResponse_MessageIsProcessed() @@ -383,4 +426,4 @@ public async Task PataPawaPostPayProxy_ProcessSaleMessage_ProcessBill_RequestFai result.Message.Contains($"Error paying bill for account number {TestData.PataPawaPostPaidAccountNumber}"); } } -} \ No newline at end of file +} diff --git a/TransactionProcessor.BusinessLogic.Tests/OperatorInterfaces/PataPawaPrePayProxyTests.cs b/TransactionProcessor.BusinessLogic.Tests/OperatorInterfaces/PataPawaPrePayProxyTests.cs index 84f76f60..ca43ec2d 100644 --- a/TransactionProcessor.BusinessLogic.Tests/OperatorInterfaces/PataPawaPrePayProxyTests.cs +++ b/TransactionProcessor.BusinessLogic.Tests/OperatorInterfaces/PataPawaPrePayProxyTests.cs @@ -143,6 +143,36 @@ public async Task PataPawaPrePayProxy_ProcessSaleMessage_MeterTransaction_Failed result.IsFailed.ShouldBeTrue(); } + [Fact] + public async Task PataPawaPrePayProxy_ProcessSaleMessage_MeterTransaction_Timeout_MessageProcessed() + { + BusinessLogic.OperatorInterfaces.OperatorResponse operatorResponse = new() + { + TransactionId = Guid.Parse("2D9D6BBA-BDF4-4248-9B27-6B68374AC037").ToString(), + AdditionalTransactionResponseMetadata = new Dictionary{ + {"PataPawaPrePaidAPIKey", "APIKey"} + } + }; + + this.MemoryCache.Set("PataPawaPrePayLogon", operatorResponse, new MemoryCacheEntryOptions()); + + Dictionary metaDataDictionary = new Dictionary(); + metaDataDictionary.Add("PataPawaPrePayMessageType", "meter"); + metaDataDictionary.Add("MeterNumber", "123456"); + + this.MockHttpMessageHandler.When("http://localhost") + .Respond(req => throw new TimeoutException("Execution Timeout Expired")); + + var result = await this.PataPawaPrePayProxy.ProcessSaleMessage(TestData.TransactionId, + TestData.OperatorId, TestData.Merchant, TestData.TransactionDateTime, TestData.TransactionReference, + metaDataDictionary, CancellationToken.None); + + result.IsFailed.ShouldBeTrue(); + result.Status.ShouldBe(ResultStatus.Failure); + result.Message.ShouldContain("Error performing meter transaction for meter number 123456"); + result.Message.ShouldContain("Execution Timeout Expired"); + } + [Fact] public async Task PataPawaPrePayProxy_ProcessSaleMessage_VendTransaction_MessageProcessed() { @@ -216,6 +246,38 @@ public async Task PataPawaPrePayProxy_ProcessSaleMessage_VendTransaction_FailedA result.IsFailed.ShouldBeTrue(); } + [Fact] + public async Task PataPawaPrePayProxy_ProcessSaleMessage_VendTransaction_Timeout_MessageProcessed() + { + BusinessLogic.OperatorInterfaces.OperatorResponse operatorResponse = new() + { + TransactionId = Guid.Parse("2D9D6BBA-BDF4-4248-9B27-6B68374AC037").ToString(), + AdditionalTransactionResponseMetadata = new Dictionary{ + {"PataPawaPrePaidAPIKey", "APIKey"} + } + }; + + this.MemoryCache.Set("PataPawaPrePayLogon", operatorResponse, new MemoryCacheEntryOptions()); + + Dictionary metaDataDictionary = new Dictionary(); + metaDataDictionary.Add("PataPawaPrePayMessageType", "vend"); + metaDataDictionary.Add("MeterNumber", "123456"); + metaDataDictionary.Add("CustomerName", "Mr Customer"); + metaDataDictionary.Add("Amount", "100"); + + this.MockHttpMessageHandler.When("http://localhost") + .Respond(req => throw new TimeoutException("Execution Timeout Expired")); + + var result = await this.PataPawaPrePayProxy.ProcessSaleMessage(TestData.TransactionId, + TestData.OperatorId, TestData.Merchant, TestData.TransactionDateTime, TestData.TransactionReference, + metaDataDictionary, CancellationToken.None); + + result.IsFailed.ShouldBeTrue(); + result.Status.ShouldBe(ResultStatus.Failure); + result.Message.ShouldContain("Error performing vend transaction for meter number 123456"); + result.Message.ShouldContain("Execution Timeout Expired"); + } + [Theory] [InlineData(null)] [InlineData("")] @@ -457,4 +519,4 @@ public async Task PataPawaPrePayProxy_ProcessSaleMessage_MeterNumberNullOrEmpty_ result.Status.ShouldBe(ResultStatus.Invalid); } -} \ No newline at end of file +} diff --git a/TransactionProcessor.BusinessLogic.Tests/OperatorInterfaces/SafaricomPinlessProxyTests.cs b/TransactionProcessor.BusinessLogic.Tests/OperatorInterfaces/SafaricomPinlessProxyTests.cs index 66b023b3..1cbc1bdb 100644 --- a/TransactionProcessor.BusinessLogic.Tests/OperatorInterfaces/SafaricomPinlessProxyTests.cs +++ b/TransactionProcessor.BusinessLogic.Tests/OperatorInterfaces/SafaricomPinlessProxyTests.cs @@ -188,6 +188,33 @@ public async Task SafaricomPinlessProxy_ProcessSaleMessage_FailedToSend_ErrorThr result.Status.ShouldBe(ResultStatus.Failure); } + [Fact] + public async Task SafaricomPinlessProxy_ProcessSaleMessage_Timeout_ErrorThrown() + { + Mock handlerMock = new Mock(MockBehavior.Strict); + handlerMock.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ThrowsAsync(new TimeoutException("Execution Timeout Expired")); + + SafaricomConfiguration safaricomConfiguration = TestData.SafaricomConfiguration; + HttpClient httpClient = new HttpClient(handlerMock.Object); + + IOperatorProxy safaricomPinlessproxy = new SafaricomPinlessProxy(safaricomConfiguration, httpClient); + + var result = await safaricomPinlessproxy.ProcessSaleMessage(TestData.TransactionId, + TestData.OperatorId, + TestData.Merchant, + TestData.TransactionDateTime, + TestData.TransactionReference, + TestData.AdditionalTransactionMetaDataForMobileTopup(), + CancellationToken.None); + + result.IsFailed.ShouldBeTrue(); + result.Status.ShouldBe(ResultStatus.Failure); + result.Message.ShouldContain("Error sending request to Safaricom"); + result.Message.ShouldContain("Execution Timeout Expired"); + } + [Theory] [InlineData("", "123456789")] [InlineData(null, "123456789")] diff --git a/TransactionProcessor.BusinessLogic.Tests/OperatorInterfaces/VoucherManagementProxyTests.cs b/TransactionProcessor.BusinessLogic.Tests/OperatorInterfaces/VoucherManagementProxyTests.cs index e88a0d19..41de6a82 100644 --- a/TransactionProcessor.BusinessLogic.Tests/OperatorInterfaces/VoucherManagementProxyTests.cs +++ b/TransactionProcessor.BusinessLogic.Tests/OperatorInterfaces/VoucherManagementProxyTests.cs @@ -78,6 +78,28 @@ public async Task VoucherManagementProxy_ProcessSaleMessage_VoucherIssueFailed_F result.IsFailed.ShouldBeTrue(); } + [Fact] + public async Task VoucherManagementProxy_ProcessSaleMessage_VoucherIssueThrowsTimeout_FailedResultReturned() + { + Mock mediator = new Mock(); + mediator.Setup(m => m.Send(It.IsAny(), It.IsAny())) + .ThrowsAsync(new TimeoutException("Execution Timeout Expired")); + IOperatorProxy voucherManagementProxy = new VoucherManagementProxy(mediator.Object); + + var result = await voucherManagementProxy.ProcessSaleMessage(TestData.TransactionId, + TestData.OperatorId, + TestData.Merchant, + TestData.TransactionDateTime, + TestData.TransactionReference, + TestData.AdditionalTransactionMetaDataForVoucher(), + CancellationToken.None); + + result.IsFailed.ShouldBeTrue(); + result.Status.ShouldBe(ResultStatus.Failure); + result.Message.ShouldContain("Error issuing voucher"); + result.Message.ShouldContain("Execution Timeout Expired"); + } + [Theory] [InlineData("")] [InlineData(null)] @@ -134,4 +156,4 @@ public async Task VoucherManagementProxy_ProcessSaleMessage_InvalidData_Recipien result.Status.ShouldBe(ResultStatus.Invalid); } } -} \ No newline at end of file +} diff --git a/TransactionProcessor.BusinessLogic/OperatorInterfaces/AgencyBanking/AgencyBankingProxy.cs b/TransactionProcessor.BusinessLogic/OperatorInterfaces/AgencyBanking/AgencyBankingProxy.cs index 0cd85b00..995bb7ea 100644 --- a/TransactionProcessor.BusinessLogic/OperatorInterfaces/AgencyBanking/AgencyBankingProxy.cs +++ b/TransactionProcessor.BusinessLogic/OperatorInterfaces/AgencyBanking/AgencyBankingProxy.cs @@ -41,41 +41,50 @@ public async Task> ProcessSaleMessage(Guid transactionI String transactionReference, Dictionary additionalTransactionMetadata, CancellationToken cancellationToken) { - Logger.LogInformation($"AgencyBanking sale request received. TransactionId=[{transactionId}], OperatorId=[{operatorId}], MerchantId=[{merchant.MerchantId}], TransactionDateTime=[{transactionDateTime:o}], TransactionReference=[{transactionReference}]"); + try { + Logger.LogInformation($"AgencyBanking sale request received. TransactionId=[{transactionId}], OperatorId=[{operatorId}], MerchantId=[{merchant.MerchantId}], TransactionDateTime=[{transactionDateTime:o}], TransactionReference=[{transactionReference}]"); - // Check the meta data for the message type - String messageType = additionalTransactionMetadata.ExtractFieldFromMetadata("AgencyBankingMessageType"); + // Check the meta data for the message type + String messageType = additionalTransactionMetadata.ExtractFieldFromMetadata("AgencyBankingMessageType"); - if (String.IsNullOrEmpty(messageType)) - { - Logger.LogWarning($"AgencyBanking sale request rejected. TransactionId=[{transactionId}] missing AgencyBankingMessageType"); - return Result.Invalid("AgencyBankingMessageType - Message Type is a required field for this transaction type"); - } + if (String.IsNullOrEmpty(messageType)) + { + Logger.LogWarning($"AgencyBanking sale request rejected. TransactionId=[{transactionId}] missing AgencyBankingMessageType"); + return Result.Invalid("AgencyBankingMessageType - Message Type is a required field for this transaction type"); + } + + // Check the meta data for the account number + String accountNumber = additionalTransactionMetadata.ExtractFieldFromMetadata("AgencyBankingAccountNumber"); + + if (String.IsNullOrEmpty(accountNumber)) + { + Logger.LogWarning($"AgencyBanking sale request rejected. TransactionId=[{transactionId}] missing AgencyBankingAccountNumber"); + return Result.Invalid("AgencyBankingAccountNumber - Account Number is a required field for this transaction type"); + } + + Logger.LogInformation($"AgencyBanking sale request metadata resolved. TransactionId=[{transactionId}], MessageType=[{messageType}], AccountNumber=[{accountNumber}]"); + + Result result = messageType switch { + "balanceenquiry" => await ProcessBalanceEnquiry(transactionId, merchant.MerchantId.ToString(), accountNumber, cancellationToken), + "deposit" => await ProcessDeposit(transactionId, merchant.MerchantId.ToString(), accountNumber, additionalTransactionMetadata, cancellationToken), + "withdrawal" => await ProcessWithdrawal(transactionId, merchant.MerchantId.ToString(), accountNumber, additionalTransactionMetadata, cancellationToken), + "ministatement" => await ProcessMiniStatement(merchant.MerchantId.ToString(), accountNumber, cancellationToken), + _ => Result.Invalid($"AgencyBankingMessageType - Message Type {messageType} is not supported for this transaction type") + }; - // Check the meta data for the account number - String accountNumber = additionalTransactionMetadata.ExtractFieldFromMetadata("AgencyBankingAccountNumber"); + if (result.Status == ResultStatus.Invalid) { + Logger.LogWarning($"AgencyBanking sale request rejected. TransactionId=[{transactionId}] unsupported message type [{messageType}]"); + } - if (String.IsNullOrEmpty(accountNumber)) - { - Logger.LogWarning($"AgencyBanking sale request rejected. TransactionId=[{transactionId}] missing AgencyBankingAccountNumber"); - return Result.Invalid("AgencyBankingAccountNumber - Account Number is a required field for this transaction type"); + return result; } - - Logger.LogInformation($"AgencyBanking sale request metadata resolved. TransactionId=[{transactionId}], MessageType=[{messageType}], AccountNumber=[{accountNumber}]"); - - Result result = messageType switch { - "balanceenquiry" => await ProcessBalanceEnquiry(transactionId, merchant.MerchantId.ToString(), accountNumber, cancellationToken), - "deposit" => await ProcessDeposit(transactionId, merchant.MerchantId.ToString(), accountNumber, additionalTransactionMetadata, cancellationToken), - "withdrawal" => await ProcessWithdrawal(transactionId, merchant.MerchantId.ToString(), accountNumber, additionalTransactionMetadata, cancellationToken), - "ministatement" => await ProcessMiniStatement(merchant.MerchantId.ToString(), accountNumber, cancellationToken), - _ => Result.Invalid($"AgencyBankingMessageType - Message Type {messageType} is not supported for this transaction type") - }; - - if (result.Status == ResultStatus.Invalid) { - Logger.LogWarning($"AgencyBanking sale request rejected. TransactionId=[{transactionId}] unsupported message type [{messageType}]"); + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { + throw; + } + catch (Exception ex) { + Logger.LogError($"AgencyBanking sale request failed. TransactionId=[{transactionId}]", ex); + return Result.Failure($"AgencyBanking request failed [{ex.Message}]"); } - - return result; } private async Task> ProcessWithdrawal(Guid transactionId, diff --git a/TransactionProcessor.BusinessLogic/OperatorInterfaces/PataPawaPostPay/PataPawaPostPayProxy.cs b/TransactionProcessor.BusinessLogic/OperatorInterfaces/PataPawaPostPay/PataPawaPostPayProxy.cs index aef0e3c6..a6f58bba 100644 --- a/TransactionProcessor.BusinessLogic/OperatorInterfaces/PataPawaPostPay/PataPawaPostPayProxy.cs +++ b/TransactionProcessor.BusinessLogic/OperatorInterfaces/PataPawaPostPay/PataPawaPostPayProxy.cs @@ -115,82 +115,103 @@ public async Task> ProcessSaleMessage(Guid transactionI } return messageType switch { - "VerifyAccount" => await this.PerformVerifyAccountTransaction(accountNumber, apiKey), - "ProcessBill" => await this.PerformProcessBillTransaction(accountNumber, apiKey, additionalTransactionMetadata), + "VerifyAccount" => await this.PerformVerifyAccountTransaction(accountNumber, apiKey, cancellationToken), + "ProcessBill" => await this.PerformProcessBillTransaction(accountNumber, apiKey, additionalTransactionMetadata, cancellationToken), _ => Result.Invalid($"PataPawaPostPaidMessageType - Unsupported Message Type {messageType}") }; } private async Task> PerformProcessBillTransaction(String accountNumber, String apiKey, - Dictionary additionalTransactionMetadata) { - String mobileNumber = additionalTransactionMetadata.ExtractFieldFromMetadata("MobileNumber"); - String customerName = additionalTransactionMetadata.ExtractFieldFromMetadata("CustomerName"); - String transactionAmount = additionalTransactionMetadata.ExtractFieldFromMetadata("Amount"); + Dictionary additionalTransactionMetadata, + CancellationToken cancellationToken) { + try { + cancellationToken.ThrowIfCancellationRequested(); - if (String.IsNullOrEmpty(mobileNumber)) { - return Result.Invalid("MobileNumber - MobileNumber is a required field for this transaction type"); - } + String mobileNumber = additionalTransactionMetadata.ExtractFieldFromMetadata("MobileNumber"); + String customerName = additionalTransactionMetadata.ExtractFieldFromMetadata("CustomerName"); + String transactionAmount = additionalTransactionMetadata.ExtractFieldFromMetadata("Amount"); - if (String.IsNullOrEmpty(customerName)) { - return Result.Invalid("CustomerName - CustomerName is a required field for this transaction type"); - } + if (String.IsNullOrEmpty(mobileNumber)) { + return Result.Invalid("MobileNumber - MobileNumber is a required field for this transaction type"); + } - if (String.IsNullOrEmpty(transactionAmount)) { - return Result.Invalid("Amount - Amount is a required field for this transaction type"); - } + if (String.IsNullOrEmpty(customerName)) { + return Result.Invalid("CustomerName - CustomerName is a required field for this transaction type"); + } - // Multiply amount before sending - // Covert the transaction amount to Decimal and remove decimal places - if (Decimal.TryParse(transactionAmount, out Decimal amountAsDecimal) == false) { - return Result.Invalid("Amount - Transaction Amount is not a valid decimal value"); - } + if (String.IsNullOrEmpty(transactionAmount)) { + return Result.Invalid("Amount - Amount is a required field for this transaction type"); + } - Decimal operatorTransactionAmount = amountAsDecimal * 100; - IPataPawaPostPayService channel = this.ChannelResolver(this.ServiceClient, "PataPawaPostPay", this.Configuration.Url); - paybill payBillResponse = await channel.getPayBillRequestAsync(this.Configuration.Username, - apiKey, - accountNumber, - mobileNumber, - customerName, - operatorTransactionAmount); - - if (payBillResponse.status != 0) { - return Result.Failure($"Error paying bill for account number {accountNumber} api status {payBillResponse.status}"); - } + // Multiply amount before sending + // Covert the transaction amount to Decimal and remove decimal places + if (Decimal.TryParse(transactionAmount, out Decimal amountAsDecimal) == false) { + return Result.Invalid("Amount - Transaction Amount is not a valid decimal value"); + } + + Decimal operatorTransactionAmount = amountAsDecimal * 100; + IPataPawaPostPayService channel = this.ChannelResolver(this.ServiceClient, "PataPawaPostPay", this.Configuration.Url); + paybill payBillResponse = await channel.getPayBillRequestAsync(this.Configuration.Username, + apiKey, + accountNumber, + mobileNumber, + customerName, + operatorTransactionAmount); + + if (payBillResponse.status != 0) { + return Result.Failure($"Error paying bill for account number {accountNumber} api status {payBillResponse.status}"); + } - return Result.Success(new OperatorResponse { - ResponseMessage = payBillResponse.msg, - IsSuccessful = true, - ResponseCode = payBillResponse.rescode, - TransactionId = payBillResponse.receipt_no, - }); + return Result.Success(new OperatorResponse { + ResponseMessage = payBillResponse.msg, + IsSuccessful = true, + ResponseCode = payBillResponse.rescode, + TransactionId = payBillResponse.receipt_no, + }); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { + throw; + } + catch (Exception ex) { + return Result.Failure($"Error paying bill for account number {accountNumber} [{ex.Message}]"); + } } private async Task> PerformVerifyAccountTransaction(String accountNumber, - String apiKey) { + String apiKey, + CancellationToken cancellationToken) { + try { + cancellationToken.ThrowIfCancellationRequested(); - IPataPawaPostPayService channel = this.ChannelResolver(this.ServiceClient, "PataPawaPostPay", this.Configuration.Url); - verify verifyResponse = await channel.getVerifyRequestAsync(this.Configuration.Username, apiKey, accountNumber); + IPataPawaPostPayService channel = this.ChannelResolver(this.ServiceClient, "PataPawaPostPay", this.Configuration.Url); + verify verifyResponse = await channel.getVerifyRequestAsync(this.Configuration.Username, apiKey, accountNumber); - if (String.IsNullOrEmpty(verifyResponse.account_name)) { - return Result.NotFound($"Error verifying account number {accountNumber}"); - } + if (String.IsNullOrEmpty(verifyResponse.account_name)) { + return Result.NotFound($"Error verifying account number {accountNumber}"); + } - OperatorResponse operatorResponse = new OperatorResponse { - IsSuccessful = true, - ResponseMessage = "SUCCESS", - AdditionalTransactionResponseMetadata = new Dictionary() - }; + OperatorResponse operatorResponse = new OperatorResponse { + IsSuccessful = true, + ResponseMessage = "SUCCESS", + AdditionalTransactionResponseMetadata = new Dictionary() + }; - operatorResponse.AdditionalTransactionResponseMetadata.Add("CustomerBillBalance", verifyResponse.account_balance.ToString()); - operatorResponse.AdditionalTransactionResponseMetadata.Add("CustomerAccountNumber", verifyResponse.account_no); - operatorResponse.AdditionalTransactionResponseMetadata.Add("CustomerAccountName", verifyResponse.account_name); - operatorResponse.AdditionalTransactionResponseMetadata.Add("CustomerBillDueDate", verifyResponse.due_date.ToString("yyyy-MM-dd")); + operatorResponse.AdditionalTransactionResponseMetadata.Add("CustomerBillBalance", verifyResponse.account_balance.ToString()); + operatorResponse.AdditionalTransactionResponseMetadata.Add("CustomerAccountNumber", verifyResponse.account_no); + operatorResponse.AdditionalTransactionResponseMetadata.Add("CustomerAccountName", verifyResponse.account_name); + operatorResponse.AdditionalTransactionResponseMetadata.Add("CustomerBillDueDate", verifyResponse.due_date.ToString("yyyy-MM-dd")); - return Result.Success(operatorResponse); + return Result.Success(operatorResponse); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { + throw; + } + catch (Exception ex) { + return Result.Failure($"Error verifying account number {accountNumber} [{ex.Message}]"); + } } #endregion } -} \ No newline at end of file +} diff --git a/TransactionProcessor.BusinessLogic/OperatorInterfaces/PataPawaPrePay/PataPawaPrePayProxy.cs b/TransactionProcessor.BusinessLogic/OperatorInterfaces/PataPawaPrePay/PataPawaPrePayProxy.cs index 7e522a71..d84f50f0 100644 --- a/TransactionProcessor.BusinessLogic/OperatorInterfaces/PataPawaPrePay/PataPawaPrePayProxy.cs +++ b/TransactionProcessor.BusinessLogic/OperatorInterfaces/PataPawaPrePay/PataPawaPrePayProxy.cs @@ -104,59 +104,75 @@ public async Task> ProcessSaleMessage(Guid transactionI } private async Task> PerformMeterTransaction(String meterNumber, String apiKey, CancellationToken cancellationToken){ - HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Post, this.Configuration.Url); - MultipartFormDataContent content = new MultipartFormDataContent(); - content.Add(new StringContent("meter"), "request"); - content.Add(new StringContent(this.Configuration.Username), "username"); - content.Add(new StringContent(meterNumber), "meter"); - content.Add(new StringContent(apiKey), "key"); - requestMessage.Content = content; + try { + HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Post, this.Configuration.Url); + MultipartFormDataContent content = new MultipartFormDataContent(); + content.Add(new StringContent("meter"), "request"); + content.Add(new StringContent(this.Configuration.Username), "username"); + content.Add(new StringContent(meterNumber), "meter"); + content.Add(new StringContent(apiKey), "key"); + requestMessage.Content = content; - HttpResponseMessage responseMessage = await this.HttpClient.SendAsync(requestMessage, cancellationToken); + HttpResponseMessage responseMessage = await this.HttpClient.SendAsync(requestMessage, cancellationToken); - // Get the response - String responseContent = await responseMessage.Content.ReadAsStringAsync(cancellationToken); + // Get the response + String responseContent = await responseMessage.Content.ReadAsStringAsync(cancellationToken); - Logger.LogInformation($"Received response message from Patapawa [{responseContent}]"); + Logger.LogInformation($"Received response message from Patapawa [{responseContent}]"); - return this.CreateFromMeter(responseContent); + return this.CreateFromMeter(responseContent); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { + throw; + } + catch (Exception ex) { + return Result.Failure($"Error performing meter transaction for meter number {meterNumber} [{ex.Message}]"); + } } private async Task> PerformVendTransaction(String meterNumber, String apiKey, Dictionary additionalTransactionMetadata, CancellationToken cancellationToken) { - String customerName = additionalTransactionMetadata.ExtractFieldFromMetadata("CustomerName"); + try { + String customerName = additionalTransactionMetadata.ExtractFieldFromMetadata("CustomerName"); - if (String.IsNullOrEmpty(customerName)) - { - return Result.Invalid("CustomerName - Customer Name is a required field for this transaction type"); - } + if (String.IsNullOrEmpty(customerName)) + { + return Result.Invalid("CustomerName - Customer Name is a required field for this transaction type"); + } - String amount = additionalTransactionMetadata.ExtractFieldFromMetadata("Amount"); + String amount = additionalTransactionMetadata.ExtractFieldFromMetadata("Amount"); - if (String.IsNullOrEmpty(amount)) - { - return Result.Invalid("Amount - Amount is a required field for this transaction type"); - } + if (String.IsNullOrEmpty(amount)) + { + return Result.Invalid("Amount - Amount is a required field for this transaction type"); + } - HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Post, this.Configuration.Url); - MultipartFormDataContent content = new MultipartFormDataContent(); - content.Add(new StringContent("vend"), "request"); - content.Add(new StringContent(this.Configuration.Username), "username"); - content.Add(new StringContent(meterNumber), "meter"); - content.Add(new StringContent(apiKey), "key"); - content.Add(new StringContent(amount), "amount"); - content.Add(new StringContent(customerName), "customerName"); - requestMessage.Content = content; + HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Post, this.Configuration.Url); + MultipartFormDataContent content = new MultipartFormDataContent(); + content.Add(new StringContent("vend"), "request"); + content.Add(new StringContent(this.Configuration.Username), "username"); + content.Add(new StringContent(meterNumber), "meter"); + content.Add(new StringContent(apiKey), "key"); + content.Add(new StringContent(amount), "amount"); + content.Add(new StringContent(customerName), "customerName"); + requestMessage.Content = content; - HttpResponseMessage responseMessage = await this.HttpClient.SendAsync(requestMessage, cancellationToken); + HttpResponseMessage responseMessage = await this.HttpClient.SendAsync(requestMessage, cancellationToken); - // Get the response - String responseContent = await responseMessage.Content.ReadAsStringAsync(cancellationToken); + // Get the response + String responseContent = await responseMessage.Content.ReadAsStringAsync(cancellationToken); - Logger.LogInformation($"Received response message from Patapawa [{responseContent}]"); + Logger.LogInformation($"Received response message from Patapawa [{responseContent}]"); - return this.CreateFromVend(responseContent); + return this.CreateFromVend(responseContent); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { + throw; + } + catch (Exception ex) { + return Result.Failure($"Error performing vend transaction for meter number {meterNumber} [{ex.Message}]"); + } } private Result CreateFromLogon(String responseContent){ @@ -293,4 +309,4 @@ public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializer writer.WriteStringValue(value.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)); } } -} \ No newline at end of file +} diff --git a/TransactionProcessor.BusinessLogic/OperatorInterfaces/SafaricomPinless/SafaricomPinlessProxy.cs b/TransactionProcessor.BusinessLogic/OperatorInterfaces/SafaricomPinless/SafaricomPinlessProxy.cs index 30f52ed7..288b3acd 100644 --- a/TransactionProcessor.BusinessLogic/OperatorInterfaces/SafaricomPinless/SafaricomPinlessProxy.cs +++ b/TransactionProcessor.BusinessLogic/OperatorInterfaces/SafaricomPinless/SafaricomPinlessProxy.cs @@ -78,51 +78,59 @@ public async Task> ProcessSaleMessage(Guid transactionI Dictionary additionalTransactionMetadata, CancellationToken cancellationToken) { - // Extract the required fields - String transactionAmount = additionalTransactionMetadata.ExtractFieldFromMetadata("Amount"); - String customerMsisdn = additionalTransactionMetadata.ExtractFieldFromMetadata("CustomerAccountNumber"); - - if (String.IsNullOrEmpty(transactionAmount)) - { - return Result.Invalid("Amount is a required field for this transaction type"); - } - - if (String.IsNullOrEmpty(customerMsisdn)) - { - return Result.Invalid("CustomerAccountNumber is a required field for this transaction type"); - } - - // Multiply amount before sending - // Covert the transaction amount to Decimal and remove decimal places - if (Decimal.TryParse(transactionAmount, out Decimal amountAsDecimal) == false) - { - return Result.Invalid("Transaction Amount is not a valid decimal value"); - } - - Decimal operatorTransactionAmount = amountAsDecimal * 100; + try { + // Extract the required fields + String transactionAmount = additionalTransactionMetadata.ExtractFieldFromMetadata("Amount"); + String customerMsisdn = additionalTransactionMetadata.ExtractFieldFromMetadata("CustomerAccountNumber"); + + if (String.IsNullOrEmpty(transactionAmount)) + { + return Result.Invalid("Amount is a required field for this transaction type"); + } + + if (String.IsNullOrEmpty(customerMsisdn)) + { + return Result.Invalid("CustomerAccountNumber is a required field for this transaction type"); + } + + // Multiply amount before sending + // Covert the transaction amount to Decimal and remove decimal places + if (Decimal.TryParse(transactionAmount, out Decimal amountAsDecimal) == false) + { + return Result.Invalid("Transaction Amount is not a valid decimal value"); + } + + Decimal operatorTransactionAmount = amountAsDecimal * 100; - String requestUrl = this.BuildRequest(transactionDateTime, transactionReference, customerMsisdn, operatorTransactionAmount); + String requestUrl = this.BuildRequest(transactionDateTime, transactionReference, customerMsisdn, operatorTransactionAmount); - Logger.LogInformation($"Sending message to Safaricom [{requestUrl}]"); + Logger.LogInformation($"Sending message to Safaricom [{requestUrl}]"); - // Concatenate the request message - HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Get, new Uri(requestUrl)); + // Concatenate the request message + HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Get, new Uri(requestUrl)); - // Send the request to Safaricom - HttpResponseMessage responseMessage = await this.HttpClient.SendAsync(requestMessage, cancellationToken); + // Send the request to Safaricom + HttpResponseMessage responseMessage = await this.HttpClient.SendAsync(requestMessage, cancellationToken); - // Check the send was successful - if (responseMessage.IsSuccessStatusCode == false) - { - return Result.Failure($"Error sending request [{requestUrl}] to Safaricom. Status Code [{responseMessage.StatusCode}]"); - } + // Check the send was successful + if (responseMessage.IsSuccessStatusCode == false) + { + return Result.Failure($"Error sending request [{requestUrl}] to Safaricom. Status Code [{responseMessage.StatusCode}]"); + } - // Get the response - String responseContent = await responseMessage.Content.ReadAsStringAsync(); + // Get the response + String responseContent = await responseMessage.Content.ReadAsStringAsync(); - Logger.LogInformation($"Received response message from Safaricom [{responseContent}]"); + Logger.LogInformation($"Received response message from Safaricom [{responseContent}]"); - return this.CreateFrom(responseContent); + return this.CreateFrom(responseContent); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { + throw; + } + catch (Exception ex) { + return Result.Failure($"Error sending request to Safaricom [{ex.Message}]"); + } } /// @@ -191,4 +199,4 @@ private Result CreateFrom(String responseContent) #endregion } -} \ No newline at end of file +} diff --git a/TransactionProcessor.BusinessLogic/OperatorInterfaces/VoucherManagement/VoucherManagementProxy.cs b/TransactionProcessor.BusinessLogic/OperatorInterfaces/VoucherManagement/VoucherManagementProxy.cs index ef712c64..8faf5256 100644 --- a/TransactionProcessor.BusinessLogic/OperatorInterfaces/VoucherManagement/VoucherManagementProxy.cs +++ b/TransactionProcessor.BusinessLogic/OperatorInterfaces/VoucherManagement/VoucherManagementProxy.cs @@ -38,56 +38,63 @@ public async Task> ProcessSaleMessage(Guid transactionI Dictionary additionalTransactionMetadata, CancellationToken cancellationToken) { - // Extract the required fields - String recipientEmail = additionalTransactionMetadata.ExtractFieldFromMetadata("RecipientEmail"); - String recipientMobile = additionalTransactionMetadata.ExtractFieldFromMetadata("RecipientMobile"); - String transactionAmount = additionalTransactionMetadata.ExtractFieldFromMetadata("Amount"); + try { + // Extract the required fields + String recipientEmail = additionalTransactionMetadata.ExtractFieldFromMetadata("RecipientEmail"); + String recipientMobile = additionalTransactionMetadata.ExtractFieldFromMetadata("RecipientMobile"); + String transactionAmount = additionalTransactionMetadata.ExtractFieldFromMetadata("Amount"); - // Covert the transaction amount to Decimal and remove decimal places - if (Decimal.TryParse(transactionAmount, out Decimal amountAsDecimal) == false) - { - return Result.Invalid("Transaction Amount is not a valid decimal value"); - } + // Covert the transaction amount to Decimal and remove decimal places + if (Decimal.TryParse(transactionAmount, out Decimal amountAsDecimal) == false) + { + return Result.Invalid("Transaction Amount is not a valid decimal value"); + } - if (String.IsNullOrEmpty(recipientEmail) && String.IsNullOrEmpty(recipientMobile)) - { - return Result.Invalid("Recipient details (either email or mobile) is a required field for this transaction type"); - } + if (String.IsNullOrEmpty(recipientEmail) && String.IsNullOrEmpty(recipientMobile)) + { + return Result.Invalid("Recipient details (either email or mobile) is a required field for this transaction type"); + } - VoucherCommands.IssueVoucherCommand command = new(Guid.NewGuid(), - operatorId, - merchant.EstateId, - transactionId, - DateTime.Now, - amountAsDecimal, - recipientEmail, - recipientMobile); + VoucherCommands.IssueVoucherCommand command = new(Guid.NewGuid(), + operatorId, + merchant.EstateId, + transactionId, + DateTime.Now, + amountAsDecimal, + recipientEmail, + recipientMobile); - Result result= await this.Mediator.Send(command, cancellationToken); + Result result= await this.Mediator.Send(command, cancellationToken); - if (result.IsFailed) { - Logger.LogWarning($"Error issuing voucher {result.Message}"); - return ResultHelpers.CreateFailure(result); - } + if (result.IsFailed) { + Logger.LogWarning($"Error issuing voucher {result.Message}"); + return ResultHelpers.CreateFailure(result); + } - // Build the response metadata - Dictionary additionalTransactionResponseMetadata = new Dictionary(); - additionalTransactionResponseMetadata.Add("VoucherCode", result.Data.VoucherCode); - additionalTransactionResponseMetadata.Add("VoucherMessage", result.Data.Message); - additionalTransactionResponseMetadata.Add("VoucherExpiryDate", result.Data.ExpiryDate.ToString("yyyy-MM-dd")); + // Build the response metadata + Dictionary additionalTransactionResponseMetadata = new Dictionary(); + additionalTransactionResponseMetadata.Add("VoucherCode", result.Data.VoucherCode); + additionalTransactionResponseMetadata.Add("VoucherMessage", result.Data.Message); + additionalTransactionResponseMetadata.Add("VoucherExpiryDate", result.Data.ExpiryDate.ToString("yyyy-MM-dd")); - return Result.Success(new OperatorResponse - { - TransactionId = transactionId.ToString("N"), - ResponseCode = "0000", - ResponseMessage = "SUCCESS", - // This may contain the voucher details to be logged with the transaction, and for possible receipt email/print - AdditionalTransactionResponseMetadata = additionalTransactionResponseMetadata, - AuthorisationCode = "ABCD1234", - IsSuccessful = true - }); - - + return Result.Success(new OperatorResponse + { + TransactionId = transactionId.ToString("N"), + ResponseCode = "0000", + ResponseMessage = "SUCCESS", + // This may contain the voucher details to be logged with the transaction, and for possible receipt email/print + AdditionalTransactionResponseMetadata = additionalTransactionResponseMetadata, + AuthorisationCode = "ABCD1234", + IsSuccessful = true + }); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { + throw; + } + catch (Exception ex) { + Logger.LogWarning($"Error issuing voucher [{ex.Message}]"); + return Result.Failure($"Error issuing voucher [{ex.Message}]"); + } } } }