< Summary

Information
Class: FAU.Logica.Services.ReciboService
Assembly: FAU.Logica
File(s): /home/runner/work/Sistema.Liquidacion.BE/Sistema.Liquidacion.BE/FAU.Logica/Services/ReciboService.cs
Line coverage
100%
Covered lines: 269
Uncovered lines: 0
Coverable lines: 269
Total lines: 343
Line coverage: 100%
Branch coverage
94%
Covered branches: 100
Total branches: 106
Branch coverage: 94.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
ObtenerResumenesAsync()100%66100%
ObtenerReciboAsync()100%11100%
GenerarPdfAsync()100%22100%
GenerarLoteZipAsync()100%66100%
.cctor()100%11100%
CargarLogo()50%22100%
GenerarPdfDesdeRecibo(...)95.83%7272100%
Monto(...)100%11100%
FilaItem(...)100%11100%
FilaSeparador(...)100%11100%
FilaTotalBold(...)100%11100%
ObtenerFechas(...)88.88%1818100%

File(s)

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

#LineLine coverage
 1using System.Globalization;
 2using System.IO.Compression;
 3using System.Reflection;
 4using FAU.DataAccess.Repositories;
 5using FAU.Entidades;
 6using FAU.Logica.DTOs;
 7using QuestPDF.Fluent;
 8using QuestPDF.Helpers;
 9using QuestPDF.Infrastructure;
 10
 11namespace FAU.Logica.Services;
 12
 13public class ReciboService : IReciboService
 14{
 15    private readonly IReciboRepository _repo;
 16
 2417    public ReciboService(IReciboRepository repo)
 18    {
 2419        _repo = repo;
 2420    }
 21
 22    public async Task<PagedResult<ReciboResumenDto>> ObtenerResumenesAsync(long periodoId, ReciboFiltroRequest filtro)
 23    {
 424        if (filtro.Page.HasValue && filtro.Page < 1) filtro.Page = 1;
 525        if (filtro.PageSize.HasValue) filtro.PageSize = Math.Clamp(filtro.PageSize.Value, 1, 100);
 26
 327        var version = await _repo.ObtenerVersionMaximaAsync(periodoId);
 328        var (items, totalCount) = await _repo.ObtenerResumenesAsync(periodoId, version, filtro);
 29
 330        return new PagedResult<ReciboResumenDto>
 331        {
 332            Items      = items,
 333            Page       = filtro.Page ?? 1,
 334            PageSize   = filtro.PageSize ?? totalCount,
 335            TotalCount = totalCount
 336        };
 337    }
 38
 39    public async Task<ReciboDto?> ObtenerReciboAsync(long periodoId, long relacionId)
 40    {
 441        var version = await _repo.ObtenerVersionMaximaAsync(periodoId);
 442        return await _repo.ObtenerDatosReciboAsync(periodoId, version, relacionId);
 443    }
 44
 45    public async Task<byte[]> GenerarPdfAsync(long periodoId, long relacionId)
 46    {
 1447        var version = await _repo.ObtenerVersionMaximaAsync(periodoId);
 1448        var recibo = await _repo.ObtenerDatosReciboAsync(periodoId, version, relacionId)
 1449            ?? throw new KeyNotFoundException($"No se encontró liquidación para la relación {relacionId} en el período {
 50
 1351        return GenerarPdfDesdeRecibo(recibo);
 1352    }
 53
 54    public async Task<Stream> GenerarLoteZipAsync(long periodoId, ReciboFiltroRequest filtro)
 55    {
 356        var version = await _repo.ObtenerVersionMaximaAsync(periodoId);
 357        var (resumenes, _) = await _repo.ObtenerResumenesAsync(periodoId, version, filtro);
 58
 359        var memoryStream = new MemoryStream();
 360        using (var zip = new ZipArchive(memoryStream, ZipArchiveMode.Create, leaveOpen: true))
 61        {
 1062            foreach (var resumen in resumenes)
 63            {
 264                var recibo = await _repo.ObtenerDatosReciboAsync(periodoId, version, resumen.RelacionLaboralId);
 265                if (recibo == null) continue;
 66
 167                var pdfBytes = GenerarPdfDesdeRecibo(recibo);
 168                var entry = zip.CreateEntry($"recibo_{resumen.Cedula}.pdf", CompressionLevel.Fastest);
 169                using var entryStream = entry.Open();
 170                await entryStream.WriteAsync(pdfBytes);
 171            }
 372        }
 73
 374        memoryStream.Position = 0;
 375        return memoryStream;
 376    }
 77
 178    private static readonly byte[]? _logoBytes = CargarLogo();
 79
 80    private static byte[]? CargarLogo()
 81    {
 182        var asm = Assembly.GetExecutingAssembly();
 183        var nombre = asm.GetManifestResourceNames()
 284            .FirstOrDefault(n => n.EndsWith("fau-logo.png", StringComparison.OrdinalIgnoreCase));
 185        if (nombre is null) return null;
 186        using var stream = asm.GetManifestResourceStream(nombre)!;
 187        using var ms = new MemoryStream();
 188        stream.CopyTo(ms);
 189        return ms.ToArray();
 190    }
 91
 92    private static byte[] GenerarPdfDesdeRecibo(ReciboDto recibo)
 93    {
 1494        var ci = CultureInfo.GetCultureInfo("es-UY");
 1495        var nombreMes = ci.TextInfo.ToTitleCase(ci.DateTimeFormat.GetMonthName(recibo.Mes));
 96
 1497        var apellidos = string.IsNullOrWhiteSpace(recibo.SegundoApellido)
 1498            ? recibo.PrimerApellido
 1499            : $"{recibo.PrimerApellido} {recibo.SegundoApellido}";
 14100        var nombres = string.IsNullOrWhiteSpace(recibo.SegundoNombre)
 14101            ? recibo.PrimerNombre
 14102            : $"{recibo.PrimerNombre} {recibo.SegundoNombre}";
 14103        var apellidoNombre = $"{apellidos.ToUpper()}, {nombres.ToUpper()}";
 104
 14105        var (fecha1Label, fecha1Value, fecha2Label, fecha2Value) = ObtenerFechas(recibo);
 14106        var descuentosFooter = recibo.Nominal - recibo.ACobrar;
 107
 14108        return Document.Create(container =>
 14109        {
 14110            container.Page(page =>
 14111            {
 14112                page.Size(PageSizes.A4);
 14113                page.MarginHorizontal(20);
 14114                page.MarginTop(15);
 14115                page.MarginBottom(15);
 28116                page.DefaultTextStyle(x => x.FontSize(7.5f).FontFamily("Courier New"));
 14117
 14118                page.Content().Column(col =>
 14119                {
 14120                    // ── ENCABEZADO ────────────────────────────────────────
 14121                    col.Item().PaddingBottom(4).Row(row =>
 14122                    {
 14123                        row.RelativeItem().Row(r =>
 14124                        {
 14125                            if (_logoBytes is not null)
 14126                                r.ConstantItem(90).Image(_logoBytes).FitArea();
 14127                            r.RelativeItem().PaddingLeft(_logoBytes is not null ? 6 : 0)
 14128                             .Column(c =>
 14129                             {
 14130                                 c.Item().Text("FUERZA AÉREA").Bold().FontSize(13).FontFamily("Arial");
 14131                                 c.Item().Text("URUGUAYA").Bold().FontSize(13).FontFamily("Arial");
 28132                             });
 28133                        });
 14134                        row.ConstantItem(165).Column(c =>
 14135                        {
 14136                            c.Item().Border(1).Padding(6).Column(inner =>
 14137                            {
 14138                                inner.Item().Text($" MES DE : {nombreMes} {recibo.Anio}");
 14139                                inner.Item().Text(" ");
 28140                            });
 28141                        });
 28142                    });
 14143
 14144                    // ── FILA DE ENCABEZADOS DEL FUNCIONARIO ───────────────
 14145                    col.Item().Border(1).Row(row =>
 14146                    {
 14147                        row.ConstantItem(85).BorderRight(1).PaddingHorizontal(5).PaddingVertical(3)
 14148                           .Text("CEDULA").Bold();
 14149                        row.ConstantItem(80).BorderRight(1).PaddingHorizontal(5).PaddingVertical(3)
 14150                           .AlignCenter().Text("GDO").Bold();
 14151                        row.ConstantItem(32).BorderRight(1).PaddingHorizontal(5).PaddingVertical(3)
 14152                           .AlignCenter().Text("ESC").Bold();
 14153                        row.RelativeItem().BorderRight(1).PaddingHorizontal(5).PaddingVertical(3)
 14154                           .Text("APELLIDO Y NOMBRE").Bold();
 14155                        row.ConstantItem(22).BorderRight(1).PaddingHorizontal(4).PaddingVertical(3)
 14156                           .AlignCenter().Text("UN").Bold();
 14157                        row.ConstantItem(22).BorderRight(1).PaddingHorizontal(4).PaddingVertical(3)
 14158                           .AlignCenter().Text("CIA").Bold();
 14159                        row.ConstantItem(138).PaddingHorizontal(5).PaddingVertical(3)
 14160                           .AlignCenter().Text("FECHAS").Bold();
 28161                    });
 14162
 14163                    // ── FILA DE DATOS DEL FUNCIONARIO ─────────────────────
 14164                    col.Item().BorderLeft(1).BorderRight(1).BorderBottom(1).Row(row =>
 14165                    {
 14166                        row.ConstantItem(85).BorderRight(1).PaddingHorizontal(5).PaddingVertical(3)
 14167                           .Text(recibo.Cedula);
 14168                        row.ConstantItem(80).BorderRight(1).PaddingHorizontal(5).PaddingVertical(3)
 14169                           .Text(recibo.Grado);
 14170                        row.ConstantItem(32).BorderRight(1).PaddingHorizontal(5).PaddingVertical(3)
 14171                           .Text(recibo.Escalafon);
 14172                        row.RelativeItem().BorderRight(1).PaddingHorizontal(5).PaddingVertical(3)
 14173                           .Text(apellidoNombre);
 14174                        row.ConstantItem(22).BorderRight(1).PaddingHorizontal(4).PaddingVertical(3)
 14175                           .AlignCenter().Text(recibo.UnidadCodigo);
 14176                        row.ConstantItem(22).BorderRight(1).PaddingHorizontal(4).PaddingVertical(3)
 14177                           .AlignCenter().Text(recibo.Compania);
 14178                        row.ConstantItem(138).BorderLeft(1).PaddingHorizontal(5).PaddingVertical(3)
 14179                           .Column(c =>
 14180                           {
 14181                               if (fecha1Label != null)
 7182                                   c.Item().Text(t =>
 7183                                   {
 7184                                       t.Span(fecha1Label).Bold();
 7185                                       t.Span(fecha1Value ?? "");
 14186                                   });
 14187                               if (fecha2Label != null)
 2188                                   c.Item().Text(t =>
 2189                                   {
 2190                                       t.Span(fecha2Label).Bold();
 2191                                       t.Span(fecha2Value ?? "");
 4192                                   });
 14193                               if (fecha1Label == null)
 7194                                   c.Item().Text(" "); // blank row for alignment
 28195                           });
 28196                    });
 14197
 14198                    // ── CUERPO DE 3 COLUMNAS ──────────────────────────────
 14199                    col.Item().Extend()
 14200                       .BorderLeft(1).BorderRight(1).BorderTop(1)
 14201                       .Row(row =>
 14202                    {
 14203                        // Columna 1: Haberes + Beneficios
 14204                        row.RelativeItem(40).BorderRight(1).Column(c =>
 14205                        {
 84206                            foreach (var item in recibo.Haberes)
 28207                                FilaItem(c, item.Descripcion, item.Monto);
 14208
 14209                            FilaSeparador(c, "----------------");
 14210                            FilaTotalBold(c, "TOTAL DE HABERES", recibo.TotalHaberes);
 14211
 14212                            if (recibo.Beneficios.Any())
 14213                            {
 1214                                c.Item().Text(" ");
 4215                                foreach (var item in recibo.Beneficios)
 1216                                    FilaItem(c, item.Descripcion, item.Monto);
 1217                                FilaSeparador(c, "----------------");
 1218                                FilaTotalBold(c, "TOTAL DE BENEFIC", recibo.TotalBeneficios);
 14219                            }
 28220                        });
 14221
 14222                        // Columna 2: Descuentos Legales + ítems de Descuentos Personales
 14223                        row.RelativeItem(35).BorderRight(1).Column(c =>
 14224                        {
 84225                            foreach (var item in recibo.DescuentosLegales)
 28226                                FilaItem(c, item.Descripcion, Math.Abs(item.Monto));
 14227
 14228                            FilaSeparador(c, "------------");
 14229                            FilaTotalBold(c, "TOTAL DESC.", recibo.TotalDescuentosLegales);
 14230
 14231                            if (recibo.DescuentosPersonales.Any())
 14232                            {
 12233                                c.Item().Text(" ");
 48234                                foreach (var item in recibo.DescuentosPersonales)
 12235                                    FilaItem(c, item.Descripcion, Math.Abs(item.Monto));
 14236                            }
 28237                        });
 14238
 14239                        // Columna 3: Total de Descuentos Personales + Ajuste
 14240                        row.RelativeItem(25).Column(c =>
 14241                        {
 14242                            if (recibo.TotalDescuentosPersonales > 0)
 14243                            {
 13244                                FilaSeparador(c, "----------------");
 13245                                FilaTotalBold(c, "TOTAL DESC. PERS.", recibo.TotalDescuentosPersonales);
 14246                            }
 14247
 14248                            if (recibo.Ajuste != 0)
 14249                            {
 2250                                if (recibo.TotalDescuentosPersonales > 0)
 1251                                    c.Item().Text(" ");
 2252                                c.Item().Row(r =>
 2253                                {
 2254                                    r.RelativeItem().PaddingHorizontal(5).Text("AJUSTE");
 2255                                    r.ConstantItem(60).AlignRight().PaddingHorizontal(5)
 2256                                     .Text(Monto(recibo.Ajuste));
 4257                                });
 14258                            }
 28259                        });
 28260                    });
 28261                });
 14262
 14263                // ── PIE: NOMINAL | DESCUENTOS | A COBRAR ─────────────────
 14264                page.Footer().Border(1).Row(row =>
 14265                {
 14266                    row.RelativeItem().BorderRight(1).Padding(5).Row(r =>
 14267                    {
 28268                        r.RelativeItem().Text(t => t.Span("NOMINAL....: ").Bold());
 14269                        r.ConstantItem(75).AlignRight().Text(Monto(recibo.Nominal));
 28270                    });
 14271                    row.RelativeItem().BorderRight(1).Padding(5).Row(r =>
 14272                    {
 28273                        r.RelativeItem().Text(t => t.Span("DESCUENTOS.: ").Bold());
 14274                        r.ConstantItem(75).AlignRight().Text(Monto(descuentosFooter));
 28275                    });
 14276                    row.RelativeItem().Padding(5).Row(r =>
 14277                    {
 28278                        r.RelativeItem().Text(t => t.Span("A COBRAR...: ").Bold());
 14279                        r.ConstantItem(75).AlignRight().Text(Monto(recibo.ACobrar));
 28280                    });
 28281                });
 28282            });
 28283        }).GeneratePdf();
 284    }
 285
 286    private static string Monto(decimal valor) =>
 155287        valor.ToString("N2", CultureInfo.InvariantCulture);
 288
 289    private static void FilaItem(ColumnDescriptor col, string descripcion, decimal monto)
 290    {
 69291        col.Item().Row(r =>
 69292        {
 69293            r.RelativeItem().PaddingHorizontal(5).Text(descripcion);
 69294            r.ConstantItem(62).AlignRight().PaddingHorizontal(5).Text(Monto(monto));
 138295        });
 69296    }
 297
 298    private static void FilaSeparador(ColumnDescriptor col, string _)
 299    {
 42300        col.Item().PaddingHorizontal(5).PaddingVertical(2).LineHorizontal(0.5f);
 42301    }
 302
 303    private static void FilaTotalBold(ColumnDescriptor col, string label, decimal total)
 304    {
 42305        col.Item().Row(r =>
 42306        {
 42307            r.RelativeItem().PaddingHorizontal(5).Text(label).Bold();
 42308            r.ConstantItem(62).AlignRight().PaddingHorizontal(5).Text(Monto(total)).Bold();
 84309        });
 42310    }
 311
 312    private static (string? l1, string? v1, string? l2, string? v2) ObtenerFechas(ReciboDto recibo)
 313    {
 314        const string fmt = "dd/MM/yyyy";
 315
 14316        if (recibo.EsCivil)
 2317            return ("Fecha Ing.(FIAP): ", recibo.FechaInicio != default
 2318                ? recibo.FechaInicio.ToString(fmt) : string.Empty, null, null);
 319
 12320        if (recibo.EsOficial)
 321        {
 2322            var v1 = recibo.FechaInicio != default ? recibo.FechaInicio.ToString(fmt) : string.Empty;
 2323            if (recibo.FechaUltimoAscenso.HasValue)
 1324                return ("Asc. a Oficial: ", v1,
 1325                        "Ult. Ascenso .: ", recibo.FechaUltimoAscenso.Value.ToString(fmt));
 1326            return ("Asc. a Oficial: ", v1, null, null);
 327        }
 328
 10329        if (recibo.EsSubalterno)
 330        {
 2331            var v1 = recibo.FechaInicio != default ? recibo.FechaInicio.ToString(fmt) : string.Empty;
 2332            if (recibo.FechaUltimoAscenso.HasValue)
 1333                return ("Fecha de Ingreso: ", v1,
 1334                        "Fecha Ult. Asc. : ", recibo.FechaUltimoAscenso.Value.ToString(fmt));
 1335            return ("Fecha de Ingreso: ", v1, null, null);
 336        }
 337
 8338        if (recibo.FechaInicio != default)
 1339            return ("Fecha Ingreso: ", recibo.FechaInicio.ToString(fmt), null, null);
 340
 7341        return (null, null, null, null);
 342    }
 343}