diff --git a/.github/workflows/pushtomaster.yml b/.github/workflows/pushtomaster.yml new file mode 100644 index 0000000..ff9d085 --- /dev/null +++ b/.github/workflows/pushtomaster.yml @@ -0,0 +1,87 @@ +name: Publish Pre-Release Nuget and Docker + +on: + push: + # branches to consider in the event; optional, defaults to all + branches: + - master + +jobs: + build: + name: "Publish Pre-Release Nugets and Docker" + env: + ASPNETCORE_ENVIRONMENT: "Production" + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2.3.4 + with: + fetch-depth: 0 + + - name: Install NET 7 + uses: actions/setup-dotnet@v2 + with: + dotnet-version: '7.0.x' + + - name: Restore Nuget Packages + run: dotnet restore EstateManagement.sln --source https://api.nuget.org/v3/index.json --source https://www.myget.org/F/transactionprocessing/api/v3/index.json + + - name: Build Code + run: dotnet build EstateManagement.sln --configuration Release + + - name: 'Get Previous tag' + id: get-latest-tag + uses: actions-ecosystem/action-get-latest-tag@v1 + with: + semver_only: true + + - name: 'Bump Version' + id: bump-semver + uses: actions-ecosystem/action-bump-semver@v1 + with: + current_version: ${{ steps.get-latest-tag.outputs.tag }} + level: patch + + - name: Print Version + id: next_version + run: echo ::set-output name=VERSION::${{ steps.bump-semver.outputs.new_version }}-build$GITHUB_RUN_NUMBER + + - name: Build and Publish Nuget Packages + run: | + dotnet pack "EstateManagement.Client\EstateManagement.Client.csproj" /p:PackageVersion=${{ steps.next_version.outputs.VERSION }} --output Nugets -c Release + dotnet nuget push Nugets/EstateManagement.Client.${{ steps.next_version.outputs.VERSION }}.nupkg --api-key ${{ secrets.MYGET_APIKEY }} --source https://www.myget.org/F/transactionprocessing/api/v2/package + dotnet pack "EstateManagement.Estate.DomainEvents\EstateManagement.Estate.DomainEvents.csproj" /p:PackageVersion=${{ steps.next_version.outputs.VERSION }} --output Nugets -c Release + dotnet nuget push Nugets/EstateManagement.Estate.DomainEvents.${{ steps.next_version.outputs.VERSION }}.nupkg --api-key ${{ secrets.MYGET_APIKEY }} --source https://www.myget.org/F/transactionprocessing/api/v2/package + dotnet pack "EstateManagement.Merchant.DomainEvents\EstateManagement.Merchant.DomainEvents.csproj" /p:PackageVersion=${{ steps.next_version.outputs.VERSION }} --output Nugets -c Release + dotnet nuget push Nugets/EstateManagement.Merchant.DomainEvents.${{ steps.next_version.outputs.VERSION }}.nupkg --api-key ${{ secrets.MYGET_APIKEY }} --source https://www.myget.org/F/transactionprocessing/api/v2/package + dotnet pack "EstateManagement.Contract.DomainEvents\EstateManagement.Contract.DomainEvents.csproj" /p:PackageVersion=${{ steps.next_version.outputs.VERSION }} --output Nugets -c Release + dotnet nuget push Nugets/EstateManagement.Contract.DomainEvents.${{ steps.next_version.outputs.VERSION }}.nupkg --api-key ${{ secrets.MYGET_APIKEY }} --source https://www.myget.org/F/transactionprocessing/api/v2/package + dotnet pack "EstateManagement.MerchantStatement.DomainEvents\EstateManagement.MerchantStatement.DomainEvents.csproj" /p:PackageVersion=${{ steps.next_version.outputs.VERSION }} --output Nugets -c Release + dotnet nuget push Nugets/EstateManagement.MerchantStatement.DomainEvents.${{ steps.next_version.outputs.VERSION }}.nupkg --api-key ${{ secrets.MYGET_APIKEY }} --source https://www.myget.org/F/transactionprocessing/api/v2/package + dotnet pack "EstateManagement.Database\EstateManagement.Database.csproj" /p:PackageVersion=${{ steps.next_version.outputs.VERSION }} --output Nugets -c Release + dotnet nuget push Nugets/EstateManagement.Database.${{ steps.next_version.outputs.VERSION }}.nupkg --api-key ${{ secrets.MYGET_APIKEY }} --source https://www.myget.org/F/transactionprocessing/api/v2/package + + - name: Publish Images to Docker Hub + run: | + docker build . --file EstateManagement/Dockerfile --tag stuartferguson/estatemanagement:master + docker login --username=${{ secrets.DOCKER_USERNAME }} --password=${{ secrets.DOCKER_PASSWORD }} + docker push stuartferguson/estatemanagement:master + + buildwidows: + name: "Publish Pre-Release Docker for Windows" + env: + ASPNETCORE_ENVIRONMENT: "Production" + + runs-on: windows-2019 + + steps: + - uses: actions/checkout@v2.3.4 + with: + fetch-depth: 0 + + - name: Publish Windows Images to Docker Hub + run: | + docker build . --file EstateManagement/Dockerfilewindows --tag stuartferguson/estatemanagementwindows:master + docker login --username=${{ secrets.DOCKER_USERNAME }} --password=${{ secrets.DOCKER_PASSWORD }} + docker push stuartferguson/estatemanagementwindows:master \ No newline at end of file diff --git a/TransactionProcessor.DataGenerator/DataGenerator/Program.cs b/TransactionProcessor.DataGenerator/DataGenerator/Program.cs index 34c11a3..13cca75 100644 --- a/TransactionProcessor.DataGenerator/DataGenerator/Program.cs +++ b/TransactionProcessor.DataGenerator/DataGenerator/Program.cs @@ -1,94 +1,61 @@ using System; -namespace TransactionDataGenerator -{ +namespace TransactionDataGenerator{ using System.Collections.Generic; - using System.Diagnostics; - using System.IO; - using System.Linq; using System.Net.Http; - using System.Net.Http.Headers; - using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; - using System.Threading.Tasks.Dataflow; using EstateManagement.Client; - using EstateManagement.DataTransferObjects.Requests; using EstateManagement.DataTransferObjects.Responses; - using Newtonsoft.Json; using SecurityService.Client; - using SecurityService.DataTransferObjects.Responses; + using TransactionProcessing.DataGeneration; using TransactionProcessor.Client; - using TransactionProcessor.DataTransferObjects; - + /// /// /// - class Program - { - /// - /// The estate client - /// - private static EstateManagement.Client.EstateClient EstateClient; - - /// - /// The security service client - /// - private static SecurityService.Client.SecurityServiceClient SecurityServiceClient; - - /// - /// The transaction processor client - /// - private static TransactionProcessor.Client.TransactionProcessorClient TransactionProcessorClient; - - /// - /// The token response - /// - private static TokenResponse TokenResponse; - + class Program{ + private static EstateClient EstateClient; + + private static SecurityServiceClient SecurityServiceClient; + + private static TransactionProcessorClient TransactionProcessorClient; + private static Func baseAddressFunc; - /// - /// Defines the entry point of the application. - /// - /// The arguments. - static async Task Main(string[] args) - { - HttpClientHandler handler = new HttpClientHandler - { - ServerCertificateCustomValidationCallback = (message, - cert, - chain, - errors) => - { - return true; - } - }; + static async Task Main(string[] args){ + HttpClientHandler handler = new HttpClientHandler{ + ServerCertificateCustomValidationCallback = (message, + cert, + chain, + errors) => { + return true; + } + }; HttpClient httpClient = new HttpClient(handler); - baseAddressFunc = (apiName) => - { + baseAddressFunc = (apiName) => { String ipaddress = "192.168.0.133"; - if (apiName == "EstateManagementApi") - { + if (apiName == "EstateManagementApi"){ return $"http://{ipaddress}:5000"; } - if (apiName == "SecurityService") - { + if (apiName == "SecurityService"){ return $"https://{ipaddress}:5001"; } - if (apiName == "TransactionProcessorApi") - { + if (apiName == "TransactionProcessorApi"){ return $"http://{ipaddress}:5002"; } - if (apiName == "FileProcessorApi") - { + if (apiName == "FileProcessorApi"){ return $"http://{ipaddress}:5009"; } + if (apiName == "TestHostApi"){ + return $"http://{ipaddress}:9000"; + } + return null; }; @@ -99,573 +66,51 @@ static async Task Main(string[] args) Program.TransactionProcessorClient = new TransactionProcessorClient(baseAddressFunc, httpClient); // Set an estate - //Guid estateId = Guid.Parse("435613ac-a468-47a3-ac4f-649d89764c22"); - Guid estateId = Guid.Parse("7c757c2c-4ec9-4d78-ac9b-3a7bfc6d5877"); - - // Get a token - await Program.GetToken(CancellationToken.None); - - // Get the the merchant list for the estate - List merchants = await Program.EstateClient.GetMerchants(Program.TokenResponse.AccessToken, estateId, CancellationToken.None); + Guid estateId = Guid.Parse("435613ac-a468-47a3-ac4f-649d89764c22"); // Set the date range - DateTime startDate = new DateTime(2022,10,1); //27/7 - DateTime endDate = new DateTime(2022,10,22); // This is the date of te last generated transaction - List dateRange = Program.GenerateDateRange(startDate, endDate); - - // Only use merchants that have a device - merchants = merchants.Where(m => m.Devices != null && m.Devices.Any()).ToList(); - - foreach (DateTime dateTime in dateRange) - { - //await Program.GenerateTransactions(merchants, dateTime, CancellationToken.None); - await Program.GenerateFileUploads(merchants, dateTime, CancellationToken.None); - } + DateTime startDate = new DateTime(2023, 4, 27); //27/7 + DateTime endDate = new DateTime(2023, 4, 28); // This is the date of the last generated transaction - Console.WriteLine($"Process Complete"); - } - - private static async Task GenerateFileUploads(List merchants, - DateTime dateTime, - CancellationToken cancellationToken) - { - foreach (MerchantResponse merchant in merchants) - { - Random r = new Random(); - - // get a number of transactions to generate - Int32 numberOfSales = r.Next(5, 15); - - List contracts = - await Program.EstateClient.GetMerchantContracts(Program.TokenResponse.AccessToken, merchant.EstateId, merchant.MerchantId, cancellationToken); - - EstateResponse estate = await Program.EstateClient.GetEstate(Program.TokenResponse.AccessToken, merchant.EstateId, cancellationToken); - - var estateUser = estate.SecurityUsers.FirstOrDefault(); - - foreach (MerchantOperatorResponse merchantOperator in merchant.Operators) - { - List fileData = null; - // get the contract - var contract = contracts.SingleOrDefault(c => c.OperatorId == merchantOperator.OperatorId); - - if (merchantOperator.Name == "Voucher") - { - // Generate a voucher file - var voucherFile = GenerateVoucherFile(dateTime, contract.Description.Replace("Contract", ""), numberOfSales); - fileData = voucherFile.fileLines; - // Need to make a deposit for this amount - last sale - Decimal depositAmount = voucherFile.totalValue - voucherFile.lastSale; - await MakeMerchantDeposit(merchant, depositAmount, dateTime.AddSeconds(1)); - } - else - { - // generate a topup file - var topupFile = GenerateTopupFile(dateTime, numberOfSales); - fileData = topupFile.fileLines; - // Need to make a deposit for this amount - last sale - Decimal depositAmount = topupFile.totalValue - topupFile.lastSale; - await MakeMerchantDeposit(merchant, depositAmount, dateTime.AddSeconds(2)); - } - - // Write this file to disk - Directory.CreateDirectory($"/home/txnproc/txngenerator/{merchantOperator.Name}"); - using(StreamWriter sw = new StreamWriter($"/home/txnproc/txngenerator/{merchantOperator.Name}/{contract.Description.Replace("Contract", "")}-{dateTime:yyyy-MM-dd-HH-mm-ss}")) - { - foreach (String fileLine in fileData) - { - sw.WriteLine(fileLine); - } - } - - // Upload the generated files for this merchant/operatorcd - // Get the files - var files = Directory.GetFiles($"/home/txnproc/txngenerator/{merchantOperator.Name}"); - - var fileDateTime = dateTime.AddHours(DateTime.Now.Hour).AddMinutes(DateTime.Now.Minute).AddSeconds(DateTime.Now.Second); - - foreach (String file in files) - { - var fileProfileId = GetFileProfileIdFromOperator(merchantOperator.Name, cancellationToken); - - await UploadFile(file, merchant.EstateId, merchant.MerchantId, fileProfileId, estateUser.SecurityUserId, fileDateTime, cancellationToken); - // Remove file onece uploaded - Console.WriteLine($"File Uploaded for Merchant {merchant.MerchantName}"); - File.Delete(file); - } - } - } - - await Task.Delay(10000); - } - - private static Guid GetFileProfileIdFromOperator(String operatorName, CancellationToken cancellationToken) - { - // TODO: get this profile list from API - - switch(operatorName) - { - case "Safaricom": - return Guid.Parse("B2A59ABF-293D-4A6B-B81B-7007503C3476"); - case "Voucher": - return Guid.Parse("8806EDBC-3ED6-406B-9E5F-A9078356BE99"); - default: - return Guid.Empty; - } - } - - private static async Task UploadFile(String filePath, Guid estateId, Guid merchantId, Guid fileProfileId, Guid userId, DateTime fileDateTime, CancellationToken cancellationToken) - { - var client = new HttpClient(); - var formData = new MultipartFormDataContent(); - - var fileContent = new ByteArrayContent(await File.ReadAllBytesAsync(filePath)); - fileContent.Headers.ContentType = MediaTypeHeaderValue.Parse("multipart/form-data"); - formData.Add(fileContent, "file", Path.GetFileName(filePath)); - formData.Add(new StringContent(estateId.ToString()), "request.EstateId"); - formData.Add(new StringContent(merchantId.ToString()), "request.MerchantId"); - formData.Add(new StringContent(fileProfileId.ToString()), "request.FileProfileId"); - formData.Add(new StringContent(userId.ToString()), "request.UserId"); - formData.Add(new StringContent(fileDateTime.ToString("yyyy-MM-dd HH:mm:ss")), "request.UploadDateTime"); - - var request = new HttpRequestMessage(HttpMethod.Post, $"{baseAddressFunc("FileProcessorApi")}/api/files") - { - Content = formData, - }; - request.Headers.Authorization = new AuthenticationHeaderValue("bearer", Program.TokenResponse.AccessToken); - var response = await client.SendAsync(request, cancellationToken); - - return response; - } - - private static (List fileLines, Decimal totalValue, Decimal lastSale) GenerateTopupFile(DateTime dateTime, - Int32 numberOfLines) - { - List fileLines = new List(); - Decimal totalValue = 0; - Decimal lastSale = 0; - String mobileNumber = "07777777305"; - Random r = new Random(); - - fileLines.Add($"H,{dateTime:yyyy-MM-dd-HH-mm-ss}"); - - for (int i = 0; i < numberOfLines; i++) - { - Int32 amount = r.Next(75, 250); - totalValue += amount; - lastSale = amount; - fileLines.Add($"D,{mobileNumber},{amount}"); - } - - fileLines.Add($"T,{numberOfLines}"); - - return (fileLines, totalValue, lastSale); - } - - private static (List fileLines, Decimal totalValue, Decimal lastSale) GenerateVoucherFile(DateTime dateTime, String issuerName, Int32 numberOfLines) - { - // Build the header - List fileLines = new List(); - fileLines.Add($"H,{dateTime:yyyy-MM-dd-HH-mm-ss}"); - String emailAddress = "testrecipient@email.com"; - String mobileNumber = "07777777305"; - Random r = new Random(); - Decimal totalValue = 0; - Decimal lastSale = 0; - - for (int i = 0; i < numberOfLines; i++) - { - Int32 amount = r.Next(75, 250); - totalValue += amount; - lastSale = amount; - if (i % 2 == 0) - { - fileLines.Add($"D,{issuerName},{emailAddress},{amount}"); - } - else - { - fileLines.Add($"D,{issuerName},{mobileNumber},{amount}"); - } - } - - fileLines.Add($"T,{numberOfLines}"); - - return (fileLines, totalValue, lastSale); - } - - /// - /// Gets the token. - /// - /// The cancellation token. - private static async Task GetToken(CancellationToken cancellationToken) - { // Get a token to talk to the estate service + CancellationToken cancellationToken = new CancellationToken(); String clientId = "serviceClient"; String clientSecret = "d192cbc46d834d0da90e8a9d50ded543"; + ITransactionDataGenerator g = new TransactionDataGenerator(Program.SecurityServiceClient, + Program.EstateClient, + Program.TransactionProcessorClient, + Program.baseAddressFunc("FileProcessorApi"), + Program.baseAddressFunc("TestHostApi"), + clientId, + clientSecret, + RunningMode.Live); - if (Program.TokenResponse == null) - { - TokenResponse token = await Program.SecurityServiceClient.GetToken(clientId, clientSecret, cancellationToken); - Program.TokenResponse = token; - } - - if (Program.TokenResponse.Expires.UtcDateTime.Subtract(DateTime.UtcNow) < TimeSpan.FromMinutes(2)) - { - TokenResponse token = await Program.SecurityServiceClient.GetToken(clientId, clientSecret, cancellationToken); - Program.TokenResponse = token; - } - } - - /// - /// Generates the transactions. - /// - /// The merchants. - /// The date range. - private static async Task GenerateTransactions(List merchants, - DateTime dateTime, - CancellationToken cancellationToken) - { - Int32 maxDegreeOfParallelism = 1; - Int32 boundedCapacityForActionBlock = merchants.Count; - - ActionBlock<(MerchantResponse merchant, CancellationToken cancellationToken)> workerBlock = - new ActionBlock<(MerchantResponse merchant, CancellationToken cancellationToken)>(async (message) => - { - try - { - Int32 transactionCount = 0; + List dateRange = g.GenerateDateRange(startDate, endDate); - // Do a logon transaction for each merchant - await Program.DoLogonTransaction(message.merchant, dateTime); - Console - .WriteLine($"Logon sent for Merchant [{message.merchant.MerchantName}]"); + List merchants = await g.GetMerchants(estateId, cancellationToken); - // Now generate some sales - List saleRequests = - await Program.CreateSaleRequests(message.merchant, - dateTime); + foreach (DateTime dateTime in dateRange){ + foreach (MerchantResponse merchant in merchants){ + // Send a logon transaction + await g.PerformMerchantLogon(dateTime, merchant, 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(); + // Get the merchants contracts + List contracts = await g.GetMerchantContracts(merchant, cancellationToken); - Decimal depositAmount = amounts.TakeLast(amounts.Count - 1) - .Sum(a => Decimal.Parse(a)); + foreach (ContractResponse contract in contracts){ + // Generate and send some sales + await g.SendSales(dateTime, merchant, contract, cancellationToken); - await Program.MakeMerchantDeposit(message.merchant, - depositAmount, - dateTime); - - // Now send the sales - saleRequests = saleRequests.OrderBy(s => s.TransactionDateTime) - .ToList(); - foreach (SaleTransactionRequest saleTransactionRequest in - saleRequests) - { - await Program.DoSaleTransaction(saleTransactionRequest); - Console - .WriteLine($"Sale sent for Merchant [{message.merchant.MerchantName}]"); - transactionCount++; - } - - Console.ForegroundColor = ConsoleColor.Green; - Console - .WriteLine($"{transactionCount} transactions generated for {message.merchant.MerchantName} on date {dateTime.ToLongDateString()}"); - } - catch(Exception ex) - { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine("Failed"); - Console.WriteLine(ex); - } - }, - new ExecutionDataflowBlockOptions - { - MaxDegreeOfParallelism = maxDegreeOfParallelism, - BoundedCapacity = boundedCapacityForActionBlock - }); - - - try - { - foreach (var merchant in merchants) - { - await workerBlock.SendAsync((merchant, cancellationToken), cancellationToken); - } - - workerBlock.Complete(); - - await workerBlock.Completion; - } - catch (Exception e) - { - Console.WriteLine(e); - } - } - - /// - /// Makes the merchant deposit. - /// - /// The merchant. - /// The deposit amount. - /// The date time. - private static async Task MakeMerchantDeposit(MerchantResponse merchant, - Decimal depositAmount, - DateTime dateTime) - { - await Program.GetToken(CancellationToken.None); - - await Program.EstateClient.MakeMerchantDeposit(Program.TokenResponse.AccessToken, - merchant.EstateId, - merchant.MerchantId, - new MakeMerchantDepositRequest - { - Amount = depositAmount, - DepositDateTime = dateTime.AddSeconds(55), - Reference = "Test Data Gen Deposit" - }, - CancellationToken.None); - Console.WriteLine($"Deposit made for Merchant [{merchant.MerchantName}]"); - } - - /// - /// Does the sale transaction. - /// - /// The sale transaction request. - private static async Task DoSaleTransaction(SaleTransactionRequest saleTransactionRequest) - { - try - { - await Program.GetToken(CancellationToken.None); - - SerialisedMessage requestSerialisedMessage = new SerialisedMessage(); - requestSerialisedMessage.Metadata.Add("estate_id", saleTransactionRequest.EstateId.ToString()); - requestSerialisedMessage.Metadata.Add("merchant_id", saleTransactionRequest.MerchantId.ToString()); - requestSerialisedMessage.SerialisedData = JsonConvert.SerializeObject(saleTransactionRequest, - new JsonSerializerSettings - { - TypeNameHandling = TypeNameHandling.All - }); - - SerialisedMessage responseSerialisedMessage = - await Program.TransactionProcessorClient.PerformTransaction(Program.TokenResponse.AccessToken, requestSerialisedMessage, CancellationToken.None); - - SaleTransactionResponse saleTransactionResponse = JsonConvert.DeserializeObject(responseSerialisedMessage.SerialisedData); - } - catch (Exception e) - { - Console.WriteLine(e); - } - } - - /// - /// Creates the sale requests. - /// - /// The merchant. - /// The date time. - /// - private static async Task> CreateSaleRequests(MerchantResponse merchant, - DateTime dateTime) - { - List contracts = - await Program.EstateClient.GetMerchantContracts(Program.TokenResponse.AccessToken, merchant.EstateId, merchant.MerchantId, CancellationToken.None); - - List saleRequests = new List(); - - Random r = new Random(); - Int32 transactionNumber = 1; - // get a number of transactions to generate - Int32 numberOfSales = r.Next(10, 50); - //Int32 numberOfSales = 2; - - for (int i = 0; i < numberOfSales; i++) - { - // Pick a contract - ContractResponse contract = contracts[r.Next(0, contracts.Count)]; - - // Pick a product - ContractProduct product = contract.Products[r.Next(0, contract.Products.Count)]; - - Decimal amount = 0; - if (product.Value.HasValue) - { - amount = product.Value.Value; - } - else - { - // generate an amount - amount = r.Next(9, 250); - } - - // Generate the time - Int32 hours = r.Next(0, 23); - Int32 minutes = r.Next(0, 59); - Int32 seconds = r.Next(0, 59); - - // Build the metadata - Dictionary requestMetaData = new Dictionary(); - requestMetaData.Add("Amount", amount.ToString()); - - var productType = Program.GetProductType(contract.OperatorName); - String operatorName = Program.GetOperatorName(contract, product); - if (productType == ProductType.MobileTopup) - { - requestMetaData.Add("CustomerAccountNumber", "1234567890"); - } - 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 = dateTime.AddHours(hours).AddMinutes(minutes).AddSeconds(seconds), - TransactionNumber = transactionNumber.ToString(), - OperatorIdentifier = contract.OperatorName, - ProductId = product.ProductId - }; - - saleRequests.Add(request); - transactionNumber++; - } - - return saleRequests; - } - - /// - /// Does the logon transaction. - /// - /// The merchant. - /// The date. - private static async Task DoLogonTransaction(MerchantResponse merchant, - DateTime date) - { - await Program.GetToken(CancellationToken.None); - - String deviceIdentifier = merchant.Devices.Single().Value; - LogonTransactionRequest logonTransactionRequest = new LogonTransactionRequest - { - DeviceIdentifier = deviceIdentifier, - EstateId = merchant.EstateId, - MerchantId = merchant.MerchantId, - TransactionDateTime = date.AddMinutes(1), - TransactionNumber = "1", - TransactionType = "Logon" - }; - - SerialisedMessage requestSerialisedMessage = new SerialisedMessage(); - requestSerialisedMessage.Metadata.Add("estate_id", merchant.EstateId.ToString()); - requestSerialisedMessage.Metadata.Add("merchant_id", merchant.MerchantId.ToString()); - requestSerialisedMessage.SerialisedData = JsonConvert.SerializeObject(logonTransactionRequest, - new JsonSerializerSettings - { - TypeNameHandling = TypeNameHandling.All - }); - - SerialisedMessage responseSerialisedMessage = - await Program.TransactionProcessorClient.PerformTransaction(Program.TokenResponse.AccessToken, requestSerialisedMessage, CancellationToken.None); - - LogonTransactionResponse logonTransactionResponse = JsonConvert.DeserializeObject(responseSerialisedMessage.SerialisedData); - } - - /// - /// Generates the date range. - /// - /// The start date. - /// The end date. - /// - private static List GenerateDateRange(DateTime startDate, - DateTime endDate) - { - List dateRange = new List(); - - if (endDate.Subtract(startDate).Days == 0) - { - dateRange.Add(startDate); - } - else - { - while (endDate.Subtract(startDate).Days >= 0) - { - dateRange.Add(startDate); - startDate = startDate.AddDays(1); + // Generate a file and upload + await g.SendUploadFile(dateTime, contract, merchant, cancellationToken); + } } - } - - return dateRange; - } - - private static String GetOperatorName(ContractResponse contractResponse, ContractProduct contractProduct) - { - String operatorName = null; - ProductType productType = Program.GetProductType(contractResponse.OperatorName); - switch (productType) - { - case ProductType.Voucher: - operatorName = contractResponse.Description; - break; - default: - operatorName = contractResponse.OperatorName; - break; - - } - - return operatorName; - } - private static ProductType GetProductType(String operatorName) - { - ProductType productType = ProductType.NotSet; - switch (operatorName) - { - case "Safaricom": - productType = ProductType.MobileTopup; - break; - case "Voucher": - productType = ProductType.Voucher; - break; + // Settlement + await g.PerformSettlement(dateTime, estateId, cancellationToken); } - return productType; + Console.WriteLine($"Process Complete"); } } - - public enum ProductType - { - /// - /// The not set - /// - NotSet = 0, - - /// - /// The mobile topup - /// - MobileTopup, - - /// - /// The mobile wallet - /// - MobileWallet, - - /// - /// The bill payment - /// - BillPayment, - - /// - /// The voucher - /// - Voucher - } -} +} \ No newline at end of file diff --git a/TransactionProcessor.DataGenerator/DataGenerator/TransactionDataGenerator.csproj b/TransactionProcessor.DataGenerator/DataGenerator/TransactionDataGenerator.csproj index 64e9307..968a283 100644 --- a/TransactionProcessor.DataGenerator/DataGenerator/TransactionDataGenerator.csproj +++ b/TransactionProcessor.DataGenerator/DataGenerator/TransactionDataGenerator.csproj @@ -6,10 +6,14 @@ - - - - + + + + + + + + diff --git a/TransactionProcessor.DataGenerator/TransactionProcessing.DataGeneration/Extensions.cs b/TransactionProcessor.DataGenerator/TransactionProcessing.DataGeneration/Extensions.cs new file mode 100644 index 0000000..2df7b7f --- /dev/null +++ b/TransactionProcessor.DataGenerator/TransactionProcessing.DataGeneration/Extensions.cs @@ -0,0 +1,49 @@ +namespace TransactionProcessing.DataGeneration; + +using Newtonsoft.Json; +using TransactionProcessor.DataTransferObjects; + +public static class Extensions{ + #region Methods + + public static Decimal GetAmount(this SaleTransactionRequest request){ + if (request.AdditionalTransactionMetadata.ContainsKey("Amount")){ + return Decimal.Parse(request.AdditionalTransactionMetadata["Amount"]); + } + + return 0; + } + + public static SerialisedMessage CreateSerialisedMessage(this LogonTransactionRequest request){ + SerialisedMessage serialisedMessage = new SerialisedMessage(); + serialisedMessage.Metadata.Add("estate_id", request.EstateId.ToString()); + serialisedMessage.Metadata.Add("merchant_id", request.MerchantId.ToString()); + serialisedMessage.SerialisedData = JsonConvert.SerializeObject(request, + new JsonSerializerSettings + { + TypeNameHandling = TypeNameHandling.All + }); + + return serialisedMessage; + } + + public static SerialisedMessage CreateSerialisedMessage(this SaleTransactionRequest request) + { + SerialisedMessage serialisedMessage = new SerialisedMessage(); + serialisedMessage.Metadata.Add("estate_id", request.EstateId.ToString()); + serialisedMessage.Metadata.Add("merchant_id", request.MerchantId.ToString()); + serialisedMessage.SerialisedData = JsonConvert.SerializeObject(request, + new JsonSerializerSettings + { + TypeNameHandling = TypeNameHandling.All + }); + + return serialisedMessage; + } + + public static T GetSerialisedMessageResponseDTO(this SerialisedMessage serialisedMessage){ + return JsonConvert.DeserializeObject(serialisedMessage.SerialisedData); + } + + #endregion +} \ No newline at end of file diff --git a/TransactionProcessor.DataGenerator/TransactionProcessing.DataGeneration/ITransactionDataGenerator.cs b/TransactionProcessor.DataGenerator/TransactionProcessing.DataGeneration/ITransactionDataGenerator.cs new file mode 100644 index 0000000..e04b022 --- /dev/null +++ b/TransactionProcessor.DataGenerator/TransactionProcessing.DataGeneration/ITransactionDataGenerator.cs @@ -0,0 +1,20 @@ +namespace TransactionProcessing.DataGeneration; + +using EstateManagement.DataTransferObjects.Responses; + +public interface ITransactionDataGenerator{ + #region Methods + + List GenerateDateRange(DateTime startDate, DateTime endDate); + + Task> GetMerchantContracts(MerchantResponse merchant, CancellationToken cancellationToken); + Task> GetMerchants(Guid estateId, CancellationToken cancellationToken); + Task PerformMerchantLogon(DateTime dateTime, MerchantResponse merchant, CancellationToken cancellationToken); + Task PerformSettlement(DateTime dateTime, Guid estateId, CancellationToken cancellationToken); + + Task SendSales(DateTime dateTime, MerchantResponse merchant, ContractResponse contract, CancellationToken cancellationToken); + + Task SendUploadFile(DateTime dateTime, ContractResponse contract, MerchantResponse merchant, CancellationToken cancellationToken); + + #endregion +} \ No newline at end of file diff --git a/TransactionProcessor.DataGenerator/TransactionProcessing.DataGeneration/MobileTopupUploadFile.cs b/TransactionProcessor.DataGenerator/TransactionProcessing.DataGeneration/MobileTopupUploadFile.cs new file mode 100644 index 0000000..97ec722 --- /dev/null +++ b/TransactionProcessor.DataGenerator/TransactionProcessing.DataGeneration/MobileTopupUploadFile.cs @@ -0,0 +1,20 @@ +namespace TransactionProcessing.DataGeneration; + +public class MobileTopupUploadFile : UploadFile{ + #region Constructors + + public MobileTopupUploadFile(Guid estateId, Guid merchantId, Guid fileProfileId, Guid userId) : base(estateId, merchantId, fileProfileId,userId){ + } + + #endregion + + #region Methods + + public void AddLine(Decimal amount, String mobileNumber){ + this.TotalValue += amount; + this.FileLines.Add($"D,{mobileNumber},{amount}"); + this.FileLineCount++; + } + + #endregion +} \ No newline at end of file diff --git a/TransactionProcessor.DataGenerator/TransactionProcessing.DataGeneration/TransactionDataGenerator.cs b/TransactionProcessor.DataGenerator/TransactionProcessing.DataGeneration/TransactionDataGenerator.cs new file mode 100644 index 0000000..aefa683 --- /dev/null +++ b/TransactionProcessor.DataGenerator/TransactionProcessing.DataGeneration/TransactionDataGenerator.cs @@ -0,0 +1,542 @@ +namespace TransactionProcessing.DataGeneration; + +using System.Net.Http.Headers; +using System.Text; +using EstateManagement.Client; +using EstateManagement.DataTransferObjects; +using EstateManagement.DataTransferObjects.Requests; +using EstateManagement.DataTransferObjects.Responses; +using Newtonsoft.Json; +using SecurityService.Client; +using SecurityService.DataTransferObjects.Responses; +using TransactionProcessor.Client; +using TransactionProcessor.DataTransferObjects; + +public class TransactionDataGenerator : ITransactionDataGenerator{ + #region Fields + + private readonly String ClientId; + + private readonly String ClientSecret; + + private readonly IEstateClient EstateClient; + + private readonly String FileProcessorApi; + + private readonly Random r = new Random(); + + private readonly RunningMode RunningMode; + + private readonly ISecurityServiceClient SecurityServiceClient; + + private String TestHostApi; + + private TokenResponse TokenResponse; + + private Int32 TransactionNumber; + + private readonly ITransactionProcessorClient TransactionProcessorClient; + + #endregion + + #region Constructors + + public TransactionDataGenerator(ISecurityServiceClient securityServiceClient, + IEstateClient estateClient, + ITransactionProcessorClient transactionProcessorClient, + String fileProcessorApi, + String testHostApi, + String clientId, + String clientSecret, + RunningMode runningMode = RunningMode.WhatIf){ + this.SecurityServiceClient = securityServiceClient; + this.EstateClient = estateClient; + this.TransactionProcessorClient = transactionProcessorClient; + this.FileProcessorApi = fileProcessorApi; + this.TestHostApi = testHostApi; + + this.ClientId = clientId; + this.ClientSecret = clientSecret; + this.RunningMode = runningMode; + } + + #endregion + + #region Methods + + public List GenerateDateRange(DateTime startDate, DateTime endDate){ + List dateRange = new List(); + + if (endDate.Subtract(startDate).Days == 0){ + dateRange.Add(startDate); + } + else{ + while (endDate.Subtract(startDate).Days >= 0){ + dateRange.Add(startDate); + startDate = startDate.AddDays(1); + } + } + + return dateRange; + } + + public async Task> GetMerchantContracts(MerchantResponse merchant, CancellationToken cancellationToken){ + String token = await this.GetAuthToken(cancellationToken); + return await this.EstateClient.GetMerchantContracts(token, merchant.EstateId, merchant.MerchantId, cancellationToken); + } + + public async Task> GetMerchants(Guid estateId, CancellationToken cancellationToken){ + String token = await this.GetAuthToken(cancellationToken); + return await this.EstateClient.GetMerchants(token, estateId, cancellationToken); + } + + public async Task PerformMerchantLogon(DateTime dateTime, MerchantResponse merchant, CancellationToken cancellationToken){ + // Build logon message + String deviceIdentifier = merchant.Devices.Single().Value; + LogonTransactionRequest logonTransactionRequest = new LogonTransactionRequest + { + DeviceIdentifier = deviceIdentifier, + EstateId = merchant.EstateId, + MerchantId = merchant.MerchantId, + TransactionDateTime = dateTime.Date.AddMinutes(1), + TransactionNumber = "1", + TransactionType = "Logon" + }; + + await this.SendLogonTransaction(merchant, logonTransactionRequest, cancellationToken); + } + + public async Task PerformSettlement(DateTime dateTime, Guid estateId, CancellationToken cancellationToken){ + await this.SendProcessSettlementRequest(dateTime, estateId, cancellationToken); + } + + public async Task SendSales(DateTime dateTime, MerchantResponse merchant, ContractResponse contract, CancellationToken cancellationToken){ + List salesToSend = new List(); + + Decimal depositAmount = 0; + (Int32 accountNumber, String accountName, Decimal balance) billDetails = default; + foreach (ContractProduct contractProduct in contract.Products){ + Console.WriteLine($"product [{contractProduct.DisplayText}]"); + + List<(SaleTransactionRequest request, Decimal amount)> saleRequests = null; + // Get a number of sales to be sent + Int32 numberOfSales = this.r.Next(5, 15); + for (Int32 i = 1; i <= numberOfSales; i++){ + ProductType productType = this.GetProductType(contract.OperatorName); + + if (productType == ProductType.BillPayment){ + // Create a bill for this sale + Decimal amount = this.GetAmount(contractProduct); + billDetails = await this.CreateBillPaymentBill(contract.OperatorName, contractProduct, cancellationToken); + } + + saleRequests = productType switch{ + ProductType.MobileTopup => this.BuildMobileTopupSaleRequests(dateTime, merchant, contract, contractProduct), + ProductType.Voucher => this.BuildVoucherSaleRequests(dateTime, merchant, contract, contractProduct), + ProductType.BillPayment => this.BuildBillPaymentSaleRequests(dateTime, merchant, contract, contractProduct, billDetails), + _ => throw new Exception($"Product Type [{productType}] not yet supported") + }; + + // Add the value of the sale to the deposit amount + Boolean addToDeposit = i switch{ + _ when i == numberOfSales => false, + _ => true + }; + + if (addToDeposit){ + depositAmount += saleRequests.Sum(sr => sr.amount); + } + + salesToSend.AddRange(saleRequests.Select(s => s.request)); + } + } + + // Build up a deposit (minus the last sale amount) + MakeMerchantDepositRequest depositRequest = this.CreateMerchantDepositRequest(depositAmount, dateTime); + + // Send the deposit + await this.SendMerchantDepositRequest(merchant, depositRequest, cancellationToken); + + IOrderedEnumerable orderedSales = salesToSend.OrderBy(s => s.TransactionDateTime); + // Send the sales to the host + foreach (SaleTransactionRequest sale in orderedSales){ + sale.TransactionNumber = this.GetTransactionNumber().ToString(); + await this.SendSaleTransaction(merchant, sale, cancellationToken); + } + } + + public async Task SendUploadFile(DateTime dateTime, ContractResponse contract, MerchantResponse merchant, CancellationToken cancellationToken){ + Int32 numberOfSales = this.r.Next(5, 15); + UploadFile uploadFile = await this.BuildUploadFile(dateTime, merchant, contract, numberOfSales, cancellationToken); + + if (uploadFile == null){ + return; + } + if (this.RunningMode == RunningMode.WhatIf){ + Console.WriteLine($"Send File for Merchant [{merchant.MerchantName}] Contract [{contract.OperatorName}] Lines [{uploadFile.GetNumberOfLines()}]"); + return; + } + + await this.UploadFile(uploadFile, Guid.Empty, dateTime, cancellationToken); + } + + private List<(SaleTransactionRequest request, Decimal amount)> BuildBillPaymentSaleRequests(DateTime dateTime, MerchantResponse merchant, ContractResponse contract, ContractProduct product, (Int32 accountNumber, String accountName, Decimal balance) billDetails){ + List<(SaleTransactionRequest request, Decimal amount)> requests = new List<(SaleTransactionRequest request, Decimal amount)>(); + + // Create the requests required + String deviceIdentifier = merchant.Devices.Single().Value; + + // First request is Get Account + Dictionary getAccountRequestMetaData = new Dictionary{ + { "CustomerAccountNumber", billDetails.accountNumber.ToString() }, + { "PataPawaPostPaidMessageType", "VerifyAccount" } + }; + + var transactionDateTime = this.GetTransactionDateTime(dateTime); + + SaleTransactionRequest getAccountRequest = new SaleTransactionRequest{ + AdditionalTransactionMetadata = getAccountRequestMetaData, + ContractId = contract.ContractId, + CustomerEmailAddress = String.Empty, + DeviceIdentifier = deviceIdentifier, + MerchantId = merchant.MerchantId, + EstateId = merchant.EstateId, + TransactionType = "Sale", + TransactionDateTime = transactionDateTime, + OperatorIdentifier = contract.OperatorName, + ProductId = product.ProductId + }; + + requests.Add((getAccountRequest, 0)); + + // Second request is Make Payment + Dictionary makePaymentRequestMetaData = new Dictionary{ + { "CustomerAccountNumber", billDetails.accountNumber.ToString() }, + { "CustomerName", billDetails.accountName }, + { "MobileNumber", "1234567890" }, + { "Amount", billDetails.balance.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(30), + OperatorIdentifier = contract.OperatorName, + ProductId = product.ProductId + }; + + requests.Add((makePaymentRequest, billDetails.balance)); + + return requests; + } + + private List<(SaleTransactionRequest request, Decimal amount)> BuildMobileTopupSaleRequests(DateTime dateTime, MerchantResponse merchant, ContractResponse contract, ContractProduct contractProduct){ + Decimal amount = this.GetAmount(contractProduct); + + Dictionary requestMetaData = new Dictionary{ + { "Amount", amount.ToString() }, + { "CustomerAccountNumber", "1234567890" } + }; + + List<(SaleTransactionRequest request, Decimal amount)> requests = new List<(SaleTransactionRequest request, Decimal amount)>(); + + 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 = this.GetTransactionDateTime(dateTime), + OperatorIdentifier = contract.OperatorName, + ProductId = contractProduct.ProductId + }; + requests.Add((request, amount)); + + return requests; + } + + private async Task BuildUploadFile(DateTime dateTime, MerchantResponse merchant, ContractResponse contract, Int32 numberOfLines, CancellationToken cancellationToken){ + ProductType productType = this.GetProductType(contract.OperatorName); + Guid fileProfileId = await TransactionDataGenerator.GetFileProfileIdFromOperator(contract.OperatorName, cancellationToken); + String token = await this.GetAuthToken(cancellationToken); + EstateResponse estate = await this.EstateClient.GetEstate(token, merchant.EstateId, cancellationToken); + Guid userId = estate.SecurityUsers.First().SecurityUserId; + + if (productType == ProductType.MobileTopup){ + MobileTopupUploadFile mobileTopupUploadFile = new MobileTopupUploadFile(contract.EstateId, merchant.MerchantId, fileProfileId, userId); + mobileTopupUploadFile.AddHeader(dateTime); + + for (Int32 i = 0; i < numberOfLines; i++){ + Decimal amount = this.GetAmount(); + String mobileNumber = String.Format($"077777777{i.ToString().PadLeft(2, '0')}"); + mobileTopupUploadFile.AddLine(amount, mobileNumber); + } + + mobileTopupUploadFile.AddTrailer(); + return mobileTopupUploadFile; + } + + if (productType == ProductType.Voucher){ + VoucherTopupUploadFile voucherTopupUploadFile = new VoucherTopupUploadFile(contract.EstateId, merchant.MerchantId, fileProfileId, userId); + voucherTopupUploadFile.AddHeader(dateTime); + + for (Int32 i = 0; i < numberOfLines; i++){ + Decimal amount = this.GetAmount(); + String mobileNumber = String.Format($"077777777{i.ToString().PadLeft(2, '0')}"); + String emailAddress = String.Format($"testrecipient{i.ToString().PadLeft(2, '0')}@testing.com"); + String recipient = mobileNumber; + if (i % 2 == 0){ + recipient = emailAddress; + } + + voucherTopupUploadFile.AddLine(amount, recipient, contract.Description.Replace("Contract", "")); + } + + voucherTopupUploadFile.AddTrailer(); + return voucherTopupUploadFile; + } + + // Not supported product type for file upload + return null; + } + + private List<(SaleTransactionRequest request, Decimal amount)> BuildVoucherSaleRequests(DateTime dateTime, MerchantResponse merchant, ContractResponse contract, ContractProduct contractProduct){ + Decimal amount = this.GetAmount(contractProduct); + + Dictionary requestMetaData = new Dictionary{ + { "Amount", amount.ToString() }, + { "RecipientMobile", "1234567890" } + }; + + List<(SaleTransactionRequest request, Decimal amount)> requests = new List<(SaleTransactionRequest request, Decimal amount)>(); + + 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 = this.GetTransactionDateTime(dateTime), + OperatorIdentifier = contract.OperatorName, + ProductId = contractProduct.ProductId + }; + requests.Add((request, amount)); + + return requests; + } + + private async Task<(Int32 accountNumber, String accountName, Decimal balance)> CreateBillPaymentBill(String contractOperatorName, ContractProduct contractProduct, CancellationToken cancellationToken){ + if (contractOperatorName == "PataPawa PostPay"){ + Int32 accountNumber = this.r.Next(1, 100000); + Decimal amount = this.GetAmount(contractProduct); + + 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"); + + if (this.RunningMode == RunningMode.WhatIf){ + Console.WriteLine($"Bill Created Account [{body.account_number}] Balance [{body.amount}]"); + return (body.account_number, body.account_name, body.amount); + } + + using(HttpClient client = new HttpClient()){ + await client.SendAsync(request, cancellationToken); + } + + return (body.account_number, body.account_name, body.amount); + } + + return default; + } + + private MakeMerchantDepositRequest CreateMerchantDepositRequest(Decimal depositAmount, DateTime dateTime){ + // TODO: generate a reference + + MakeMerchantDepositRequest request = new MakeMerchantDepositRequest{ + Amount = depositAmount, + DepositDateTime = dateTime, + Reference = "ABC" + }; + return request; + } + + private Decimal GetAmount(ContractProduct product = null){ + if (product == null){ + return this.r.Next(9, 250); + } + + return product.Value.HasValue switch{ + true => product.Value.Value, + _ => this.r.Next(9, 250) + }; + } + + private async Task GetAuthToken(CancellationToken cancellationToken){ + if (this.TokenResponse == null){ + TokenResponse token = await this.SecurityServiceClient.GetToken(this.ClientId, this.ClientSecret, cancellationToken); + this.TokenResponse = token; + } + + if (this.TokenResponse.Expires.UtcDateTime.Subtract(DateTime.UtcNow) < TimeSpan.FromMinutes(2)){ + TokenResponse token = await this.SecurityServiceClient.GetToken(this.ClientId, this.ClientSecret, cancellationToken); + this.TokenResponse = token; + } + + return this.TokenResponse.AccessToken; + } + + private static async Task GetFileProfileIdFromOperator(String operatorName, CancellationToken cancellationToken){ + // TODO: get this profile list from API + + switch(operatorName){ + case "Safaricom": + return Guid.Parse("B2A59ABF-293D-4A6B-B81B-7007503C3476"); + case "Voucher": + return Guid.Parse("8806EDBC-3ED6-406B-9E5F-A9078356BE99"); + default: + return Guid.Empty; + } + } + + private ProductType GetProductType(String operatorName){ + ProductType productType = ProductType.NotSet; + switch(operatorName){ + case "Safaricom": + productType = ProductType.MobileTopup; + break; + case "Voucher": + productType = ProductType.Voucher; + break; + case "PataPawa PostPay": + productType = ProductType.BillPayment; + break; + } + + return productType; + } + + private DateTime GetTransactionDateTime(DateTime dateTime){ + // Generate the time + Int32 hours = this.r.Next(0, 23); + Int32 minutes = this.r.Next(0, 59); + Int32 seconds = this.r.Next(0, 59); + + return dateTime.AddHours(hours).AddMinutes(minutes).AddSeconds(seconds); + } + + private Int32 GetTransactionNumber(){ + this.TransactionNumber++; + return this.TransactionNumber; + } + + private async Task SendMerchantDepositRequest(MerchantResponse merchant, MakeMerchantDepositRequest request, CancellationToken cancellationToken){ + if (this.RunningMode == RunningMode.WhatIf){ + Console.WriteLine($"Make Deposit [{request.Amount}] for Merchant [{merchant.MerchantName}]"); + return; + } + String token = await this.GetAuthToken(cancellationToken); + MakeMerchantDepositResponse response = await this.EstateClient.MakeMerchantDeposit(token, merchant.EstateId, merchant.MerchantId, request, cancellationToken); + Console.WriteLine($"Deposit [{request.Amount}] made for Merchant [{merchant.MerchantName}]"); + } + + private async Task SendSaleTransaction(MerchantResponse merchant, SaleTransactionRequest request, CancellationToken cancellationToken){ + if (this.RunningMode == RunningMode.WhatIf){ + Console.WriteLine($"Send Sale for Merchant [{merchant.MerchantName}] - {request.TransactionNumber} - {request.OperatorIdentifier} - {request.GetAmount()}"); + return; + } + + String token = await this.GetAuthToken(cancellationToken); + SerialisedMessage requestSerialisedMessage = request.CreateSerialisedMessage(); + + SerialisedMessage responseSerialisedMessage = + await this.TransactionProcessorClient.PerformTransaction(token, requestSerialisedMessage, CancellationToken.None); + + SaleTransactionResponse saleTransactionResponse = responseSerialisedMessage.GetSerialisedMessageResponseDTO(); + + Console.WriteLine($"Sale Transaction for Merchant [{merchant.MerchantName}] sent"); + } + + private async Task SendLogonTransaction(MerchantResponse merchant, LogonTransactionRequest request, CancellationToken cancellationToken) + { + if (this.RunningMode == RunningMode.WhatIf) + { + Console.WriteLine($"Send Logon Transaction for Merchant [{merchant.MerchantName}]"); + return; + } + + String token = await this.GetAuthToken(cancellationToken); + SerialisedMessage requestSerialisedMessage = request.CreateSerialisedMessage(); + + SerialisedMessage responseSerialisedMessage = + await this.TransactionProcessorClient.PerformTransaction(token, requestSerialisedMessage, CancellationToken.None); + + SaleTransactionResponse saleTransactionResponse = responseSerialisedMessage.GetSerialisedMessageResponseDTO(); + + Console.WriteLine($"Logon Transaction for Merchant [{merchant.MerchantName}] sent"); + } + + private async Task UploadFile(UploadFile uploadFile, Guid userId, DateTime fileDateTime, CancellationToken cancellationToken){ + var formData = new MultipartFormDataContent(); + String token = await this.GetAuthToken(cancellationToken); + + var fileContent = new ByteArrayContent(uploadFile.GetFileContents()); + fileContent.Headers.ContentType = MediaTypeHeaderValue.Parse("multipart/form-data"); + formData.Add(fileContent, "file", $"bulkfile{fileDateTime:yyyy-MM-dd HH:mm:ss}"); + formData.Add(new StringContent(uploadFile.EstateId.ToString()), "request.EstateId"); + formData.Add(new StringContent(uploadFile.MerchantId.ToString()), "request.MerchantId"); + formData.Add(new StringContent(uploadFile.FileProfileId.ToString()), "request.FileProfileId"); + formData.Add(new StringContent(userId.ToString()), "request.UserId"); + formData.Add(new StringContent(fileDateTime.ToString("yyyy-MM-dd HH:mm:ss")), "request.UploadDateTime"); + + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, $"{this.FileProcessorApi}/api/files"){ + Content = formData, + }; + + request.Headers.Authorization = new AuthenticationHeaderValue("bearer", token); + HttpResponseMessage response = null; + + using(HttpClient client = new HttpClient()){ + response = await client.SendAsync(request, cancellationToken); + } + } + + private async Task SendProcessSettlementRequest(DateTime dateTime, Guid estateId, CancellationToken cancellationToken) + { + if (this.RunningMode == RunningMode.WhatIf) + { + Console.WriteLine($"Sending Settlement for Date [{dateTime.Date}] Estate [{estateId}]"); + return; + } + + String token = await this.GetAuthToken(cancellationToken); + await this.TransactionProcessorClient.ProcessSettlement(token, dateTime, estateId, cancellationToken); + } + + #endregion +} + +public enum RunningMode +{ + WhatIf, + + Live +} \ No newline at end of file diff --git a/TransactionProcessor.DataGenerator/TransactionProcessing.DataGeneration/TransactionProcessing.DataGeneration.csproj b/TransactionProcessor.DataGenerator/TransactionProcessing.DataGeneration/TransactionProcessing.DataGeneration.csproj new file mode 100644 index 0000000..1e8d95d --- /dev/null +++ b/TransactionProcessor.DataGenerator/TransactionProcessing.DataGeneration/TransactionProcessing.DataGeneration.csproj @@ -0,0 +1,15 @@ + + + + net7.0 + enable + enable + + + + + + + + + diff --git a/TransactionProcessor.DataGenerator/TransactionProcessing.DataGeneration/UploadFile.cs b/TransactionProcessor.DataGenerator/TransactionProcessing.DataGeneration/UploadFile.cs new file mode 100644 index 0000000..47eb582 --- /dev/null +++ b/TransactionProcessor.DataGenerator/TransactionProcessing.DataGeneration/UploadFile.cs @@ -0,0 +1,69 @@ +namespace TransactionProcessing.DataGeneration; + +using System.Text; + +public abstract class UploadFile{ + #region Fields + + protected Int32 FileLineCount; + + protected List FileLines; + + protected Decimal TotalValue; + + #endregion + + #region Constructors + + protected UploadFile(Guid estateId, Guid merchantId, Guid fileProfileId, Guid userId) + { + this.UserId = userId; + this.FileLines = new List(); + this.EstateId = estateId; + this.MerchantId = merchantId; + this.FileProfileId = fileProfileId; + } + + #endregion + + #region Properties + + public Guid EstateId{ get; protected set; } + public Guid FileProfileId{ get; protected set; } + public Guid MerchantId{ get; protected set; } + + public Guid UserId { get; protected set; } + + #endregion + + #region Methods + + public void AddHeader(DateTime dateTime){ + this.FileLines.Add($"H,{dateTime:yyyy-MM-dd-HH-mm-ss}"); + } + + public void AddTrailer(){ + this.FileLines.Add($"T,{this.FileLineCount}"); + } + + public Byte[] GetFileContents(){ + StringBuilder fileData = new StringBuilder(); + String? header = this.FileLines.SingleOrDefault(f => f.StartsWith("H")); + fileData.Append(header); + + List detailLines = this.FileLines.Where(f => f.StartsWith("D")).ToList(); + foreach (String detailLine in detailLines){ + fileData.AppendLine(detailLine); + } + String? trailer = this.FileLines.SingleOrDefault(f => f.StartsWith("T")); + fileData.Append(trailer); + + return Encoding.UTF8.GetBytes(fileData.ToString()); + } + + public Int32 GetNumberOfLines(){ + return this.FileLines.Count; + } + + #endregion +} \ No newline at end of file diff --git a/TransactionProcessor.DataGenerator/TransactionProcessing.DataGeneration/VoucherTopupUploadFile.cs b/TransactionProcessor.DataGenerator/TransactionProcessing.DataGeneration/VoucherTopupUploadFile.cs new file mode 100644 index 0000000..2930fc3 --- /dev/null +++ b/TransactionProcessor.DataGenerator/TransactionProcessing.DataGeneration/VoucherTopupUploadFile.cs @@ -0,0 +1,20 @@ +namespace TransactionProcessing.DataGeneration; + +public class VoucherTopupUploadFile : UploadFile{ + #region Constructors + + public VoucherTopupUploadFile(Guid estateId, Guid merchantId, Guid fileProfileId, Guid userId) : base(estateId, merchantId, fileProfileId, userId){ + } + + #endregion + + #region Methods + + public void AddLine(Decimal amount, String recipient, String issuerName){ + this.TotalValue += amount; + this.FileLines.Add($"D,{issuerName},{recipient},{amount}"); + this.FileLineCount++; + } + + #endregion +} \ No newline at end of file diff --git a/TransactionProcessor.DataGenerator/TransactionProcessor.DataGenerator.sln b/TransactionProcessor.DataGenerator/TransactionProcessor.DataGenerator.sln index 87c260b..006332d 100644 --- a/TransactionProcessor.DataGenerator/TransactionProcessor.DataGenerator.sln +++ b/TransactionProcessor.DataGenerator/TransactionProcessor.DataGenerator.sln @@ -1,26 +1,26 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.30523.141 +# Visual Studio Version 17 +VisualStudioVersion = 17.6.33606.364 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TransactionGeneratorWorker", "TransactionGeneratorWorker\TransactionGeneratorWorker.csproj", "{DF5050CF-91D8-4248-BEF6-25FAF1A05677}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TransactionDataGenerator", "DataGenerator\TransactionDataGenerator.csproj", "{BC85594F-6F0B-425D-868F-560B9A3AC104}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TransactionProcessing.DataGeneration", "TransactionProcessing.DataGeneration\TransactionProcessing.DataGeneration.csproj", "{7476752B-9954-4039-879A-4154F9426D64}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {DF5050CF-91D8-4248-BEF6-25FAF1A05677}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DF5050CF-91D8-4248-BEF6-25FAF1A05677}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DF5050CF-91D8-4248-BEF6-25FAF1A05677}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DF5050CF-91D8-4248-BEF6-25FAF1A05677}.Release|Any CPU.Build.0 = Release|Any CPU {BC85594F-6F0B-425D-868F-560B9A3AC104}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BC85594F-6F0B-425D-868F-560B9A3AC104}.Debug|Any CPU.Build.0 = Debug|Any CPU {BC85594F-6F0B-425D-868F-560B9A3AC104}.Release|Any CPU.ActiveCfg = Release|Any CPU {BC85594F-6F0B-425D-868F-560B9A3AC104}.Release|Any CPU.Build.0 = Release|Any CPU + {7476752B-9954-4039-879A-4154F9426D64}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7476752B-9954-4039-879A-4154F9426D64}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7476752B-9954-4039-879A-4154F9426D64}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7476752B-9954-4039-879A-4154F9426D64}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE