From 9ebffbd0d453ee1481dcc5629479de64683c4408 Mon Sep 17 00:00:00 2001 From: Stuart Ferguson Date: Wed, 26 Apr 2023 15:56:29 +0100 Subject: [PATCH] Generate bill payment sales --- .../ScheduledJobDriver/Program.cs | 12 ++ .../ScheduledJobDriver.csproj | 14 ++ .../GenerateTransactionsJob.cs | 191 +++++++++++++----- ...tionProcessing.SchedulerService.dll.config | 2 +- .../appsettings.json | 2 +- .../quartz.config | 3 +- 6 files changed, 173 insertions(+), 51 deletions(-) create mode 100644 TransactionProcessing.SchedulerService/ScheduledJobDriver/Program.cs create mode 100644 TransactionProcessing.SchedulerService/ScheduledJobDriver/ScheduledJobDriver.csproj diff --git a/TransactionProcessing.SchedulerService/ScheduledJobDriver/Program.cs b/TransactionProcessing.SchedulerService/ScheduledJobDriver/Program.cs new file mode 100644 index 0000000..043c223 --- /dev/null +++ b/TransactionProcessing.SchedulerService/ScheduledJobDriver/Program.cs @@ -0,0 +1,12 @@ +namespace ScheduledJobDriver +{ + using TransactionProcessing.SchedulerService.Jobs; + + internal class Program + { + static void Main(string[] args) + { + GenerateTransactionsJob + } + } +} \ No newline at end of file diff --git a/TransactionProcessing.SchedulerService/ScheduledJobDriver/ScheduledJobDriver.csproj b/TransactionProcessing.SchedulerService/ScheduledJobDriver/ScheduledJobDriver.csproj new file mode 100644 index 0000000..78daeda --- /dev/null +++ b/TransactionProcessing.SchedulerService/ScheduledJobDriver/ScheduledJobDriver.csproj @@ -0,0 +1,14 @@ + + + + Exe + net7.0 + enable + enable + + + + + + + diff --git a/TransactionProcessing.SchedulerService/TransactionProcessing.SchedulerService.Jobs/GenerateTransactions/GenerateTransactionsJob.cs b/TransactionProcessing.SchedulerService/TransactionProcessing.SchedulerService.Jobs/GenerateTransactions/GenerateTransactionsJob.cs index 0580495..a704338 100644 --- a/TransactionProcessing.SchedulerService/TransactionProcessing.SchedulerService.Jobs/GenerateTransactions/GenerateTransactionsJob.cs +++ b/TransactionProcessing.SchedulerService/TransactionProcessing.SchedulerService.Jobs/GenerateTransactions/GenerateTransactionsJob.cs @@ -3,6 +3,8 @@ using System; using System.Collections.Generic; using System.Linq; + using System.Net.Http; + using System.Text; using System.Threading; using System.Threading.Tasks; using EstateManagement.Client; @@ -38,6 +40,8 @@ public class GenerateTransactionsJob : IJob /// private IEstateClient EstateClient; + private String TestHostApi; + /// /// The security service client /// @@ -88,6 +92,7 @@ public async Task Execute(IJobExecutionContext context) Guid merchantId = context.MergedJobDataMap.GetGuidValueFromString("MerchantId"); Boolean requireLogon = context.MergedJobDataMap.GetBooleanValueFromString("requireLogon"); String contractsToSkip = context.MergedJobDataMap.GetString("contractsToSkip"); + this.TestHostApi = context.MergedJobDataMap.GetString("TestHostApi"); this.SecurityServiceClient = this.Bootstrapper.GetService(); this.TransactionProcessorClient = this.Bootstrapper.GetService(); @@ -109,20 +114,20 @@ public async Task Execute(IJobExecutionContext context) /// The date time. /// The cancellation token. /// - private async Task> CreateSaleRequests(String accessToken, + private async Task> CreateSaleRequests(String accessToken, MerchantResponse merchant, DateTime dateTime, String contractsToSkip, CancellationToken cancellationToken) { List contracts = await this.EstateClient.GetMerchantContracts(accessToken, merchant.EstateId, merchant.MerchantId, cancellationToken); - + if (String.IsNullOrEmpty(contractsToSkip) == false) { String[] skipContracts = contractsToSkip.Split('|'); - contracts = contracts.Where(c => skipContracts.Contains(c.Description) == false).ToList(); + contracts = contracts.Where(c => skipContracts.Contains(c.Description) == true).ToList(); } - List saleRequests = new List(); + List<(SaleTransactionRequest request, Decimal amount)> saleRequests = new List<(SaleTransactionRequest request, Decimal amount)>(); Random r = new Random(); Int32 transactionNumber = 1; @@ -131,46 +136,86 @@ private async Task> CreateSaleRequests(String acces for (Int32 i = 0; i < numberOfSales; i++) { - // Pick a contract - ContractResponse contract = contracts[r.Next(0, contracts.Count)]; + var requests = await this.CreateSaleTransactionRequest(merchant, dateTime, contracts, r, transactionNumber); - // Pick a product - ContractProduct product = contract.Products[r.Next(0, contract.Products.Count)]; + saleRequests.AddRange(requests); + transactionNumber++; + } - Decimal amount = 0; - if (product.Value.HasValue) - { - amount = product.Value.Value; - } - else - { - // generate an amount - amount = r.Next(9, 250); - } + return saleRequests; + } - // Generate the time - DateTime transactionDateTime = new DateTime(dateTime.Year, dateTime.Month, dateTime.Day, dateTime.Hour, dateTime.Minute, 0); + private async Task> CreateSaleTransactionRequest(MerchantResponse merchant, DateTime dateTime, List contracts, Random r, Int32 transactionNumber){ + // Pick a contract + ContractResponse contract = contracts[r.Next(0, contracts.Count)]; - // Build the metadata - Dictionary requestMetaData = new Dictionary(); - requestMetaData.Add("Amount", amount.ToString()); + // Pick a product + ContractProduct product = contract.Products[r.Next(0, contract.Products.Count)]; - var productType = GenerateTransactionsJob.GetProductType(contract.OperatorName); + Decimal amount = 0; + if (product.Value.HasValue){ + amount = product.Value.Value; + } + else{ + // generate an amount + amount = r.Next(9, 250); + } + + // Generate the time + DateTime transactionDateTime = new DateTime(dateTime.Year, dateTime.Month, dateTime.Day, dateTime.Hour, dateTime.Minute, 0); + + // Build the metadata + Dictionary requestMetaData = new Dictionary(); + requestMetaData.Add("Amount", amount.ToString()); + + List<(SaleTransactionRequest request, Decimal amount)> requests = new List<(SaleTransactionRequest request, Decimal amount)>(); + + ProductType productType = GenerateTransactionsJob.GetProductType(contract.OperatorName); + if (productType != ProductType.BillPayment){ String operatorName = GenerateTransactionsJob.GetOperatorName(contract); - if (productType == ProductType.MobileTopup) - { + if (productType == ProductType.MobileTopup){ requestMetaData.Add("CustomerAccountNumber", "1234567890"); } - else if (productType == ProductType.Voucher) - { + else if (productType == ProductType.Voucher){ requestMetaData.Add("RecipientMobile", "1234567890"); } + + String deviceIdentifier = merchant.Devices.Single().Value; + + SaleTransactionRequest request = new SaleTransactionRequest{ + AdditionalTransactionMetadata = requestMetaData, + ContractId = contract.ContractId, + CustomerEmailAddress = string.Empty, + DeviceIdentifier = deviceIdentifier, + MerchantId = merchant.MerchantId, + EstateId = merchant.EstateId, + TransactionType = "Sale", + TransactionDateTime = transactionDateTime.AddSeconds(r.Next(0, 59)), + TransactionNumber = transactionNumber.ToString(), + OperatorIdentifier = contract.OperatorName, + ProductId = product.ProductId + }; + requests.Add((request, amount)); + } + else{ + (Int32 accountNumber, String accountName, Decimal balance) billDetails = await CreateBill(contract, amount, r); + if (billDetails == default) + return new List<(SaleTransactionRequest request, Decimal amount)>(); + + // Create the requests required String deviceIdentifier = merchant.Devices.Single().Value; - SaleTransactionRequest request = new SaleTransactionRequest + + // First request is Get Account + Dictionary getAccountRequestMetaData = new Dictionary{ + { "CustomerAccountNumber", billDetails.accountNumber.ToString() }, + { "PataPawaPostPaidMessageType", "VerifyAccount" } + }; + + SaleTransactionRequest getAccountRequest = new SaleTransactionRequest { - AdditionalTransactionMetadata = requestMetaData, + AdditionalTransactionMetadata = getAccountRequestMetaData, ContractId = contract.ContractId, CustomerEmailAddress = string.Empty, DeviceIdentifier = deviceIdentifier, @@ -183,11 +228,62 @@ private async Task> CreateSaleRequests(String acces ProductId = product.ProductId }; - saleRequests.Add(request); - transactionNumber++; + requests.Add((getAccountRequest,0)); + + // Second request is Make Payment + var makePaymentRequestMetaData = new Dictionary{ + { "CustomerAccountNumber", billDetails.accountNumber.ToString() }, + { "CustomerName", billDetails.accountName}, + { "MobileNumber", "1234567890"}, + { "Amount", amount.ToString()}, + { "PataPawaPostPaidMessageType", "ProcessBill" } + }; + + SaleTransactionRequest makePaymentRequest = new SaleTransactionRequest + { + AdditionalTransactionMetadata = makePaymentRequestMetaData, + ContractId = contract.ContractId, + CustomerEmailAddress = string.Empty, + DeviceIdentifier = deviceIdentifier, + MerchantId = merchant.MerchantId, + EstateId = merchant.EstateId, + TransactionType = "Sale", + TransactionDateTime = transactionDateTime.AddSeconds(r.Next(0, 59)), + TransactionNumber = transactionNumber.ToString(), + OperatorIdentifier = contract.OperatorName, + ProductId = product.ProductId + }; + + requests.Add((makePaymentRequest, amount)); + } - return saleRequests; + return requests; + } + + private async Task<(Int32 accountNumber, String accountName, Decimal balance)> CreateBill(ContractResponse contract, Decimal amount, Random r) + { + if (contract.OperatorName == "PataPawa PostPay"){ + + Int32 accountNumber = r.Next(1, 100000); + + HttpClient httpClient = new HttpClient(); + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, $"{this.TestHostApi}/api/developer/patapawapostpay/createbill"); + var body = new{ + due_date = DateTime.Now.AddDays(1), + amount = amount * 100, + account_number = accountNumber, + account_name = "Test Account 1" + }; + request.Content = new StringContent(JsonConvert.SerializeObject(body), Encoding.UTF8, "application/json"); + HttpResponseMessage response = await httpClient.SendAsync(request); + + if (response.IsSuccessStatusCode){ + return (body.account_number, body.account_name, body.amount); + } + } + + return default; } /// @@ -269,11 +365,10 @@ private async Task DoSaleTransaction(String accessToken, /// The cancellation token. /// private async Task GenerateTransactions(Guid estateId, - Guid merchantId, - Boolean requiresLogon, - String contractsToSkip, - CancellationToken cancellationToken) - { + Guid merchantId, + Boolean requiresLogon, + String contractsToSkip, + CancellationToken cancellationToken){ DateTime transactionDate = DateTime.Now; // get a token @@ -284,8 +379,7 @@ private async Task GenerateTransactions(Guid estateId, Int32 transactionCount = 0; - if (requiresLogon) - { + if (requiresLogon){ // Do a logon transaction for the merchant await this.DoLogonTransaction(accessToken, merchant, transactionDate, cancellationToken); @@ -293,21 +387,19 @@ private async Task GenerateTransactions(Guid estateId, } // Now generate some sales - List saleRequests = await this.CreateSaleRequests(accessToken, merchant, transactionDate, contractsToSkip, cancellationToken); + List<(SaleTransactionRequest request, Decimal amount)> saleRequests = await this.CreateSaleRequests(accessToken, merchant, transactionDate, contractsToSkip, cancellationToken); // Work out how much of a deposit the merchant needs (minus 1 sale) - IEnumerable> metadata = saleRequests.Select(s => s.AdditionalTransactionMetadata); - List amounts = metadata.Select(m => m["Amount"]).ToList(); + IEnumerable amountList = saleRequests.Select(s => s.amount).ToList(); - Decimal depositAmount = amounts.TakeLast(amounts.Count - 1).Sum(a => decimal.Parse(a)); + Decimal depositAmount = amountList.TakeLast(amountList.Count() - 1).Sum(a => a); await this.MakeMerchantDeposit(accessToken, merchant, depositAmount, transactionDate, cancellationToken); // Now send the sales - saleRequests = saleRequests.OrderBy(s => s.TransactionDateTime).ToList(); - foreach (SaleTransactionRequest saleTransactionRequest in saleRequests) - { - await this.DoSaleTransaction(accessToken, saleTransactionRequest, cancellationToken); + saleRequests = saleRequests.OrderBy(s => s.request.TransactionDateTime).ToList(); + foreach ((SaleTransactionRequest request, Decimal amount) saleTransactionRequest in saleRequests){ + await this.DoSaleTransaction(accessToken, saleTransactionRequest.request, cancellationToken); Console.WriteLine($"Sale sent for Merchant [{merchant.MerchantName}]"); transactionCount++; } @@ -351,6 +443,9 @@ private static ProductType GetProductType(String operatorName) case "Voucher": productType = ProductType.Voucher; break; + case "PataPawa PostPay": + productType = ProductType.BillPayment; + break; } return productType; diff --git a/TransactionProcessing.SchedulerService/TransactionProcessing.SchedulerService/TransactionProcessing.SchedulerService.dll.config b/TransactionProcessing.SchedulerService/TransactionProcessing.SchedulerService/TransactionProcessing.SchedulerService.dll.config index 7118483..08f94bf 100644 --- a/TransactionProcessing.SchedulerService/TransactionProcessing.SchedulerService/TransactionProcessing.SchedulerService.dll.config +++ b/TransactionProcessing.SchedulerService/TransactionProcessing.SchedulerService/TransactionProcessing.SchedulerService.dll.config @@ -1,6 +1,6 @@  - + \ No newline at end of file diff --git a/TransactionProcessing.SchedulerService/TransactionProcessing.SchedulerService/appsettings.json b/TransactionProcessing.SchedulerService/TransactionProcessing.SchedulerService/appsettings.json index 270a6ab..6b8a386 100644 --- a/TransactionProcessing.SchedulerService/TransactionProcessing.SchedulerService/appsettings.json +++ b/TransactionProcessing.SchedulerService/TransactionProcessing.SchedulerService/appsettings.json @@ -7,6 +7,6 @@ } }, "ConnectionStrings": { - "SchedulerReadModel": "server=127.0.0.1;user id=sa;password=sp1ttal;database=Scheduler" + "SchedulerReadModel": "server=127.0.0.1;user id=sa;password=sp1ttal;database=Scheduler;Encrypt=True;TrustServerCertificate=True" } } diff --git a/TransactionProcessing.SchedulerService/TransactionProcessing.SchedulerService/quartz.config b/TransactionProcessing.SchedulerService/TransactionProcessing.SchedulerService/quartz.config index 616509f..fea4ad1 100644 --- a/TransactionProcessing.SchedulerService/TransactionProcessing.SchedulerService/quartz.config +++ b/TransactionProcessing.SchedulerService/TransactionProcessing.SchedulerService/quartz.config @@ -4,9 +4,10 @@ quartz.dataSource.JobStore.connectionStringName = SchedulerReadModel quartz.jobStore.useProperties = true quartz.jobStore.tablePrefix = QRTZ_ quartz.jobStore.type = Quartz.Impl.AdoJobStore.JobStoreTX, Quartz +quartz.jobStore.performSchemaValidation = false quartz.serializer.type = json quartz.jobStore.dataSource = JobStore quartz.jobStore.driverDelegateType = Quartz.Impl.AdoJobStore.SqlServerDelegate, Quartz quartz.plugin.recentHistory.type = Quartz.Plugins.RecentHistory.ExecutionHistoryPlugin, Quartz.Plugins.RecentHistory quartz.plugin.recentHistory.storeType = Quartz.Plugins.RecentHistory.Impl.InProcExecutionHistoryStore, Quartz.Plugins.RecentHistory -quartz.plugin.timeZoneConverter.type = Quartz.Plugin.TimeZoneConverter.TimeZoneConverterPlugin, Quartz.Plugins.TimeZoneConverter +quartz.plugin.timeZoneConverter.type = Quartz.Plugin.TimeZoneConverter.TimeZoneConverterPlugin, Quartz.Plugins.TimeZoneConverter \ No newline at end of file