diff --git a/MoneyMap/Models/CategoryMapping.cs b/MoneyMap/Models/CategoryMapping.cs
new file mode 100644
index 0000000..0d362f0
--- /dev/null
+++ b/MoneyMap/Models/CategoryMapping.cs
@@ -0,0 +1,47 @@
+namespace MoneyMap.Models
+{
+ ///
+ /// Represents a mapping rule that associates transaction name patterns with categories.
+ /// Used for automatic categorization of transactions during import.
+ ///
+ public class CategoryMapping
+ {
+ public int Id { get; set; }
+
+ ///
+ /// The category to assign when a transaction matches the pattern.
+ ///
+ public required string Category { get; set; }
+
+ ///
+ /// The pattern to match against transaction names (case-insensitive contains).
+ ///
+ public required string Pattern { get; set; }
+
+ ///
+ /// Higher priority mappings are checked first. Default is 0.
+ ///
+ public int Priority { get; set; } = 0;
+
+ ///
+ /// Optional merchant to auto-assign when this pattern matches.
+ ///
+ public int? MerchantId { get; set; }
+ public Merchant? Merchant { get; set; }
+
+ ///
+ /// AI confidence score when this rule was created by AI (0.0 - 1.0).
+ ///
+ public decimal? Confidence { get; set; }
+
+ ///
+ /// Who created this rule: "User" or "AI".
+ ///
+ public string? CreatedBy { get; set; }
+
+ ///
+ /// When this rule was created.
+ ///
+ public DateTime? CreatedAt { get; set; }
+ }
+}
diff --git a/MoneyMap/Pages/CategoryMappings.cshtml.cs b/MoneyMap/Pages/CategoryMappings.cshtml.cs
index 058b134..ff93f97 100644
--- a/MoneyMap/Pages/CategoryMappings.cshtml.cs
+++ b/MoneyMap/Pages/CategoryMappings.cshtml.cs
@@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using MoneyMap.Data;
+using MoneyMap.Models;
using MoneyMap.Services;
using System.ComponentModel.DataAnnotations;
using System.Text;
diff --git a/MoneyMap/Services/TransactionCategorizer.cs b/MoneyMap/Services/TransactionCategorizer.cs
index 3632ef2..e961707 100644
--- a/MoneyMap/Services/TransactionCategorizer.cs
+++ b/MoneyMap/Services/TransactionCategorizer.cs
@@ -1,34 +1,17 @@
using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Caching.Memory;
using MoneyMap.Data;
+using MoneyMap.Models;
namespace MoneyMap.Services
{
- // ===== Models =====
-
- public class CategoryMapping
- {
- public int Id { get; set; }
- public required string Category { get; set; }
- public required string Pattern { get; set; }
- public int Priority { get; set; } = 0; // Higher priority = checked first
-
- // Merchant relationship
- public int? MerchantId { get; set; }
- public Models.Merchant? Merchant { get; set; }
-
- // AI categorization tracking
- public decimal? Confidence { get; set; } // AI confidence score (0.0 - 1.0)
- public string? CreatedBy { get; set; } // "User" or "AI"
- public DateTime? CreatedAt { get; set; } // When rule was created
- }
-
- // ===== Service Interface =====
public interface ITransactionCategorizer
{
Task CategorizeAsync(string merchantName, decimal? amount = null);
Task> GetAllMappingsAsync();
Task SeedDefaultMappingsAsync();
+ void InvalidateMappingsCache();
}
public class CategorizationResult
@@ -42,11 +25,20 @@ namespace MoneyMap.Services
public class TransactionCategorizer : ITransactionCategorizer
{
private readonly MoneyMapContext _db;
+ private readonly IMemoryCache _cache;
private const decimal GasStationThreshold = -20m;
+ private const string MappingsCacheKey = "CategoryMappings";
+ private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(10);
- public TransactionCategorizer(MoneyMapContext db)
+ public TransactionCategorizer(MoneyMapContext db, IMemoryCache cache)
{
_db = db;
+ _cache = cache;
+ }
+
+ public void InvalidateMappingsCache()
+ {
+ _cache.Remove(MappingsCacheKey);
}
public async Task CategorizeAsync(string merchantName, decimal? amount = null)
@@ -56,11 +48,8 @@ namespace MoneyMap.Services
var merchantUpper = merchantName.ToUpperInvariant();
- // Get all mappings ordered by priority
- var mappings = await _db.CategoryMappings
- .OrderByDescending(m => m.Priority)
- .ThenBy(m => m.Category)
- .ToListAsync();
+ // Get cached mappings or load from database
+ var mappings = await GetCachedMappingsAsync();
// Special case: Gas stations with small purchases
if (amount.HasValue && amount.Value > GasStationThreshold)
@@ -91,12 +80,26 @@ namespace MoneyMap.Services
return new CategorizationResult(); // No match - needs manual categorization
}
+ private async Task> GetCachedMappingsAsync()
+ {
+ if (_cache.TryGetValue(MappingsCacheKey, out List? cachedMappings) && cachedMappings != null)
+ {
+ return cachedMappings;
+ }
+
+ var mappings = await _db.CategoryMappings
+ .OrderByDescending(m => m.Priority)
+ .ThenBy(m => m.Category)
+ .ToListAsync();
+
+ _cache.Set(MappingsCacheKey, mappings, CacheDuration);
+ return mappings;
+ }
+
public async Task> GetAllMappingsAsync()
{
- return await _db.CategoryMappings
- .OrderBy(m => m.Category)
- .ThenByDescending(m => m.Priority)
- .ToListAsync();
+ var mappings = await GetCachedMappingsAsync();
+ return mappings.OrderBy(m => m.Category).ThenByDescending(m => m.Priority).ToList();
}
public async Task SeedDefaultMappingsAsync()