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 @@ -13,5 +13,5 @@ public record TransactionDetailReportQuery(Guid EstateId, TransactionDetailRepor
public record TransactionSummaryByMerchantQuery(Guid EstateId, TransactionSummaryByMerchantRequest Request) : IRequest<Result<TransactionSummaryByMerchantResponse>>;
public record TransactionSummaryByOperatorQuery(Guid EstateId, TransactionSummaryByOperatorRequest Request) : IRequest<Result<TransactionSummaryByOperatorResponse>>;
public record ProductPerformanceQuery(Guid EstateId, DateTime StartDate, DateTime EndDate) : IRequest<Result<ProductPerformanceResponse>>;

public record TodaysSalesByHour(Guid estateId, DateTime comparisonDate) : IRequest<Result<List<Models.TodaysSalesByHour>>>;
}
64 changes: 56 additions & 8 deletions EstateReportingAPI.BusinessLogic/ReportingManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ Task<Result<TransactionSummaryByOperatorResponse>> GetTransactionSummaryByOperat
Task<Result<ProductPerformanceResponse>> GetProductPerformanceReport(TransactionQueries.ProductPerformanceQuery request,
CancellationToken cancellationToken);

Task<Result<List<TodaysSalesByHour>>> GetTodaysSalesByHour(TransactionQueries.TodaysSalesByHour request,
CancellationToken cancellationToken);
#endregion
}

Expand Down Expand Up @@ -1449,22 +1451,68 @@ into g
return Result.Success(response);
}

public async Task<Result<List<TodaysSalesByHour>>> GetTodaysSalesByHour(TransactionQueries.TodaysSalesByHour request,
CancellationToken cancellationToken) {
using ResolvedDbContext<EstateManagementContext>? resolvedContext = this.Resolver.Resolve(EstateManagementDatabaseName, request.estateId.ToString());
await using EstateManagementContext context = resolvedContext.Context;

IQueryable<TodayTransaction> todaysSales = this.BuildTodaySalesQuery(context);
IQueryable<TransactionHistory> comparisonSales = this.BuildComparisonSalesQuery(context, request.comparisonDate);

// First we need to get a value of todays sales
var todaysSalesByHour = await (from t in todaysSales
group t.TransactionAmount by t.Hour into g select new { Hour = g.Key, TotalSalesCount = g.Count(), TotalSalesValue = g.Sum() }).ToListAsync(cancellationToken);

var comparisonSalesByHour = await (from t in comparisonSales group t.TransactionAmount by t.Hour into g select new { Hour = g.Key, TotalSalesCount = g.Count(), TotalSalesValue = g.Sum() }).ToListAsync(cancellationToken);

var response =
(
from today in todaysSalesByHour
join comparison in comparisonSalesByHour
on today.Hour equals comparison.Hour into compGroup
from comparison in compGroup.DefaultIfEmpty()
select new TodaysSalesByHour
{
Hour = today.Hour.Value,
TodaysSalesCount = today.TotalSalesCount,
TodaysSalesValue = today.TotalSalesValue,
ComparisonSalesCount = comparison?.TotalSalesCount ?? 0,
ComparisonSalesValue = comparison?.TotalSalesValue ?? 0
}
)
.Union
(
from comparison in comparisonSalesByHour
join today in todaysSalesByHour
on comparison.Hour equals today.Hour into todayGroup
from today in todayGroup.DefaultIfEmpty()
where today == null
select new TodaysSalesByHour
{
Hour = comparison.Hour.Value,
TodaysSalesCount = 0,
TodaysSalesValue = 0,
ComparisonSalesCount = comparison.TotalSalesCount,
ComparisonSalesValue = comparison.TotalSalesValue
}
)
.ToList();


return Result.Success(response);
}

private IQueryable<TodayTransaction> BuildTodaySalesQuery(EstateManagementContext context) {
return from t in context.TodayTransactions where t.IsAuthorised && t.TransactionType == "Sale" && t.TransactionDate == DateTime.Now.Date && t.TransactionTime <= DateTime.Now.TimeOfDay select t;
return from t in context.TodayTransactions where t.IsAuthorised && t.TransactionType == "Sale"
&& t.TransactionDate == DateTime.Now.Date && t.TransactionTime <= DateTime.Now.TimeOfDay
select t;
}

private IQueryable<TransactionHistory> BuildComparisonSalesQuery(EstateManagementContext context,
DateTime comparisonDate) {
return from t in context.TransactionHistory where t.IsAuthorised && t.TransactionType == "Sale" && t.TransactionDate == comparisonDate && t.TransactionTime <= DateTime.Now.TimeOfDay select t;
}








#endregion
}

Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ public class TransactionRequestHandler : IRequestHandler<TransactionQueries.Toda
IRequestHandler<TransactionQueries.TransactionDetailReportQuery, Result<TransactionDetailReportResponse>>,
IRequestHandler<TransactionQueries.TransactionSummaryByMerchantQuery, Result<TransactionSummaryByMerchantResponse>>,
IRequestHandler<TransactionQueries.TransactionSummaryByOperatorQuery, Result<TransactionSummaryByOperatorResponse>>,
IRequestHandler<TransactionQueries.ProductPerformanceQuery, Result<ProductPerformanceResponse>>
IRequestHandler<TransactionQueries.ProductPerformanceQuery, Result<ProductPerformanceResponse>>,
IRequestHandler<TransactionQueries.TodaysSalesByHour, Result<List<TodaysSalesByHour>>>

{
private readonly IReportingManager Manager;

Expand Down Expand Up @@ -49,4 +51,9 @@ public async Task<Result<ProductPerformanceResponse>> Handle(TransactionQueries.
CancellationToken cancellationToken) {
return await this.Manager.GetProductPerformanceReport(request, cancellationToken);
}

public async Task<Result<List<TodaysSalesByHour>>> Handle(TransactionQueries.TodaysSalesByHour request,
CancellationToken cancellationToken) {
return await this.Manager.GetTodaysSalesByHour(request, cancellationToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@
using Newtonsoft.Json;
using System;

public class TodaysSalesValueByHour{
public class TodaysSalesByHour{
[JsonProperty("hour")]
public Int32 Hour{ get; set; }
[JsonProperty("todays_sales_value")]
public Decimal TodaysSalesValue { get; set; }
[JsonProperty("comparison_sales_value")]
public Decimal ComparisonSalesValue { get; set; }
[JsonProperty("todays_sales_count")]
public Int32 TodaysSalesCount { get; set; }
[JsonProperty("comparison_sales_count")]
public Int32 ComparisonSalesCount { get; set; }
}
}
14 changes: 0 additions & 14 deletions EstateReportingAPI.DataTrasferObjects/TodaysSalesCountByHour.cs

This file was deleted.

129 changes: 129 additions & 0 deletions EstateReportingAPI.IntegrationTests/TransactionsEndpointTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -939,6 +939,135 @@ public async Task TransactionsEndpoint_ProductPerformanceReport_SummaryDataRetur
productPerformanceResponseDetail.TransactionCount.ShouldBe(transactions.Count(t => t.ContractProductId == product.productId));
productPerformanceResponseDetail.TransactionValue.ShouldBe(transactions.Where(t => t.ContractProductId == product.productId).Sum(t => t.TransactionAmount));
}
}

private static int RandomizeCount(int baseCount, Random rnd, double variability = 0.3)
{
if (baseCount <= 0) return 0;
// factor in [-variability, +variability]
double factor = 1.0 + (rnd.NextDouble() * 2.0 - 1.0) * variability;
int result = (int)Math.Round(baseCount * factor);
return Math.Max(0, result);
}

[Fact]
public async Task TransactionsEndpoint_TodaysSalesByHour_SummaryDataReturned()
{
Stopwatch sw = Stopwatch.StartNew();

List<Transaction> todaysTransactions = new List<Transaction>();
List<Transaction> comparisonDateTransactions = new List<Transaction>();

Dictionary<string, int> transactionCounts = new() { { "Test Merchant 1", 3 }, { "Test Merchant 2", 6 }, { "Test Merchant 3", 2 }, { "Test Merchant 4", 0 } };

// TODO: make counts dynamic
DateTime todaysDateTime = DateTime.Now;

for (int hour = 0; hour < 24; hour++)
{
List<Transaction> localList = new List<Transaction>();
DateTime date = new DateTime(todaysDateTime.Year, todaysDateTime.Month, todaysDateTime.Day, hour, 0, 0);

// Seed per-hour RNG deterministically so test results are reproducible per-day/per-hour
var hourSeed = todaysDateTime.Date.GetHashCode() ^ hour;
var hourRnd = new Random(hourSeed);

foreach (var merchant in merchantsList)
{
foreach (var contract in contractList)
{
var productList = contractProducts.Single(cp => cp.Key == contract.contractId).Value;
foreach ((Guid productId, String productName, Decimal? productValue, Int32 contractProductReportingId) product in productList)
{
var baseCount = transactionCounts.Single(m => m.Key == merchant.Name).Value;

// keep the original hour-based multipliers, but apply random variation to the final count
int hourMultiplierCount = hour switch
{
_ when hour >= 9 && hour < 18 => baseCount * hour, // business hours
_ when hour >= 18 && hour < 21 => baseCount * (24 - hour), // evening spike
_ => baseCount // off hours
};

int transactionCount = RandomizeCount(hourMultiplierCount, hourRnd, variability: 0.3);

for (int i = 0; i < transactionCount; i++)
{
Transaction transaction = await helper.BuildTransactionX(date, merchant.MerchantId, contract.operatorId, contract.contractId, product.productId, "0000", product.productValue);
todaysTransactions.Add(transaction);
}
}
}
}

todaysTransactions.AddRange(localList);
}

await this.helper.AddTransactionsX(todaysTransactions);

sw.Stop();
this.TestOutputHelper.WriteLine($"Setup Todays Txns {sw.ElapsedMilliseconds}ms");
sw.Restart();

DateTime comparisonDate = todaysDateTime.AddDays(-1);
for (int hour = 0; hour < 24; hour++)
{
List<Transaction> localList = new List<Transaction>();
DateTime date = new DateTime(comparisonDate.Year, comparisonDate.Month, comparisonDate.Day, hour, 0, 0);

// Separate deterministic seed for comparison date hour
var compHourSeed = comparisonDate.Date.GetHashCode() ^ hour;
var compHourRnd = new Random(compHourSeed);

foreach (var merchant in merchantsList)
{
foreach (var contract in contractList)
{
var productList = contractProducts.Single(cp => cp.Key == contract.contractId).Value;
foreach (var product in productList)
{
var baseCount = transactionCounts.Single(m => m.Key == merchant.Name).Value;

int hourMultiplierCount = hour switch
{
_ when hour >= 12 && hour < 18 => baseCount * hour, // business hours
_ when hour >= 18 && hour < 21 => baseCount * (24 - hour), // evening spike
_ => baseCount // off hours
};

int transactionCount = RandomizeCount(hourMultiplierCount, compHourRnd, variability: 0.3);

for (int i = 0; i < transactionCount; i++)
{
Transaction transaction = await helper.BuildTransactionX(date, merchant.MerchantId, contract.operatorId, contract.contractId, product.productId, "0000", product.productValue);
comparisonDateTransactions.Add(transaction);
}
}
}
}

comparisonDateTransactions.AddRange(localList);

}

await this.helper.AddTransactionsX(comparisonDateTransactions);

await this.helper.RunTodaysTransactionsSummaryProcessing(comparisonDate.Date);
await this.helper.RunHistoricTransactionsSummaryProcessing(comparisonDate.Date);
await this.helper.RunTodaysTransactionsSummaryProcessing(todaysDateTime.Date);

var result = await this.CreateAndSendHttpRequestMessage<List<DataTransferObjects.TodaysSalesByHour>>($"{this.BaseRoute}/todayssalesbyhour?comparisondate={comparisonDate:yyyy-MM-dd}", CancellationToken.None);
result.IsSuccess.ShouldBeTrue();
var todaysSalesByHour = result.Data;
todaysSalesByHour.ShouldNotBeNull();

foreach (var hour in todaysSalesByHour) {
hour.ShouldNotBeNull();
hour.TodaysSalesCount.ShouldBe(todaysTransactions.Count(t => t.TransactionDateTime.Hour == hour.Hour && t.TransactionTime <= DateTime.Now.TimeOfDay), hour.Hour.ToString());
hour.TodaysSalesValue.ShouldBe(todaysTransactions.Where(t => t.TransactionDateTime.Hour == hour.Hour && t.TransactionTime <= DateTime.Now.TimeOfDay).Sum(t => t.TransactionAmount), hour.Hour.ToString());
hour.ComparisonSalesCount.ShouldBe(comparisonDateTransactions.Count(t => t.TransactionDateTime.Hour == hour.Hour && t.TransactionTime <= DateTime.Now.TimeOfDay), hour.Hour.ToString());
hour.ComparisonSalesValue.ShouldBe(comparisonDateTransactions.Where(t => t.TransactionDateTime.Hour == hour.Hour && t.TransactionTime <= DateTime.Now.TimeOfDay).Sum(t => t.TransactionAmount), hour.Hour.ToString());

}
}
}
10 changes: 10 additions & 0 deletions EstateReportingAPI.Models/TodaysSalesByHour.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace EstateReportingAPI.Models;

public class TodaysSalesByHour
{
public Int32 Hour { get; set; }
public Decimal TodaysSalesValue { get; set; }
public Decimal ComparisonSalesValue { get; set; }
public Int32 TodaysSalesCount { get; set; }
public Int32 ComparisonSalesCount { get; set; }
}
2 changes: 2 additions & 0 deletions EstateReportingAPI/Bootstrapper/MediatorRegistry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public MediatorRegistry() {
this.AddSingleton<IRequestHandler<TransactionQueries.TransactionSummaryByMerchantQuery, Result<TransactionSummaryByMerchantResponse>>, TransactionRequestHandler>();
this.AddSingleton<IRequestHandler<TransactionQueries.TransactionSummaryByOperatorQuery, Result<TransactionSummaryByOperatorResponse>>, TransactionRequestHandler>();
this.AddSingleton<IRequestHandler<TransactionQueries.ProductPerformanceQuery, Result<ProductPerformanceResponse>>, TransactionRequestHandler>();
this.AddSingleton<IRequestHandler<TransactionQueries.TodaysSalesByHour, Result<List<TodaysSalesByHour>>>, TransactionRequestHandler>();

this.AddSingleton<IRequestHandler<CalendarQueries.GetYearsQuery, Result<List<Int32>>>, CalendarRequestHandler>();
this.AddSingleton<IRequestHandler<CalendarQueries.GetAllDatesQuery, Result<List<Calendar>>>, CalendarRequestHandler>();
Expand All @@ -44,3 +45,4 @@ public MediatorRegistry() {
}
}


2 changes: 2 additions & 0 deletions EstateReportingAPI/Endpoints/TransactionEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ public static void MapTransactionEndpoints(this IEndpointRouteBuilder app)
.WithStandardProduces<TransactionSummaryByOperatorResponse>();
group.MapGet("productperformancereport", TransactionHandler.ProductPerformanceReport)
.WithStandardProduces<ProductPerformanceResponse>();
group.MapGet("todayssalesbyhour", TransactionHandler.TodaysSalesByHour)
.WithStandardProduces<List<TodaysSalesByHour>>();

}
}
23 changes: 23 additions & 0 deletions EstateReportingAPI/Handlers/TransactionHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -181,4 +181,27 @@ TransactionSummaryByOperatorResponse SuccessFactory(Models.TransactionSummaryByO
};
return ResponseFactory.FromResult(result, SuccessFactory);
}

public static async Task<IResult> TodaysSalesByHour([FromHeader] Guid estateId,
[FromQuery] DateTime comparisonDate,
IMediator mediator,
CancellationToken cancellationToken)
{
var query = new TransactionQueries.TodaysSalesByHour(estateId, comparisonDate);
var result = await mediator.Send(query, cancellationToken);

List<DataTransferObjects.TodaysSalesByHour> SuccessFactory(List<Models.TodaysSalesByHour> r) =>
r.Select(item => new DataTransferObjects.TodaysSalesByHour
{
Hour = item.Hour,
ComparisonSalesCount = item.ComparisonSalesCount,
TodaysSalesCount = item.TodaysSalesCount,
ComparisonSalesValue = item.ComparisonSalesValue,
TodaysSalesValue = item.TodaysSalesValue
}).ToList();


return ResponseFactory.FromResult(result, SuccessFactory);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@
using SortDirection = EstateReportingAPI.DataTransferObjects.SortDirection;
using SortField = EstateReportingAPI.DataTransferObjects.SortField;
using TodaysSales = EstateReportingAPI.DataTransferObjects.TodaysSales;
using TodaysSalesCountByHour = EstateReportingAPI.DataTransferObjects.TodaysSalesCountByHour;
using TodaysSalesValueByHour = EstateReportingAPI.DataTransferObjects.TodaysSalesValueByHour;
using TodaysSettlement = EstateReportingAPI.DataTransferObjects.TodaysSettlement;
using TopBottom = EstateReportingAPI.DataTransferObjects.TopBottom;
using TransactionResult = EstateReportingAPI.DataTransferObjects.TransactionResult;
Expand Down
Loading