From 5b4a673f9dc5ea41374ca7e7d6c6b34ca4a8190c Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Mon, 20 Apr 2026 20:34:36 -0400 Subject: [PATCH] feat(api): add CategoriesController with list, mappings, and add-mapping endpoints Co-Authored-By: Claude Opus 4.6 --- MoneyMap/Controllers/CategoriesController.cs | 88 ++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 MoneyMap/Controllers/CategoriesController.cs diff --git a/MoneyMap/Controllers/CategoriesController.cs b/MoneyMap/Controllers/CategoriesController.cs new file mode 100644 index 0000000..d39b999 --- /dev/null +++ b/MoneyMap/Controllers/CategoriesController.cs @@ -0,0 +1,88 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using MoneyMap.Data; +using MoneyMap.Models; +using MoneyMap.Services; + +namespace MoneyMap.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class CategoriesController : ControllerBase +{ + private readonly MoneyMapContext _db; + private readonly ITransactionCategorizer _categorizer; + private readonly IMerchantService _merchantService; + + public CategoriesController(MoneyMapContext db, ITransactionCategorizer categorizer, IMerchantService merchantService) + { + _db = db; + _categorizer = categorizer; + _merchantService = merchantService; + } + + [HttpGet] + public async Task List() + { + var categories = await _db.Transactions + .Where(t => t.Category != null && t.Category != "") + .GroupBy(t => t.Category!) + .Select(g => new { Category = g.Key, Count = g.Count(), TotalSpent = g.Where(t => t.Amount < 0).Sum(t => Math.Abs(t.Amount)) }) + .OrderByDescending(x => x.Count) + .ToListAsync(); + + var uncategorized = await _db.Transactions + .CountAsync(t => t.Category == null || t.Category == ""); + + return Ok(new { Categories = categories, UncategorizedCount = uncategorized }); + } + + [HttpGet("mappings")] + public async Task GetMappings([FromQuery] string? category = null) + { + var mappings = await _categorizer.GetAllMappingsAsync(); + + if (!string.IsNullOrWhiteSpace(category)) + mappings = mappings.Where(m => m.Category.Equals(category, StringComparison.OrdinalIgnoreCase)).ToList(); + + var result = mappings.Select(m => new + { + m.Id, + m.Pattern, + m.Category, + m.MerchantId, + m.Priority + }).OrderBy(m => m.Category).ThenByDescending(m => m.Priority).ToList(); + + return Ok(result); + } + + [HttpPost("mappings")] + public async Task AddMapping([FromBody] CreateCategoryMappingRequest request) + { + int? merchantId = null; + if (!string.IsNullOrWhiteSpace(request.MerchantName)) + merchantId = await _merchantService.GetOrCreateIdAsync(request.MerchantName); + + var mapping = new CategoryMapping + { + Pattern = request.Pattern, + Category = request.Category, + MerchantId = merchantId, + Priority = request.Priority + }; + + _db.CategoryMappings.Add(mapping); + await _db.SaveChangesAsync(); + + return Ok(new { Created = true, mapping.Id, mapping.Pattern, mapping.Category, Merchant = request.MerchantName, mapping.Priority }); + } +} + +public class CreateCategoryMappingRequest +{ + public string Pattern { get; set; } = ""; + public string Category { get; set; } = ""; + public string? MerchantName { get; set; } + public int Priority { get; set; } +}