using Microsoft.EntityFrameworkCore; using Purrse.Core.DTOs; using Purrse.Core.Helpers; using Purrse.Core.Interfaces.Services; using Purrse.Core.Models; using Purrse.Data; namespace Purrse.Api.Services; public class PayeeService : IPayeeService { private readonly PurrseDbContext _db; public PayeeService(PurrseDbContext db) { _db = db; } public async Task> GetAllAsync(Guid userId) { return await _db.Payees .Include(p => p.Aliases).Include(p => p.DefaultCategory) .Where(p => p.UserId == userId) .OrderBy(p => p.Name) .Select(p => new PayeeResponse(p.Id, p.Name, p.DefaultCategoryId, p.DefaultCategory != null ? p.DefaultCategory.Name : null, p.IsActive, p.Aliases.Select(a => new PayeeAliasResponse(a.Id, a.Alias)).ToList())) .ToListAsync(); } public async Task GetByIdAsync(Guid userId, Guid payeeId) { var p = await _db.Payees.Include(p => p.Aliases).Include(p => p.DefaultCategory) .FirstOrDefaultAsync(p => p.Id == payeeId && p.UserId == userId); return p == null ? null : new PayeeResponse(p.Id, p.Name, p.DefaultCategoryId, p.DefaultCategory?.Name, p.IsActive, p.Aliases.Select(a => new PayeeAliasResponse(a.Id, a.Alias)).ToList()); } public async Task CreateAsync(Guid userId, CreatePayeeRequest request) { var payee = new Payee { Id = Guid.NewGuid(), UserId = userId, Name = request.Name, DefaultCategoryId = request.DefaultCategoryId }; if (request.Aliases?.Any() == true) { foreach (var alias in request.Aliases) payee.Aliases.Add(new PayeeAlias { Id = Guid.NewGuid(), Alias = alias }); } _db.Payees.Add(payee); await _db.SaveChangesAsync(); return new PayeeResponse(payee.Id, payee.Name, payee.DefaultCategoryId, null, true, payee.Aliases.Select(a => new PayeeAliasResponse(a.Id, a.Alias)).ToList()); } public async Task UpdateAsync(Guid userId, Guid payeeId, UpdatePayeeRequest request) { var payee = await _db.Payees.Include(p => p.Aliases) .FirstOrDefaultAsync(p => p.Id == payeeId && p.UserId == userId) ?? throw new KeyNotFoundException("Payee not found"); payee.Name = request.Name; payee.DefaultCategoryId = request.DefaultCategoryId; payee.IsActive = request.IsActive; await _db.SaveChangesAsync(); return new PayeeResponse(payee.Id, payee.Name, payee.DefaultCategoryId, null, payee.IsActive, payee.Aliases.Select(a => new PayeeAliasResponse(a.Id, a.Alias)).ToList()); } public async Task DeleteAsync(Guid userId, Guid payeeId) { var payee = await _db.Payees.FirstOrDefaultAsync(p => p.Id == payeeId && p.UserId == userId) ?? throw new KeyNotFoundException("Payee not found"); _db.Payees.Remove(payee); await _db.SaveChangesAsync(); } public async Task AddAliasAsync(Guid userId, Guid payeeId, string alias) { var payee = await _db.Payees.FirstOrDefaultAsync(p => p.Id == payeeId && p.UserId == userId) ?? throw new KeyNotFoundException("Payee not found"); _db.PayeeAliases.Add(new PayeeAlias { Id = Guid.NewGuid(), PayeeId = payeeId, Alias = alias }); await _db.SaveChangesAsync(); } public async Task RemoveAliasAsync(Guid userId, Guid payeeId, Guid aliasId) { var alias = await _db.PayeeAliases.FirstOrDefaultAsync(a => a.Id == aliasId && a.PayeeId == payeeId) ?? throw new KeyNotFoundException("Alias not found"); _db.PayeeAliases.Remove(alias); await _db.SaveChangesAsync(); } public async Task MatchPayeeAsync(Guid userId, string importedName) { if (string.IsNullOrEmpty(importedName)) return null; // Check exact match on name var exact = await _db.Payees.FirstOrDefaultAsync(p => p.UserId == userId && p.Name == importedName && p.IsActive); if (exact != null) return exact; // Check aliases var aliasMatch = await _db.PayeeAliases .Include(a => a.Payee) .FirstOrDefaultAsync(a => a.Payee.UserId == userId && a.Alias == importedName); if (aliasMatch != null) return aliasMatch.Payee; // Fuzzy match var payees = await _db.Payees.Include(p => p.Aliases) .Where(p => p.UserId == userId && p.IsActive).ToListAsync(); Payee? bestMatch = null; double bestScore = 0; foreach (var payee in payees) { var score = StringSimilarityHelper.Calculate(importedName, payee.Name); if (score > bestScore) { bestScore = score; bestMatch = payee; } foreach (var alias in payee.Aliases) { score = StringSimilarityHelper.Calculate(importedName, alias.Alias); if (score > bestScore) { bestScore = score; bestMatch = payee; } } } return bestScore >= 0.85 ? bestMatch : null; } }