diff --git a/Directory.Packages.props b/Directory.Packages.props index 0b5a68e8..fc5b0573 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -44,7 +44,7 @@ - + diff --git a/TransactionProcessor.Mobile.BusinessLogic.Tests/RequestHandlerTests/ReportRequestHandlerTests.cs b/TransactionProcessor.Mobile.BusinessLogic.Tests/RequestHandlerTests/ReportRequestHandlerTests.cs index d02db4ca..55609806 100644 --- a/TransactionProcessor.Mobile.BusinessLogic.Tests/RequestHandlerTests/ReportRequestHandlerTests.cs +++ b/TransactionProcessor.Mobile.BusinessLogic.Tests/RequestHandlerTests/ReportRequestHandlerTests.cs @@ -1,9 +1,11 @@ using MediatR; +using Moq; using SimpleResults; using Shouldly; using TransactionProcessor.Mobile.BusinessLogic.RequestHandlers; using TransactionProcessor.Mobile.BusinessLogic.Requests; using TransactionProcessor.Mobile.BusinessLogic.Models; +using TransactionProcessor.Mobile.BusinessLogic.Services; namespace TransactionProcessor.Mobile.BusinessLogic.Tests.RequestHandlerTests; @@ -12,7 +14,23 @@ public class ReportRequestHandlerTests [Fact] public async Task GetDailyPerformanceSummaryQuery_ReturnsMockedSummaryForToday() { - ReportRequestHandler handler = new(); + Mock reportsService = new(); + Mock applicationCache = new(); + MerchantDetailsModel merchantDetails = new() + { + MerchantReportingId = 12345 + }; + + applicationCache.Setup(a => a.GetMerchantDetails()).Returns(merchantDetails); + reportsService.Setup(r => r.GetDailyPerformanceSummary( + PerformanceSummaryPeriod.Today, + merchantDetails.MerchantReportingId, + It.IsAny(), + It.IsAny(), + It.IsAny())) + .ReturnsAsync(Result.Success(DailyPerformanceSummaryModel.CreateMock(PerformanceSummaryPeriod.Today))); + + ReportRequestHandler handler = new(reportsService.Object, applicationCache.Object); Result result = await handler.Handle(new ReportQueries.GetDailyPerformanceSummaryQuery(PerformanceSummaryPeriod.Today), CancellationToken.None); @@ -20,5 +38,12 @@ public async Task GetDailyPerformanceSummaryQuery_ReturnsMockedSummaryForToday() result.Data.Period.ShouldBe(PerformanceSummaryPeriod.Today); result.Data.Metrics.ShouldContain(m => m.Title == "Total transaction count" && m.Value == "48"); result.Data.DrillDownTransactions.ShouldContain(t => t.Reference == "TXN-00048"); + reportsService.Verify(r => r.GetDailyPerformanceSummary( + PerformanceSummaryPeriod.Today, + merchantDetails.MerchantReportingId, + It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Once); } } diff --git a/TransactionProcessor.Mobile.BusinessLogic.Tests/ServicesTests/MerchantServiceTests.cs b/TransactionProcessor.Mobile.BusinessLogic.Tests/ServicesTests/MerchantServiceTests.cs index 7bd51c08..45f7fccc 100644 --- a/TransactionProcessor.Mobile.BusinessLogic.Tests/ServicesTests/MerchantServiceTests.cs +++ b/TransactionProcessor.Mobile.BusinessLogic.Tests/ServicesTests/MerchantServiceTests.cs @@ -75,7 +75,7 @@ public async Task MerchantService_GetContractProducts_ContractProductsAreReturne }); - this.MockHttpMessageHandler.When($"http://localhost/api/merchants/contracts?application_version=1.0.0") + this.MockHttpMessageHandler.When($"http://localhost/api/merchants/contracts?applicationVersion=1.0.0") .Respond("application/json", StringSerialiser.Serialise(contracts)); // Respond with JSON var result = await this.MerchantService.GetContractProducts(CancellationToken.None); @@ -86,7 +86,7 @@ public async Task MerchantService_GetContractProducts_ContractProductsAreReturne [Fact] public async Task MerchantService_GetContractProducts_HttpCallFailed_FailureReturned(){ - this.MockHttpMessageHandler.When($"http://localhost/api/merchants/contracts?application_version=1.0.0") + this.MockHttpMessageHandler.When($"http://localhost/api/merchants/contracts?applicationVersion=1.0.0") .Respond(HttpStatusCode.InternalServerError); Result> result = await this.MerchantService.GetContractProducts(CancellationToken.None); @@ -122,7 +122,7 @@ public async Task MerchantService_GetMerchantDetails_MerchantDetailsReturned(){ } }; - this.MockHttpMessageHandler.When($"http://localhost/api/merchants?application_version=1.0.0") + this.MockHttpMessageHandler.When($"http://localhost/api/merchants?applicationVersion=1.0.0") .Respond("application/json", StringSerialiser.Serialise(merchantResponse)); // Respond with JSON Result merchantDetails = await this.MerchantService.GetMerchantDetails(CancellationToken.None); @@ -147,7 +147,7 @@ public async Task MerchantService_GetMerchantDetails_MerchantDetailsReturned(){ [Fact] public async Task MerchantService_GetMerchantDetails_ResultFailed_FailedResultIsReturned() { - this.MockHttpMessageHandler.When($"http://localhost/api/merchants?application_version=1.0.0") + this.MockHttpMessageHandler.When($"http://localhost/api/merchants?applicationVersion=1.0.0") .Respond(HttpStatusCode.BadRequest); Result merchantDetails = await this.MerchantService.GetMerchantDetails(CancellationToken.None); @@ -156,7 +156,7 @@ public async Task MerchantService_GetMerchantDetails_ResultFailed_FailedResultIs [Fact] public async Task MerchantService_GetMerchantDetails_ExceptionThrown_FailedResultReturned(){ - this.MockHttpMessageHandler.When($"http://localhost/api/merchants?application_version=1.0.0") + this.MockHttpMessageHandler.When($"http://localhost/api/merchants?applicationVersion=1.0.0") .Respond(HttpStatusCode.InternalServerError); Result merchantDetails = await this.MerchantService.GetMerchantDetails(CancellationToken.None); @@ -196,4 +196,4 @@ public void GetProductSubType_CorrectProductSubTypeReturned(String operatorName, Models.ProductSubType result = Services.MerchantService.GetProductSubType(operatorName); result.ShouldBe(expectedProductSubType); } -} \ No newline at end of file +} diff --git a/TransactionProcessor.Mobile.BusinessLogic/Models/DailyPerformanceSummaryModel.cs b/TransactionProcessor.Mobile.BusinessLogic/Models/DailyPerformanceSummaryModel.cs index c1cd666f..41f5b71f 100644 --- a/TransactionProcessor.Mobile.BusinessLogic/Models/DailyPerformanceSummaryModel.cs +++ b/TransactionProcessor.Mobile.BusinessLogic/Models/DailyPerformanceSummaryModel.cs @@ -1,3 +1,5 @@ +using TransactionProcessor.Mobile.BusinessLogic.Services; + namespace TransactionProcessor.Mobile.BusinessLogic.Models; public enum PerformanceSummaryPeriod @@ -8,14 +10,15 @@ public enum PerformanceSummaryPeriod MonthToDate = 3, } -public sealed record DailyPerformanceSummaryModel( - PerformanceSummaryPeriod Period, - DateTime FromDate, - DateTime ToDate, - string PeriodLabel, - IReadOnlyList Metrics, - IReadOnlyList DrillDownTransactions) +public sealed record DailyPerformanceSummaryModel() { + public PerformanceSummaryPeriod Period { get; set; } + public DateTime FromDate { get; set; } + public DateTime ToDate { get; set; } + public string PeriodLabel { get; set; } + public List Metrics { get; set; } + public List DrillDownTransactions { get; set; } + public static DailyPerformanceSummaryModel CreateMock(PerformanceSummaryPeriod period) { DateTime today = DateTime.Today; @@ -46,15 +49,22 @@ public static DailyPerformanceSummaryModel CreateMock(PerformanceSummaryPeriod p List drillDownTransactions = new() { - new DailyPerformanceTransactionModel("TXN-00048", "Mobile Topup", "Success", "250.00 KES", today.AddHours(-1)), - new DailyPerformanceTransactionModel("TXN-00047", "Bill Payment", "Success", "1,500.00 KES", today.AddHours(-2)), - new DailyPerformanceTransactionModel("TXN-00046", "Voucher Issue", "Failed", "0.00 KES", today.AddHours(-3)), + new DailyPerformanceTransactionModel("TXN-00048", "Mobile Topup", "Success", 250.00m, today.AddHours(-1)), + new DailyPerformanceTransactionModel("TXN-00047", "Bill Payment", "Success", 1500.00m, today.AddHours(-2)), + new DailyPerformanceTransactionModel("TXN-00046", "Voucher Issue", "Failed", 0.00m, today.AddHours(-3)), }; - return new DailyPerformanceSummaryModel(period, fromDate, toDate, period.ToString(), metrics, drillDownTransactions); + return new DailyPerformanceSummaryModel() { + DrillDownTransactions = drillDownTransactions, + FromDate = fromDate, + ToDate = toDate, + Metrics = metrics, + Period = period, + PeriodLabel = period.ToString() + }; } - private static DateTime StartOfWeek(DateTime date) + internal static DateTime StartOfWeek(DateTime date) { int daysSinceSunday = (int)date.DayOfWeek; return date.Date.AddDays(-daysSinceSunday); @@ -67,8 +77,10 @@ public enum DailyPerformanceMetricCategory Total = 1, Success = 2, Failure = 3, + Average = 4, + TopSalesCount = 5 } public sealed record DailyPerformanceMetricModel(string Title, string Value, string Description, DailyPerformanceMetricCategory Category = DailyPerformanceMetricCategory.Neutral); -public sealed record DailyPerformanceTransactionModel(string Reference, string Product, string Status, string Amount, DateTime TransactionDateTime); +public sealed record DailyPerformanceTransactionModel(string Reference, string Product, string Status, Decimal Amount, DateTime TransactionDateTime); diff --git a/TransactionProcessor.Mobile.BusinessLogic/Models/MerchantDetailsModel.cs b/TransactionProcessor.Mobile.BusinessLogic/Models/MerchantDetailsModel.cs index a1270f8e..6f28c387 100644 --- a/TransactionProcessor.Mobile.BusinessLogic/Models/MerchantDetailsModel.cs +++ b/TransactionProcessor.Mobile.BusinessLogic/Models/MerchantDetailsModel.cs @@ -15,4 +15,5 @@ public class MerchantDetailsModel public String SettlementSchedule { get; set; } public AddressModel Address { get; set; } public ContactModel Contact { get; set; } + public int MerchantReportingId { get; set; } } \ No newline at end of file diff --git a/TransactionProcessor.Mobile.BusinessLogic/RequestHandlers/ReportRequestHandler.cs b/TransactionProcessor.Mobile.BusinessLogic/RequestHandlers/ReportRequestHandler.cs index c11e4ddc..201261d7 100644 --- a/TransactionProcessor.Mobile.BusinessLogic/RequestHandlers/ReportRequestHandler.cs +++ b/TransactionProcessor.Mobile.BusinessLogic/RequestHandlers/ReportRequestHandler.cs @@ -2,14 +2,39 @@ using SimpleResults; using TransactionProcessor.Mobile.BusinessLogic.Models; using TransactionProcessor.Mobile.BusinessLogic.Requests; +using TransactionProcessor.Mobile.BusinessLogic.Services; namespace TransactionProcessor.Mobile.BusinessLogic.RequestHandlers; -public sealed class ReportRequestHandler : IRequestHandler> -{ - public Task> Handle(ReportQueries.GetDailyPerformanceSummaryQuery request, CancellationToken cancellationToken) - { - DailyPerformanceSummaryModel summary = DailyPerformanceSummaryModel.CreateMock(request.Period); - return Task.FromResult(Result.Success(summary)); +public sealed class ReportRequestHandler : IRequestHandler> { + private readonly IReportsService ReportsService; + private readonly IApplicationCache ApplicationCache; + + public ReportRequestHandler(IReportsService reportsService, IApplicationCache applicationCache) { + ReportsService = reportsService; + this.ApplicationCache = applicationCache; + } + + + public async Task> Handle(ReportQueries.GetDailyPerformanceSummaryQuery request, + CancellationToken cancellationToken) { + + DateTime current = DateTime.Now.Date; + + (DateTime startDate, DateTime endDate) dates = request.Period switch { + PerformanceSummaryPeriod.Today => (current, current), + PerformanceSummaryPeriod.Yesterday => (current.AddDays(-1), current.AddDays(-1)), + PerformanceSummaryPeriod.ThisWeek => (DailyPerformanceSummaryModel.StartOfWeek(current), current), + PerformanceSummaryPeriod.MonthToDate => (new DateTime(current.Year, current.Month, 1), current), + _ => throw new ArgumentOutOfRangeException(nameof(request.Period), request.Period, "Invalid performance summary period.") + }; + + MerchantDetailsModel merchant = this.ApplicationCache.GetMerchantDetails(); + if (merchant == null) + { + return Result.Failure("Merchant details are not available."); + } + + return await this.ReportsService.GetDailyPerformanceSummary(request.Period, merchant.MerchantReportingId, dates.startDate, dates.endDate, cancellationToken); } } diff --git a/TransactionProcessor.Mobile.BusinessLogic/RequestHandlers/TransactionRequestHandler.cs b/TransactionProcessor.Mobile.BusinessLogic/RequestHandlers/TransactionRequestHandler.cs index b19f3cb7..5968e397 100644 --- a/TransactionProcessor.Mobile.BusinessLogic/RequestHandlers/TransactionRequestHandler.cs +++ b/TransactionProcessor.Mobile.BusinessLogic/RequestHandlers/TransactionRequestHandler.cs @@ -113,7 +113,7 @@ public async Task> Handle(TransactionCommands. TransactionDateTime = request.TransactionDateTime, TransactionNumber = transaction.transactionNumber.ToString(), DeviceIdentifier = this.DeviceService.GetIdentifier(), - ApplicationVersion = "1.0.5"//this.ApplicationInfoService.VersionString + ApplicationVersion = this.ApplicationInfoService.VersionString }; Result result = await transactionService.PerformLogon(model, cancellationToken); diff --git a/TransactionProcessor.Mobile.BusinessLogic/Services/MerchantService.cs b/TransactionProcessor.Mobile.BusinessLogic/Services/MerchantService.cs index d4eb0d7b..66e302a8 100644 --- a/TransactionProcessor.Mobile.BusinessLogic/Services/MerchantService.cs +++ b/TransactionProcessor.Mobile.BusinessLogic/Services/MerchantService.cs @@ -65,7 +65,7 @@ public async Task>> GetContractProducts(Cancel TokenResponseModel accessToken = this.ApplicationCache.GetAccessToken(); - String requestUri = this.BuildRequestUrl($"/api/merchants/contracts?application_version={this.ApplicationInfoService.VersionString}"); + String requestUri = this.BuildRequestUrl($"/api/merchants/contracts?applicationVersion={this.ApplicationInfoService.VersionString}"); Logger.LogInformation("About to request merchant contracts"); Logger.LogDebug($"Merchant Contract Request details: Access Token {accessToken.AccessToken}"); @@ -122,7 +122,7 @@ public async Task> GetMerchantBalance(CancellationToken cancella public async Task> GetMerchantDetails(CancellationToken cancellationToken) { TokenResponseModel accessToken = this.ApplicationCache.GetAccessToken(); - String requestUri = this.BuildRequestUrl($"/api/merchants?application_version={this.ApplicationInfoService.VersionString}"); + String requestUri = this.BuildRequestUrl($"/api/merchants?applicationVersion={this.ApplicationInfoService.VersionString}"); Logger.LogInformation("About to request merchant details"); Logger.LogDebug($"Merchant Details Request details: Access Token {accessToken.AccessToken}"); @@ -141,6 +141,7 @@ public async Task> GetMerchantDetails(CancellationT MerchantDetailsModel model = new MerchantDetailsModel { EstateId = responseDataResult.Data.EstateId, MerchantId = responseDataResult.Data.MerchantId, + MerchantReportingId = responseDataResult.Data.MerchantReportingId, MerchantName = responseDataResult.Data.MerchantName, NextStatementDate = responseDataResult.Data.NextStatementDate, LastStatementDate = new DateTime(), @@ -202,4 +203,4 @@ public static ProductSubType GetProductSubType(String operatorName) } #endregion -} \ No newline at end of file +} diff --git a/TransactionProcessor.Mobile.BusinessLogic/Services/ReportsService.cs b/TransactionProcessor.Mobile.BusinessLogic/Services/ReportsService.cs new file mode 100644 index 00000000..be46913c --- /dev/null +++ b/TransactionProcessor.Mobile.BusinessLogic/Services/ReportsService.cs @@ -0,0 +1,121 @@ +using Shared.Results; +using SimpleResults; +using System; +using System.Collections.Generic; +using System.Text; +using TransactionProcessor.Mobile.BusinessLogic.Logging; +using TransactionProcessor.Mobile.BusinessLogic.Models; +using TransactionProcessor.Mobile.BusinessLogic.Serialisation; +using TransactionProcessor.Mobile.BusinessLogic.UIServices; +using TransactionProcessorACL.DataTransferObjects.Responses; + +namespace TransactionProcessor.Mobile.BusinessLogic.Services +{ + public interface IReportsService { + Task> GetDailyPerformanceSummary(PerformanceSummaryPeriod period, int merchantReportingId, DateTime startDate, DateTime endDate, CancellationToken cancellationToken); + } + public class ReportsService : ClientProxyBase.ClientProxyBase, IReportsService + { + #region Fields + + private readonly IApplicationCache ApplicationCache; + private readonly IApplicationInfoService ApplicationInfoService; + + private readonly Func BaseAddressResolver; + + #endregion + public ReportsService(Func baseAddressResolver, + HttpClient httpClient, + IApplicationCache applicationCache, Func serialise, + Func deserialise, + IApplicationInfoService applicationInfoService) : base(httpClient, serialise, deserialise) + { + this.BaseAddressResolver = baseAddressResolver; + this.ApplicationCache = applicationCache; + this.ApplicationInfoService = applicationInfoService; + } + + private String BuildRequestUrl(String route) + { + String baseAddress = this.BaseAddressResolver("TransactionProcessorACL"); + + String requestUri = $"{baseAddress}{route}"; + + return requestUri; + } + + public async Task> GetDailyPerformanceSummary(PerformanceSummaryPeriod period,int merchantReportingId, DateTime startDate, DateTime endDate, CancellationToken cancellationToken) + { + TokenResponseModel accessToken = this.ApplicationCache.GetAccessToken(); + + String requestUri = this.BuildRequestUrl($"/api/reporting/dailymerchantprformancesummary?applicationVersion={this.ApplicationInfoService.VersionString}"); + + MerchantDailyPerformanceSummaryRequest requestBody = new (){ MerchantReportingId = merchantReportingId, StartDate = startDate, EndDate = endDate }; + + Result? responseDataResult = await this.Post(requestUri, requestBody, accessToken.AccessToken, cancellationToken); + + if (responseDataResult.IsFailed) + { + Logger.LogInformation($"GetDailyPerformanceSummary failed {responseDataResult.Status}"); + return ResultHelpers.CreateFailure(responseDataResult); + } + + DailyPerformanceSummaryModel model = new() { DrillDownTransactions = responseDataResult.Data.DrillDownTransactions.Select(d => new DailyPerformanceTransactionModel(d.Reference, d.Product, d.Status, d.Amount, d.TransactionDateTime) + ).ToList(), + Metrics = responseDataResult.Data.Metrics.Select(m => new DailyPerformanceMetricModel( + m.Title, + m.Value.ToString("N2"), + m.Description, + (DailyPerformanceMetricCategory)m.Category + )).ToList(), + Period = period, + PeriodLabel = period.ToString(), + ToDate = endDate, + FromDate = startDate + }; + + return Result.Success(model); + + } + } + + public class MerchantDailyPerformanceSummaryRequest + { + public int MerchantReportingId { get; set; } + + public DateTime StartDate { get; set; } + + public DateTime EndDate { get; set; } + } + + public class MerchantDailyPerformanceSummaryResponse + { + public List Metrics { get; set; } = []; + + public List DrillDownTransactions { get; set; } = []; + } + + public class MetricItem + { + public string Title { get; set; } + + public decimal Value { get; set; } + + public string Description { get; set; } + + public int Category { get; set; } + } + + public class DrillDownTransaction + { + public string Reference { get; set; } + + public string Product { get; set; } + + public string Status { get; set; } + + public decimal Amount { get; set; } + + public DateTime TransactionDateTime { get; set; } + } +} diff --git a/TransactionProcessor.Mobile.BusinessLogic/ViewModels/Reports/DailyPerformanceSummaryPageViewModel.cs b/TransactionProcessor.Mobile.BusinessLogic/ViewModels/Reports/DailyPerformanceSummaryPageViewModel.cs index a4ab9fa0..5b4c89b7 100644 --- a/TransactionProcessor.Mobile.BusinessLogic/ViewModels/Reports/DailyPerformanceSummaryPageViewModel.cs +++ b/TransactionProcessor.Mobile.BusinessLogic/ViewModels/Reports/DailyPerformanceSummaryPageViewModel.cs @@ -72,7 +72,7 @@ public DailyPerformanceSummaryModel? Summary private set => SetProperty(ref this.summary, value); } - public IReadOnlyList SummaryCards => this.Summary?.Metrics ?? Array.Empty(); + public IReadOnlyList SummaryCards => this.Summary?.Metrics ?? new List(); public IReadOnlyList TopSummaryCards => this.SummaryCards.Take(4).ToList(); @@ -80,7 +80,7 @@ public DailyPerformanceSummaryModel? Summary public IReadOnlyList TopSummaryCardsRow2 => this.TopSummaryCards.Skip(2).Take(2).ToList(); - public IReadOnlyList DrillDownTransactions => this.Summary?.DrillDownTransactions ?? Array.Empty(); + public IReadOnlyList DrillDownTransactions => this.Summary?.DrillDownTransactions ?? new List(); public bool HasSummaryData => this.SummaryCards.Count > 0; @@ -122,7 +122,7 @@ private async Task LoadSummaryAsync(PerformanceSummaryPeriod period, Cancellatio this.Summary = result.Data; } - catch + catch(Exception ex) { this.Summary = null; this.ErrorMessage = "Unable to load performance summary."; diff --git a/TransactionProcessor.Mobile/Extensions/MauiAppBuilderExtensions.cs b/TransactionProcessor.Mobile/Extensions/MauiAppBuilderExtensions.cs index 44591f0f..247100a1 100644 --- a/TransactionProcessor.Mobile/Extensions/MauiAppBuilderExtensions.cs +++ b/TransactionProcessor.Mobile/Extensions/MauiAppBuilderExtensions.cs @@ -81,6 +81,7 @@ public static MauiAppBuilder ConfigureAppServices(this MauiAppBuilder builder) { builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.RegisterConfigurationService().RegisterAuthenticationService().RegisterTransactionService().RegisterMerchantService(); diff --git a/TransactionProcessor.Mobile/UIServices/ApplicationInfoService.cs b/TransactionProcessor.Mobile/UIServices/ApplicationInfoService.cs index 3ab7931a..f76f8cc3 100644 --- a/TransactionProcessor.Mobile/UIServices/ApplicationInfoService.cs +++ b/TransactionProcessor.Mobile/UIServices/ApplicationInfoService.cs @@ -15,5 +15,5 @@ public class ApplicationInfoService : IApplicationInfoService public Version Version => AppInfo.Version; - public String VersionString => AppInfo.VersionString; + public String VersionString => "1.0.5"; //AppInfo.VersionString; } diff --git a/TransactionProcessor.Mobile/UIServices/DeviceService.cs b/TransactionProcessor.Mobile/UIServices/DeviceService.cs index 092752fc..701e5ff8 100644 --- a/TransactionProcessor.Mobile/UIServices/DeviceService.cs +++ b/TransactionProcessor.Mobile/UIServices/DeviceService.cs @@ -20,8 +20,8 @@ public String GetIdentifier(){ String Id = deviceInformation.Id.ToString(); return Id.Replace("-","");; #endif - - return this.DeviceIdProvider.GetDeviceId().Replace("-",""); + return "stagingmerchant1device"; + //return this.DeviceIdProvider.GetDeviceId().Replace("-",""); } public String GetModel()