162 lines
6.2 KiB
C#
162 lines
6.2 KiB
C#
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<ProfitLossDto> 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<List<LedgerEntryDto>> 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<LedgerEntryDto> 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<List<LedgerEntry>> GetManualEntriesRawAsync(Guid clientId, DateTime fromUtc, DateTime toUtc)
|
|
{
|
|
return await _repository.GetManualEntriesAsync(clientId, fromUtc, toUtc);
|
|
}
|
|
|
|
private async Task<Guid> 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
|
|
};
|
|
}
|
|
}
|