line-gestao-api/Controllers/ParcelamentosController.cs

427 lines
14 KiB
C#

using line_gestao_api.Data;
using line_gestao_api.Dtos;
using line_gestao_api.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace line_gestao_api.Controllers;
[ApiController]
[Route("api/parcelamentos")]
[Authorize]
public class ParcelamentosController : ControllerBase
{
private readonly AppDbContext _db;
public ParcelamentosController(AppDbContext db)
{
_db = db;
}
[HttpGet]
public async Task<ActionResult<PagedResult<ParcelamentoListDto>>> GetAll(
[FromQuery] int? anoRef,
[FromQuery] string? linha,
[FromQuery] string? cliente,
[FromQuery] int? competenciaAno,
[FromQuery] int? competenciaMes,
[FromQuery] int page = 1,
[FromQuery] int pageSize = 20)
{
page = page < 1 ? 1 : page;
pageSize = pageSize < 1 ? 20 : pageSize;
var query = _db.ParcelamentoLines.AsNoTracking();
if (anoRef.HasValue)
{
query = query.Where(x => x.AnoRef == anoRef.Value);
}
if (!string.IsNullOrWhiteSpace(linha))
{
var l = linha.Trim();
query = query.Where(x => x.Linha == l);
}
if (!string.IsNullOrWhiteSpace(cliente))
{
var c = cliente.Trim();
query = query.Where(x => x.Cliente != null && EF.Functions.ILike(x.Cliente, $"%{c}%"));
}
if (competenciaAno.HasValue && competenciaMes.HasValue)
{
var competencia = new DateOnly(competenciaAno.Value, competenciaMes.Value, 1);
query = query.Where(x => x.MonthValues.Any(m => m.Competencia == competencia));
}
var total = await query.CountAsync();
var today = DateOnly.FromDateTime(DateTime.Today);
var rows = await query
.OrderBy(x => x.Item)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.Select(x => new
{
x.Id,
x.AnoRef,
x.Item,
x.Linha,
x.Cliente,
x.QtParcelas,
x.ParcelaAtual,
x.TotalParcelas,
x.ValorCheio,
x.Desconto,
x.ValorComDesconto,
ParcelaAtualCalc = x.MonthValues.Count(m => m.Competencia <= today),
TotalMeses = x.MonthValues.Count()
})
.ToListAsync();
var items = rows.Select(x =>
{
var (parcelaAtual, totalParcelas) = ResolveParcelasFromCounts(
x.ParcelaAtual,
x.TotalParcelas,
x.QtParcelas,
x.ParcelaAtualCalc,
x.TotalMeses);
return new ParcelamentoListDto
{
Id = x.Id,
AnoRef = x.AnoRef,
Item = x.Item,
Linha = x.Linha,
Cliente = x.Cliente,
QtParcelas = BuildQtParcelas(parcelaAtual, totalParcelas, x.QtParcelas),
ParcelaAtual = parcelaAtual,
TotalParcelas = totalParcelas,
ValorCheio = x.ValorCheio,
Desconto = x.Desconto,
ValorComDesconto = x.ValorComDesconto
};
}).ToList();
return Ok(new PagedResult<ParcelamentoListDto>
{
Page = page,
PageSize = pageSize,
Total = total,
Items = items
});
}
[HttpGet("{id:guid}")]
public async Task<ActionResult<ParcelamentoDetailDto>> GetById(Guid id)
{
var entity = await _db.ParcelamentoLines
.AsNoTracking()
.Include(x => x.MonthValues)
.FirstOrDefaultAsync(x => x.Id == id);
if (entity == null)
{
return NotFound();
}
var monthValues = entity.MonthValues
.OrderBy(m => m.Competencia)
.Select(m => new ParcelamentoMonthDto
{
Competencia = m.Competencia,
Valor = m.Valor
})
.ToList();
var (parcelaAtual, totalParcelas) = ResolveParcelasFromCompetencias(
entity.ParcelaAtual,
entity.TotalParcelas,
entity.QtParcelas,
monthValues.Select(m => m.Competencia));
var dto = new ParcelamentoDetailDto
{
Id = entity.Id,
AnoRef = entity.AnoRef,
Item = entity.Item,
Linha = entity.Linha,
Cliente = entity.Cliente,
QtParcelas = BuildQtParcelas(parcelaAtual, totalParcelas, entity.QtParcelas),
ParcelaAtual = parcelaAtual,
TotalParcelas = totalParcelas,
ValorCheio = entity.ValorCheio,
Desconto = entity.Desconto,
ValorComDesconto = entity.ValorComDesconto,
MonthValues = monthValues,
AnnualRows = BuildAnnualRows(monthValues)
};
return Ok(dto);
}
[HttpPost]
public async Task<ActionResult<ParcelamentoDetailDto>> Create([FromBody] ParcelamentoUpsertDto req)
{
var now = DateTime.UtcNow;
var entity = new ParcelamentoLine
{
Id = Guid.NewGuid(),
AnoRef = req.AnoRef,
Item = req.Item,
Linha = TrimOrNull(req.Linha),
Cliente = TrimOrNull(req.Cliente),
QtParcelas = TrimOrNull(req.QtParcelas),
ParcelaAtual = req.ParcelaAtual,
TotalParcelas = req.TotalParcelas,
ValorCheio = req.ValorCheio,
Desconto = req.Desconto,
ValorComDesconto = ResolveValorComDesconto(req.ValorComDesconto, req.ValorCheio, req.Desconto),
CreatedAt = now,
UpdatedAt = now
};
if (req.AnoRef.HasValue && req.Item.HasValue)
{
var exists = await _db.ParcelamentoLines.AnyAsync(x => x.AnoRef == req.AnoRef && x.Item == req.Item);
if (exists) return Conflict("Já existe um parcelamento com o mesmo AnoRef e Item.");
}
entity.MonthValues = BuildMonthValues(req.MonthValues, entity.Id, now);
ApplyComputedParcelas(entity);
_db.ParcelamentoLines.Add(entity);
await _db.SaveChangesAsync();
return CreatedAtAction(nameof(GetById), new { id = entity.Id }, ToDetailDto(entity));
}
[HttpPut("{id:guid}")]
public async Task<IActionResult> Update(Guid id, [FromBody] ParcelamentoUpsertDto req)
{
var entity = await _db.ParcelamentoLines
.Include(x => x.MonthValues)
.FirstOrDefaultAsync(x => x.Id == id);
if (entity == null) return NotFound();
if (req.AnoRef.HasValue && req.Item.HasValue)
{
var exists = await _db.ParcelamentoLines.AnyAsync(x =>
x.Id != id && x.AnoRef == req.AnoRef && x.Item == req.Item);
if (exists) return Conflict("Já existe um parcelamento com o mesmo AnoRef e Item.");
}
entity.AnoRef = req.AnoRef;
entity.Item = req.Item;
entity.Linha = TrimOrNull(req.Linha);
entity.Cliente = TrimOrNull(req.Cliente);
entity.QtParcelas = TrimOrNull(req.QtParcelas);
entity.ParcelaAtual = req.ParcelaAtual;
entity.TotalParcelas = req.TotalParcelas;
entity.ValorCheio = req.ValorCheio;
entity.Desconto = req.Desconto;
entity.ValorComDesconto = ResolveValorComDesconto(req.ValorComDesconto, req.ValorCheio, req.Desconto);
entity.UpdatedAt = DateTime.UtcNow;
_db.ParcelamentoMonthValues.RemoveRange(entity.MonthValues);
entity.MonthValues = BuildMonthValues(req.MonthValues, entity.Id, entity.UpdatedAt);
ApplyComputedParcelas(entity);
await _db.SaveChangesAsync();
return NoContent();
}
[HttpDelete("{id:guid}")]
[Authorize(Roles = "admin")]
public async Task<IActionResult> Delete(Guid id)
{
var entity = await _db.ParcelamentoLines.FirstOrDefaultAsync(x => x.Id == id);
if (entity == null) return NotFound();
_db.ParcelamentoLines.Remove(entity);
await _db.SaveChangesAsync();
return NoContent();
}
private static List<ParcelamentoAnnualRowDto> BuildAnnualRows(List<ParcelamentoMonthDto> monthValues)
{
if (monthValues.Count == 0) return new List<ParcelamentoAnnualRowDto>();
var groups = monthValues
.GroupBy(m => m.Competencia.Year)
.OrderBy(g => g.Key);
var rows = new List<ParcelamentoAnnualRowDto>();
foreach (var group in groups)
{
var monthMap = group
.GroupBy(m => m.Competencia.Month)
.ToDictionary(g => g.Key, g => g.Sum(x => x.Valor ?? 0m));
var months = Enumerable.Range(1, 12)
.Select(month => new ParcelamentoAnnualMonthDto
{
Month = month,
Valor = monthMap.TryGetValue(month, out var value) ? value : null
})
.ToList();
var total = months.Sum(m => m.Valor ?? 0m);
rows.Add(new ParcelamentoAnnualRowDto
{
Year = group.Key,
Total = total,
Months = months
});
}
return rows;
}
private static List<ParcelamentoMonthValue> BuildMonthValues(
List<ParcelamentoMonthInputDto> inputs,
Guid parcelamentoId,
DateTime now)
{
var map = new Dictionary<DateOnly, decimal?>();
foreach (var input in inputs ?? new List<ParcelamentoMonthInputDto>())
{
if (input.Competencia == default) continue;
map[input.Competencia] = input.Valor;
}
return map
.OrderBy(x => x.Key)
.Select(x => new ParcelamentoMonthValue
{
Id = Guid.NewGuid(),
ParcelamentoLineId = parcelamentoId,
Competencia = x.Key,
Valor = x.Value,
CreatedAt = now
})
.ToList();
}
private static void ApplyComputedParcelas(ParcelamentoLine entity)
{
var competencias = entity.MonthValues.Select(m => m.Competencia).ToList();
var (parcelaAtual, totalParcelas) = ResolveParcelasFromCompetencias(
entity.ParcelaAtual,
entity.TotalParcelas,
entity.QtParcelas,
competencias);
entity.ParcelaAtual = parcelaAtual;
entity.TotalParcelas = totalParcelas;
entity.QtParcelas = BuildQtParcelas(parcelaAtual, totalParcelas, entity.QtParcelas);
}
private static ParcelamentoDetailDto ToDetailDto(ParcelamentoLine entity)
{
var monthValues = entity.MonthValues
.OrderBy(m => m.Competencia)
.Select(m => new ParcelamentoMonthDto
{
Competencia = m.Competencia,
Valor = m.Valor
})
.ToList();
var (parcelaAtual, totalParcelas) = ResolveParcelasFromCompetencias(
entity.ParcelaAtual,
entity.TotalParcelas,
entity.QtParcelas,
monthValues.Select(m => m.Competencia));
return new ParcelamentoDetailDto
{
Id = entity.Id,
AnoRef = entity.AnoRef,
Item = entity.Item,
Linha = entity.Linha,
Cliente = entity.Cliente,
QtParcelas = BuildQtParcelas(parcelaAtual, totalParcelas, entity.QtParcelas),
ParcelaAtual = parcelaAtual,
TotalParcelas = totalParcelas,
ValorCheio = entity.ValorCheio,
Desconto = entity.Desconto,
ValorComDesconto = entity.ValorComDesconto,
MonthValues = monthValues,
AnnualRows = BuildAnnualRows(monthValues)
};
}
private static (int? ParcelaAtual, int? TotalParcelas) ResolveParcelasFromCounts(
int? storedAtual,
int? storedTotal,
string? qtParcelas,
int mesesAteHoje,
int totalMeses)
{
var total = storedTotal ?? ParseQtParcelas(qtParcelas).Total ?? (totalMeses > 0 ? totalMeses : (int?)null);
int? atual = totalMeses > 0 ? mesesAteHoje : storedAtual ?? ParseQtParcelas(qtParcelas).Atual;
if (atual.HasValue && atual.Value < 0) atual = 0;
if (total.HasValue && atual.HasValue)
{
atual = Math.Min(atual.Value, total.Value);
}
return (atual, total);
}
private static (int? ParcelaAtual, int? TotalParcelas) ResolveParcelasFromCompetencias(
int? storedAtual,
int? storedTotal,
string? qtParcelas,
IEnumerable<DateOnly> competencias)
{
var list = competencias?.ToList() ?? new List<DateOnly>();
var totalMeses = list.Count;
var hoje = DateOnly.FromDateTime(DateTime.Today);
var mesesAteHoje = list.Count(c => c <= hoje);
return ResolveParcelasFromCounts(storedAtual, storedTotal, qtParcelas, mesesAteHoje, totalMeses);
}
private static (int? Atual, int? Total) ParseQtParcelas(string? raw)
{
if (string.IsNullOrWhiteSpace(raw)) return (null, null);
var parts = raw.Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
int? atual = null;
int? total = null;
if (parts.Length >= 1 && int.TryParse(OnlyDigits(parts[0]), out var a)) atual = a;
if (parts.Length >= 2 && int.TryParse(OnlyDigits(parts[1]), out var t)) total = t;
return (atual, total);
}
private static string? BuildQtParcelas(int? atual, int? total, string? fallback)
{
if (atual.HasValue && total.HasValue) return $"{atual}/{total}";
return string.IsNullOrWhiteSpace(fallback) ? null : fallback.Trim();
}
private static decimal? ResolveValorComDesconto(decimal? valorCom, decimal? valorCheio, decimal? desconto)
{
if (valorCom.HasValue) return valorCom;
if (!valorCheio.HasValue) return null;
return Math.Max(0, valorCheio.Value - (desconto ?? 0m));
}
private static string? TrimOrNull(string? s)
{
if (string.IsNullOrWhiteSpace(s)) return null;
return s.Trim();
}
private static string OnlyDigits(string? s)
{
if (string.IsNullOrWhiteSpace(s)) return "";
return new string(s.Where(char.IsDigit).ToArray());
}
}