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