< Summary

Information
Class: FAU.Logica.Services.AguinaldoReciboService
Assembly: FAU.Logica
File(s): /home/runner/work/Sistema.Liquidacion.BE/Sistema.Liquidacion.BE/FAU.Logica/Services/AguinaldoReciboService.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 332
Coverable lines: 332
Total lines: 464
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 104
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/Sistema.Liquidacion.BE/Sistema.Liquidacion.BE/FAU.Logica/Services/AguinaldoReciboService.cs

#LineLine coverage
 1using System.Globalization;
 2using System.IO.Compression;
 3using System.Reflection;
 4using FAU.DataAccess;
 5using FAU.Entidades;
 6using Microsoft.EntityFrameworkCore;
 7using QuestPDF.Fluent;
 8using QuestPDF.Helpers;
 9using QuestPDF.Infrastructure;
 10
 11namespace FAU.Logica.Services;
 12
 13public class AguinaldoReciboService : IAguinaldoReciboService
 14{
 15    private readonly ApplicationDbContext _context;
 16
 017    public AguinaldoReciboService(ApplicationDbContext context)
 18    {
 019        _context = context;
 020    }
 21
 22    // ── SEMESTRAL ────────────────────────────────────────────────────────────
 23
 24    public async Task<(byte[] Pdf, string FileName)> GenerarPdfSemestralAsync(long personaId, int anio, short semestre)
 25    {
 026        var (aguinaldo, relacion) = await CargarDatosSemestralAsync(personaId, anio, semestre);
 027        var mes = semestre == 1 ? 6 : 12;
 028        var fileName = $"aguinaldo_{anio}_{mes:D2}_{relacion.Persona.Cedula}.pdf";
 029        return (GenerarPdfSemestral(aguinaldo, relacion), fileName);
 030    }
 31
 32    public async Task<(Stream Zip, string FileName)> GenerarLoteSemestralZipAsync(int anio, short semestre, IEnumerable<
 33    {
 034        var query = _context.Aguinaldos
 035            .Where(a => a.Anio == anio && a.Semestre == semestre);
 36
 037        if (personaIds != null)
 38        {
 039            var ids = personaIds.ToList();
 040            query = query.Where(a => ids.Contains(a.PersonaId));
 41        }
 42
 043        var aguinaldos = await query.ToListAsync();
 44        var personaIdsUsados = aguinaldos.Select(a => a.PersonaId).ToList();
 045        var relacionesPorPersona = await CargarRelacionesPorPersonaAsync(personaIdsUsados);
 46
 047        var mes = semestre == 1 ? 6 : 12;
 048        var memoryStream = new MemoryStream();
 049        using (var zip = new ZipArchive(memoryStream, ZipArchiveMode.Create, leaveOpen: true))
 50        {
 051            foreach (var aguinaldo in aguinaldos)
 52            {
 053                if (!relacionesPorPersona.TryGetValue(aguinaldo.PersonaId, out var relacion))
 54                    continue;
 55
 056                var pdfBytes = GenerarPdfSemestral(aguinaldo, relacion);
 057                var entryName = $"aguinaldo_{anio}_{mes:D2}_{relacion.Persona.Cedula}.pdf";
 058                var entry = zip.CreateEntry(entryName, CompressionLevel.Fastest);
 059                using var entryStream = entry.Open();
 060                await entryStream.WriteAsync(pdfBytes);
 061            }
 062        }
 63
 064        memoryStream.Position = 0;
 065        return (memoryStream, $"aguinaldos_{anio}_{mes:D2}.zip");
 066    }
 67
 68    // ── BAJA ─────────────────────────────────────────────────────────────────
 69
 70    public async Task<(byte[] Pdf, string FileName)> GenerarPdfBajaAsync(long aguinaldoBajaId)
 71    {
 072        var baja = await CargarAguinaldoBajaAsync(aguinaldoBajaId);
 073        var relacion = await CargarRelacionActivaPorPersonaIdAsync(baja.PersonaId);
 074        var fileName = $"aguinaldo_{baja.Anio}_{baja.Mes:D2}_{baja.Cedula}.pdf";
 075        return (GenerarPdfBaja(baja, relacion), fileName);
 076    }
 77
 78    public async Task<(Stream Zip, string FileName)> GenerarLoteBajaZipAsync(IEnumerable<long> aguinaldoBajaIds)
 79    {
 080        var ids = aguinaldoBajaIds.ToList();
 081        var bajas = await _context.AguinaldosBaja
 082            .Include(b => b.Items)
 083            .Where(b => ids.Contains(b.Id))
 084            .ToListAsync();
 85
 86        var personaIdsUsados = bajas.Select(b => b.PersonaId).ToList();
 087        var relacionesPorPersona = await CargarRelacionesPorPersonaAsync(personaIdsUsados);
 88
 089        var memoryStream = new MemoryStream();
 090        using (var zip = new ZipArchive(memoryStream, ZipArchiveMode.Create, leaveOpen: true))
 91        {
 092            foreach (var baja in bajas)
 93            {
 094                relacionesPorPersona.TryGetValue(baja.PersonaId, out var relacion);
 095                var pdfBytes = GenerarPdfBaja(baja, relacion);
 096                var entryName = $"aguinaldo_{baja.Anio}_{baja.Mes:D2}_{baja.Cedula}.pdf";
 097                var entry = zip.CreateEntry(entryName, CompressionLevel.Fastest);
 098                using var entryStream = entry.Open();
 099                await entryStream.WriteAsync(pdfBytes);
 0100            }
 0101        }
 102
 0103        memoryStream.Position = 0;
 0104        var zipName = bajas.Count > 0
 0105            ? $"aguinaldos_{bajas[0].Anio}_{bajas[0].Mes:D2}.zip"
 0106            : "aguinaldos_baja.zip";
 0107        return (memoryStream, zipName);
 0108    }
 109
 110    // ── DATA LOADING ──────────────────────────────────────────────────────────
 111
 112    private async Task<(Aguinaldo aguinaldo, RelacionLaboral relacion)> CargarDatosSemestralAsync(
 113        long personaId, int anio, short semestre)
 114    {
 0115        var aguinaldo = await _context.Aguinaldos
 0116            .FirstOrDefaultAsync(a => a.PersonaId == personaId && a.Anio == anio && a.Semestre == semestre)
 0117            ?? throw new KeyNotFoundException(
 0118                $"No se encontró aguinaldo para persona {personaId}, año {anio}, semestre {semestre}.");
 119
 0120        var relacion = await CargarRelacionActivaPorPersonaIdAsync(personaId);
 0121        return (aguinaldo, relacion);
 0122    }
 123
 124    private async Task<RelacionLaboral> CargarRelacionActivaPorPersonaIdAsync(long personaId)
 125    {
 0126        return await _context.RelacionesLaborales
 0127            .Include(rl => rl.Persona)
 0128            .Include(rl => rl.Grado)
 0129            .Include(rl => rl.Escalafon)
 0130            .Include(rl => rl.Unidad)
 0131            .Where(rl => rl.PersonaId == personaId && rl.Estado == EstadoRelacion.Activo)
 0132            .FirstOrDefaultAsync()
 0133            ?? throw new KeyNotFoundException($"No se encontró relación laboral activa para persona {personaId}.");
 0134    }
 135
 136    private async Task<Dictionary<long, RelacionLaboral>> CargarRelacionesPorPersonaAsync(List<long> personaIds)
 137    {
 0138        var relaciones = await _context.RelacionesLaborales
 0139            .Include(rl => rl.Persona)
 0140            .Include(rl => rl.Grado)
 0141            .Include(rl => rl.Escalafon)
 0142            .Include(rl => rl.Unidad)
 0143            .Where(rl => personaIds.Contains(rl.PersonaId) && rl.Estado == EstadoRelacion.Activo)
 0144            .ToListAsync();
 145
 146        return relaciones.ToDictionary(rl => rl.PersonaId);
 0147    }
 148
 149    private async Task<AguinaldoBaja> CargarAguinaldoBajaAsync(long aguinaldoBajaId)
 150    {
 0151        return await _context.AguinaldosBaja
 0152            .Include(b => b.Items)
 0153            .FirstOrDefaultAsync(b => b.Id == aguinaldoBajaId)
 0154            ?? throw new KeyNotFoundException($"No se encontró aguinaldo por baja con id {aguinaldoBajaId}.");
 0155    }
 156
 157    // ── PDF BUILDERS ──────────────────────────────────────────────────────────
 158
 159    private static byte[] GenerarPdfSemestral(Aguinaldo ag, RelacionLaboral relacion)
 160    {
 0161        var ci = CultureInfo.GetCultureInfo("es-UY");
 162        // Semestre 1 → junio (mes 6), Semestre 2 → diciembre (mes 12)
 0163        var mes = ag.Semestre == 1 ? 6 : 12;
 0164        var nombreMes = ci.TextInfo.ToTitleCase(ci.DateTimeFormat.GetMonthName(mes));
 0165        var tituloPeriodo = $"MES DE : {nombreMes} {ag.Anio}";
 166
 0167        var nominal = ag.Nominal ?? 0m;
 0168        var irpf = ag.IrpfAguinaldo;
 0169        var montepio = ag.Montepio ?? 0m;
 0170        var sanidad = ag.Sanidad ?? 0m;
 0171        var totalDescuentos = ag.TotalDescuentos ?? 0m;
 0172        var liquido = ag.Liquido ?? 0m;
 0173        var redondeo = ag.Redondeo;
 174
 0175        var haberes = new List<(string Desc, decimal Monto)>
 0176        {
 0177            ("AGUINALDO MILITAR", nominal)
 0178        };
 179
 0180        var descuentos = new List<(string Desc, decimal Monto)>
 0181        {
 0182            ("IRPF", irpf),
 0183            ("MONTEPÍO", montepio),
 0184            ("D.N.S.FF.AA.", sanidad)
 0185        };
 186
 0187        return GenerarPdfAguinaldo(
 0188            tituloPeriodo, relacion,
 0189            haberes, descuentos,
 0190            totalHaberes: nominal,
 0191            totalDescuentos: totalDescuentos,
 0192            redondeo: redondeo,
 0193            aCobrar: liquido,
 0194            fechaIngreso: relacion.FechaInicio);
 195    }
 196
 197    private static byte[] GenerarPdfBaja(AguinaldoBaja baja, RelacionLaboral? relacion)
 198    {
 0199        var ci = CultureInfo.GetCultureInfo("es-UY");
 0200        var nombreMes = ci.TextInfo.ToTitleCase(ci.DateTimeFormat.GetMonthName(baja.Mes));
 0201        var tituloPeriodo = $"MES DE : {nombreMes} {baja.Anio}";
 202
 0203        var haberes = baja.Items
 0204            .Where(i => i.Tipo == "haber")
 0205            .Select(i => (i.NombreConcepto, Math.Abs(i.Importe)))
 0206            .ToList();
 207
 0208        var descuentos = baja.Items
 0209            .Where(i => i.Tipo == "descuento")
 0210            .Select(i => (i.NombreConcepto, Math.Abs(i.Importe)))
 0211            .ToList();
 212
 213        // fallback si no hay items registrados
 0214        if (!haberes.Any())
 0215            haberes = new List<(string, decimal)> { ("AGUINALDO MILITAR", baja.Nominal) };
 0216        if (!descuentos.Any())
 0217            descuentos = new List<(string, decimal)>
 0218            {
 0219                ("IRPF", baja.Irpf),
 0220                ("MONTEPÍO", baja.Montepio),
 0221                ("D.N.S.FF.AA.", baja.Sanidad)
 0222            };
 223
 0224        return GenerarPdfAguinaldo(
 0225            tituloPeriodo, relacion,
 0226            haberes, descuentos,
 0227            totalHaberes: baja.Nominal,
 0228            totalDescuentos: baja.TotalDescuentos,
 0229            redondeo: baja.Redondeo,
 0230            aCobrar: baja.Liquido,
 0231            fechaIngreso: relacion?.FechaInicio,
 0232            cedula: baja.Cedula,
 0233            nombreCompleto: baja.NombreCompleto);
 234    }
 235
 236    // ── CORE PDF ──────────────────────────────────────────────────────────────
 237
 0238    private static readonly byte[]? _logoBytes = CargarLogo();
 239
 240    private static byte[]? CargarLogo()
 241    {
 0242        var asm = Assembly.GetExecutingAssembly();
 0243        var nombre = asm.GetManifestResourceNames()
 0244            .FirstOrDefault(n => n.EndsWith("fau-logo.png", StringComparison.OrdinalIgnoreCase));
 0245        if (nombre is null) return null;
 0246        using var stream = asm.GetManifestResourceStream(nombre)!;
 0247        using var ms = new MemoryStream();
 0248        stream.CopyTo(ms);
 0249        return ms.ToArray();
 0250    }
 251
 252    private static byte[] GenerarPdfAguinaldo(
 253        string tituloPeriodo,
 254        RelacionLaboral? relacion,
 255        List<(string Desc, decimal Monto)> haberes,
 256        List<(string Desc, decimal Monto)> descuentos,
 257        decimal totalHaberes,
 258        decimal totalDescuentos,
 259        decimal redondeo,
 260        decimal aCobrar,
 261        DateTime? fechaIngreso,
 262        string? cedula = null,
 263        string? nombreCompleto = null)
 264    {
 0265        var cedulaMostrar = cedula ?? relacion?.Persona.Cedula ?? "-";
 0266        var gradoCodigo   = relacion?.Grado.Codigo ?? "-";
 0267        var escalafonCodigo = relacion?.Escalafon.Codigo ?? "-";
 0268        var unidadCodigo  = relacion?.Unidad.Codigo ?? "-";
 269
 270        string apellidoNombre;
 0271        if (nombreCompleto != null)
 272        {
 0273            apellidoNombre = nombreCompleto.ToUpper();
 274        }
 0275        else if (relacion != null)
 276        {
 0277            var p = relacion.Persona;
 0278            var apellidos = string.IsNullOrWhiteSpace(p.SegundoApellido)
 0279                ? p.PrimerApellido
 0280                : $"{p.PrimerApellido} {p.SegundoApellido}";
 0281            var nombres = string.IsNullOrWhiteSpace(p.SegundoNombre)
 0282                ? p.PrimerNombre
 0283                : $"{p.PrimerNombre} {p.SegundoNombre}";
 0284            apellidoNombre = $"{apellidos.ToUpper()}, {nombres.ToUpper()}";
 285        }
 286        else
 287        {
 0288            apellidoNombre = "-";
 289        }
 290
 0291        var fechaIngresoStr = fechaIngreso.HasValue
 0292            ? fechaIngreso.Value.ToString("dd/MM/yyyy")
 0293            : string.Empty;
 294
 0295        return Document.Create(container =>
 0296        {
 0297            container.Page(page =>
 0298            {
 0299                page.Size(PageSizes.A4);
 0300                page.MarginHorizontal(20);
 0301                page.MarginTop(15);
 0302                page.MarginBottom(15);
 0303                page.DefaultTextStyle(x => x.FontSize(7.5f).FontFamily("Courier New"));
 0304
 0305                page.Content().Column(col =>
 0306                {
 0307                    // ── ENCABEZADO ────────────────────────────────────────
 0308                    col.Item().PaddingBottom(4).Row(row =>
 0309                    {
 0310                        row.RelativeItem().Row(r =>
 0311                        {
 0312                            if (_logoBytes is not null)
 0313                                r.ConstantItem(90).Image(_logoBytes).FitArea();
 0314                            r.RelativeItem().PaddingLeft(_logoBytes is not null ? 6 : 0)
 0315                             .Column(c =>
 0316                             {
 0317                                 c.Item().Text("FUERZA AÉREA").Bold().FontSize(13).FontFamily("Arial");
 0318                                 c.Item().Text("URUGUAYA").Bold().FontSize(13).FontFamily("Arial");
 0319                             });
 0320                        });
 0321                        row.ConstantItem(165).Column(c =>
 0322                        {
 0323                            c.Item().Border(1).Padding(6).Column(inner =>
 0324                            {
 0325                                inner.Item().Text($" {tituloPeriodo}");
 0326                                inner.Item().Text(" ");
 0327                            });
 0328                        });
 0329                    });
 0330
 0331                    // ── CABECERA FUNCIONARIO ──────────────────────────────
 0332                    col.Item().Border(1).Row(row =>
 0333                    {
 0334                        row.ConstantItem(85).BorderRight(1).PaddingHorizontal(5).PaddingVertical(3)
 0335                           .Text("CEDULA").Bold();
 0336                        row.ConstantItem(80).BorderRight(1).PaddingHorizontal(5).PaddingVertical(3)
 0337                           .AlignCenter().Text("GDO").Bold();
 0338                        row.ConstantItem(32).BorderRight(1).PaddingHorizontal(5).PaddingVertical(3)
 0339                           .AlignCenter().Text("ESC").Bold();
 0340                        row.RelativeItem().BorderRight(1).PaddingHorizontal(5).PaddingVertical(3)
 0341                           .Text("APELLIDO Y NOMBRE").Bold();
 0342                        row.ConstantItem(22).BorderRight(1).PaddingHorizontal(4).PaddingVertical(3)
 0343                           .AlignCenter().Text("UN").Bold();
 0344                        row.ConstantItem(138).PaddingHorizontal(5).PaddingVertical(3)
 0345                           .AlignCenter().Text("FECHAS").Bold();
 0346                    });
 0347
 0348                    // ── DATOS FUNCIONARIO ─────────────────────────────────
 0349                    col.Item().BorderLeft(1).BorderRight(1).BorderBottom(1).Row(row =>
 0350                    {
 0351                        row.ConstantItem(85).BorderRight(1).PaddingHorizontal(5).PaddingVertical(3)
 0352                           .Text(cedulaMostrar);
 0353                        row.ConstantItem(80).BorderRight(1).PaddingHorizontal(5).PaddingVertical(3)
 0354                           .Text(gradoCodigo);
 0355                        row.ConstantItem(32).BorderRight(1).PaddingHorizontal(5).PaddingVertical(3)
 0356                           .Text(escalafonCodigo);
 0357                        row.RelativeItem().BorderRight(1).PaddingHorizontal(5).PaddingVertical(3)
 0358                           .Text(apellidoNombre);
 0359                        row.ConstantItem(22).BorderRight(1).PaddingHorizontal(4).PaddingVertical(3)
 0360                           .AlignCenter().Text(unidadCodigo);
 0361                        row.ConstantItem(138).PaddingHorizontal(5).PaddingVertical(3)
 0362                           .Column(c =>
 0363                           {
 0364                               if (!string.IsNullOrEmpty(fechaIngresoStr))
 0365                                   c.Item().Text(t =>
 0366                                   {
 0367                                       t.Span("Fecha Ingreso: ").Bold();
 0368                                       t.Span(fechaIngresoStr);
 0369                                   });
 0370                               else
 0371                                   c.Item().Text(" ");
 0372                           });
 0373                    });
 0374
 0375                    // ── CUERPO DE 3 COLUMNAS ──────────────────────────────
 0376                    col.Item().Extend()
 0377                       .BorderLeft(1).BorderRight(1).BorderTop(1)
 0378                       .Row(row =>
 0379                    {
 0380                        // Columna 1: Haberes
 0381                        row.RelativeItem(40).BorderRight(1).Column(c =>
 0382                        {
 0383                            foreach (var item in haberes)
 0384                                FilaItem(c, item.Desc, item.Monto);
 0385
 0386                            FilaSeparador(c);
 0387                            FilaTotalBold(c, "TOTAL DE HABERES", totalHaberes);
 0388                        });
 0389
 0390                        // Columna 2: Descuentos
 0391                        row.RelativeItem(35).BorderRight(1).Column(c =>
 0392                        {
 0393                            foreach (var item in descuentos.Where(d => d.Monto != 0))
 0394                                FilaItem(c, item.Desc, item.Monto);
 0395
 0396                            FilaSeparador(c);
 0397                            FilaTotalBold(c, "TOTAL DESC.", totalDescuentos);
 0398                        });
 0399
 0400                        // Columna 3: Ajuste (redondeo)
 0401                        row.RelativeItem(25).Column(c =>
 0402                        {
 0403                            if (redondeo != 0m)
 0404                            {
 0405                                c.Item().Row(r =>
 0406                                {
 0407                                    r.RelativeItem().PaddingHorizontal(5).Text("AJUSTE");
 0408                                    r.ConstantItem(60).AlignRight().PaddingHorizontal(5)
 0409                                     .Text(Monto(redondeo));
 0410                                });
 0411                            }
 0412                        });
 0413                    });
 0414                });
 0415
 0416                // ── PIE: NOMINAL | DESCUENTOS | A COBRAR ─────────────────
 0417                page.Footer().Border(1).Row(row =>
 0418                {
 0419                    row.RelativeItem().BorderRight(1).Padding(5).Row(r =>
 0420                    {
 0421                        r.RelativeItem().Text(t => t.Span("NOMINAL....: ").Bold());
 0422                        r.ConstantItem(75).AlignRight().Text(Monto(totalHaberes));
 0423                    });
 0424                    row.RelativeItem().BorderRight(1).Padding(5).Row(r =>
 0425                    {
 0426                        r.RelativeItem().Text(t => t.Span("DESCUENTOS.: ").Bold());
 0427                        r.ConstantItem(75).AlignRight().Text(Monto(totalDescuentos));
 0428                    });
 0429                    row.RelativeItem().Padding(5).Row(r =>
 0430                    {
 0431                        r.RelativeItem().Text(t => t.Span("A COBRAR...: ").Bold());
 0432                        r.ConstantItem(75).AlignRight().Text(Monto(aCobrar));
 0433                    });
 0434                });
 0435            });
 0436        }).GeneratePdf();
 437    }
 438
 439    private static string Monto(decimal valor) =>
 0440        valor.ToString("N2", CultureInfo.InvariantCulture);
 441
 442    private static void FilaItem(ColumnDescriptor col, string descripcion, decimal monto)
 443    {
 0444        col.Item().Row(r =>
 0445        {
 0446            r.RelativeItem().PaddingHorizontal(5).Text(descripcion);
 0447            r.ConstantItem(62).AlignRight().PaddingHorizontal(5).Text(Monto(monto));
 0448        });
 0449    }
 450
 451    private static void FilaSeparador(ColumnDescriptor col)
 452    {
 0453        col.Item().PaddingHorizontal(5).PaddingVertical(2).LineHorizontal(0.5f);
 0454    }
 455
 456    private static void FilaTotalBold(ColumnDescriptor col, string label, decimal total)
 457    {
 0458        col.Item().Row(r =>
 0459        {
 0460            r.RelativeItem().PaddingHorizontal(5).Text(label).Bold();
 0461            r.ConstantItem(62).AlignRight().PaddingHorizontal(5).Text(Monto(total)).Bold();
 0462        });
 0463    }
 464}