- CardService.GetAllCardsWithStatsAsync: Use single query with Select projection - AccountService.GetAccountDetailsAsync: Use single query with Select projection - TransactionStatisticsService: Calculate stats at DB level, fix N+1 in GetCardStatsForAccountAsync 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
203 lines
5.5 KiB
C#
203 lines
5.5 KiB
C#
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;
|
|
|
|
// Single query with projection to avoid N+1
|
|
var cardStats = await _db.Cards
|
|
.Where(c => c.AccountId == id)
|
|
.OrderBy(c => c.Owner)
|
|
.ThenBy(c => c.Last4)
|
|
.Select(c => new CardWithStats
|
|
{
|
|
Card = c,
|
|
TransactionCount = c.Transactions.Count
|
|
})
|
|
.ToListAsync();
|
|
|
|
// 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; } = "";
|
|
}
|