refactor: move services and AITools to MoneyMap.Core
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,268 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MoneyMap.Data;
|
||||
using MoneyMap.Models.Dashboard;
|
||||
|
||||
namespace MoneyMap.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Service for retrieving dashboard data.
|
||||
/// </summary>
|
||||
public interface IDashboardService
|
||||
{
|
||||
Task<DashboardData> GetDashboardDataAsync(int topCategoriesCount = 8, int recentTransactionsCount = 20);
|
||||
}
|
||||
|
||||
public class DashboardService : IDashboardService
|
||||
{
|
||||
private readonly IDashboardStatsCalculator _statsCalculator;
|
||||
private readonly ITopCategoriesProvider _topCategoriesProvider;
|
||||
private readonly IRecentTransactionsProvider _recentTransactionsProvider;
|
||||
private readonly ISpendTrendsProvider _spendTrendsProvider;
|
||||
|
||||
public DashboardService(
|
||||
IDashboardStatsCalculator statsCalculator,
|
||||
ITopCategoriesProvider topCategoriesProvider,
|
||||
IRecentTransactionsProvider recentTransactionsProvider,
|
||||
ISpendTrendsProvider spendTrendsProvider)
|
||||
{
|
||||
_statsCalculator = statsCalculator;
|
||||
_topCategoriesProvider = topCategoriesProvider;
|
||||
_recentTransactionsProvider = recentTransactionsProvider;
|
||||
_spendTrendsProvider = spendTrendsProvider;
|
||||
}
|
||||
|
||||
public async Task<DashboardData> GetDashboardDataAsync(int topCategoriesCount = 8, int recentTransactionsCount = 20)
|
||||
{
|
||||
var stats = await _statsCalculator.CalculateAsync();
|
||||
var topCategories = await _topCategoriesProvider.GetTopCategoriesAsync(topCategoriesCount);
|
||||
var recent = await _recentTransactionsProvider.GetRecentTransactionsAsync(recentTransactionsCount);
|
||||
var trends = await _spendTrendsProvider.GetDailyTrendsAsync(30);
|
||||
|
||||
return new DashboardData
|
||||
{
|
||||
Stats = stats,
|
||||
TopCategories = topCategories,
|
||||
RecentTransactions = recent,
|
||||
Trends = trends
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates dashboard statistics.
|
||||
/// </summary>
|
||||
public interface IDashboardStatsCalculator
|
||||
{
|
||||
Task<DashboardStats> CalculateAsync();
|
||||
}
|
||||
|
||||
public class DashboardStatsCalculator : IDashboardStatsCalculator
|
||||
{
|
||||
private readonly MoneyMapContext _db;
|
||||
|
||||
public DashboardStatsCalculator(MoneyMapContext db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
public async Task<DashboardStats> CalculateAsync()
|
||||
{
|
||||
var transactionStats = await GetTransactionStatsAsync();
|
||||
var receiptsCount = await _db.Receipts.CountAsync();
|
||||
var cardsCount = await _db.Cards.CountAsync();
|
||||
|
||||
return new DashboardStats(
|
||||
transactionStats.Total,
|
||||
transactionStats.Credits,
|
||||
transactionStats.Debits,
|
||||
transactionStats.Uncategorized,
|
||||
receiptsCount,
|
||||
cardsCount
|
||||
);
|
||||
}
|
||||
|
||||
private async Task<TransactionStatsInternal> GetTransactionStatsAsync()
|
||||
{
|
||||
var stats = await _db.Transactions
|
||||
.GroupBy(_ => 1)
|
||||
.Select(g => new TransactionStatsInternal
|
||||
{
|
||||
Total = g.Count(),
|
||||
Credits = g.Count(t => t.Amount > 0),
|
||||
Debits = g.Count(t => t.Amount < 0),
|
||||
Uncategorized = g.Count(t => t.Category == null || t.Category == "")
|
||||
})
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
return stats ?? new TransactionStatsInternal();
|
||||
}
|
||||
|
||||
private class TransactionStatsInternal
|
||||
{
|
||||
public int Total { get; set; }
|
||||
public int Credits { get; set; }
|
||||
public int Debits { get; set; }
|
||||
public int Uncategorized { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides top spending categories.
|
||||
/// </summary>
|
||||
public interface ITopCategoriesProvider
|
||||
{
|
||||
Task<List<TopCategoryRow>> GetTopCategoriesAsync(int count = 8, int lastDays = 90);
|
||||
}
|
||||
|
||||
public class TopCategoriesProvider : ITopCategoriesProvider
|
||||
{
|
||||
private readonly MoneyMapContext _db;
|
||||
|
||||
public TopCategoriesProvider(MoneyMapContext db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
public async Task<List<TopCategoryRow>> GetTopCategoriesAsync(int count = 8, int lastDays = 90)
|
||||
{
|
||||
var since = DateTime.UtcNow.Date.AddDays(-lastDays);
|
||||
|
||||
var expenseTransactions = await _db.Transactions
|
||||
.Where(t => t.Date >= since && t.Amount < 0)
|
||||
.ExcludeTransfers()
|
||||
.ToListAsync();
|
||||
|
||||
var totalSpend = expenseTransactions.Sum(t => -t.Amount);
|
||||
|
||||
var topCategories = expenseTransactions
|
||||
.GroupBy(t => t.Category ?? "")
|
||||
.Select(g => new TopCategoryRow
|
||||
{
|
||||
Category = g.Key,
|
||||
TotalSpend = g.Sum(x => -x.Amount),
|
||||
Count = g.Count(),
|
||||
PercentageOfTotal = totalSpend > 0 ? (g.Sum(x => -x.Amount) / totalSpend) * 100 : 0,
|
||||
AveragePerTransaction = g.Count() > 0 ? g.Sum(x => -x.Amount) / g.Count() : 0
|
||||
})
|
||||
.OrderByDescending(x => x.TotalSpend)
|
||||
.Take(count)
|
||||
.ToList();
|
||||
|
||||
return topCategories;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides recent transactions.
|
||||
/// </summary>
|
||||
public interface IRecentTransactionsProvider
|
||||
{
|
||||
Task<List<RecentTransactionRow>> GetRecentTransactionsAsync(int count = 20);
|
||||
}
|
||||
|
||||
public class RecentTransactionsProvider : IRecentTransactionsProvider
|
||||
{
|
||||
private readonly MoneyMapContext _db;
|
||||
|
||||
public RecentTransactionsProvider(MoneyMapContext db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
public async Task<List<RecentTransactionRow>> GetRecentTransactionsAsync(int count = 20)
|
||||
{
|
||||
return await _db.Transactions
|
||||
.Include(t => t.Card)
|
||||
.OrderByDescending(t => t.Date)
|
||||
.ThenByDescending(t => t.Id)
|
||||
.Select(t => new RecentTransactionRow
|
||||
{
|
||||
Id = t.Id,
|
||||
Date = t.Date,
|
||||
Name = t.Name,
|
||||
Memo = t.Memo,
|
||||
Amount = t.Amount,
|
||||
Category = t.Category ?? "",
|
||||
CardLabel = t.PaymentMethodLabel,
|
||||
ReceiptCount = t.Receipts.Count()
|
||||
})
|
||||
.Take(count)
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides spending trends over time.
|
||||
/// </summary>
|
||||
public interface ISpendTrendsProvider
|
||||
{
|
||||
Task<SpendTrends> GetDailyTrendsAsync(int lastDays = 30);
|
||||
}
|
||||
|
||||
public class SpendTrendsProvider : ISpendTrendsProvider
|
||||
{
|
||||
private readonly MoneyMapContext _db;
|
||||
|
||||
public SpendTrendsProvider(MoneyMapContext db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
public async Task<SpendTrends> GetDailyTrendsAsync(int lastDays = 30)
|
||||
{
|
||||
var today = DateTime.UtcNow.Date;
|
||||
var since = today.AddDays(-(lastDays - 1));
|
||||
|
||||
var raw = await _db.Transactions
|
||||
.Where(t => t.Date >= since)
|
||||
.ExcludeTransfers()
|
||||
.GroupBy(t => t.Date.Date)
|
||||
.Select(g => new
|
||||
{
|
||||
Date = g.Key,
|
||||
Debits = g.Where(t => t.Amount < 0).Sum(t => t.Amount),
|
||||
Credits = g.Where(t => t.Amount > 0).Sum(t => t.Amount)
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
var dict = raw.ToDictionary(x => x.Date, x => x);
|
||||
var labels = new List<string>();
|
||||
var debitsAbs = new List<decimal>();
|
||||
var credits = new List<decimal>();
|
||||
var net = new List<decimal>();
|
||||
var runningBalance = new List<decimal>();
|
||||
decimal cumulative = 0;
|
||||
|
||||
for (var d = since; d <= today; d = d.AddDays(1))
|
||||
{
|
||||
labels.Add(d.ToString("MMM d"));
|
||||
if (dict.TryGetValue(d, out var v))
|
||||
{
|
||||
var debit = v.Debits;
|
||||
var credit = v.Credits;
|
||||
debitsAbs.Add(Math.Abs(debit));
|
||||
credits.Add(credit);
|
||||
net.Add(credit + debit);
|
||||
cumulative += credit + debit;
|
||||
}
|
||||
else
|
||||
{
|
||||
debitsAbs.Add(0);
|
||||
credits.Add(0);
|
||||
net.Add(0);
|
||||
}
|
||||
runningBalance.Add(cumulative);
|
||||
}
|
||||
|
||||
return new SpendTrends
|
||||
{
|
||||
Labels = labels,
|
||||
DebitsAbs = debitsAbs,
|
||||
Credits = credits,
|
||||
Net = net,
|
||||
RunningBalance = runningBalance
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user