Files
MoneyMap/MoneyMap/Pages/Upload.cshtml.cs
T
aj 2ceb3a7b57 Refactor: Extract import services from Upload page
Extract to separate files for better maintainability:
- Models/Import/ImportContext.cs - Import context and PaymentSelectMode enum
- Models/Import/ImportResults.cs - Import result DTOs and TransactionKey
- Models/Import/PaymentResolutionResult.cs - Payment resolution DTO
- Services/TransactionImporter.cs - CSV import logic
- Services/CardResolver.cs - Payment method resolution

Reduces Upload.cshtml.cs from 615 lines to 216 lines.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-24 21:11:39 -05:00

217 lines
8.1 KiB
C#

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using MoneyMap.Data;
using MoneyMap.Models;
using MoneyMap.Models.Import;
using MoneyMap.Services;
using System.Text.Json;
namespace MoneyMap.Pages
{
public class UploadModel : PageModel
{
private readonly MoneyMapContext _db;
private readonly ITransactionImporter _importer;
private readonly ITransactionCategorizer _categorizer;
private readonly ITransactionService _transactionService;
public UploadModel(MoneyMapContext db, ITransactionImporter importer, ITransactionCategorizer categorizer, ITransactionService transactionService)
{
_db = db;
_importer = importer;
_categorizer = categorizer;
_transactionService = transactionService;
}
[BindProperty] public IFormFile? Csv { get; set; }
[BindProperty] public PaymentSelectMode PaymentMode { get; set; } = PaymentSelectMode.Auto;
[BindProperty] public int? SelectedCardId { get; set; }
[BindProperty] public int? SelectedAccountId { get; set; }
public List<Card> Cards { get; set; } = new();
public List<Account> Accounts { get; set; } = new();
public ImportResult? Result { get; set; }
public List<TransactionPreview> PreviewTransactions { get; set; } = new();
private const string PreviewSessionKey = "TransactionPreview";
public async Task OnGetAsync()
{
Cards = await _db.Cards
.Include(c => c.Account)
.OrderBy(c => c.Owner)
.ThenBy(c => c.Last4)
.ToListAsync();
Accounts = await _db.Accounts
.OrderBy(a => a.Institution)
.ThenBy(a => a.Last4)
.ToListAsync();
}
public async Task<IActionResult> OnPostAsync()
{
if (!ValidateInput())
{
await OnGetAsync();
return Page();
}
Cards = await _db.Cards
.Include(c => c.Account)
.OrderBy(c => c.Owner)
.ThenBy(c => c.Last4)
.ToListAsync();
Accounts = await _db.Accounts
.OrderBy(a => a.Institution)
.ThenBy(a => a.Last4)
.ToListAsync();
var importContext = new ImportContext
{
PaymentMode = PaymentMode,
SelectedCardId = SelectedCardId,
SelectedAccountId = SelectedAccountId,
AvailableCards = Cards,
AvailableAccounts = Accounts,
FileName = Csv!.FileName
};
var previewResult = await _importer.PreviewAsync(Csv!.OpenReadStream(), importContext);
if (!previewResult.IsSuccess)
{
ModelState.AddModelError(string.Empty, previewResult.ErrorMessage!);
await OnGetAsync();
return Page();
}
// Apply categorization to preview
foreach (var preview in previewResult.Data!)
{
if (string.IsNullOrWhiteSpace(preview.Transaction.Category))
{
var categorizationResult = await _categorizer.CategorizeAsync(preview.Transaction.Name, preview.Transaction.Amount);
preview.Transaction.Category = categorizationResult.Category;
preview.Transaction.MerchantId = categorizationResult.MerchantId;
preview.SuggestedCategory = categorizationResult.Category;
}
}
PreviewTransactions = previewResult.Data!;
// Store preview data in Session for confirm step
HttpContext.Session.SetString(PreviewSessionKey,
JsonSerializer.Serialize(PreviewTransactions.Select(p => p.Transaction).ToList()));
return Page();
}
public async Task<IActionResult> OnPostConfirmAsync(string selectedIndices, string paymentData)
{
var previewDataJson = HttpContext.Session.GetString(PreviewSessionKey);
if (string.IsNullOrWhiteSpace(previewDataJson))
{
ModelState.AddModelError(string.Empty, "Preview data expired. Please upload again.");
await OnGetAsync();
return Page();
}
var transactions = JsonSerializer.Deserialize<List<Transaction>>(previewDataJson);
if (transactions == null || !transactions.Any())
{
ModelState.AddModelError(string.Empty, "No transactions to import.");
await OnGetAsync();
return Page();
}
// Parse selected indices
var selectedIndexList = string.IsNullOrWhiteSpace(selectedIndices)
? new List<int>()
: selectedIndices.Split(',').Select(int.Parse).ToList();
// Parse payment data
var paymentSelections = string.IsNullOrWhiteSpace(paymentData)
? new Dictionary<int, PaymentSelection>()
: JsonSerializer.Deserialize<Dictionary<int, PaymentSelection>>(paymentData) ?? new();
// Filter transactions based on user selection and update payment methods
var transactionsToImport = new List<Transaction>();
foreach (var index in selectedIndexList)
{
if (index >= 0 && index < transactions.Count)
{
var txn = transactions[index];
// Update payment method based on user selection
if (paymentSelections.TryGetValue(index, out var payment))
{
// Account is required and set globally for all transactions
if (payment.AccountId.HasValue)
{
var account = await _db.Accounts.FindAsync(payment.AccountId.Value);
if (account != null)
{
txn.AccountId = payment.AccountId.Value;
txn.Last4 = account.Last4;
}
}
// Card is optional per transaction
if (payment.CardId.HasValue)
{
var card = await _db.Cards.FindAsync(payment.CardId.Value);
if (card != null)
{
txn.CardId = card.Id;
}
}
else
{
txn.CardId = null; // Direct account transaction, no card
}
// Category (user can edit in preview)
if (!string.IsNullOrWhiteSpace(payment.Category))
{
txn.Category = payment.Category.Trim();
}
}
// Check for duplicates
if (!await _transactionService.IsDuplicateAsync(txn))
{
transactionsToImport.Add(txn);
}
}
}
var result = await _importer.ImportAsync(transactionsToImport);
if (!result.IsSuccess)
{
ModelState.AddModelError(string.Empty, result.ErrorMessage!);
await OnGetAsync();
return Page();
}
Result = result.Data;
HttpContext.Session.Remove(PreviewSessionKey); // Clear preview data
await OnGetAsync();
return Page();
}
private bool ValidateInput()
{
if (Csv is null || Csv.Length == 0)
{
ModelState.AddModelError(nameof(Csv), "Please choose a CSV file.");
return false;
}
return true;
}
}
}