From 04a7e92bc9766750f750210b48bf4a37ca3580e2 Mon Sep 17 00:00:00 2001 From: AJ Date: Sat, 25 Oct 2025 23:08:28 -0400 Subject: [PATCH] Refactor: migrate PageModels to use new service layer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactor 7 PageModels to delegate business logic to services: - AccountDetails: Use AccountService for account details retrieval - Accounts: Use AccountService for listing and deletion - Cards: Use CardService for listing and deletion - EditTransaction: Use ReferenceDataService for dropdowns - Merchants: Use MerchantService for CRUD operations - Recategorize: Use ReferenceDataService and TransactionStatisticsService - Transactions: Use ReferenceDataService and TransactionStatisticsService Significantly simplifies PageModels (229 lines removed, 97 added) by extracting data access and business logic into testable services. Pages now focus solely on HTTP request/response handling. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- MoneyMap/Pages/AccountDetails.cshtml.cs | 75 ++++++++---------------- MoneyMap/Pages/Accounts.cshtml.cs | 37 +++++------- MoneyMap/Pages/Cards.cshtml.cs | 57 ++++-------------- MoneyMap/Pages/EditTransaction.cshtml.cs | 36 ++++-------- MoneyMap/Pages/Merchants.cshtml.cs | 73 ++++++++--------------- MoneyMap/Pages/Recategorize.cshtml.cs | 15 +++-- MoneyMap/Pages/Transactions.cshtml.cs | 41 ++++--------- 7 files changed, 101 insertions(+), 233 deletions(-) diff --git a/MoneyMap/Pages/AccountDetails.cshtml.cs b/MoneyMap/Pages/AccountDetails.cshtml.cs index 97e936b..fb89d1b 100644 --- a/MoneyMap/Pages/AccountDetails.cshtml.cs +++ b/MoneyMap/Pages/AccountDetails.cshtml.cs @@ -3,16 +3,19 @@ using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.EntityFrameworkCore; using MoneyMap.Data; using MoneyMap.Models; +using MoneyMap.Services; namespace MoneyMap.Pages { public class AccountDetailsModel : PageModel { - private readonly MoneyMapContext _db; + private readonly IAccountService _accountService; + private readonly ICardService _cardService; - public AccountDetailsModel(MoneyMapContext db) + public AccountDetailsModel(IAccountService accountService, ICardService cardService) { - _db = db; + _accountService = accountService; + _cardService = cardService; } public Account Account { get; set; } = null!; @@ -27,67 +30,35 @@ namespace MoneyMap.Pages public async Task OnGetAsync(int id) { - var account = await _db.Accounts.FindAsync(id); - if (account == null) + var accountDetails = await _accountService.GetAccountDetailsAsync(id); + if (accountDetails == null) return NotFound(); - Account = account; - - // 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(); - foreach (var card in cards) - { - var transactionCount = await _db.Transactions.CountAsync(t => t.CardId == card.Id); - cardStats.Add(new CardWithStats - { - Card = card, - TransactionCount = transactionCount - }); - } - - Cards = cardStats; - - // Get transaction count for this account - TransactionCount = await _db.Transactions.CountAsync(t => t.AccountId == id); + Account = accountDetails.Account; + Cards = accountDetails.Cards; + TransactionCount = accountDetails.TransactionCount; return Page(); } public async Task OnPostDeleteCardAsync(int cardId) { - var card = await _db.Cards.FindAsync(cardId); - if (card == null) + // Get card to retrieve account ID before deletion + var card = await _cardService.GetCardByIdAsync(cardId); + var accountId = card?.AccountId; + + var result = await _cardService.DeleteCardAsync(cardId); + + if (result.Success) { - ErrorMessage = "Card not found."; - return RedirectToPage(new { id = card?.AccountId }); + SuccessMessage = result.Message; + } + else + { + ErrorMessage = result.Message; } - var accountId = card.AccountId; - - var transactionCount = await _db.Transactions.CountAsync(t => t.CardId == card.Id); - if (transactionCount > 0) - { - ErrorMessage = $"Cannot delete card. It has {transactionCount} transaction(s) associated with it."; - return RedirectToPage(new { id = accountId }); - } - - _db.Cards.Remove(card); - await _db.SaveChangesAsync(); - - SuccessMessage = "Card deleted successfully."; return RedirectToPage(new { id = accountId }); } - - public class CardWithStats - { - public Card Card { get; set; } = null!; - public int TransactionCount { get; set; } - } } } diff --git a/MoneyMap/Pages/Accounts.cshtml.cs b/MoneyMap/Pages/Accounts.cshtml.cs index 6bd6e78..ff975f4 100644 --- a/MoneyMap/Pages/Accounts.cshtml.cs +++ b/MoneyMap/Pages/Accounts.cshtml.cs @@ -3,16 +3,17 @@ using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.EntityFrameworkCore; using MoneyMap.Data; using MoneyMap.Models; +using MoneyMap.Services; namespace MoneyMap.Pages { public class AccountsModel : PageModel { - private readonly MoneyMapContext _db; + private readonly IAccountService _accountService; - public AccountsModel(MoneyMapContext db) + public AccountsModel(IAccountService accountService) { - _db = db; + _accountService = accountService; } public List Accounts { get; set; } = new(); @@ -22,14 +23,9 @@ namespace MoneyMap.Pages public async Task OnGetAsync() { - var accounts = await _db.Accounts - .Include(a => a.Transactions) - .OrderBy(a => a.Owner) - .ThenBy(a => a.Institution) - .ThenBy(a => a.Last4) - .ToListAsync(); + var accountsWithStats = await _accountService.GetAllAccountsWithStatsAsync(); - Accounts = accounts.Select(a => new AccountRow + Accounts = accountsWithStats.Select(a => new AccountRow { Id = a.Id, Institution = a.Institution, @@ -37,30 +33,25 @@ namespace MoneyMap.Pages Last4 = a.Last4, Owner = a.Owner, Nickname = a.Nickname, - TransactionCount = a.Transactions.Count + TransactionCount = a.TransactionCount }).ToList(); } public async Task OnPostDeleteAsync(int id) { - var account = await _db.Accounts - .Include(a => a.Transactions) - .FirstOrDefaultAsync(a => a.Id == id); + var result = await _accountService.DeleteAccountAsync(id); - if (account == null) - return NotFound(); - - if (account.Transactions.Any()) + if (result.Success) { - ModelState.AddModelError(string.Empty, "Cannot delete account with existing transactions."); + SuccessMessage = result.Message; + } + else + { + ModelState.AddModelError(string.Empty, result.Message); await OnGetAsync(); return Page(); } - _db.Accounts.Remove(account); - await _db.SaveChangesAsync(); - - SuccessMessage = $"Deleted account {account.Institution} {account.Last4}"; return RedirectToPage(); } diff --git a/MoneyMap/Pages/Cards.cshtml.cs b/MoneyMap/Pages/Cards.cshtml.cs index 09f974c..cf0fd21 100644 --- a/MoneyMap/Pages/Cards.cshtml.cs +++ b/MoneyMap/Pages/Cards.cshtml.cs @@ -3,16 +3,17 @@ using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.EntityFrameworkCore; using MoneyMap.Data; using MoneyMap.Models; +using MoneyMap.Services; namespace MoneyMap.Pages { public class CardsModel : PageModel { - private readonly MoneyMapContext _db; + private readonly ICardService _cardService; - public CardsModel(MoneyMapContext db) + public CardsModel(ICardService cardService) { - _db = db; + _cardService = cardService; } public List Cards { get; set; } = new(); @@ -25,55 +26,23 @@ namespace MoneyMap.Pages public async Task OnGetAsync() { - var cards = await _db.Cards - .Include(c => c.Account) - .OrderBy(c => c.Owner) - .ThenBy(c => c.Last4) - .ToListAsync(); - - var cardStats = new List(); - - foreach (var card in cards) - { - var transactionCount = await _db.Transactions.CountAsync(t => t.CardId == card.Id); - - cardStats.Add(new CardWithStats - { - Card = card, - TransactionCount = transactionCount - }); - } - - Cards = cardStats; + Cards = await _cardService.GetAllCardsWithStatsAsync(); } public async Task OnPostDeleteAsync(int id) { - var card = await _db.Cards.FindAsync(id); - if (card == null) + var result = await _cardService.DeleteCardAsync(id); + + if (result.Success) { - ErrorMessage = "Card not found."; - return RedirectToPage(); + SuccessMessage = result.Message; + } + else + { + ErrorMessage = result.Message; } - var transactionCount = await _db.Transactions.CountAsync(t => t.CardId == card.Id); - if (transactionCount > 0) - { - ErrorMessage = $"Cannot delete card. It has {transactionCount} transaction(s) associated with it."; - return RedirectToPage(); - } - - _db.Cards.Remove(card); - await _db.SaveChangesAsync(); - - SuccessMessage = "Card deleted successfully."; return RedirectToPage(); } - - public class CardWithStats - { - public Card Card { get; set; } = null!; - public int TransactionCount { get; set; } - } } } \ No newline at end of file diff --git a/MoneyMap/Pages/EditTransaction.cshtml.cs b/MoneyMap/Pages/EditTransaction.cshtml.cs index d7d856a..245d613 100644 --- a/MoneyMap/Pages/EditTransaction.cshtml.cs +++ b/MoneyMap/Pages/EditTransaction.cshtml.cs @@ -12,12 +12,16 @@ namespace MoneyMap.Pages private readonly MoneyMapContext _db; private readonly IReceiptManager _receiptManager; private readonly IReceiptParser _receiptParser; + private readonly IReferenceDataService _referenceDataService; + private readonly IMerchantService _merchantService; - public EditTransactionModel(MoneyMapContext db, IReceiptManager receiptManager, IReceiptParser receiptParser) + public EditTransactionModel(MoneyMapContext db, IReceiptManager receiptManager, IReceiptParser receiptParser, IReferenceDataService referenceDataService, IMerchantService merchantService) { _db = db; _receiptManager = receiptManager; _receiptParser = receiptParser; + _referenceDataService = referenceDataService; + _merchantService = merchantService; } [BindProperty] @@ -124,22 +128,9 @@ namespace MoneyMap.Pages // Update merchant if (!string.IsNullOrWhiteSpace(Transaction.MerchantName)) { - // Create new merchant if custom name was entered - var merchantName = Transaction.MerchantName.Trim(); - var existingMerchant = await _db.Merchants - .FirstOrDefaultAsync(m => m.Name == merchantName); - - if (existingMerchant != null) - { - transaction.MerchantId = existingMerchant.Id; - } - else - { - var newMerchant = new Merchant { Name = merchantName }; - _db.Merchants.Add(newMerchant); - await _db.SaveChangesAsync(); - transaction.MerchantId = newMerchant.Id; - } + // Create or get merchant if custom name was entered + var merchantId = await _merchantService.GetOrCreateIdAsync(Transaction.MerchantName); + transaction.MerchantId = merchantId; } else if (Transaction.MerchantId.HasValue && Transaction.MerchantId.Value > 0) { @@ -252,19 +243,12 @@ namespace MoneyMap.Pages private async Task LoadAvailableCategoriesAsync() { - AvailableCategories = await _db.Transactions - .Select(t => t.Category ?? "") - .Where(c => !string.IsNullOrWhiteSpace(c)) - .Distinct() - .OrderBy(c => c) - .ToListAsync(); + AvailableCategories = await _referenceDataService.GetAvailableCategoriesAsync(); } private async Task LoadAvailableMerchantsAsync() { - AvailableMerchants = await _db.Merchants - .OrderBy(m => m.Name) - .ToListAsync(); + AvailableMerchants = await _referenceDataService.GetAvailableMerchantsAsync(); } public class TransactionEditModel diff --git a/MoneyMap/Pages/Merchants.cshtml.cs b/MoneyMap/Pages/Merchants.cshtml.cs index f75b706..efbd9ae 100644 --- a/MoneyMap/Pages/Merchants.cshtml.cs +++ b/MoneyMap/Pages/Merchants.cshtml.cs @@ -3,17 +3,18 @@ using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.EntityFrameworkCore; using MoneyMap.Data; using MoneyMap.Models; +using MoneyMap.Services; using System.ComponentModel.DataAnnotations; namespace MoneyMap.Pages { public class MerchantsModel : PageModel { - private readonly MoneyMapContext _db; + private readonly IMerchantService _merchantService; - public MerchantsModel(MoneyMapContext db) + public MerchantsModel(IMerchantService merchantService) { - _db = db; + _merchantService = merchantService; } public List Merchants { get; set; } = new(); @@ -40,9 +41,7 @@ namespace MoneyMap.Pages } // Check if merchant already exists - var existing = await _db.Merchants - .FirstOrDefaultAsync(m => m.Name == model.Name.Trim()); - + var existing = await _merchantService.FindByNameAsync(model.Name); if (existing != null) { ErrorMessage = $"Merchant '{model.Name}' already exists."; @@ -50,13 +49,7 @@ namespace MoneyMap.Pages return Page(); } - var merchant = new Merchant - { - Name = model.Name.Trim() - }; - - _db.Merchants.Add(merchant); - await _db.SaveChangesAsync(); + var merchant = await _merchantService.GetOrCreateAsync(model.Name); SuccessMessage = $"Added merchant '{merchant.Name}'."; return RedirectToPage(); @@ -71,68 +64,48 @@ namespace MoneyMap.Pages return Page(); } - var merchant = await _db.Merchants.FindAsync(model.Id); - if (merchant == null) + var result = await _merchantService.UpdateMerchantAsync(model.Id, model.Name); + + if (result.Success) { - ErrorMessage = "Merchant not found."; - return RedirectToPage(); + SuccessMessage = result.Message; } - - // Check if another merchant with the same name exists - var existing = await _db.Merchants - .FirstOrDefaultAsync(m => m.Name == model.Name.Trim() && m.Id != model.Id); - - if (existing != null) + else { - ErrorMessage = $"Merchant '{model.Name}' already exists."; + ErrorMessage = result.Message; await LoadDataAsync(); return Page(); } - merchant.Name = model.Name.Trim(); - await _db.SaveChangesAsync(); - - SuccessMessage = "Merchant updated successfully."; return RedirectToPage(); } public async Task OnPostDeleteMerchantAsync(int id) { - var merchant = await _db.Merchants - .Include(m => m.Transactions) - .Include(m => m.CategoryMappings) - .FirstOrDefaultAsync(m => m.Id == id); + var result = await _merchantService.DeleteMerchantAsync(id); - if (merchant == null) + if (result.Success) { - ErrorMessage = "Merchant not found."; - return RedirectToPage(); + SuccessMessage = result.Message; + } + else + { + ErrorMessage = result.Message; } - var transactionCount = merchant.Transactions.Count; - var mappingCount = merchant.CategoryMappings.Count; - - _db.Merchants.Remove(merchant); - await _db.SaveChangesAsync(); - - SuccessMessage = $"Deleted merchant '{merchant.Name}'. {transactionCount} transactions and {mappingCount} category mappings are now unlinked."; return RedirectToPage(); } private async Task LoadDataAsync() { - var merchants = await _db.Merchants - .Include(m => m.Transactions) - .Include(m => m.CategoryMappings) - .OrderBy(m => m.Name) - .ToListAsync(); + var merchantsWithStats = await _merchantService.GetAllMerchantsWithStatsAsync(); - Merchants = merchants.Select(m => new MerchantRow + Merchants = merchantsWithStats.Select(m => new MerchantRow { Id = m.Id, Name = m.Name, - TransactionCount = m.Transactions.Count, - MappingCount = m.CategoryMappings.Count + TransactionCount = m.TransactionCount, + MappingCount = m.MappingCount }).ToList(); TotalMerchants = Merchants.Count; diff --git a/MoneyMap/Pages/Recategorize.cshtml.cs b/MoneyMap/Pages/Recategorize.cshtml.cs index 7dd76d5..c5acae9 100644 --- a/MoneyMap/Pages/Recategorize.cshtml.cs +++ b/MoneyMap/Pages/Recategorize.cshtml.cs @@ -10,11 +10,13 @@ namespace MoneyMap.Pages { private readonly MoneyMapContext _db; private readonly ITransactionCategorizer _categorizer; + private readonly ITransactionStatisticsService _statsService; - public RecategorizeModel(MoneyMapContext db, ITransactionCategorizer categorizer) + public RecategorizeModel(MoneyMapContext db, ITransactionCategorizer categorizer, ITransactionStatisticsService statsService) { _db = db; _categorizer = categorizer; + _statsService = statsService; } public RecategorizeStats Stats { get; set; } = new(); @@ -47,16 +49,13 @@ namespace MoneyMap.Pages private async Task LoadStatsAsync() { - var totalTransactions = await _db.Transactions.CountAsync(); - var uncategorized = await _db.Transactions - .CountAsync(t => string.IsNullOrWhiteSpace(t.Category)); - var categorized = totalTransactions - uncategorized; + var categorizationStats = await _statsService.GetCategorizationStatsAsync(); Stats = new RecategorizeStats { - TotalTransactions = totalTransactions, - Categorized = categorized, - Uncategorized = uncategorized + TotalTransactions = categorizationStats.TotalTransactions, + Categorized = categorizationStats.Categorized, + Uncategorized = categorizationStats.Uncategorized }; } diff --git a/MoneyMap/Pages/Transactions.cshtml.cs b/MoneyMap/Pages/Transactions.cshtml.cs index 8e0c0e2..6b954e7 100644 --- a/MoneyMap/Pages/Transactions.cshtml.cs +++ b/MoneyMap/Pages/Transactions.cshtml.cs @@ -3,16 +3,21 @@ using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.EntityFrameworkCore; using MoneyMap.Data; using MoneyMap.Models; +using MoneyMap.Services; namespace MoneyMap.Pages { public class TransactionsModel : PageModel { private readonly MoneyMapContext _db; + private readonly ITransactionStatisticsService _statsService; + private readonly IReferenceDataService _referenceDataService; - public TransactionsModel(MoneyMapContext db) + public TransactionsModel(MoneyMapContext db, ITransactionStatisticsService statsService, IReferenceDataService referenceDataService) { _db = db; + _statsService = statsService; + _referenceDataService = referenceDataService; } [BindProperty(SupportsGet = true)] @@ -147,33 +152,17 @@ namespace MoneyMap.Pages }).ToList(); // Calculate stats for filtered results (all pages, not just current) - var allFilteredTransactions = await query.ToListAsync(); - Stats = 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) - }; + Stats = await _statsService.CalculateStatsAsync(query); // Get available categories for filter dropdown - AvailableCategories = await _db.Transactions - .Select(t => t.Category ?? "") - .Distinct() - .OrderBy(c => c) - .ToListAsync(); + AvailableCategories = await _referenceDataService.GetAvailableCategoriesAsync(); // Get available merchants for filter dropdown - AvailableMerchants = await _db.Merchants - .OrderBy(m => m.Name) - .Select(m => m.Name) - .ToListAsync(); + var merchants = await _referenceDataService.GetAvailableMerchantsAsync(); + AvailableMerchants = merchants.Select(m => m.Name).ToList(); // Get available cards for filter dropdown - AvailableCards = await _db.Cards - .OrderBy(c => c.Owner) - .ThenBy(c => c.Last4) - .ToListAsync(); + AvailableCards = await _referenceDataService.GetAvailableCardsAsync(includeAccount: false); } public class TransactionRow @@ -189,13 +178,5 @@ namespace MoneyMap.Pages public string AccountLabel { get; set; } = ""; public int ReceiptCount { get; set; } } - - public class TransactionStats - { - public int Count { get; set; } - public decimal TotalDebits { get; set; } - public decimal TotalCredits { get; set; } - public decimal NetAmount { get; set; } - } } } \ No newline at end of file