Refactor: Extract CategoryMapping model and add caching
- Move CategoryMapping class to Models/CategoryMapping.cs - Add IMemoryCache with 10-minute TTL for category mappings - Add InvalidateMappingsCache() method for cache invalidation - Reduces repeated DB queries during bulk categorization 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
47
MoneyMap/Models/CategoryMapping.cs
Normal file
47
MoneyMap/Models/CategoryMapping.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
namespace MoneyMap.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a mapping rule that associates transaction name patterns with categories.
|
||||
/// Used for automatic categorization of transactions during import.
|
||||
/// </summary>
|
||||
public class CategoryMapping
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The category to assign when a transaction matches the pattern.
|
||||
/// </summary>
|
||||
public required string Category { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The pattern to match against transaction names (case-insensitive contains).
|
||||
/// </summary>
|
||||
public required string Pattern { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Higher priority mappings are checked first. Default is 0.
|
||||
/// </summary>
|
||||
public int Priority { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Optional merchant to auto-assign when this pattern matches.
|
||||
/// </summary>
|
||||
public int? MerchantId { get; set; }
|
||||
public Merchant? Merchant { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// AI confidence score when this rule was created by AI (0.0 - 1.0).
|
||||
/// </summary>
|
||||
public decimal? Confidence { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Who created this rule: "User" or "AI".
|
||||
/// </summary>
|
||||
public string? CreatedBy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When this rule was created.
|
||||
/// </summary>
|
||||
public DateTime? CreatedAt { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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<CategorizationResult> CategorizeAsync(string merchantName, decimal? amount = null);
|
||||
Task<List<CategoryMapping>> 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<CategorizationResult> 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<List<CategoryMapping>> GetCachedMappingsAsync()
|
||||
{
|
||||
if (_cache.TryGetValue(MappingsCacheKey, out List<CategoryMapping>? 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<List<CategoryMapping>> 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()
|
||||
|
||||
Reference in New Issue
Block a user