3b01efd8a6
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
185 lines
5.6 KiB
C#
185 lines
5.6 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using MoneyMap.Data;
|
|
using MoneyMap.Models;
|
|
|
|
namespace MoneyMap.Services
|
|
{
|
|
public interface IMerchantService
|
|
{
|
|
Task<Merchant?> FindByNameAsync(string name);
|
|
Task<Merchant> GetOrCreateAsync(string name);
|
|
Task<int?> GetOrCreateIdAsync(string? name);
|
|
Task<Merchant?> GetMerchantByIdAsync(int id, bool includeRelated = false);
|
|
Task<List<MerchantWithStats>> GetAllMerchantsWithStatsAsync();
|
|
Task<MerchantUpdateResult> UpdateMerchantAsync(int id, string newName);
|
|
Task<MerchantDeleteResult> DeleteMerchantAsync(int id);
|
|
}
|
|
|
|
public class MerchantService : IMerchantService
|
|
{
|
|
private readonly MoneyMapContext _db;
|
|
|
|
public MerchantService(MoneyMapContext db)
|
|
{
|
|
_db = db;
|
|
}
|
|
|
|
public async Task<Merchant?> FindByNameAsync(string name)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(name))
|
|
return null;
|
|
|
|
return await _db.Merchants
|
|
.FirstOrDefaultAsync(m => m.Name == name.Trim());
|
|
}
|
|
|
|
public async Task<Merchant> GetOrCreateAsync(string name)
|
|
{
|
|
var trimmedName = name.Trim();
|
|
|
|
var existing = await _db.Merchants
|
|
.FirstOrDefaultAsync(m => m.Name == trimmedName);
|
|
|
|
if (existing != null)
|
|
return existing;
|
|
|
|
var merchant = new Merchant { Name = trimmedName };
|
|
_db.Merchants.Add(merchant);
|
|
await _db.SaveChangesAsync();
|
|
|
|
return merchant;
|
|
}
|
|
|
|
public async Task<int?> GetOrCreateIdAsync(string? name)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(name))
|
|
return null;
|
|
|
|
var merchant = await GetOrCreateAsync(name);
|
|
return merchant.Id;
|
|
}
|
|
|
|
public async Task<Merchant?> GetMerchantByIdAsync(int id, bool includeRelated = false)
|
|
{
|
|
var query = _db.Merchants.AsQueryable();
|
|
|
|
if (includeRelated)
|
|
{
|
|
query = query
|
|
.Include(m => m.Transactions)
|
|
.Include(m => m.CategoryMappings);
|
|
}
|
|
|
|
return await query.FirstOrDefaultAsync(m => m.Id == id);
|
|
}
|
|
|
|
public async Task<List<MerchantWithStats>> GetAllMerchantsWithStatsAsync()
|
|
{
|
|
var merchants = await _db.Merchants
|
|
.Include(m => m.Transactions)
|
|
.Include(m => m.CategoryMappings)
|
|
.OrderBy(m => m.Name)
|
|
.ToListAsync();
|
|
|
|
return merchants.Select(m => new MerchantWithStats
|
|
{
|
|
Id = m.Id,
|
|
Name = m.Name,
|
|
TransactionCount = m.Transactions.Count,
|
|
MappingCount = m.CategoryMappings.Count
|
|
}).ToList();
|
|
}
|
|
|
|
public async Task<MerchantUpdateResult> UpdateMerchantAsync(int id, string newName)
|
|
{
|
|
var merchant = await _db.Merchants.FindAsync(id);
|
|
if (merchant == null)
|
|
{
|
|
return new MerchantUpdateResult
|
|
{
|
|
Success = false,
|
|
Message = "Merchant not found."
|
|
};
|
|
}
|
|
|
|
var trimmedName = newName.Trim();
|
|
|
|
// Check if another merchant with the same name exists
|
|
var existing = await _db.Merchants
|
|
.FirstOrDefaultAsync(m => m.Name == trimmedName && m.Id != id);
|
|
|
|
if (existing != null)
|
|
{
|
|
return new MerchantUpdateResult
|
|
{
|
|
Success = false,
|
|
Message = $"Merchant '{trimmedName}' already exists."
|
|
};
|
|
}
|
|
|
|
merchant.Name = trimmedName;
|
|
await _db.SaveChangesAsync();
|
|
|
|
return new MerchantUpdateResult
|
|
{
|
|
Success = true,
|
|
Message = "Merchant updated successfully."
|
|
};
|
|
}
|
|
|
|
public async Task<MerchantDeleteResult> DeleteMerchantAsync(int id)
|
|
{
|
|
var merchant = await _db.Merchants
|
|
.Include(m => m.Transactions)
|
|
.Include(m => m.CategoryMappings)
|
|
.FirstOrDefaultAsync(m => m.Id == id);
|
|
|
|
if (merchant == null)
|
|
{
|
|
return new MerchantDeleteResult
|
|
{
|
|
Success = false,
|
|
Message = "Merchant not found."
|
|
};
|
|
}
|
|
|
|
var transactionCount = merchant.Transactions.Count;
|
|
var mappingCount = merchant.CategoryMappings.Count;
|
|
|
|
_db.Merchants.Remove(merchant);
|
|
await _db.SaveChangesAsync();
|
|
|
|
return new MerchantDeleteResult
|
|
{
|
|
Success = true,
|
|
Message = $"Deleted merchant '{merchant.Name}'. {transactionCount} transactions and {mappingCount} category mappings are now unlinked.",
|
|
TransactionCount = transactionCount,
|
|
MappingCount = mappingCount
|
|
};
|
|
}
|
|
}
|
|
|
|
// DTOs
|
|
public class MerchantWithStats
|
|
{
|
|
public int Id { get; set; }
|
|
public string Name { get; set; } = "";
|
|
public int TransactionCount { get; set; }
|
|
public int MappingCount { get; set; }
|
|
}
|
|
|
|
public class MerchantUpdateResult
|
|
{
|
|
public bool Success { get; set; }
|
|
public string Message { get; set; } = "";
|
|
}
|
|
|
|
public class MerchantDeleteResult
|
|
{
|
|
public bool Success { get; set; }
|
|
public string Message { get; set; } = "";
|
|
public int TransactionCount { get; set; }
|
|
public int MappingCount { get; set; }
|
|
}
|
|
}
|