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
35 changes: 23 additions & 12 deletions EstateReportingAPI.BusinessLogic/ReportingManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using SimpleResults;
using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore.DynamicLinq;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion.Internal;
using TransactionProcessor.Database.Contexts;
using TransactionProcessor.Database.Entities;
using TransactionProcessor.Database.Entities.Summary;
Expand Down Expand Up @@ -1996,6 +1997,7 @@ private sealed class MerchantTransactionFinalProjection {
private sealed class MerchantDailyPerformanceGroupProjection {
public int ContractProductReportingId { get; init; }
public string? ProductName { get; init; }
public string OperatorName { get; init; }
public bool IsAuthorised { get; init; }
public int SalesCount { get; init; }
public decimal SalesValue { get; init; }
Expand All @@ -2004,6 +2006,7 @@ private sealed class MerchantDailyPerformanceGroupProjection {
private sealed class MerchantDailyPerformanceRecentSaleProjection {
public string Reference { get; init; }
public string? Product { get; init; }
public String Operator { get; init; }
public string Status { get; init; }
public decimal Amount { get; init; }
public DateTime TransactionDateTime { get; init; }
Expand Down Expand Up @@ -2040,14 +2043,14 @@ private static List<MetricItem> BuildMerchantDailyPerformanceMetrics(List<Mercha
decimal averageSalesValue = totalSalesCount == 0 ? 0m : totalSalesValue / totalSalesCount;

List<MetricItem> metrics = new() {
new() { Title = "Total Sales Count", Value = totalSalesCount, Description = "All sales transactions in the range", Category = 0 },
new() { Title = "Total Sales Value", Value = totalSalesValue, Description = "All sales value in the range", Category = 1 },
new() { Title = "Successful Sales Count", Value = successfulSalesCount, Description = "Authorised sales count in the range", Category = 2 },
new() { Title = "Successful Sales Value", Value = successfulSalesValue, Description = "Authorised sales value in the range", Category = 3 },
new() { Title = "Failed Sales Count", Value = failedSalesCount, Description = "Declined sales count in the range", Category = 4 },
new() { Title = "Failed Sales Value", Value = failedSalesValue, Description = "Declined sales value in the range", Category = 5 },
new() { Title = "Average Sales Count", Value = averageSalesCount, Description = "Average sales count per day in the range", Category = 6 },
new() { Title = "Average Sales Value", Value = averageSalesValue, Description = "Average value per sale in the range", Category = 7 }
new() { Title = "Total Sales Count", Value = totalSalesCount, Description = "All sales transactions in the range", Category = 1, Type = 0},

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This renumbers the base metric categories, but the empty-summary path still uses the old values. The response will now emit two different category schemes depending on whether data exists.

new() { Title = "Total Sales Value", Value = totalSalesValue, Description = "All sales value in the range", Category = 1, Type = 1 },
new() { Title = "Successful Sales Count", Value = successfulSalesCount, Description = "Authorised sales count in the range", Category = 2, Type = 2 },
new() { Title = "Successful Sales Value", Value = successfulSalesValue, Description = "Authorised sales value in the range", Category = 2, Type = 3 },
new() { Title = "Failed Sales Count", Value = failedSalesCount, Description = "Declined sales count in the range", Category = 3, Type = 4 },
new() { Title = "Failed Sales Value", Value = failedSalesValue, Description = "Declined sales value in the range", Category = 3, Type = 5 },
new() { Title = "Average Sales Count", Value = averageSalesCount, Description = "Average sales count per day in the range", Category = 4, Type = 6 },
new() { Title = "Average Sales Value", Value = averageSalesValue, Description = "Average value per sale in the range", Category = 4, Type = 7 }
};

MetricItem? topProductMetric = BuildTopProductMetric(groupedTransactions);
Expand All @@ -2059,10 +2062,11 @@ private static List<MetricItem> BuildMerchantDailyPerformanceMetrics(List<Mercha

private static MetricItem? BuildTopProductMetric(List<MerchantDailyPerformanceGroupProjection> groupedTransactions) {
var topProduct = groupedTransactions
.GroupBy(x => new { x.ContractProductReportingId, x.ProductName })
.GroupBy(x => new { x.ContractProductReportingId, x.OperatorName, x.ProductName })
.Select(g => new {
g.Key.ContractProductReportingId,
g.Key.ProductName,
g.Key.ProductName,
g.Key.OperatorName,
SalesCount = g.Sum(x => x.SalesCount),
SalesValue = g.Sum(x => x.SalesValue)
})
Expand All @@ -2076,8 +2080,9 @@ private static List<MetricItem> BuildMerchantDailyPerformanceMetrics(List<Mercha
return new MetricItem {
Title = "Top Product Sales Count",
Value = topProduct.SalesCount,
Description = topProduct.ProductName ?? "Unknown product",
Category = 8
Description = $"{topProduct.OperatorName} {topProduct.ProductName}" ?? "Unknown product",

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fallback never triggers because the interpolated string is not null. If either name can be missing, use explicit null/empty checks on the operands instead.

Category = 5,
Type = 8
};
}

Expand All @@ -2090,13 +2095,15 @@ private async Task<Result<List<MerchantDailyPerformanceGroupProjection>>> LoadMe
from t in context.Transactions
join m in context.Merchants on t.MerchantId equals m.MerchantId
join cp in context.ContractProducts on new { t.ContractProductId, t.ContractId } equals new { cp.ContractProductId, cp.ContractId }
join op in context.Operators on t.OperatorId equals op.OperatorId

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is now an inner join on operators. Any transaction without a matching operator row will disappear from the summary, which can undercount totals compared with the rest of the reporting code.

where t.TransactionType == "Sale"
&& t.TransactionDate >= startDate
&& t.TransactionDate <= endDate
&& m.MerchantReportingId == request.Request.MerchantReportingId
group t by new
{
cp.ContractProductReportingId,
op.Name,
cp.ProductName,
t.IsAuthorised
}
Expand All @@ -2105,6 +2112,7 @@ into g
{
ContractProductReportingId = g.Key.ContractProductReportingId,
ProductName = g.Key.ProductName,
OperatorName = g.Key.Name,
IsAuthorised = g.Key.IsAuthorised,
SalesCount = g.Count(),
SalesValue = g.Sum(x => x.TransactionAmount)
Expand All @@ -2122,6 +2130,7 @@ private async Task<Result<List<MerchantDailyPerformanceRecentSaleProjection>>> L
(from t in context.Transactions
join m in context.Merchants on t.MerchantId equals m.MerchantId
join cp in context.ContractProducts on new { t.ContractProductId, t.ContractId } equals new { cp.ContractProductId, cp.ContractId }
join op in context.Operators on t.OperatorId equals op.OperatorId
where t.TransactionType == "Sale"
&& t.TransactionDate >= startDate
&& t.TransactionDate <= endDate
Expand All @@ -2131,6 +2140,7 @@ orderby t.TransactionDateTime descending
{
Reference = t.TransactionNumber,
Product = cp.ProductName,
Operator = op.Name,
Status = t.IsAuthorised ? "Successful" : "Failed",
Amount = t.TransactionAmount,
TransactionDateTime = t.TransactionDateTime
Expand All @@ -2143,6 +2153,7 @@ private static List<DrillDownTransaction> MapMerchantDailyPerformanceRecentSales
return recentSales.Select(x => new DrillDownTransaction {
Reference = x.Reference,
Product = x.Product,
Operator = x.Operator,
Status = x.Status,
Amount = x.Amount,
TransactionDateTime = x.TransactionDateTime
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1107,7 +1107,7 @@ public async Task TransactionsEndpoint_MerchantDailyPerformanceSummary_ReturnsMe
TransactionProcessor.Database.Entities.Merchant merchant = this.merchantsList.Single(m => m.Name == "Test Merchant 1");
var contract = this.contractList.Single(c => c.contractName == "Safaricom Contract");
var product = this.contractProducts.Single(cp => cp.Key == contract.contractId).Value.First();

var operatorRecord = this.operatorsList.Single(o => o.OperatorId == contract.operatorId);
DateTime transactionDate = DateTime.Now.Date;
List<Transaction> transactions = new();

Expand Down Expand Up @@ -1155,7 +1155,7 @@ public async Task TransactionsEndpoint_MerchantDailyPerformanceSummary_ReturnsMe
response.Metrics.Single(m => m.Title == "Successful Sales Count").Value.ShouldBe(3);
response.Metrics.Single(m => m.Title == "Failed Sales Count").Value.ShouldBe(3);
response.Metrics.Single(m => m.Title == "Top Product Sales Count").Value.ShouldBe(6);
response.Metrics.Single(m => m.Title == "Top Product Sales Count").Description.ShouldBe(product.productName);
response.Metrics.Single(m => m.Title == "Top Product Sales Count").Description.ShouldBe($"{operatorRecord.Name} {product.productName}");

response.DrillDownTransactions.Count.ShouldBe(5);
response.DrillDownTransactions.Select(x => x.Reference).ShouldBe(new[] { "0006", "0005", "0004", "0003", "0002" });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,16 @@ public class MetricItem {
public string Description { get; set; }

public Int32 Category { get; set; }
public Int32 Type { get; set; }

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type is now part of the model, but it also needs to be projected through the handler and response contract. Otherwise clients still only see Category, and the new identifier is effectively dropped.

}

public class DrillDownTransaction {
public string Reference { get; set; }

public string Product { get; set; }

public string Operator { get; set; }

public string Status { get; set; }

public Decimal Amount { get; set; }
Expand Down
2 changes: 0 additions & 2 deletions EstateReportingAPI/Endpoints/TransactionEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,6 @@ public static void MapTransactionEndpoints(this IEndpointRouteBuilder app)
.WithStandardProduces<List<TodaysSalesByHour>>();
group.MapPost("merchantdailyperformancesummary", TransactionHandler.MerchantDailyPerformanceSummary)
.WithStandardProduces<MerchantDailyPerformanceSummaryResponse>();
group.MapPost("dailymerchaantprformancesummary", TransactionHandler.MerchantDailyPerformanceSummary)
.WithStandardProduces<MerchantDailyPerformanceSummaryResponse>();

}
}
2 changes: 1 addition & 1 deletion EstateReportingAPI/Handlers/TransactionHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,7 @@ MerchantDailyPerformanceSummaryResponse SuccessFactory(Models.MerchantDailyPerfo
DrillDownTransactions = r.DrillDownTransactions?.Select(transaction => new DrillDownTransaction
{
Reference = transaction.Reference,
Product = transaction.Product,
Product = $"{transaction.Operator} {transaction.Product}",
Status = transaction.Status,
Amount = transaction.Amount,
TransactionDateTime = transaction.TransactionDateTime
Expand Down
Loading