Feature: expand MerchantService with full CRUD operations

Extend MerchantService with additional operations:
- GetMerchantByIdAsync: Retrieve merchant with optional related data
- GetAllMerchantsWithStatsAsync: Get all merchants with transaction/mapping counts
- UpdateMerchantAsync: Update merchant name with duplicate validation
- DeleteMerchantAsync: Delete merchant (unlinks transactions and mappings)

Includes DTOs for merchant stats, update results, and delete results. Consolidates merchant management logic previously scattered across PageModels.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
AJ
2025-10-25 23:08:13 -04:00
parent 77cab2595f
commit c09a8c36a8

View File

@@ -9,6 +9,10 @@ namespace MoneyMap.Services
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
@@ -54,5 +58,127 @@ namespace MoneyMap.Services
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; }
}
}