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 @@ -6,6 +6,7 @@
using SimpleResults;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text.Json;
Expand Down Expand Up @@ -531,6 +532,123 @@ public async Task AgencyBankingProxy_ProcessSaleMessage_WithdrawalSuccess_WithMi
requestJson.RootElement.GetProperty("transactionId").GetString().ShouldBe(TestData.TransactionId.ToString());
}

[Fact]
public async Task AgencyBankingProxy_ProcessSaleMessage_MiniStatementFailure_ReturnsInvalidResult()
{
HttpResponseMessage responseMessage = new HttpResponseMessage(HttpStatusCode.BadRequest)
{
Content = new StringContent("mini statement 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", "ministatement" },
{ "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 mini-statement mini statement rejected");

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

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("count").GetInt32().ShouldBe(5);
}

[Fact]
public async Task AgencyBankingProxy_ProcessSaleMessage_MiniStatementSuccess_ReturnsMappedOperatorResponse()
{
HttpResponseMessage responseMessage = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent("""
{
"responseCode": "0000",
"responseMessage": "Mini statement successful",
"transactions": [
{
"transactionDate": "2026-07-21T00:00:00",
"transactionType": "D",
"amount": 25.00
},
{
"transactionDate": "2026-07-21T00:01:00",
"transactionType": "C",
"amount": 10.50
}
]
}
""")
};

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", "ministatement" },
{ "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.ResponseMessage.ShouldBe("SUCCESS");
result.Data.AdditionalTransactionResponseMetadata.ShouldContainKey("StatementLines");

String statementLinesJson = result.Data.AdditionalTransactionResponseMetadata["StatementLines"];
using JsonDocument statementLinesDocument = JsonDocument.Parse(statementLinesJson);
statementLinesDocument.RootElement.ValueKind.ShouldBe(JsonValueKind.Array);
statementLinesDocument.RootElement.GetArrayLength().ShouldBe(2);

JsonElement firstLine = statementLinesDocument.RootElement[0];
firstLine.EnumerateObject().Any(property => property.Value.ValueKind == JsonValueKind.String && property.Value.GetString() == "D").ShouldBeTrue();
firstLine.EnumerateObject().Any(property => property.Value.ValueKind == JsonValueKind.Number && property.Value.GetDecimal() == 25.00m).ShouldBeTrue();

JsonElement secondLine = statementLinesDocument.RootElement[1];
secondLine.EnumerateObject().Any(property => property.Value.ValueKind == JsonValueKind.String && property.Value.GetString() == "C").ShouldBeTrue();
secondLine.EnumerateObject().Any(property => property.Value.ValueKind == JsonValueKind.Number && property.Value.GetDecimal() == 10.50m).ShouldBeTrue();

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

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("count").GetInt32().ShouldBe(5);
}

private static AgencyBankingConfiguration CreateConfiguration()
{
return new AgencyBankingConfiguration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using SimpleResults;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading;
Expand Down Expand Up @@ -66,10 +67,10 @@ public async Task<Result<OperatorResponse>> ProcessSaleMessage(Guid transactionI
"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),
"ministatement" => await ProcessMiniStatement(merchant.MerchantId.ToString(), accountNumber, cancellationToken),
_ => Result.Invalid($"AgencyBankingMessageType - Message Type {messageType} is not supported for this transaction type")
};



if (result.Status == ResultStatus.Invalid) {
Logger.LogWarning($"AgencyBanking sale request rejected. TransactionId=[{transactionId}] unsupported message type [{messageType}]");
}
Expand Down Expand Up @@ -130,6 +131,42 @@ private async Task<Result<OperatorResponse>> ProcessWithdrawal(Guid transactionI
return Result.Success(operatorResponse);
}

private async Task<Result<OperatorResponse>> ProcessMiniStatement(String agentId,
String accountNumber,
CancellationToken cancellationToken) {
HttpRequestMessage request = new(HttpMethod.Post, $"{this.Configuration.Url}/transactions/mini-statement");

MiniStatementRequest body = new() { AgentId = agentId, AccountNumber = accountNumber };

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

Logger.LogWarning($"AgencyBanking mini-statement request starting. 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 mini-statement response received. AgentId=[{agentId}], AccountNumber=[{accountNumber}], StatusCode=[{response.StatusCode}], ContentLength=[{responseContent?.Length ?? 0}]");

if (response.IsSuccessStatusCode == false) {
Logger.LogWarning($"AgencyBanking mini-statement failed. AgentId=[{agentId}], AccountNumber=[{accountNumber}], StatusCode=[{response.StatusCode}], ResponseBody=[{responseContent}]");
return Result.Invalid($"Failed to process mini-statement {responseContent}");
}

MiniStatementResponse miniStatementResponse = StringSerialiser.Deserialise<MiniStatementResponse>(responseContent, SerialiserOptions);
Logger.LogWarning($"AgencyBanking mini-statement completed. AgentId=[{agentId}], AccountNumber=[{accountNumber}], ResponseCode=[{miniStatementResponse.ResponseCode}]");

// Transform the mini-statement response into an operator response
List<OperatorStatementLine> statementLines = miniStatementResponse.Transactions.Select(line => new OperatorStatementLine { Amount = line.Amount, TransactionDate = line.TransactionDate, TransactionType = line.TransactionType }).ToList();


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

operatorResponse.AdditionalTransactionResponseMetadata.Add("StatementLines", StringSerialiser.Serialise(statementLines));

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 @@ -63,4 +63,31 @@ public class TransactionResult

public string TransactionId { get; set; } = "";
}

public class MiniStatementRequest
{
public string AgentId { get; set; }

public string AccountNumber { get; set; }

public int Count { get; set; } = 5;
}

public class MiniStatementResponse
{
public string ResponseCode { get; set; }

public string ResponseMessage { get; set; }

public List<MiniStatementItem> Transactions { get; set; }
}

public class MiniStatementItem
{
public DateTime TransactionDate { get; set; }

public string TransactionType { get; set; }

public decimal Amount { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,13 @@ public class OperatorResponse

#endregion
}

public class OperatorStatementLine
{
public DateTime TransactionDate { get; set; }

public string TransactionType { get; set; }

public decimal Amount { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,8 @@ Background:
| Test Estate 1 | PataPawa PrePay | PataPawa PrePay Contract | Pre Pay Bill Pay | Bill Pay (Pre) | | BillPayment |
| Test Estate 1 | AgencyBanking | AgencyBanking Contract | Balance Enquiry | Balance Enquiry | | MobileTopup |
| Test Estate 1 | AgencyBanking | AgencyBanking Contract | Deposit | Deposit | | MobileTopup |
| Test Estate 1 | AgencyBanking | AgencyBanking Contract | Withdrawal | Withdrawal | | MobileTopup |
| Test Estate 1 | AgencyBanking | AgencyBanking Contract | Withdrawal | Withdrawal | | MobileTopup |
| Test Estate 1 | AgencyBanking | AgencyBanking Contract | MiniStatement | Mini Statement | | MobileTopup |

When I add the following Transaction Fees
| EstateName | OperatorName | ContractDescription | ProductName | CalculationType | FeeDescription | Value |
Expand Down Expand Up @@ -180,7 +181,8 @@ Scenario: Sale Transactions
| Today | 11 | Sale | 2 | Test Merchant 1 | 123456780 | Test Estate 1 | PataPawa PrePay | 25.00 | | | PataPawa PrePay Contract | Pre Pay Bill Pay | test@recipient.co.uk | | vend | 00000001 | Customer 1 | 00000001 |
| Today | 12 | Sale | 1 | Test Merchant 1 | 123456780 | Test Estate 1 | AgencyBanking | 0.00 | | | AgencyBanking Contract | Balance Enquiry | | 12345678 | balanceenquiry | 12345678 | | |
| Today | 13 | Sale | 1 | Test Merchant 1 | 123456780 | Test Estate 1 | AgencyBanking | 100.00 | | | AgencyBanking Contract | Deposit | | 12345678 | deposit | 12345678 | | |
| Today | 14 | Sale | 1 | Test Merchant 1 | 123456780 | Test Estate 1 | AgencyBanking | 9.00 | | | AgencyBanking Contract | Withdrawal | | 12345678 | withdrawal | 12345678 | | |
| Today | 14 | Sale | 1 | Test Merchant 1 | 123456780 | Test Estate 1 | AgencyBanking | 9.00 | | | AgencyBanking Contract | Withdrawal | | 12345678 | withdrawal | 12345678 | | |
| Today | 15 | Sale | 1 | Test Merchant 1 | 123456780 | Test Estate 1 | AgencyBanking | 0.00 | | | AgencyBanking Contract | MiniStatement | | 12345678 | ministatement | 12345678 | | |
| Today | 100 | Sale | 1 | Test Merchant 1 | 123456781 | Test Estate 1 | Safaricom | 100.00 | 123456789 | testcustomer@customer.co.uk | Safaricom Contract | Variable Topup | | | | | | |
| Today | 101 | Sale | 1 | Test Merchant 1 | 123456780 | InvalidEstate | Safaricom | 100.00 | 123456789 | testcustomer@customer.co.uk | Safaricom Contract | Variable Topup | | | | | | |
| Today | 102 | Sale | 1 | InvalidMerchant | 123456780 | Test Estate 1 | Safaricom | 100.00 | 123456789 | testcustomer@customer.co.uk | Safaricom Contract | Variable Topup | | | | | | |
Expand All @@ -206,6 +208,7 @@ Scenario: Sale Transactions
| Test Estate 1 | Test Merchant 1 | 12 | 0000 | SUCCESS |
| Test Estate 1 | Test Merchant 1 | 13 | 0000 | SUCCESS |
| Test Estate 1 | Test Merchant 1 | 14 | 0000 | SUCCESS |
| Test Estate 1 | Test Merchant 1 | 15 | 0000 | SUCCESS |
| Test Estate 1 | Test Merchant 1 | 100 | 1000 | Device Identifier 123456781 not valid for Merchant Test Merchant 1 |
| InvalidEstate | Test Merchant 1 | 101 | 1001 | Estate Id [79902550-64df-4491-b0c1-4e78943928a3] is not a valid estate |
| Test Estate 1 | InvalidMerchant | 102 | 1002 | Merchant Id [d59320fa-4c3e-4900-a999-483f6a10c69a] is not a valid merchant for estate [Test Estate 1] |
Expand Down
Loading
Loading