Major refactor: Split Cards and Accounts into separate tables
Schema Changes: - Add Account model (Institution, AccountType enum, Last4, Owner, Nickname) - Add Transfer model for tracking money movement between accounts - Update Transaction to support both CardId and AccountId (nullable FKs) - Rename Transaction.CardLast4 → Last4 (works for both cards and accounts) - Add PaymentMethodLabel computed property to Transaction - Create EF Core migration: SplitCardsAndAccounts Data Model Improvements: - Accounts: Checking, Savings, Other types - Transfers: Source/Destination accounts, optional link to original transaction - Transactions can now link to either a Card OR an Account - Transfer categories excluded from spending reports via TransactionFilters UI Pages: - Add Accounts.cshtml - List all bank accounts with transaction counts - Add EditAccount.cshtml - Create/edit bank accounts - Add Accounts link to navigation - Update all references from CardLast4 to Last4 Service Layer Updates: - Update CardResolutionResult to use nullable CardId and renamed Last4 - Update TransactionKey record to include AccountId - Update IsDuplicate check to include both CardId and AccountId - Update all PaymentMethodLabel usage across pages This architecture allows proper separation of credit cards from bank accounts and enables tracking of transfers between accounts without double-counting in spending reports. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,9 @@ namespace MoneyMap.Data
|
||||
public MoneyMapContext(DbContextOptions<MoneyMapContext> options) : base(options) { }
|
||||
|
||||
public DbSet<Card> Cards => Set<Card>();
|
||||
public DbSet<Account> Accounts => Set<Account>();
|
||||
public DbSet<Transaction> Transactions => Set<Transaction>();
|
||||
public DbSet<Transfer> Transfers => Set<Transfer>();
|
||||
public DbSet<Receipt> Receipts => Set<Receipt>();
|
||||
public DbSet<ReceiptParseLog> ReceiptParseLogs => Set<ReceiptParseLog>();
|
||||
public DbSet<ReceiptLineItem> ReceiptLineItems => Set<ReceiptLineItem>();
|
||||
@@ -29,6 +31,16 @@ namespace MoneyMap.Data
|
||||
e.HasIndex(x => new { x.Issuer, x.Last4, x.Owner });
|
||||
});
|
||||
|
||||
// ---------- ACCOUNT ----------
|
||||
modelBuilder.Entity<Account>(e =>
|
||||
{
|
||||
e.Property(x => x.Institution).HasMaxLength(100).IsRequired();
|
||||
e.Property(x => x.Last4).HasMaxLength(4).IsRequired();
|
||||
e.Property(x => x.Owner).HasMaxLength(100).IsRequired();
|
||||
e.Property(x => x.Nickname).HasMaxLength(50);
|
||||
e.HasIndex(x => new { x.Institution, x.Last4, x.Owner });
|
||||
});
|
||||
|
||||
// ---------- TRANSACTION ----------
|
||||
modelBuilder.Entity<Transaction>(e =>
|
||||
{
|
||||
@@ -37,13 +49,53 @@ namespace MoneyMap.Data
|
||||
e.Property(x => x.Memo).HasMaxLength(500).HasDefaultValue(string.Empty);
|
||||
e.Property(x => x.Amount).HasColumnType("decimal(18,2)");
|
||||
e.Property(x => x.Category).HasMaxLength(100);
|
||||
e.Property(x => x.CardLast4).HasMaxLength(4);
|
||||
e.Property(x => x.Last4).HasMaxLength(4);
|
||||
|
||||
// Card (required). If a card is deleted, block delete when txns exist (no cascades).
|
||||
// Card (optional). If a card is deleted, block delete when txns exist.
|
||||
e.HasOne(x => x.Card)
|
||||
.WithMany(c => c.Transactions)
|
||||
.HasForeignKey(x => x.CardId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired(false);
|
||||
|
||||
// Account (optional). If an account is deleted, block delete when txns exist.
|
||||
e.HasOne(x => x.Account)
|
||||
.WithMany(a => a.Transactions)
|
||||
.HasForeignKey(x => x.AccountId)
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired(false);
|
||||
});
|
||||
|
||||
// ---------- TRANSFER ----------
|
||||
modelBuilder.Entity<Transfer>(e =>
|
||||
{
|
||||
e.Property(x => x.Amount).HasColumnType("decimal(18,2)");
|
||||
e.Property(x => x.Description).HasMaxLength(500);
|
||||
|
||||
// Source account (optional - can be "Unknown")
|
||||
e.HasOne(x => x.SourceAccount)
|
||||
.WithMany(a => a.SourceTransfers)
|
||||
.HasForeignKey(x => x.SourceAccountId)
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired(false);
|
||||
|
||||
// Destination account (optional - can be "Unknown")
|
||||
e.HasOne(x => x.DestinationAccount)
|
||||
.WithMany(a => a.DestinationTransfers)
|
||||
.HasForeignKey(x => x.DestinationAccountId)
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired(false);
|
||||
|
||||
// Original transaction link (optional)
|
||||
e.HasOne(x => x.OriginalTransaction)
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.OriginalTransactionId)
|
||||
.OnDelete(DeleteBehavior.SetNull)
|
||||
.IsRequired(false);
|
||||
|
||||
e.HasIndex(x => x.Date);
|
||||
e.HasIndex(x => x.SourceAccountId);
|
||||
e.HasIndex(x => x.DestinationAccountId);
|
||||
});
|
||||
|
||||
// ---------- RECEIPT ----------
|
||||
|
||||
Reference in New Issue
Block a user