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>
This commit is contained in:
@@ -19,14 +19,24 @@ builder.Services.AddSession(options =>
|
|||||||
options.IOTimeout = TimeSpan.FromMinutes(5); // Increase timeout for large data
|
options.IOTimeout = TimeSpan.FromMinutes(5); // Increase timeout for large data
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add the new services here
|
// Core transaction and import services
|
||||||
builder.Services.AddScoped<ITransactionImporter, TransactionImporter>();
|
builder.Services.AddScoped<ITransactionImporter, TransactionImporter>();
|
||||||
builder.Services.AddScoped<ICardResolver, CardResolver>();
|
builder.Services.AddScoped<ICardResolver, CardResolver>();
|
||||||
builder.Services.AddScoped<ITransactionCategorizer, TransactionCategorizer>();
|
builder.Services.AddScoped<ITransactionCategorizer, TransactionCategorizer>();
|
||||||
builder.Services.AddScoped<IMerchantService, MerchantService>();
|
|
||||||
builder.Services.AddScoped<ITransactionService, TransactionService>();
|
builder.Services.AddScoped<ITransactionService, TransactionService>();
|
||||||
|
builder.Services.AddScoped<ITransactionStatisticsService, TransactionStatisticsService>();
|
||||||
|
|
||||||
|
// Entity management services
|
||||||
|
builder.Services.AddScoped<IAccountService, AccountService>();
|
||||||
|
builder.Services.AddScoped<ICardService, CardService>();
|
||||||
|
builder.Services.AddScoped<IMerchantService, MerchantService>();
|
||||||
|
|
||||||
|
// Receipt services
|
||||||
builder.Services.AddScoped<IReceiptMatchingService, ReceiptMatchingService>();
|
builder.Services.AddScoped<IReceiptMatchingService, ReceiptMatchingService>();
|
||||||
|
|
||||||
|
// Reference data services
|
||||||
|
builder.Services.AddScoped<IReferenceDataService, ReferenceDataService>();
|
||||||
|
|
||||||
// Dashboard services
|
// Dashboard services
|
||||||
builder.Services.AddScoped<IDashboardService, DashboardService>();
|
builder.Services.AddScoped<IDashboardService, DashboardService>();
|
||||||
builder.Services.AddScoped<IDashboardStatsCalculator, DashboardStatsCalculator>();
|
builder.Services.AddScoped<IDashboardStatsCalculator, DashboardStatsCalculator>();
|
||||||
|
|||||||
208
MoneyMap/Services/AccountService.cs
Normal file
208
MoneyMap/Services/AccountService.cs
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using MoneyMap.Data;
|
||||||
|
using MoneyMap.Models;
|
||||||
|
|
||||||
|
namespace MoneyMap.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Service for account management including retrieval, validation, and deletion.
|
||||||
|
/// </summary>
|
||||||
|
public interface IAccountService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets an account by ID with optional related data.
|
||||||
|
/// </summary>
|
||||||
|
Task<Account?> GetAccountByIdAsync(int id, bool includeRelated = false);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets all accounts with optional statistics.
|
||||||
|
/// </summary>
|
||||||
|
Task<List<AccountWithStats>> GetAllAccountsWithStatsAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets account details with cards and transaction count.
|
||||||
|
/// </summary>
|
||||||
|
Task<AccountDetails?> GetAccountDetailsAsync(int id);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if an account can be deleted (no transactions exist).
|
||||||
|
/// </summary>
|
||||||
|
Task<DeleteValidationResult> CanDeleteAccountAsync(int id);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deletes an account if it has no associated transactions.
|
||||||
|
/// </summary>
|
||||||
|
Task<DeleteResult> DeleteAccountAsync(int id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AccountService : IAccountService
|
||||||
|
{
|
||||||
|
private readonly MoneyMapContext _db;
|
||||||
|
|
||||||
|
public AccountService(MoneyMapContext db)
|
||||||
|
{
|
||||||
|
_db = db;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Account?> GetAccountByIdAsync(int id, bool includeRelated = false)
|
||||||
|
{
|
||||||
|
var query = _db.Accounts.AsQueryable();
|
||||||
|
|
||||||
|
if (includeRelated)
|
||||||
|
{
|
||||||
|
query = query
|
||||||
|
.Include(a => a.Cards)
|
||||||
|
.Include(a => a.Transactions);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await query.FirstOrDefaultAsync(a => a.Id == id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<AccountWithStats>> GetAllAccountsWithStatsAsync()
|
||||||
|
{
|
||||||
|
var accounts = await _db.Accounts
|
||||||
|
.Include(a => a.Transactions)
|
||||||
|
.OrderBy(a => a.Owner)
|
||||||
|
.ThenBy(a => a.Institution)
|
||||||
|
.ThenBy(a => a.Last4)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
return accounts.Select(a => new AccountWithStats
|
||||||
|
{
|
||||||
|
Id = a.Id,
|
||||||
|
Institution = a.Institution,
|
||||||
|
AccountType = a.AccountType,
|
||||||
|
Last4 = a.Last4,
|
||||||
|
Owner = a.Owner,
|
||||||
|
Nickname = a.Nickname,
|
||||||
|
TransactionCount = a.Transactions.Count
|
||||||
|
}).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<AccountDetails?> GetAccountDetailsAsync(int id)
|
||||||
|
{
|
||||||
|
var account = await _db.Accounts.FindAsync(id);
|
||||||
|
if (account == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
// Get cards linked to this account
|
||||||
|
var cards = await _db.Cards
|
||||||
|
.Where(c => c.AccountId == id)
|
||||||
|
.OrderBy(c => c.Owner)
|
||||||
|
.ThenBy(c => c.Last4)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var cardStats = new List<CardWithStats>();
|
||||||
|
foreach (var card in cards)
|
||||||
|
{
|
||||||
|
var transactionCount = await _db.Transactions.CountAsync(t => t.CardId == card.Id);
|
||||||
|
cardStats.Add(new CardWithStats
|
||||||
|
{
|
||||||
|
Card = card,
|
||||||
|
TransactionCount = transactionCount
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get transaction count for this account
|
||||||
|
var accountTransactionCount = await _db.Transactions.CountAsync(t => t.AccountId == id);
|
||||||
|
|
||||||
|
return new AccountDetails
|
||||||
|
{
|
||||||
|
Account = account,
|
||||||
|
Cards = cardStats,
|
||||||
|
TransactionCount = accountTransactionCount
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<DeleteValidationResult> CanDeleteAccountAsync(int id)
|
||||||
|
{
|
||||||
|
var account = await _db.Accounts
|
||||||
|
.Include(a => a.Transactions)
|
||||||
|
.FirstOrDefaultAsync(a => a.Id == id);
|
||||||
|
|
||||||
|
if (account == null)
|
||||||
|
return new DeleteValidationResult
|
||||||
|
{
|
||||||
|
CanDelete = false,
|
||||||
|
Reason = "Account not found."
|
||||||
|
};
|
||||||
|
|
||||||
|
if (account.Transactions.Any())
|
||||||
|
return new DeleteValidationResult
|
||||||
|
{
|
||||||
|
CanDelete = false,
|
||||||
|
Reason = $"Cannot delete account. It has {account.Transactions.Count} transaction(s) associated with it."
|
||||||
|
};
|
||||||
|
|
||||||
|
return new DeleteValidationResult { CanDelete = true };
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<DeleteResult> DeleteAccountAsync(int id)
|
||||||
|
{
|
||||||
|
var validation = await CanDeleteAccountAsync(id);
|
||||||
|
if (!validation.CanDelete)
|
||||||
|
{
|
||||||
|
return new DeleteResult
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = validation.Reason ?? "Cannot delete account."
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var account = await _db.Accounts.FindAsync(id);
|
||||||
|
if (account == null)
|
||||||
|
{
|
||||||
|
return new DeleteResult
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "Account not found."
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
_db.Accounts.Remove(account);
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
|
||||||
|
return new DeleteResult
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Message = $"Deleted account {account.Institution} {account.Last4}"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DTOs
|
||||||
|
public class AccountWithStats
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Institution { get; set; } = "";
|
||||||
|
public AccountType AccountType { get; set; }
|
||||||
|
public string Last4 { get; set; } = "";
|
||||||
|
public string Owner { get; set; } = "";
|
||||||
|
public string? Nickname { get; set; }
|
||||||
|
public int TransactionCount { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AccountDetails
|
||||||
|
{
|
||||||
|
public Account Account { get; set; } = null!;
|
||||||
|
public List<CardWithStats> Cards { get; set; } = new();
|
||||||
|
public int TransactionCount { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CardWithStats
|
||||||
|
{
|
||||||
|
public Card Card { get; set; } = null!;
|
||||||
|
public int TransactionCount { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DeleteValidationResult
|
||||||
|
{
|
||||||
|
public bool CanDelete { get; set; }
|
||||||
|
public string? Reason { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DeleteResult
|
||||||
|
{
|
||||||
|
public bool Success { get; set; }
|
||||||
|
public string Message { get; set; } = "";
|
||||||
|
}
|
||||||
132
MoneyMap/Services/CardService.cs
Normal file
132
MoneyMap/Services/CardService.cs
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using MoneyMap.Data;
|
||||||
|
using MoneyMap.Models;
|
||||||
|
|
||||||
|
namespace MoneyMap.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Service for card management including retrieval, validation, and deletion.
|
||||||
|
/// </summary>
|
||||||
|
public interface ICardService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a card by ID with optional related data.
|
||||||
|
/// </summary>
|
||||||
|
Task<Card?> GetCardByIdAsync(int id, bool includeRelated = false);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets all cards with transaction statistics.
|
||||||
|
/// </summary>
|
||||||
|
Task<List<CardWithStats>> GetAllCardsWithStatsAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if a card can be deleted (no transactions exist).
|
||||||
|
/// </summary>
|
||||||
|
Task<DeleteValidationResult> CanDeleteCardAsync(int id);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deletes a card if it has no associated transactions.
|
||||||
|
/// </summary>
|
||||||
|
Task<DeleteResult> DeleteCardAsync(int id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CardService : ICardService
|
||||||
|
{
|
||||||
|
private readonly MoneyMapContext _db;
|
||||||
|
|
||||||
|
public CardService(MoneyMapContext db)
|
||||||
|
{
|
||||||
|
_db = db;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Card?> GetCardByIdAsync(int id, bool includeRelated = false)
|
||||||
|
{
|
||||||
|
var query = _db.Cards.AsQueryable();
|
||||||
|
|
||||||
|
if (includeRelated)
|
||||||
|
{
|
||||||
|
query = query
|
||||||
|
.Include(c => c.Account)
|
||||||
|
.Include(c => c.Transactions);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await query.FirstOrDefaultAsync(c => c.Id == id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<CardWithStats>> GetAllCardsWithStatsAsync()
|
||||||
|
{
|
||||||
|
var cards = await _db.Cards
|
||||||
|
.Include(c => c.Account)
|
||||||
|
.OrderBy(c => c.Owner)
|
||||||
|
.ThenBy(c => c.Last4)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var cardStats = new List<CardWithStats>();
|
||||||
|
|
||||||
|
foreach (var card in cards)
|
||||||
|
{
|
||||||
|
var transactionCount = await _db.Transactions.CountAsync(t => t.CardId == card.Id);
|
||||||
|
|
||||||
|
cardStats.Add(new CardWithStats
|
||||||
|
{
|
||||||
|
Card = card,
|
||||||
|
TransactionCount = transactionCount
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return cardStats;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<DeleteValidationResult> CanDeleteCardAsync(int id)
|
||||||
|
{
|
||||||
|
var card = await _db.Cards.FindAsync(id);
|
||||||
|
if (card == null)
|
||||||
|
return new DeleteValidationResult
|
||||||
|
{
|
||||||
|
CanDelete = false,
|
||||||
|
Reason = "Card not found."
|
||||||
|
};
|
||||||
|
|
||||||
|
var transactionCount = await _db.Transactions.CountAsync(t => t.CardId == id);
|
||||||
|
if (transactionCount > 0)
|
||||||
|
return new DeleteValidationResult
|
||||||
|
{
|
||||||
|
CanDelete = false,
|
||||||
|
Reason = $"Cannot delete card. It has {transactionCount} transaction(s) associated with it."
|
||||||
|
};
|
||||||
|
|
||||||
|
return new DeleteValidationResult { CanDelete = true };
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<DeleteResult> DeleteCardAsync(int id)
|
||||||
|
{
|
||||||
|
var validation = await CanDeleteCardAsync(id);
|
||||||
|
if (!validation.CanDelete)
|
||||||
|
{
|
||||||
|
return new DeleteResult
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = validation.Reason ?? "Cannot delete card."
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var card = await _db.Cards.FindAsync(id);
|
||||||
|
if (card == null)
|
||||||
|
{
|
||||||
|
return new DeleteResult
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "Card not found."
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
_db.Cards.Remove(card);
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
|
||||||
|
return new DeleteResult
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Message = "Card deleted successfully."
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
81
MoneyMap/Services/ReferenceDataService.cs
Normal file
81
MoneyMap/Services/ReferenceDataService.cs
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using MoneyMap.Data;
|
||||||
|
using MoneyMap.Models;
|
||||||
|
|
||||||
|
namespace MoneyMap.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Service for retrieving reference/lookup data used in dropdowns and filters.
|
||||||
|
/// </summary>
|
||||||
|
public interface IReferenceDataService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets all distinct categories from transactions, sorted alphabetically.
|
||||||
|
/// </summary>
|
||||||
|
Task<List<string>> GetAvailableCategoriesAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets all merchants, sorted by name.
|
||||||
|
/// </summary>
|
||||||
|
Task<List<Merchant>> GetAvailableMerchantsAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets all cards with optional account information included.
|
||||||
|
/// </summary>
|
||||||
|
Task<List<Card>> GetAvailableCardsAsync(bool includeAccount = true);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets all accounts, sorted by institution and last4.
|
||||||
|
/// </summary>
|
||||||
|
Task<List<Account>> GetAvailableAccountsAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ReferenceDataService : IReferenceDataService
|
||||||
|
{
|
||||||
|
private readonly MoneyMapContext _db;
|
||||||
|
|
||||||
|
public ReferenceDataService(MoneyMapContext db)
|
||||||
|
{
|
||||||
|
_db = db;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<string>> GetAvailableCategoriesAsync()
|
||||||
|
{
|
||||||
|
return await _db.Transactions
|
||||||
|
.Select(t => t.Category ?? "")
|
||||||
|
.Where(c => !string.IsNullOrWhiteSpace(c))
|
||||||
|
.Distinct()
|
||||||
|
.OrderBy(c => c)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<Merchant>> GetAvailableMerchantsAsync()
|
||||||
|
{
|
||||||
|
return await _db.Merchants
|
||||||
|
.OrderBy(m => m.Name)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<Card>> GetAvailableCardsAsync(bool includeAccount = true)
|
||||||
|
{
|
||||||
|
var query = _db.Cards.AsQueryable();
|
||||||
|
|
||||||
|
if (includeAccount)
|
||||||
|
{
|
||||||
|
query = query.Include(c => c.Account);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await query
|
||||||
|
.OrderBy(c => c.Owner)
|
||||||
|
.ThenBy(c => c.Last4)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<Account>> GetAvailableAccountsAsync()
|
||||||
|
{
|
||||||
|
return await _db.Accounts
|
||||||
|
.OrderBy(a => a.Institution)
|
||||||
|
.ThenBy(a => a.Last4)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
108
MoneyMap/Services/TransactionStatisticsService.cs
Normal file
108
MoneyMap/Services/TransactionStatisticsService.cs
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
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; }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user