Files
MoneyMap/MoneyMap.Core/Services/TransactionStatisticsService.cs
T
2026-04-20 18:18:20 -04:00

106 lines
3.1 KiB
C#

using Microsoft.EntityFrameworkCore;
using MoneyMap.Data;
using MoneyMap.Models;
namespace MoneyMap.Services;
/// <summary>
/// Service for calculating transaction statistics and aggregates.
/// </summary>
public interface ITransactionStatisticsService
{
/// <summary>
/// Calculates statistics for a filtered set of transactions.
/// </summary>
Task<TransactionStats> CalculateStatsAsync(IQueryable<Transaction> query);
/// <summary>
/// Gets categorization statistics for the entire database.
/// </summary>
Task<CategorizationStats> GetCategorizationStatsAsync();
/// <summary>
/// Gets card statistics for a specific account.
/// </summary>
Task<List<CardStats>> GetCardStatsForAccountAsync(int accountId);
}
public class TransactionStatisticsService : ITransactionStatisticsService
{
private readonly MoneyMapContext _db;
public TransactionStatisticsService(MoneyMapContext db)
{
_db = db;
}
public async Task<TransactionStats> CalculateStatsAsync(IQueryable<Transaction> query)
{
// Calculate stats at database level instead of loading all transactions into memory
var stats = await query
.GroupBy(_ => 1) // Group all into one group to aggregate
.Select(g => new TransactionStats
{
Count = g.Count(),
TotalDebits = g.Where(t => t.Amount < 0).Sum(t => t.Amount),
TotalCredits = g.Where(t => t.Amount > 0).Sum(t => t.Amount),
NetAmount = g.Sum(t => t.Amount)
})
.FirstOrDefaultAsync();
return stats ?? new TransactionStats();
}
public async Task<CategorizationStats> GetCategorizationStatsAsync()
{
var totalTransactions = await _db.Transactions.CountAsync();
var uncategorized = await _db.Transactions
.CountAsync(t => string.IsNullOrWhiteSpace(t.Category));
var categorized = totalTransactions - uncategorized;
return new CategorizationStats
{
TotalTransactions = totalTransactions,
Categorized = categorized,
Uncategorized = uncategorized
};
}
public async Task<List<CardStats>> GetCardStatsForAccountAsync(int accountId)
{
// Single query with projection to avoid N+1
return await _db.Cards
.Where(c => c.AccountId == accountId)
.OrderBy(c => c.Owner)
.ThenBy(c => c.Last4)
.Select(c => new CardStats
{
Card = c,
TransactionCount = c.Transactions.Count
})
.ToListAsync();
}
}
// DTOs
public class TransactionStats
{
public int Count { get; set; }
public decimal TotalDebits { get; set; }
public decimal TotalCredits { get; set; }
public decimal NetAmount { get; set; }
}
public class CategorizationStats
{
public int TotalTransactions { get; set; }
public int Categorized { get; set; }
public int Uncategorized { get; set; }
}
public class CardStats
{
public Card Card { get; set; } = null!;
public int TransactionCount { get; set; }
}