diff --git a/Directory.Packages.props b/Directory.Packages.props
index c62d98aa..00a12cfe 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -84,6 +84,7 @@
+
diff --git a/TransactionProcessor.BusinessLogic.Tests/OperatorInterfaces/AgencyBankingProxyTests.cs b/TransactionProcessor.BusinessLogic.Tests/OperatorInterfaces/AgencyBankingProxyTests.cs
new file mode 100644
index 00000000..05ad3ec9
--- /dev/null
+++ b/TransactionProcessor.BusinessLogic.Tests/OperatorInterfaces/AgencyBankingProxyTests.cs
@@ -0,0 +1,216 @@
+using Moq;
+using Moq.Protected;
+using Shared.Logger;
+using Shared.Serialisation;
+using Shouldly;
+using SimpleResults;
+using System;
+using System.Collections.Generic;
+using System.Net;
+using System.Net.Http;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using TransactionProcessor.BusinessLogic.OperatorInterfaces;
+using TransactionProcessor.BusinessLogic.OperatorInterfaces.AgencyBanking;
+using TransactionProcessor.BusinessLogic.OperatorInterfaces.SafaricomPinless;
+using TransactionProcessor.Testing;
+using Xunit;
+
+namespace TransactionProcessor.BusinessLogic.Tests.OperatorInterfaces
+{
+ public class AgencyBankingProxyTests
+ {
+ public AgencyBankingProxyTests()
+ {
+ Logger.Initialise(NullLogger.Instance);
+ StringSerialiser.Initialise(new SystemTextJsonSerializer(new JsonSerializerOptions()));
+ }
+
+ [Fact]
+ public async Task AgencyBankingProxy_ProcessLogonMessage_ReturnsSuccessfulResult()
+ {
+ HttpClient httpClient = SetupMockHttpClient(new HttpResponseMessage(HttpStatusCode.OK));
+ AgencyBankingConfiguration configuration = CreateConfiguration();
+ IOperatorProxy proxy = new AgencyBankingProxy(httpClient, configuration);
+
+ var result = await proxy.ProcessLogonMessage(CancellationToken.None);
+
+ result.IsSuccess.ShouldBeTrue();
+ result.Data.ShouldBeNull();
+ }
+
+ [Fact]
+ public async Task AgencyBankingProxy_ProcessSaleMessage_MissingMessageType_ReturnsInvalidResult()
+ {
+ HttpClient httpClient = SetupMockHttpClient(new HttpResponseMessage(HttpStatusCode.OK));
+ AgencyBankingConfiguration configuration = CreateConfiguration();
+ IOperatorProxy proxy = new AgencyBankingProxy(httpClient, configuration);
+
+ Dictionary metadata = new Dictionary
+ {
+ { "AgencyBankingAccountNumber", "123456789" }
+ };
+
+ var result = await proxy.ProcessSaleMessage(TestData.TransactionId,
+ TestData.OperatorId,
+ TestData.Merchant,
+ TestData.TransactionDateTime,
+ TestData.TransactionReference,
+ metadata,
+ CancellationToken.None);
+
+ result.IsFailed.ShouldBeTrue();
+ result.Status.ShouldBe(ResultStatus.Invalid);
+ result.Message.ShouldBe("AgencyBankingMessageType - Message Type is a required field for this transaction type");
+ }
+
+ [Fact]
+ public async Task AgencyBankingProxy_ProcessSaleMessage_MissingAccountNumber_ReturnsInvalidResult()
+ {
+ HttpClient httpClient = SetupMockHttpClient(new HttpResponseMessage(HttpStatusCode.OK));
+ AgencyBankingConfiguration configuration = CreateConfiguration();
+ IOperatorProxy proxy = new AgencyBankingProxy(httpClient, configuration);
+
+ Dictionary metadata = new Dictionary
+ {
+ { "AgencyBankingMessageType", "balanceenquiry" }
+ };
+
+ var result = await proxy.ProcessSaleMessage(TestData.TransactionId,
+ TestData.OperatorId,
+ TestData.Merchant,
+ TestData.TransactionDateTime,
+ TestData.TransactionReference,
+ metadata,
+ CancellationToken.None);
+
+ result.IsFailed.ShouldBeTrue();
+ result.Status.ShouldBe(ResultStatus.Invalid);
+ result.Message.ShouldBe("AgencyBankingAccountNumber - Account Number is a required field for this transaction type");
+ }
+
+ [Fact]
+ public async Task AgencyBankingProxy_ProcessSaleMessage_UnsupportedMessageType_ReturnsInvalidResult()
+ {
+ HttpClient httpClient = SetupMockHttpClient(new HttpResponseMessage(HttpStatusCode.OK));
+ AgencyBankingConfiguration configuration = CreateConfiguration();
+ IOperatorProxy proxy = new AgencyBankingProxy(httpClient, configuration);
+
+ Dictionary metadata = new Dictionary
+ {
+ { "AgencyBankingMessageType", "cashwithdrawal" },
+ { "AgencyBankingAccountNumber", "123456789" }
+ };
+
+ var result = await proxy.ProcessSaleMessage(TestData.TransactionId,
+ TestData.OperatorId,
+ TestData.Merchant,
+ TestData.TransactionDateTime,
+ TestData.TransactionReference,
+ metadata,
+ CancellationToken.None);
+
+ result.IsFailed.ShouldBeTrue();
+ result.Status.ShouldBe(ResultStatus.Invalid);
+ result.Message.ShouldBe("AgencyBankingMessageType - Message Type cashwithdrawal is not supported for this transaction type");
+ }
+
+ [Fact]
+ public async Task AgencyBankingProxy_ProcessSaleMessage_BalanceEnquiryFailure_ReturnsInvalidResult()
+ {
+ HttpResponseMessage responseMessage = new HttpResponseMessage(HttpStatusCode.BadRequest)
+ {
+ Content = new StringContent("operator unavailable")
+ };
+
+ HttpRequestMessage capturedRequest = null;
+ HttpClient httpClient = SetupMockHttpClient(responseMessage, request => capturedRequest = request);
+ AgencyBankingConfiguration configuration = CreateConfiguration();
+ IOperatorProxy proxy = new AgencyBankingProxy(httpClient, configuration);
+
+ Dictionary metadata = new Dictionary
+ {
+ { "AgencyBankingMessageType", "balanceenquiry" },
+ { "AgencyBankingAccountNumber", "123456789" }
+ };
+
+ var result = await proxy.ProcessSaleMessage(TestData.TransactionId,
+ TestData.OperatorId,
+ TestData.Merchant,
+ TestData.TransactionDateTime,
+ TestData.TransactionReference,
+ metadata,
+ CancellationToken.None);
+
+ result.IsFailed.ShouldBeTrue();
+ result.Status.ShouldBe(ResultStatus.Invalid);
+ result.Message.ShouldBe("Failed to process balance enquiry operator unavailable");
+ capturedRequest.ShouldNotBeNull();
+ capturedRequest.Method.ShouldBe(HttpMethod.Post);
+ capturedRequest.RequestUri.ShouldBe(new Uri("http://localhost/transactions/balance-enquiry"));
+ }
+
+ [Fact]
+ public async Task AgencyBankingProxy_ProcessSaleMessage_BalanceEnquirySuccess_ReturnsMappedOperatorResponse()
+ {
+ HttpResponseMessage responseMessage = new HttpResponseMessage(HttpStatusCode.OK)
+ {
+ Content = new StringContent("{\"ResponseCode\":\"0000\",\"ResponseMessage\":\"Balance enquiry successful\",\"AvailableBalance\":2500}")
+ };
+
+ HttpRequestMessage capturedRequest = null;
+ HttpClient httpClient = SetupMockHttpClient(responseMessage, request => capturedRequest = request);
+ AgencyBankingConfiguration configuration = CreateConfiguration();
+ IOperatorProxy proxy = new AgencyBankingProxy(httpClient, configuration);
+
+ Dictionary metadata = new Dictionary
+ {
+ { "agencybankingmessagetype", "balanceenquiry" },
+ { "agencybankingaccountnumber", "123456789" }
+ };
+
+ var result = await proxy.ProcessSaleMessage(TestData.TransactionId,
+ TestData.OperatorId,
+ TestData.Merchant,
+ TestData.TransactionDateTime,
+ TestData.TransactionReference,
+ metadata,
+ CancellationToken.None);
+
+ result.IsSuccess.ShouldBeTrue();
+ result.Data.ShouldNotBeNull();
+ result.Data.IsSuccessful.ShouldBeTrue();
+ result.Data.ResponseCode.ShouldBe("0000");
+ result.Data.AdditionalTransactionResponseMetadata.ShouldContainKey("AgencyBankingBalance");
+
+ capturedRequest.ShouldNotBeNull();
+ capturedRequest.Method.ShouldBe(HttpMethod.Post);
+ capturedRequest.RequestUri.ShouldBe(new Uri("http://localhost/transactions/balance-enquiry"));
+
+ String requestBody = await capturedRequest.Content.ReadAsStringAsync();
+ using JsonDocument requestJson = JsonDocument.Parse(requestBody);
+ requestJson.RootElement.GetProperty("agentId").GetString().ShouldBe(TestData.Merchant.MerchantId.ToString());
+ requestJson.RootElement.GetProperty("accountNumber").GetString().ShouldBe("123456789");
+ }
+
+ private static AgencyBankingConfiguration CreateConfiguration()
+ {
+ return new AgencyBankingConfiguration
+ {
+ Url = "http://localhost"
+ };
+ }
+
+ private static HttpClient SetupMockHttpClient(HttpResponseMessage responseMessage, Action onRequest = null)
+ {
+ Mock handlerMock = new Mock(MockBehavior.Strict);
+ handlerMock.Protected()
+ .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny())
+ .Callback((request, cancellationToken) => onRequest?.Invoke(request))
+ .ReturnsAsync(responseMessage);
+
+ return new HttpClient(handlerMock.Object);
+ }
+ }
+}
diff --git a/TransactionProcessor.BusinessLogic/OperatorInterfaces/AgencyBanking/AgencyBankingConfiguration.cs b/TransactionProcessor.BusinessLogic/OperatorInterfaces/AgencyBanking/AgencyBankingConfiguration.cs
new file mode 100644
index 00000000..db3c1671
--- /dev/null
+++ b/TransactionProcessor.BusinessLogic/OperatorInterfaces/AgencyBanking/AgencyBankingConfiguration.cs
@@ -0,0 +1,13 @@
+namespace TransactionProcessor.BusinessLogic.OperatorInterfaces.SafaricomPinless
+{
+ using System;
+ using System.Diagnostics.CodeAnalysis;
+
+ ///
+ ///
+ ///
+ [ExcludeFromCodeCoverage]
+ public class AgencyBankingConfiguration : BaseOperatorConfiguration
+ {
+ }
+}
\ No newline at end of file
diff --git a/TransactionProcessor.BusinessLogic/OperatorInterfaces/AgencyBanking/AgencyBankingProxy.cs b/TransactionProcessor.BusinessLogic/OperatorInterfaces/AgencyBanking/AgencyBankingProxy.cs
new file mode 100644
index 00000000..39871a0f
--- /dev/null
+++ b/TransactionProcessor.BusinessLogic/OperatorInterfaces/AgencyBanking/AgencyBankingProxy.cs
@@ -0,0 +1,123 @@
+using Shared.Logger;
+using Shared.Serialisation;
+using SimpleResults;
+using System;
+using System.Collections.Generic;
+using System.Net.Http;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using TransactionProcessor.BusinessLogic.Common;
+using TransactionProcessor.BusinessLogic.OperatorInterfaces.PataPawaPostPay;
+using TransactionProcessor.BusinessLogic.OperatorInterfaces.PataPawaPrePay;
+using TransactionProcessor.BusinessLogic.OperatorInterfaces.SafaricomPinless;
+using TransactionProcessor.Database.Entities;
+using TransactionProcessor.Models.Merchant;
+using Merchant = TransactionProcessor.Models.Merchant.Merchant;
+
+namespace TransactionProcessor.BusinessLogic.OperatorInterfaces.AgencyBanking
+{
+ public class AgencyBankingProxy:IOperatorProxy
+
+ {
+ private readonly HttpClient HttpClient;
+ private readonly AgencyBankingConfiguration Configuration;
+ private static SerialiserOptions SerialiserOptions = new SerialiserOptions(SerialiserPropertyFormat.CamelCase);
+ public AgencyBankingProxy(HttpClient httpClient, AgencyBankingConfiguration configuration) {
+ HttpClient = httpClient;
+ this.Configuration = configuration;
+ }
+
+ public async Task> ProcessLogonMessage(CancellationToken cancellationToken) {
+ Logger.LogInformation("AgencyBanking logon requested");
+ return Result.Success();
+ }
+
+ public async Task> ProcessSaleMessage(Guid transactionId,
+ Guid operatorId,
+ Merchant merchant,
+ DateTime transactionDateTime,
+ String transactionReference,
+ Dictionary additionalTransactionMetadata,
+ CancellationToken cancellationToken) {
+ Logger.LogInformation($"AgencyBanking sale request received. TransactionId=[{transactionId}], OperatorId=[{operatorId}], MerchantId=[{merchant.MerchantId}], TransactionDateTime=[{transactionDateTime:o}], TransactionReference=[{transactionReference}]");
+
+ // Check the meta data for the message type
+ String messageType = additionalTransactionMetadata.ExtractFieldFromMetadata("AgencyBankingMessageType");
+
+ if (String.IsNullOrEmpty(messageType))
+ {
+ Logger.LogWarning($"AgencyBanking sale request rejected. TransactionId=[{transactionId}] missing AgencyBankingMessageType");
+ return Result.Invalid("AgencyBankingMessageType - Message Type is a required field for this transaction type");
+ }
+
+ //// Check the meta data for the agent id
+ //String agentId = additionalTransactionMetadata.ExtractFieldFromMetadata("AgencyBankingAgentId");
+
+ //if (String.IsNullOrEmpty(agentId))
+ //{
+ // return Result.Invalid("AgencyBankingAgentId - Agent Id is a required field for this transaction type");
+ //}
+
+ // Check the meta data for the account number
+ String accountNumber = additionalTransactionMetadata.ExtractFieldFromMetadata("AgencyBankingAccountNumber");
+
+ if (String.IsNullOrEmpty(accountNumber))
+ {
+ Logger.LogWarning($"AgencyBanking sale request rejected. TransactionId=[{transactionId}] missing AgencyBankingAccountNumber");
+ return Result.Invalid("AgencyBankingAccountNumber - Account Number is a required field for this transaction type");
+ }
+
+ Logger.LogInformation($"AgencyBanking sale request metadata resolved. TransactionId=[{transactionId}], MessageType=[{messageType}], AccountNumber=[{accountNumber}]");
+
+ if (messageType == "balanceenquiry")
+ {
+ return await ProcessBalanceEnquiry(transactionId, merchant.MerchantId.ToString(), accountNumber, cancellationToken);
+ }
+
+ Logger.LogWarning($"AgencyBanking sale request rejected. TransactionId=[{transactionId}] unsupported message type [{messageType}]");
+ return Result.Invalid($"AgencyBankingMessageType - Message Type {messageType} is not supported for this transaction type");
+ }
+
+ private async Task> ProcessBalanceEnquiry(Guid transactionId,
+ String agentId,
+ String accountNumber,
+ CancellationToken cancellationToken) {
+ HttpRequestMessage request = new(HttpMethod.Post, $"{this.Configuration.Url}/transactions/balance-enquiry");
+
+ BalanceEnquiryRequest body = new BalanceEnquiryRequest(){
+ AgentId = agentId,
+ AccountNumber = accountNumber
+ };
+
+ String requestBody = StringSerialiser.Serialise(body, SerialiserOptions);
+ request.Content = new StringContent(requestBody, Encoding.UTF8, "application/json");
+
+ Logger.LogWarning($"AgencyBanking balance enquiry request starting. TransactionId=[{transactionId}], AgentId=[{agentId}], AccountNumber=[{accountNumber}], RequestUri=[{request.RequestUri}], RequestBody=[{requestBody}]");
+
+ HttpResponseMessage response = await this.HttpClient.SendAsync(request, cancellationToken);
+ String responseContent = await response.Content.ReadAsStringAsync(cancellationToken);
+
+ Logger.LogWarning($"AgencyBanking balance enquiry response received. TransactionId=[{transactionId}], StatusCode=[{response.StatusCode}], ContentLength=[{responseContent?.Length ?? 0}]");
+
+ if (response.IsSuccessStatusCode == false) {
+ Logger.LogWarning($"AgencyBanking balance enquiry failed. TransactionId=[{transactionId}], StatusCode=[{response.StatusCode}], ResponseBody=[{responseContent}]");
+ return Result.Invalid($"Failed to process balance enquiry {responseContent}");
+ }
+
+ BalanceEnquiryResponse balanceEnquiryResponse = StringSerialiser.Deserialise(responseContent, SerialiserOptions);
+ Logger.LogWarning($"AgencyBanking balance enquiry completed. TransactionId=[{transactionId}], ResponseMessage=[{balanceEnquiryResponse.ResponseMessage}], AvailableBalance=[{balanceEnquiryResponse.AvailableBalance}]");
+
+ OperatorResponse operatorResponse = new() {
+ IsSuccessful = true,
+ ResponseCode = "0000",
+ ResponseMessage = balanceEnquiryResponse.ResponseMessage,
+ AdditionalTransactionResponseMetadata = new Dictionary()
+ };
+
+ operatorResponse.AdditionalTransactionResponseMetadata.Add("AgencyBankingBalance", balanceEnquiryResponse.AvailableBalance.ToString());
+
+ return Result.Success(operatorResponse);
+ }
+ }
+}
diff --git a/TransactionProcessor.BusinessLogic/OperatorInterfaces/AgencyBanking/BalanceEnquiryResponse.cs b/TransactionProcessor.BusinessLogic/OperatorInterfaces/AgencyBanking/BalanceEnquiryResponse.cs
new file mode 100644
index 00000000..8dc58b3c
--- /dev/null
+++ b/TransactionProcessor.BusinessLogic/OperatorInterfaces/AgencyBanking/BalanceEnquiryResponse.cs
@@ -0,0 +1,25 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Text;
+
+namespace TransactionProcessor.BusinessLogic.OperatorInterfaces.AgencyBanking
+{
+ [ExcludeFromCodeCoverage]
+ public class BalanceEnquiryRequest
+ {
+ public String AgentId { get; set; }
+
+ public string AccountNumber { get; set; }
+ }
+
+ [ExcludeFromCodeCoverage]
+ public class BalanceEnquiryResponse
+ {
+ public String ResponseCode { get; set; }
+
+ public string ResponseMessage { get; set; }
+
+ public decimal AvailableBalance { get; set; }
+ }
+}
diff --git a/TransactionProcessor.IntegrationTesting.Helpers/SpecflowExtensions.cs b/TransactionProcessor.IntegrationTesting.Helpers/SpecflowExtensions.cs
index 6fdaeeae..f4f3fc01 100644
--- a/TransactionProcessor.IntegrationTesting.Helpers/SpecflowExtensions.cs
+++ b/TransactionProcessor.IntegrationTesting.Helpers/SpecflowExtensions.cs
@@ -574,6 +574,7 @@ public static List ToAutomaticDepositRequests(this DataTableRows tableRo
"Voucher" => BuildVoucherTransactionMetaData(recipientEmail, recipientMobile, transactionAmount),
"PataPawa PostPay" => BuildPataPawaPostPayMetaData(messageType, accountNumber, recipientMobile, customerName, transactionAmount),
"PataPawa PrePay" => BuildPataPawaPrePayMetaData(messageType, meterNumber, customerName, transactionAmount),
+ "AgencyBanking" => BuildAgencyBankingMetaData(messageType, accountNumber),
_ => BuildMobileTopupMetaData(transactionAmount, customerAccountNumber)
};
@@ -652,6 +653,13 @@ private static Dictionary BuildMobileTopupMetaData(Decimal trans
};
}
+ private static Dictionary BuildAgencyBankingMetaData(String messageType, String accountNumber){
+ return new Dictionary{
+ { "AgencyBankingMessageType", messageType },
+ { "AgencyBankingAccountNumber", accountNumber }
+ };
+ }
+
private static Dictionary BuildPataPawaPostPayMetaData(String messageType,
String accountNumber,
String recipientMobile,
@@ -1022,9 +1030,9 @@ public static List ToCreateEstateRequests(this DataTableRow
return requests;
}
- public static List<(EstateDetails estate, CreateMerchantRequest)> ToCreateMerchantRequests(this DataTableRows tableRows, List estateDetailsList)
+ public static List<(EstateDetails estate, CreateMerchantRequest, Boolean EnableAgencyBanking)> ToCreateMerchantRequests(this DataTableRows tableRows, List estateDetailsList)
{
- List<(EstateDetails estate, CreateMerchantRequest)> requests = new();
+ List<(EstateDetails estate, CreateMerchantRequest, Boolean EnableAgencyBanking)> requests = new();
foreach (DataTableRow tableRow in tableRows)
{
String estateName = ReqnrollTableHelper.GetStringRowValue(tableRow, "EstateName");
@@ -1043,34 +1051,21 @@ public static List ToCreateEstateRequests(this DataTableRow
Name = ReqnrollTableHelper.GetStringRowValue(tableRow, "MerchantName"),
Contact = new Contact
{
- ContactName =
- ReqnrollTableHelper.GetStringRowValue(tableRow,
- "ContactName"),
- EmailAddress =
- ReqnrollTableHelper.GetStringRowValue(tableRow,
- "EmailAddress")
+ ContactName = ReqnrollTableHelper.GetStringRowValue(tableRow, "ContactName"),
+ EmailAddress = ReqnrollTableHelper.GetStringRowValue(tableRow, "EmailAddress")
},
Address = new Address
{
- AddressLine1 =
- ReqnrollTableHelper.GetStringRowValue(tableRow,
- "AddressLine1"),
- Town =
- ReqnrollTableHelper.GetStringRowValue(tableRow, "Town"),
- Region =
- ReqnrollTableHelper.GetStringRowValue(tableRow,
- "Region"),
- Country =
- ReqnrollTableHelper.GetStringRowValue(tableRow,
- "Country"),
- PostalCode =
- ReqnrollTableHelper.GetStringRowValue(tableRow,
- "PostalCode")
+ AddressLine1 = ReqnrollTableHelper.GetStringRowValue(tableRow, "AddressLine1"),
+ Town = ReqnrollTableHelper.GetStringRowValue(tableRow, "Town"),
+ Region = ReqnrollTableHelper.GetStringRowValue(tableRow, "Region"),
+ Country = ReqnrollTableHelper.GetStringRowValue(tableRow, "Country"),
+ PostalCode = ReqnrollTableHelper.GetStringRowValue(tableRow, "PostalCode")
},
SettlementSchedule = schedule,
MerchantId = Guid.NewGuid()
};
- requests.Add((estateDetails, createMerchantRequest));
+ requests.Add((estateDetails, createMerchantRequest, ReqnrollTableHelper.GetBooleanValue(tableRow, "AgencyBankingEnabled")));
}
return requests;
diff --git a/TransactionProcessor.IntegrationTesting.Helpers/TransactionProcessor.IntegrationTesting.Helpers.csproj b/TransactionProcessor.IntegrationTesting.Helpers/TransactionProcessor.IntegrationTesting.Helpers.csproj
index 08e1ba90..cb5c6d1d 100644
--- a/TransactionProcessor.IntegrationTesting.Helpers/TransactionProcessor.IntegrationTesting.Helpers.csproj
+++ b/TransactionProcessor.IntegrationTesting.Helpers/TransactionProcessor.IntegrationTesting.Helpers.csproj
@@ -15,6 +15,7 @@
+
diff --git a/TransactionProcessor.IntegrationTesting.Helpers/TransactionProcessorSteps.cs b/TransactionProcessor.IntegrationTesting.Helpers/TransactionProcessorSteps.cs
index f8100d97..aaf27977 100644
--- a/TransactionProcessor.IntegrationTesting.Helpers/TransactionProcessorSteps.cs
+++ b/TransactionProcessor.IntegrationTesting.Helpers/TransactionProcessorSteps.cs
@@ -1,6 +1,8 @@
using EventStore.Client;
using Shared.Serialisation;
using SimpleResults;
+using TestHosts.Clients;
+using TestHosts.DataTransferObjects.AgencyBanking;
using TransactionProcessor.DataTransferObjects.Requests.Contract;
using TransactionProcessor.DataTransferObjects.Requests.Estate;
using TransactionProcessor.DataTransferObjects.Requests.Merchant;
@@ -29,12 +31,16 @@ public class TransactionProcessorSteps
private readonly HttpClient TestHostHttpClient;
private readonly EventStoreProjectionManagementClient ProjectionManagementClient;
+ private readonly IAgencyBankingClient AgencyBankingClient;
- public TransactionProcessorSteps(ITransactionProcessorClient transactionProcessorClient, HttpClient testHostHttpClient, EventStoreProjectionManagementClient projectionManagementClient)
+ public TransactionProcessorSteps(ITransactionProcessorClient transactionProcessorClient,
+ HttpClient testHostHttpClient, EventStoreProjectionManagementClient projectionManagementClient,
+ IAgencyBankingClient agencyBankingClient)
{
this.TransactionProcessorClient = transactionProcessorClient;
this.TestHostHttpClient = testHostHttpClient;
this.ProjectionManagementClient = projectionManagementClient;
+ this.AgencyBankingClient = agencyBankingClient;
}
public async Task WhenIGetTheEstateSettlementReportForEstateForMerchantWithTheStartDateAndTheEndDateTheFollowingDataIsReturned(String accessToken, DateTime stateDate, DateTime endDate, ReqnrollExtensions.SettlementDetails expectedSettlementDetails)
@@ -295,13 +301,13 @@ public async Task WhenIMakeTheFollowingMerchantWithdrawals(String accessToken, L
}
}
- public async Task> WhenICreateTheFollowingMerchants(string accessToken, List<(EstateDetails estate, CreateMerchantRequest request)> requests)
+ public async Task> WhenICreateTheFollowingMerchants(string accessToken, List<(EstateDetails estate, CreateMerchantRequest request, Boolean enableAgencyBanking)> requests)
{
List responses = new List();
List<(Guid, Guid, String)> merchants = new List<(Guid, Guid, String)>();
- foreach ((EstateDetails estate, CreateMerchantRequest request) request in requests)
+ foreach ((EstateDetails estate, CreateMerchantRequest request, Boolean enableAgencyBanking) request in requests)
{
Result? result = await this.TransactionProcessorClient
@@ -310,6 +316,43 @@ public async Task> WhenICreateTheFollowingMerchants(strin
result.IsSuccess.ShouldBeTrue();
+
+ if (request.enableAgencyBanking) {
+ CreateAgentRequest createAgentRequest = new() {
+ AgentId = request.request.MerchantId.Value.ToString(),
+ DailyLimit = 100000,
+ Name = request.request.Name,
+ MinimumFloat = 10000,
+ Email = request.request.Contact.EmailAddress,
+ Location = request.request.Address.AddressLine1,
+ PhoneNumber = request.request.Contact.PhoneNumber
+ };
+ Result? enableAgencyBankingResult = await this.AgencyBankingClient.CreateRetailAgent(createAgentRequest, CancellationToken.None);
+ enableAgencyBankingResult.IsSuccess.ShouldBeTrue();
+
+ // Activate the agent
+ ActivateAgentRequest activateAgentRequest = new ActivateAgentRequest { ActivatedBy = "INTEGRATIONTEST" };
+ var activateAgentResult = await this.AgencyBankingClient.ActivateAgent(createAgentRequest.AgentId, activateAgentRequest, CancellationToken.None);
+ activateAgentResult.IsSuccess.ShouldBeTrue();
+
+ ConfigureFloatRequest configureFloatRequest = new ConfigureFloatRequest {
+ AgentId = createAgentRequest.AgentId, DailyFloatLimit = 100000, MaximumFloat = 100000, MinimumFloat = 10000,
+ };
+ var configureFloatResult = await this.AgencyBankingClient.ConfigureFloat(configureFloatRequest, CancellationToken.None);
+ configureFloatResult.IsSuccess.ShouldBeTrue();
+
+ FloatCreditRequest floatCreditRequest = new FloatCreditRequest() {
+ AgentId = createAgentRequest.AgentId,
+ Amount = 10000,
+ Narration = "Test Credit",
+ SourceAccount = "Test Account",
+ TransactionId = Guid.NewGuid().ToString()
+ };
+ var floatCreditResult = await this.AgencyBankingClient.CreditFloat(floatCreditRequest, CancellationToken.None);
+ floatCreditResult.IsSuccess.ShouldBeTrue();
+ }
+
+
merchants.Add((request.estate.EstateId, request.request.MerchantId.Value, request.request.Name));
}
@@ -352,19 +395,29 @@ private void ValidateTransactionResponse(LogonTransactionResponse logonTransacti
{
logonTransactionResponse.ResponseCode.ShouldBe(expectedResponseCode, $"Transaction Number {transactionNumber} verification failed");
- logonTransactionResponse.ResponseMessage.ShouldBe(expectedResponseMessage, $"Transaction Number {transactionNumber} verification failed");
+ this.ValidateResponseMessage(logonTransactionResponse.ResponseMessage, transactionNumber, expectedResponseMessage);
}
private void ValidateTransactionResponse(SaleTransactionResponse saleTransactionResponse, String transactionNumber, String expectedResponseCode, String expectedResponseMessage)
{
saleTransactionResponse.ResponseCode.ShouldBe(expectedResponseCode, $"Transaction Number {transactionNumber} verification failed");
- saleTransactionResponse.ResponseMessage.ShouldBe(expectedResponseMessage, $"Transaction Number {transactionNumber} verification failed");
+ this.ValidateResponseMessage(saleTransactionResponse.ResponseMessage, transactionNumber, expectedResponseMessage);
}
private void ValidateTransactionResponse(ReconciliationResponse reconciliationResponse, String transactionNumber, String expectedResponseCode, String expectedResponseMessage)
{
reconciliationResponse.ResponseCode.ShouldBe(expectedResponseCode, $"Transaction Number {transactionNumber} verification failed");
- reconciliationResponse.ResponseMessage.ShouldBe(expectedResponseMessage, $"Transaction Number {transactionNumber} verification failed");
+ this.ValidateResponseMessage(reconciliationResponse.ResponseMessage, transactionNumber, expectedResponseMessage);
+ }
+
+ private void ValidateResponseMessage(String actualResponseMessage, String transactionNumber, String expectedResponseMessage)
+ {
+ if (String.IsNullOrWhiteSpace(expectedResponseMessage) || expectedResponseMessage == "*")
+ {
+ return;
+ }
+
+ actualResponseMessage.ShouldBe(expectedResponseMessage, $"Transaction Number {transactionNumber} verification failed");
}
public async Task WhenIRequestTheReceiptIsResent(String accessToken, List transactions)
diff --git a/TransactionProcessor.IntegrationTests/Common/DockerHelper.cs b/TransactionProcessor.IntegrationTests/Common/DockerHelper.cs
index e0e85fce..eb6cd143 100644
--- a/TransactionProcessor.IntegrationTests/Common/DockerHelper.cs
+++ b/TransactionProcessor.IntegrationTests/Common/DockerHelper.cs
@@ -1,5 +1,7 @@
using DotNet.Testcontainers.Builders;
using Syncfusion.Pdf.Xmp;
+using System.IO;
+using TestHosts.Clients;
using TransactionProcessor.Database.Contexts;
namespace TransactionProcessor.IntegrationTests.Common
@@ -7,6 +9,7 @@ namespace TransactionProcessor.IntegrationTests.Common
using Client;
using EventStore.Client;
using global::Shared.IntegrationTesting;
+ using global::Shared.IntegrationTesting.TestContainers;
using global::Shared.Serialisation;
using SecurityService.Client;
using Shouldly;
@@ -34,7 +37,7 @@ public class DockerHelper : global::Shared.IntegrationTesting.TestContainers.Doc
public static String TestBankSortCode = "112233";
public HttpClient TestHostHttpClient;
-
+
///
/// The security service client
///
@@ -49,6 +52,8 @@ public class DockerHelper : global::Shared.IntegrationTesting.TestContainers.Doc
public EventStoreProjectionManagementClient ProjectionManagementClient;
+ public IAgencyBankingClient AgencyBankingClient;
+
#endregion
#region Constructors
@@ -89,17 +94,54 @@ await Retry.For(async () =>
});
}
- public override ContainerBuilder SetupTransactionProcessorContainer(){
+ //public override ContainerBuilder SetupTestHostContainer()
+ //{
+ // var variables = new Dictionary();
+ // variables.Add("ConnectionStrings:AgencyBankingReadModel", $"server={this.SqlServerContainerName};user id={this.SqlCredentials.usename};password={this.SqlCredentials.password};database=AgencyBankingReadModel;Encrypt=false");
+ // variables.Add("ASPNETCORE_ENVIRONMENT", $"PRODUCTION");
+ // this.AdditionalVariables.Add(ContainerType.TestHost, variables);
+
+ // return base.SetupTestHostContainer();
+ //}
- List variables = new List();
- variables.Add($"OperatorConfiguration:PataPawaPrePay:Url=http://{this.TestHostContainerName}:{DockerPorts.TestHostPort}/api/patapawaprepay");
+ public override ContainerBuilder SetupTransactionProcessorContainer() {
+ Dictionary additionalVariables = new();
+ additionalVariables.Add("OperatorConfiguration:AgencyBanking:Url",$"http://{this.TestHostContainerName}:{DockerPorts.TestHostPort}/api/agencybanking");
- //this.AdditionalVariables.Add(ContainerType.FileProcessor, variables);
- //this.SetAdditionalVariables(ContainerType.FileProcessor, variables);
+ this.AdditionalVariables.Add(ContainerType.TransactionProcessor, additionalVariables);
return base.SetupTransactionProcessorContainer();
}
+ public override ContainerBuilder SetupTestHostContainer()
+ {
+ this.Trace("About to Start Test Hosts Container");
+
+ Dictionary environmentVariables = this.GetCommonEnvironmentVariables();
+ environmentVariables.Add("ConnectionStrings:TestBankReadModel", this.SetConnectionString("TestBankReadModel", this.UseSecureSqlServerDatabase));
+ environmentVariables.Add("ConnectionStrings:PataPawaReadModel", this.SetConnectionString("PataPawaReadModel", this.UseSecureSqlServerDatabase));
+ environmentVariables.Add("ConnectionStrings:AgencyBankingReadModel", this.SetConnectionString("AgencyBankingReadModel", this.UseSecureSqlServerDatabase));
+ //environmentVariables.Add("ASPNETCORE_ENVIRONMENT", "IntegrationTest");
+
+ Dictionary additionalEnvironmentVariables = this.GetAdditionalVariables(ContainerType.TestHost);
+
+ foreach (KeyValuePair additionalEnvironmentVariable in additionalEnvironmentVariables)
+ {
+ environmentVariables.Add(additionalEnvironmentVariable.Key, additionalEnvironmentVariable.Value);
+ }
+
+ (String imageName, Boolean useLatest) imageDetails = this.GetImageDetails(ContainerType.TestHost).Data;
+
+ ContainerBuilder testHostContainer = new ContainerBuilder()
+ .WithName(this.TestHostContainerName) // similar to WithName()
+ .WithImage(imageDetails.imageName)
+ .WithEnvironment(environmentVariables)
+ .MountHostFolder(this.DockerPlatform, this.HostTraceFolder)
+ .WithPortBinding(DockerPorts.TestHostPort, true);
+
+ return testHostContainer;
+ }
+
public override async Task CreateSubscriptions(){
List<(String streamName, String groupName, Int32 maxRetries)> subscriptions = new();
subscriptions.AddRange(MessagingService.IntegrationTesting.Helpers.SubscriptionsHelper.GetSubscriptions());
@@ -172,16 +214,28 @@ Object Deserialise(String arg, Type type)
return StringSerialiser.DeserializeObject
diff --git a/TransactionProcessor.Tests/HandlerTests/TransactionHandlersTests.cs b/TransactionProcessor.Tests/HandlerTests/TransactionHandlersTests.cs
index 3430768e..31fd6095 100644
--- a/TransactionProcessor.Tests/HandlerTests/TransactionHandlersTests.cs
+++ b/TransactionProcessor.Tests/HandlerTests/TransactionHandlersTests.cs
@@ -91,6 +91,108 @@ public async Task PerformTransaction_SalePayloadWithoutTypeMetadata_SendsSaleCom
mediator.VerifyAll();
}
+ [Fact]
+ public async Task PerformTransaction_SalePayloadWithoutTransactionSource_UsesDefaultSource()
+ {
+ Mock mediator = new Mock(MockBehavior.Strict);
+ TransactionCommands.ProcessSaleTransactionCommand? capturedCommand = null;
+ SaleTransactionRequest request = new SaleTransactionRequest
+ {
+ AdditionalTransactionMetadata = new Dictionary { { "amount", "12.34" } },
+ ContractId = Guid.NewGuid(),
+ CustomerEmailAddress = "customer@test.local",
+ DeviceIdentifier = "device-1",
+ OperatorId = Guid.NewGuid(),
+ ProductId = Guid.NewGuid(),
+ TransactionDateTime = new DateTime(2024, 1, 2, 3, 4, 5),
+ TransactionNumber = "000002",
+ TransactionType = "Sale",
+ EstateId = TestData.EstateId,
+ MerchantId = TestData.MerchantId
+ };
+
+ mediator.Setup(m => m.Send(It.IsAny(),
+ It.IsAny()))
+ .Callback((IRequest> request, CancellationToken _) => capturedCommand = (TransactionCommands.ProcessSaleTransactionCommand)request)
+ .ReturnsAsync(Result.Success(new ProcessSaleTransactionResponse
+ {
+ EstateId = TestData.EstateId,
+ MerchantId = TestData.MerchantId,
+ ResponseCode = "0000",
+ ResponseMessage = "SUCCESS",
+ TransactionId = Guid.NewGuid()
+ }));
+
+ IResult result = await TransactionHandlers.PerformSaleTransaction(mediator.Object,
+ new DefaultHttpContext(),
+ request,
+ CancellationToken.None);
+
+ result.ShouldNotBeNull();
+ capturedCommand.ShouldNotBeNull();
+ capturedCommand!.TransactionSource.ShouldBe(1);
+ mediator.VerifyAll();
+ }
+
+ [Fact]
+ public async Task PerformTransaction_SalePayload_WhenMediatorFails_ReturnsFailure()
+ {
+ Mock mediator = new Mock(MockBehavior.Strict);
+ SaleTransactionRequest request = new SaleTransactionRequest
+ {
+ AdditionalTransactionMetadata = new Dictionary { { "amount", "12.34" } },
+ ContractId = Guid.NewGuid(),
+ CustomerEmailAddress = "customer@test.local",
+ DeviceIdentifier = "device-1",
+ OperatorId = Guid.NewGuid(),
+ ProductId = Guid.NewGuid(),
+ TransactionDateTime = new DateTime(2024, 1, 2, 3, 4, 5),
+ TransactionNumber = "000002",
+ TransactionType = "Sale",
+ TransactionSource = 2,
+ EstateId = TestData.EstateId,
+ MerchantId = TestData.MerchantId
+ };
+
+ mediator.Setup(m => m.Send(It.IsAny(),
+ It.IsAny()))
+ .ReturnsAsync(Result.Failure("boom").ToResult(new ProcessSaleTransactionResponse()));
+
+ IResult result = await TransactionHandlers.PerformSaleTransaction(mediator.Object,
+ new DefaultHttpContext(),
+ request,
+ CancellationToken.None);
+
+ result.ShouldNotBeNull();
+ mediator.VerifyAll();
+ }
+
+ [Fact]
+ public async Task PerformTransaction_LogonPayload_WhenMediatorFails_ReturnsFailure()
+ {
+ Mock mediator = new Mock(MockBehavior.Strict);
+ LogonTransactionRequest request = new LogonTransactionRequest
+ {
+ DeviceIdentifier = "device-1",
+ TransactionDateTime = DateTime.SpecifyKind(new DateTime(2024, 1, 2, 3, 4, 5), DateTimeKind.Utc),
+ TransactionNumber = "000001",
+ TransactionType = "Logon",
+ EstateId = TestData.EstateId,
+ MerchantId = TestData.MerchantId
+ };
+
+ mediator.Setup(m => m.Send(It.IsAny(), It.IsAny()))
+ .ReturnsAsync(Result.Failure("boom").ToResult(new ProcessLogonTransactionResponse()));
+
+ IResult result = await TransactionHandlers.PerformLogonTransaction(mediator.Object,
+ new DefaultHttpContext(),
+ request,
+ CancellationToken.None);
+
+ result.ShouldNotBeNull();
+ mediator.VerifyAll();
+ }
+
[Fact]
public async Task PerformTransaction_ReconciliationPayloadWithoutTypeMetadata_SendsReconciliationCommand()
{
@@ -127,6 +229,53 @@ public async Task PerformTransaction_ReconciliationPayloadWithoutTypeMetadata_Se
mediator.VerifyAll();
}
+ [Fact]
+ public async Task PerformTransaction_ReconciliationPayload_WhenMediatorFails_ReturnsFailure()
+ {
+ Mock mediator = new Mock(MockBehavior.Strict);
+ ReconciliationRequest request = new ReconciliationRequest
+ {
+ DeviceIdentifier = "device-1",
+ OperatorTotals = new List(),
+ TransactionCount = 4,
+ TransactionDateTime = new DateTime(2024, 1, 2, 3, 4, 5),
+ TransactionValue = 42.50m,
+ EstateId = TestData.EstateId,
+ MerchantId = TestData.MerchantId,
+ TransactionType = "Reconciliation"
+ };
+
+ mediator.Setup(m => m.Send(It.IsAny(),
+ It.IsAny()))
+ .ReturnsAsync(Result.Failure("boom").ToResult(new ProcessReconciliationTransactionResponse()));
+
+ IResult result = await TransactionHandlers.PerformReconciliationTransaction(mediator.Object,
+ new DefaultHttpContext(),
+ request,
+ CancellationToken.None);
+
+ result.ShouldNotBeNull();
+ mediator.VerifyAll();
+ }
+
+ [Fact]
+ public async Task ResendTransactionReceipt_SendsCommand()
+ {
+ Mock mediator = new Mock(MockBehavior.Strict);
+
+ mediator.Setup(m => m.Send(It.IsAny(), It.IsAny()))
+ .ReturnsAsync(Result.Success());
+
+ IResult result = await TransactionHandlers.ResendTransactionReceipt(mediator.Object,
+ new DefaultHttpContext(),
+ TestData.EstateId,
+ Guid.NewGuid(),
+ CancellationToken.None);
+
+ result.ShouldNotBeNull();
+ mediator.VerifyAll();
+ }
+
private static class TestData
{
public static Guid EstateId => Guid.Parse("11111111-1111-1111-1111-111111111111");
diff --git a/TransactionProcessor.Tests/HandlerTests/WebHandlersTests.cs b/TransactionProcessor.Tests/HandlerTests/WebHandlersTests.cs
new file mode 100644
index 00000000..33dd6c8e
--- /dev/null
+++ b/TransactionProcessor.Tests/HandlerTests/WebHandlersTests.cs
@@ -0,0 +1,1167 @@
+using MediatR;
+using Microsoft.AspNetCore.Http;
+using Moq;
+using Shouldly;
+using SimpleResults;
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using TransactionProcessor.BusinessLogic.Requests;
+using TransactionProcessor.DataTransferObjects;
+using TransactionProcessor.DataTransferObjects.Requests.Contract;
+using TransactionProcessor.DataTransferObjects.Requests.Estate;
+using TransactionProcessor.DataTransferObjects.Requests.Merchant;
+using TransactionProcessor.DataTransferObjects.Requests.MerchantSchedule;
+using TransactionProcessor.DataTransferObjects.Requests.Operator;
+using TransactionProcessor.DataTransferObjects.Responses.Contract;
+using TransactionProcessor.Handlers;
+using TransactionProcessor.Models;
+using TransactionProcessor.Models.Contract;
+using TransactionProcessor.Models.Estate;
+using TransactionProcessor.Models.MerchantSchedule;
+using TransactionProcessor.Models.Operator;
+using TransactionProcessor.Models.Settlement;
+using TransactionProcessor.ProjectionEngine.Models;
+using TransactionProcessor.ProjectionEngine.State;
+using Xunit;
+using MerchantModel = TransactionProcessor.Models.Merchant.Merchant;
+using EstateModel = TransactionProcessor.Models.Estate.Estate;
+using ProjectionMerchant = TransactionProcessor.ProjectionEngine.State.Merchant;
+using OperatorModel = TransactionProcessor.Models.Operator.Operator;
+using RedeemVoucherModel = TransactionProcessor.Models.RedeemVoucherResponse;
+
+namespace TransactionProcessor.Tests.HandlerTests;
+
+public class WebHandlersTests
+{
+ private static readonly Guid EstateId = Guid.Parse("11111111-1111-1111-1111-111111111111");
+ private static readonly Guid MerchantId = Guid.Parse("22222222-2222-2222-2222-222222222222");
+ private static readonly Guid ContractId = Guid.Parse("33333333-3333-3333-3333-333333333333");
+ private static readonly Guid ProductId = Guid.Parse("44444444-4444-4444-4444-444444444444");
+ private static readonly Guid OperatorId = Guid.Parse("55555555-5555-5555-5555-555555555555");
+ private static readonly Guid TransactionId = Guid.Parse("66666666-6666-6666-6666-666666666666");
+ private static readonly Guid FloatId = Guid.Parse("77777777-7777-7777-7777-777777777777");
+ private static readonly Guid SettlementId = Guid.Parse("88888888-8888-8888-8888-888888888888");
+ private static readonly Guid AddressId = Guid.Parse("99999999-9999-9999-9999-999999999999");
+ private static readonly Guid ContactId = Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa");
+ private static readonly Guid VoucherTransactionId = Guid.Parse("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb");
+ private static readonly string VoucherCode = "VOUCHER-1";
+ private static readonly DateTime Instant = new DateTime(2024, 1, 2, 3, 4, 5, DateTimeKind.Utc);
+
+ [Theory]
+ [MemberData(nameof(ContractCases))]
+ public async Task ContractHandlers_AreCovered(HandlerCase testCase)
+ {
+ Mock mediator = new(MockBehavior.Strict);
+ testCase.Setup(mediator);
+
+ IResult result = await testCase.Invoke(mediator.Object);
+
+ result.ShouldNotBeNull();
+ testCase.Verify?.Invoke(mediator);
+ if (testCase.Verify is null)
+ {
+ mediator.VerifyAll();
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(EstateCases))]
+ public async Task EstateHandlers_AreCovered(HandlerCase testCase)
+ {
+ Mock mediator = new(MockBehavior.Strict);
+ testCase.Setup(mediator);
+
+ IResult result = await testCase.Invoke(mediator.Object);
+
+ result.ShouldNotBeNull();
+ testCase.Verify?.Invoke(mediator);
+ if (testCase.Verify is null)
+ {
+ mediator.VerifyAll();
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(FloatCases))]
+ public async Task FloatHandlers_AreCovered(HandlerCase testCase)
+ {
+ Mock mediator = new(MockBehavior.Strict);
+ testCase.Setup(mediator);
+
+ IResult result = await testCase.Invoke(mediator.Object);
+
+ result.ShouldNotBeNull();
+ testCase.Verify?.Invoke(mediator);
+ if (testCase.Verify is null)
+ {
+ mediator.VerifyAll();
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(MerchantCases))]
+ public async Task MerchantHandlers_AreCovered(HandlerCase testCase)
+ {
+ Mock mediator = new(MockBehavior.Strict);
+ testCase.Setup(mediator);
+
+ IResult result = await testCase.Invoke(mediator.Object);
+
+ result.ShouldNotBeNull();
+ testCase.Verify?.Invoke(mediator);
+ if (testCase.Verify is null)
+ {
+ mediator.VerifyAll();
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(OperatorCases))]
+ public async Task OperatorHandlers_AreCovered(HandlerCase testCase)
+ {
+ Mock mediator = new(MockBehavior.Strict);
+ testCase.Setup(mediator);
+
+ IResult result = await testCase.Invoke(mediator.Object);
+
+ result.ShouldNotBeNull();
+ testCase.Verify?.Invoke(mediator);
+ if (testCase.Verify is null)
+ {
+ mediator.VerifyAll();
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(SettlementCases))]
+ public async Task SettlementHandlers_AreCovered(HandlerCase testCase)
+ {
+ Mock mediator = new(MockBehavior.Strict);
+ testCase.Setup(mediator);
+
+ IResult result = await testCase.Invoke(mediator.Object);
+
+ result.ShouldNotBeNull();
+ testCase.Verify?.Invoke(mediator);
+ if (testCase.Verify is null)
+ {
+ mediator.VerifyAll();
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(VoucherCases))]
+ public async Task VoucherHandlers_AreCovered(HandlerCase testCase)
+ {
+ Mock mediator = new(MockBehavior.Strict);
+ testCase.Setup(mediator);
+
+ IResult result = await testCase.Invoke(mediator.Object);
+
+ result.ShouldNotBeNull();
+ testCase.Verify?.Invoke(mediator);
+ if (testCase.Verify is null)
+ {
+ mediator.VerifyAll();
+ }
+ }
+
+ public static IEnumerable ContractCases()
+ {
+ yield return Case(
+ "GetContract",
+ SetupSuccess(new Contract
+ {
+ ContractId = ContractId,
+ EstateId = EstateId,
+ Description = "Contract"
+ }),
+ mediator => ContractHandlers.GetContract(mediator, new DefaultHttpContext(), EstateId, ContractId, CancellationToken.None));
+
+ yield return Case(
+ "GetContracts",
+ SetupSuccess>(new List
+ {
+ new()
+ {
+ ContractId = ContractId,
+ EstateId = EstateId,
+ Description = "Contract"
+ }
+ }),
+ mediator => ContractHandlers.GetContracts(mediator, new DefaultHttpContext(), EstateId, CancellationToken.None));
+
+ yield return Case(
+ "AddProductToContract",
+ SetupSuccess(command =>
+ {
+ command.EstateId.ShouldBe(EstateId);
+ command.ContractId.ShouldBe(ContractId);
+ command.ProductId.ShouldNotBe(Guid.Empty);
+ command.RequestDTO.DisplayText.ShouldBe("Display");
+ }),
+ mediator => ContractHandlers.AddProductToContract(mediator,
+ new DefaultHttpContext(),
+ EstateId,
+ ContractId,
+ new AddProductToContractRequest
+ {
+ DisplayText = "Display",
+ ProductName = "Product",
+ ProductType = TransactionProcessor.DataTransferObjects.Responses.Contract.ProductType.MobileTopup,
+ Value = 12.34m
+ },
+ CancellationToken.None));
+
+ yield return Case(
+ "AddTransactionFeeForProductToContract",
+ SetupSuccess(command =>
+ {
+ command.EstateId.ShouldBe(EstateId);
+ command.ContractId.ShouldBe(ContractId);
+ command.ProductId.ShouldBe(ProductId);
+ command.TransactionFeeId.ShouldNotBe(Guid.Empty);
+ }),
+ mediator => ContractHandlers.AddTransactionFeeForProductToContract(mediator,
+ new DefaultHttpContext(),
+ EstateId,
+ ContractId,
+ ProductId,
+ new AddTransactionFeeForProductToContractRequest
+ {
+ CalculationType = TransactionProcessor.DataTransferObjects.Responses.Contract.CalculationType.Fixed,
+ Description = "Fee",
+ FeeType = TransactionProcessor.DataTransferObjects.Responses.Contract.FeeType.Merchant,
+ Value = 1.23m
+ },
+ CancellationToken.None));
+
+ yield return Case(
+ "DisableTransactionFeeForProduct",
+ SetupSuccess(command =>
+ {
+ command.EstateId.ShouldBe(EstateId);
+ command.ContractId.ShouldBe(ContractId);
+ command.ProductId.ShouldBe(ProductId);
+ command.TransactionFeeId.ShouldBe(SettlementId);
+ }),
+ mediator => ContractHandlers.DisableTransactionFeeForProduct(mediator,
+ new DefaultHttpContext(),
+ EstateId,
+ ContractId,
+ ProductId,
+ SettlementId,
+ CancellationToken.None));
+
+ yield return Case(
+ "CreateContract",
+ SetupSuccess(command =>
+ {
+ command.EstateId.ShouldBe(EstateId);
+ command.ContractId.ShouldNotBe(Guid.Empty);
+ command.RequestDTO.Description.ShouldBe("Contract");
+ }),
+ mediator => ContractHandlers.CreateContract(mediator,
+ new DefaultHttpContext(),
+ EstateId,
+ new CreateContractRequest
+ {
+ OperatorId = OperatorId,
+ Description = "Contract"
+ },
+ CancellationToken.None));
+ }
+
+ public static IEnumerable EstateCases()
+ {
+ yield return Case(
+ "CreateEstate",
+ SetupSuccess(command =>
+ {
+ command.RequestDto.EstateId.ShouldBe(EstateId);
+ command.RequestDto.EstateName.ShouldBe("Estate");
+ }),
+ mediator => EstateHandlers.CreateEstate(mediator,
+ new DefaultHttpContext(),
+ new CreateEstateRequest
+ {
+ EstateId = EstateId,
+ EstateName = "Estate"
+ },
+ CancellationToken.None));
+
+ yield return Case(
+ "GetEstate",
+ SetupSuccess(new EstateModel
+ {
+ EstateId = EstateId,
+ Name = "Estate"
+ }),
+ mediator => EstateHandlers.GetEstate(mediator, new DefaultHttpContext(), EstateId, CancellationToken.None));
+
+ yield return Case(
+ "GetEstates",
+ SetupSuccess>(new List
+ {
+ new()
+ {
+ EstateId = EstateId,
+ Name = "Estate"
+ }
+ }),
+ mediator => EstateHandlers.GetEstates(mediator, new DefaultHttpContext(), EstateId, CancellationToken.None));
+
+ yield return Case(
+ "CreateEstateUser",
+ SetupSuccess(command =>
+ {
+ command.EstateId.ShouldBe(EstateId);
+ command.RequestDto.EmailAddress.ShouldBe("user@test.local");
+ }),
+ mediator => EstateHandlers.CreateEstateUser(mediator,
+ new DefaultHttpContext(),
+ EstateId,
+ new CreateEstateUserRequest
+ {
+ EmailAddress = "user@test.local",
+ Password = "password",
+ GivenName = "Given",
+ FamilyName = "Family"
+ },
+ CancellationToken.None));
+
+ yield return Case(
+ "AssignOperator",
+ SetupSuccess(command =>
+ {
+ command.EstateId.ShouldBe(EstateId);
+ command.RequestDto.OperatorId.ShouldBe(OperatorId);
+ }),
+ mediator => EstateHandlers.AssignOperator(mediator,
+ new DefaultHttpContext(),
+ EstateId,
+ new TransactionProcessor.DataTransferObjects.Requests.Estate.AssignOperatorRequest { OperatorId = OperatorId },
+ CancellationToken.None));
+
+ yield return Case(
+ "RemoveOperator",
+ SetupSuccess(command =>
+ {
+ command.EstateId.ShouldBe(EstateId);
+ command.OperatorId.ShouldBe(OperatorId);
+ }),
+ mediator => EstateHandlers.RemoveOperator(mediator,
+ new DefaultHttpContext(),
+ EstateId,
+ OperatorId,
+ CancellationToken.None));
+ }
+
+ public static IEnumerable FloatCases()
+ {
+ yield return Case(
+ "CreateFloatForContractProduct",
+ SetupSuccess(command =>
+ {
+ command.EstateId.ShouldBe(EstateId);
+ command.ContractId.ShouldBe(ContractId);
+ command.ProductId.ShouldBe(ProductId);
+ }),
+ mediator => FloatHandlers.CreateFloatForContractProduct(mediator,
+ new DefaultHttpContext(),
+ EstateId,
+ new CreateFloatForContractProductRequest
+ {
+ ContractId = ContractId,
+ ProductId = ProductId,
+ CreateDateTime = Instant
+ },
+ CancellationToken.None));
+
+ yield return Case(
+ "RecordFloatCreditPurchase",
+ SetupSuccess(command =>
+ {
+ command.EstateId.ShouldBe(EstateId);
+ command.FloatId.ShouldBe(FloatId);
+ command.CreditAmount.ShouldBe(10m);
+ command.CostPrice.ShouldBe(8m);
+ }),
+ mediator => FloatHandlers.RecordFloatCreditPurchase(mediator,
+ new DefaultHttpContext(),
+ EstateId,
+ new RecordFloatCreditPurchaseRequest
+ {
+ FloatId = FloatId,
+ PurchaseDateTime = Instant,
+ CreditAmount = 10m,
+ CostPrice = 8m
+ },
+ CancellationToken.None));
+ }
+
+ public static IEnumerable MerchantCases()
+ {
+ yield return Case(
+ "GetMerchantBalance",
+ SetupSuccess(new MerchantBalanceState
+ {
+ EstateId = EstateId,
+ MerchantId = MerchantId,
+ Balance = 12m,
+ AvailableBalance = 8m
+ }),
+ mediator => MerchantHandlers.GetMerchantBalance(mediator, new DefaultHttpContext(), EstateId, MerchantId, CancellationToken.None));
+
+ yield return Case(
+ "GetMerchantBalanceLive",
+ SetupSuccess(
+ new MerchantBalanceProjectionState1(new ProjectionMerchant(MerchantId.ToString(), "Merchant", 1, 9m))),
+ mediator => MerchantHandlers.GetMerchantBalanceLive(mediator, new DefaultHttpContext(), EstateId, MerchantId, CancellationToken.None));
+
+ yield return Case(
+ "GetMerchantBalanceHistory",
+ SetupSuccess>(new List
+ {
+ new()
+ {
+ EstateId = EstateId,
+ MerchantId = MerchantId,
+ ChangeAmount = 3m,
+ Balance = 12m,
+ DateTime = Instant,
+ DebitOrCredit = "Credit",
+ OriginalEventId = TransactionId,
+ Reference = "REF"
+ }
+ }),
+ mediator => MerchantHandlers.GetMerchantBalanceHistory(mediator,
+ new DefaultHttpContext(),
+ EstateId,
+ MerchantId,
+ Instant.AddDays(-1),
+ Instant,
+ CancellationToken.None));
+
+ yield return Case(
+ "CreateMerchant",
+ SetupSuccess(command =>
+ {
+ command.EstateId.ShouldBe(EstateId);
+ command.RequestDto.Name.ShouldBe("Merchant");
+ }),
+ mediator => MerchantHandlers.CreateMerchant(mediator,
+ new DefaultHttpContext(),
+ EstateId,
+ new CreateMerchantRequest
+ {
+ MerchantId = MerchantId,
+ Name = "Merchant"
+ },
+ CancellationToken.None));
+
+ yield return Case(
+ "AssignOperator",
+ SetupSuccess(command =>
+ {
+ command.EstateId.ShouldBe(EstateId);
+ command.MerchantId.ShouldBe(MerchantId);
+ command.RequestDto.OperatorId.ShouldBe(OperatorId);
+ }),
+ mediator => MerchantHandlers.AssignOperator(mediator,
+ new DefaultHttpContext(),
+ EstateId,
+ MerchantId,
+ new TransactionProcessor.DataTransferObjects.Requests.Merchant.AssignOperatorRequest
+ {
+ OperatorId = OperatorId
+ },
+ CancellationToken.None));
+
+ yield return Case(
+ "RemoveOperator",
+ SetupSuccess(command =>
+ {
+ command.EstateId.ShouldBe(EstateId);
+ command.MerchantId.ShouldBe(MerchantId);
+ command.OperatorId.ShouldBe(OperatorId);
+ }),
+ mediator => MerchantHandlers.RemoveOperator(mediator,
+ new DefaultHttpContext(),
+ EstateId,
+ MerchantId,
+ OperatorId,
+ CancellationToken.None));
+
+ yield return Case(
+ "AddDevice",
+ SetupSuccess(command =>
+ {
+ command.EstateId.ShouldBe(EstateId);
+ command.MerchantId.ShouldBe(MerchantId);
+ command.RequestDto.DeviceIdentifier.ShouldBe("device-1");
+ }),
+ mediator => MerchantHandlers.AddDevice(mediator,
+ new DefaultHttpContext(),
+ EstateId,
+ MerchantId,
+ new AddMerchantDeviceRequest
+ {
+ DeviceIdentifier = "device-1"
+ },
+ CancellationToken.None));
+
+ yield return Case(
+ "SwapMerchantDevice",
+ SetupSuccess(command =>
+ {
+ command.EstateId.ShouldBe(EstateId);
+ command.MerchantId.ShouldBe(MerchantId);
+ command.DeviceIdentifier.ShouldBe("device-old");
+ command.RequestDto.NewDeviceIdentifier.ShouldBe("device-new");
+ }),
+ mediator => MerchantHandlers.SwapMerchantDevice(mediator,
+ new DefaultHttpContext(),
+ EstateId,
+ MerchantId,
+ "device-old",
+ new SwapMerchantDeviceRequest
+ {
+ NewDeviceIdentifier = "device-new"
+ },
+ CancellationToken.None));
+
+ yield return Case(
+ "AddContract",
+ SetupSuccess(command =>
+ {
+ command.EstateId.ShouldBe(EstateId);
+ command.MerchantId.ShouldBe(MerchantId);
+ command.RequestDto.ContractId.ShouldBe(ContractId);
+ }),
+ mediator => MerchantHandlers.AddContract(mediator,
+ new DefaultHttpContext(),
+ EstateId,
+ MerchantId,
+ new AddMerchantContractRequest
+ {
+ ContractId = ContractId
+ },
+ CancellationToken.None));
+
+ yield return Case(
+ "RemoveContract",
+ SetupSuccess(command =>
+ {
+ command.EstateId.ShouldBe(EstateId);
+ command.MerchantId.ShouldBe(MerchantId);
+ command.ContractId.ShouldBe(ContractId);
+ }),
+ mediator => MerchantHandlers.RemoveContract(mediator,
+ new DefaultHttpContext(),
+ EstateId,
+ MerchantId,
+ ContractId,
+ CancellationToken.None));
+
+ yield return Case(
+ "CreateMerchantUser",
+ SetupSuccess(command =>
+ {
+ command.EstateId.ShouldBe(EstateId);
+ command.MerchantId.ShouldBe(MerchantId);
+ command.RequestDto.EmailAddress.ShouldBe("user@test.local");
+ }),
+ mediator => MerchantHandlers.CreateMerchantUser(mediator,
+ new DefaultHttpContext(),
+ EstateId,
+ MerchantId,
+ new CreateMerchantUserRequest
+ {
+ EmailAddress = "user@test.local",
+ Password = "password",
+ GivenName = "Given",
+ MiddleName = "Middle",
+ FamilyName = "Family"
+ },
+ CancellationToken.None));
+
+ yield return Case(
+ "MakeDeposit",
+ SetupSuccess(command =>
+ {
+ command.EstateId.ShouldBe(EstateId);
+ command.MerchantId.ShouldBe(MerchantId);
+ command.DepositSource.ShouldBe(TransactionProcessor.DataTransferObjects.Requests.Merchant.MerchantDepositSource.Manual);
+ command.RequestDto.Amount.ShouldBe(15.50m);
+ }),
+ mediator => MerchantHandlers.MakeDeposit(mediator,
+ new DefaultHttpContext(),
+ EstateId,
+ MerchantId,
+ new MakeMerchantDepositRequest
+ {
+ Amount = 15.50m,
+ DepositDateTime = Instant,
+ Reference = "DEP-1"
+ },
+ CancellationToken.None));
+
+ yield return Case(
+ "MakeWithdrawal",
+ SetupSuccess(command =>
+ {
+ command.EstateId.ShouldBe(EstateId);
+ command.MerchantId.ShouldBe(MerchantId);
+ command.RequestDto.Amount.ShouldBe(9.25m);
+ }),
+ mediator => MerchantHandlers.MakeWithdrawal(mediator,
+ new DefaultHttpContext(),
+ EstateId,
+ MerchantId,
+ new MakeMerchantWithdrawalRequest
+ {
+ Amount = 9.25m,
+ WithdrawalDateTime = Instant,
+ Reference = "WDL-1"
+ },
+ CancellationToken.None));
+
+ yield return Case(
+ "UpdateMerchant",
+ SetupSuccess(command =>
+ {
+ command.EstateId.ShouldBe(EstateId);
+ command.MerchantId.ShouldBe(MerchantId);
+ command.RequestDto.Name.ShouldBe("Merchant");
+ }),
+ mediator => MerchantHandlers.UpdateMerchant(mediator,
+ new DefaultHttpContext(),
+ EstateId,
+ MerchantId,
+ new UpdateMerchantRequest
+ {
+ Name = "Merchant"
+ },
+ CancellationToken.None));
+
+ yield return Case(
+ "UpdateMerchantOpening",
+ SetupSuccess(command =>
+ {
+ command.EstateId.ShouldBe(EstateId);
+ command.MerchantId.ShouldBe(MerchantId);
+ command.RequestDto.Sunday.ShouldNotBeNull();
+ }),
+ mediator => MerchantHandlers.UpdateMerchantOpening(mediator,
+ new DefaultHttpContext(),
+ EstateId,
+ MerchantId,
+ new MerchantOpeningRequest
+ {
+ Sunday = new TransactionProcessor.DataTransferObjects.Requests.Merchant.OpeningHours
+ {
+ Opening = "08:00",
+ Closing = "17:00"
+ }
+ },
+ CancellationToken.None));
+
+ yield return Case(
+ "CreateMerchantSchedule",
+ SetupSuccess(command =>
+ {
+ command.EstateId.ShouldBe(EstateId);
+ command.MerchantId.ShouldBe(MerchantId);
+ command.RequestDto.Year.ShouldBe(2024);
+ }),
+ mediator => MerchantHandlers.CreateMerchantSchedule(mediator,
+ new DefaultHttpContext(),
+ EstateId,
+ MerchantId,
+ new CreateMerchantScheduleRequest
+ {
+ Year = 2024,
+ Months = new List
+ {
+ new()
+ {
+ Month = 1,
+ ClosedDays = new List { 1, 2 }
+ }
+ }
+ },
+ CancellationToken.None));
+
+ yield return Case(
+ "UpdateMerchantSchedule",
+ SetupSuccess(command =>
+ {
+ command.EstateId.ShouldBe(EstateId);
+ command.MerchantId.ShouldBe(MerchantId);
+ command.Year.ShouldBe(2024);
+ }),
+ mediator => MerchantHandlers.UpdateMerchantSchedule(mediator,
+ new DefaultHttpContext(),
+ EstateId,
+ MerchantId,
+ 2024,
+ new UpdateMerchantScheduleRequest
+ {
+ Months = new List
+ {
+ new()
+ {
+ Month = 2,
+ ClosedDays = new List { 10 }
+ }
+ }
+ },
+ CancellationToken.None));
+
+ yield return Case(
+ "GetMerchantSchedule",
+ SetupSuccess(new TransactionProcessor.Models.MerchantSchedule.MerchantSchedule
+ {
+ MerchantScheduleId = Guid.NewGuid(),
+ EstateId = EstateId,
+ MerchantId = MerchantId,
+ Year = 2024,
+ Months = new List
+ {
+ new()
+ {
+ Month = 3,
+ ClosedDays = new List { 5, 6 }
+ }
+ }
+ }),
+ mediator => MerchantHandlers.GetMerchantSchedule(mediator,
+ new DefaultHttpContext(),
+ EstateId,
+ MerchantId,
+ 2024,
+ CancellationToken.None));
+
+ yield return Case(
+ "GetMerchantScheduleFromReadModel",
+ SetupSuccess(new TransactionProcessor.Models.MerchantSchedule.MerchantSchedule
+ {
+ MerchantScheduleId = Guid.NewGuid(),
+ EstateId = EstateId,
+ MerchantId = MerchantId,
+ Year = 2025,
+ Months = new List
+ {
+ new()
+ {
+ Month = 8,
+ ClosedDays = new List { 1 }
+ }
+ }
+ }),
+ mediator => MerchantHandlers.GetMerchantScheduleFromReadModel(mediator,
+ new DefaultHttpContext(),
+ EstateId,
+ MerchantId,
+ 2025,
+ CancellationToken.None));
+
+ yield return Case(
+ "AddMerchantAddress",
+ SetupSuccess(command =>
+ {
+ command.EstateId.ShouldBe(EstateId);
+ command.MerchantId.ShouldBe(MerchantId);
+ command.RequestDto.AddressLine1.ShouldBe("Line 1");
+ }),
+ mediator => MerchantHandlers.AddMerchantAddress(mediator,
+ new DefaultHttpContext(),
+ EstateId,
+ MerchantId,
+ new TransactionProcessor.DataTransferObjects.Requests.Merchant.Address
+ {
+ AddressLine1 = "Line 1",
+ AddressLine2 = "Line 2",
+ Country = "GB",
+ PostalCode = "PC1 1AA",
+ Region = "Region",
+ Town = "Town"
+ },
+ CancellationToken.None));
+
+ yield return Case(
+ "UpdateMerchantAddress",
+ SetupSuccess(command =>
+ {
+ command.EstateId.ShouldBe(EstateId);
+ command.MerchantId.ShouldBe(MerchantId);
+ command.AddressId.ShouldBe(AddressId);
+ command.RequestDto.AddressLine1.ShouldBe("Updated Line 1");
+ }),
+ mediator => MerchantHandlers.UpdateMerchantAddress(mediator,
+ new DefaultHttpContext(),
+ EstateId,
+ MerchantId,
+ AddressId,
+ new TransactionProcessor.DataTransferObjects.Requests.Merchant.Address
+ {
+ AddressLine1 = "Updated Line 1",
+ Country = "GB",
+ PostalCode = "PC1 2BB",
+ Region = "Updated Region",
+ Town = "Updated Town"
+ },
+ CancellationToken.None));
+
+ yield return Case(
+ "AddMerchantContact",
+ SetupSuccess(command =>
+ {
+ command.EstateId.ShouldBe(EstateId);
+ command.MerchantId.ShouldBe(MerchantId);
+ command.RequestDto.ContactName.ShouldBe("Contact");
+ }),
+ mediator => MerchantHandlers.AddMerchantContact(mediator,
+ new DefaultHttpContext(),
+ EstateId,
+ MerchantId,
+ new TransactionProcessor.DataTransferObjects.Requests.Merchant.Contact
+ {
+ ContactName = "Contact",
+ EmailAddress = "contact@test.local",
+ PhoneNumber = "01234 567890"
+ },
+ CancellationToken.None));
+
+ yield return Case(
+ "UpdateMerchantContact",
+ SetupSuccess(command =>
+ {
+ command.EstateId.ShouldBe(EstateId);
+ command.MerchantId.ShouldBe(MerchantId);
+ command.ContactId.ShouldBe(ContactId);
+ command.RequestDto.EmailAddress.ShouldBe("updated@test.local");
+ }),
+ mediator => MerchantHandlers.UpdateMerchantContact(mediator,
+ new DefaultHttpContext(),
+ EstateId,
+ MerchantId,
+ ContactId,
+ new TransactionProcessor.DataTransferObjects.Requests.Merchant.Contact
+ {
+ ContactName = "Updated Contact",
+ EmailAddress = "updated@test.local",
+ PhoneNumber = "09876 543210"
+ },
+ CancellationToken.None));
+
+ yield return Case(
+ "GetMerchant",
+ SetupSuccess(new MerchantModel
+ {
+ MerchantId = MerchantId,
+ EstateId = EstateId,
+ MerchantName = "Merchant"
+ }),
+ mediator => MerchantHandlers.GetMerchant(mediator, new DefaultHttpContext(), EstateId, MerchantId, CancellationToken.None));
+
+ yield return Case(
+ "GetMerchantContracts",
+ SetupSuccess>(new List
+ {
+ new()
+ {
+ ContractId = ContractId,
+ EstateId = EstateId,
+ Description = "Contract"
+ }
+ }),
+ mediator => MerchantHandlers.GetMerchantContracts(mediator, new DefaultHttpContext(), EstateId, MerchantId, CancellationToken.None));
+
+ yield return Case(
+ "GetMerchants",
+ SetupSuccess>(new List
+ {
+ new()
+ {
+ MerchantId = MerchantId,
+ EstateId = EstateId,
+ MerchantName = "Merchant"
+ }
+ }),
+ mediator => MerchantHandlers.GetMerchants(mediator, new DefaultHttpContext(), EstateId, CancellationToken.None));
+
+ yield return Case(
+ "GetTransactionFeesForProduct",
+ SetupSuccess>(new List()),
+ mediator => MerchantHandlers.GetTransactionFeesForProduct(mediator,
+ new DefaultHttpContext(),
+ EstateId,
+ MerchantId,
+ ContractId,
+ ProductId,
+ CancellationToken.None));
+
+ yield return Case(
+ "GenerateMerchantStatement",
+ SetupSuccess(command =>
+ {
+ command.EstateId.ShouldBe(EstateId);
+ command.MerchantId.ShouldBe(MerchantId);
+ command.RequestDto.MerchantStatementDate.ShouldBe(Instant);
+ }),
+ mediator => MerchantHandlers.GenerateMerchantStatement(mediator,
+ new DefaultHttpContext(),
+ EstateId,
+ MerchantId,
+ new GenerateMerchantStatementRequest
+ {
+ MerchantStatementDate = Instant
+ },
+ CancellationToken.None));
+ }
+
+ public static IEnumerable OperatorCases()
+ {
+ yield return Case(
+ "CreateOperator",
+ SetupSuccess(command =>
+ {
+ command.EstateId.ShouldBe(EstateId);
+ command.RequestDto.Name.ShouldBe("Operator");
+ }),
+ mediator => OperatorHandlers.CreateOperator(mediator,
+ new DefaultHttpContext(),
+ EstateId,
+ new CreateOperatorRequest
+ {
+ OperatorId = OperatorId,
+ Name = "Operator"
+ },
+ CancellationToken.None));
+
+ yield return Case(
+ "UpdateOperator",
+ SetupSuccess(command =>
+ {
+ command.EstateId.ShouldBe(EstateId);
+ command.OperatorId.ShouldBe(OperatorId);
+ }),
+ mediator => OperatorHandlers.UpdateOperator(mediator,
+ new DefaultHttpContext(),
+ EstateId,
+ OperatorId,
+ new UpdateOperatorRequest
+ {
+ Name = "Operator"
+ },
+ CancellationToken.None));
+
+ yield return Case(
+ "GetOperator",
+ SetupSuccess(new OperatorModel
+ {
+ OperatorId = OperatorId,
+ Name = "Operator"
+ }),
+ mediator => OperatorHandlers.GetOperator(mediator, new DefaultHttpContext(), EstateId, OperatorId, CancellationToken.None));
+
+ yield return Case(
+ "GetOperators",
+ SetupSuccess>(new List
+ {
+ new()
+ {
+ OperatorId = OperatorId,
+ Name = "Operator"
+ }
+ }),
+ mediator => OperatorHandlers.GetOperators(mediator, new DefaultHttpContext(), EstateId, CancellationToken.None));
+ }
+
+ public static IEnumerable SettlementCases()
+ {
+ yield return Case(
+ "GetPendingSettlement",
+ SetupSuccess(new PendingSettlementModel
+ {
+ EstateId = EstateId,
+ MerchantId = MerchantId,
+ SettlementDate = Instant,
+ NumberOfFeesPendingSettlement = 2,
+ NumberOfFeesSettled = 1,
+ SettlementCompleted = false
+ }),
+ mediator => SettlementHandlers.GetPendingSettlement(mediator, new DefaultHttpContext(), Instant, EstateId, MerchantId, CancellationToken.None));
+
+ yield return Case(
+ "ProcessSettlement",
+ SetupSuccess(SettlementId),
+ mediator => SettlementHandlers.ProcessSettlement(mediator, new DefaultHttpContext(), Instant, EstateId, MerchantId, CancellationToken.None));
+
+ yield return Case(
+ "GetSettlement",
+ SetupSuccess(new SettlementModel
+ {
+ SettlementId = SettlementId,
+ SettlementDate = Instant,
+ IsCompleted = true,
+ NumberOfFeesSettled = 1,
+ ValueOfFeesSettled = 12m,
+ SettlementFees = new List
+ {
+ new()
+ {
+ TransactionId = TransactionId,
+ MerchantId = MerchantId,
+ MerchantName = "Merchant",
+ SettlementId = SettlementId,
+ SettlementDate = Instant,
+ CalculatedValue = 1.5m,
+ FeeDescription = "Fee",
+ IsSettled = true,
+ OperatorIdentifier = "OP-1"
+ }
+ }
+ }),
+ mediator => SettlementHandlers.GetSettlement(mediator, new DefaultHttpContext(), EstateId, SettlementId, MerchantId, CancellationToken.None));
+
+ yield return Case(
+ "GetSettlements",
+ SetupSuccess>(new List
+ {
+ new()
+ {
+ SettlementId = SettlementId,
+ SettlementDate = Instant,
+ IsCompleted = true,
+ NumberOfFeesSettled = 1,
+ ValueOfFeesSettled = 12m,
+ SettlementFees = new List
+ {
+ new()
+ {
+ TransactionId = TransactionId,
+ MerchantId = MerchantId,
+ MerchantName = "Merchant",
+ SettlementDate = Instant,
+ SettlementId = SettlementId,
+ CalculatedValue = 1.5m,
+ FeeDescription = "Fee",
+ IsSettled = true,
+ OperatorIdentifier = "OP-1"
+ }
+ }
+ }
+ }),
+ mediator => SettlementHandlers.GetSettlements(mediator,
+ new DefaultHttpContext(),
+ EstateId,
+ MerchantId,
+ "2024-01-01",
+ "2024-01-31",
+ CancellationToken.None));
+ }
+
+ public static IEnumerable VoucherCases()
+ {
+ yield return Case(
+ "RedeemVoucher_ExplicitDate",
+ SetupSuccess(new RedeemVoucherModel
+ {
+ VoucherCode = VoucherCode,
+ RemainingBalance = 10m,
+ ExpiryDate = Instant.AddDays(1)
+ }),
+ mediator => VoucherHandlers.RedeemVoucher(mediator,
+ new DefaultHttpContext(),
+ new RedeemVoucherRequest
+ {
+ EstateId = EstateId,
+ VoucherCode = VoucherCode,
+ RedeemedDateTime = Instant
+ },
+ CancellationToken.None));
+
+ yield return Case(
+ "RedeemVoucher_DefaultDate",
+ SetupSuccess(new RedeemVoucherModel
+ {
+ VoucherCode = VoucherCode,
+ RemainingBalance = 10m,
+ ExpiryDate = Instant.AddDays(1)
+ }, command =>
+ {
+ command.EstateId.ShouldBe(EstateId);
+ command.VoucherCode.ShouldBe(VoucherCode);
+ command.RedeemedDateTime.ShouldBeGreaterThan(DateTime.Now.AddSeconds(-5));
+ command.RedeemedDateTime.ShouldBeLessThan(DateTime.Now.AddSeconds(5));
+ }),
+ mediator => VoucherHandlers.RedeemVoucher(mediator,
+ new DefaultHttpContext(),
+ new RedeemVoucherRequest
+ {
+ EstateId = EstateId,
+ VoucherCode = VoucherCode
+ },
+ CancellationToken.None));
+
+ yield return Case(
+ "GetVoucher_ByCode",
+ SetupSuccess(new Voucher
+ {
+ VoucherCode = VoucherCode,
+ EstateId = EstateId
+ }),
+ mediator => VoucherHandlers.GetVoucher(mediator, new DefaultHttpContext(), EstateId, VoucherCode, null, CancellationToken.None));
+
+ yield return Case(
+ "GetVoucher_ByTransactionId",
+ SetupSuccess(new Voucher
+ {
+ VoucherCode = VoucherCode,
+ EstateId = EstateId,
+ TransactionId = VoucherTransactionId
+ }),
+ mediator => VoucherHandlers.GetVoucher(mediator, new DefaultHttpContext(), EstateId, "", VoucherTransactionId, CancellationToken.None));
+
+ yield return Case(
+ "GetVoucher_Invalid",
+ mediator => { },
+ mediator => VoucherHandlers.GetVoucher(mediator, new DefaultHttpContext(), EstateId, "", null, CancellationToken.None),
+ mediator => mediator.VerifyNoOtherCalls());
+ }
+
+ private static object[] Case(string name, Action> setup, Func> invoke, Action>? verify = null)
+ => new object[] { new HandlerCase(name, setup, invoke, verify) };
+
+ private static Action> SetupSuccess(TResponse response, Action? capture = null)
+ where TRequest : class, IRequest>
+ {
+ return mediator =>
+ {
+ mediator.Setup(m => m.Send(It.IsAny>>(), It.IsAny()))
+ .Callback((IRequest> request, CancellationToken _) => capture?.Invoke((TRequest)request))
+ .ReturnsAsync(Result.Success(response));
+ };
+ }
+
+ private static Action> SetupSuccess(Action? capture = null)
+ where TRequest : class, IRequest
+ {
+ return mediator =>
+ {
+ mediator.Setup(m => m.Send(It.IsAny>(), It.IsAny()))
+ .Callback((IRequest request, CancellationToken _) => capture?.Invoke((TRequest)request))
+ .ReturnsAsync(Result.Success());
+ };
+ }
+
+ public sealed record HandlerCase(string Name,
+ Action> Setup,
+ Func> Invoke,
+ Action>? Verify = null);
+}
diff --git a/TransactionProcessor/Bootstrapper/MiddlewareRegistry.cs b/TransactionProcessor/Bootstrapper/MiddlewareRegistry.cs
index a2b51790..469f7fff 100644
--- a/TransactionProcessor/Bootstrapper/MiddlewareRegistry.cs
+++ b/TransactionProcessor/Bootstrapper/MiddlewareRegistry.cs
@@ -49,7 +49,7 @@ public MiddlewareRegistry()
KurrentDBClientSettings eventStoreClientSettings = KurrentDBClientSettings.Create(connectionString);
this.ConfigureHealthChecks(eventStoreClientSettings);
- this.ConfigureSwagger();
+ //this.ConfigureSwagger();
this.ConfigureAuthentication();
this.ConfigureControllers();
this.ConfigureMvc();
@@ -187,6 +187,7 @@ private HttpClientHandler ApiEndpointHttpHandler(IServiceProvider serviceProvide
#endregion
}
+ [ExcludeFromCodeCoverage]
public static class JsonSerializerConfiguration
{
public static void ConfigureMinimalApi(JsonSerializerOptions serializerOptions)
diff --git a/TransactionProcessor/Bootstrapper/OperatorRegistry.cs b/TransactionProcessor/Bootstrapper/OperatorRegistry.cs
index 6c725a00..5095c3ce 100644
--- a/TransactionProcessor/Bootstrapper/OperatorRegistry.cs
+++ b/TransactionProcessor/Bootstrapper/OperatorRegistry.cs
@@ -1,4 +1,5 @@
using Shared.Middleware;
+using TransactionProcessor.BusinessLogic.OperatorInterfaces.AgencyBanking;
namespace TransactionProcessor.Bootstrapper
{
@@ -33,12 +34,14 @@ public OperatorRegistry()
this.ConfigureOperator("Safaricom");
this.ConfigureOperator("PataPawaPostPay");
this.ConfigureOperator("PataPawaPrePay");
+ this.ConfigureOperator("AgencyBanking");
this.AddSingleton(new PataPawaPostPayServiceClient(PataPawaPostPayServiceClient.EndpointConfiguration.BasicHttpBinding_IPataPawaPostPayService));
this.For().Add().Named("Safaricom").Singleton();
this.For().Add().Named("PataPawaPostPay").Singleton();
this.For().Add().Named("PataPawaPrePay").Singleton();
this.For().Add().Named("Voucher").Singleton();
+ this.For().Add().Named("AgencyBanking").Singleton();
this.AddTransient>(context =>
diff --git a/TransactionProcessor/Endpoints/ContractEndpoints.cs b/TransactionProcessor/Endpoints/ContractEndpoints.cs
index d489ba0f..c513d925 100644
--- a/TransactionProcessor/Endpoints/ContractEndpoints.cs
+++ b/TransactionProcessor/Endpoints/ContractEndpoints.cs
@@ -1,3 +1,4 @@
+using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
@@ -6,6 +7,7 @@
namespace TransactionProcessor.Endpoints
{
+ [ExcludeFromCodeCoverage]
public static class ContractEndpoints
{
private const string BaseRoute = "/api/estates/{estateId:guid}/contracts";
diff --git a/TransactionProcessor/Endpoints/EstateEndpoints.cs b/TransactionProcessor/Endpoints/EstateEndpoints.cs
index 0d5638b9..f76cc309 100644
--- a/TransactionProcessor/Endpoints/EstateEndpoints.cs
+++ b/TransactionProcessor/Endpoints/EstateEndpoints.cs
@@ -4,11 +4,13 @@
using Microsoft.AspNetCore.Routing;
using Shared.Authorisation;
using Shared.General;
+using System.Diagnostics.CodeAnalysis;
using System.Security.Claims;
using TransactionProcessor.Handlers;
namespace TransactionProcessor.Endpoints
{
+ [ExcludeFromCodeCoverage]
public static class EstateEndpoints
{
private const string BaseRoute = "/api/estates";
diff --git a/TransactionProcessor/Endpoints/FloatEndpoints.cs b/TransactionProcessor/Endpoints/FloatEndpoints.cs
index 603b1cba..a1b862ed 100644
--- a/TransactionProcessor/Endpoints/FloatEndpoints.cs
+++ b/TransactionProcessor/Endpoints/FloatEndpoints.cs
@@ -2,10 +2,12 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Shared.Authorisation;
+using System.Diagnostics.CodeAnalysis;
using TransactionProcessor.Handlers;
namespace TransactionProcessor.Endpoints
{
+ [ExcludeFromCodeCoverage]
public static class FloatEndpoints
{
private const string BaseRoute = "/api/estates/{estateId:guid}/floats";
diff --git a/TransactionProcessor/Endpoints/MerchantEndpoints.cs b/TransactionProcessor/Endpoints/MerchantEndpoints.cs
index eed8e18d..8b57ed89 100644
--- a/TransactionProcessor/Endpoints/MerchantEndpoints.cs
+++ b/TransactionProcessor/Endpoints/MerchantEndpoints.cs
@@ -2,10 +2,12 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Shared.Authorisation;
+using System.Diagnostics.CodeAnalysis;
using TransactionProcessor.Handlers;
namespace TransactionProcessor.Endpoints;
+[ExcludeFromCodeCoverage]
public static class MerchantEndpoints
{
private const string BaseRoute = "/api/estates/{estateId:guid}/merchants";
diff --git a/TransactionProcessor/Endpoints/OperatorEndpoints.cs b/TransactionProcessor/Endpoints/OperatorEndpoints.cs
index b15ce4db..6997a256 100644
--- a/TransactionProcessor/Endpoints/OperatorEndpoints.cs
+++ b/TransactionProcessor/Endpoints/OperatorEndpoints.cs
@@ -2,10 +2,12 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Shared.Authorisation;
+using System.Diagnostics.CodeAnalysis;
using TransactionProcessor.Handlers;
namespace TransactionProcessor.Endpoints
{
+ [ExcludeFromCodeCoverage]
public static class OperatorEndpoints
{
private const string BaseRoute = "/api/estates/{estateId:guid}/operators";
diff --git a/TransactionProcessor/Endpoints/SettlementEndpoints.cs b/TransactionProcessor/Endpoints/SettlementEndpoints.cs
index 34d5ed4b..0678e7e6 100644
--- a/TransactionProcessor/Endpoints/SettlementEndpoints.cs
+++ b/TransactionProcessor/Endpoints/SettlementEndpoints.cs
@@ -2,10 +2,12 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Shared.Authorisation;
+using System.Diagnostics.CodeAnalysis;
using TransactionProcessor.Handlers;
namespace TransactionProcessor.Endpoints
{
+ [ExcludeFromCodeCoverage]
public static class SettlementEndpoints
{
private const string BaseRoute = "/api/estates/{estateId:guid}/settlements";
diff --git a/TransactionProcessor/Endpoints/TransactionEndpoints.cs b/TransactionProcessor/Endpoints/TransactionEndpoints.cs
index db014212..5cd2121e 100644
--- a/TransactionProcessor/Endpoints/TransactionEndpoints.cs
+++ b/TransactionProcessor/Endpoints/TransactionEndpoints.cs
@@ -2,20 +2,20 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Shared.Authorisation;
+using System.Diagnostics.CodeAnalysis;
using TransactionProcessor.Handlers;
namespace TransactionProcessor.Endpoints
{
+ [ExcludeFromCodeCoverage]
public static class TransactionEndpoints
{
private const string BaseRoute = "/api/transactions";
- public static IEndpointRouteBuilder MapTransactionEndpoints(this IEndpointRouteBuilder endpoints)
- {
- var group = endpoints.MapGroup(BaseRoute)
- .WithTags("Transactions")
- .RequireAuthorization()
- .RequireAuthorization(AuthorizationExtensions.PolicyNames.ClientCredentialsOnlyPolicy);
+ public static IEndpointRouteBuilder MapTransactionEndpoints(this IEndpointRouteBuilder endpoints) {
+ var group = endpoints.MapGroup(BaseRoute).WithTags("Transactions");
+ //.RequireAuthorization()
+ //.RequireAuthorization(AuthorizationExtensions.PolicyNames.ClientCredentialsOnlyPolicy);
// POST /api/transactions - rejects password tokens => require client credentials
group.MapPost("/logon", TransactionHandlers.PerformLogonTransaction)
diff --git a/TransactionProcessor/Endpoints/VoucherEndpoints.cs b/TransactionProcessor/Endpoints/VoucherEndpoints.cs
index beb051c7..66fbf015 100644
--- a/TransactionProcessor/Endpoints/VoucherEndpoints.cs
+++ b/TransactionProcessor/Endpoints/VoucherEndpoints.cs
@@ -2,10 +2,12 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Shared.Authorisation;
+using System.Diagnostics.CodeAnalysis;
using TransactionProcessor.Handlers;
namespace TransactionProcessor.Endpoints
{
+ [ExcludeFromCodeCoverage]
public static class VoucherEndpoints
{
private const string BaseRoute = "/api/vouchers";
diff --git a/TransactionProcessor/Startup.cs b/TransactionProcessor/Startup.cs
index 8812afa6..41d34cbe 100644
--- a/TransactionProcessor/Startup.cs
+++ b/TransactionProcessor/Startup.cs
@@ -118,9 +118,9 @@ public void Configure(IApplicationBuilder app,
});
this.ConfigureEndpoints(app);
- app.UseSwagger();
+ //app.UseSwagger();
- app.UseSwaggerUI();
+ //app.UseSwaggerUI();
app.PreWarm();
this.TraceLoadedTokenAssemblies();
diff --git a/TransactionProcessor/appsettings.json b/TransactionProcessor/appsettings.json
index b327a7b9..120e738d 100644
--- a/TransactionProcessor/appsettings.json
+++ b/TransactionProcessor/appsettings.json
@@ -217,6 +217,10 @@
"Username": "operatora",
"Password": "1234567898",
"ApiLogonRequired": true
+ },
+ "AgencyBanking": {
+ "Url": "http://192.168.1.163:9000/api/agencybanking",
+ "ApiLogonRequired": false
}
}
}
\ No newline at end of file