263 lines
12 KiB
C#
263 lines
12 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using Rs_system.Data;
|
|
using Rs_system.Models;
|
|
using Rs_system.Models.ViewModels;
|
|
|
|
namespace Rs_system.Services;
|
|
|
|
public class DiezmoCierreService : IDiezmoCierreService
|
|
{
|
|
private readonly ApplicationDbContext _context;
|
|
private readonly IDiezmoCalculoService _calculo;
|
|
|
|
public DiezmoCierreService(ApplicationDbContext context, IDiezmoCalculoService calculo)
|
|
{
|
|
_context = context;
|
|
_calculo = calculo;
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────────────────────────────
|
|
// Catálogos
|
|
// ──────────────────────────────────────────────────────────────────────────
|
|
|
|
public async Task<List<DiezmoTipoSalida>> GetTiposSalidaActivosAsync()
|
|
=> await _context.DiezmoTiposSalida
|
|
.Where(t => t.Activo && !t.Eliminado)
|
|
.OrderBy(t => t.Nombre)
|
|
.ToListAsync();
|
|
|
|
public async Task<List<DiezmoBeneficiario>> GetBeneficiariosActivosAsync()
|
|
=> await _context.DiezmoBeneficiarios
|
|
.Where(b => b.Activo && !b.Eliminado)
|
|
.OrderBy(b => b.Nombre)
|
|
.ToListAsync();
|
|
|
|
// ──────────────────────────────────────────────────────────────────────────
|
|
// Cierres
|
|
// ──────────────────────────────────────────────────────────────────────────
|
|
|
|
public async Task<List<DiezmoCierre>> GetCierresAsync(int? anio = null)
|
|
{
|
|
var query = _context.DiezmoCierres
|
|
.Where(c => !c.Eliminado);
|
|
|
|
if (anio.HasValue)
|
|
query = query.Where(c => c.Fecha.Year == anio.Value);
|
|
|
|
return await query
|
|
.OrderByDescending(c => c.Fecha)
|
|
.ToListAsync();
|
|
}
|
|
|
|
public async Task<DiezmoCierre?> GetCierreByIdAsync(long id)
|
|
=> await _context.DiezmoCierres
|
|
.Include(c => c.Detalles.Where(d => !d.Eliminado))
|
|
.ThenInclude(d => d.Miembro)
|
|
.ThenInclude(m => m.Persona)
|
|
.Include(c => c.Salidas.Where(s => !s.Eliminado))
|
|
.ThenInclude(s => s.TipoSalida)
|
|
.Include(c => c.Salidas.Where(s => !s.Eliminado))
|
|
.ThenInclude(s => s.Beneficiario)
|
|
.FirstOrDefaultAsync(c => c.Id == id && !c.Eliminado);
|
|
|
|
public async Task<DiezmoCierre> CrearCierreAsync(DateOnly fecha, string? observaciones, string creadoPor)
|
|
{
|
|
// Verificar que no exista ya un cierre para esa fecha
|
|
var yaExiste = await _context.DiezmoCierres
|
|
.AnyAsync(c => c.Fecha == fecha && !c.Eliminado);
|
|
|
|
if (yaExiste)
|
|
throw new InvalidOperationException($"Ya existe un cierre para la fecha {fecha:dd/MM/yyyy}.");
|
|
|
|
var cierre = new DiezmoCierre
|
|
{
|
|
Fecha = fecha,
|
|
Observaciones = observaciones,
|
|
CreadoPor = creadoPor,
|
|
CreadoEn = DateTime.UtcNow,
|
|
ActualizadoEn = DateTime.UtcNow
|
|
};
|
|
|
|
_context.DiezmoCierres.Add(cierre);
|
|
await _context.SaveChangesAsync();
|
|
return cierre;
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────────────────────────────
|
|
// Detalles
|
|
// ──────────────────────────────────────────────────────────────────────────
|
|
|
|
public async Task AgregarDetalleAsync(long cierreId, DiezmoDetalleFormViewModel vm, string usuario)
|
|
{
|
|
var cierre = await GetCierreOrThrowAsync(cierreId);
|
|
GuardarSiAbierto(cierre);
|
|
|
|
// Se invierte la lógica: El net (Diezmo) se introduce manual. El cambio es derivado.
|
|
var neto = vm.MontoNeto;
|
|
var cambio = vm.MontoEntregado - neto;
|
|
if (cambio < 0) cambio = 0; // Prevenir errores; si entregó menos del neto se asume cambio 0
|
|
|
|
var detalle = new DiezmoDetalle
|
|
{
|
|
DiezmoCierreId = cierreId,
|
|
MiembroId = vm.MiembroId,
|
|
MontoEntregado = vm.MontoEntregado,
|
|
CambioEntregado = cambio,
|
|
MontoNeto = neto,
|
|
Observaciones = vm.Observaciones,
|
|
Fecha = DateTime.UtcNow,
|
|
CreadoPor = usuario,
|
|
CreadoEn = DateTime.UtcNow,
|
|
ActualizadoEn = DateTime.UtcNow
|
|
};
|
|
|
|
_context.DiezmoDetalles.Add(detalle);
|
|
await _context.SaveChangesAsync();
|
|
await RecalcularTotalesAsync(cierreId);
|
|
}
|
|
|
|
public async Task EliminarDetalleAsync(long detalleId, string usuario)
|
|
{
|
|
var detalle = await _context.DiezmoDetalles
|
|
.FirstOrDefaultAsync(d => d.Id == detalleId && !d.Eliminado)
|
|
?? throw new InvalidOperationException("Detalle no encontrado.");
|
|
|
|
var cierre = await GetCierreOrThrowAsync(detalle.DiezmoCierreId);
|
|
GuardarSiAbierto(cierre);
|
|
|
|
detalle.Eliminado = true;
|
|
detalle.ActualizadoEn = DateTime.UtcNow;
|
|
detalle.ActualizadoPor = usuario;
|
|
await _context.SaveChangesAsync();
|
|
|
|
await RegistrarBitacoraAsync(detalle.DiezmoCierreId, "ELIMINAR_DETALLE",
|
|
$"Detalle #{detalleId} eliminado", usuario);
|
|
await RecalcularTotalesAsync(detalle.DiezmoCierreId);
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────────────────────────────
|
|
// Salidas
|
|
// ──────────────────────────────────────────────────────────────────────────
|
|
|
|
public async Task AgregarSalidaAsync(long cierreId, DiezmoSalidaFormViewModel vm, string usuario)
|
|
{
|
|
var cierre = await GetCierreOrThrowAsync(cierreId);
|
|
GuardarSiAbierto(cierre);
|
|
|
|
var salida = new DiezmoSalida
|
|
{
|
|
DiezmoCierreId = cierreId,
|
|
TipoSalidaId = vm.TipoSalidaId,
|
|
BeneficiarioId = vm.BeneficiarioId,
|
|
Monto = vm.Monto,
|
|
Concepto = vm.Concepto,
|
|
Fecha = DateTime.UtcNow,
|
|
CreadoPor = usuario,
|
|
CreadoEn = DateTime.UtcNow,
|
|
ActualizadoEn = DateTime.UtcNow
|
|
};
|
|
|
|
_context.DiezmoSalidas.Add(salida);
|
|
await _context.SaveChangesAsync();
|
|
await RecalcularTotalesAsync(cierreId);
|
|
}
|
|
|
|
public async Task EliminarSalidaAsync(long salidaId, string usuario)
|
|
{
|
|
var salida = await _context.DiezmoSalidas
|
|
.FirstOrDefaultAsync(s => s.Id == salidaId && !s.Eliminado)
|
|
?? throw new InvalidOperationException("Salida no encontrada.");
|
|
|
|
var cierre = await GetCierreOrThrowAsync(salida.DiezmoCierreId);
|
|
GuardarSiAbierto(cierre);
|
|
|
|
salida.Eliminado = true;
|
|
salida.ActualizadoEn = DateTime.UtcNow;
|
|
await _context.SaveChangesAsync();
|
|
|
|
await RegistrarBitacoraAsync(salida.DiezmoCierreId, "ELIMINAR_SALIDA",
|
|
$"Salida #{salidaId} eliminada", usuario);
|
|
await RecalcularTotalesAsync(salida.DiezmoCierreId);
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────────────────────────────
|
|
// Flujo de cierre / reapertura
|
|
// ──────────────────────────────────────────────────────────────────────────
|
|
|
|
public async Task CerrarCierreAsync(long cierreId, string usuario)
|
|
{
|
|
var cierre = await GetCierreByIdAsync(cierreId)
|
|
?? throw new InvalidOperationException("Cierre no encontrado.");
|
|
|
|
if (cierre.Cerrado)
|
|
throw new InvalidOperationException("El cierre ya se encuentra cerrado.");
|
|
|
|
// Recalcular por si hay cambios recientes antes de sellar
|
|
_calculo.RecalcularTotales(cierre);
|
|
|
|
cierre.Cerrado = true;
|
|
cierre.FechaCierre = DateTime.UtcNow;
|
|
cierre.CerradoPor = usuario;
|
|
cierre.ActualizadoEn = DateTime.UtcNow;
|
|
cierre.ActualizadoPor = usuario;
|
|
|
|
await _context.SaveChangesAsync();
|
|
await RegistrarBitacoraAsync(cierreId, "CIERRE", $"Cierre sellado. Saldo final: {cierre.SaldoFinal:C}", usuario);
|
|
}
|
|
|
|
public async Task ReabrirCierreAsync(long cierreId, string usuario)
|
|
{
|
|
var cierre = await GetCierreOrThrowAsync(cierreId);
|
|
|
|
if (!cierre.Cerrado)
|
|
throw new InvalidOperationException("El cierre ya se encuentra abierto.");
|
|
|
|
cierre.Cerrado = false;
|
|
cierre.FechaCierre = null;
|
|
cierre.CerradoPor = null;
|
|
cierre.ActualizadoEn = DateTime.UtcNow;
|
|
cierre.ActualizadoPor = usuario;
|
|
|
|
await _context.SaveChangesAsync();
|
|
await RegistrarBitacoraAsync(cierreId, "REAPERTURA", "Cierre reabierto", usuario);
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────────────────────────────
|
|
// Totales
|
|
// ──────────────────────────────────────────────────────────────────────────
|
|
|
|
public async Task RecalcularTotalesAsync(long cierreId)
|
|
{
|
|
var cierre = await GetCierreByIdAsync(cierreId)
|
|
?? throw new InvalidOperationException("Cierre no encontrado.");
|
|
|
|
_calculo.RecalcularTotales(cierre);
|
|
cierre.ActualizadoEn = DateTime.UtcNow;
|
|
await _context.SaveChangesAsync();
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────────────────────────────
|
|
// Helpers privados
|
|
// ──────────────────────────────────────────────────────────────────────────
|
|
|
|
private async Task<DiezmoCierre> GetCierreOrThrowAsync(long id)
|
|
=> await _context.DiezmoCierres.FirstOrDefaultAsync(c => c.Id == id && !c.Eliminado)
|
|
?? throw new InvalidOperationException("Cierre no encontrado.");
|
|
|
|
private static void GuardarSiAbierto(DiezmoCierre cierre)
|
|
{
|
|
if (cierre.Cerrado)
|
|
throw new InvalidOperationException("No se puede modificar un cierre que ya está cerrado.");
|
|
}
|
|
|
|
private async Task RegistrarBitacoraAsync(long cierreId, string accion, string detalle, string usuario)
|
|
{
|
|
await _context.Database.ExecuteSqlRawAsync(
|
|
"""
|
|
INSERT INTO public.diezmo_bitacora (diezmo_cierre_id, accion, detalle, realizado_por, realizado_en)
|
|
VALUES ({0}, {1}, {2}, {3}, {4})
|
|
""",
|
|
cierreId, accion, detalle, usuario, DateTime.UtcNow);
|
|
}
|
|
}
|