feat(api): add MerchantsController with list and merge endpoints

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-20 20:35:49 -04:00
parent 9dc1a9064d
commit c34ea74459
@@ -0,0 +1,97 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MoneyMap.Data;
namespace MoneyMap.Controllers;
[ApiController]
[Route("api/[controller]")]
public class MerchantsController : ControllerBase
{
private readonly MoneyMapContext _db;
public MerchantsController(MoneyMapContext db) => _db = db;
[HttpGet]
public async Task<IActionResult> List([FromQuery] string? query = null)
{
var q = _db.Merchants
.Include(m => m.Transactions)
.Include(m => m.CategoryMappings)
.AsQueryable();
if (!string.IsNullOrWhiteSpace(query))
q = q.Where(m => m.Name.Contains(query));
var merchants = await q
.OrderBy(m => m.Name)
.Select(m => new
{
m.Id,
m.Name,
TransactionCount = m.Transactions.Count,
MappingCount = m.CategoryMappings.Count,
Categories = m.CategoryMappings.Select(cm => cm.Category).Distinct().ToList()
})
.ToListAsync();
return Ok(new { Count = merchants.Count, Merchants = merchants });
}
[HttpPost("merge")]
public async Task<IActionResult> Merge([FromBody] MergeMerchantsRequest request)
{
if (request.SourceMerchantId == request.TargetMerchantId)
return BadRequest(new { message = "Source and target merchant cannot be the same" });
var source = await _db.Merchants.FindAsync(request.SourceMerchantId);
var target = await _db.Merchants.FindAsync(request.TargetMerchantId);
if (source == null)
return NotFound(new { message = $"Source merchant {request.SourceMerchantId} not found" });
if (target == null)
return NotFound(new { message = $"Target merchant {request.TargetMerchantId} not found" });
var transactions = await _db.Transactions
.Where(t => t.MerchantId == request.SourceMerchantId)
.ToListAsync();
foreach (var t in transactions)
t.MerchantId = request.TargetMerchantId;
var sourceMappings = await _db.CategoryMappings
.Where(cm => cm.MerchantId == request.SourceMerchantId)
.ToListAsync();
var targetMappingPatterns = await _db.CategoryMappings
.Where(cm => cm.MerchantId == request.TargetMerchantId)
.Select(cm => cm.Pattern)
.ToListAsync();
foreach (var mapping in sourceMappings)
{
if (targetMappingPatterns.Contains(mapping.Pattern))
_db.CategoryMappings.Remove(mapping);
else
mapping.MerchantId = request.TargetMerchantId;
}
_db.Merchants.Remove(source);
await _db.SaveChangesAsync();
return Ok(new
{
Merged = true,
Source = new { source.Id, source.Name },
Target = new { target.Id, target.Name },
TransactionsReassigned = transactions.Count,
MappingsReassigned = sourceMappings.Count
});
}
}
public class MergeMerchantsRequest
{
public int SourceMerchantId { get; set; }
public int TargetMerchantId { get; set; }
}