Files
MoneyMap/MoneyMap/Services/TransactionStatisticsService.cs
AJ 77cab2595f Feature: add entity management and statistics services
Add four new services to extract business logic from PageModels:

- AccountService: Account retrieval, stats, and deletion with validation
- CardService: Card retrieval, stats, and deletion with validation
- ReferenceDataService: Centralized reference data for dropdowns (categories, merchants, cards, accounts)
- TransactionStatisticsService: Transaction statistics and aggregate calculations

All services follow the established service layer pattern with interfaces for DI and improved testability. Includes comprehensive DTOs for stats and validation results.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-25 23:07:57 -04:00

109 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)
{
var allFilteredTransactions = await query.ToListAsync();
return new TransactionStats
{
Count = allFilteredTransactions.Count,
TotalDebits = allFilteredTransactions.Where(t => t.Amount < 0).Sum(t => t.Amount),
TotalCredits = allFilteredTransactions.Where(t => t.Amount > 0).Sum(t => t.Amount),
NetAmount = allFilteredTransactions.Sum(t => t.Amount)
};
}
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)
{
var cards = await _db.Cards
.Where(c => c.AccountId == accountId)
.OrderBy(c => c.Owner)
.ThenBy(c => c.Last4)
.ToListAsync();
var cardStats = new List<CardStats>();
foreach (var card in cards)
{
var transactionCount = await _db.Transactions.CountAsync(t => t.CardId == card.Id);
cardStats.Add(new CardStats
{
Card = card,
TransactionCount = transactionCount
});
}
return cardStats;
}
}
// 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; }
}