using Fitness_core_BE.DTOs.Ledger; using Fitness_core_BE.Exceptions; using Fitness_core_BE.Helpers; using Fitness_core_BE.Infra.UoW; using Fitness_core_BE.Interfaces; using Fitness_core_BE.Interfaces.Services; using Fitness_core_BE.Models; namespace Fitness_core_BE.Services { public class ProfitLossService : IProfitLossService { private readonly IProfitLossRepository _repository; private readonly UserRoleHelper _userRoleHelper; private readonly IUnitOfWork _uow; public ProfitLossService( IProfitLossRepository repository, UserRoleHelper userRoleHelper, IUnitOfWork uow) { _repository = repository; _userRoleHelper = userRoleHelper; _uow = uow; } public async Task GetProfitLossAsync(DateTime from, DateTime to, HttpContext httpContext) { var clientId = await GetClientIdAsync(httpContext); var (fromUtc, toUtc) = NormalizeRange(from, to); var entries = await _repository.GetDerivedEntriesAsync( clientId, fromUtc, toUtc); // Manual entries. var manualEntries = await GetManualEntriesRawAsync(clientId, fromUtc, toUtc); entries.AddRange(manualEntries.Select(ToDto)); var totalIncome = entries.Where(e => e.EntryType == "Income").Sum(e => e.Amount); var totalExpense = entries.Where(e => e.EntryType == "Expense").Sum(e => e.Amount); return new ProfitLossDto { From = fromUtc, To = toUtc, TotalIncome = totalIncome, TotalExpense = totalExpense, NetProfit = totalIncome - totalExpense, IncomeByCategory = entries.Where(e => e.EntryType == "Income") .GroupBy(e => e.Category) .Select(g => new CategoryTotalDto { Category = g.Key, Amount = g.Sum(e => e.Amount) }) .OrderByDescending(c => c.Amount) .ToList(), ExpenseByCategory = entries.Where(e => e.EntryType == "Expense") .GroupBy(e => e.Category) .Select(g => new CategoryTotalDto { Category = g.Key, Amount = g.Sum(e => e.Amount) }) .OrderByDescending(c => c.Amount) .ToList(), Entries = entries.OrderByDescending(e => e.EntryDate).ToList() }; } public async Task> GetManualEntriesAsync(DateTime from, DateTime to, HttpContext httpContext) { var clientId = await GetClientIdAsync(httpContext); var (fromUtc, toUtc) = NormalizeRange(from, to); var entries = await GetManualEntriesRawAsync(clientId, fromUtc, toUtc); return entries.Select(ToDto).OrderByDescending(e => e.EntryDate).ToList(); } public async Task AddManualEntryAsync(AddLedgerEntryRequest request, HttpContext httpContext) { var clientId = await GetClientIdAsync(httpContext); if (request.EntryType != "Income" && request.EntryType != "Expense") throw new ArgumentException("entryType must be 'Income' or 'Expense'."); if (string.IsNullOrWhiteSpace(request.Category)) throw new ArgumentException("category is required."); if (request.Amount <= 0) throw new ArgumentException("amount must be greater than zero."); await _uow.BeginAsync(); try { var entry = new LedgerEntry { Id = Guid.NewGuid(), FKClientId = clientId, EntryType = request.EntryType, Category = request.Category, Description = request.Description, Amount = request.Amount, EntryDate = request.EntryDate ?? DateTime.UtcNow, CreatedAt = DateTime.UtcNow }; await _repository.AddManualEntryAsync(entry); await _uow.CommitAsync(); return ToDto(entry); } catch { await _uow.RollbackAsync(); throw; } } public async Task DeleteManualEntryAsync(Guid id, HttpContext httpContext) { var clientId = await GetClientIdAsync(httpContext); var entry = await _repository.GetManualEntryByIdAsync(id) ?? throw new NotFoundException("Ledger entry not found."); if (entry.FKClientId != clientId) throw new ForbiddenException("This ledger entry does not belong to your gym."); await _uow.BeginAsync(); try { _repository.RemoveManualEntry(entry); await _uow.CommitAsync(); } catch { await _uow.RollbackAsync(); throw; } } private async Task> GetManualEntriesRawAsync(Guid clientId, DateTime fromUtc, DateTime toUtc) { return await _repository.GetManualEntriesAsync(clientId, fromUtc, toUtc); } private async Task GetClientIdAsync(HttpContext httpContext) { var userId = DataExtractor.GetUserIdFromContext(httpContext); return await _userRoleHelper.GetClientId(userId); } private static (DateTime From, DateTime To) NormalizeRange(DateTime from, DateTime to) { var fromUtc = DateTime.SpecifyKind(from.Date, DateTimeKind.Utc); // Treat `to` as inclusive of the whole day. var toUtc = DateTime.SpecifyKind(to.Date.AddDays(1), DateTimeKind.Utc); return (fromUtc, toUtc); } private static LedgerEntryDto ToDto(LedgerEntry entry) => new() { Id = entry.Id, EntryType = entry.EntryType, Category = entry.Category, Description = entry.Description, Amount = entry.Amount, EntryDate = entry.EntryDate, IsManual = true }; } }