< 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
76%
Covered lines: 205
Uncovered lines: 64
Coverable lines: 269
Total lines: 343
Line coverage: 76.2%
Branch coverage
54%
Covered branches: 58
Total branches: 106
Branch coverage: 54.7%
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()0%4260%
ObtenerReciboAsync()100%11100%
GenerarPdfAsync()100%22100%
GenerarLoteZipAsync()0%4260%
.cctor()100%11100%
CargarLogo()50%22100%
GenerarPdfDesdeRecibo(...)70.83%827287.7%
Monto(...)100%11100%
FilaItem(...)100%11100%
FilaSeparador(...)100%11100%
FilaTotalBold(...)100%11100%
ObtenerFechas(...)22.22%1401827.77%

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
 817    public ReciboService(IReciboRepository repo)
 18    {
 819        _repo = repo;
 820    }
 21
 22    public async Task<PagedResult<ReciboResumenDto>> ObtenerResumenesAsync(long periodoId, ReciboFiltroRequest filtro)
 23    {
 024        if (filtro.Page.HasValue && filtro.Page < 1) filtro.Page = 1;
 025        if (filtro.PageSize.HasValue) filtro.PageSize = Math.Clamp(filtro.PageSize.Value, 1, 100);
 26
 027        var version = await _repo.ObtenerVersionMaximaAsync(periodoId);
 028        var (items, totalCount) = await _repo.ObtenerResumenesAsync(periodoId, version, filtro);
 29
 030        return new PagedResult<ReciboResumenDto>
 031        {
 032            Items      = items,
 033            Page       = filtro.Page ?? 1,
 034            PageSize   = filtro.PageSize ?? totalCount,
 035            TotalCount = totalCount
 036        };
 037    }
 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    {
 447        var version = await _repo.ObtenerVersionMaximaAsync(periodoId);
 448        var recibo = await _repo.ObtenerDatosReciboAsync(periodoId, version, relacionId)
 449            ?? throw new KeyNotFoundException($"No se encontró liquidación para la relación {relacionId} en el período {
 50
 351        return GenerarPdfDesdeRecibo(recibo);
 352    }
 53
 54    public async Task<Stream> GenerarLoteZipAsync(long periodoId, ReciboFiltroRequest filtro)
 55    {
 056        var version = await _repo.ObtenerVersionMaximaAsync(periodoId);
 057        var (resumenes, _) = await _repo.ObtenerResumenesAsync(periodoId, version, filtro);
 58
 059        var memoryStream = new MemoryStream();
 060        using (var zip = new ZipArchive(memoryStream, ZipArchiveMode.Create, leaveOpen: true))
 61        {
 062            foreach (var resumen in resumenes)
 63            {
 064                var recibo = await _repo.ObtenerDatosReciboAsync(periodoId, version, resumen.RelacionLaboralId);
 065                if (recibo == null) continue;
 66
 067                var pdfBytes = GenerarPdfDesdeRecibo(recibo);
 068                var entry = zip.CreateEntry($"recibo_{resumen.Cedula}.pdf", CompressionLevel.Fastest);
 069                using var entryStream = entry.Open();
 070                await entryStream.WriteAsync(pdfBytes);
 071            }
 072        }
 73
 074        memoryStream.Position = 0;
 075        return memoryStream;
 076    }
 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    {
 394        var ci = CultureInfo.GetCultureInfo("es-UY");
 395        var nombreMes = ci.TextInfo.ToTitleCase(ci.DateTimeFormat.GetMonthName(recibo.Mes));
 96
 397        var apellidos = string.IsNullOrWhiteSpace(recibo.SegundoApellido)
 398            ? recibo.PrimerApellido
 399            : $"{recibo.PrimerApellido} {recibo.SegundoApellido}";
 3100        var nombres = string.IsNullOrWhiteSpace(recibo.SegundoNombre)
 3101            ? recibo.PrimerNombre
 3102            : $"{recibo.PrimerNombre} {recibo.SegundoNombre}";
 3103        var apellidoNombre = $"{apellidos.ToUpper()}, {nombres.ToUpper()}";
 104
 3105        var (fecha1Label, fecha1Value, fecha2Label, fecha2Value) = ObtenerFechas(recibo);
 3106        var descuentosFooter = recibo.Nominal - recibo.ACobrar;
 107
 3108        return Document.Create(container =>
 3109        {
 3110            container.Page(page =>
 3111            {
 3112                page.Size(PageSizes.A4);
 3113                page.MarginHorizontal(20);
 3114                page.MarginTop(15);
 3115                page.MarginBottom(15);
 6116                page.DefaultTextStyle(x => x.FontSize(7.5f).FontFamily("Courier New"));
 3117
 3118                page.Content().Column(col =>
 3119                {
 3120                    // ── ENCABEZADO ────────────────────────────────────────
 3121                    col.Item().PaddingBottom(4).Row(row =>
 3122                    {
 3123                        row.RelativeItem().Row(r =>
 3124                        {
 3125                            if (_logoBytes is not null)
 3126                                r.ConstantItem(90).Image(_logoBytes).FitArea();
 3127                            r.RelativeItem().PaddingLeft(_logoBytes is not null ? 6 : 0)
 3128                             .Column(c =>
 3129                             {
 3130                                 c.Item().Text("FUERZA AÉREA").Bold().FontSize(13).FontFamily("Arial");
 3131                                 c.Item().Text("URUGUAYA").Bold().FontSize(13).FontFamily("Arial");
 6132                             });
 6133                        });
 3134                        row.ConstantItem(165).Column(c =>
 3135                        {
 3136                            c.Item().Border(1).Padding(6).Column(inner =>
 3137                            {
 3138                                inner.Item().Text($" MES DE : {nombreMes} {recibo.Anio}");
 3139                                inner.Item().Text(" ");
 6140                            });
 6141                        });
 6142                    });
 3143
 3144                    // ── FILA DE ENCABEZADOS DEL FUNCIONARIO ───────────────
 3145                    col.Item().Border(1).Row(row =>
 3146                    {
 3147                        row.ConstantItem(85).BorderRight(1).PaddingHorizontal(5).PaddingVertical(3)
 3148                           .Text("CEDULA").Bold();
 3149                        row.ConstantItem(80).BorderRight(1).PaddingHorizontal(5).PaddingVertical(3)
 3150                           .AlignCenter().Text("GDO").Bold();
 3151                        row.ConstantItem(32).BorderRight(1).PaddingHorizontal(5).PaddingVertical(3)
 3152                           .AlignCenter().Text("ESC").Bold();
 3153                        row.RelativeItem().BorderRight(1).PaddingHorizontal(5).PaddingVertical(3)
 3154                           .Text("APELLIDO Y NOMBRE").Bold();
 3155                        row.ConstantItem(22).BorderRight(1).PaddingHorizontal(4).PaddingVertical(3)
 3156                           .AlignCenter().Text("UN").Bold();
 3157                        row.ConstantItem(22).BorderRight(1).PaddingHorizontal(4).PaddingVertical(3)
 3158                           .AlignCenter().Text("CIA").Bold();
 3159                        row.ConstantItem(138).PaddingHorizontal(5).PaddingVertical(3)
 3160                           .AlignCenter().Text("FECHAS").Bold();
 6161                    });
 3162
 3163                    // ── FILA DE DATOS DEL FUNCIONARIO ─────────────────────
 3164                    col.Item().BorderLeft(1).BorderRight(1).BorderBottom(1).Row(row =>
 3165                    {
 3166                        row.ConstantItem(85).BorderRight(1).PaddingHorizontal(5).PaddingVertical(3)
 3167                           .Text(recibo.Cedula);
 3168                        row.ConstantItem(80).BorderRight(1).PaddingHorizontal(5).PaddingVertical(3)
 3169                           .Text(recibo.Grado);
 3170                        row.ConstantItem(32).BorderRight(1).PaddingHorizontal(5).PaddingVertical(3)
 3171                           .Text(recibo.Escalafon);
 3172                        row.RelativeItem().BorderRight(1).PaddingHorizontal(5).PaddingVertical(3)
 3173                           .Text(apellidoNombre);
 3174                        row.ConstantItem(22).BorderRight(1).PaddingHorizontal(4).PaddingVertical(3)
 3175                           .AlignCenter().Text(recibo.UnidadCodigo);
 3176                        row.ConstantItem(22).BorderRight(1).PaddingHorizontal(4).PaddingVertical(3)
 3177                           .AlignCenter().Text(recibo.Compania);
 3178                        row.ConstantItem(138).BorderLeft(1).PaddingHorizontal(5).PaddingVertical(3)
 3179                           .Column(c =>
 3180                           {
 3181                               if (fecha1Label != null)
 0182                                   c.Item().Text(t =>
 0183                                   {
 0184                                       t.Span(fecha1Label).Bold();
 0185                                       t.Span(fecha1Value ?? "");
 0186                                   });
 3187                               if (fecha2Label != null)
 0188                                   c.Item().Text(t =>
 0189                                   {
 0190                                       t.Span(fecha2Label).Bold();
 0191                                       t.Span(fecha2Value ?? "");
 0192                                   });
 3193                               if (fecha1Label == null)
 3194                                   c.Item().Text(" "); // blank row for alignment
 6195                           });
 6196                    });
 3197
 3198                    // ── CUERPO DE 3 COLUMNAS ──────────────────────────────
 3199                    col.Item().Extend()
 3200                       .BorderLeft(1).BorderRight(1).BorderTop(1)
 3201                       .Row(row =>
 3202                    {
 3203                        // Columna 1: Haberes + Beneficios
 3204                        row.RelativeItem(40).BorderRight(1).Column(c =>
 3205                        {
 18206                            foreach (var item in recibo.Haberes)
 6207                                FilaItem(c, item.Descripcion, item.Monto);
 3208
 3209                            FilaSeparador(c, "----------------");
 3210                            FilaTotalBold(c, "TOTAL DE HABERES", recibo.TotalHaberes);
 3211
 3212                            if (recibo.Beneficios.Any())
 3213                            {
 0214                                c.Item().Text(" ");
 0215                                foreach (var item in recibo.Beneficios)
 0216                                    FilaItem(c, item.Descripcion, item.Monto);
 0217                                FilaSeparador(c, "----------------");
 0218                                FilaTotalBold(c, "TOTAL DE BENEFIC", recibo.TotalBeneficios);
 3219                            }
 6220                        });
 3221
 3222                        // Columna 2: Descuentos Legales + ítems de Descuentos Personales
 3223                        row.RelativeItem(35).BorderRight(1).Column(c =>
 3224                        {
 18225                            foreach (var item in recibo.DescuentosLegales)
 6226                                FilaItem(c, item.Descripcion, Math.Abs(item.Monto));
 3227
 3228                            FilaSeparador(c, "------------");
 3229                            FilaTotalBold(c, "TOTAL DESC.", recibo.TotalDescuentosLegales);
 3230
 3231                            if (recibo.DescuentosPersonales.Any())
 3232                            {
 2233                                c.Item().Text(" ");
 8234                                foreach (var item in recibo.DescuentosPersonales)
 2235                                    FilaItem(c, item.Descripcion, Math.Abs(item.Monto));
 3236                            }
 6237                        });
 3238
 3239                        // Columna 3: Total de Descuentos Personales + Ajuste
 3240                        row.RelativeItem(25).Column(c =>
 3241                        {
 3242                            if (recibo.TotalDescuentosPersonales > 0)
 3243                            {
 3244                                FilaSeparador(c, "----------------");
 3245                                FilaTotalBold(c, "TOTAL DESC. PERS.", recibo.TotalDescuentosPersonales);
 3246                            }
 3247
 3248                            if (recibo.Ajuste != 0)
 3249                            {
 0250                                if (recibo.TotalDescuentosPersonales > 0)
 0251                                    c.Item().Text(" ");
 0252                                c.Item().Row(r =>
 0253                                {
 0254                                    r.RelativeItem().PaddingHorizontal(5).Text("AJUSTE");
 0255                                    r.ConstantItem(60).AlignRight().PaddingHorizontal(5)
 0256                                     .Text(Monto(recibo.Ajuste));
 0257                                });
 3258                            }
 6259                        });
 6260                    });
 6261                });
 3262
 3263                // ── PIE: NOMINAL | DESCUENTOS | A COBRAR ─────────────────
 3264                page.Footer().Border(1).Row(row =>
 3265                {
 3266                    row.RelativeItem().BorderRight(1).Padding(5).Row(r =>
 3267                    {
 6268                        r.RelativeItem().Text(t => t.Span("NOMINAL....: ").Bold());
 3269                        r.ConstantItem(75).AlignRight().Text(Monto(recibo.Nominal));
 6270                    });
 3271                    row.RelativeItem().BorderRight(1).Padding(5).Row(r =>
 3272                    {
 6273                        r.RelativeItem().Text(t => t.Span("DESCUENTOS.: ").Bold());
 3274                        r.ConstantItem(75).AlignRight().Text(Monto(descuentosFooter));
 6275                    });
 3276                    row.RelativeItem().Padding(5).Row(r =>
 3277                    {
 6278                        r.RelativeItem().Text(t => t.Span("A COBRAR...: ").Bold());
 3279                        r.ConstantItem(75).AlignRight().Text(Monto(recibo.ACobrar));
 6280                    });
 6281                });
 6282            });
 6283        }).GeneratePdf();
 284    }
 285
 286    private static string Monto(decimal valor) =>
 32287        valor.ToString("N2", CultureInfo.InvariantCulture);
 288
 289    private static void FilaItem(ColumnDescriptor col, string descripcion, decimal monto)
 290    {
 14291        col.Item().Row(r =>
 14292        {
 14293            r.RelativeItem().PaddingHorizontal(5).Text(descripcion);
 14294            r.ConstantItem(62).AlignRight().PaddingHorizontal(5).Text(Monto(monto));
 28295        });
 14296    }
 297
 298    private static void FilaSeparador(ColumnDescriptor col, string _)
 299    {
 9300        col.Item().PaddingHorizontal(5).PaddingVertical(2).LineHorizontal(0.5f);
 9301    }
 302
 303    private static void FilaTotalBold(ColumnDescriptor col, string label, decimal total)
 304    {
 9305        col.Item().Row(r =>
 9306        {
 9307            r.RelativeItem().PaddingHorizontal(5).Text(label).Bold();
 9308            r.ConstantItem(62).AlignRight().PaddingHorizontal(5).Text(Monto(total)).Bold();
 18309        });
 9310    }
 311
 312    private static (string? l1, string? v1, string? l2, string? v2) ObtenerFechas(ReciboDto recibo)
 313    {
 314        const string fmt = "dd/MM/yyyy";
 315
 3316        if (recibo.EsCivil)
 0317            return ("Fecha Ing.(FIAP): ", recibo.FechaInicio != default
 0318                ? recibo.FechaInicio.ToString(fmt) : string.Empty, null, null);
 319
 3320        if (recibo.EsOficial)
 321        {
 0322            var v1 = recibo.FechaInicio != default ? recibo.FechaInicio.ToString(fmt) : string.Empty;
 0323            if (recibo.FechaUltimoAscenso.HasValue)
 0324                return ("Asc. a Oficial: ", v1,
 0325                        "Ult. Ascenso .: ", recibo.FechaUltimoAscenso.Value.ToString(fmt));
 0326            return ("Asc. a Oficial: ", v1, null, null);
 327        }
 328
 3329        if (recibo.EsSubalterno)
 330        {
 0331            var v1 = recibo.FechaInicio != default ? recibo.FechaInicio.ToString(fmt) : string.Empty;
 0332            if (recibo.FechaUltimoAscenso.HasValue)
 0333                return ("Fecha de Ingreso: ", v1,
 0334                        "Fecha Ult. Asc. : ", recibo.FechaUltimoAscenso.Value.ToString(fmt));
 0335            return ("Fecha de Ingreso: ", v1, null, null);
 336        }
 337
 3338        if (recibo.FechaInicio != default)
 0339            return ("Fecha Ingreso: ", recibo.FechaInicio.ToString(fmt), null, null);
 340
 3341        return (null, null, null, null);
 342    }
 343}