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:
@@ -9,6 +9,10 @@ namespace MoneyMap.Services
|
|||||||
Task<Merchant?> FindByNameAsync(string name);
|
Task<Merchant?> FindByNameAsync(string name);
|
||||||
Task<Merchant> GetOrCreateAsync(string name);
|
Task<Merchant> GetOrCreateAsync(string name);
|
||||||
Task<int?> GetOrCreateIdAsync(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
|
public class MerchantService : IMerchantService
|
||||||
@@ -54,5 +58,127 @@ namespace MoneyMap.Services
|
|||||||
var merchant = await GetOrCreateAsync(name);
|
var merchant = await GetOrCreateAsync(name);
|
||||||
return merchant.Id;
|
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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user