Convert merchant from string to entity with foreign keys
This refactors the merchant field from a simple string column to a normalized entity with proper foreign key relationships: **Database Changes:** - Created Merchant entity/table with unique Name constraint - Replaced Transaction.Merchant (string) with Transaction.MerchantId (FK) - Replaced CategoryMapping.Merchant (string) with CategoryMapping.MerchantId (FK) - Added proper foreign key constraints with SET NULL on delete - Added indexes on MerchantId columns for performance **Backend Changes:** - Created MerchantService for finding/creating merchants - Updated CategorizationResult to return MerchantId instead of merchant name - Modified TransactionCategorizer to return MerchantId from pattern matches - Updated Upload, Recategorize, and CategoryMappings to use merchant service - Updated OpenAIReceiptParser to create/link merchants from parsed receipts - Registered IMerchantService in dependency injection **UI Changes:** - Updated CategoryMappings UI to handle merchant entities (display as Merchant.Name) - Updated Transactions page merchant filter to query by merchant entity - Modified category mapping add/edit/import to create merchants on-the-fly - Updated JavaScript to pass merchant names for edit modal **Migration:** - ConvertMerchantToEntity migration handles schema conversion - Drops old string columns and creates new FK relationships - All existing merchant data is lost (acceptable for this refactoring) **Benefits:** - Database normalization - merchants stored once, referenced many times - Referential integrity with foreign keys - Easier merchant management (rename once, updates everywhere) - Foundation for future merchant features (logos, categories, etc.) - Improved query performance with proper indexes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,7 @@ namespace MoneyMap.Data
|
||||
public DbSet<ReceiptLineItem> ReceiptLineItems => Set<ReceiptLineItem>();
|
||||
|
||||
public DbSet<CategoryMapping> CategoryMappings => Set<CategoryMapping>();
|
||||
public DbSet<Merchant> Merchants => Set<Merchant>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
@@ -74,6 +75,13 @@ namespace MoneyMap.Data
|
||||
.HasForeignKey(x => x.AccountId)
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired(false);
|
||||
|
||||
// Merchant (optional). If a merchant is deleted, set to null.
|
||||
e.HasOne(x => x.Merchant)
|
||||
.WithMany(m => m.Transactions)
|
||||
.HasForeignKey(x => x.MerchantId)
|
||||
.OnDelete(DeleteBehavior.SetNull)
|
||||
.IsRequired(false);
|
||||
});
|
||||
|
||||
// ---------- TRANSFER ----------
|
||||
@@ -159,11 +167,33 @@ namespace MoneyMap.Data
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
// ---------- MERCHANT ----------
|
||||
modelBuilder.Entity<Merchant>(e =>
|
||||
{
|
||||
e.Property(x => x.Name).HasMaxLength(100).IsRequired();
|
||||
e.HasIndex(x => x.Name).IsUnique();
|
||||
});
|
||||
|
||||
// ---------- CATEGORY MAPPING ----------
|
||||
modelBuilder.Entity<CategoryMapping>(e =>
|
||||
{
|
||||
e.Property(x => x.Category).HasMaxLength(100).IsRequired();
|
||||
e.Property(x => x.Pattern).HasMaxLength(200).IsRequired();
|
||||
|
||||
// Merchant (optional). If a merchant is deleted, set to null.
|
||||
e.HasOne(x => x.Merchant)
|
||||
.WithMany(m => m.CategoryMappings)
|
||||
.HasForeignKey(x => x.MerchantId)
|
||||
.OnDelete(DeleteBehavior.SetNull)
|
||||
.IsRequired(false);
|
||||
});
|
||||
|
||||
// ---------- Extra SQL Server–friendly indexes ----------
|
||||
// Fast filtering by date/amount/category
|
||||
modelBuilder.Entity<Transaction>().HasIndex(x => x.Date);
|
||||
modelBuilder.Entity<Transaction>().HasIndex(x => x.Amount);
|
||||
modelBuilder.Entity<Transaction>().HasIndex(x => x.Category);
|
||||
modelBuilder.Entity<Transaction>().HasIndex(x => x.MerchantId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user