From 4e4372b6f00413a84987eb4291802050c78b74ca Mon Sep 17 00:00:00 2001 From: StuartFerguson Date: Wed, 22 Jul 2026 15:36:52 +0100 Subject: [PATCH] wip --- .../AgencyBankingProxyTests.cs | 118 ++++++++++++++++++ .../AgencyBanking/AgencyBankingProxy.cs | 41 +++++- .../AgencyBanking/BalanceEnquiryResponse.cs | 27 ++++ .../OperatorInterfaces/OperatorResponse.cs | 9 ++ 4 files changed, 193 insertions(+), 2 deletions(-) diff --git a/TransactionProcessor.BusinessLogic.Tests/OperatorInterfaces/AgencyBankingProxyTests.cs b/TransactionProcessor.BusinessLogic.Tests/OperatorInterfaces/AgencyBankingProxyTests.cs index d60efced..e6756aec 100644 --- a/TransactionProcessor.BusinessLogic.Tests/OperatorInterfaces/AgencyBankingProxyTests.cs +++ b/TransactionProcessor.BusinessLogic.Tests/OperatorInterfaces/AgencyBankingProxyTests.cs @@ -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; @@ -339,6 +340,123 @@ public async Task AgencyBankingProxy_ProcessSaleMessage_DepositSuccess_ReturnsMa 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 metadata = new Dictionary + { + { "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 metadata = new Dictionary + { + { "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 diff --git a/TransactionProcessor.BusinessLogic/OperatorInterfaces/AgencyBanking/AgencyBankingProxy.cs b/TransactionProcessor.BusinessLogic/OperatorInterfaces/AgencyBanking/AgencyBankingProxy.cs index cfac4f48..7d6d3629 100644 --- a/TransactionProcessor.BusinessLogic/OperatorInterfaces/AgencyBanking/AgencyBankingProxy.cs +++ b/TransactionProcessor.BusinessLogic/OperatorInterfaces/AgencyBanking/AgencyBankingProxy.cs @@ -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; @@ -65,10 +66,10 @@ public async Task> ProcessSaleMessage(Guid transactionI Result result = messageType switch { "balanceenquiry" => await ProcessBalanceEnquiry(transactionId, merchant.MerchantId.ToString(), accountNumber, cancellationToken), "deposit" => await ProcessDeposit(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}]"); } @@ -76,6 +77,42 @@ public async Task> ProcessSaleMessage(Guid transactionI return result; } + private async Task> 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(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 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() }; + + operatorResponse.AdditionalTransactionResponseMetadata.Add("StatementLines", StringSerialiser.Serialise(statementLines)); + + return Result.Success(operatorResponse); + } + private async Task> ProcessDeposit(Guid transactionId, String agentId, String accountNumber, diff --git a/TransactionProcessor.BusinessLogic/OperatorInterfaces/AgencyBanking/BalanceEnquiryResponse.cs b/TransactionProcessor.BusinessLogic/OperatorInterfaces/AgencyBanking/BalanceEnquiryResponse.cs index 712cc974..bfc6e2f1 100644 --- a/TransactionProcessor.BusinessLogic/OperatorInterfaces/AgencyBanking/BalanceEnquiryResponse.cs +++ b/TransactionProcessor.BusinessLogic/OperatorInterfaces/AgencyBanking/BalanceEnquiryResponse.cs @@ -45,4 +45,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 Transactions { get; set; } + } + + public class MiniStatementItem + { + public DateTime TransactionDate { get; set; } + + public string TransactionType { get; set; } + + public decimal Amount { get; set; } + } } diff --git a/TransactionProcessor.BusinessLogic/OperatorInterfaces/OperatorResponse.cs b/TransactionProcessor.BusinessLogic/OperatorInterfaces/OperatorResponse.cs index fac2a70c..2b063f00 100644 --- a/TransactionProcessor.BusinessLogic/OperatorInterfaces/OperatorResponse.cs +++ b/TransactionProcessor.BusinessLogic/OperatorInterfaces/OperatorResponse.cs @@ -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; } + } } \ No newline at end of file