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 @@ -46,4 +46,54 @@ public async Task GetDailyPerformanceSummaryQuery_ReturnsMockedSummaryForToday()
It.IsAny<CancellationToken>()),
Times.Once);
}

[Fact]
public async Task GetTransactionMixSummaryQuery_ReturnsRequestedBreakdown()
{
Mock<IReportsService> reportsService = new();
Mock<IApplicationCache> applicationCache = new();
MerchantDetailsModel merchantDetails = new()
{
MerchantReportingId = 12345
};

applicationCache.Setup(a => a.GetMerchantDetails()).Returns(merchantDetails);
reportsService.Setup(r => r.GetTransactionMixSummary(
merchantDetails.MerchantReportingId,
It.IsAny<DateTime>(),
It.IsAny<DateTime>(),
TransactionMixBreakdown.Product,
TransactionMixMeasure.Value,
5,
It.IsAny<CancellationToken>()))
.ReturnsAsync(Result.Success(TransactionMixSummaryModel.CreateMock(
merchantReportingId: merchantDetails.MerchantReportingId,
breakdown: TransactionMixBreakdown.Product,
measure: TransactionMixMeasure.Value)));

ReportRequestHandler handler = new(reportsService.Object, applicationCache.Object);

Result<TransactionMixSummaryModel> result = await handler.Handle(
new ReportQueries.GetTransactionMixSummaryQuery(
StartDate: new DateTime(2026, 7, 1),
EndDate: new DateTime(2026, 7, 31),
Breakdown: TransactionMixBreakdown.Product,
Measure: TransactionMixMeasure.Value,
TopN: 5),
CancellationToken.None);

result.IsSuccess.ShouldBeTrue();
result.Data.Breakdown.ShouldBe(TransactionMixBreakdown.Product);
result.Data.Measure.ShouldBe(TransactionMixMeasure.Value);
result.Data.Items.ShouldNotBeEmpty();
reportsService.Verify(r => r.GetTransactionMixSummary(
merchantDetails.MerchantReportingId,
new DateTime(2026, 7, 1),
new DateTime(2026, 7, 31),
TransactionMixBreakdown.Product,
TransactionMixMeasure.Value,
5,
It.IsAny<CancellationToken>()),
Times.Once);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
using Moq;
using RichardSzalay.MockHttp;
using Shouldly;
using SimpleResults;
using System.Net;
using System.Text;
using TransactionProcessor.Mobile.BusinessLogic.Logging;
using TransactionProcessor.Mobile.BusinessLogic.Models;
using TransactionProcessor.Mobile.BusinessLogic.Serialisation;
using TransactionProcessor.Mobile.BusinessLogic.Services;
using TransactionProcessor.Mobile.BusinessLogic.UIServices;

namespace TransactionProcessor.Mobile.BusinessLogic.Tests.ServicesTests;

public class ReportsServiceTests
{
private readonly MockHttpMessageHandler MockHttpMessageHandler;
private readonly Mock<IApplicationCache> ApplicationCache;
private readonly Mock<IApplicationInfoService> ApplicationInfoService;
private readonly IReportsService ReportsService;

public ReportsServiceTests()
{
this.MockHttpMessageHandler = new MockHttpMessageHandler();
this.ApplicationCache = new Mock<IApplicationCache>();
this.ApplicationInfoService = new Mock<IApplicationInfoService>();
this.ApplicationCache.Setup(s => s.GetAccessToken()).Returns(new TokenResponseModel
{
AccessToken = "token"
});
this.ApplicationInfoService.Setup(s => s.VersionString).Returns("1.2.3");
this.ReportsService = new ReportsService(_ => "http://localhost",
this.MockHttpMessageHandler.ToHttpClient(),
this.ApplicationCache.Object,
obj => StringSerialiser.Serialise(obj),
(json, type) => StringSerialiser.DeserializeObject<object>(json, type),
this.ApplicationInfoService.Object);
Logger.Initialise(new NullLogger());
StringSerialiser.Initialise((IStringSerialiser)new SystemTextJsonSerializer(SystemTextJsonSerializer.GetDefaultJsonSerializerOptions()));
}

[Fact]
public async Task GetTransactionMixSummary_SendsRequestAndMapsResponse()
{
MerchantTransactionMixSummaryResponseDto response = new()
{
FromDate = new DateTime(2026, 7, 1),
ToDate = new DateTime(2026, 7, 31),
Breakdown = TransactionMixBreakdown.Product,
Measure = TransactionMixMeasure.Value,
TotalCount = 3,
TotalValue = 3110.00m,
Items =
[
new TransactionMixSummaryItemDto { Key = "custom", Label = "Custom", Count = 2, Value = 1250.00m },
new TransactionMixSummaryItemDto { Key = "bill-pay-post", Label = "Bill Pay (Post)", Count = 1, Value = 1900.00m },
],
DrillDownTransactions =
[
new TransactionMixDrillDownTransactionDto
{
Reference = "TXN-10001",
TransactionType = "Mobile Topup",
Product = "Custom",
Operator = "Safaricom",
Status = "Success",
Amount = 100.00m,
TransactionDateTime = new DateTime(2026, 7, 6, 9, 30, 0)
}
]
};

String requestPayload = string.Empty;
this.MockHttpMessageHandler.When(HttpMethod.Post, "http://localhost/api/reporting/transactionmixsummary?applicationVersion=1.2.3")
.Respond(req =>
{
requestPayload = req.Content!.ReadAsStringAsync().GetAwaiter().GetResult();
return new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(StringSerialiser.Serialise(response), Encoding.UTF8, "application/json")
};
});

Result<TransactionMixSummaryModel> result = await this.ReportsService.GetTransactionMixSummary(12345,
new DateTime(2026, 7, 1),
new DateTime(2026, 7, 31),
TransactionMixBreakdown.Product,
TransactionMixMeasure.Value,
5,
CancellationToken.None);

result.IsSuccess.ShouldBeTrue();
result.Data.Breakdown.ShouldBe(TransactionMixBreakdown.Product);
result.Data.Measure.ShouldBe(TransactionMixMeasure.Value);
result.Data.TotalCount.ShouldBe(3);
result.Data.TotalValue.ShouldBe(3110.00m);
result.Data.Items.Count.ShouldBe(2);
result.Data.DrillDownTransactions.Count.ShouldBe(1);
requestPayload.ShouldContain("\"merchant_reporting_id\": 12345");
requestPayload.ShouldContain("\"top_n\": 5");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,21 @@ public async Task ReportsPageViewModel_OptionSelectedCommand_Execute_IsExecuted(
public async Task ReportsPageViewModel_Initialise_IsInitialised()
{
await this.ViewModel.Initialise(CancellationToken.None);
this.ViewModel.ReportsMenuOptions.Count.ShouldBe(1);
this.ViewModel.ReportsMenuOptions.Count.ShouldBe(2);
}

[Fact]
public async Task ReportsPageViewModel_TransactionMixCommand_Execute_IsExecuted()
{
ItemSelected<ListViewItem> itemSelected = new ItemSelected<ListViewItem>
{
SelectedItem = new ListViewItem { Title = "Transaction Mix" },
SelectedItemIndex = 1,
};

this.ViewModel.OptionSelectedCommand.Execute(itemSelected);

this.NavigationService.Verify(v => v.GoToTransactionMixSummaryPage(), Times.Once);
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using MediatR;
using Moq;
using Shouldly;
using SimpleResults;
using TransactionProcessor.Mobile.BusinessLogic.Common;
using TransactionProcessor.Mobile.BusinessLogic.Models;
using TransactionProcessor.Mobile.BusinessLogic.Requests;
using TransactionProcessor.Mobile.BusinessLogic.Services;
using TransactionProcessor.Mobile.BusinessLogic.UIServices;
using TransactionProcessor.Mobile.BusinessLogic.ViewModels.Reports;

namespace TransactionProcessor.Mobile.BusinessLogic.Tests.ViewModelTests.Reports;

public class TransactionMixPageViewModelTests
{
private readonly Mock<IMediator> Mediator;
private readonly Mock<INavigationService> NavigationService;
private readonly Mock<IApplicationCache> ApplicationCache;
private readonly Mock<IDialogService> DialogService;
private readonly Mock<IDeviceService> DeviceService;
private readonly Mock<INavigationParameterService> NavigationParameterService;
private readonly TransactionMixPageViewModel ViewModel;

public TransactionMixPageViewModelTests()
{
this.Mediator = new Mock<IMediator>();
this.Mediator
.Setup(m => m.Send(It.IsAny<ReportQueries.GetTransactionMixSummaryQuery>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(Result.Success(TransactionMixSummaryModel.CreateMock(
merchantReportingId: 12345,
breakdown: TransactionMixBreakdown.TransactionType,
measure: TransactionMixMeasure.Count)));

this.NavigationService = new Mock<INavigationService>();
this.ApplicationCache = new Mock<IApplicationCache>();
this.DialogService = new Mock<IDialogService>();
this.DeviceService = new Mock<IDeviceService>();
this.NavigationParameterService = new Mock<INavigationParameterService>();

this.ViewModel = new TransactionMixPageViewModel(this.Mediator.Object,
this.NavigationService.Object,
this.ApplicationCache.Object,
this.DialogService.Object,
this.DeviceService.Object,
this.NavigationParameterService.Object);
}

[Fact]
public async Task Initialise_LoadsDefaultTransactionMixSummary()
{
await this.ViewModel.Initialise(CancellationToken.None);

this.ViewModel.Title.ShouldBe("Transaction Mix");
this.ViewModel.SelectedBreakdown.ShouldBe(TransactionMixBreakdown.TransactionType);
this.ViewModel.SelectedMeasure.ShouldBe(TransactionMixMeasure.Count);
this.ViewModel.Summary.ShouldNotBeNull();
this.ViewModel.Items.Count.ShouldBeGreaterThan(0);
this.ViewModel.TopItems.Count.ShouldBeGreaterThan(0);
this.ViewModel.HasChartData.ShouldBeTrue();
this.ViewModel.ChartSeries.Length.ShouldBe(1);
this.ViewModel.ChartYAxes.Count.ShouldBe(1);
this.ViewModel.ChartXAxes.Count.ShouldBe(1);
this.ViewModel.IsLoading.ShouldBeFalse();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
namespace TransactionProcessor.Mobile.BusinessLogic.Models;

public enum TransactionMixBreakdown
{
NotSet = 0,
TransactionType = 1,
Product = 2,
Operator = 3,
Status = 4,
}

public enum TransactionMixMeasure
{
NotSet = 0,
Count = 1,
Value = 2,
}

public sealed class MerchantTransactionMixSummaryRequest
{
public int MerchantReportingId { get; set; }

public DateTime StartDate { get; set; }

public DateTime EndDate { get; set; }

public TransactionMixBreakdown Breakdown { get; set; }

public TransactionMixMeasure Measure { get; set; }

public int TopN { get; set; } = 5;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
namespace TransactionProcessor.Mobile.BusinessLogic.Models;

public sealed record TransactionMixSummaryModel
{
public DateTime FromDate { get; set; }

public DateTime ToDate { get; set; }

public TransactionMixBreakdown Breakdown { get; set; }

public TransactionMixMeasure Measure { get; set; }

public decimal TotalCount { get; set; }

public decimal TotalValue { get; set; }

public List<TransactionMixSummaryItemModel> Items { get; set; } = [];

public List<TransactionMixDrillDownTransactionModel> DrillDownTransactions { get; set; } = [];

public static TransactionMixSummaryModel CreateMock(int merchantReportingId,
TransactionMixBreakdown breakdown,
TransactionMixMeasure measure)
{
DateTime today = DateTime.Today;

List<TransactionMixSummaryItemModel> items = breakdown switch
{
TransactionMixBreakdown.Product => new List<TransactionMixSummaryItemModel>
{
new("custom", "Custom", 12, 1250.00m),
new("bill-pay-post", "Bill Pay (Post)", 8, 1900.00m),
new("10-kes", "10 KES", 6, 60.00m),
},
TransactionMixBreakdown.Operator => new List<TransactionMixSummaryItemModel>
{
new("safaricom", "Safaricom", 18, 1800.00m),
new("voucher", "Voucher", 6, 60.00m),
new("patapawa-postpay", "PataPawa PostPay", 4, 1500.00m),
},
TransactionMixBreakdown.Status => new List<TransactionMixSummaryItemModel>
{
new("success", "Success", 22, 3110.00m),
new("failed", "Failed", 6, 0.00m),
},
_ => new List<TransactionMixSummaryItemModel>
{
new("mobile-topup", "Mobile Topup", 14, 1250.00m),
new("bill-payment", "Bill Payment", 10, 1900.00m),
new("voucher-issue", "Voucher Issue", 4, 60.00m),
}
};

List<TransactionMixDrillDownTransactionModel> transactions = new()
{
new("TXN-10001", "Mobile Topup", "Custom", "Safaricom", "Success", 100.00m, today.AddHours(-1)),
new("TXN-10002", "Bill Payment", "Bill Pay (Post)", "PataPawa PostPay", "Success", 250.00m, today.AddHours(-2)),
new("TXN-10003", "Voucher Issue", "10 KES", "Voucher", "Failed", 0.00m, today.AddHours(-3)),
};

return new TransactionMixSummaryModel
{
Breakdown = breakdown,
DrillDownTransactions = transactions,
FromDate = today.AddDays(-7),
ToDate = today,
Items = items,
Measure = measure,
TotalCount = 28,
TotalValue = 3110.00m,
};
}
}

public sealed record TransactionMixSummaryItemModel(string Key,
string Label,
decimal Count,
decimal Value);

public sealed record TransactionMixDrillDownTransactionModel(string Reference,
string TransactionType,
string Product,
string Operator,
string Status,
decimal Amount,
DateTime TransactionDateTime);
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@

namespace TransactionProcessor.Mobile.BusinessLogic.RequestHandlers;

public sealed class ReportRequestHandler : IRequestHandler<ReportQueries.GetDailyPerformanceSummaryQuery, Result<DailyPerformanceSummaryModel>> {
public sealed class ReportRequestHandler : IRequestHandler<ReportQueries.GetDailyPerformanceSummaryQuery, Result<DailyPerformanceSummaryModel>>,
IRequestHandler<ReportQueries.GetTransactionMixSummaryQuery, Result<TransactionMixSummaryModel>> {
private readonly IReportsService ReportsService;
private readonly IApplicationCache ApplicationCache;

Expand Down Expand Up @@ -37,4 +38,22 @@ public async Task<Result<DailyPerformanceSummaryModel>> Handle(ReportQueries.Get

return await this.ReportsService.GetDailyPerformanceSummary(request.Period, merchant.MerchantReportingId, dates.startDate, dates.endDate, cancellationToken);
}

public async Task<Result<TransactionMixSummaryModel>> Handle(ReportQueries.GetTransactionMixSummaryQuery request,
CancellationToken cancellationToken)
{
MerchantDetailsModel merchant = this.ApplicationCache.GetMerchantDetails();
if (merchant == null)
{
return Result.Failure("Merchant details are not available.");
}

return await this.ReportsService.GetTransactionMixSummary(merchant.MerchantReportingId,
request.StartDate,
request.EndDate,
request.Breakdown,
request.Measure,
request.TopN,
cancellationToken);
}
}
Loading
Loading