From 374023de2cea38ac5733cfce35fd7120a0ba0753 Mon Sep 17 00:00:00 2001 From: Stuart Ferguson Date: Fri, 17 Oct 2025 17:00:29 +0100 Subject: [PATCH 1/7] more refactoring --- .../DatabaseProjections.cs | 50 + .../ReportingManager.cs | 1029 +++++++++-------- .../ReportingManagerExtensions.cs | 60 +- .../ReportingManagerRefactored.cs | 246 ---- 4 files changed, 631 insertions(+), 754 deletions(-) create mode 100644 EstateReportingAPI.BusinessLogic/DatabaseProjections.cs delete mode 100644 EstateReportingAPI.BusinessLogic/ReportingManagerRefactored.cs diff --git a/EstateReportingAPI.BusinessLogic/DatabaseProjections.cs b/EstateReportingAPI.BusinessLogic/DatabaseProjections.cs new file mode 100644 index 0000000..73fb93a --- /dev/null +++ b/EstateReportingAPI.BusinessLogic/DatabaseProjections.cs @@ -0,0 +1,50 @@ +using EstateReportingAPI.Models; +using Microsoft.EntityFrameworkCore; +using Shared.EntityFramework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TransactionProcessor.Database.Contexts; +using TransactionProcessor.Database.Entities; +using TransactionProcessor.Database.Entities.Summary; +using Merchant = TransactionProcessor.Database.Entities.Merchant; +using Operator = TransactionProcessor.Database.Entities.Operator; + +namespace EstateReportingAPI.BusinessLogic +{ + + public class DatabaseProjections { + public class FeeTransactionProjection + { + public MerchantSettlementFee Fee { get; set; } + public Transaction Txn { get; set; } + } + + public class TodaySettlementTransactionProjection + { + public MerchantSettlementFee Fee { get; set; } + public TodayTransaction Txn { get; set; } + } + + public class ComparisonSettlementTransactionProjection + { + public MerchantSettlementFee Fee { get; set; } + public TransactionHistory Txn { get; set; } + } + + public class TransactionSearchProjection { + public Transaction Transaction { get; set; } + public Operator Operator { get; set; } + public Merchant Merchant { get; set; } + public ContractProduct Product { get; set; } + } + + public class TopBottomData + { + public String DimensionName { get; set; } + public Decimal SalesValue { get; set; } + } + } +} diff --git a/EstateReportingAPI.BusinessLogic/ReportingManager.cs b/EstateReportingAPI.BusinessLogic/ReportingManager.cs index 8884e14..0c5a481 100644 --- a/EstateReportingAPI.BusinessLogic/ReportingManager.cs +++ b/EstateReportingAPI.BusinessLogic/ReportingManager.cs @@ -2,562 +2,577 @@ using TransactionProcessor.Database.Entities; using TransactionProcessor.Database.Entities.Summary; -namespace EstateReportingAPI.BusinessLogic{ - using Microsoft.EntityFrameworkCore; - using Models; - using Shared.EntityFramework; - using System; - using System.Linq; - using System.Threading; - using Calendar = Models.Calendar; - using Merchant = Models.Merchant; - using Operator = Models.Operator; - - - public partial class ReportingManager : IReportingManager{ - private readonly IDbContextResolver Resolver; - - - private Guid Id; - private static readonly String EstateManagementDatabaseName = "TransactionProcessorReadModel"; - #region Constructors - - public ReportingManager(IDbContextResolver resolver) { - this.Resolver = resolver; - } +namespace EstateReportingAPI.BusinessLogic; - #endregion - - #region Methods - - public async Task> GetCalendarComparisonDates(Guid estateId, CancellationToken cancellationToken){ - using ResolvedDbContext? resolvedContext = this.Resolver.Resolve(EstateManagementDatabaseName, estateId.ToString()); - await using EstateManagementContext context = resolvedContext.Context; - - DateTime startOfYear = new DateTime(DateTime.Now.Year, 1, 1); - - List entities = context.Calendar.Where(c => c.Date >= startOfYear && c.Date < DateTime.Now.Date.AddDays(-1)).OrderByDescending(d => d.Date).ToList(); - - List result = new List(); - foreach (TransactionProcessor.Database.Entities.Calendar calendar in entities){ - result.Add(new Calendar{ - Date = calendar.Date, - DayOfWeek = calendar.DayOfWeek, - Year = calendar.Year, - DayOfWeekNumber = calendar.DayOfWeekNumber, - DayOfWeekShort = calendar.DayOfWeekShort, - MonthNameLong = calendar.MonthNameLong, - MonthNameShort = calendar.MonthNameShort, - MonthNumber = calendar.MonthNumber, - WeekNumber = calendar.WeekNumber, - WeekNumberString = calendar.WeekNumberString, - YearWeekNumber = calendar.YearWeekNumber, - }); - } +using Microsoft.EntityFrameworkCore; +using Models; +using Shared.EntityFramework; +using System; +using System.Linq; +using System.Threading; +using Calendar = Models.Calendar; +using Merchant = Models.Merchant; +using Operator = Models.Operator; - return result; - } +public partial class ReportingManager : IReportingManager { + private readonly IDbContextResolver Resolver; - public async Task> GetCalendarDates(Guid estateId, CancellationToken cancellationToken){ - using ResolvedDbContext? resolvedContext = this.Resolver.Resolve(EstateManagementDatabaseName, estateId.ToString()); - await using EstateManagementContext context = resolvedContext.Context; - - List entities = context.Calendar.Where(c => c.Date <= DateTime.Now.Date).ToList(); - - List result = new List(); - foreach (TransactionProcessor.Database.Entities.Calendar calendar in entities){ - result.Add(new Calendar{ - Date = calendar.Date, - DayOfWeek = calendar.DayOfWeek, - Year = calendar.Year, - DayOfWeekNumber = calendar.DayOfWeekNumber, - DayOfWeekShort = calendar.DayOfWeekShort, - MonthNameLong = calendar.MonthNameLong, - MonthNameShort = calendar.MonthNameShort, - MonthNumber = calendar.MonthNumber, - WeekNumber = calendar.WeekNumber, - WeekNumberString = calendar.WeekNumberString, - YearWeekNumber = calendar.YearWeekNumber, - }); - } - return result; - } + private Guid Id; + private static readonly String EstateManagementDatabaseName = "TransactionProcessorReadModel"; - public async Task> GetCalendarYears(Guid estateId, CancellationToken cancellationToken){ - using ResolvedDbContext? resolvedContext = this.Resolver.Resolve(EstateManagementDatabaseName, estateId.ToString()); - await using EstateManagementContext context = resolvedContext.Context; + #region Constructors - List years = context.Calendar.Where(c => c.Date <= DateTime.Now.Date).GroupBy(c => c.Year).Select(y => y.Key).ToList(); + public ReportingManager(IDbContextResolver resolver) { + this.Resolver = resolver; + } - return years; - } + #endregion + + #region Methods + + public async Task> GetCalendarComparisonDates(Guid estateId, + CancellationToken cancellationToken) { + using ResolvedDbContext? resolvedContext = this.Resolver.Resolve(EstateManagementDatabaseName, estateId.ToString()); + await using EstateManagementContext context = resolvedContext.Context; + + DateTime startOfYear = new(DateTime.Now.Year, 1, 1); + + List entities = context.Calendar.Where(c => c.Date >= startOfYear && c.Date < DateTime.Now.Date.AddDays(-1)).OrderByDescending(d => d.Date).ToList(); + + List result = new(); + foreach (TransactionProcessor.Database.Entities.Calendar calendar in entities) + result.Add(new Calendar { + Date = calendar.Date, + DayOfWeek = calendar.DayOfWeek, + Year = calendar.Year, + DayOfWeekNumber = calendar.DayOfWeekNumber, + DayOfWeekShort = calendar.DayOfWeekShort, + MonthNameLong = calendar.MonthNameLong, + MonthNameShort = calendar.MonthNameShort, + MonthNumber = calendar.MonthNumber, + WeekNumber = calendar.WeekNumber, + WeekNumberString = calendar.WeekNumberString, + YearWeekNumber = calendar.YearWeekNumber + }); - public async Task GetLastSettlement(Guid estateId, CancellationToken cancellationToken){ - using ResolvedDbContext? resolvedContext = this.Resolver.Resolve(EstateManagementDatabaseName, estateId.ToString()); - await using EstateManagementContext context = resolvedContext.Context; - - DateTime settlementDate = await context.SettlementSummary.Where(s => s.IsCompleted).OrderByDescending(s => s.SettlementDate).Select(s => s.SettlementDate).FirstOrDefaultAsync(cancellationToken); - - IQueryable settlements = from settlement in context.SettlementSummary - where settlement.SettlementDate == settlementDate - group new - { - settlement.SettlementDate, - FeeValue = settlement.FeeValue.GetValueOrDefault(), - SalesValue = settlement.SalesValue.GetValueOrDefault(), - SalesCount = settlement.SalesCount.GetValueOrDefault(), - } by settlement.SettlementDate into grouped - select new LastSettlement - { - FeesValue = grouped.Sum(g => g.FeeValue), - SalesCount = grouped.Sum(g => g.SalesCount), - SalesValue = grouped.Sum(g => g.SalesValue), - SettlementDate = grouped.Key - }; - - return await settlements.SingleOrDefaultAsync(cancellationToken); - } + return result; + } - public async Task GetMerchantsTransactionKpis(Guid estateId, CancellationToken cancellationToken){ - using ResolvedDbContext? resolvedContext = this.Resolver.Resolve(EstateManagementDatabaseName, estateId.ToString()); - await using EstateManagementContext context = resolvedContext.Context; + public async Task> GetCalendarDates(Guid estateId, + CancellationToken cancellationToken) { + using ResolvedDbContext? resolvedContext = this.Resolver.Resolve(EstateManagementDatabaseName, estateId.ToString()); + await using EstateManagementContext context = resolvedContext.Context; + + List entities = context.Calendar.Where(c => c.Date <= DateTime.Now.Date).ToList(); + + List result = new(); + foreach (TransactionProcessor.Database.Entities.Calendar calendar in entities) + result.Add(new Calendar { + Date = calendar.Date, + DayOfWeek = calendar.DayOfWeek, + Year = calendar.Year, + DayOfWeekNumber = calendar.DayOfWeekNumber, + DayOfWeekShort = calendar.DayOfWeekShort, + MonthNameLong = calendar.MonthNameLong, + MonthNameShort = calendar.MonthNameShort, + MonthNumber = calendar.MonthNumber, + WeekNumber = calendar.WeekNumber, + WeekNumberString = calendar.WeekNumberString, + YearWeekNumber = calendar.YearWeekNumber + }); - Int32 merchantsWithSaleInLastHour = (from m in context.Merchants - where m.LastSaleDate == DateTime.Now.Date - && m.LastSaleDateTime >= DateTime.Now.AddHours(-1) - select m.MerchantReportingId).Count(); + return result; + } - Int32 merchantsWithNoSaleToday = (from m in context.Merchants - where m.LastSaleDate == DateTime.Now.Date.AddDays(-1) - select m.MerchantReportingId).Count(); + public async Task> GetCalendarYears(Guid estateId, + CancellationToken cancellationToken) { + using ResolvedDbContext? resolvedContext = this.Resolver.Resolve(EstateManagementDatabaseName, estateId.ToString()); + await using EstateManagementContext context = resolvedContext.Context; - Int32 merchantsWithNoSaleInLast7Days = (from m in context.Merchants - where m.LastSaleDate <= DateTime.Now.Date.AddDays(-7) - select m.MerchantReportingId).Count(); + List years = context.Calendar.Where(c => c.Date <= DateTime.Now.Date).GroupBy(c => c.Year).Select(y => y.Key).ToList(); - MerchantKpi response = new MerchantKpi{ - MerchantsWithSaleInLastHour = merchantsWithSaleInLastHour, - MerchantsWithNoSaleToday = merchantsWithNoSaleToday, - MerchantsWithNoSaleInLast7Days = merchantsWithNoSaleInLast7Days - }; + return years; + } - return response; - } + public async Task GetLastSettlement(Guid estateId, + CancellationToken cancellationToken) { + using ResolvedDbContext? resolvedContext = this.Resolver.Resolve(EstateManagementDatabaseName, estateId.ToString()); + await using EstateManagementContext context = resolvedContext.Context; - public async Task> GetMerchantsByLastSale(Guid estateId, DateTime startDateTime, DateTime endDateTime, CancellationToken cancellationToken){ - using ResolvedDbContext? resolvedContext = this.Resolver.Resolve(EstateManagementDatabaseName, estateId.ToString()); - await using EstateManagementContext context = resolvedContext.Context; - - List response = new(); - - var merchants = await context.Merchants - .Where(m => m.LastSaleDateTime >= startDateTime && m.LastSaleDateTime <= endDateTime) - .Select(m => new - { - MerchantReportingId = m.MerchantReportingId, - EstateReportingId = context.Estates.Single(e => e.EstateId == m.EstateId).EstateReportingId, - Name = m.Name, - LastSaleDateTime = m.LastSaleDateTime, - LastSale = m.LastSaleDate, - CreatedDateTime = m.CreatedDateTime, - LastStatement = m.LastStatementGenerated, - MerchantId = m.MerchantId, - Reference = m.Reference, - AddressInfo = context.MerchantAddresses - .Where(ma => ma.MerchantId == m.MerchantId) - .OrderByDescending(ma => ma.CreatedDateTime) - .Select(ma => new - { - PostCode = ma.PostalCode, - Region = ma.Region, - Town = ma.Town, - // Add more properties as needed - }) - .FirstOrDefault() // Get the first matching MerchantAddress or null - }).ToListAsync(cancellationToken); - - merchants.ForEach(m => response.Add(new Merchant - { - LastSaleDateTime = m.LastSaleDateTime, - CreatedDateTime = m.CreatedDateTime, - EstateReportingId = m.EstateReportingId, - LastSale = m.LastSale, - LastStatement = m.LastStatement, - MerchantId = m.MerchantId, - MerchantReportingId = m.MerchantReportingId, - Name = m.Name, - Reference = m.Reference, - PostCode = m.AddressInfo?.PostCode, - Region = m.AddressInfo?.Region, - Town = m.AddressInfo?.Town - })); - - return response; - } + DateTime settlementDate = await context.SettlementSummary.Where(s => s.IsCompleted).OrderByDescending(s => s.SettlementDate).Select(s => s.SettlementDate).FirstOrDefaultAsync(cancellationToken); - public async Task> GetResponseCodes(Guid estateId, CancellationToken cancellationToken){ - using ResolvedDbContext? resolvedContext = this.Resolver.Resolve(EstateManagementDatabaseName, estateId.ToString()); - await using EstateManagementContext context = resolvedContext.Context; - List response = new List(); - - List responseCodes = await context.ResponseCodes.ToListAsync(cancellationToken); + IQueryable settlements = from settlement in context.SettlementSummary where settlement.SettlementDate == settlementDate group new { settlement.SettlementDate, FeeValue = settlement.FeeValue.GetValueOrDefault(), SalesValue = settlement.SalesValue.GetValueOrDefault(), SalesCount = settlement.SalesCount.GetValueOrDefault() } by settlement.SettlementDate into grouped select new LastSettlement { FeesValue = grouped.Sum(g => g.FeeValue), SalesCount = grouped.Sum(g => g.SalesCount), SalesValue = grouped.Sum(g => g.SalesValue), SettlementDate = grouped.Key }; - responseCodes.ForEach(r => response.Add(new ResponseCode{ - Code = r.ResponseCode, - Description = r.Description - })); + return await settlements.SingleOrDefaultAsync(cancellationToken); + } - return response; - } + public async Task GetMerchantsTransactionKpis(Guid estateId, + CancellationToken cancellationToken) { + using ResolvedDbContext? resolvedContext = this.Resolver.Resolve(EstateManagementDatabaseName, estateId.ToString()); + await using EstateManagementContext context = resolvedContext.Context; - public async Task GetTodaysFailedSales(Guid estateId, DateTime comparisonDate, String responseCode, CancellationToken cancellationToken){ - using ResolvedDbContext? resolvedContext = this.Resolver.Resolve(EstateManagementDatabaseName, estateId.ToString()); - await using EstateManagementContext context = resolvedContext.Context; - - List todaysSales = await (from t in context.TodayTransactions - where t.IsAuthorised == false - && t.TransactionType == "Sale" - && t.ResponseCode == responseCode - select t.TransactionAmount).ToListAsync(cancellationToken); - - List comparisonSales = await (from t in context.TransactionHistory - where t.IsAuthorised == false && t.TransactionType == "Sale" - && t.TransactionDate == comparisonDate - && t.TransactionTime <= DateTime.Now.TimeOfDay - && t.ResponseCode == responseCode - select t.TransactionAmount).ToListAsync(cancellationToken); - - TodaysSales response = new TodaysSales{ - ComparisonSalesCount = comparisonSales.Count, - ComparisonSalesValue = comparisonSales.Sum(), - ComparisonAverageSalesValue = SafeDivide(comparisonSales.Sum(),comparisonSales.Count), - TodaysSalesCount = todaysSales.Count, - TodaysSalesValue = todaysSales.Sum(), - TodaysAverageSalesValue = SafeDivide(todaysSales.Sum(),todaysSales.Count) - }; - return response; - } + Int32 merchantsWithSaleInLastHour = (from m in context.Merchants where m.LastSaleDate == DateTime.Now.Date && m.LastSaleDateTime >= DateTime.Now.AddHours(-1) select m.MerchantReportingId).Count(); - private IQueryable GetTodaysSales(EstateManagementContext context, - Int32 merchantReportingId, Int32 operatorReportingId){ - var salesForDate = (from t in context.TodayTransactions - where t.IsAuthorised && t.TransactionType == "Sale" - && t.TransactionDate == DateTime.Now.Date - && t.TransactionTime <= DateTime.Now.TimeOfDay - select t).AsQueryable(); - - if (merchantReportingId> 0){ - salesForDate = salesForDate.Where(t => t.MerchantReportingId == merchantReportingId).AsQueryable(); - } + Int32 merchantsWithNoSaleToday = (from m in context.Merchants where m.LastSaleDate == DateTime.Now.Date.AddDays(-1) select m.MerchantReportingId).Count(); - if (operatorReportingId> 0) - { - salesForDate = salesForDate.Where(t => t.OperatorReportingId == operatorReportingId).AsQueryable(); - } + Int32 merchantsWithNoSaleInLast7Days = (from m in context.Merchants where m.LastSaleDate <= DateTime.Now.Date.AddDays(-7) select m.MerchantReportingId).Count(); - return salesForDate; - } + MerchantKpi response = new() { MerchantsWithSaleInLastHour = merchantsWithSaleInLastHour, MerchantsWithNoSaleToday = merchantsWithNoSaleToday, MerchantsWithNoSaleInLast7Days = merchantsWithNoSaleInLast7Days }; - private IQueryable GetSalesForDate(EstateManagementContext context, - DateTime queryDate, - Int32 merchantReportingId, Int32 operatorReportingId) - { - var salesForDate = (from t in context.TransactionHistory - where t.IsAuthorised && t.TransactionType == "Sale" - && t.TransactionDate == queryDate.Date - && t.TransactionTime <= DateTime.Now.TimeOfDay - select t).AsQueryable(); - - if (merchantReportingId > 0) - { - salesForDate = salesForDate.Where(t => t.MerchantReportingId == merchantReportingId).AsQueryable(); - } + return response; + } - if (operatorReportingId > 0) - { - salesForDate = salesForDate.Where(t => t.OperatorReportingId == operatorReportingId).AsQueryable(); - } + public async Task> GetMerchantsByLastSale(Guid estateId, + DateTime startDateTime, + DateTime endDateTime, + CancellationToken cancellationToken) { + using ResolvedDbContext? resolvedContext = this.Resolver.Resolve(EstateManagementDatabaseName, estateId.ToString()); + await using EstateManagementContext context = resolvedContext.Context; + + List response = new(); + + var merchants = await context.Merchants.Where(m => m.LastSaleDateTime >= startDateTime && m.LastSaleDateTime <= endDateTime).Select(m => new { + MerchantReportingId = m.MerchantReportingId, + EstateReportingId = context.Estates.Single(e => e.EstateId == m.EstateId).EstateReportingId, + Name = m.Name, + LastSaleDateTime = m.LastSaleDateTime, + LastSale = m.LastSaleDate, + CreatedDateTime = m.CreatedDateTime, + LastStatement = m.LastStatementGenerated, + MerchantId = m.MerchantId, + Reference = m.Reference, + AddressInfo = context.MerchantAddresses.Where(ma => ma.MerchantId == m.MerchantId).OrderByDescending(ma => ma.CreatedDateTime).Select(ma => new { + PostCode = ma.PostalCode, Region = ma.Region, Town = ma.Town + // Add more properties as needed + }).FirstOrDefault() // Get the first matching MerchantAddress or null + }).ToListAsync(cancellationToken); + + merchants.ForEach(m => response.Add(new Merchant { + LastSaleDateTime = m.LastSaleDateTime, + CreatedDateTime = m.CreatedDateTime, + EstateReportingId = m.EstateReportingId, + LastSale = m.LastSale, + LastStatement = m.LastStatement, + MerchantId = m.MerchantId, + MerchantReportingId = m.MerchantReportingId, + Name = m.Name, + Reference = m.Reference, + PostCode = m.AddressInfo?.PostCode, + Region = m.AddressInfo?.Region, + Town = m.AddressInfo?.Town + })); + + return response; + } - return salesForDate; - } + public async Task> GetResponseCodes(Guid estateId, + CancellationToken cancellationToken) { + using ResolvedDbContext? resolvedContext = this.Resolver.Resolve(EstateManagementDatabaseName, estateId.ToString()); + await using EstateManagementContext context = resolvedContext.Context; + List response = new(); - public async Task GetTodaysSales(Guid estateId, Int32 merchantReportingId, Int32 operatorReportingId, DateTime comparisonDate, CancellationToken cancellationToken){ - using ResolvedDbContext? resolvedContext = this.Resolver.Resolve(EstateManagementDatabaseName, estateId.ToString()); - await using EstateManagementContext context = resolvedContext.Context; - - IQueryable todaysSales = GetTodaysSales(context, merchantReportingId, operatorReportingId); - IQueryable comparisonSales = GetSalesForDate(context, comparisonDate, merchantReportingId, operatorReportingId); - - var todaysSalesValue = await todaysSales.SumAsync(t => t.TransactionAmount, cancellationToken); - var todaysSalesCount = await todaysSales.CountAsync(cancellationToken); - var comparisonSalesValue = await comparisonSales.SumAsync(t => t.TransactionAmount, cancellationToken); - var comparisonSalesCount = await comparisonSales.CountAsync(cancellationToken); - - TodaysSales response = new TodaysSales{ - ComparisonSalesCount = comparisonSalesCount, - ComparisonSalesValue = comparisonSalesValue, - TodaysSalesCount = todaysSalesCount, - TodaysSalesValue = todaysSalesValue, - TodaysAverageSalesValue = todaysSalesValue / todaysSalesCount, - ComparisonAverageSalesValue = comparisonSalesValue / comparisonSalesCount - }; - return response; - } + List responseCodes = await context.ResponseCodes.ToListAsync(cancellationToken); - public async Task> GetTodaysSalesCountByHour(Guid estateId, Int32 merchantReportingId, Int32 operatorReportingId, DateTime comparisonDate, CancellationToken cancellationToken){ - using ResolvedDbContext? resolvedContext = this.Resolver.Resolve(EstateManagementDatabaseName, estateId.ToString()); - await using EstateManagementContext context = resolvedContext.Context; - - IQueryable todaysSales = GetTodaysSales(context, merchantReportingId, operatorReportingId); - IQueryable comparisonSales = GetSalesForDate(context, comparisonDate, merchantReportingId, operatorReportingId); - - // 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() - }).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() - }).ToListAsync(cancellationToken); - - List response = (from today in todaysSalesByHour - join comparison in comparisonSalesByHour - on today.Hour equals comparison.Hour - select new TodaysSalesCountByHour - { - Hour = today.Hour.Value, - TodaysSalesCount = today.TotalSalesCount, - ComparisonSalesCount = comparison.TotalSalesCount - }).ToList(); - - return response; - } + responseCodes.ForEach(r => response.Add(new ResponseCode { Code = r.ResponseCode, Description = r.Description })); - public async Task> GetTodaysSalesValueByHour(Guid estateId, Int32 merchantReportingId, Int32 operatorReportingId, DateTime comparisonDate, CancellationToken cancellationToken){ - using ResolvedDbContext? resolvedContext = this.Resolver.Resolve(EstateManagementDatabaseName, estateId.ToString()); - await using EstateManagementContext context = resolvedContext.Context; - - IQueryable todaysSales = GetTodaysSales(context, merchantReportingId, operatorReportingId); - IQueryable comparisonSales = GetSalesForDate(context, comparisonDate, merchantReportingId, operatorReportingId); - - // 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, - 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, - TotalSalesValue = g.Sum() - }).ToListAsync(cancellationToken); - - List response = (from today in todaysSalesByHour - join comparison in comparisonSalesByHour - on today.Hour equals comparison.Hour - select new TodaysSalesValueByHour - { - Hour = today.Hour.Value, - TodaysSalesValue = today.TotalSalesValue, - ComparisonSalesValue = comparison.TotalSalesValue - }).ToList(); - - return response; - } + return response; + } - private IQueryable GetSettlementDataForDate(EstateManagementContext context, Int32 merchantReportingId, Int32 operatorReportingId, DateTime queryDate) - { - if (queryDate.Date == DateTime.Today.Date) - { - return this.GetTodaysSettlement(context, merchantReportingId, operatorReportingId); - } - - var settlementData = (from s in context.Settlements - join f in context.MerchantSettlementFees on s.SettlementId equals f.SettlementId - join t in context.TransactionHistory on f.TransactionId equals t.TransactionId - where s.SettlementDate == queryDate.Date - select new { Settlement = s, Fees = f, t.MerchantReportingId, t.OperatorReportingId }).AsQueryable(); - - if (merchantReportingId > 0) - { - settlementData = settlementData.Where(t => t.MerchantReportingId== merchantReportingId).AsQueryable(); - } + public async Task GetTodaysFailedSales(Guid estateId, + DateTime comparisonDate, + String responseCode, + CancellationToken cancellationToken) { + using ResolvedDbContext? resolvedContext = this.Resolver.Resolve(EstateManagementDatabaseName, estateId.ToString()); + await using EstateManagementContext context = resolvedContext.Context; + + List todaysSales = await (from t in context.TodayTransactions where t.IsAuthorised == false && t.TransactionType == "Sale" && t.ResponseCode == responseCode select t.TransactionAmount).ToListAsync(cancellationToken); + + List comparisonSales = await (from t in context.TransactionHistory where t.IsAuthorised == false && t.TransactionType == "Sale" && t.TransactionDate == comparisonDate && t.TransactionTime <= DateTime.Now.TimeOfDay && t.ResponseCode == responseCode select t.TransactionAmount).ToListAsync(cancellationToken); + + TodaysSales response = new() { + ComparisonSalesCount = comparisonSales.Count, + ComparisonSalesValue = comparisonSales.Sum(), + ComparisonAverageSalesValue = this.SafeDivide(comparisonSales.Sum(), comparisonSales.Count), + TodaysSalesCount = todaysSales.Count, + TodaysSalesValue = todaysSales.Sum(), + TodaysAverageSalesValue = this.SafeDivide(todaysSales.Sum(), todaysSales.Count) + }; + return response; + } - if (operatorReportingId > 0) - { - settlementData = settlementData.Where(t => t.OperatorReportingId == operatorReportingId).AsQueryable(); - } + public async Task GetTodaysSales(Guid estateId, + Int32 merchantReportingId, + Int32 operatorReportingId, + DateTime comparisonDate, + CancellationToken cancellationToken) { + using ResolvedDbContext? resolvedContext = this.Resolver.Resolve(EstateManagementDatabaseName, estateId.ToString()); + await using EstateManagementContext context = resolvedContext.Context; + + IQueryable todaysSales = this.BuildTodaySalesQuery(context); + IQueryable comparisonSales = this.BuildComparisonSalesQuery(context, comparisonDate); + + todaysSales = todaysSales.ApplyMerchantFilter(merchantReportingId).ApplyOperatorFilter(operatorReportingId); + comparisonSales = comparisonSales.ApplyMerchantFilter(merchantReportingId).ApplyOperatorFilter(operatorReportingId); + + Decimal todaysSalesValue = await todaysSales.SumAsync(t => t.TransactionAmount, cancellationToken); + Int32 todaysSalesCount = await todaysSales.CountAsync(cancellationToken); + Decimal comparisonSalesValue = await comparisonSales.SumAsync(t => t.TransactionAmount, cancellationToken); + Int32 comparisonSalesCount = await comparisonSales.CountAsync(cancellationToken); + + TodaysSales response = new() { + ComparisonSalesCount = comparisonSalesCount, + ComparisonSalesValue = comparisonSalesValue, + TodaysSalesCount = todaysSalesCount, + TodaysSalesValue = todaysSalesValue, + TodaysAverageSalesValue = todaysSalesValue / todaysSalesCount, + ComparisonAverageSalesValue = comparisonSalesValue / comparisonSalesCount + }; + return response; + } - return settlementData.AsQueryable().Select(s => s.Fees); - } + public async Task> GetTodaysSalesCountByHour(Guid estateId, + Int32 merchantReportingId, + Int32 operatorReportingId, + DateTime comparisonDate, + CancellationToken cancellationToken) { + using ResolvedDbContext? resolvedContext = this.Resolver.Resolve(EstateManagementDatabaseName, estateId.ToString()); + await using EstateManagementContext context = resolvedContext.Context; - private IQueryable GetTodaysSettlement(EstateManagementContext? context, Int32 merchantReportingId, Int32 operatorReportingId) - { - var settlementData = (from s in context.Settlements - join f in context.MerchantSettlementFees on s.SettlementId equals f.SettlementId - join t in context.TodayTransactions on f.TransactionId equals t.TransactionId - where s.SettlementDate == DateTime.Today.Date - select new { Settlement = s, Fees = f, Transactions = t}).AsQueryable(); - - if (merchantReportingId > 0) - { - settlementData = settlementData.Where(t => t.Transactions.MerchantReportingId == merchantReportingId).AsQueryable(); - } + IQueryable todaysSales = this.BuildTodaySalesQuery(context); + IQueryable comparisonSales = this.BuildComparisonSalesQuery(context, comparisonDate); - if (operatorReportingId > 0) - { - settlementData = settlementData.Where(t => t.Transactions.OperatorReportingId == operatorReportingId).AsQueryable(); - } + todaysSales = todaysSales.ApplyMerchantFilter(merchantReportingId).ApplyOperatorFilter(operatorReportingId); + comparisonSales = comparisonSales.ApplyMerchantFilter(merchantReportingId).ApplyOperatorFilter(operatorReportingId); - return settlementData.AsQueryable().Select(s => s.Fees); - } + // 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() }).ToListAsync(cancellationToken); - public async Task GetTodaysSettlement(Guid estateId, Int32 merchantReportingId, Int32 operatorReportingId, DateTime comparisonDate, CancellationToken cancellationToken){ - using ResolvedDbContext? resolvedContext = this.Resolver.Resolve(EstateManagementDatabaseName, estateId.ToString()); - await using EstateManagementContext context = resolvedContext.Context; - - IQueryable todaySettlementData = GetTodaysSettlement(context, merchantReportingId, operatorReportingId); - IQueryable comparisonSettlementData = GetSettlementDataForDate(context, merchantReportingId, operatorReportingId, comparisonDate); - - var todaySettlement = await (from f in todaySettlementData - group f by f.IsSettled into grouped - select new - { - IsSettled = grouped.Key, - CalculatedValueSum = grouped.Sum(item => item.CalculatedValue), - CalculatedValueCount = grouped.Count() - }).ToListAsync(cancellationToken); - - var comparisonSettlement = await (from f in comparisonSettlementData - group f by f.IsSettled into grouped - select new - { - IsSettled = grouped.Key, - CalculatedValueSum = grouped.Sum(item => item.CalculatedValue), - CalculatedValueCount = grouped.Count() - }).ToListAsync(cancellationToken); - - - TodaysSettlement response = new TodaysSettlement - { - ComparisonSettlementCount = comparisonSettlement.FirstOrDefault(x => x.IsSettled)?.CalculatedValueCount ?? 0, - ComparisonSettlementValue = comparisonSettlement.FirstOrDefault(x => x.IsSettled)?.CalculatedValueSum ?? 0, - ComparisonPendingSettlementCount = comparisonSettlement.FirstOrDefault(x => x.IsSettled == false)?.CalculatedValueCount ?? 0, - ComparisonPendingSettlementValue = comparisonSettlement.FirstOrDefault(x => x.IsSettled == false)?.CalculatedValueSum ?? 0, - TodaysSettlementCount = todaySettlement.FirstOrDefault(x => x.IsSettled)?.CalculatedValueCount ?? 0, - TodaysSettlementValue = todaySettlement.FirstOrDefault(x => x.IsSettled)?.CalculatedValueSum ?? 0, - TodaysPendingSettlementCount = todaySettlement.FirstOrDefault(x => x.IsSettled == false)?.CalculatedValueCount ?? 0, - TodaysPendingSettlementValue = todaySettlement.FirstOrDefault(x => x.IsSettled == false)?.CalculatedValueSum ?? 0 - }; + var comparisonSalesByHour = await (from t in comparisonSales group t.TransactionAmount by t.Hour into g select new { Hour = g.Key, TotalSalesCount = g.Count() }).ToListAsync(cancellationToken); - return response; - } + List response = (from today in todaysSalesByHour join comparison in comparisonSalesByHour on today.Hour equals comparison.Hour select new TodaysSalesCountByHour { Hour = today.Hour.Value, TodaysSalesCount = today.TotalSalesCount, ComparisonSalesCount = comparison.TotalSalesCount }).ToList(); - private Int32 SafeDivide(Int32 number, Int32 divisor) - { - if (divisor == 0) - { - return number; - } + return response; + } - return number / divisor; - } + public async Task> GetTodaysSalesValueByHour(Guid estateId, + Int32 merchantReportingId, + Int32 operatorReportingId, + DateTime comparisonDate, + CancellationToken cancellationToken) { + using ResolvedDbContext? resolvedContext = this.Resolver.Resolve(EstateManagementDatabaseName, estateId.ToString()); + await using EstateManagementContext context = resolvedContext.Context; + + IQueryable todaysSales = this.BuildTodaySalesQuery(context); + IQueryable comparisonSales = this.BuildComparisonSalesQuery(context, comparisonDate); + + todaysSales = todaysSales.ApplyMerchantFilter(merchantReportingId).ApplyOperatorFilter(operatorReportingId); + comparisonSales = comparisonSales.ApplyMerchantFilter(merchantReportingId).ApplyOperatorFilter(operatorReportingId); + + // 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, 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, TotalSalesValue = g.Sum() }).ToListAsync(cancellationToken); + + List response = (from today in todaysSalesByHour join comparison in comparisonSalesByHour on today.Hour equals comparison.Hour select new TodaysSalesValueByHour { Hour = today.Hour.Value, TodaysSalesValue = today.TotalSalesValue, ComparisonSalesValue = comparison.TotalSalesValue }).ToList(); + + return response; + } + + private Int32 SafeDivide(Int32 number, + Int32 divisor) { + if (divisor == 0) return number; + + return number / divisor; + } + + private Decimal SafeDivide(Decimal number, + Int32 divisor) { + if (divisor == 0) return number; + + return number / divisor; + } - private Decimal SafeDivide(Decimal number, Int32 divisor) - { - if (divisor == 0) - { - return number; + public async Task> GetMerchants(Guid estateId, + CancellationToken cancellationToken) { + using ResolvedDbContext? resolvedContext = this.Resolver.Resolve(EstateManagementDatabaseName, estateId.ToString()); + await using EstateManagementContext context = resolvedContext.Context; + + var merchants = context.Merchants.Select(m => new { + MerchantReportingId = m.MerchantReportingId, + Name = m.Name, + LastSaleDateTime = m.LastSaleDateTime, + LastSale = m.LastSaleDate, + CreatedDateTime = m.CreatedDateTime, + LastStatement = m.LastStatementGenerated, + MerchantId = m.MerchantId, + Reference = m.Reference, + AddressInfo = context.MerchantAddresses.Where(ma => ma.MerchantId == m.MerchantId).OrderByDescending(ma => ma.CreatedDateTime).Select(ma => new { + PostCode = ma.PostalCode, Region = ma.Region, Town = ma.Town + // Add more properties as needed + }).FirstOrDefault(), // Get the first matching MerchantAddress or null + EstateReportingId = context.Estates.Single(e => e.EstateId == m.EstateId).EstateReportingId + }); + + List merchantList = new(); + foreach (var result in merchants) { + Merchant model = new() { + MerchantId = result.MerchantId, + Name = result.Name, + Reference = result.Reference, + MerchantReportingId = result.MerchantReportingId, + CreatedDateTime = result.CreatedDateTime, + EstateReportingId = result.EstateReportingId, + LastSale = result.LastSale, + LastSaleDateTime = result.LastSaleDateTime, + LastStatement = result.LastStatement + }; + + if (result.AddressInfo != null) { + model.PostCode = result.AddressInfo.PostCode; + model.Town = result.AddressInfo.Town; + model.Region = result.AddressInfo.Region; } - return number / divisor; + merchantList.Add(model); } - - public async Task> GetMerchants(Guid estateId, - CancellationToken cancellationToken) { - using ResolvedDbContext? resolvedContext = this.Resolver.Resolve(EstateManagementDatabaseName, estateId.ToString()); - await using EstateManagementContext context = resolvedContext.Context; - - var merchants = context.Merchants.Select(m => new { - MerchantReportingId = m.MerchantReportingId, - Name = m.Name, - LastSaleDateTime = m.LastSaleDateTime, - LastSale = m.LastSaleDate, - CreatedDateTime = m.CreatedDateTime, - LastStatement = m.LastStatementGenerated, - MerchantId = m.MerchantId, - Reference = m.Reference, - AddressInfo = context.MerchantAddresses.Where(ma => ma.MerchantId == m.MerchantId).OrderByDescending(ma => ma.CreatedDateTime).Select(ma => new { - PostCode = ma.PostalCode, Region = ma.Region, Town = ma.Town, - // Add more properties as needed - }).FirstOrDefault(), // Get the first matching MerchantAddress or null - EstateReportingId = context.Estates.Single(e => e.EstateId == m.EstateId).EstateReportingId + + return merchantList; + } + + private IQueryable BuildUnsettledFeesQuery(EstateManagementContext context, + DateTime startDate, + DateTime endDate) { + return from merchantSettlementFee in context.MerchantSettlementFees join transaction in context.Transactions on merchantSettlementFee.TransactionId equals transaction.TransactionId where merchantSettlementFee.FeeCalculatedDateTime.Date >= startDate && merchantSettlementFee.FeeCalculatedDateTime.Date <= endDate select new DatabaseProjections.FeeTransactionProjection { Fee = merchantSettlementFee, Txn = transaction }; + } + + private IQueryable BuildTransactionSearchQuery(EstateManagementContext context, + DateTime queryDate) { + return from txn in context.Transactions join merchant in context.Merchants on txn.MerchantId equals merchant.MerchantId join @operator in context.Operators on txn.OperatorId equals @operator.OperatorId join product in context.ContractProducts on txn.ContractProductId equals product.ContractProductId where txn.TransactionDate == queryDate select new DatabaseProjections.TransactionSearchProjection { Transaction = txn, Merchant = merchant, Operator = @operator, Product = product }; + } + + private IQueryable 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; + } + + private IQueryable 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; + } + + private IQueryable BuildTodaySettlementQuery(EstateManagementContext context, + DateTime settlementDate) { + IQueryable settlementData = from s in context.Settlements join f in context.MerchantSettlementFees on s.SettlementId equals f.SettlementId join t in context.TodayTransactions on f.TransactionId equals t.TransactionId where s.SettlementDate == settlementDate select new DatabaseProjections.TodaySettlementTransactionProjection { Fee = f, Txn = t }; + return settlementData; + } + + private IQueryable BuildComparisonSettlementQuery(EstateManagementContext context, + DateTime settlementDate) { + IQueryable settlementData = from s in context.Settlements join f in context.MerchantSettlementFees on s.SettlementId equals f.SettlementId join t in context.TransactionHistory on f.TransactionId equals t.TransactionId where s.SettlementDate == settlementDate.Date select new DatabaseProjections.ComparisonSettlementTransactionProjection { Fee = f, Txn = t }; + return settlementData; + } + + + public async Task> GetUnsettledFees(Guid estateId, + DateTime startDate, + DateTime endDate, + List merchantIds, + List operatorIds, + List productIds, + GroupByOption? groupByOption, + CancellationToken cancellationToken) { + using ResolvedDbContext? resolvedContext = this.Resolver.Resolve(EstateManagementDatabaseName, estateId.ToString()); + await using EstateManagementContext context = resolvedContext.Context; + + IQueryable query = this.BuildUnsettledFeesQuery(context, startDate, endDate).ApplyMerchantFilter(context, merchantIds).ApplyOperatorFilter(context, operatorIds).ApplyProductFilter(context, productIds); + + // Perform grouping + IQueryable groupedQuery = groupByOption switch { + GroupByOption.Merchant => query.ApplyMerchantGrouping(context), + GroupByOption.Operator => query.ApplyOperatorGrouping(context), + GroupByOption.Product => query.ApplyProductGrouping(context) + }; + return await groupedQuery.ToListAsync(cancellationToken); + } + + public async Task> TransactionSearch(Guid estateId, + TransactionSearchRequest searchRequest, + PagingRequest pagingRequest, + SortingRequest sortingRequest, + CancellationToken cancellationToken) { + // Base query before any filtering is added + using ResolvedDbContext? resolvedContext = this.Resolver.Resolve(EstateManagementDatabaseName, estateId.ToString()); + await using EstateManagementContext context = resolvedContext.Context; + + IQueryable mainQuery = this.BuildTransactionSearchQuery(context, searchRequest.QueryDate); + + mainQuery = mainQuery.ApplyFilters(searchRequest); + mainQuery = mainQuery.ApplySorting(sortingRequest); + mainQuery = mainQuery.ApplyPagination(pagingRequest); + + // Now build the results + List queryResults = await mainQuery.ToListAsync(cancellationToken); + + List results = new(); + + queryResults.ForEach(qr => { + results.Add(new TransactionResult { + MerchantReportingId = qr.Merchant.MerchantReportingId, + ResponseCode = qr.Transaction.ResponseCode, + IsAuthorised = qr.Transaction.IsAuthorised, + MerchantName = qr.Merchant.Name, + OperatorName = qr.Operator.Name, + OperatorReportingId = qr.Operator.OperatorReportingId, + Product = qr.Product.ProductName, + ProductReportingId = qr.Product.ContractProductReportingId, + ResponseMessage = qr.Transaction.ResponseMessage, + TransactionDateTime = qr.Transaction.TransactionDateTime, + TransactionId = qr.Transaction.TransactionId, + TransactionReportingId = qr.Transaction.TransactionReportingId, + TransactionSource = qr.Transaction.TransactionSource.ToString(), + TransactionAmount = qr.Transaction.TransactionAmount }); + }); - List merchantList = new List(); - foreach (var result in merchants) { - var model = new Merchant { - MerchantId = result.MerchantId, - Name = result.Name, - Reference = result.Reference, - MerchantReportingId = result.MerchantReportingId, - CreatedDateTime = result.CreatedDateTime, - EstateReportingId = result.EstateReportingId, - LastSale = result.LastSale, - LastSaleDateTime = result.LastSaleDateTime, - LastStatement = result.LastStatement - }; - - if (result.AddressInfo != null) { - model.PostCode = result.AddressInfo.PostCode; - model.Town = result.AddressInfo.Town; - model.Region = result.AddressInfo.Region; - } - - merchantList.Add(model); - } + return results; + } - return merchantList; - } + public async Task GetMerchantPerformance(Guid estateId, + DateTime comparisonDate, + List merchantReportingIds, + CancellationToken cancellationToken) { + using ResolvedDbContext? resolvedContext = this.Resolver.Resolve(EstateManagementDatabaseName, estateId.ToString()); + await using EstateManagementContext context = resolvedContext.Context; - public async Task> GetOperators(Guid estateId, CancellationToken cancellationToken){ - using ResolvedDbContext? resolvedContext = this.Resolver.Resolve(EstateManagementDatabaseName, estateId.ToString()); - await using EstateManagementContext context = resolvedContext.Context; - - List operators = await (from o in context.Operators - select new Operator - { - Name = o.Name, - EstateReportingId = context.Estates.Single(e => e.EstateId == o.EstateId).EstateReportingId, - OperatorId = o.OperatorId, - OperatorReportingId = o.OperatorReportingId - }).ToListAsync(cancellationToken); - - return operators; - } + // First we need to get a value of todays sales + IQueryable todaysSalesQuery = this.BuildTodaySalesQuery(context); + IQueryable comparisonSalesQuery = this.BuildComparisonSalesQuery(context, comparisonDate); + + todaysSalesQuery = todaysSalesQuery.ApplyMerchantFilter(merchantReportingIds); + comparisonSalesQuery = comparisonSalesQuery.ApplyMerchantFilter(merchantReportingIds); + + // Build the response + TodaysSales response = new() { ComparisonSalesCount = await comparisonSalesQuery.CountAsync(cancellationToken), ComparisonSalesValue = await comparisonSalesQuery.SumAsync(t => t.TransactionAmount, cancellationToken), TodaysSalesCount = await todaysSalesQuery.CountAsync(cancellationToken), TodaysSalesValue = await todaysSalesQuery.SumAsync(t => t.TransactionAmount, cancellationToken) }; + response.ComparisonAverageSalesValue = this.SafeDivide(response.ComparisonSalesValue, response.ComparisonSalesCount); + response.TodaysAverageSalesValue = this.SafeDivide(response.TodaysSalesValue, response.TodaysSalesCount); + + return response; + } + + public async Task GetProductPerformance(Guid estateId, + DateTime comparisonDate, + List productReportingIds, + CancellationToken cancellationToken) { + using ResolvedDbContext? resolvedContext = this.Resolver.Resolve(EstateManagementDatabaseName, estateId.ToString()); + await using EstateManagementContext context = resolvedContext.Context; - #endregion + // First we need to get a value of todays sales + IQueryable todaysSalesQuery = this.BuildTodaySalesQuery(context); + IQueryable comparisonSalesQuery = this.BuildComparisonSalesQuery(context, comparisonDate); - #region Others + todaysSalesQuery = todaysSalesQuery.ApplyProductFilter(productReportingIds); + comparisonSalesQuery = comparisonSalesQuery.ApplyProductFilter(productReportingIds); - private const String ConnectionStringIdentifier = "EstateReportingReadModel"; + TodaysSales response = new() { ComparisonSalesCount = await comparisonSalesQuery.CountAsync(cancellationToken), ComparisonSalesValue = await comparisonSalesQuery.SumAsync(t => t.TransactionAmount, cancellationToken), TodaysSalesCount = await todaysSalesQuery.CountAsync(cancellationToken), TodaysSalesValue = await todaysSalesQuery.SumAsync(t => t.TransactionAmount, cancellationToken) }; + response.ComparisonAverageSalesValue = this.SafeDivide(response.ComparisonSalesValue, response.ComparisonSalesCount); + response.TodaysAverageSalesValue = this.SafeDivide(response.TodaysSalesValue, response.TodaysSalesCount); - #endregion + return response; } + + public async Task GetOperatorPerformance(Guid estateId, + DateTime comparisonDate, + List operatorReportingIds, + CancellationToken cancellationToken) { + using ResolvedDbContext? resolvedContext = this.Resolver.Resolve(EstateManagementDatabaseName, estateId.ToString()); + await using EstateManagementContext context = resolvedContext.Context; + + // First we need to get a value of todays sales + IQueryable todaysSalesQuery = this.BuildTodaySalesQuery(context); + IQueryable comparisonSalesQuery = this.BuildComparisonSalesQuery(context, comparisonDate); + + todaysSalesQuery = todaysSalesQuery.ApplyOperatorFilter(operatorReportingIds); + comparisonSalesQuery = comparisonSalesQuery.ApplyOperatorFilter(operatorReportingIds); + + TodaysSales response = new() { ComparisonSalesCount = await comparisonSalesQuery.CountAsync(cancellationToken), ComparisonSalesValue = await comparisonSalesQuery.SumAsync(t => t.TransactionAmount, cancellationToken), TodaysSalesCount = await todaysSalesQuery.CountAsync(cancellationToken), TodaysSalesValue = await todaysSalesQuery.SumAsync(t => t.TransactionAmount, cancellationToken) }; + response.ComparisonAverageSalesValue = this.SafeDivide(response.ComparisonSalesValue, response.ComparisonSalesCount); + response.TodaysAverageSalesValue = this.SafeDivide(response.TodaysSalesValue, response.TodaysSalesCount); + + return response; + } + + public async Task> GetTopBottomData(Guid estateId, + TopBottom direction, + Int32 resultCount, + Dimension dimension, + CancellationToken cancellationToken) { + using ResolvedDbContext? resolvedContext = this.Resolver.Resolve(EstateManagementDatabaseName, estateId.ToString()); + await using EstateManagementContext context = resolvedContext.Context; + + IQueryable mainQuery = this.BuildTodaySalesQuery(context); + + IQueryable topBottomData = dimension switch { + Dimension.Merchant => mainQuery.ApplyMerchantGrouping(context), + Dimension.Operator => mainQuery.ApplyOperatorGrouping(context), + Dimension.Product => mainQuery.ApplyProductGrouping(context) + }; + + topBottomData = direction switch { + TopBottom.Top => topBottomData.OrderByDescending(g => g.SalesValue), + _ => topBottomData.OrderBy(g => g.SalesValue) + }; + + return await topBottomData.Take(resultCount).ToListAsync(cancellationToken); + } + + public async Task GetTodaysSettlement(Guid estateId, + Int32 merchantReportingId, + Int32 operatorReportingId, + DateTime comparisonDate, + CancellationToken cancellationToken) { + using ResolvedDbContext? resolvedContext = this.Resolver.Resolve(EstateManagementDatabaseName, estateId.ToString()); + await using EstateManagementContext context = resolvedContext.Context; + + IQueryable todaySettlementData = this.BuildTodaySettlementQuery(context, DateTime.Now); + IQueryable comparisonSettlementData = this.BuildComparisonSettlementQuery(context, comparisonDate); + + todaySettlementData = todaySettlementData.ApplyMerchantFilter(merchantReportingId).ApplyOperatorFilter(operatorReportingId); + comparisonSettlementData = comparisonSettlementData.ApplyMerchantFilter(merchantReportingId).ApplyOperatorFilter(operatorReportingId); + + var todaySettlement = await (from f in todaySettlementData group f by f.Fee.IsSettled into grouped select new { IsSettled = grouped.Key, CalculatedValueSum = grouped.Sum(item => item.Fee.CalculatedValue), CalculatedValueCount = grouped.Count() }).ToListAsync(cancellationToken); + + var comparisonSettlement = await (from f in comparisonSettlementData group f by f.Fee.IsSettled into grouped select new { IsSettled = grouped.Key, CalculatedValueSum = grouped.Sum(item => item.Fee.CalculatedValue), CalculatedValueCount = grouped.Count() }).ToListAsync(cancellationToken); + + + TodaysSettlement response = new() { + ComparisonSettlementCount = comparisonSettlement.FirstOrDefault(x => x.IsSettled)?.CalculatedValueCount ?? 0, + ComparisonSettlementValue = comparisonSettlement.FirstOrDefault(x => x.IsSettled)?.CalculatedValueSum ?? 0, + ComparisonPendingSettlementCount = comparisonSettlement.FirstOrDefault(x => x.IsSettled == false)?.CalculatedValueCount ?? 0, + ComparisonPendingSettlementValue = comparisonSettlement.FirstOrDefault(x => x.IsSettled == false)?.CalculatedValueSum ?? 0, + TodaysSettlementCount = todaySettlement.FirstOrDefault(x => x.IsSettled)?.CalculatedValueCount ?? 0, + TodaysSettlementValue = todaySettlement.FirstOrDefault(x => x.IsSettled)?.CalculatedValueSum ?? 0, + TodaysPendingSettlementCount = todaySettlement.FirstOrDefault(x => x.IsSettled == false)?.CalculatedValueCount ?? 0, + TodaysPendingSettlementValue = todaySettlement.FirstOrDefault(x => x.IsSettled == false)?.CalculatedValueSum ?? 0 + }; + + return response; + } + + public async Task> GetOperators(Guid estateId, + CancellationToken cancellationToken) { + using ResolvedDbContext? resolvedContext = this.Resolver.Resolve(EstateManagementDatabaseName, estateId.ToString()); + await using EstateManagementContext context = resolvedContext.Context; + + List operators = await (from o in context.Operators select new Operator { Name = o.Name, EstateReportingId = context.Estates.Single(e => e.EstateId == o.EstateId).EstateReportingId, OperatorId = o.OperatorId, OperatorReportingId = o.OperatorReportingId }).ToListAsync(cancellationToken); + + return operators; + } + + #endregion + + #region Others + + private const String ConnectionStringIdentifier = "EstateReportingReadModel"; + + #endregion } \ No newline at end of file diff --git a/EstateReportingAPI.BusinessLogic/ReportingManagerExtensions.cs b/EstateReportingAPI.BusinessLogic/ReportingManagerExtensions.cs index f91123a..8813533 100644 --- a/EstateReportingAPI.BusinessLogic/ReportingManagerExtensions.cs +++ b/EstateReportingAPI.BusinessLogic/ReportingManagerExtensions.cs @@ -13,6 +13,49 @@ public static IQueryable ApplyMerchantFilter(this IQueryable merchantReportingIds.Contains(t.MerchantReportingId)).AsQueryable(); } + public static IQueryable ApplyMerchantFilter(this IQueryable query, int merchantReportingId) { + if (merchantReportingId == 0) + return query; + List merchantReportingIds = [merchantReportingId]; + return query.ApplyMerchantFilter(merchantReportingIds); + } + + public static IQueryable ApplyMerchantFilter(this IQueryable query, int merchantReportingId) + { + if (merchantReportingId == 0) + return query; + return query.Where(q => q.Txn.MerchantReportingId == merchantReportingId).AsQueryable(); + } + + public static IQueryable ApplyOperatorFilter(this IQueryable query, int operatorReportingId) + { + if (operatorReportingId == 0) + return query; + return query.Where(q => q.Txn.OperatorReportingId == operatorReportingId).AsQueryable(); + } + + public static IQueryable ApplyMerchantFilter(this IQueryable query, int merchantReportingId) + { + if (merchantReportingId == 0) + return query; + return query.Where(q => q.Txn.MerchantReportingId == merchantReportingId).AsQueryable(); + } + + public static IQueryable ApplyOperatorFilter(this IQueryable query, int operatorReportingId) + { + if (operatorReportingId == 0) + return query; + return query.Where(q => q.Txn.OperatorReportingId == operatorReportingId).AsQueryable(); + } + + public static IQueryable ApplyMerchantFilter(this IQueryable query, int merchantReportingId) + { + if (merchantReportingId == 0) + return query; + List merchantReportingIds = [merchantReportingId]; + return query.ApplyMerchantFilter(merchantReportingIds); + } + public static IQueryable ApplyMerchantFilter(this IQueryable query, List merchantReportingIds) { if (merchantReportingIds == null || merchantReportingIds.Count == 0) @@ -49,10 +92,25 @@ public static IQueryable ApplyOperatorFilter(this IQueryable { if (operatorReportingIds == null || operatorReportingIds.Count == 0) return query; - return query.Where(t => operatorReportingIds.Contains(t.OperatorReportingId)).AsQueryable(); } + public static IQueryable ApplyOperatorFilter(this IQueryable query, int operatorReportingId) { + if (operatorReportingId == 0) + return query; + List operatorReportingIds = [operatorReportingId]; + return query.ApplyOperatorFilter(operatorReportingIds); + } + + public static IQueryable ApplyOperatorFilter(this IQueryable query, int operatorReportingId) + { + if (operatorReportingId == 0) + return query; + List operatorReportingIds = [operatorReportingId]; + return query.ApplyOperatorFilter(operatorReportingIds); + } + + public static IQueryable ApplyMerchantFilter(this IQueryable query, EstateManagementContext context, List merchantIds) diff --git a/EstateReportingAPI.BusinessLogic/ReportingManagerRefactored.cs b/EstateReportingAPI.BusinessLogic/ReportingManagerRefactored.cs deleted file mode 100644 index fda822d..0000000 --- a/EstateReportingAPI.BusinessLogic/ReportingManagerRefactored.cs +++ /dev/null @@ -1,246 +0,0 @@ -using EstateReportingAPI.Models; -using Microsoft.EntityFrameworkCore; -using Shared.EntityFramework; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using TransactionProcessor.Database.Contexts; -using TransactionProcessor.Database.Entities; -using TransactionProcessor.Database.Entities.Summary; -using Merchant = TransactionProcessor.Database.Entities.Merchant; -using Operator = TransactionProcessor.Database.Entities.Operator; - -namespace EstateReportingAPI.BusinessLogic -{ - public partial class ReportingManager : IReportingManager { - - private IQueryable BuildUnsettledFeesQuery(EstateManagementContext context, - DateTime startDate, - DateTime endDate) - { - return from merchantSettlementFee in context.MerchantSettlementFees - join transaction in context.Transactions - on merchantSettlementFee.TransactionId equals transaction.TransactionId - where merchantSettlementFee.FeeCalculatedDateTime.Date >= startDate && - merchantSettlementFee.FeeCalculatedDateTime.Date <= endDate - select new DatabaseProjections.FeeTransactionProjection { Fee = merchantSettlementFee, Txn = transaction }; - } - - private IQueryable BuildTransactionSearchQuery(EstateManagementContext context, - DateTime queryDate) { - return from txn in context.Transactions - join merchant in context.Merchants on txn.MerchantId equals merchant.MerchantId - join @operator in context.Operators on txn.OperatorId equals @operator.OperatorId - join product in context.ContractProducts on txn.ContractProductId equals product.ContractProductId - where txn.TransactionDate == queryDate - select new DatabaseProjections.TransactionSearchProjection { - Transaction = txn, - Merchant = merchant, - Operator = @operator, - Product = product - }; - } - - private IQueryable 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; - } - - private IQueryable 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; - } - - public async Task> GetUnsettledFees(Guid estateId, DateTime startDate, DateTime endDate, List merchantIds, List operatorIds, List productIds, GroupByOption? groupByOption, CancellationToken cancellationToken) - { - - using ResolvedDbContext? resolvedContext = this.Resolver.Resolve(EstateManagementDatabaseName, estateId.ToString()); - await using EstateManagementContext context = resolvedContext.Context; - - IQueryable query = BuildUnsettledFeesQuery(context, startDate, endDate) - .ApplyMerchantFilter(context, merchantIds) - .ApplyOperatorFilter(context, operatorIds) - .ApplyProductFilter(context, productIds); - - // Perform grouping - IQueryable groupedQuery = groupByOption switch - { - GroupByOption.Merchant => query.ApplyMerchantGrouping(context), - GroupByOption.Operator => query.ApplyOperatorGrouping(context), - GroupByOption.Product => query.ApplyProductGrouping(context), - }; - return await groupedQuery.ToListAsync(cancellationToken); - - } - - public async Task> TransactionSearch(Guid estateId, TransactionSearchRequest searchRequest, PagingRequest pagingRequest, SortingRequest sortingRequest, CancellationToken cancellationToken) - { - // Base query before any filtering is added - using ResolvedDbContext? resolvedContext = this.Resolver.Resolve(EstateManagementDatabaseName, estateId.ToString()); - await using EstateManagementContext context = resolvedContext.Context; - - IQueryable mainQuery = BuildTransactionSearchQuery(context, searchRequest.QueryDate); - - mainQuery = mainQuery.ApplyFilters(searchRequest); - mainQuery = mainQuery.ApplySorting(sortingRequest); - mainQuery = mainQuery.ApplyPagination(pagingRequest); - - // Now build the results - List queryResults = await mainQuery.ToListAsync(cancellationToken); - - List results = new List(); - - queryResults.ForEach(qr => - { - results.Add(new TransactionResult - { - MerchantReportingId = qr.Merchant.MerchantReportingId, - ResponseCode = qr.Transaction.ResponseCode, - IsAuthorised = qr.Transaction.IsAuthorised, - MerchantName = qr.Merchant.Name, - OperatorName = qr.Operator.Name, - OperatorReportingId = qr.Operator.OperatorReportingId, - Product = qr.Product.ProductName, - ProductReportingId = qr.Product.ContractProductReportingId, - ResponseMessage = qr.Transaction.ResponseMessage, - TransactionDateTime = qr.Transaction.TransactionDateTime, - TransactionId = qr.Transaction.TransactionId, - TransactionReportingId = qr.Transaction.TransactionReportingId, - TransactionSource = qr.Transaction.TransactionSource.ToString(), - TransactionAmount = qr.Transaction.TransactionAmount - }); - }); - - return results; - } - - public async Task GetMerchantPerformance(Guid estateId, DateTime comparisonDate, List merchantReportingIds, CancellationToken cancellationToken) - { - using ResolvedDbContext? resolvedContext = this.Resolver.Resolve(EstateManagementDatabaseName, estateId.ToString()); - await using EstateManagementContext context = resolvedContext.Context; - - // First we need to get a value of todays sales - IQueryable todaysSalesQuery = BuildTodaySalesQuery(context); - IQueryable comparisonSalesQuery = BuildComparisonSalesQuery(context, comparisonDate); - - todaysSalesQuery = todaysSalesQuery.ApplyMerchantFilter(merchantReportingIds); - comparisonSalesQuery = comparisonSalesQuery.ApplyMerchantFilter(merchantReportingIds); - - // Build the response - TodaysSales response = new TodaysSales - { - ComparisonSalesCount = await comparisonSalesQuery.CountAsync(cancellationToken), - ComparisonSalesValue = await comparisonSalesQuery.SumAsync(t => t.TransactionAmount, cancellationToken), - TodaysSalesCount = await todaysSalesQuery.CountAsync(cancellationToken), - TodaysSalesValue = await todaysSalesQuery.SumAsync(t => t.TransactionAmount, cancellationToken), - }; - response.ComparisonAverageSalesValue = - SafeDivide(response.ComparisonSalesValue, response.ComparisonSalesCount); - response.TodaysAverageSalesValue = - SafeDivide(response.TodaysSalesValue, response.TodaysSalesCount); - - return response; - } - - public async Task GetProductPerformance(Guid estateId, DateTime comparisonDate, List productReportingIds, CancellationToken cancellationToken) - { - using ResolvedDbContext? resolvedContext = this.Resolver.Resolve(EstateManagementDatabaseName, estateId.ToString()); - await using EstateManagementContext context = resolvedContext.Context; - - // First we need to get a value of todays sales - IQueryable todaysSalesQuery = BuildTodaySalesQuery(context); - IQueryable comparisonSalesQuery = BuildComparisonSalesQuery(context, comparisonDate); - - todaysSalesQuery = todaysSalesQuery.ApplyProductFilter(productReportingIds); - comparisonSalesQuery = comparisonSalesQuery.ApplyProductFilter(productReportingIds); - - TodaysSales response = new TodaysSales - { - ComparisonSalesCount = await comparisonSalesQuery.CountAsync(cancellationToken), - ComparisonSalesValue = await comparisonSalesQuery.SumAsync(t => t.TransactionAmount, cancellationToken), - TodaysSalesCount = await todaysSalesQuery.CountAsync(cancellationToken), - TodaysSalesValue = await todaysSalesQuery.SumAsync(t => t.TransactionAmount, cancellationToken), - }; - response.ComparisonAverageSalesValue = - SafeDivide(response.ComparisonSalesValue, response.ComparisonSalesCount); - response.TodaysAverageSalesValue = - SafeDivide(response.TodaysSalesValue, response.TodaysSalesCount); - - return response; - } - - public async Task GetOperatorPerformance(Guid estateId, DateTime comparisonDate, List operatorReportingIds, CancellationToken cancellationToken) - { - using ResolvedDbContext? resolvedContext = this.Resolver.Resolve(EstateManagementDatabaseName, estateId.ToString()); - await using EstateManagementContext context = resolvedContext.Context; - - // First we need to get a value of todays sales - IQueryable todaysSalesQuery = BuildTodaySalesQuery(context); - IQueryable comparisonSalesQuery = BuildComparisonSalesQuery(context, comparisonDate); - - todaysSalesQuery = todaysSalesQuery.ApplyOperatorFilter(operatorReportingIds); - comparisonSalesQuery = comparisonSalesQuery.ApplyOperatorFilter(operatorReportingIds); - - TodaysSales response = new TodaysSales - { - ComparisonSalesCount = await comparisonSalesQuery.CountAsync(cancellationToken), - ComparisonSalesValue = await comparisonSalesQuery.SumAsync(t => t.TransactionAmount, cancellationToken), - TodaysSalesCount = await todaysSalesQuery.CountAsync(cancellationToken), - TodaysSalesValue = await todaysSalesQuery.SumAsync(t => t.TransactionAmount, cancellationToken), - }; - response.ComparisonAverageSalesValue = - SafeDivide(response.ComparisonSalesValue, response.ComparisonSalesCount); - response.TodaysAverageSalesValue = - SafeDivide(response.TodaysSalesValue, response.TodaysSalesCount); - - return response; - } - - public async Task> GetTopBottomData(Guid estateId, - TopBottom direction, - Int32 resultCount, - Dimension dimension, - CancellationToken cancellationToken) { - using ResolvedDbContext? resolvedContext = this.Resolver.Resolve(EstateManagementDatabaseName, estateId.ToString()); - await using EstateManagementContext context = resolvedContext.Context; - - IQueryable mainQuery = BuildTodaySalesQuery(context); - - IQueryable topBottomData = dimension switch - { - Dimension.Merchant => mainQuery.ApplyMerchantGrouping(context), - Dimension.Operator => mainQuery.ApplyOperatorGrouping(context), - Dimension.Product => mainQuery.ApplyProductGrouping(context), - }; - - topBottomData = direction switch { - TopBottom.Top => topBottomData.OrderByDescending(g => g.SalesValue), - _ => topBottomData.OrderBy(g => g.SalesValue) - }; - - return await topBottomData.Take(resultCount).ToListAsync(cancellationToken); - } - - } - - public class DatabaseProjections { - public class FeeTransactionProjection - { - public MerchantSettlementFee Fee { get; set; } - public Transaction Txn { get; set; } - } - - public class TransactionSearchProjection { - public Transaction Transaction { get; set; } - public Operator Operator { get; set; } - public Merchant Merchant { get; set; } - public ContractProduct Product { get; set; } - } - - public class TopBottomData - { - public String DimensionName { get; set; } - public Decimal SalesValue { get; set; } - } - } -} From 5f358a89748ffdcefff742e669e5c40a429e6d5b Mon Sep 17 00:00:00 2001 From: Stuart Ferguson Date: Fri, 17 Oct 2025 18:14:23 +0100 Subject: [PATCH 2/7] small further refactor --- .../DatabaseProjections.cs | 12 +++ .../ReportingManager.cs | 88 ++++++++++++++++--- 2 files changed, 88 insertions(+), 12 deletions(-) diff --git a/EstateReportingAPI.BusinessLogic/DatabaseProjections.cs b/EstateReportingAPI.BusinessLogic/DatabaseProjections.cs index 73fb93a..7d6fd5e 100644 --- a/EstateReportingAPI.BusinessLogic/DatabaseProjections.cs +++ b/EstateReportingAPI.BusinessLogic/DatabaseProjections.cs @@ -16,6 +16,18 @@ namespace EstateReportingAPI.BusinessLogic { public class DatabaseProjections { + public class SettlementGroupProjection + { + public decimal SettledValue { get; set; } + public int SettledCount { get; set; } + + public decimal UnSettledValue { get; set; } + public int UnSettledCount { get; set; } + } + + + + public class FeeTransactionProjection { public MerchantSettlementFee Fee { get; set; } diff --git a/EstateReportingAPI.BusinessLogic/ReportingManager.cs b/EstateReportingAPI.BusinessLogic/ReportingManager.cs index 0c5a481..dd7b1d3 100644 --- a/EstateReportingAPI.BusinessLogic/ReportingManager.cs +++ b/EstateReportingAPI.BusinessLogic/ReportingManager.cs @@ -539,25 +539,89 @@ public async Task GetTodaysSettlement(Guid estateId, todaySettlementData = todaySettlementData.ApplyMerchantFilter(merchantReportingId).ApplyOperatorFilter(operatorReportingId); comparisonSettlementData = comparisonSettlementData.ApplyMerchantFilter(merchantReportingId).ApplyOperatorFilter(operatorReportingId); - var todaySettlement = await (from f in todaySettlementData group f by f.Fee.IsSettled into grouped select new { IsSettled = grouped.Key, CalculatedValueSum = grouped.Sum(item => item.Fee.CalculatedValue), CalculatedValueCount = grouped.Count() }).ToListAsync(cancellationToken); - - var comparisonSettlement = await (from f in comparisonSettlementData group f by f.Fee.IsSettled into grouped select new { IsSettled = grouped.Key, CalculatedValueSum = grouped.Sum(item => item.Fee.CalculatedValue), CalculatedValueCount = grouped.Count() }).ToListAsync(cancellationToken); - + DatabaseProjections.SettlementGroupProjection todaySettlement = await this.GetSettlementSummary(todaySettlementData, cancellationToken); + DatabaseProjections.SettlementGroupProjection comparisonSettlement = await this.GetSettlementSummary(comparisonSettlementData, cancellationToken); TodaysSettlement response = new() { - ComparisonSettlementCount = comparisonSettlement.FirstOrDefault(x => x.IsSettled)?.CalculatedValueCount ?? 0, - ComparisonSettlementValue = comparisonSettlement.FirstOrDefault(x => x.IsSettled)?.CalculatedValueSum ?? 0, - ComparisonPendingSettlementCount = comparisonSettlement.FirstOrDefault(x => x.IsSettled == false)?.CalculatedValueCount ?? 0, - ComparisonPendingSettlementValue = comparisonSettlement.FirstOrDefault(x => x.IsSettled == false)?.CalculatedValueSum ?? 0, - TodaysSettlementCount = todaySettlement.FirstOrDefault(x => x.IsSettled)?.CalculatedValueCount ?? 0, - TodaysSettlementValue = todaySettlement.FirstOrDefault(x => x.IsSettled)?.CalculatedValueSum ?? 0, - TodaysPendingSettlementCount = todaySettlement.FirstOrDefault(x => x.IsSettled == false)?.CalculatedValueCount ?? 0, - TodaysPendingSettlementValue = todaySettlement.FirstOrDefault(x => x.IsSettled == false)?.CalculatedValueSum ?? 0 + ComparisonSettlementCount = comparisonSettlement.SettledCount, + ComparisonSettlementValue = comparisonSettlement.SettledValue, + ComparisonPendingSettlementCount = comparisonSettlement.UnSettledCount, + ComparisonPendingSettlementValue = comparisonSettlement.UnSettledValue, + TodaysSettlementCount = todaySettlement.SettledCount, + TodaysSettlementValue = todaySettlement.SettledValue, + TodaysPendingSettlementCount = todaySettlement.UnSettledCount, + TodaysPendingSettlementValue = todaySettlement.UnSettledValue }; return response; } + private async Task GetSettlementSummary( + IQueryable query, + CancellationToken cancellationToken) + { + // Get the settleed fees summary + var settledFees = await (from f in query + where f.Fee.IsSettled + group f by f.Fee.IsSettled + into grouped + select new + { + Value = grouped.Sum(g => g.Fee.CalculatedValue), + Count = grouped.Count() + }).SingleOrDefaultAsync(cancellationToken); + + var unSettledFees = await (from f in query + where f.Fee.IsSettled + group f by f.Fee.IsSettled == false + into grouped + select new + { + Value = grouped.Sum(g => g.Fee.CalculatedValue), + Count = grouped.Count() + }).SingleOrDefaultAsync(cancellationToken); + + return new DatabaseProjections.SettlementGroupProjection + { + SettledCount = settledFees?.Count ?? 0, + SettledValue = settledFees?.Value ?? 0, + UnSettledCount = unSettledFees? .Count ?? 0, + UnSettledValue = unSettledFees?.Value ?? 0 + }; + } + + private async Task GetSettlementSummary( + IQueryable query, + CancellationToken cancellationToken) { + + // Get the settleed fees summary + var settledFees = await (from f in query + where f.Fee.IsSettled + group f by f.Fee.IsSettled + into grouped + select new { + Value = grouped.Sum(g => g.Fee.CalculatedValue), + Count= grouped.Count() + }).SingleOrDefaultAsync(cancellationToken); + + var unSettledFees = await (from f in query + where f.Fee.IsSettled + group f by f.Fee.IsSettled == false + into grouped + select new + { + Value = grouped.Sum(g => g.Fee.CalculatedValue), + Count = grouped.Count() + }).SingleOrDefaultAsync(cancellationToken); + + return new DatabaseProjections.SettlementGroupProjection { + SettledCount = settledFees.Count, + SettledValue = settledFees.Value, + UnSettledCount = unSettledFees.Count, + UnSettledValue = unSettledFees.Value + }; + } + public async Task> GetOperators(Guid estateId, CancellationToken cancellationToken) { using ResolvedDbContext? resolvedContext = this.Resolver.Resolve(EstateManagementDatabaseName, estateId.ToString()); From f3fa1f98d78205ca05f5aacf9531aa60e98c0963 Mon Sep 17 00:00:00 2001 From: Stuart Ferguson Date: Fri, 17 Oct 2025 20:20:07 +0100 Subject: [PATCH 3/7] :| --- EstateReportingAPI.BusinessLogic/ReportingManager.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/EstateReportingAPI.BusinessLogic/ReportingManager.cs b/EstateReportingAPI.BusinessLogic/ReportingManager.cs index dd7b1d3..e085fa2 100644 --- a/EstateReportingAPI.BusinessLogic/ReportingManager.cs +++ b/EstateReportingAPI.BusinessLogic/ReportingManager.cs @@ -572,8 +572,8 @@ into grouped }).SingleOrDefaultAsync(cancellationToken); var unSettledFees = await (from f in query - where f.Fee.IsSettled - group f by f.Fee.IsSettled == false + where f.Fee.IsSettled == false + group f by f.Fee.IsSettled into grouped select new { @@ -581,7 +581,7 @@ into grouped Count = grouped.Count() }).SingleOrDefaultAsync(cancellationToken); - return new DatabaseProjections.SettlementGroupProjection + return new DatabaseProjections.SettlementGroupProjection() { SettledCount = settledFees?.Count ?? 0, SettledValue = settledFees?.Value ?? 0, @@ -605,8 +605,8 @@ into grouped }).SingleOrDefaultAsync(cancellationToken); var unSettledFees = await (from f in query - where f.Fee.IsSettled - group f by f.Fee.IsSettled == false + where f.Fee.IsSettled == false + group f by f.Fee.IsSettled into grouped select new { From 71e24aeaf4b30a157c23c0db433ea568a0c1d2af Mon Sep 17 00:00:00 2001 From: Stuart Ferguson Date: Fri, 17 Oct 2025 20:26:41 +0100 Subject: [PATCH 4/7] simplify method --- .../ReportingManager.cs | 73 +++++-------------- 1 file changed, 20 insertions(+), 53 deletions(-) diff --git a/EstateReportingAPI.BusinessLogic/ReportingManager.cs b/EstateReportingAPI.BusinessLogic/ReportingManager.cs index e085fa2..4f70957 100644 --- a/EstateReportingAPI.BusinessLogic/ReportingManager.cs +++ b/EstateReportingAPI.BusinessLogic/ReportingManager.cs @@ -556,38 +556,13 @@ public async Task GetTodaysSettlement(Guid estateId, return response; } - private async Task GetSettlementSummary( - IQueryable query, - CancellationToken cancellationToken) - { + private async Task GetSettlementSummary(IQueryable query, + CancellationToken cancellationToken) { // Get the settleed fees summary - var settledFees = await (from f in query - where f.Fee.IsSettled - group f by f.Fee.IsSettled - into grouped - select new - { - Value = grouped.Sum(g => g.Fee.CalculatedValue), - Count = grouped.Count() - }).SingleOrDefaultAsync(cancellationToken); - - var unSettledFees = await (from f in query - where f.Fee.IsSettled == false - group f by f.Fee.IsSettled - into grouped - select new - { - Value = grouped.Sum(g => g.Fee.CalculatedValue), - Count = grouped.Count() - }).SingleOrDefaultAsync(cancellationToken); - - return new DatabaseProjections.SettlementGroupProjection() - { - SettledCount = settledFees?.Count ?? 0, - SettledValue = settledFees?.Value ?? 0, - UnSettledCount = unSettledFees? .Count ?? 0, - UnSettledValue = unSettledFees?.Value ?? 0 - }; + var summary = await query.GroupBy(_ => 1) // group everything together + .Select(g => new { SettledCount = g.Count(x => x.Fee.IsSettled), SettledValue = g.Where(x => x.Fee.IsSettled).Sum(x => x.Fee.CalculatedValue), UnSettledCount = g.Count(x => !x.Fee.IsSettled), UnSettledValue = g.Where(x => !x.Fee.IsSettled).Sum(x => x.Fee.CalculatedValue) }).SingleOrDefaultAsync(cancellationToken); + + return new DatabaseProjections.SettlementGroupProjection { SettledCount = summary?.SettledCount ?? 0, SettledValue = summary?.SettledValue ?? 0, UnSettledCount = summary?.UnSettledCount ?? 0, UnSettledValue = summary?.UnSettledValue ?? 0 }; } private async Task GetSettlementSummary( @@ -595,31 +570,23 @@ into grouped CancellationToken cancellationToken) { // Get the settleed fees summary - var settledFees = await (from f in query - where f.Fee.IsSettled - group f by f.Fee.IsSettled - into grouped - select new { - Value = grouped.Sum(g => g.Fee.CalculatedValue), - Count= grouped.Count() - }).SingleOrDefaultAsync(cancellationToken); - - var unSettledFees = await (from f in query - where f.Fee.IsSettled == false - group f by f.Fee.IsSettled - into grouped - select new + var summary = await query + .GroupBy(_ => 1) // group everything together + .Select(g => new { - Value = grouped.Sum(g => g.Fee.CalculatedValue), - Count = grouped.Count() - }).SingleOrDefaultAsync(cancellationToken); + SettledCount = g.Count(x => x.Fee.IsSettled), + SettledValue = g.Where(x => x.Fee.IsSettled).Sum(x => x.Fee.CalculatedValue), + UnSettledCount = g.Count(x => !x.Fee.IsSettled), + UnSettledValue = g.Where(x => !x.Fee.IsSettled).Sum(x => x.Fee.CalculatedValue) + }) + .SingleOrDefaultAsync(cancellationToken); return new DatabaseProjections.SettlementGroupProjection { - SettledCount = settledFees.Count, - SettledValue = settledFees.Value, - UnSettledCount = unSettledFees.Count, - UnSettledValue = unSettledFees.Value - }; + SettledCount = summary?.SettledCount ?? 0, + SettledValue = summary?.SettledValue ?? 0, + UnSettledCount = summary?.UnSettledCount ?? 0, + UnSettledValue = summary?.UnSettledValue ?? 0 + }; } public async Task> GetOperators(Guid estateId, From 5c6f291dbd4caba9e6bbc2a3ab1b6ca6cd0ce3b6 Mon Sep 17 00:00:00 2001 From: Stuart Ferguson Date: Fri, 17 Oct 2025 20:39:02 +0100 Subject: [PATCH 5/7] more tweaking --- .../IReportingManager.cs | 2 +- .../ReportingManager.cs | 44 ++++++++++++------- .../ReportingManagerExtensions.cs | 18 ++++---- 3 files changed, 37 insertions(+), 27 deletions(-) diff --git a/EstateReportingAPI.BusinessLogic/IReportingManager.cs b/EstateReportingAPI.BusinessLogic/IReportingManager.cs index 8b76c59..7a51dee 100644 --- a/EstateReportingAPI.BusinessLogic/IReportingManager.cs +++ b/EstateReportingAPI.BusinessLogic/IReportingManager.cs @@ -20,7 +20,7 @@ public interface IReportingManager{ Task> GetTodaysSalesCountByHour(Guid estateId, Int32 merchantReportingId, Int32 operatorReportingId, DateTime comparisonDate, CancellationToken cancellationToken); Task> GetTodaysSalesValueByHour(Guid estateId, Int32 merchantReportingId, Int32 operatorReportingId, DateTime comparisonDate, CancellationToken cancellationToken); Task GetTodaysSettlement(Guid estateId, Int32 merchantReportingId, Int32 operatorReportingId, DateTime comparisonDate, CancellationToken cancellationToken); - Task> GetTopBottomData(Guid estateId, TopBottom direction, Int32 resultCount, Dimension dimension, CancellationToken cancellationToken); + Task> GetTopBottomData(Guid estateId, TopBottom direction, Int32 resultCount, Dimension dimension, CancellationToken cancellationToken); Task GetMerchantPerformance(Guid estateId, DateTime comparisonDate, List merchantReportingIds, CancellationToken cancellationToken); Task GetProductPerformance(Guid estateId, DateTime comparisonDate, List productReportingIds, CancellationToken cancellationToken); diff --git a/EstateReportingAPI.BusinessLogic/ReportingManager.cs b/EstateReportingAPI.BusinessLogic/ReportingManager.cs index 4f70957..d181ff2 100644 --- a/EstateReportingAPI.BusinessLogic/ReportingManager.cs +++ b/EstateReportingAPI.BusinessLogic/ReportingManager.cs @@ -10,6 +10,7 @@ namespace EstateReportingAPI.BusinessLogic; using System; using System.Linq; using System.Threading; +using static EstateReportingAPI.BusinessLogic.DatabaseProjections; using Calendar = Models.Calendar; using Merchant = Models.Merchant; using Operator = Models.Operator; @@ -501,17 +502,17 @@ public async Task GetOperatorPerformance(Guid estateId, return response; } - public async Task> GetTopBottomData(Guid estateId, - TopBottom direction, - Int32 resultCount, - Dimension dimension, - CancellationToken cancellationToken) { + public async Task> GetTopBottomData(Guid estateId, + TopBottom direction, + Int32 resultCount, + Dimension dimension, + CancellationToken cancellationToken) { using ResolvedDbContext? resolvedContext = this.Resolver.Resolve(EstateManagementDatabaseName, estateId.ToString()); await using EstateManagementContext context = resolvedContext.Context; IQueryable mainQuery = this.BuildTodaySalesQuery(context); - IQueryable topBottomData = dimension switch { + IQueryable topBottomData = dimension switch { Dimension.Merchant => mainQuery.ApplyMerchantGrouping(context), Dimension.Operator => mainQuery.ApplyOperatorGrouping(context), Dimension.Product => mainQuery.ApplyProductGrouping(context) @@ -522,7 +523,11 @@ public async Task> GetTopBottomData(Guid estateId, _ => topBottomData.OrderBy(g => g.SalesValue) }; - return await topBottomData.Take(resultCount).ToListAsync(cancellationToken); + List queryResult = await topBottomData.Take(resultCount).ToListAsync(cancellationToken); + + List results = new(); + queryResult.ForEach(qr => results.Add(new Models.TopBottomData { DimensionName = qr.DimensionName, SalesValue = qr.SalesValue })); + return results; } public async Task GetTodaysSettlement(Guid estateId, @@ -565,21 +570,26 @@ public async Task GetTodaysSettlement(Guid estateId, return new DatabaseProjections.SettlementGroupProjection { SettledCount = summary?.SettledCount ?? 0, SettledValue = summary?.SettledValue ?? 0, UnSettledCount = summary?.UnSettledCount ?? 0, UnSettledValue = summary?.UnSettledValue ?? 0 }; } - private async Task GetSettlementSummary( - IQueryable query, - CancellationToken cancellationToken) { - - // Get the settleed fees summary - var summary = await query - .GroupBy(_ => 1) // group everything together - .Select(g => new + private static IQueryable BuildSettlementSummaryQuery( + IQueryable query) + { + return query + .GroupBy(_ => 1) + .Select(g => new SettlementGroupProjection { SettledCount = g.Count(x => x.Fee.IsSettled), SettledValue = g.Where(x => x.Fee.IsSettled).Sum(x => x.Fee.CalculatedValue), UnSettledCount = g.Count(x => !x.Fee.IsSettled), UnSettledValue = g.Where(x => !x.Fee.IsSettled).Sum(x => x.Fee.CalculatedValue) - }) - .SingleOrDefaultAsync(cancellationToken); + }); + } + + private async Task GetSettlementSummary( + IQueryable query, + CancellationToken cancellationToken) { + + // Get the settleed fees summary + SettlementGroupProjection? summary = await BuildSettlementSummaryQuery(query).SingleOrDefaultAsync(cancellationToken); return new DatabaseProjections.SettlementGroupProjection { SettledCount = summary?.SettledCount ?? 0, diff --git a/EstateReportingAPI.BusinessLogic/ReportingManagerExtensions.cs b/EstateReportingAPI.BusinessLogic/ReportingManagerExtensions.cs index 8813533..e23bfaf 100644 --- a/EstateReportingAPI.BusinessLogic/ReportingManagerExtensions.cs +++ b/EstateReportingAPI.BusinessLogic/ReportingManagerExtensions.cs @@ -169,8 +169,8 @@ join op in context.Operators on c.OperatorId equals op.OperatorId }; } - public static IQueryable ApplyMerchantGrouping(this IQueryable transactions, - EstateManagementContext context) + public static IQueryable ApplyMerchantGrouping(this IQueryable transactions, + EstateManagementContext context) { return transactions.Join(context.Merchants, t => t.MerchantReportingId, @@ -181,15 +181,15 @@ public static IQueryable ApplyMerchantGrouping(this IQueryable joined.Merchant.Name) - .Select(g => new TopBottomData + .Select(g => new DatabaseProjections.TopBottomData { DimensionName = g.Key, SalesValue = g.Sum(t => t.Transaction.TransactionAmount) }); } - public static IQueryable ApplyOperatorGrouping(this IQueryable transactions, - EstateManagementContext context) + public static IQueryable ApplyOperatorGrouping(this IQueryable transactions, + EstateManagementContext context) { return transactions.Join(context.Operators, t => t.OperatorReportingId, @@ -200,15 +200,15 @@ public static IQueryable ApplyOperatorGrouping(this IQueryable joined.Operator.Name) - .Select(g => new TopBottomData + .Select(g => new DatabaseProjections.TopBottomData { DimensionName = g.Key, SalesValue = g.Sum(t => t.Transaction.TransactionAmount) }); } - public static IQueryable ApplyProductGrouping(this IQueryable transactions, - EstateManagementContext context) + public static IQueryable ApplyProductGrouping(this IQueryable transactions, + EstateManagementContext context) { return transactions .Join(context.ContractProducts, @@ -220,7 +220,7 @@ public static IQueryable ApplyProductGrouping(this IQueryable joined.ContractProduct.ProductName) - .Select(g => new TopBottomData + .Select(g => new DatabaseProjections.TopBottomData { DimensionName = g.Key, SalesValue = g.Sum(t => t.Transaction.TransactionAmount) From e55ed67c3b988ee5675eb57a605517e49c3c3fbb Mon Sep 17 00:00:00 2001 From: Stuart Ferguson Date: Fri, 17 Oct 2025 20:45:27 +0100 Subject: [PATCH 6/7] :| --- .../ReportingManager.cs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/EstateReportingAPI.BusinessLogic/ReportingManager.cs b/EstateReportingAPI.BusinessLogic/ReportingManager.cs index d181ff2..6cf151f 100644 --- a/EstateReportingAPI.BusinessLogic/ReportingManager.cs +++ b/EstateReportingAPI.BusinessLogic/ReportingManager.cs @@ -564,12 +564,25 @@ public async Task GetTodaysSettlement(Guid estateId, private async Task GetSettlementSummary(IQueryable query, CancellationToken cancellationToken) { // Get the settleed fees summary - var summary = await query.GroupBy(_ => 1) // group everything together - .Select(g => new { SettledCount = g.Count(x => x.Fee.IsSettled), SettledValue = g.Where(x => x.Fee.IsSettled).Sum(x => x.Fee.CalculatedValue), UnSettledCount = g.Count(x => !x.Fee.IsSettled), UnSettledValue = g.Where(x => !x.Fee.IsSettled).Sum(x => x.Fee.CalculatedValue) }).SingleOrDefaultAsync(cancellationToken); + SettlementGroupProjection? summary = await BuildSettlementSummaryQuery(query).SingleOrDefaultAsync(cancellationToken); return new DatabaseProjections.SettlementGroupProjection { SettledCount = summary?.SettledCount ?? 0, SettledValue = summary?.SettledValue ?? 0, UnSettledCount = summary?.UnSettledCount ?? 0, UnSettledValue = summary?.UnSettledValue ?? 0 }; } + private static IQueryable BuildSettlementSummaryQuery( + IQueryable query) + { + return query + .GroupBy(_ => 1) + .Select(g => new SettlementGroupProjection + { + SettledCount = g.Count(x => x.Fee.IsSettled), + SettledValue = g.Where(x => x.Fee.IsSettled).Sum(x => x.Fee.CalculatedValue), + UnSettledCount = g.Count(x => !x.Fee.IsSettled), + UnSettledValue = g.Where(x => !x.Fee.IsSettled).Sum(x => x.Fee.CalculatedValue) + }); + } + private static IQueryable BuildSettlementSummaryQuery( IQueryable query) { From 39af28083174302cc9a38e784e468465fea78ee5 Mon Sep 17 00:00:00 2001 From: Stuart Ferguson Date: Fri, 17 Oct 2025 22:05:44 +0100 Subject: [PATCH 7/7] :| --- .../ReportingManager.cs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/EstateReportingAPI.BusinessLogic/ReportingManager.cs b/EstateReportingAPI.BusinessLogic/ReportingManager.cs index 6cf151f..4c76ffd 100644 --- a/EstateReportingAPI.BusinessLogic/ReportingManager.cs +++ b/EstateReportingAPI.BusinessLogic/ReportingManager.cs @@ -564,9 +564,12 @@ public async Task GetTodaysSettlement(Guid estateId, private async Task GetSettlementSummary(IQueryable query, CancellationToken cancellationToken) { // Get the settleed fees summary - SettlementGroupProjection? summary = await BuildSettlementSummaryQuery(query).SingleOrDefaultAsync(cancellationToken); + SettlementGroupProjection summary = await BuildSettlementSummaryQuery(query).SingleOrDefaultAsync(cancellationToken); - return new DatabaseProjections.SettlementGroupProjection { SettledCount = summary?.SettledCount ?? 0, SettledValue = summary?.SettledValue ?? 0, UnSettledCount = summary?.UnSettledCount ?? 0, UnSettledValue = summary?.UnSettledValue ?? 0 }; + return new DatabaseProjections.SettlementGroupProjection { SettledCount = summary.SettledCount, + SettledValue = summary.SettledValue, + UnSettledCount = summary.UnSettledCount, + UnSettledValue = summary.UnSettledValue }; } private static IQueryable BuildSettlementSummaryQuery( @@ -602,13 +605,13 @@ private static IQueryable BuildSettlementSummaryQuery CancellationToken cancellationToken) { // Get the settleed fees summary - SettlementGroupProjection? summary = await BuildSettlementSummaryQuery(query).SingleOrDefaultAsync(cancellationToken); + SettlementGroupProjection summary = await BuildSettlementSummaryQuery(query).SingleOrDefaultAsync(cancellationToken); return new DatabaseProjections.SettlementGroupProjection { - SettledCount = summary?.SettledCount ?? 0, - SettledValue = summary?.SettledValue ?? 0, - UnSettledCount = summary?.UnSettledCount ?? 0, - UnSettledValue = summary?.UnSettledValue ?? 0 + SettledCount = summary.SettledCount, + SettledValue = summary.SettledValue, + UnSettledCount = summary.UnSettledCount, + UnSettledValue = summary.UnSettledValue }; }