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
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,198 @@ public async Task AgencyBankingProxy_ProcessSaleMessage_DepositSuccess_ReturnsMa
requestJson.RootElement.GetProperty("transactionId").GetString().ShouldBe(TestData.TransactionId.ToString());
}

[Fact]
public async Task AgencyBankingProxy_ProcessSaleMessage_WithdrawalFailure_ReturnsInvalidResult()
{
HttpResponseMessage responseMessage = new HttpResponseMessage(HttpStatusCode.BadRequest)
{
Content = new StringContent("withdrawal rejected")
};

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", "withdrawal" },
{ "AgencyBankingAccountNumber", "123456789" },
{ "Amount", "250.75" }
};

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 withdrawal withdrawal rejected");

capturedRequest.ShouldNotBeNull();
capturedRequest.Method.ShouldBe(HttpMethod.Post);
capturedRequest.RequestUri.ShouldBe(new Uri("http://localhost/transactions/withdrawal"));

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");
requestJson.RootElement.GetProperty("amount").GetDecimal().ShouldBe(250.75m);
requestJson.RootElement.GetProperty("transactionId").GetString().ShouldBe(TestData.TransactionId.ToString());
}

[Fact]
public async Task AgencyBankingProxy_ProcessSaleMessage_WithdrawalMissingAmount_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", "withdrawal" },
{ "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("Amount - Amount is a required field for this transaction type");
}

[Fact]
public async Task AgencyBankingProxy_ProcessSaleMessage_WithdrawalInvalidAmount_ReturnsInvalidResult()
{
Mock<HttpMessageHandler> handlerMock = new Mock<HttpMessageHandler>(MockBehavior.Strict);
HttpClient httpClient = new HttpClient(handlerMock.Object);
AgencyBankingConfiguration configuration = CreateConfiguration();
IOperatorProxy proxy = new AgencyBankingProxy(httpClient, configuration);

Dictionary<String, String> metadata = new Dictionary<String, String>
{
{ "AgencyBankingMessageType", "withdrawal" },
{ "AgencyBankingAccountNumber", "123456789" },
{ "Amount", "not-a-number" }
};

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("Amount - Amount is not a valid decimal value");
}

[Fact]
public async Task AgencyBankingProxy_ProcessSaleMessage_WithdrawalSuccess_ReturnsMappedOperatorResponse()
{
HttpResponseMessage responseMessage = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent("{\"ResponseCode\":\"0000\",\"TransactionId\":\"abc123\"}")
};

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", "withdrawal" },
{ "AgencyBankingAccountNumber", "123456789" },
{ "Amount", "250.75" }
};

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.ResponseMessage.ShouldBe("SUCCESS");
result.Data.AdditionalTransactionResponseMetadata.ShouldBeEmpty();

capturedRequest.ShouldNotBeNull();
capturedRequest.Method.ShouldBe(HttpMethod.Post);
capturedRequest.RequestUri.ShouldBe(new Uri("http://localhost/transactions/withdrawal"));

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");
requestJson.RootElement.GetProperty("amount").GetDecimal().ShouldBe(250.75m);
requestJson.RootElement.GetProperty("transactionId").GetString().ShouldBe(TestData.TransactionId.ToString());
}

[Fact]
public async Task AgencyBankingProxy_ProcessSaleMessage_WithdrawalSuccess_WithMixedCaseMetadata_ReturnsMappedOperatorResponse()
{
HttpResponseMessage responseMessage = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent("{\"ResponseCode\":\"0000\",\"TransactionId\":\"abc123\"}")
};

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", "withdrawal" },
{ "agencybankingaccountnumber", "123456789" },
{ "aMoUnT", "250.75" }
};

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.ResponseMessage.ShouldBe("SUCCESS");
result.Data.AdditionalTransactionResponseMetadata.ShouldBeEmpty();

capturedRequest.ShouldNotBeNull();
capturedRequest.Method.ShouldBe(HttpMethod.Post);
capturedRequest.RequestUri.ShouldBe(new Uri("http://localhost/transactions/withdrawal"));

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");
requestJson.RootElement.GetProperty("amount").GetDecimal().ShouldBe(250.75m);
requestJson.RootElement.GetProperty("transactionId").GetString().ShouldBe(TestData.TransactionId.ToString());
}

private static AgencyBankingConfiguration CreateConfiguration()
{
return new AgencyBankingConfiguration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<DebugType>Full</DebugType>
<DebugType>None</DebugType>
<IsPackable>false</IsPackable>
</PropertyGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ public async Task<Result<OperatorResponse>> ProcessSaleMessage(Guid transactionI
Result<OperatorResponse> result = messageType switch {
"balanceenquiry" => await ProcessBalanceEnquiry(transactionId, merchant.MerchantId.ToString(), accountNumber, cancellationToken),
"deposit" => await ProcessDeposit(transactionId, merchant.MerchantId.ToString(), accountNumber, additionalTransactionMetadata, cancellationToken),
"withdrawal" => await ProcessWithdrawal(transactionId, merchant.MerchantId.ToString(), accountNumber, additionalTransactionMetadata, cancellationToken),
_ => Result.Invalid($"AgencyBankingMessageType - Message Type {messageType} is not supported for this transaction type")
};

Expand All @@ -76,6 +77,59 @@ public async Task<Result<OperatorResponse>> ProcessSaleMessage(Guid transactionI
return result;
}

private async Task<Result<OperatorResponse>> ProcessWithdrawal(Guid transactionId,
String agentId,
String accountNumber,
Dictionary<String, String> additionalTransactionMetadata,
CancellationToken cancellationToken) {
HttpRequestMessage request = new(HttpMethod.Post, $"{this.Configuration.Url}/transactions/withdrawal");
// Extract required data from metadata
String amountValue = additionalTransactionMetadata.ExtractFieldFromMetadata<String>("Amount");

if (String.IsNullOrWhiteSpace(amountValue))
{
Logger.LogWarning($"AgencyBanking withdrawal request rejected. TransactionId=[{transactionId}] missing Amount");
return Result.Invalid("Amount - Amount is a required field for this transaction type");
}

if (Decimal.TryParse(amountValue, out Decimal amount) == false)
{
Logger.LogWarning($"AgencyBanking withdrawal request rejected. TransactionId=[{transactionId}] invalid Amount [{amountValue}]");
return Result.Invalid("Amount - Amount is not a valid decimal value");
}

WithdrawalRequest body = new() { AgentId = agentId, AccountNumber = accountNumber, Amount = amount, TransactionId = transactionId.ToString() };

String requestBody = StringSerialiser.Serialise(body, SerialiserOptions);
request.Content = new StringContent(requestBody, Encoding.UTF8, "application/json");

Logger.LogWarning($"AgencyBanking withdrawal 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 withdrawal response received. TransactionId=[{transactionId}], StatusCode=[{response.StatusCode}], ContentLength=[{responseContent?.Length ?? 0}]");

if (response.IsSuccessStatusCode == false)
{
Logger.LogWarning($"AgencyBanking withdrawal failed. TransactionId=[{transactionId}], StatusCode=[{response.StatusCode}], ResponseBody=[{responseContent}]");
return Result.Invalid($"Failed to process withdrawal {responseContent}");
}

TransactionResult balanceEnquiryResponse = StringSerialiser.Deserialise<TransactionResult>(responseContent, SerialiserOptions);
Logger.LogWarning($"AgencyBanking withdrawal completed. TransactionId=[{transactionId}], ResponseCode=[{balanceEnquiryResponse.ResponseCode}]");

OperatorResponse operatorResponse = new()
{
IsSuccessful = true,
ResponseCode = "0000",
ResponseMessage = "SUCCESS",
AdditionalTransactionResponseMetadata = new Dictionary<String, String>()
};

return Result.Success(operatorResponse);
}

private async Task<Result<OperatorResponse>> ProcessDeposit(Guid transactionId,
String agentId,
String accountNumber,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public class BalanceEnquiryResponse
public decimal AvailableBalance { get; set; }
}

[ExcludeFromCodeCoverage]
public class DepositRequest
{
public string TransactionId { get; set; }
Expand All @@ -39,6 +40,23 @@ public class DepositRequest
public String ReferenceNumber { get; set; }
}

public class WithdrawalRequest
{
public string TransactionId { get; set; }

public string CustomerId { get; set; }

public string AgentId { get; set; }

public string AccountNumber { get; set; }

public decimal Amount { get; set; }
public string Currency { get; set; }
public String Channel { get; set; }
public String Narration { get; set; }
public String ReferenceNumber { get; set; }
}

public class TransactionResult
{
public string ResponseCode { get; set; } = "";
Expand Down
Loading
Loading