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
@@ -0,0 +1,14 @@
namespace TransactionProcessor.Mobile.BusinessLogic.Common;

public static class MetadataFields
{
public static string Amount { get; } = "Amount";
public static string RecipientEmail { get; } = "RecipientEmail";
public static string RecipientMobile { get; } = "RecipientMobile";
public static string CustomerAccountNumber { get; } = "CustomerAccountNumber";
public static string PataPawaPostPaidMessageType { get; } = "PataPawaPostPaidMessageType";
public static string PataPawaPrePayMessageType { get; } = "PataPawaPrePayMessageType";
public static string MeterNumber { get; } = "MeterNumber";
public static string CustomerName { get; } = "CustomerName";
public static string MobileNumber { get; } = "MobileNumber";
}
37 changes: 19 additions & 18 deletions TransactionProcessor.Mobile.BusinessLogic/Common/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

namespace TransactionProcessor.Mobile.BusinessLogic.Common;

public static class Extensions
public static partial class Extensions
{
public static TransactionRecord ToTransactionRecord(this TransactionCommands.PerformLogonCommand request,
Boolean isTrainingMode) {
Expand Down Expand Up @@ -199,6 +199,7 @@ public static ReconciliationRequestMessage ToReconciliationRequest(this PerformR
return reconciliationRequest;
}


public static SaleTransactionRequestMessage ToSaleTransactionRequest(this PerformVoucherIssueRequestModel model)
{
SaleTransactionRequestMessage saleTransactionRequest = new() {
Expand All @@ -214,9 +215,9 @@ public static SaleTransactionRequestMessage ToSaleTransactionRequest(this Perfor

// Add the additional request data
saleTransactionRequest.AdditionalRequestMetadata = new Dictionary<String, String> {
{"Amount", model.VoucherAmount.ToString()},
{"RecipientEmail", model.RecipientEmailAddress},
{"RecipientMobile", model.RecipientMobileNumber}
{MetadataFields.Amount, model.VoucherAmount.ToString()},
{MetadataFields.RecipientEmail, model.RecipientEmailAddress},
{MetadataFields.RecipientMobile, model.RecipientMobileNumber}
};

return saleTransactionRequest;
Expand All @@ -237,8 +238,8 @@ public static SaleTransactionRequestMessage ToSaleTransactionRequest(this Perfor

// Add the additional request data
saleTransactionRequest.AdditionalRequestMetadata = new Dictionary<String, String> {
{"Amount", model.TopupAmount.ToString()},
{"CustomerAccountNumber", model.CustomerAccountNumber}
{MetadataFields.Amount, model.TopupAmount.ToString()},
{MetadataFields.CustomerAccountNumber, model.CustomerAccountNumber}
};

return saleTransactionRequest;
Expand All @@ -256,8 +257,8 @@ public static SaleTransactionRequestMessage ToSaleTransactionRequest(this Perfor
};
// Add the additional request data
saleTransactionRequest.AdditionalRequestMetadata = new Dictionary<String, String> {
{"CustomerAccountNumber", model.CustomerAccountNumber},
{"PataPawaPostPaidMessageType", "VerifyAccount"}
{MetadataFields.CustomerAccountNumber, model.CustomerAccountNumber},
{MetadataFields.PataPawaPostPaidMessageType, "VerifyAccount"}
};

return saleTransactionRequest;
Expand All @@ -277,19 +278,19 @@ public static SaleTransactionRequestMessage ToSaleTransactionRequest(this Perfor
if (model.PostPayment){
// Add the additional request data
saleTransactionRequest.AdditionalRequestMetadata = new Dictionary<String, String>{
{ "CustomerAccountNumber", model.CustomerAccountNumber },
{ "CustomerName", model.CustomerAccountName },
{ "MobileNumber", model.CustomerMobileNumber },
{ "Amount", model.PaymentAmount.ToString() },
{ "PataPawaPostPaidMessageType", "ProcessBill" }
{ MetadataFields.CustomerAccountNumber, model.CustomerAccountNumber },
{ MetadataFields.CustomerName, model.CustomerAccountName },
{ MetadataFields.MobileNumber, model.CustomerMobileNumber },
{ MetadataFields.Amount, model.PaymentAmount.ToString() },
{ MetadataFields.PataPawaPostPaidMessageType, "ProcessBill" }
};
}
else{
saleTransactionRequest.AdditionalRequestMetadata = new Dictionary<String, String>{
{ "MeterNumber", model.CustomerAccountNumber },
{ "CustomerName", model.CustomerAccountName },
{ "PataPawaPrePayMessageType", "vend" },
{ "Amount", model.PaymentAmount.ToString() },
{ MetadataFields.MeterNumber, model.CustomerAccountNumber },
{ MetadataFields.CustomerName, model.CustomerAccountName },
{ MetadataFields.PataPawaPrePayMessageType, "vend" },
{ MetadataFields.Amount, model.PaymentAmount.ToString() },
};
}

Expand All @@ -310,7 +311,7 @@ public static SaleTransactionRequestMessage ToSaleTransactionRequest(this Perfor
// Add the additional request data
saleTransactionRequest.AdditionalRequestMetadata = new Dictionary<String, String> {
{"MeterNumber", model.MeterNumber.ToString()},
{"PataPawaPrePayMessageType", "meter"}
{MetadataFields.PataPawaPrePayMessageType, "meter"}
};

return saleTransactionRequest;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ private void CacheAccessToken(TokenResponseModel token)
(key, value, reason, state) =>
{
_ = AccessTokenExpired(key, value, reason, state);
}); ;
});

this.ApplicationCache.SetAccessToken(token, cacheEntryOptions);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System.Net;
using System.Net.Http.Json;
using System.Net.Sockets;
using System.Text;
using System.Text.Json;
using NUnit.Framework;
using Shouldly;
using TransactionProcessor.DataTransferObjects.Responses.Merchant;
Expand Down Expand Up @@ -84,11 +86,103 @@ public async Task Seed_CanBeApplied_AndResetRestoresDefaultMerchantState()
resetMerchant.Contacts[0].ContactEmailAddress.ShouldBe("test@example.com");
}

[Test]
public async Task TransactionMixSummary_ReturnsTransactionTypeAsString()
{
await using TestBackendHost host = await TestBackendHost.StartAsync(GetFreePort()).ConfigureAwait(false);

using HttpClient client = host.CreateClient();
SeedReportTransaction(host, RequestKind.MobileTopup);

StringContent requestContent = new(
JsonSerializer.Serialize(new
{
MerchantReportingId = 12345,
StartDate = new DateTime(2026, 8, 27),
EndDate = new DateTime(2026, 8, 27),
Breakdown = 1,
Measure = 1,
TopN = 5
}, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower }),
Encoding.UTF8,
"application/json");

HttpResponseMessage response = await client.PostAsync("/api/reporting/transactionmixsummary", requestContent).ConfigureAwait(false);

response.StatusCode.ShouldBe(HttpStatusCode.OK);

using JsonDocument document = JsonDocument.Parse(await response.Content.ReadAsStringAsync().ConfigureAwait(false));
JsonElement transactionType = document.RootElement.GetProperty("drill_down_transactions")
.EnumerateArray()
.Single()
.GetProperty("transaction_type");

transactionType.ValueKind.ShouldBe(JsonValueKind.String);
transactionType.GetString().ShouldBe("Mobile Topup");
}

[Test]
public async Task RecentActivityReceiptSearch_ReturnsTransactionTypeAsString()
{
await using TestBackendHost host = await TestBackendHost.StartAsync(GetFreePort()).ConfigureAwait(false);

using HttpClient client = host.CreateClient();
SeedReportTransaction(host, RequestKind.BillPaymentMakePayment);

StringContent requestContent = new(
JsonSerializer.Serialize(new
{
MerchantReportingId = 12345,
ReportDate = new DateTime(2026, 8, 27),
SearchText = (string?)null,
PageNumber = 1,
PageSize = 5
}, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower }),
Encoding.UTF8,
"application/json");

HttpResponseMessage response = await client.PostAsync("/api/reporting/recentactivityreceiptsearch", requestContent).ConfigureAwait(false);

response.StatusCode.ShouldBe(HttpStatusCode.OK);

using JsonDocument document = JsonDocument.Parse(await response.Content.ReadAsStringAsync().ConfigureAwait(false));
JsonElement transactionType = document.RootElement.GetProperty("items")
.EnumerateArray()
.Single()
.GetProperty("transaction_type");

transactionType.ValueKind.ShouldBe(JsonValueKind.String);
transactionType.GetString().ShouldBe("Bill Payment");
}

private static int GetFreePort()
{
using TcpListener listener = new(IPAddress.Loopback, 0);
listener.Start();
int port = ((IPEndPoint)listener.LocalEndpoint).Port;
return port;
}

private static void SeedReportTransaction(TestBackendHost host, RequestKind transactionType)
{
BackendSeed seed = new()
{
ReportTransactions =
[
new ReportTransactionSeed
{
Reference = "TXN-10001",
ReceiptReference = "RCPT-10001",
TransactionType = transactionType,
Product = "Custom",
Operator = "Safaricom",
Status = "Success",
Amount = 100.00m,
TransactionDateTime = new DateTime(2026, 8, 27, 9, 30, 0)
}
]
};

host.ApplySeed(seed);
}
}
18 changes: 15 additions & 3 deletions TransactionProcessor.Mobile.UITests/Steps/SharedSteps.cs
Original file line number Diff line number Diff line change
Expand Up @@ -417,12 +417,12 @@
[Given(@"the following transaction mix report transactions exist")]
public Task GivenTheFollowingTransactionMixReportTransactionsExist(DataTable table)
{
foreach (DataTableRow row in table.Rows)
{
foreach (DataTableRow row in table.Rows) {
RequestKind requestKind = ParseRequestKind(GetString(row, "TransactionType"));
this.testingContext.ScenarioSeed.ReportTransactions.Add(new ReportTransactionSeed
{
Reference = GetString(row, "Reference"),
TransactionType = GetString(row, "TransactionType"),
TransactionType = requestKind,
Product = GetString(row, "Product"),
Operator = GetString(row, "Operator"),
Status = GetString(row, "Status"),
Expand All @@ -436,6 +436,18 @@
return Task.CompletedTask;
}

private static RequestKind ParseRequestKind(string transactionType)
{
string normalizedTransactionType = transactionType.Trim();
return normalizedTransactionType switch
{
"Mobile Topup" => RequestKind.MobileTopup,
"Bill Payment" => RequestKind.BillPaymentMakePayment,
"Voucher Issue" => RequestKind.Voucher,
_ => Enum.Parse<RequestKind>(normalizedTransactionType, ignoreCase: true)
};
}

[Given(@"the following bills are available at the PataPawa PostPaid Host")]
public Task GivenTheFollowingBillsAreAvailableAtThePataPawaPostPaidHost(DataTable table)
{
Expand Down Expand Up @@ -562,7 +574,7 @@

private EstateDetails EnsureEstate(Guid estateId, string estateName, string estateReference)
{
EstateDetails estate = this.testingContext.Estates.FirstOrDefault(e => e.EstateId == estateId || string.Equals(e.EstateName, estateName, StringComparison.OrdinalIgnoreCase));

Check warning on line 577 in TransactionProcessor.Mobile.UITests/Steps/SharedSteps.cs

View workflow job for this annotation

GitHub Actions / end_to_end_tests

Converting null literal or possible null value to non-nullable type.
if (estate != null)
{
return estate;
Expand Down
32 changes: 32 additions & 0 deletions TransactionProcessor.Mobile.UITests/Steps/SharedStepsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using Reqnroll;
using Shouldly;
using TransactionProcessor.Mobile.UiTestBackend;
using TransactionProcessor.Mobile.UITests.Common;

namespace TransactionProcessor.Mobile.UITests.Steps;

[TestFixture]
public class SharedStepsTests
{
[Test]
public async Task GivenTheFollowingTransactionMixReportTransactionsExist_MapsEstablishedLabelsToRequestKinds()
{
TestingContext testingContext = new();
testingContext.TestHostHelper = new TestHostHelper(testingContext);
SharedSteps sut = new(testingContext);

Table table = new(["Reference", "TransactionType", "Product", "Operator", "Status", "Amount", "TransactionDateTime"]);
table.AddRow(["TXN-10001", "Mobile Topup", "Custom", "Safaricom", "Success", "100.00", "Today"]);
table.AddRow(["TXN-10002", "Bill Payment", "Bill Pay (Post)", "PataPawa PostPay", "Success", "250.00", "Today"]);
table.AddRow(["TXN-10003", "Voucher Issue", "10 KES", "Voucher", "Failed", "0.00", "Today"]);

DataTable dataTable = DataTable.FromTable(table);

await sut.GivenTheFollowingTransactionMixReportTransactionsExist(dataTable);

testingContext.ScenarioSeed.ReportTransactions.Count.ShouldBe(3);
testingContext.ScenarioSeed.ReportTransactions[0].TransactionType.ShouldBe(RequestKind.MobileTopup);
testingContext.ScenarioSeed.ReportTransactions[1].TransactionType.ShouldBe(RequestKind.BillPaymentMakePayment);
testingContext.ScenarioSeed.ReportTransactions[2].TransactionType.ShouldBe(RequestKind.Voucher);
}
}
17 changes: 15 additions & 2 deletions TransactionProcessor.Mobile.UiTestBackend/BackendModels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -160,11 +160,24 @@ public sealed record DepositSeed
public DateTime DateTime { get; init; } = DateTime.UtcNow;
}

public enum RequestKind
{
NotSet,
Logon,
MerchantDeposit,
BillPaymentGetAccount,
BillPaymentGetMeter,
BillPaymentMakePayment,
Voucher,
MobileTopup

}

public sealed record ReceiptSeed
{
public string Reference { get; init; } = string.Empty;
public string ReceiptReference { get; init; } = string.Empty;
public string TransactionType { get; init; } = string.Empty;
public RequestKind TransactionType { get; init; } = RequestKind.NotSet;
public string Product { get; init; } = string.Empty;
public string Operator { get; init; } = string.Empty;
public string Status { get; init; } = "Success";
Expand All @@ -175,7 +188,7 @@ public sealed record ReceiptSeed
public sealed record ReportTransactionSeed
{
public string Reference { get; init; } = string.Empty;
public string TransactionType { get; init; } = string.Empty;
public RequestKind TransactionType { get; init; } = RequestKind.NotSet;
public string Product { get; init; } = string.Empty;
public string Operator { get; init; } = string.Empty;
public string Status { get; init; } = "Success";
Expand Down
Loading
Loading