Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
<PackageVersion Include="System.ServiceModel.Http" Version="10.0.652802" />
<PackageVersion Include="System.ServiceModel.NetTcp" Version="10.0.652802" />
<PackageVersion Include="System.ServiceModel.Security" Version="6.0.0" />
<PackageVersion Include="TestHosts.Clients" Version="2026.6.2-build80" />
<PackageVersion Include="xunit.v3" Version="3.2.2" />
<PackageVersion Include="xunit.v3.extensibility.core" Version="3.2.2" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, String> metadata = new Dictionary<String, String>
{
{ "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<String, String> metadata = new Dictionary<String, String>
{
{ "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<String, String> metadata = new Dictionary<String, String>
{
{ "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<String, String> metadata = new Dictionary<String, String>
{
{ "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<String, String> metadata = new Dictionary<String, String>
{
{ "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<HttpRequestMessage> onRequest = null)
{
Mock<HttpMessageHandler> handlerMock = new Mock<HttpMessageHandler>(MockBehavior.Strict);
handlerMock.Protected()
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
.Callback<HttpRequestMessage, CancellationToken>((request, cancellationToken) => onRequest?.Invoke(request))
.ReturnsAsync(responseMessage);

return new HttpClient(handlerMock.Object);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace TransactionProcessor.BusinessLogic.OperatorInterfaces.SafaricomPinless
{
using System;
using System.Diagnostics.CodeAnalysis;

/// <summary>
///
/// </summary>
[ExcludeFromCodeCoverage]
public class AgencyBankingConfiguration : BaseOperatorConfiguration
{
}
}
Original file line number Diff line number Diff line change
@@ -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<Result<OperatorResponse>> ProcessLogonMessage(CancellationToken cancellationToken) {
Logger.LogInformation("AgencyBanking logon requested");
return Result.Success();
}

public async Task<Result<OperatorResponse>> ProcessSaleMessage(Guid transactionId,
Guid operatorId,
Merchant merchant,
DateTime transactionDateTime,
String transactionReference,
Dictionary<String, String> 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<String>("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<String>("AgencyBankingAgentId");

Check notice on line 55 in TransactionProcessor.BusinessLogic/OperatorInterfaces/AgencyBanking/AgencyBankingProxy.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

TransactionProcessor.BusinessLogic/OperatorInterfaces/AgencyBanking/AgencyBankingProxy.cs#L55

Remove this commented out code.

//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<String>("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<Result<OperatorResponse>> 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<BalanceEnquiryResponse>(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<String, String>()
};

operatorResponse.AdditionalTransactionResponseMetadata.Add("AgencyBankingBalance", balanceEnquiryResponse.AvailableBalance.ToString());

return Result.Success(operatorResponse);
}
}
}
Original file line number Diff line number Diff line change
@@ -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; }
}
}
Loading
Loading