Compare commits
	
		
			25 Commits
		
	
	
		
			8101756638
			...
			Suscripcio
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 8c194b8441 | |||
| 1a288fcfa5 | |||
| 7dc0940001 | |||
| 5a806eda38 | |||
| 21c5c1d7d9 | |||
| 899e0a173f | |||
| 9cfe9d012e | |||
| 9e248efc84 | |||
| 84187a66df | |||
| b14c5de1b4 | |||
| d62ca7feb3 | |||
| f09c795fb0 | |||
| 19e7192a16 | |||
| 7e4d3282fb | |||
| 28c1b88a92 | |||
| 052141a45b | |||
| 9e8ccf6cfb | |||
| 9c225845c2 | |||
| f46dd82e27 | |||
| c251a0adf4 | |||
| 3e1ac6f742 | |||
| a35a3a66ea | |||
| c96d259892 | |||
| 95aa09d62a | |||
| dc52c9aff2 | 
| @@ -14,7 +14,7 @@ jobs: | |||||||
|         run: | |         run: | | ||||||
|           set -e |           set -e | ||||||
|            |            | ||||||
|           # Configura SSH |           # Configura SSH (sin cambios) | ||||||
|           apt-get update -qq && apt-get install -y openssh-client git |           apt-get update -qq && apt-get install -y openssh-client git | ||||||
|           mkdir -p ~/.ssh |           mkdir -p ~/.ssh | ||||||
|           echo "${{ secrets.PROD_SERVER_SSH_KEY }}" > ~/.ssh/id_rsa |           echo "${{ secrets.PROD_SERVER_SSH_KEY }}" > ~/.ssh/id_rsa | ||||||
| @@ -26,6 +26,12 @@ jobs: | |||||||
|             set -e |             set -e | ||||||
|             echo "--- INICIO DEL DESPLIEGUE OPTIMIZADO ---" |             echo "--- INICIO DEL DESPLIEGUE OPTIMIZADO ---" | ||||||
|              |              | ||||||
|  |             # --- Asegurar que el Stack de la Base de Datos esté corriendo --- | ||||||
|  |             echo "Asegurando que el stack de la base de datos esté activo..." | ||||||
|  |             cd /opt/shared-services/database | ||||||
|  |             # El comando 'up -d' es idempotente. Si ya está corriendo, no hace nada. | ||||||
|  |             docker compose up -d | ||||||
|  |              | ||||||
|             # 1. Preparar entorno |             # 1. Preparar entorno | ||||||
|             TEMP_DIR=$(mktemp -d) |             TEMP_DIR=$(mktemp -d) | ||||||
|             REPO_OWNER="dmolinari" |             REPO_OWNER="dmolinari" | ||||||
| @@ -37,7 +43,7 @@ jobs: | |||||||
|             cd "$TEMP_DIR" |             cd "$TEMP_DIR" | ||||||
|             git checkout "${{ gitea.sha }}" |             git checkout "${{ gitea.sha }}" | ||||||
|              |              | ||||||
|             # 2. Construcción paralela con Docker nativo (más rápido y fiable) |             # 2. Construcción paralela | ||||||
|             build_image() { |             build_image() { | ||||||
|               local dockerfile=$1 |               local dockerfile=$1 | ||||||
|               local image_name=$2 |               local image_name=$2 | ||||||
| @@ -59,14 +65,13 @@ jobs: | |||||||
|             cd /opt/gestion-integral |             cd /opt/gestion-integral | ||||||
|             export DB_SA_PASSWORD='${{ secrets.DB_SA_PASSWORD_SECRET }}' |             export DB_SA_PASSWORD='${{ secrets.DB_SA_PASSWORD_SECRET }}' | ||||||
|              |              | ||||||
|             echo "Recreando servicios..." |             echo "Recreando servicios de la aplicación..." | ||||||
|             docker compose up -d --force-recreate |             docker compose up -d --force-recreate | ||||||
|              |              | ||||||
|             # 4. Limpieza |             # 4. Limpieza | ||||||
|             echo "Realizando limpieza..." |             echo "Realizando limpieza..." | ||||||
|             rm -rf "$TEMP_DIR" |             rm -rf "$TEMP_DIR" | ||||||
|             docker image prune -af --filter "until=24h" |             docker image prune -f --filter "dangling=true" | ||||||
|              |              | ||||||
|             echo "--- DESPLIEGUE COMPLETADO CON ÉXITO ---" |             echo "--- DESPLIEGUE COMPLETADO CON ÉXITO ---" | ||||||
|             echo "Tiempo total: $(($SECONDS / 60)) minutos y $(($SECONDS % 60)) segundos" |  | ||||||
|           EOSSH |           EOSSH | ||||||
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -19,9 +19,6 @@ lerna-debug.log* | |||||||
|  |  | ||||||
| # Variables de entorno | # Variables de entorno | ||||||
| # ------------------------------- | # ------------------------------- | ||||||
| # Nunca subas tus claves de API, contraseñas de BD, etc. |  | ||||||
| # Crea un archivo .env.example con las variables vacías para guiar a otros desarrolladores. |  | ||||||
| .env |  | ||||||
| .env.local | .env.local | ||||||
| .env.development.local | .env.development.local | ||||||
| .env.test.local | .env.test.local | ||||||
|   | |||||||
							
								
								
									
										12
									
								
								Backend/GestionIntegral.Api/.env
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								Backend/GestionIntegral.Api/.env
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | |||||||
|  | # ================================================ | ||||||
|  | # VARIABLES DE ENTORNO PARA LA CONFIGURACIÓN DE CORREO | ||||||
|  | # ================================================ | ||||||
|  | # El separador de doble guion bajo (__) se usa para mapear la jerarquía del JSON. | ||||||
|  | # MailSettings:SmtpHost se convierte en MailSettings__SmtpHost | ||||||
|  |  | ||||||
|  | MailSettings__SmtpHost="mail.eldia.com" | ||||||
|  | MailSettings__SmtpPort=587 | ||||||
|  | MailSettings__SenderName="Club - Diario El Día" | ||||||
|  | MailSettings__SenderEmail="alertas@eldia.com" | ||||||
|  | MailSettings__SmtpUser="alertas@eldia.com" | ||||||
|  | MailSettings__SmtpPass="@Alertas713550@" | ||||||
| @@ -0,0 +1,73 @@ | |||||||
|  | using Microsoft.AspNetCore.Mvc; | ||||||
|  | using Microsoft.AspNetCore.Authorization; | ||||||
|  | using System.Collections.Generic; | ||||||
|  | using System.Threading.Tasks; | ||||||
|  | using GestionIntegral.Api.Dtos.Anomalia; | ||||||
|  | using GestionIntegral.Api.Services.Anomalia; | ||||||
|  |  | ||||||
|  | namespace GestionIntegral.Api.Controllers.Anomalia | ||||||
|  | { | ||||||
|  |     [Route("api/alertas")] | ||||||
|  |     [ApiController] | ||||||
|  |     [Authorize] | ||||||
|  |     public class AlertasController : ControllerBase | ||||||
|  |     { | ||||||
|  |         private readonly IAlertaService _alertaService; | ||||||
|  |  | ||||||
|  |         public AlertasController(IAlertaService alertaService) | ||||||
|  |         { | ||||||
|  |             _alertaService = alertaService; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // GET: api/alertas | ||||||
|  |         [HttpGet] | ||||||
|  |         [ProducesResponseType(typeof(IEnumerable<AlertaGenericaDto>), StatusCodes.Status200OK)] | ||||||
|  |         public async Task<IActionResult> GetAlertasNoLeidas() | ||||||
|  |         { | ||||||
|  |             var alertas = await _alertaService.ObtenerAlertasNoLeidasAsync(); | ||||||
|  |             return Ok(alertas); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // POST: api/alertas/{idAlerta}/marcar-leida | ||||||
|  |         [HttpPost("{idAlerta:int}/marcar-leida")] | ||||||
|  |         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||||
|  |         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||||
|  |         public async Task<IActionResult> MarcarComoLeida(int idAlerta) | ||||||
|  |         { | ||||||
|  |             var (exito, error) = await _alertaService.MarcarComoLeidaAsync(idAlerta); | ||||||
|  |             if (!exito) | ||||||
|  |             { | ||||||
|  |                 return NotFound(new { message = error }); | ||||||
|  |             } | ||||||
|  |             return NoContent(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // POST: api/alertas/marcar-grupo-leido | ||||||
|  |         [HttpPost("marcar-grupo-leido")] | ||||||
|  |         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||||
|  |         [ProducesResponseType(StatusCodes.Status400BadRequest)] | ||||||
|  |         public async Task<IActionResult> MarcarGrupoLeido([FromBody] MarcarGrupoLeidoRequestDto request) | ||||||
|  |         { | ||||||
|  |             if (!ModelState.IsValid) | ||||||
|  |             { | ||||||
|  |                 return BadRequest(ModelState); | ||||||
|  |             } | ||||||
|  |             var (exito, error) = await _alertaService.MarcarGrupoComoLeidoAsync(request.TipoAlerta, request.IdEntidad); | ||||||
|  |             if (!exito) | ||||||
|  |             { | ||||||
|  |                 return BadRequest(new { message = error }); | ||||||
|  |             } | ||||||
|  |             return NoContent(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // DTO para el cuerpo del request de marcar grupo | ||||||
|  |     public class MarcarGrupoLeidoRequestDto  | ||||||
|  |     { | ||||||
|  |         [System.ComponentModel.DataAnnotations.Required] | ||||||
|  |         public string TipoAlerta { get; set; } = string.Empty; | ||||||
|  |  | ||||||
|  |         [System.ComponentModel.DataAnnotations.Required] | ||||||
|  |         public int IdEntidad { get; set; } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,40 @@ | |||||||
|  | using GestionIntegral.Api.Dtos.Comunicaciones; | ||||||
|  | using GestionIntegral.Api.Services.Comunicaciones; | ||||||
|  | using Microsoft.AspNetCore.Authorization; | ||||||
|  | using Microsoft.AspNetCore.Mvc; | ||||||
|  |  | ||||||
|  | namespace GestionIntegral.Api.Controllers.Comunicaciones | ||||||
|  | { | ||||||
|  |     [Route("api/lotes-envio")] | ||||||
|  |     [ApiController] | ||||||
|  |     [Authorize] | ||||||
|  |     public class LotesEnvioController : ControllerBase | ||||||
|  |     { | ||||||
|  |         private readonly IEmailLogService _emailLogService; | ||||||
|  |  | ||||||
|  |         public LotesEnvioController(IEmailLogService emailLogService) | ||||||
|  |         { | ||||||
|  |             _emailLogService = emailLogService; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // GET: api/lotes-envio/123/detalles | ||||||
|  |         [HttpGet("{idLote:int}/detalles")] | ||||||
|  |         [ProducesResponseType(typeof(IEnumerable<EmailLogDto>), StatusCodes.Status200OK)] | ||||||
|  |         [ProducesResponseType(StatusCodes.Status403Forbidden)] | ||||||
|  |         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||||
|  |         public async Task<IActionResult> GetDetallesLote(int idLote) | ||||||
|  |         { | ||||||
|  |             // Reutilizamos un permiso existente, ya que esta es una función de auditoría relacionada. | ||||||
|  |             var tienePermiso = User.IsInRole("SuperAdmin") || User.HasClaim(c => c.Type == "permission" && c.Value == "SU006"); | ||||||
|  |             if (!tienePermiso) | ||||||
|  |             { | ||||||
|  |                 return Forbid(); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             var detalles = await _emailLogService.ObtenerDetallesPorLoteId(idLote); | ||||||
|  |              | ||||||
|  |             // Devolvemos OK con un array vacío si no hay resultados, el frontend lo manejará. | ||||||
|  |             return Ok(detalles); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -50,10 +50,19 @@ namespace GestionIntegral.Api.Controllers.Distribucion | |||||||
|         public async Task<IActionResult> GetAllCanillas([FromQuery] string? nomApe, [FromQuery] int? legajo, [FromQuery] bool? esAccionista, [FromQuery] bool? soloActivos = true) |         public async Task<IActionResult> GetAllCanillas([FromQuery] string? nomApe, [FromQuery] int? legajo, [FromQuery] bool? esAccionista, [FromQuery] bool? soloActivos = true) | ||||||
|         { |         { | ||||||
|             if (!TienePermiso(PermisoVer)) return Forbid(); |             if (!TienePermiso(PermisoVer)) return Forbid(); | ||||||
|             var canillitas = await _canillaService.ObtenerTodosAsync(nomApe, legajo, soloActivos, esAccionista); // <<-- Pasa el parámetro |             var canillitas = await _canillaService.ObtenerTodosAsync(nomApe, legajo, soloActivos, esAccionista); | ||||||
|             return Ok(canillitas); |             return Ok(canillitas); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         [HttpGet("dropdown")] | ||||||
|  |         [ProducesResponseType(typeof(IEnumerable<CanillaDropdownDto>), StatusCodes.Status200OK)] | ||||||
|  |         public async Task<IActionResult> GetAllDropdownCanillas([FromQuery] bool? esAccionista, [FromQuery] bool? soloActivos = true) | ||||||
|  |         { | ||||||
|  |             var canillitas = await _canillaService.ObtenerTodosDropdownAsync(esAccionista, soloActivos); | ||||||
|  |             return Ok(canillitas); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |  | ||||||
|         // GET: api/canillas/{id} |         // GET: api/canillas/{id} | ||||||
|         [HttpGet("{id:int}", Name = "GetCanillaById")] |         [HttpGet("{id:int}", Name = "GetCanillaById")] | ||||||
|         [ProducesResponseType(typeof(CanillaDto), StatusCodes.Status200OK)] |         [ProducesResponseType(typeof(CanillaDto), StatusCodes.Status200OK)] | ||||||
|   | |||||||
| @@ -64,6 +64,23 @@ namespace GestionIntegral.Api.Controllers.Distribucion | |||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         [HttpGet("dropdown")] | ||||||
|  |         [ProducesResponseType(typeof(IEnumerable<OtroDestinoDropdownDto>), StatusCodes.Status200OK)] | ||||||
|  |         [ProducesResponseType(StatusCodes.Status500InternalServerError)] | ||||||
|  |         public async Task<IActionResult> GetAllOtrosDestinosDropdown() | ||||||
|  |         { | ||||||
|  |             try | ||||||
|  |             { | ||||||
|  |                 var destinos = await _otroDestinoService.ObtenerTodosDropdownAsync(); | ||||||
|  |                 return Ok(destinos); | ||||||
|  |             } | ||||||
|  |             catch (Exception ex) | ||||||
|  |             { | ||||||
|  |                 _logger.LogError(ex, "Error al obtener Otros Destinos para dropdown."); | ||||||
|  |                 return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al obtener la lista de destinos."); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|         // GET: api/otrosdestinos/{id} |         // GET: api/otrosdestinos/{id} | ||||||
|         [HttpGet("{id:int}", Name = "GetOtroDestinoById")] |         [HttpGet("{id:int}", Name = "GetOtroDestinoById")] | ||||||
|         [ProducesResponseType(typeof(OtroDestinoDto), StatusCodes.Status200OK)] |         [ProducesResponseType(typeof(OtroDestinoDto), StatusCodes.Status200OK)] | ||||||
|   | |||||||
| @@ -42,7 +42,7 @@ namespace GestionIntegral.Api.Controllers.Distribucion | |||||||
|         [HttpGet] |         [HttpGet] | ||||||
|         [ProducesResponseType(typeof(IEnumerable<PublicacionDto>), StatusCodes.Status200OK)] |         [ProducesResponseType(typeof(IEnumerable<PublicacionDto>), StatusCodes.Status200OK)] | ||||||
|         [ProducesResponseType(StatusCodes.Status403Forbidden)] |         [ProducesResponseType(StatusCodes.Status403Forbidden)] | ||||||
|         public async Task<IActionResult> GetAllPublicaciones([FromQuery] string? nombre, [FromQuery] int? idEmpresa, [FromQuery] bool? soloHabilitadas = true) |         public async Task<IActionResult> GetAllPublicaciones([FromQuery] string? nombre, [FromQuery] int? idEmpresa, [FromQuery] bool? soloHabilitadas) | ||||||
|         { |         { | ||||||
|             if (!TienePermiso(PermisoVer)) return Forbid(); |             if (!TienePermiso(PermisoVer)) return Forbid(); | ||||||
|             var publicaciones = await _publicacionService.ObtenerTodasAsync(nombre, idEmpresa, soloHabilitadas); |             var publicaciones = await _publicacionService.ObtenerTodasAsync(nombre, idEmpresa, soloHabilitadas); | ||||||
| @@ -54,7 +54,7 @@ namespace GestionIntegral.Api.Controllers.Distribucion | |||||||
|         [ProducesResponseType(typeof(IEnumerable<PublicacionDropdownDto>), StatusCodes.Status200OK)] |         [ProducesResponseType(typeof(IEnumerable<PublicacionDropdownDto>), StatusCodes.Status200OK)] | ||||||
|         [ProducesResponseType(StatusCodes.Status500InternalServerError)] |         [ProducesResponseType(StatusCodes.Status500InternalServerError)] | ||||||
|         // No se verifica permiso DP001, solo requiere autenticación general ([Authorize] del controlador) |         // No se verifica permiso DP001, solo requiere autenticación general ([Authorize] del controlador) | ||||||
|         public async Task<IActionResult> GetPublicacionesForDropdown([FromQuery] bool soloHabilitadas = true) |         public async Task<IActionResult> GetPublicacionesForDropdown([FromQuery] bool? soloHabilitadas) | ||||||
|         { |         { | ||||||
|             try |             try | ||||||
|             { |             { | ||||||
|   | |||||||
| @@ -67,6 +67,23 @@ namespace GestionIntegral.Api.Controllers.Impresion | |||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         // GET: api/estadosbobina/dropdown | ||||||
|  |         [HttpGet("dropdown")] | ||||||
|  |         [ProducesResponseType(typeof(IEnumerable<EstadoBobinaDto>), StatusCodes.Status200OK)] | ||||||
|  |         public async Task<IActionResult> GetAllDropdownEstadosBobina() | ||||||
|  |         { | ||||||
|  |             try | ||||||
|  |             { | ||||||
|  |                 var estados = await _estadoBobinaService.ObtenerTodosDropdownAsync(); | ||||||
|  |                 return Ok(estados); | ||||||
|  |             } | ||||||
|  |             catch (Exception ex) | ||||||
|  |             { | ||||||
|  |                 _logger.LogError(ex, "Error al obtener todos los Estados de Bobina."); | ||||||
|  |                 return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al obtener los estados de bobina."); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|         // GET: api/estadosbobina/{id} |         // GET: api/estadosbobina/{id} | ||||||
|         [HttpGet("{id:int}", Name = "GetEstadoBobinaById")] |         [HttpGet("{id:int}", Name = "GetEstadoBobinaById")] | ||||||
|         [ProducesResponseType(typeof(EstadoBobinaDto), StatusCodes.Status200OK)] |         [ProducesResponseType(typeof(EstadoBobinaDto), StatusCodes.Status200OK)] | ||||||
|   | |||||||
| @@ -62,6 +62,25 @@ namespace GestionIntegral.Api.Controllers.Impresion | |||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         // GET: api/tiposbobina/dropdown | ||||||
|  |         [HttpGet("dropdown")] | ||||||
|  |         [ProducesResponseType(typeof(IEnumerable<TipoBobinaDto>), StatusCodes.Status200OK)] | ||||||
|  |         [ProducesResponseType(StatusCodes.Status403Forbidden)] | ||||||
|  |         [ProducesResponseType(StatusCodes.Status500InternalServerError)] | ||||||
|  |         public async Task<IActionResult> GetAllTiposBobina() | ||||||
|  |         { | ||||||
|  |             try | ||||||
|  |             { | ||||||
|  |                 var tiposBobina = await _tipoBobinaService.ObtenerTodosDropdownAsync(); | ||||||
|  |                 return Ok(tiposBobina); | ||||||
|  |             } | ||||||
|  |             catch (Exception ex) | ||||||
|  |             { | ||||||
|  |                 _logger.LogError(ex, "Error al obtener todos los Tipos de Bobina."); | ||||||
|  |                 return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al obtener los tipos de bobina."); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|         // GET: api/tiposbobina/{id} |         // GET: api/tiposbobina/{id} | ||||||
|         // Permiso: IB006 (Ver Tipos Bobinas) |         // Permiso: IB006 (Ver Tipos Bobinas) | ||||||
|         [HttpGet("{id:int}", Name = "GetTipoBobinaById")] |         [HttpGet("{id:int}", Name = "GetTipoBobinaById")] | ||||||
|   | |||||||
| @@ -22,7 +22,8 @@ namespace GestionIntegral.Api.Controllers.Impresion | |||||||
|         // Permisos para Tiradas (IT001 a IT003) |         // Permisos para Tiradas (IT001 a IT003) | ||||||
|         private const string PermisoVerTiradas = "IT001"; |         private const string PermisoVerTiradas = "IT001"; | ||||||
|         private const string PermisoRegistrarTirada = "IT002"; |         private const string PermisoRegistrarTirada = "IT002"; | ||||||
|         private const string PermisoEliminarTirada = "IT003"; // Asumo que se refiere a eliminar una tirada completa (cabecera y detalles) |         private const string PermisoEliminarTirada = "IT003"; | ||||||
|  |         private const string PermisoModificarTirada = "IT004"; | ||||||
|  |  | ||||||
|         public TiradasController(ITiradaService tiradaService, ILogger<TiradasController> logger) |         public TiradasController(ITiradaService tiradaService, ILogger<TiradasController> logger) | ||||||
|         { |         { | ||||||
| @@ -83,6 +84,43 @@ namespace GestionIntegral.Api.Controllers.Impresion | |||||||
|             return StatusCode(StatusCodes.Status201Created, tiradaCreada); |             return StatusCode(StatusCodes.Status201Created, tiradaCreada); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         [HttpPut] | ||||||
|  |         [ProducesResponseType(typeof(TiradaDto), StatusCodes.Status200OK)] | ||||||
|  |         [ProducesResponseType(StatusCodes.Status400BadRequest)] | ||||||
|  |         [ProducesResponseType(StatusCodes.Status403Forbidden)] | ||||||
|  |         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||||
|  |         public async Task<IActionResult> ModificarTirada( | ||||||
|  |             [FromQuery, BindRequired] DateTime fecha, | ||||||
|  |             [FromQuery, BindRequired] int idPublicacion, | ||||||
|  |             [FromQuery, BindRequired] int idPlanta, | ||||||
|  |             [FromBody] UpdateTiradaRequestDto updateDto) | ||||||
|  |         { | ||||||
|  |             if (!TienePermiso(PermisoModificarTirada)) return Forbid(); | ||||||
|  |             if (!ModelState.IsValid) return BadRequest(ModelState); | ||||||
|  |  | ||||||
|  |             var userId = GetCurrentUserId(); | ||||||
|  |             if (userId == null) return Unauthorized(); | ||||||
|  |  | ||||||
|  |             var (tiradaActualizada, error) = await _tiradaService.ModificarTiradaCompletaAsync(fecha, idPublicacion, idPlanta, updateDto, userId.Value); | ||||||
|  |  | ||||||
|  |             if (error != null) | ||||||
|  |             { | ||||||
|  |                 // Chequear si el error es porque no se encontró la tirada. | ||||||
|  |                 if (error.StartsWith("No se encontró la tirada")) | ||||||
|  |                 { | ||||||
|  |                     return NotFound(new { message = error }); | ||||||
|  |                 } | ||||||
|  |                 return BadRequest(new { message = error }); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             if (tiradaActualizada == null) | ||||||
|  |             { | ||||||
|  |                 return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al modificar la tirada."); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             return Ok(tiradaActualizada); | ||||||
|  |         } | ||||||
|  |  | ||||||
|         // DELETE: api/tiradas |         // DELETE: api/tiradas | ||||||
|         // Se identifica la tirada a eliminar por su combinación única de Fecha, IdPublicacion, IdPlanta |         // Se identifica la tirada a eliminar por su combinación única de Fecha, IdPublicacion, IdPlanta | ||||||
|         [HttpDelete] |         [HttpDelete] | ||||||
|   | |||||||
| @@ -1,12 +1,9 @@ | |||||||
| // --- REEMPLAZAR ARCHIVO: Controllers/Reportes/PdfTemplates/DistribucionCanillasDocument.cs --- |  | ||||||
| using GestionIntegral.Api.Dtos.Reportes; | using GestionIntegral.Api.Dtos.Reportes; | ||||||
| using GestionIntegral.Api.Dtos.Reportes.ViewModels; | using GestionIntegral.Api.Dtos.Reportes.ViewModels; | ||||||
| using QuestPDF.Fluent; | using QuestPDF.Fluent; | ||||||
| using QuestPDF.Helpers; | using QuestPDF.Helpers; | ||||||
| using QuestPDF.Infrastructure; | using QuestPDF.Infrastructure; | ||||||
| using System.Collections.Generic; |  | ||||||
| using System.Globalization; | using System.Globalization; | ||||||
| using System.Linq; |  | ||||||
|  |  | ||||||
| namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates | namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates | ||||||
| { | { | ||||||
|   | |||||||
| @@ -0,0 +1,152 @@ | |||||||
|  | using GestionIntegral.Api.Dtos.Reportes.ViewModels; | ||||||
|  | using QuestPDF.Fluent; | ||||||
|  | using QuestPDF.Helpers; | ||||||
|  | using QuestPDF.Infrastructure; | ||||||
|  |  | ||||||
|  | namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates | ||||||
|  | { | ||||||
|  |     public class DistribucionSuscripcionesDocument : IDocument | ||||||
|  |     { | ||||||
|  |         public DistribucionSuscripcionesViewModel Model { get; } | ||||||
|  |  | ||||||
|  |         public DistribucionSuscripcionesDocument(DistribucionSuscripcionesViewModel model) | ||||||
|  |         { | ||||||
|  |             Model = model; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public DocumentMetadata GetMetadata() => DocumentMetadata.Default; | ||||||
|  |  | ||||||
|  |         public void Compose(IDocumentContainer container) | ||||||
|  |         { | ||||||
|  |             container.Page(page => | ||||||
|  |             { | ||||||
|  |                 page.Margin(1, Unit.Centimetre); | ||||||
|  |                 page.DefaultTextStyle(x => x.FontFamily("Arial").FontSize(9)); | ||||||
|  |                 page.Header().Element(ComposeHeader); | ||||||
|  |                 page.Content().Element(ComposeContent); | ||||||
|  |                 page.Footer().AlignCenter().Text(x => { x.Span("Página "); x.CurrentPageNumber(); }); | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         void ComposeHeader(IContainer container) | ||||||
|  |         { | ||||||
|  |             container.Column(column => | ||||||
|  |             { | ||||||
|  |                 column.Item().Row(row => | ||||||
|  |                 { | ||||||
|  |                     row.RelativeItem().Column(col => | ||||||
|  |                     { | ||||||
|  |                         col.Item().Text("Reporte de Distribución de Suscripciones").SemiBold().FontSize(14); | ||||||
|  |                         col.Item().Text($"Período: {Model.FechaDesde} al {Model.FechaHasta}").FontSize(11); | ||||||
|  |                     }); | ||||||
|  |                     row.ConstantItem(150).AlignRight().Text($"Generado: {Model.FechaGeneracion}"); | ||||||
|  |                 }); | ||||||
|  |                 column.Item().PaddingTop(5).BorderBottom(1).BorderColor(Colors.Grey.Lighten2); | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         void ComposeContent(IContainer container) | ||||||
|  |         { | ||||||
|  |             container.PaddingTop(10).Column(column => | ||||||
|  |             { | ||||||
|  |                 column.Spacing(20); // Espacio entre elementos principales (sección de altas y sección de bajas) | ||||||
|  |  | ||||||
|  |                 // --- Sección 1: Altas y Activas --- | ||||||
|  |                 column.Item().Column(colAltas => | ||||||
|  |                 { | ||||||
|  |                     colAltas.Item().Text("Altas y Suscripciones Activas en el Período").Bold().FontSize(14).Underline(); | ||||||
|  |                     colAltas.Item().PaddingBottom(10).Text("Listado de suscriptores que deben recibir entregas en el período seleccionado."); | ||||||
|  |  | ||||||
|  |                     if (!Model.DatosAgrupadosAltas.Any()) | ||||||
|  |                     { | ||||||
|  |                         colAltas.Item().PaddingTop(10).Text("No se encontraron suscripciones activas para este período.").Italic(); | ||||||
|  |                     } | ||||||
|  |                     else | ||||||
|  |                     { | ||||||
|  |                         foreach (var empresa in Model.DatosAgrupadosAltas) | ||||||
|  |                         { | ||||||
|  |                             colAltas.Item().Element(c => ComposeTablaEmpresa(c, empresa, esBaja: false)); | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 }); | ||||||
|  |                  | ||||||
|  |                 // --- Sección 2: Bajas --- | ||||||
|  |                 if (Model.DatosAgrupadosBajas.Any()) | ||||||
|  |                 { | ||||||
|  |                     column.Item().PageBreak(); // Salto de página para separar las secciones | ||||||
|  |                     column.Item().Column(colBajas => | ||||||
|  |                     { | ||||||
|  |                         colBajas.Item().Text("Bajas de Suscripciones en el Período").Bold().FontSize(14).Underline().FontColor(Colors.Red.Medium); | ||||||
|  |                         colBajas.Item().PaddingBottom(10).Text("Listado de suscriptores cuya suscripción finalizó. NO se les debe entregar a partir de su 'Fecha de Baja'."); | ||||||
|  |  | ||||||
|  |                         foreach (var empresa in Model.DatosAgrupadosBajas) | ||||||
|  |                         { | ||||||
|  |                             colBajas.Item().Element(c => ComposeTablaEmpresa(c, empresa, esBaja: true)); | ||||||
|  |                         } | ||||||
|  |                     }); | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         void ComposeTablaEmpresa(IContainer container, GrupoEmpresa empresa, bool esBaja) | ||||||
|  |         { | ||||||
|  |             container.Column(column => | ||||||
|  |             { | ||||||
|  |                 // Cabecera de la EMPRESA (ej. EL DIA) | ||||||
|  |                 column.Item().Background(Colors.Grey.Lighten2).Padding(5).Text(empresa.NombreEmpresa).Bold().FontSize(12); | ||||||
|  |                  | ||||||
|  |                 // Contenedor para las tablas de las publicaciones de esta empresa | ||||||
|  |                 column.Item().PaddingTop(5).Column(colPub => | ||||||
|  |                 { | ||||||
|  |                     colPub.Spacing(10); // Espacio entre cada tabla de publicación | ||||||
|  |                     foreach (var publicacion in empresa.Publicaciones) | ||||||
|  |                     { | ||||||
|  |                         colPub.Item().Element(c => ComposeTablaPublicacion(c, publicacion, esBaja)); | ||||||
|  |                     } | ||||||
|  |                 }); | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         void ComposeTablaPublicacion(IContainer container, GrupoPublicacion publicacion, bool esBaja) | ||||||
|  |         { | ||||||
|  |             // Se envuelve la tabla en una columna para poder ponerle un título simple arriba. | ||||||
|  |             container.Column(column => | ||||||
|  |             { | ||||||
|  |                 column.Item().PaddingLeft(2).PaddingBottom(2).Text(publicacion.NombrePublicacion).SemiBold().FontSize(10); | ||||||
|  |                 column.Item().Table(table => | ||||||
|  |                 { | ||||||
|  |                     table.ColumnsDefinition(columns => | ||||||
|  |                     { | ||||||
|  |                         columns.RelativeColumn(2.5f); // Nombre | ||||||
|  |                         columns.RelativeColumn(3);   // Dirección | ||||||
|  |                         columns.RelativeColumn(1.5f); // Teléfono | ||||||
|  |                         columns.ConstantColumn(65);  // Fecha Inicio / Baja | ||||||
|  |                         columns.RelativeColumn(1.5f); // Días | ||||||
|  |                         columns.RelativeColumn(2.5f); // Observaciones | ||||||
|  |                     }); | ||||||
|  |  | ||||||
|  |                     table.Header(header => | ||||||
|  |                     { | ||||||
|  |                         header.Cell().BorderBottom(1).Padding(2).Text("Suscriptor").SemiBold(); | ||||||
|  |                         header.Cell().BorderBottom(1).Padding(2).Text("Dirección").SemiBold(); | ||||||
|  |                         header.Cell().BorderBottom(1).Padding(2).Text("Teléfono").SemiBold(); | ||||||
|  |                         header.Cell().BorderBottom(1).Padding(2).Text(esBaja ? "Fecha de Baja" : "Fecha Inicio").SemiBold(); | ||||||
|  |                         header.Cell().BorderBottom(1).Padding(2).Text("Días Entrega").SemiBold(); | ||||||
|  |                         header.Cell().BorderBottom(1).Padding(2).Text("Observaciones").SemiBold(); | ||||||
|  |                     }); | ||||||
|  |  | ||||||
|  |                     foreach (var item in publicacion.Suscripciones) | ||||||
|  |                     { | ||||||
|  |                         table.Cell().Padding(2).Text(item.NombreSuscriptor); | ||||||
|  |                         table.Cell().Padding(2).Text(item.Direccion); | ||||||
|  |                         table.Cell().Padding(2).Text(item.Telefono ?? "-"); | ||||||
|  |                         var fecha = esBaja ? item.FechaFin : item.FechaInicio; | ||||||
|  |                         table.Cell().Padding(2).Text(fecha?.ToString("dd/MM/yyyy") ?? "-");                         | ||||||
|  |                         table.Cell().Padding(2).Text(item.DiasEntrega); | ||||||
|  |                         table.Cell().Padding(2).Text(item.Observaciones ?? "-"); | ||||||
|  |                     } | ||||||
|  |                 }); | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,121 @@ | |||||||
|  | using GestionIntegral.Api.Dtos.Reportes.ViewModels; | ||||||
|  | using QuestPDF.Fluent; | ||||||
|  | using QuestPDF.Helpers; | ||||||
|  | using QuestPDF.Infrastructure; | ||||||
|  | using System.Globalization; | ||||||
|  | using System.Linq; | ||||||
|  |  | ||||||
|  | namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates | ||||||
|  | { | ||||||
|  |     public class FacturasPublicidadDocument : IDocument | ||||||
|  |     { | ||||||
|  |         public FacturasPublicidadViewModel Model { get; } | ||||||
|  |  | ||||||
|  |         public FacturasPublicidadDocument(FacturasPublicidadViewModel model) | ||||||
|  |         { | ||||||
|  |             Model = model; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public DocumentMetadata GetMetadata() => DocumentMetadata.Default; | ||||||
|  |  | ||||||
|  |         public void Compose(IDocumentContainer container) | ||||||
|  |         { | ||||||
|  |             container.Page(page => | ||||||
|  |             { | ||||||
|  |                 page.Margin(1, Unit.Centimetre); | ||||||
|  |                 page.DefaultTextStyle(x => x.FontFamily("Arial").FontSize(9)); | ||||||
|  |                  | ||||||
|  |                 page.Header().Element(ComposeHeader); | ||||||
|  |                 page.Content().Element(ComposeContent); | ||||||
|  |                 page.Footer().AlignCenter().Text(x => { x.Span("Página "); x.CurrentPageNumber(); }); | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         void ComposeHeader(IContainer container) | ||||||
|  |         { | ||||||
|  |             // Se envuelve todo el contenido del header en una única Columna. | ||||||
|  |             container.Column(column => | ||||||
|  |             { | ||||||
|  |                 // El primer item de la columna es la fila con los títulos. | ||||||
|  |                 column.Item().Row(row => | ||||||
|  |                 { | ||||||
|  |                     row.RelativeItem().Column(col => | ||||||
|  |                     { | ||||||
|  |                         col.Item().Text($"Reporte de Suscripciones a Facturar").SemiBold().FontSize(14); | ||||||
|  |                         col.Item().Text($"Período: {Model.Periodo}").FontSize(11); | ||||||
|  |                     }); | ||||||
|  |  | ||||||
|  |                     row.ConstantItem(150).AlignRight().Column(col => { | ||||||
|  |                         col.Item().AlignRight().Text($"Fecha de Generación:"); | ||||||
|  |                         col.Item().AlignRight().Text(Model.FechaGeneracion); | ||||||
|  |                     }); | ||||||
|  |                 }); | ||||||
|  |  | ||||||
|  |                 // El segundo item de la columna es el separador. | ||||||
|  |                 column.Item().PaddingTop(5).BorderBottom(1).BorderColor(Colors.Grey.Lighten2); | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         void ComposeContent(IContainer container) | ||||||
|  |         { | ||||||
|  |             container.PaddingTop(10).Column(column => | ||||||
|  |             { | ||||||
|  |                 column.Spacing(20); | ||||||
|  |  | ||||||
|  |                 foreach (var empresaData in Model.DatosPorEmpresa) | ||||||
|  |                 { | ||||||
|  |                     column.Item().Element(c => ComposeTablaPorEmpresa(c, empresaData)); | ||||||
|  |                 } | ||||||
|  |                  | ||||||
|  |                 column.Item().AlignRight().PaddingTop(15).Text($"Total General a Facturar: {Model.TotalGeneral.ToString("C", new CultureInfo("es-AR"))}").Bold().FontSize(12); | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         void ComposeTablaPorEmpresa(IContainer container, DatosEmpresaViewModel empresaData) | ||||||
|  |         { | ||||||
|  |             container.Table(table => | ||||||
|  |             { | ||||||
|  |                 table.ColumnsDefinition(columns => | ||||||
|  |                 { | ||||||
|  |                     columns.RelativeColumn(3); // Nombre Suscriptor | ||||||
|  |                     columns.ConstantColumn(100); // Documento | ||||||
|  |                     columns.ConstantColumn(100, Unit.Point); // Importe | ||||||
|  |                 }); | ||||||
|  |  | ||||||
|  |                 table.Header(header => | ||||||
|  |                 { | ||||||
|  |                     header.Cell().ColumnSpan(3).Background(Colors.Grey.Lighten2) | ||||||
|  |                         .Padding(5).Text(empresaData.NombreEmpresa).Bold().FontSize(12); | ||||||
|  |                      | ||||||
|  |                     header.Cell().BorderBottom(1).BorderColor(Colors.Grey.Lighten3).Padding(2).Text("Suscriptor").SemiBold(); | ||||||
|  |                     header.Cell().BorderBottom(1).BorderColor(Colors.Grey.Lighten3).Padding(2).Text("Documento").SemiBold(); | ||||||
|  |                     header.Cell().BorderBottom(1).BorderColor(Colors.Grey.Lighten3).Padding(2).AlignRight().Text("Importe a Facturar").SemiBold(); | ||||||
|  |                 }); | ||||||
|  |                  | ||||||
|  |                 var facturasPorSuscriptor = empresaData.Facturas.GroupBy(f => f.NombreSuscriptor); | ||||||
|  |  | ||||||
|  |                 foreach (var grupoSuscriptor in facturasPorSuscriptor.OrderBy(g => g.Key)) | ||||||
|  |                 { | ||||||
|  |                     foreach(var item in grupoSuscriptor) | ||||||
|  |                     { | ||||||
|  |                         table.Cell().Padding(2).Text(item.NombreSuscriptor); | ||||||
|  |                         table.Cell().Padding(2).Text($"{item.TipoDocumento} {item.NroDocumento}"); | ||||||
|  |                         table.Cell().Padding(2).AlignRight().Text(item.ImporteFinal.ToString("C", new CultureInfo("es-AR"))); | ||||||
|  |                     } | ||||||
|  |  | ||||||
|  |                     if(grupoSuscriptor.Count() > 1) | ||||||
|  |                     { | ||||||
|  |                         var subtotal = grupoSuscriptor.Sum(i => i.ImporteFinal); | ||||||
|  |                         table.Cell().ColumnSpan(2).AlignRight().Padding(2).Text($"Subtotal {grupoSuscriptor.Key}:").Italic(); | ||||||
|  |                         table.Cell().AlignRight().Padding(2).Text(subtotal.ToString("C", new CultureInfo("es-AR"))).Italic().SemiBold(); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                  | ||||||
|  |                 table.Cell().ColumnSpan(2).BorderTop(1).BorderColor(Colors.Grey.Darken1).AlignRight() | ||||||
|  |                     .PaddingTop(5).Text("Total Empresa:").Bold(); | ||||||
|  |                 table.Cell().BorderTop(1).BorderColor(Colors.Grey.Darken1).AlignRight() | ||||||
|  |                     .PaddingTop(5).Text(empresaData.TotalEmpresa.ToString("C", new CultureInfo("es-AR"))).Bold(); | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -69,7 +69,7 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates | |||||||
|             { |             { | ||||||
|                 table.ColumnsDefinition(columns => |                 table.ColumnsDefinition(columns => | ||||||
|                 { |                 { | ||||||
|                     columns.ConstantColumn(40); |                     columns.ConstantColumn(60); | ||||||
|                     columns.RelativeColumn(); |                     columns.RelativeColumn(); | ||||||
|                     columns.RelativeColumn(); |                     columns.RelativeColumn(); | ||||||
|                     columns.RelativeColumn(); |                     columns.RelativeColumn(); | ||||||
| @@ -89,9 +89,20 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates | |||||||
|                     header.Cell().Border(1).Background(Colors.Grey.Lighten3).Padding(4).AlignRight().Text("Vendidos"); |                     header.Cell().Border(1).Background(Colors.Grey.Lighten3).Padding(4).AlignRight().Text("Vendidos"); | ||||||
|                 }); |                 }); | ||||||
|  |  | ||||||
|  |                 var dayAbbreviations = new Dictionary<System.DayOfWeek, string> | ||||||
|  |                 { | ||||||
|  |                     { System.DayOfWeek.Sunday, "Dom" }, | ||||||
|  |                     { System.DayOfWeek.Monday, "Lun" }, | ||||||
|  |                     { System.DayOfWeek.Tuesday, "Mar" }, | ||||||
|  |                     { System.DayOfWeek.Wednesday, "Mie" }, | ||||||
|  |                     { System.DayOfWeek.Thursday, "Jue" }, | ||||||
|  |                     { System.DayOfWeek.Friday, "Vie" }, | ||||||
|  |                     { System.DayOfWeek.Saturday, "Sab" } | ||||||
|  |                 }; | ||||||
|  |  | ||||||
|                 foreach (var item in Model.ResumenMensual.OrderBy(x => x.Fecha)) |                 foreach (var item in Model.ResumenMensual.OrderBy(x => x.Fecha)) | ||||||
|                 { |                 { | ||||||
|                     table.Cell().Border(1).Padding(3).Text(item.Fecha.Day.ToString()); |                     table.Cell().Border(1).Padding(3).Text($"{dayAbbreviations[item.Fecha.DayOfWeek]} {item.Fecha.Day}"); | ||||||
|                     table.Cell().Border(1).Padding(3).AlignRight().Text(item.CantidadTirada.ToString("N0")); |                     table.Cell().Border(1).Padding(3).AlignRight().Text(item.CantidadTirada.ToString("N0")); | ||||||
|                     table.Cell().Border(1).Padding(3).AlignRight().Text(item.SinCargo.ToString("N0")); |                     table.Cell().Border(1).Padding(3).AlignRight().Text(item.SinCargo.ToString("N0")); | ||||||
|                     table.Cell().Border(1).Padding(3).AlignRight().Text(item.Perdidos.ToString("N0")); |                     table.Cell().Border(1).Padding(3).AlignRight().Text(item.Perdidos.ToString("N0")); | ||||||
|   | |||||||
| @@ -1,15 +1,8 @@ | |||||||
| using GestionIntegral.Api.Services.Reportes; | using GestionIntegral.Api.Services.Reportes; | ||||||
| using Microsoft.AspNetCore.Authorization; | using Microsoft.AspNetCore.Authorization; | ||||||
| using Microsoft.AspNetCore.Mvc; | using Microsoft.AspNetCore.Mvc; | ||||||
| using Microsoft.Reporting.NETCore; |  | ||||||
| using Microsoft.Extensions.Logging; |  | ||||||
| using System; |  | ||||||
| using System.Collections.Generic; |  | ||||||
| using System.Threading.Tasks; |  | ||||||
| using GestionIntegral.Api.Dtos.Reportes; | using GestionIntegral.Api.Dtos.Reportes; | ||||||
| using GestionIntegral.Api.Data.Repositories.Impresion; | using GestionIntegral.Api.Data.Repositories.Impresion; | ||||||
| using System.IO; |  | ||||||
| using System.Linq; |  | ||||||
| using GestionIntegral.Api.Data.Repositories.Distribucion; | using GestionIntegral.Api.Data.Repositories.Distribucion; | ||||||
| using GestionIntegral.Api.Services.Distribucion; | using GestionIntegral.Api.Services.Distribucion; | ||||||
| using GestionIntegral.Api.Services.Pdf; | using GestionIntegral.Api.Services.Pdf; | ||||||
| @@ -45,6 +38,8 @@ namespace GestionIntegral.Api.Controllers | |||||||
|         private const string PermisoVerReporteConsumoBobinas = "RR007"; |         private const string PermisoVerReporteConsumoBobinas = "RR007"; | ||||||
|         private const string PermisoVerReporteNovedadesCanillas = "RR004"; |         private const string PermisoVerReporteNovedadesCanillas = "RR004"; | ||||||
|         private const string PermisoVerReporteListadoDistMensual = "RR009"; |         private const string PermisoVerReporteListadoDistMensual = "RR009"; | ||||||
|  |         private const string PermisoVerReporteFacturasPublicidad = "RR010"; | ||||||
|  |         private const string PermisoVerReporteDistSuscripciones = "RR011"; | ||||||
|  |  | ||||||
|         public ReportesController( |         public ReportesController( | ||||||
|             IReportesService reportesService, |             IReportesService reportesService, | ||||||
| @@ -1676,5 +1671,88 @@ namespace GestionIntegral.Api.Controllers | |||||||
|                 return StatusCode(500, "Error interno al generar el PDF del reporte."); |                 return StatusCode(500, "Error interno al generar el PDF del reporte."); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         [HttpGet("suscripciones/facturas-para-publicidad/pdf")] | ||||||
|  |         [ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)] | ||||||
|  |         [ProducesResponseType(StatusCodes.Status400BadRequest)] | ||||||
|  |         [ProducesResponseType(StatusCodes.Status403Forbidden)] | ||||||
|  |         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||||
|  |         [ProducesResponseType(StatusCodes.Status500InternalServerError)] | ||||||
|  |         public async Task<IActionResult> GetReporteFacturasPublicidadPdf([FromQuery] int anio, [FromQuery] int mes) | ||||||
|  |         { | ||||||
|  |             if (!TienePermiso(PermisoVerReporteFacturasPublicidad)) return Forbid(); | ||||||
|  |  | ||||||
|  |             var (data, error) = await _reportesService.ObtenerFacturasParaReportePublicidad(anio, mes); | ||||||
|  |             if (error != null) return BadRequest(new { message = error }); | ||||||
|  |             if (data == null || !data.Any()) | ||||||
|  |             { | ||||||
|  |                 return NotFound(new { message = "No hay facturas pagadas y pendientes de facturar para el período seleccionado." }); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             try | ||||||
|  |             { | ||||||
|  |                 // --- INICIO DE LA LÓGICA DE AGRUPACIÓN --- | ||||||
|  |                 var datosAgrupados = data | ||||||
|  |                     .GroupBy(f => f.IdEmpresa) | ||||||
|  |                     .Select(g => new DatosEmpresaViewModel | ||||||
|  |                     { | ||||||
|  |                         NombreEmpresa = g.First().NombreEmpresa, | ||||||
|  |                         Facturas = g.ToList() | ||||||
|  |                     }) | ||||||
|  |                     .OrderBy(e => e.NombreEmpresa); | ||||||
|  |  | ||||||
|  |                 var viewModel = new FacturasPublicidadViewModel | ||||||
|  |                 { | ||||||
|  |                     DatosPorEmpresa = datosAgrupados, | ||||||
|  |                     Periodo = new DateTime(anio, mes, 1).ToString("MMMM yyyy", new CultureInfo("es-ES")), | ||||||
|  |                     FechaGeneracion = DateTime.Now.ToString("dd/MM/yyyy HH:mm") | ||||||
|  |                 }; | ||||||
|  |                 // --- FIN DE LA LÓGICA DE AGRUPACIÓN --- | ||||||
|  |  | ||||||
|  |                 var document = new FacturasPublicidadDocument(viewModel); | ||||||
|  |                 byte[] pdfBytes = await _pdfGenerator.GeneratePdfAsync(document); | ||||||
|  |                 string fileName = $"ReportePublicidad_Suscripciones_{anio}-{mes:D2}.pdf"; | ||||||
|  |                 return File(pdfBytes, "application/pdf", fileName); | ||||||
|  |             } | ||||||
|  |             catch (Exception ex) | ||||||
|  |             { | ||||||
|  |                 _logger.LogError(ex, "Error al generar PDF para Reporte de Facturas a Publicidad."); | ||||||
|  |                 return StatusCode(500, "Error interno al generar el PDF del reporte."); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         [HttpGet("suscripciones/distribucion/pdf")] | ||||||
|  |         [ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)] | ||||||
|  |         public async Task<IActionResult> GetReporteDistribucionSuscripcionesPdf([FromQuery] DateTime fechaDesde, [FromQuery] DateTime fechaHasta) | ||||||
|  |         { | ||||||
|  |             if (!TienePermiso(PermisoVerReporteDistSuscripciones)) return Forbid(); | ||||||
|  |  | ||||||
|  |             var (altas, bajas, error) = await _reportesService.ObtenerReporteDistribucionSuscripcionesAsync(fechaDesde, fechaHasta); | ||||||
|  |             if (error != null) return BadRequest(new { message = error }); | ||||||
|  |             if ((altas == null || !altas.Any()) && (bajas == null || !bajas.Any())) | ||||||
|  |             { | ||||||
|  |                 return NotFound(new { message = "No se encontraron suscripciones activas ni bajas para el período seleccionado." }); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             try | ||||||
|  |             { | ||||||
|  |                 var viewModel = new DistribucionSuscripcionesViewModel(altas ?? Enumerable.Empty<DistribucionSuscripcionDto>(), bajas ?? Enumerable.Empty<DistribucionSuscripcionDto>()) | ||||||
|  |                 { | ||||||
|  |                     FechaDesde = fechaDesde.ToString("dd/MM/yyyy"), | ||||||
|  |                     FechaHasta = fechaHasta.ToString("dd/MM/yyyy"), | ||||||
|  |                     FechaGeneracion = DateTime.Now.ToString("dd/MM/yyyy HH:mm") | ||||||
|  |                 }; | ||||||
|  |  | ||||||
|  |                 var document = new DistribucionSuscripcionesDocument(viewModel); | ||||||
|  |                 byte[] pdfBytes = await _pdfGenerator.GeneratePdfAsync(document); | ||||||
|  |                 string fileName = $"ReporteDistribucionSuscripciones_{fechaDesde:yyyyMMdd}_al_{fechaHasta:yyyyMMdd}.pdf"; | ||||||
|  |                 return File(pdfBytes, "application/pdf", fileName); | ||||||
|  |             } | ||||||
|  |             catch (Exception ex) | ||||||
|  |             { | ||||||
|  |                 _logger.LogError(ex, "Error al generar PDF para Reporte de Distribución de Suscripciones."); | ||||||
|  |                 return StatusCode(500, "Error interno al generar el PDF del reporte."); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -0,0 +1,94 @@ | |||||||
|  | using GestionIntegral.Api.Dtos.Suscripciones; | ||||||
|  | using GestionIntegral.Api.Services.Suscripciones; | ||||||
|  | using Microsoft.AspNetCore.Authorization; | ||||||
|  | using Microsoft.AspNetCore.Mvc; | ||||||
|  | using System.Security.Claims; | ||||||
|  |  | ||||||
|  | namespace GestionIntegral.Api.Controllers.Suscripciones | ||||||
|  | { | ||||||
|  |     [Route("api/ajustes")] | ||||||
|  |     [ApiController] | ||||||
|  |     [Authorize] | ||||||
|  |     public class AjustesController : ControllerBase | ||||||
|  |     { | ||||||
|  |         private readonly IAjusteService _ajusteService; | ||||||
|  |         private readonly ILogger<AjustesController> _logger; | ||||||
|  |  | ||||||
|  |         // Permiso a crear en BD | ||||||
|  |         private const string PermisoGestionarAjustes = "SU011"; | ||||||
|  |  | ||||||
|  |         public AjustesController(IAjusteService ajusteService, ILogger<AjustesController> logger) | ||||||
|  |         { | ||||||
|  |             _ajusteService = ajusteService; | ||||||
|  |             _logger = logger; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         private bool TienePermiso(string codAcc) => User.IsInRole("SuperAdmin") || User.HasClaim(c => c.Type == "permission" && c.Value == codAcc); | ||||||
|  |  | ||||||
|  |         private int? GetCurrentUserId() | ||||||
|  |         { | ||||||
|  |             if (int.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub"), out int userId)) return userId; | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // GET: api/suscriptores/{idSuscriptor}/ajustes | ||||||
|  |         [HttpGet("~/api/suscriptores/{idSuscriptor:int}/ajustes")] | ||||||
|  |         [ProducesResponseType(typeof(IEnumerable<AjusteDto>), StatusCodes.Status200OK)] | ||||||
|  |         public async Task<IActionResult> GetAjustesPorSuscriptor(int idSuscriptor, [FromQuery] DateTime? fechaDesde, [FromQuery] DateTime? fechaHasta) | ||||||
|  |         { | ||||||
|  |             if (!TienePermiso(PermisoGestionarAjustes)) return Forbid(); | ||||||
|  |             var ajustes = await _ajusteService.ObtenerAjustesPorSuscriptor(idSuscriptor, fechaDesde, fechaHasta); | ||||||
|  |             return Ok(ajustes); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // POST: api/ajustes | ||||||
|  |         [HttpPost] | ||||||
|  |         [ProducesResponseType(typeof(AjusteDto), StatusCodes.Status201Created)] | ||||||
|  |         public async Task<IActionResult> CreateAjuste([FromBody] CreateAjusteDto createDto) | ||||||
|  |         { | ||||||
|  |             if (!TienePermiso(PermisoGestionarAjustes)) return Forbid(); | ||||||
|  |             if (!ModelState.IsValid) return BadRequest(ModelState); | ||||||
|  |  | ||||||
|  |             var userId = GetCurrentUserId(); | ||||||
|  |             if (userId == null) return Unauthorized(); | ||||||
|  |  | ||||||
|  |             var (dto, error) = await _ajusteService.CrearAjusteManual(createDto, userId.Value); | ||||||
|  |  | ||||||
|  |             if (error != null) return BadRequest(new { message = error }); | ||||||
|  |             if (dto == null) return StatusCode(500, "Error al crear el ajuste."); | ||||||
|  |  | ||||||
|  |             // Devolvemos el objeto creado con un 201 | ||||||
|  |             return StatusCode(201, dto); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // POST: api/ajustes/{id}/anular | ||||||
|  |         [HttpPost("{id:int}/anular")] | ||||||
|  |         public async Task<IActionResult> Anular(int id) | ||||||
|  |         { | ||||||
|  |             if (!TienePermiso(PermisoGestionarAjustes)) return Forbid(); | ||||||
|  |             var userId = GetCurrentUserId(); | ||||||
|  |             if (userId == null) return Unauthorized(); | ||||||
|  |  | ||||||
|  |             var (exito, error) = await _ajusteService.AnularAjuste(id, userId.Value); | ||||||
|  |             if (!exito) return BadRequest(new { message = error }); | ||||||
|  |  | ||||||
|  |             return Ok(new { message = "Ajuste anulado correctamente." }); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // PUT: api/ajustes/{id} | ||||||
|  |         [HttpPut("{id:int}")] | ||||||
|  |         public async Task<IActionResult> UpdateAjuste(int id, [FromBody] UpdateAjusteDto updateDto) | ||||||
|  |         { | ||||||
|  |             if (!TienePermiso(PermisoGestionarAjustes)) return Forbid(); | ||||||
|  |             if (!ModelState.IsValid) return BadRequest(ModelState); | ||||||
|  |  | ||||||
|  |             var (exito, error) = await _ajusteService.ActualizarAjuste(id, updateDto); | ||||||
|  |             if (!exito) | ||||||
|  |             { | ||||||
|  |                 if (error != null && error.Contains("no encontrado")) return NotFound(new { message = error }); | ||||||
|  |                 return BadRequest(new { message = error }); | ||||||
|  |             } | ||||||
|  |             return NoContent(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,93 @@ | |||||||
|  | // Archivo: GestionIntegral.Api/Controllers/Suscripciones/DebitosController.cs | ||||||
|  |  | ||||||
|  | using GestionIntegral.Api.Dtos.Suscripciones; | ||||||
|  | using GestionIntegral.Api.Services.Suscripciones; | ||||||
|  | using Microsoft.AspNetCore.Authorization; | ||||||
|  | using Microsoft.AspNetCore.Mvc; | ||||||
|  | using System.Security.Claims; | ||||||
|  | using System.Text; | ||||||
|  |  | ||||||
|  | namespace GestionIntegral.Api.Controllers.Suscripciones | ||||||
|  | { | ||||||
|  |     [Route("api/debitos")] | ||||||
|  |     [ApiController] | ||||||
|  |     [Authorize] | ||||||
|  |     public class DebitosController : ControllerBase | ||||||
|  |     { | ||||||
|  |         private readonly IDebitoAutomaticoService _debitoService; | ||||||
|  |         private readonly ILogger<DebitosController> _logger; | ||||||
|  |  | ||||||
|  |         // Permiso para generar archivos de débito (a crear en BD) | ||||||
|  |         private const string PermisoGenerarDebitos = "SU007"; | ||||||
|  |  | ||||||
|  |         public DebitosController(IDebitoAutomaticoService debitoService, ILogger<DebitosController> logger) | ||||||
|  |         { | ||||||
|  |             _debitoService = debitoService; | ||||||
|  |             _logger = logger; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         private bool TienePermiso(string codAcc) => User.IsInRole("SuperAdmin") || User.HasClaim(c => c.Type == "permission" && c.Value == codAcc); | ||||||
|  |         private int? GetCurrentUserId() | ||||||
|  |         { | ||||||
|  |             if (int.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub"), out int userId)) return userId; | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // POST: api/debitos/{anio}/{mes}/generar-archivo | ||||||
|  |         [HttpPost("{anio:int}/{mes:int}/generar-archivo")] | ||||||
|  |         [ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)] | ||||||
|  |         [ProducesResponseType(StatusCodes.Status400BadRequest)] | ||||||
|  |         [ProducesResponseType(StatusCodes.Status403Forbidden)] | ||||||
|  |         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||||
|  |         public async Task<IActionResult> GenerarArchivo(int anio, int mes) | ||||||
|  |         { | ||||||
|  |             if (!TienePermiso(PermisoGenerarDebitos)) return Forbid(); | ||||||
|  |  | ||||||
|  |             var userId = GetCurrentUserId(); | ||||||
|  |             if (userId == null) return Unauthorized(); | ||||||
|  |  | ||||||
|  |             var (contenido, nombreArchivo, error) = await _debitoService.GenerarArchivoPagoDirecto(anio, mes, userId.Value); | ||||||
|  |  | ||||||
|  |             if (error != null) | ||||||
|  |             { | ||||||
|  |                 // Si el error es "No se encontraron facturas", es un 404. Otros son 400. | ||||||
|  |                 if (error.Contains("No se encontraron")) | ||||||
|  |                 { | ||||||
|  |                     return NotFound(new { message = error }); | ||||||
|  |                 } | ||||||
|  |                 return BadRequest(new { message = error }); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             if (string.IsNullOrEmpty(contenido) || string.IsNullOrEmpty(nombreArchivo)) | ||||||
|  |             { | ||||||
|  |                 return StatusCode(500, new { message = "El servicio no pudo generar el contenido del archivo correctamente." }); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             // Devolver el archivo para descarga | ||||||
|  |             var fileBytes = Encoding.UTF8.GetBytes(contenido); | ||||||
|  |             return File(fileBytes, "text/plain", nombreArchivo); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // POST: api/debitos/procesar-respuesta | ||||||
|  |         [HttpPost("procesar-respuesta")] | ||||||
|  |         [ProducesResponseType(typeof(ProcesamientoLoteResponseDto), StatusCodes.Status200OK)] | ||||||
|  |         [ProducesResponseType(StatusCodes.Status400BadRequest)] | ||||||
|  |         public async Task<IActionResult> ProcesarArchivoRespuesta(IFormFile archivo) | ||||||
|  |         { | ||||||
|  |             // Usamos el mismo permiso de generar débitos para procesar la respuesta. | ||||||
|  |             if (!TienePermiso(PermisoGenerarDebitos)) return Forbid(); | ||||||
|  |  | ||||||
|  |             var userId = GetCurrentUserId(); | ||||||
|  |             if (userId == null) return Unauthorized(); | ||||||
|  |  | ||||||
|  |             var resultado = await _debitoService.ProcesarArchivoRespuesta(archivo, userId.Value); | ||||||
|  |  | ||||||
|  |             if (resultado.Errores.Any() && resultado.PagosAprobados == 0 && resultado.PagosRechazados == 0) | ||||||
|  |             { | ||||||
|  |                 return BadRequest(resultado); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             return Ok(resultado); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,125 @@ | |||||||
|  | using GestionIntegral.Api.Dtos.Comunicaciones; | ||||||
|  | using GestionIntegral.Api.Services.Comunicaciones; | ||||||
|  | using GestionIntegral.Api.Services.Suscripciones; | ||||||
|  | using Microsoft.AspNetCore.Authorization; | ||||||
|  | using Microsoft.AspNetCore.Mvc; | ||||||
|  | using System.Security.Claims; | ||||||
|  |  | ||||||
|  | namespace GestionIntegral.Api.Controllers.Suscripciones | ||||||
|  | { | ||||||
|  |     [Route("api/facturacion")] | ||||||
|  |     [ApiController] | ||||||
|  |     [Authorize] | ||||||
|  |     public class FacturacionController : ControllerBase | ||||||
|  |     { | ||||||
|  |         private readonly IFacturacionService _facturacionService; | ||||||
|  |         private readonly ILogger<FacturacionController> _logger; | ||||||
|  |         private readonly IEmailLogService _emailLogService; | ||||||
|  |         private const string PermisoGestionarFacturacion = "SU006"; | ||||||
|  |         private const string PermisoEnviarEmail = "SU009"; | ||||||
|  |  | ||||||
|  |         public FacturacionController(IFacturacionService facturacionService, ILogger<FacturacionController> logger, IEmailLogService emailLogService) | ||||||
|  |         { | ||||||
|  |             _facturacionService = facturacionService; | ||||||
|  |             _logger = logger; | ||||||
|  |             _emailLogService = emailLogService; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         private bool TienePermiso(string codAcc) => User.IsInRole("SuperAdmin") || User.HasClaim(c => c.Type == "permission" && c.Value == codAcc); | ||||||
|  |  | ||||||
|  |         private int? GetCurrentUserId() | ||||||
|  |         { | ||||||
|  |             if (int.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub"), out int userId)) return userId; | ||||||
|  |             _logger.LogWarning("No se pudo obtener el UserId del token JWT en FacturacionController."); | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         [HttpPut("{idFactura:int}/numero-factura")] | ||||||
|  |         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||||
|  |         public async Task<IActionResult> UpdateNumeroFactura(int idFactura, [FromBody] string numeroFactura) | ||||||
|  |         { | ||||||
|  |             if (!TienePermiso(PermisoGestionarFacturacion)) return Forbid(); | ||||||
|  |  | ||||||
|  |             var userId = GetCurrentUserId(); | ||||||
|  |             if (userId == null) return Unauthorized(); | ||||||
|  |  | ||||||
|  |             var (exito, error) = await _facturacionService.ActualizarNumeroFactura(idFactura, numeroFactura, userId.Value); | ||||||
|  |  | ||||||
|  |             if (!exito) | ||||||
|  |             { | ||||||
|  |                 if (error != null && error.Contains("no existe")) return NotFound(new { message = error }); | ||||||
|  |                 return BadRequest(new { message = error }); | ||||||
|  |             } | ||||||
|  |             return NoContent(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         [HttpPost("{idFactura:int}/enviar-factura-pdf")] | ||||||
|  |         public async Task<IActionResult> EnviarFacturaPdf(int idFactura) | ||||||
|  |         { | ||||||
|  |             if (!TienePermiso(PermisoEnviarEmail)) return Forbid(); | ||||||
|  |             var userId = GetCurrentUserId(); | ||||||
|  |             if (userId == null) return Unauthorized(); | ||||||
|  |             var (exito, error, emailDestino) = await _facturacionService.EnviarFacturaPdfPorEmail(idFactura, userId.Value); | ||||||
|  |  | ||||||
|  |             if (!exito) | ||||||
|  |             { | ||||||
|  |                 return BadRequest(new { message = error }); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             var mensajeExito = $"El email con la factura PDF se ha enviado correctamente a {emailDestino}."; | ||||||
|  |             return Ok(new { message = mensajeExito }); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         [HttpGet("{anio:int}/{mes:int}")] | ||||||
|  |         public async Task<IActionResult> GetFacturas( | ||||||
|  |             int anio, int mes, | ||||||
|  |             [FromQuery] string? nombreSuscriptor, | ||||||
|  |             [FromQuery] string? estadoPago, | ||||||
|  |             [FromQuery] string? estadoFacturacion) | ||||||
|  |         { | ||||||
|  |             if (!TienePermiso(PermisoGestionarFacturacion)) return Forbid(); | ||||||
|  |             if (anio < 2020 || mes < 1 || mes > 12) return BadRequest(new { message = "El período no es válido." }); | ||||||
|  |  | ||||||
|  |             var resumenes = await _facturacionService.ObtenerResumenesDeCuentaPorPeriodo(anio, mes, nombreSuscriptor, estadoPago, estadoFacturacion); | ||||||
|  |             return Ok(resumenes); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         [HttpPost("{anio:int}/{mes:int}")] | ||||||
|  |         public async Task<IActionResult> GenerarFacturacion(int anio, int mes) | ||||||
|  |         { | ||||||
|  |             if (!TienePermiso(PermisoGestionarFacturacion)) return Forbid(); | ||||||
|  |             var userId = GetCurrentUserId(); | ||||||
|  |             if (userId == null) return Unauthorized(); | ||||||
|  |             if (anio < 2020 || mes < 1 || mes > 12) return BadRequest(new { message = "El año y el mes proporcionados no son válidos." }); | ||||||
|  |  | ||||||
|  |             var (exito, mensaje, resultadoEnvio) = await _facturacionService.GenerarFacturacionMensual(anio, mes, userId.Value); | ||||||
|  |  | ||||||
|  |             if (!exito) return StatusCode(StatusCodes.Status500InternalServerError, new { message = mensaje }); | ||||||
|  |  | ||||||
|  |             return Ok(new { message = mensaje, resultadoEnvio }); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         [HttpGet("historial-lotes-envio")] | ||||||
|  |         [ProducesResponseType(typeof(IEnumerable<LoteDeEnvioHistorialDto>), StatusCodes.Status200OK)] | ||||||
|  |         public async Task<IActionResult> GetHistorialLotesEnvio([FromQuery] int? anio, [FromQuery] int? mes) | ||||||
|  |         { | ||||||
|  |             if (!TienePermiso("SU006")) return Forbid(); | ||||||
|  |             var historial = await _facturacionService.ObtenerHistorialLotesEnvio(anio, mes); | ||||||
|  |             return Ok(historial); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Endpoint para el historial de envíos de una factura individual | ||||||
|  |         [HttpGet("{idFactura:int}/historial-envios")] | ||||||
|  |         [ProducesResponseType(typeof(IEnumerable<EmailLogDto>), StatusCodes.Status200OK)] | ||||||
|  |         public async Task<IActionResult> GetHistorialEnvios(int idFactura) | ||||||
|  |         { | ||||||
|  |             if (!TienePermiso(PermisoGestionarFacturacion)) return Forbid(); // Reutilizamos el permiso | ||||||
|  |  | ||||||
|  |             // Construimos la referencia que se guarda en el log | ||||||
|  |             string referencia = $"Factura-{idFactura}"; | ||||||
|  |             var historial = await _emailLogService.ObtenerHistorialPorReferencia(referencia); | ||||||
|  |  | ||||||
|  |             return Ok(historial); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,27 @@ | |||||||
|  | using GestionIntegral.Api.Services.Suscripciones; | ||||||
|  | using Microsoft.AspNetCore.Authorization; | ||||||
|  | using Microsoft.AspNetCore.Mvc; | ||||||
|  |  | ||||||
|  | namespace GestionIntegral.Api.Controllers.Suscripciones | ||||||
|  | { | ||||||
|  |     [Route("api/formaspago")] | ||||||
|  |     [ApiController] | ||||||
|  |     [Authorize] // Solo usuarios logueados pueden ver esto | ||||||
|  |     public class FormasDePagoController : ControllerBase | ||||||
|  |     { | ||||||
|  |         private readonly IFormaPagoService _formaPagoService; | ||||||
|  |  | ||||||
|  |         public FormasDePagoController(IFormaPagoService formaPagoService) | ||||||
|  |         { | ||||||
|  |             _formaPagoService = formaPagoService; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // GET: api/formaspago | ||||||
|  |         [HttpGet] | ||||||
|  |         public async Task<IActionResult> GetAll() | ||||||
|  |         { | ||||||
|  |             var formasDePago = await _formaPagoService.ObtenerTodos(); | ||||||
|  |             return Ok(formasDePago); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,69 @@ | |||||||
|  | using GestionIntegral.Api.Dtos.Suscripciones; | ||||||
|  | using GestionIntegral.Api.Services.Suscripciones; | ||||||
|  | using Microsoft.AspNetCore.Authorization; | ||||||
|  | using Microsoft.AspNetCore.Mvc; | ||||||
|  | using System.Security.Claims; | ||||||
|  |  | ||||||
|  | namespace GestionIntegral.Api.Controllers.Suscripciones | ||||||
|  | { | ||||||
|  |     [Route("api/pagos")] | ||||||
|  |     [ApiController] | ||||||
|  |     [Authorize] | ||||||
|  |     public class PagosController : ControllerBase | ||||||
|  |     { | ||||||
|  |         private readonly IPagoService _pagoService; | ||||||
|  |         private readonly ILogger<PagosController> _logger; | ||||||
|  |  | ||||||
|  |         // Permiso para registrar pagos manuales (a crear en BD) | ||||||
|  |         private const string PermisoRegistrarPago = "SU008"; | ||||||
|  |  | ||||||
|  |         public PagosController(IPagoService pagoService, ILogger<PagosController> logger) | ||||||
|  |         { | ||||||
|  |             _pagoService = pagoService; | ||||||
|  |             _logger = logger; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         private bool TienePermiso(string codAcc) => User.IsInRole("SuperAdmin") || User.HasClaim(c => c.Type == "permission" && c.Value == codAcc); | ||||||
|  |          | ||||||
|  |         private int? GetCurrentUserId() | ||||||
|  |         { | ||||||
|  |             if (int.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub"), out int userId)) return userId; | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // GET: api/facturas/{idFactura}/pagos | ||||||
|  |         [HttpGet("~/api/facturas/{idFactura:int}/pagos")] | ||||||
|  |         [ProducesResponseType(typeof(IEnumerable<PagoDto>), StatusCodes.Status200OK)] | ||||||
|  |         [ProducesResponseType(StatusCodes.Status403Forbidden)] | ||||||
|  |         public async Task<IActionResult> GetPagosPorFactura(int idFactura) | ||||||
|  |         { | ||||||
|  |             // Se podría usar un permiso de "Ver Facturación" | ||||||
|  |             if (!TienePermiso("SU006")) return Forbid(); | ||||||
|  |  | ||||||
|  |             var pagos = await _pagoService.ObtenerPagosPorFacturaId(idFactura); | ||||||
|  |             return Ok(pagos); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // POST: api/pagos | ||||||
|  |         [HttpPost] | ||||||
|  |         [ProducesResponseType(typeof(PagoDto), StatusCodes.Status201Created)] | ||||||
|  |         [ProducesResponseType(StatusCodes.Status400BadRequest)] | ||||||
|  |         [ProducesResponseType(StatusCodes.Status403Forbidden)] | ||||||
|  |         public async Task<IActionResult> RegistrarPago([FromBody] CreatePagoDto createDto) | ||||||
|  |         { | ||||||
|  |             if (!TienePermiso(PermisoRegistrarPago)) return Forbid(); | ||||||
|  |             if (!ModelState.IsValid) return BadRequest(ModelState); | ||||||
|  |  | ||||||
|  |             var userId = GetCurrentUserId(); | ||||||
|  |             if (userId == null) return Unauthorized(); | ||||||
|  |  | ||||||
|  |             var (dto, error) = await _pagoService.RegistrarPagoManual(createDto, userId.Value); | ||||||
|  |  | ||||||
|  |             if (error != null) return BadRequest(new { message = error }); | ||||||
|  |             if (dto == null) return StatusCode(StatusCodes.Status500InternalServerError, "Error al registrar el pago."); | ||||||
|  |  | ||||||
|  |             // No tenemos un "GetById" para pagos, así que devolvemos el objeto con un 201. | ||||||
|  |             return StatusCode(201, dto); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,90 @@ | |||||||
|  | using GestionIntegral.Api.Dtos.Suscripciones; | ||||||
|  | using GestionIntegral.Api.Services.Suscripciones; | ||||||
|  | using Microsoft.AspNetCore.Authorization; | ||||||
|  | using Microsoft.AspNetCore.Mvc; | ||||||
|  | using System.Security.Claims; | ||||||
|  |  | ||||||
|  | namespace GestionIntegral.Api.Controllers.Suscripciones | ||||||
|  | { | ||||||
|  |     [Route("api/promociones")] | ||||||
|  |     [ApiController] | ||||||
|  |     [Authorize] | ||||||
|  |     public class PromocionesController : ControllerBase | ||||||
|  |     { | ||||||
|  |         private readonly IPromocionService _promocionService; | ||||||
|  |         private readonly ILogger<PromocionesController> _logger; | ||||||
|  |  | ||||||
|  |         // Permiso a crear en BD | ||||||
|  |         private const string PermisoGestionarPromociones = "SU010"; | ||||||
|  |  | ||||||
|  |         public PromocionesController(IPromocionService promocionService, ILogger<PromocionesController> logger) | ||||||
|  |         { | ||||||
|  |             _promocionService = promocionService; | ||||||
|  |             _logger = logger; | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         private bool TienePermiso(string codAcc) => User.IsInRole("SuperAdmin") || User.HasClaim(c => c.Type == "permission" && c.Value == codAcc); | ||||||
|  |         private int? GetCurrentUserId() | ||||||
|  |         { | ||||||
|  |             if (int.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub"), out int userId)) return userId; | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // GET: api/promociones | ||||||
|  |         [HttpGet] | ||||||
|  |         public async Task<IActionResult> GetAll([FromQuery] bool soloActivas = true) | ||||||
|  |         { | ||||||
|  |             if (!TienePermiso(PermisoGestionarPromociones)) return Forbid(); | ||||||
|  |             var promociones = await _promocionService.ObtenerTodas(soloActivas); | ||||||
|  |             return Ok(promociones); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // GET: api/promociones/{id} | ||||||
|  |         [HttpGet("{id:int}", Name = "GetPromocionById")] | ||||||
|  |         public async Task<IActionResult> GetById(int id) | ||||||
|  |         { | ||||||
|  |             if (!TienePermiso(PermisoGestionarPromociones)) return Forbid(); | ||||||
|  |             var promocion = await _promocionService.ObtenerPorId(id); | ||||||
|  |             if (promocion == null) return NotFound(); | ||||||
|  |             return Ok(promocion); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // POST: api/promociones | ||||||
|  |         [HttpPost] | ||||||
|  |         public async Task<IActionResult> Create([FromBody] CreatePromocionDto createDto) | ||||||
|  |         { | ||||||
|  |             if (!TienePermiso(PermisoGestionarPromociones)) return Forbid(); | ||||||
|  |             if (!ModelState.IsValid) return BadRequest(ModelState); | ||||||
|  |              | ||||||
|  |             var userId = GetCurrentUserId(); | ||||||
|  |             if (userId == null) return Unauthorized(); | ||||||
|  |  | ||||||
|  |             var (dto, error) = await _promocionService.Crear(createDto, userId.Value); | ||||||
|  |              | ||||||
|  |             if (error != null) return BadRequest(new { message = error }); | ||||||
|  |             if (dto == null) return StatusCode(500, "Error al crear la promoción."); | ||||||
|  |              | ||||||
|  |             return CreatedAtRoute("GetPromocionById", new { id = dto.IdPromocion }, dto); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // PUT: api/promociones/{id} | ||||||
|  |         [HttpPut("{id:int}")] | ||||||
|  |         public async Task<IActionResult> Update(int id, [FromBody] UpdatePromocionDto updateDto) | ||||||
|  |         { | ||||||
|  |             if (!TienePermiso(PermisoGestionarPromociones)) return Forbid(); | ||||||
|  |             if (!ModelState.IsValid) return BadRequest(ModelState); | ||||||
|  |  | ||||||
|  |             var userId = GetCurrentUserId(); | ||||||
|  |             if (userId == null) return Unauthorized(); | ||||||
|  |  | ||||||
|  |             var (exito, error) = await _promocionService.Actualizar(id, updateDto, userId.Value); | ||||||
|  |              | ||||||
|  |             if (!exito) | ||||||
|  |             { | ||||||
|  |                 if (error != null && error.Contains("no encontrada")) return NotFound(new { message = error }); | ||||||
|  |                 return BadRequest(new { message = error }); | ||||||
|  |             } | ||||||
|  |             return NoContent(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,138 @@ | |||||||
|  | // Archivo: GestionIntegral.Api/Controllers/Suscripciones/SuscripcionesController.cs | ||||||
|  |  | ||||||
|  | using GestionIntegral.Api.Dtos.Suscripciones; | ||||||
|  | using GestionIntegral.Api.Services.Suscripciones; | ||||||
|  | using Microsoft.AspNetCore.Authorization; | ||||||
|  | using Microsoft.AspNetCore.Mvc; | ||||||
|  | using System.Security.Claims; | ||||||
|  |  | ||||||
|  | namespace GestionIntegral.Api.Controllers.Suscripciones | ||||||
|  | { | ||||||
|  |     [Route("api/suscripciones")] // Ruta base para acciones sobre una suscripción específica | ||||||
|  |     [ApiController] | ||||||
|  |     [Authorize] | ||||||
|  |     public class SuscripcionesController : ControllerBase | ||||||
|  |     { | ||||||
|  |         private readonly ISuscripcionService _suscripcionService; | ||||||
|  |         private readonly ILogger<SuscripcionesController> _logger; | ||||||
|  |  | ||||||
|  |         // Permisos (nuevos, a crear en la BD) | ||||||
|  |         private const string PermisoGestionarSuscripciones = "SU005"; | ||||||
|  |  | ||||||
|  |         public SuscripcionesController(ISuscripcionService suscripcionService, ILogger<SuscripcionesController> logger) | ||||||
|  |         { | ||||||
|  |             _suscripcionService = suscripcionService; | ||||||
|  |             _logger = logger; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         private bool TienePermiso(string codAcc) => User.IsInRole("SuperAdmin") || User.HasClaim(c => c.Type == "permission" && c.Value == codAcc); | ||||||
|  |  | ||||||
|  |         private int? GetCurrentUserId() | ||||||
|  |         { | ||||||
|  |             if (int.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub"), out int userId)) return userId; | ||||||
|  |             _logger.LogWarning("No se pudo obtener el UserId del token JWT en SuscripcionesController."); | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Endpoint anidado para obtener las suscripciones de un suscriptor | ||||||
|  |         // GET: api/suscriptores/{idSuscriptor}/suscripciones | ||||||
|  |         [HttpGet("~/api/suscriptores/{idSuscriptor:int}/suscripciones")] | ||||||
|  |         public async Task<IActionResult> GetBySuscriptor(int idSuscriptor) | ||||||
|  |         { | ||||||
|  |             // Se podría usar el permiso de ver suscriptores (SU001) o el de gestionar suscripciones (SU005) | ||||||
|  |             if (!TienePermiso("SU001")) return Forbid(); | ||||||
|  |  | ||||||
|  |             var suscripciones = await _suscripcionService.ObtenerPorSuscriptorId(idSuscriptor); | ||||||
|  |             return Ok(suscripciones); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // GET: api/suscripciones/{id} | ||||||
|  |         [HttpGet("{id:int}", Name = "GetSuscripcionById")] | ||||||
|  |         public async Task<IActionResult> GetById(int id) | ||||||
|  |         { | ||||||
|  |             if (!TienePermiso(PermisoGestionarSuscripciones)) return Forbid(); | ||||||
|  |             var suscripcion = await _suscripcionService.ObtenerPorId(id); | ||||||
|  |             if (suscripcion == null) return NotFound(); | ||||||
|  |             return Ok(suscripcion); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // POST: api/suscripciones | ||||||
|  |         [HttpPost] | ||||||
|  |         public async Task<IActionResult> Create([FromBody] CreateSuscripcionDto createDto) | ||||||
|  |         { | ||||||
|  |             if (!TienePermiso(PermisoGestionarSuscripciones)) return Forbid(); | ||||||
|  |             if (!ModelState.IsValid) return BadRequest(ModelState); | ||||||
|  |  | ||||||
|  |             var userId = GetCurrentUserId(); | ||||||
|  |             if (userId == null) return Unauthorized(); | ||||||
|  |  | ||||||
|  |             var (dto, error) = await _suscripcionService.Crear(createDto, userId.Value); | ||||||
|  |  | ||||||
|  |             if (error != null) return BadRequest(new { message = error }); | ||||||
|  |             if (dto == null) return StatusCode(StatusCodes.Status500InternalServerError, "Error al crear la suscripción."); | ||||||
|  |  | ||||||
|  |             return CreatedAtRoute("GetSuscripcionById", new { id = dto.IdSuscripcion }, dto); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // PUT: api/suscripciones/{id} | ||||||
|  |         [HttpPut("{id:int}")] | ||||||
|  |         public async Task<IActionResult> Update(int id, [FromBody] UpdateSuscripcionDto updateDto) | ||||||
|  |         { | ||||||
|  |             if (!TienePermiso(PermisoGestionarSuscripciones)) return Forbid(); | ||||||
|  |             if (!ModelState.IsValid) return BadRequest(ModelState); | ||||||
|  |  | ||||||
|  |             var userId = GetCurrentUserId(); | ||||||
|  |             if (userId == null) return Unauthorized(); | ||||||
|  |  | ||||||
|  |             var (exito, error) = await _suscripcionService.Actualizar(id, updateDto, userId.Value); | ||||||
|  |  | ||||||
|  |             if (!exito) | ||||||
|  |             { | ||||||
|  |                 if (error != null && error.Contains("no encontrada")) return NotFound(new { message = error }); | ||||||
|  |                 return BadRequest(new { message = error }); | ||||||
|  |             } | ||||||
|  |             return NoContent(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // GET: api/suscripciones/{idSuscripcion}/promociones | ||||||
|  |         [HttpGet("{idSuscripcion:int}/promociones")] | ||||||
|  |         public async Task<IActionResult> GetPromocionesAsignadas(int idSuscripcion) | ||||||
|  |         { | ||||||
|  |             if (!TienePermiso(PermisoGestionarSuscripciones)) return Forbid(); | ||||||
|  |             var promos = await _suscripcionService.ObtenerPromocionesAsignadas(idSuscripcion); | ||||||
|  |             return Ok(promos); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // GET: api/suscripciones/{idSuscripcion}/promociones-disponibles | ||||||
|  |         [HttpGet("{idSuscripcion:int}/promociones-disponibles")] | ||||||
|  |         public async Task<IActionResult> GetPromocionesDisponibles(int idSuscripcion) | ||||||
|  |         { | ||||||
|  |             if (!TienePermiso(PermisoGestionarSuscripciones)) return Forbid(); | ||||||
|  |             var promos = await _suscripcionService.ObtenerPromocionesDisponibles(idSuscripcion); | ||||||
|  |             return Ok(promos); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // POST: api/suscripciones/{idSuscripcion}/promociones | ||||||
|  |         [HttpPost("{idSuscripcion:int}/promociones")] | ||||||
|  |         public async Task<IActionResult> AsignarPromocion(int idSuscripcion, [FromBody] AsignarPromocionDto dto) | ||||||
|  |         { | ||||||
|  |             if (!TienePermiso(PermisoGestionarSuscripciones)) return Forbid(); | ||||||
|  |             var userId = GetCurrentUserId(); | ||||||
|  |             if (userId == null) return Unauthorized(); | ||||||
|  |  | ||||||
|  |             var (exito, error) = await _suscripcionService.AsignarPromocion(idSuscripcion, dto, userId.Value); | ||||||
|  |             if (!exito) return BadRequest(new { message = error }); | ||||||
|  |             return Ok(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // DELETE: api/suscripciones/{idSuscripcion}/promociones/{idPromocion} | ||||||
|  |         [HttpDelete("{idSuscripcion:int}/promociones/{idPromocion:int}")] | ||||||
|  |         public async Task<IActionResult> QuitarPromocion(int idSuscripcion, int idPromocion) | ||||||
|  |         { | ||||||
|  |             if (!TienePermiso(PermisoGestionarSuscripciones)) return Forbid(); | ||||||
|  |             var (exito, error) = await _suscripcionService.QuitarPromocion(idSuscripcion, idPromocion); | ||||||
|  |             if (!exito) return BadRequest(new { message = error }); | ||||||
|  |             return NoContent(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,153 @@ | |||||||
|  | using GestionIntegral.Api.Dtos.Suscripciones; | ||||||
|  | using GestionIntegral.Api.Services.Suscripciones; | ||||||
|  | using Microsoft.AspNetCore.Authorization; | ||||||
|  | using Microsoft.AspNetCore.Mvc; | ||||||
|  | using System.Security.Claims; | ||||||
|  |  | ||||||
|  | namespace GestionIntegral.Api.Controllers.Suscripciones | ||||||
|  | { | ||||||
|  |     [Route("api/suscriptores")] | ||||||
|  |     [ApiController] | ||||||
|  |     [Authorize] | ||||||
|  |     public class SuscriptoresController : ControllerBase | ||||||
|  |     { | ||||||
|  |         private readonly ISuscriptorService _suscriptorService; | ||||||
|  |         private readonly ILogger<SuscriptoresController> _logger; | ||||||
|  |  | ||||||
|  |         // Permisos para Suscriptores | ||||||
|  |         private const string PermisoVer = "SU001"; | ||||||
|  |         private const string PermisoCrear = "SU002"; | ||||||
|  |         private const string PermisoModificar = "SU003"; | ||||||
|  |         private const string PermisoActivarDesactivar = "SU004"; | ||||||
|  |  | ||||||
|  |         public SuscriptoresController(ISuscriptorService suscriptorService, ILogger<SuscriptoresController> logger) | ||||||
|  |         { | ||||||
|  |             _suscriptorService = suscriptorService; | ||||||
|  |             _logger = logger; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         private bool TienePermiso(string codAcc) => User.IsInRole("SuperAdmin") || User.HasClaim(c => c.Type == "permission" && c.Value == codAcc); | ||||||
|  |          | ||||||
|  |         private int? GetCurrentUserId() | ||||||
|  |         { | ||||||
|  |             if (int.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub"), out int userId)) return userId; | ||||||
|  |             _logger.LogWarning("No se pudo obtener el UserId del token JWT en SuscriptoresController."); | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // GET: api/suscriptores | ||||||
|  |         [HttpGet] | ||||||
|  |         [ProducesResponseType(typeof(IEnumerable<SuscriptorDto>), StatusCodes.Status200OK)] | ||||||
|  |         [ProducesResponseType(StatusCodes.Status403Forbidden)] | ||||||
|  |         public async Task<IActionResult> GetAll([FromQuery] string? nombre, [FromQuery] string? nroDoc, [FromQuery] bool soloActivos = true) | ||||||
|  |         { | ||||||
|  |             if (!TienePermiso(PermisoVer)) return Forbid(); | ||||||
|  |             var suscriptores = await _suscriptorService.ObtenerTodos(nombre, nroDoc, soloActivos); | ||||||
|  |             return Ok(suscriptores); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // GET: api/suscriptores/{id} | ||||||
|  |         [HttpGet("{id:int}", Name = "GetSuscriptorById")] | ||||||
|  |         [ProducesResponseType(typeof(SuscriptorDto), StatusCodes.Status200OK)] | ||||||
|  |         [ProducesResponseType(StatusCodes.Status403Forbidden)] | ||||||
|  |         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||||
|  |         public async Task<IActionResult> GetById(int id) | ||||||
|  |         { | ||||||
|  |             if (!TienePermiso(PermisoVer)) return Forbid(); | ||||||
|  |             var suscriptor = await _suscriptorService.ObtenerPorId(id); | ||||||
|  |             if (suscriptor == null) return NotFound(); | ||||||
|  |             return Ok(suscriptor); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // POST: api/suscriptores | ||||||
|  |         [HttpPost] | ||||||
|  |         [ProducesResponseType(typeof(SuscriptorDto), StatusCodes.Status201Created)] | ||||||
|  |         [ProducesResponseType(StatusCodes.Status400BadRequest)] | ||||||
|  |         [ProducesResponseType(StatusCodes.Status403Forbidden)] | ||||||
|  |         public async Task<IActionResult> Create([FromBody] CreateSuscriptorDto createDto) | ||||||
|  |         { | ||||||
|  |             if (!TienePermiso(PermisoCrear)) return Forbid(); | ||||||
|  |             if (!ModelState.IsValid) return BadRequest(ModelState); | ||||||
|  |              | ||||||
|  |             var userId = GetCurrentUserId(); | ||||||
|  |             if (userId == null) return Unauthorized(); | ||||||
|  |  | ||||||
|  |             var (dto, error) = await _suscriptorService.Crear(createDto, userId.Value); | ||||||
|  |              | ||||||
|  |             if (error != null) return BadRequest(new { message = error }); | ||||||
|  |             if (dto == null) return StatusCode(StatusCodes.Status500InternalServerError, "Error al crear el suscriptor."); | ||||||
|  |              | ||||||
|  |             return CreatedAtRoute("GetSuscriptorById", new { id = dto.IdSuscriptor }, dto); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // PUT: api/suscriptores/{id} | ||||||
|  |         [HttpPut("{id:int}")] | ||||||
|  |         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||||
|  |         [ProducesResponseType(StatusCodes.Status400BadRequest)] | ||||||
|  |         [ProducesResponseType(StatusCodes.Status403Forbidden)] | ||||||
|  |         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||||
|  |         public async Task<IActionResult> Update(int id, [FromBody] UpdateSuscriptorDto updateDto) | ||||||
|  |         { | ||||||
|  |             if (!TienePermiso(PermisoModificar)) return Forbid(); | ||||||
|  |             if (!ModelState.IsValid) return BadRequest(ModelState); | ||||||
|  |  | ||||||
|  |             var userId = GetCurrentUserId(); | ||||||
|  |             if (userId == null) return Unauthorized(); | ||||||
|  |  | ||||||
|  |             var (exito, error) = await _suscriptorService.Actualizar(id, updateDto, userId.Value); | ||||||
|  |              | ||||||
|  |             if (!exito) | ||||||
|  |             { | ||||||
|  |                 if (error != null && error.Contains("no encontrado")) return NotFound(new { message = error }); | ||||||
|  |                 return BadRequest(new { message = error }); | ||||||
|  |             } | ||||||
|  |             return NoContent(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // DELETE: api/suscriptores/{id} (Desactivar) | ||||||
|  |         [HttpDelete("{id:int}")] | ||||||
|  |         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||||
|  |         [ProducesResponseType(StatusCodes.Status400BadRequest)] | ||||||
|  |         [ProducesResponseType(StatusCodes.Status403Forbidden)] | ||||||
|  |         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||||
|  |         public async Task<IActionResult> Deactivate(int id) | ||||||
|  |         { | ||||||
|  |             if (!TienePermiso(PermisoActivarDesactivar)) return Forbid(); | ||||||
|  |              | ||||||
|  |             var userId = GetCurrentUserId(); | ||||||
|  |             if (userId == null) return Unauthorized(); | ||||||
|  |  | ||||||
|  |             var (exito, error) = await _suscriptorService.Desactivar(id, userId.Value); | ||||||
|  |              | ||||||
|  |             if (!exito) | ||||||
|  |             { | ||||||
|  |                 if (error != null && error.Contains("no encontrado")) return NotFound(new { message = error }); | ||||||
|  |                 return BadRequest(new { message = error }); | ||||||
|  |             } | ||||||
|  |             return NoContent(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // POST: api/suscriptores/{id}/activar | ||||||
|  |         [HttpPost("{id:int}/activar")] | ||||||
|  |         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||||
|  |         [ProducesResponseType(StatusCodes.Status400BadRequest)] | ||||||
|  |         [ProducesResponseType(StatusCodes.Status403Forbidden)] | ||||||
|  |         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||||
|  |         public async Task<IActionResult> Activate(int id) | ||||||
|  |         { | ||||||
|  |             if (!TienePermiso(PermisoActivarDesactivar)) return Forbid(); | ||||||
|  |              | ||||||
|  |             var userId = GetCurrentUserId(); | ||||||
|  |             if (userId == null) return Unauthorized(); | ||||||
|  |  | ||||||
|  |             var (exito, error) = await _suscriptorService.Activar(id, userId.Value); | ||||||
|  |              | ||||||
|  |             if (!exito) | ||||||
|  |             { | ||||||
|  |                 if (error != null && error.Contains("no encontrado")) return NotFound(new { message = error }); | ||||||
|  |                 return BadRequest(new { message = error }); | ||||||
|  |             } | ||||||
|  |             return NoContent(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,65 @@ | |||||||
|  | using Dapper; | ||||||
|  | using GestionIntegral.Api.Models.Comunicaciones; | ||||||
|  |  | ||||||
|  | namespace GestionIntegral.Api.Data.Repositories.Comunicaciones | ||||||
|  | { | ||||||
|  |   public class EmailLogRepository : IEmailLogRepository | ||||||
|  |   { | ||||||
|  |     private readonly DbConnectionFactory _connectionFactory; | ||||||
|  |     private readonly ILogger<EmailLogRepository> _logger; | ||||||
|  |     public EmailLogRepository(DbConnectionFactory connectionFactory, ILogger<EmailLogRepository> logger) | ||||||
|  |     { | ||||||
|  |       _connectionFactory = connectionFactory; | ||||||
|  |       _logger = logger; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task CreateAsync(EmailLog log) | ||||||
|  |     { | ||||||
|  |       const string sql = @" | ||||||
|  |         INSERT INTO dbo.com_EmailLogs  | ||||||
|  |             (FechaEnvio, DestinatarioEmail, Asunto, Estado, Error, IdUsuarioDisparo, Origen, ReferenciaId, IdLoteDeEnvio)  | ||||||
|  |         VALUES  | ||||||
|  |             (@FechaEnvio, @DestinatarioEmail, @Asunto, @Estado, @Error, @IdUsuarioDisparo, @Origen, @ReferenciaId, @IdLoteDeEnvio);"; | ||||||
|  |  | ||||||
|  |       using var connection = _connectionFactory.CreateConnection(); | ||||||
|  |       await connection.ExecuteAsync(sql, log); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task<IEnumerable<EmailLog>> GetByReferenceAsync(string referenciaId) | ||||||
|  |     { | ||||||
|  |       const string sql = @" | ||||||
|  |             SELECT * FROM dbo.com_EmailLogs | ||||||
|  |             WHERE ReferenciaId = @ReferenciaId | ||||||
|  |             ORDER BY FechaEnvio DESC;"; | ||||||
|  |       try | ||||||
|  |       { | ||||||
|  |         using var connection = _connectionFactory.CreateConnection(); | ||||||
|  |         return await connection.QueryAsync<EmailLog>(sql, new { ReferenciaId = referenciaId }); | ||||||
|  |       } | ||||||
|  |       catch (System.Exception ex) | ||||||
|  |       { | ||||||
|  |         _logger.LogError(ex, "Error al obtener logs de email por ReferenciaId: {ReferenciaId}", referenciaId); | ||||||
|  |         return Enumerable.Empty<EmailLog>(); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task<IEnumerable<EmailLog>> GetByLoteIdAsync(int idLoteDeEnvio) | ||||||
|  |     { | ||||||
|  |       // Ordenamos por Estado descendente para que los 'Fallidos' aparezcan primero | ||||||
|  |       const string sql = @" | ||||||
|  |             SELECT * FROM dbo.com_EmailLogs | ||||||
|  |             WHERE IdLoteDeEnvio = @IdLoteDeEnvio | ||||||
|  |             ORDER BY Estado DESC, FechaEnvio DESC;"; | ||||||
|  |       try | ||||||
|  |       { | ||||||
|  |         using var connection = _connectionFactory.CreateConnection(); | ||||||
|  |         return await connection.QueryAsync<EmailLog>(sql, new { IdLoteDeEnvio = idLoteDeEnvio }); | ||||||
|  |       } | ||||||
|  |       catch (Exception ex) | ||||||
|  |       { | ||||||
|  |         _logger.LogError(ex, "Error al obtener logs de email por IdLoteDeEnvio: {IdLoteDeEnvio}", idLoteDeEnvio); | ||||||
|  |         return Enumerable.Empty<EmailLog>(); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -0,0 +1,26 @@ | |||||||
|  | using GestionIntegral.Api.Models.Comunicaciones; | ||||||
|  |  | ||||||
|  | namespace GestionIntegral.Api.Data.Repositories.Comunicaciones | ||||||
|  | { | ||||||
|  |     public interface IEmailLogRepository | ||||||
|  |     { | ||||||
|  |         /// <summary> | ||||||
|  |         /// Guarda un nuevo registro de log de email en la base de datos. | ||||||
|  |         /// </summary> | ||||||
|  |         Task CreateAsync(EmailLog log); | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Obtiene todos los registros de log de email que coinciden con una referencia específica. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="referenciaId">El identificador de la entidad (ej. "Factura-59").</param> | ||||||
|  |         /// <returns>Una colección de registros de log de email.</returns> | ||||||
|  |         Task<IEnumerable<EmailLog>> GetByReferenceAsync(string referenciaId); | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Obtiene todos los registros de log de email que pertenecen a un lote de envío masivo. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="idLoteDeEnvio">El ID del lote de envío.</param> | ||||||
|  |         /// <returns>Una colección de registros de log de email.</returns> | ||||||
|  |         Task<IEnumerable<EmailLog>> GetByLoteIdAsync(int idLoteDeEnvio); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,12 @@ | |||||||
|  | using GestionIntegral.Api.Models.Comunicaciones; | ||||||
|  |  | ||||||
|  | namespace GestionIntegral.Api.Data.Repositories.Comunicaciones | ||||||
|  | { | ||||||
|  |     public interface ILoteDeEnvioRepository | ||||||
|  |     { | ||||||
|  |         Task<LoteDeEnvio> CreateAsync(LoteDeEnvio lote); | ||||||
|  |         Task<bool> UpdateAsync(LoteDeEnvio lote); | ||||||
|  |         Task<IEnumerable<LoteDeEnvio>> GetAllAsync(int? anio, int? mes); | ||||||
|  |         Task<LoteDeEnvio?> GetByIdAsync(int id); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,69 @@ | |||||||
|  | using System.Text; | ||||||
|  | using Dapper; | ||||||
|  | using GestionIntegral.Api.Models.Comunicaciones; | ||||||
|  |  | ||||||
|  | namespace GestionIntegral.Api.Data.Repositories.Comunicaciones | ||||||
|  | { | ||||||
|  |     public class LoteDeEnvioRepository : ILoteDeEnvioRepository | ||||||
|  |     { | ||||||
|  |         private readonly DbConnectionFactory _connectionFactory; | ||||||
|  |         public LoteDeEnvioRepository(DbConnectionFactory connectionFactory) | ||||||
|  |         { | ||||||
|  |             _connectionFactory = connectionFactory; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public async Task<LoteDeEnvio> CreateAsync(LoteDeEnvio lote) | ||||||
|  |         { | ||||||
|  |             const string sql = @" | ||||||
|  |                 INSERT INTO dbo.com_LotesDeEnvio (FechaInicio, Periodo, Origen, Estado, IdUsuarioDisparo) | ||||||
|  |                 OUTPUT INSERTED.* | ||||||
|  |                 VALUES (@FechaInicio, @Periodo, @Origen, @Estado, @IdUsuarioDisparo);"; | ||||||
|  |             using var connection = _connectionFactory.CreateConnection(); | ||||||
|  |             return await connection.QuerySingleAsync<LoteDeEnvio>(sql, lote); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public async Task<bool> UpdateAsync(LoteDeEnvio lote) | ||||||
|  |         { | ||||||
|  |             const string sql = @" | ||||||
|  |                 UPDATE dbo.com_LotesDeEnvio SET | ||||||
|  |                     FechaFin = @FechaFin, | ||||||
|  |                     Estado = @Estado, | ||||||
|  |                     TotalCorreos = @TotalCorreos, | ||||||
|  |                     TotalEnviados = @TotalEnviados, | ||||||
|  |                     TotalFallidos = @TotalFallidos | ||||||
|  |                 WHERE IdLoteDeEnvio = @IdLoteDeEnvio;"; | ||||||
|  |             using var connection = _connectionFactory.CreateConnection(); | ||||||
|  |             var rows = await connection.ExecuteAsync(sql, lote); | ||||||
|  |             return rows == 1; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public async Task<IEnumerable<LoteDeEnvio>> GetAllAsync(int? anio, int? mes) | ||||||
|  |         { | ||||||
|  |             var sqlBuilder = new StringBuilder("SELECT * FROM dbo.com_LotesDeEnvio WHERE 1=1"); | ||||||
|  |             var parameters = new DynamicParameters(); | ||||||
|  |  | ||||||
|  |             if (anio.HasValue) | ||||||
|  |             { | ||||||
|  |                 sqlBuilder.Append(" AND YEAR(FechaInicio) = @Anio"); | ||||||
|  |                 parameters.Add("Anio", anio.Value); | ||||||
|  |             } | ||||||
|  |             if (mes.HasValue) | ||||||
|  |             { | ||||||
|  |                 sqlBuilder.Append(" AND MONTH(FechaInicio) = @Mes"); | ||||||
|  |                 parameters.Add("Mes", mes.Value); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             sqlBuilder.Append(" ORDER BY FechaInicio DESC;"); | ||||||
|  |  | ||||||
|  |             using var connection = _connectionFactory.CreateConnection(); | ||||||
|  |             return await connection.QueryAsync<LoteDeEnvio>(sqlBuilder.ToString(), parameters); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public async Task<LoteDeEnvio?> GetByIdAsync(int id) | ||||||
|  |         { | ||||||
|  |             const string sql = "SELECT * FROM dbo.com_LotesDeEnvio WHERE IdLoteDeEnvio = @Id;"; | ||||||
|  |             using var connection = _connectionFactory.CreateConnection(); | ||||||
|  |             return await connection.QuerySingleOrDefaultAsync<LoteDeEnvio>(sql, new { Id = id }); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,4 +1,5 @@ | |||||||
| using Dapper; | using Dapper; | ||||||
|  | using GestionIntegral.Api.Dtos.Distribucion; | ||||||
| using GestionIntegral.Api.Models.Distribucion; | using GestionIntegral.Api.Models.Distribucion; | ||||||
| using Microsoft.Extensions.Logging; | using Microsoft.Extensions.Logging; | ||||||
| using System; // Para Exception | using System; // Para Exception | ||||||
| @@ -25,7 +26,7 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion | |||||||
|     string? nomApeFilter, |     string? nomApeFilter, | ||||||
|     int? legajoFilter, |     int? legajoFilter, | ||||||
|     bool? esAccionista, |     bool? esAccionista, | ||||||
|     bool? soloActivos) // <<-- Parámetro aquí |     bool? soloActivos) | ||||||
|         { |         { | ||||||
|             using var connection = _connectionFactory.CreateConnection(); |             using var connection = _connectionFactory.CreateConnection(); | ||||||
|             var sqlBuilder = new System.Text.StringBuilder(@" |             var sqlBuilder = new System.Text.StringBuilder(@" | ||||||
| @@ -73,6 +74,37 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion | |||||||
|             return result; |             return result; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         public async Task<IEnumerable<CanillaDropdownDto>> GetAllDropdownAsync(bool? esAccionista, bool? soloActivos) | ||||||
|  |         { | ||||||
|  |             using var connection = _connectionFactory.CreateConnection(); | ||||||
|  |             var sqlBuilder = new System.Text.StringBuilder(@" | ||||||
|  |             SELECT c.Id_Canilla AS IdCanilla, c.Legajo, c.NomApe | ||||||
|  |             FROM dbo.dist_dtCanillas c | ||||||
|  |             WHERE 1=1 "); | ||||||
|  |  | ||||||
|  |             var parameters = new DynamicParameters(); | ||||||
|  |              | ||||||
|  |             if (soloActivos.HasValue) | ||||||
|  |             { | ||||||
|  |                 sqlBuilder.Append(" AND c.Baja = @BajaStatus "); | ||||||
|  |                 parameters.Add("BajaStatus", !soloActivos.Value); // Si soloActivos es true, Baja debe ser false | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             if (esAccionista.HasValue) | ||||||
|  |             { | ||||||
|  |                 sqlBuilder.Append(" AND c.Accionista = @EsAccionista "); | ||||||
|  |                 parameters.Add("EsAccionista", esAccionista.Value); // true para accionistas, false para no accionistas (canillitas) | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             sqlBuilder.Append(" ORDER BY c.NomApe;"); | ||||||
|  |  | ||||||
|  |             var result = await connection.QueryAsync<CanillaDropdownDto>( | ||||||
|  |                 sqlBuilder.ToString(), | ||||||
|  |                 parameters | ||||||
|  |             ); | ||||||
|  |             return result; | ||||||
|  |         } | ||||||
|  |  | ||||||
|         public async Task<(Canilla? Canilla, string? NombreZona, string? NombreEmpresa)> GetByIdAsync(int id) |         public async Task<(Canilla? Canilla, string? NombreZona, string? NombreEmpresa)> GetByIdAsync(int id) | ||||||
|         { |         { | ||||||
|             const string sql = @" |             const string sql = @" | ||||||
|   | |||||||
| @@ -2,12 +2,14 @@ using GestionIntegral.Api.Models.Distribucion; | |||||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||||
| using System.Data; | using System.Data; | ||||||
|  | using GestionIntegral.Api.Dtos.Distribucion; | ||||||
|  |  | ||||||
| namespace GestionIntegral.Api.Data.Repositories.Distribucion | namespace GestionIntegral.Api.Data.Repositories.Distribucion | ||||||
| { | { | ||||||
|     public interface ICanillaRepository |     public interface ICanillaRepository | ||||||
|     { |     { | ||||||
|         Task<IEnumerable<(Canilla Canilla, string? NombreZona, string? NombreEmpresa)>> GetAllAsync(string? nomApeFilter, int? legajoFilter, bool? soloActivos, bool? esAccionista); |         Task<IEnumerable<(Canilla Canilla, string? NombreZona, string? NombreEmpresa)>> GetAllAsync(string? nomApeFilter, int? legajoFilter, bool? soloActivos, bool? esAccionista); | ||||||
|  |         Task<IEnumerable<CanillaDropdownDto>> GetAllDropdownAsync(bool? esAccionista, bool? soloActivos); | ||||||
|         Task<(Canilla? Canilla, string? NombreZona, string? NombreEmpresa)> GetByIdAsync(int id); |         Task<(Canilla? Canilla, string? NombreZona, string? NombreEmpresa)> GetByIdAsync(int id); | ||||||
|         Task<Canilla?> GetByIdSimpleAsync(int id); // Para obtener solo la entidad Canilla |         Task<Canilla?> GetByIdSimpleAsync(int id); // Para obtener solo la entidad Canilla | ||||||
|         Task<Canilla?> CreateAsync(Canilla nuevoCanilla, int idUsuario, IDbTransaction transaction); |         Task<Canilla?> CreateAsync(Canilla nuevoCanilla, int idUsuario, IDbTransaction transaction); | ||||||
|   | |||||||
| @@ -2,12 +2,14 @@ using GestionIntegral.Api.Models.Distribucion; | |||||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||||
| using System.Data; | using System.Data; | ||||||
|  | using GestionIntegral.Api.Dtos.Distribucion; | ||||||
|  |  | ||||||
| namespace GestionIntegral.Api.Data.Repositories.Distribucion | namespace GestionIntegral.Api.Data.Repositories.Distribucion | ||||||
| { | { | ||||||
|     public interface IOtroDestinoRepository |     public interface IOtroDestinoRepository | ||||||
|     { |     { | ||||||
|         Task<IEnumerable<OtroDestino>> GetAllAsync(string? nombreFilter); |         Task<IEnumerable<OtroDestino>> GetAllAsync(string? nombreFilter); | ||||||
|  |         Task<IEnumerable<OtroDestinoDropdownDto>> GetAllDropdownAsync(); | ||||||
|         Task<OtroDestino?> GetByIdAsync(int id); |         Task<OtroDestino?> GetByIdAsync(int id); | ||||||
|         Task<OtroDestino?> CreateAsync(OtroDestino nuevoDestino, int idUsuario, IDbTransaction transaction); |         Task<OtroDestino?> CreateAsync(OtroDestino nuevoDestino, int idUsuario, IDbTransaction transaction); | ||||||
|         Task<bool> UpdateAsync(OtroDestino destinoAActualizar, int idUsuario, IDbTransaction transaction); |         Task<bool> UpdateAsync(OtroDestino destinoAActualizar, int idUsuario, IDbTransaction transaction); | ||||||
|   | |||||||
| @@ -9,10 +9,11 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion | |||||||
|     { |     { | ||||||
|         Task<IEnumerable<PubliSeccion>> GetByPublicacionIdAsync(int idPublicacion, bool? soloActivas = null); |         Task<IEnumerable<PubliSeccion>> GetByPublicacionIdAsync(int idPublicacion, bool? soloActivas = null); | ||||||
|         Task<PubliSeccion?> GetByIdAsync(int idSeccion); |         Task<PubliSeccion?> GetByIdAsync(int idSeccion); | ||||||
|  |         Task<IEnumerable<PubliSeccion>> GetByIdsAndPublicacionAsync(IEnumerable<int> idsSeccion, int idPublicacion, bool? soloActivas = null); | ||||||
|         Task<PubliSeccion?> CreateAsync(PubliSeccion nuevaSeccion, int idUsuario, IDbTransaction transaction); |         Task<PubliSeccion?> CreateAsync(PubliSeccion nuevaSeccion, int idUsuario, IDbTransaction transaction); | ||||||
|         Task<bool> UpdateAsync(PubliSeccion seccionAActualizar, int idUsuario, IDbTransaction transaction); |         Task<bool> UpdateAsync(PubliSeccion seccionAActualizar, int idUsuario, IDbTransaction transaction); | ||||||
|         Task<bool> DeleteAsync(int idSeccion, int idUsuario, IDbTransaction transaction); |         Task<bool> DeleteAsync(int idSeccion, int idUsuario, IDbTransaction transaction); | ||||||
|         Task<bool> DeleteByPublicacionIdAsync(int idPublicacion, int idUsuarioAuditoria, IDbTransaction transaction); // Ya existe |         Task<bool> DeleteByPublicacionIdAsync(int idPublicacion, int idUsuarioAuditoria, IDbTransaction transaction); | ||||||
|         Task<bool> ExistsByNameInPublicacionAsync(string nombre, int idPublicacion, int? excludeIdSeccion = null); |         Task<bool> ExistsByNameInPublicacionAsync(string nombre, int idPublicacion, int? excludeIdSeccion = null); | ||||||
|         Task<bool> IsInUseAsync(int idSeccion); // Verificar en bob_RegPublicaciones, bob_StockBobinas |         Task<bool> IsInUseAsync(int idSeccion); // Verificar en bob_RegPublicaciones, bob_StockBobinas | ||||||
|         Task<IEnumerable<(PubliSeccionHistorico Historial, string NombreUsuarioModifico)>> GetHistorialAsync( |         Task<IEnumerable<(PubliSeccionHistorico Historial, string NombreUsuarioModifico)>> GetHistorialAsync( | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| using Dapper; | using Dapper; | ||||||
|  | using GestionIntegral.Api.Dtos.Distribucion; | ||||||
| using GestionIntegral.Api.Models.Distribucion; | using GestionIntegral.Api.Models.Distribucion; | ||||||
| using Microsoft.Extensions.Logging; | using Microsoft.Extensions.Logging; | ||||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||||
| @@ -44,6 +45,21 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion | |||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         public async Task<IEnumerable<OtroDestinoDropdownDto>> GetAllDropdownAsync() | ||||||
|  |         { | ||||||
|  |             const string sql = "SELECT Id_Destino AS IdDestino, Nombre FROM dbo.dist_dtOtrosDestinos ORDER BY Nombre;"; | ||||||
|  |             try | ||||||
|  |             { | ||||||
|  |                 using var connection = _connectionFactory.CreateConnection(); | ||||||
|  |                 return await connection.QueryAsync<OtroDestinoDropdownDto>(sql); | ||||||
|  |             } | ||||||
|  |             catch (Exception ex) | ||||||
|  |             { | ||||||
|  |                 _logger.LogError(ex, "Error al obtener Otros Destinos para dropdown."); | ||||||
|  |                 return Enumerable.Empty<OtroDestinoDropdownDto>(); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|         public async Task<OtroDestino?> GetByIdAsync(int id) |         public async Task<OtroDestino?> GetByIdAsync(int id) | ||||||
|         { |         { | ||||||
|             const string sql = "SELECT Id_Destino AS IdDestino, Nombre, Obs FROM dbo.dist_dtOtrosDestinos WHERE Id_Destino = @Id"; |             const string sql = "SELECT Id_Destino AS IdDestino, Nombre, Obs FROM dbo.dist_dtOtrosDestinos WHERE Id_Destino = @Id"; | ||||||
|   | |||||||
| @@ -169,6 +169,32 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion | |||||||
|             return rowsAffected == 1; |             return rowsAffected == 1; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         public async Task<IEnumerable<PubliSeccion>> GetByIdsAndPublicacionAsync(IEnumerable<int> idsSeccion, int idPublicacion, bool? soloActivas = null) | ||||||
|  |         { | ||||||
|  |             if (idsSeccion == null || !idsSeccion.Any()) | ||||||
|  |             { | ||||||
|  |                 return Enumerable.Empty<PubliSeccion>(); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             var sqlBuilder = new StringBuilder(@" | ||||||
|  |                 SELECT Id_Seccion AS IdSeccion, Id_Publicacion AS IdPublicacion, Nombre, Estado  | ||||||
|  |                 FROM dbo.dist_dtPubliSecciones  | ||||||
|  |                 WHERE Id_Publicacion = @IdPublicacionParam AND Id_Seccion IN @IdsSeccionParam"); | ||||||
|  |  | ||||||
|  |             var parameters = new DynamicParameters(); | ||||||
|  |             parameters.Add("IdPublicacionParam", idPublicacion); | ||||||
|  |             parameters.Add("IdsSeccionParam", idsSeccion); | ||||||
|  |  | ||||||
|  |             if (soloActivas.HasValue) | ||||||
|  |             { | ||||||
|  |                 sqlBuilder.Append(" AND Estado = @EstadoParam"); | ||||||
|  |                 parameters.Add("EstadoParam", soloActivas.Value); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             using var connection = _cf.CreateConnection(); | ||||||
|  |             return await connection.QueryAsync<PubliSeccion>(sqlBuilder.ToString(), parameters); | ||||||
|  |         } | ||||||
|  |  | ||||||
|         public async Task<bool> DeleteAsync(int idSeccion, int idUsuario, IDbTransaction transaction) |         public async Task<bool> DeleteAsync(int idSeccion, int idUsuario, IDbTransaction transaction) | ||||||
|         { |         { | ||||||
|             var actual = await transaction.Connection!.QuerySingleOrDefaultAsync<PubliSeccion>( |             var actual = await transaction.Connection!.QuerySingleOrDefaultAsync<PubliSeccion>( | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| using Dapper; | using Dapper; | ||||||
| using GestionIntegral.Api.Data.Repositories.Impresion; | using GestionIntegral.Api.Data.Repositories.Impresion; | ||||||
|  | using GestionIntegral.Api.Dtos.Impresion; | ||||||
| using GestionIntegral.Api.Models.Impresion; | using GestionIntegral.Api.Models.Impresion; | ||||||
| using Microsoft.Extensions.Logging; | using Microsoft.Extensions.Logging; | ||||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||||
| @@ -45,6 +46,25 @@ namespace GestionIntegral.Api.Data.Repositories.Impresion | |||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         public async Task<IEnumerable<EstadoBobinaDropdownDto>> GetAllDropdownAsync() | ||||||
|  |         { | ||||||
|  |             var sqlBuilder = new StringBuilder("SELECT Id_EstadoBobina AS IdEstadoBobina, Denominacion FROM dbo.bob_dtEstadosBobinas WHERE 1=1"); | ||||||
|  |             var parameters = new DynamicParameters(); | ||||||
|  |  | ||||||
|  |             sqlBuilder.Append(" ORDER BY Denominacion;"); | ||||||
|  |  | ||||||
|  |             try | ||||||
|  |             { | ||||||
|  |                 using var connection = _connectionFactory.CreateConnection(); | ||||||
|  |                 return await connection.QueryAsync<EstadoBobinaDropdownDto>(sqlBuilder.ToString(), parameters); | ||||||
|  |             } | ||||||
|  |             catch (Exception ex) | ||||||
|  |             { | ||||||
|  |                 _logger.LogError(ex, "Error al obtener todos los Estados de Bobina."); | ||||||
|  |                 return Enumerable.Empty<EstadoBobinaDropdownDto>(); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|         public async Task<EstadoBobina?> GetByIdAsync(int id) |         public async Task<EstadoBobina?> GetByIdAsync(int id) | ||||||
|         { |         { | ||||||
|             const string sql = "SELECT Id_EstadoBobina AS IdEstadoBobina, Denominacion, Obs FROM dbo.bob_dtEstadosBobinas WHERE Id_EstadoBobina = @Id"; |             const string sql = "SELECT Id_EstadoBobina AS IdEstadoBobina, Denominacion, Obs FROM dbo.bob_dtEstadosBobinas WHERE Id_EstadoBobina = @Id"; | ||||||
|   | |||||||
| @@ -2,12 +2,14 @@ using GestionIntegral.Api.Models.Impresion; | |||||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||||
| using System.Data; | using System.Data; | ||||||
|  | using GestionIntegral.Api.Dtos.Impresion; | ||||||
|  |  | ||||||
| namespace GestionIntegral.Api.Data.Repositories.Impresion | namespace GestionIntegral.Api.Data.Repositories.Impresion | ||||||
| { | { | ||||||
|     public interface IEstadoBobinaRepository |     public interface IEstadoBobinaRepository | ||||||
|     { |     { | ||||||
|         Task<IEnumerable<EstadoBobina>> GetAllAsync(string? denominacionFilter); |         Task<IEnumerable<EstadoBobina>> GetAllAsync(string? denominacionFilter); | ||||||
|  |         Task<IEnumerable<EstadoBobinaDropdownDto>> GetAllDropdownAsync(); | ||||||
|         Task<EstadoBobina?> GetByIdAsync(int id); |         Task<EstadoBobina?> GetByIdAsync(int id); | ||||||
|         Task<EstadoBobina?> CreateAsync(EstadoBobina nuevoEstadoBobina, int idUsuario, IDbTransaction transaction); |         Task<EstadoBobina?> CreateAsync(EstadoBobina nuevoEstadoBobina, int idUsuario, IDbTransaction transaction); | ||||||
|         Task<bool> UpdateAsync(EstadoBobina estadoBobinaAActualizar, int idUsuario, IDbTransaction transaction); |         Task<bool> UpdateAsync(EstadoBobina estadoBobinaAActualizar, int idUsuario, IDbTransaction transaction); | ||||||
|   | |||||||
| @@ -1,3 +1,5 @@ | |||||||
|  | // --- FICHERO MODIFICADO: IRegTiradaRepository.cs --- | ||||||
|  |  | ||||||
| using GestionIntegral.Api.Models.Impresion; | using GestionIntegral.Api.Models.Impresion; | ||||||
| using System; | using System; | ||||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||||
| @@ -6,11 +8,12 @@ using System.Threading.Tasks; | |||||||
|  |  | ||||||
| namespace GestionIntegral.Api.Data.Repositories.Impresion | namespace GestionIntegral.Api.Data.Repositories.Impresion | ||||||
| { | { | ||||||
|     public interface IRegTiradaRepository // Para bob_RegTiradas |     public interface IRegTiradaRepository | ||||||
|     { |     { | ||||||
|         Task<RegTirada?> GetByIdAsync(int idRegistro); |         Task<RegTirada?> GetByIdAsync(int idRegistro); | ||||||
|         Task<IEnumerable<RegTirada>> GetByCriteriaAsync(DateTime? fecha, int? idPublicacion, int? idPlanta); |         Task<IEnumerable<RegTirada>> GetByCriteriaAsync(DateTime? fecha, int? idPublicacion, int? idPlanta); | ||||||
|         Task<RegTirada?> CreateAsync(RegTirada nuevaTirada, int idUsuario, IDbTransaction transaction); |         Task<RegTirada?> CreateAsync(RegTirada nuevaTirada, int idUsuario, IDbTransaction transaction); | ||||||
|  |         Task<bool> UpdateAsync(RegTirada tiradaAActualizar, int idUsuario, IDbTransaction transaction); | ||||||
|         Task<bool> DeleteAsync(int idRegistro, int idUsuario, IDbTransaction transaction); // Si se borra el registro principal |         Task<bool> DeleteAsync(int idRegistro, int idUsuario, IDbTransaction transaction); // Si se borra el registro principal | ||||||
|         Task<bool> DeleteByFechaPublicacionPlantaAsync(DateTime fecha, int idPublicacion, int idPlanta, int idUsuario, IDbTransaction transaction); |         Task<bool> DeleteByFechaPublicacionPlantaAsync(DateTime fecha, int idPublicacion, int idPlanta, int idUsuario, IDbTransaction transaction); | ||||||
|         Task<RegTirada?> GetByFechaPublicacionPlantaAsync(DateTime fecha, int idPublicacion, int idPlanta, IDbTransaction? transaction = null); |         Task<RegTirada?> GetByFechaPublicacionPlantaAsync(DateTime fecha, int idPublicacion, int idPlanta, IDbTransaction? transaction = null); | ||||||
| @@ -27,9 +30,11 @@ namespace GestionIntegral.Api.Data.Repositories.Impresion | |||||||
|  |  | ||||||
|     public interface IRegPublicacionSeccionRepository // Para bob_RegPublicaciones |     public interface IRegPublicacionSeccionRepository // Para bob_RegPublicaciones | ||||||
|     { |     { | ||||||
|  |         Task<RegPublicacionSeccion?> GetByIdAsync(int idTirada); | ||||||
|         Task<IEnumerable<RegPublicacionSeccion>> GetByFechaPublicacionPlantaAsync(DateTime fecha, int idPublicacion, int idPlanta); |         Task<IEnumerable<RegPublicacionSeccion>> GetByFechaPublicacionPlantaAsync(DateTime fecha, int idPublicacion, int idPlanta); | ||||||
|         Task<RegPublicacionSeccion?> CreateAsync(RegPublicacionSeccion nuevaSeccionTirada, int idUsuario, IDbTransaction transaction); |         Task<RegPublicacionSeccion?> CreateAsync(RegPublicacionSeccion nuevaSeccionTirada, int idUsuario, IDbTransaction transaction); | ||||||
|  |         Task<bool> UpdateAsync(RegPublicacionSeccion seccionAActualizar, int idUsuario, IDbTransaction transaction); | ||||||
|  |         Task<bool> DeleteByIdAsync(int idTirada, int idUsuario, IDbTransaction transaction); | ||||||
|         Task<bool> DeleteByFechaPublicacionPlantaAsync(DateTime fecha, int idPublicacion, int idPlanta, int idUsuario, IDbTransaction transaction); |         Task<bool> DeleteByFechaPublicacionPlantaAsync(DateTime fecha, int idPublicacion, int idPlanta, int idUsuario, IDbTransaction transaction); | ||||||
|         // Podría tener un DeleteByIdAsync si se permite borrar secciones individuales de una tirada |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -8,6 +8,7 @@ namespace GestionIntegral.Api.Data.Repositories.Impresion | |||||||
|     public interface ITipoBobinaRepository |     public interface ITipoBobinaRepository | ||||||
|     { |     { | ||||||
|         Task<IEnumerable<TipoBobina>> GetAllAsync(string? denominacionFilter); |         Task<IEnumerable<TipoBobina>> GetAllAsync(string? denominacionFilter); | ||||||
|  |         Task<IEnumerable<TipoBobina>> GetAllDropdownAsync(); | ||||||
|         Task<TipoBobina?> GetByIdAsync(int id); |         Task<TipoBobina?> GetByIdAsync(int id); | ||||||
|         Task<TipoBobina?> CreateAsync(TipoBobina nuevoTipoBobina, int idUsuario, IDbTransaction transaction); |         Task<TipoBobina?> CreateAsync(TipoBobina nuevoTipoBobina, int idUsuario, IDbTransaction transaction); | ||||||
|         Task<bool> UpdateAsync(TipoBobina tipoBobinaAActualizar, int idUsuario, IDbTransaction transaction); |         Task<bool> UpdateAsync(TipoBobina tipoBobinaAActualizar, int idUsuario, IDbTransaction transaction); | ||||||
|   | |||||||
| @@ -83,6 +83,42 @@ namespace GestionIntegral.Api.Data.Repositories.Impresion | |||||||
|             return inserted; |             return inserted; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         public async Task<bool> UpdateAsync(RegTirada tiradaAActualizar, int idUsuario, IDbTransaction transaction) | ||||||
|  |         { | ||||||
|  |             // 1. Obtener el estado actual para guardarlo en el historial | ||||||
|  |             const string sqlSelectActual = "SELECT * FROM dbo.bob_RegTiradas WHERE Id_Registro = @IdRegistro"; | ||||||
|  |             var estadoActual = await transaction.Connection!.QuerySingleOrDefaultAsync<RegTirada>(sqlSelectActual, new { IdRegistro = tiradaAActualizar.IdRegistro }, transaction); | ||||||
|  |  | ||||||
|  |             if (estadoActual == null) | ||||||
|  |             { | ||||||
|  |                 throw new KeyNotFoundException("No se encontró el registro de tirada a actualizar para generar el historial."); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             // 2. Guardar el estado PREVIO en el historial | ||||||
|  |             const string sqlHistorico = @"INSERT INTO dbo.bob_RegTiradas_H (Id_Registro, Ejemplares, Id_Publicacion, Fecha, Id_Planta, Id_Usuario, FechaMod, TipoMod) | ||||||
|  |                                           VALUES (@IdRegistroParam, @EjemplaresParam, @IdPublicacionParam, @FechaParam, @IdPlantaParam, @IdUsuarioParam, @FechaModParam, @TipoModParam);"; | ||||||
|  |             await transaction.Connection!.ExecuteAsync(sqlHistorico, new | ||||||
|  |             { | ||||||
|  |                 IdRegistroParam = estadoActual.IdRegistro, | ||||||
|  |                 EjemplaresParam = estadoActual.Ejemplares, | ||||||
|  |                 IdPublicacionParam = estadoActual.IdPublicacion, | ||||||
|  |                 FechaParam = estadoActual.Fecha, | ||||||
|  |                 IdPlantaParam = estadoActual.IdPlanta, | ||||||
|  |                 IdUsuarioParam = idUsuario, | ||||||
|  |                 FechaModParam = DateTime.Now, | ||||||
|  |                 TipoModParam = "Modificado" | ||||||
|  |             }, transaction); | ||||||
|  |  | ||||||
|  |             // 3. Actualizar el registro principal | ||||||
|  |             const string sqlUpdate = @" | ||||||
|  |                 UPDATE dbo.bob_RegTiradas  | ||||||
|  |                 SET Ejemplares = @Ejemplares | ||||||
|  |                 WHERE Id_Registro = @IdRegistro;"; | ||||||
|  |  | ||||||
|  |             var rowsAffected = await transaction.Connection!.ExecuteAsync(sqlUpdate, tiradaAActualizar, transaction); | ||||||
|  |             return rowsAffected == 1; | ||||||
|  |         } | ||||||
|  |  | ||||||
|         public async Task<bool> DeleteAsync(int idRegistro, int idUsuario, IDbTransaction transaction) |         public async Task<bool> DeleteAsync(int idRegistro, int idUsuario, IDbTransaction transaction) | ||||||
|         { |         { | ||||||
|             var actual = await GetByIdAsync(idRegistro); // No necesita TX aquí ya que es solo para historial |             var actual = await GetByIdAsync(idRegistro); // No necesita TX aquí ya que es solo para historial | ||||||
| @@ -276,6 +312,66 @@ namespace GestionIntegral.Api.Data.Repositories.Impresion | |||||||
|             return inserted; |             return inserted; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         public async Task<RegPublicacionSeccion?> GetByIdAsync(int idTirada) | ||||||
|  |         { | ||||||
|  |             const string sql = @"SELECT Id_Tirada AS IdTirada, Id_Publicacion AS IdPublicacion, Id_Seccion AS IdSeccion, CantPag, Fecha, Id_Planta AS IdPlanta  | ||||||
|  |                              FROM dbo.bob_RegPublicaciones WHERE Id_Tirada = @IdTiradaParam"; | ||||||
|  |             using var connection = _cf.CreateConnection(); | ||||||
|  |             return await connection.QuerySingleOrDefaultAsync<RegPublicacionSeccion>(sql, new { IdTiradaParam = idTirada }); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public async Task<bool> UpdateAsync(RegPublicacionSeccion seccionAActualizar, int idUsuario, IDbTransaction transaction) | ||||||
|  |         { | ||||||
|  |             // Obtener estado PREVIO para historial | ||||||
|  |             var actual = await GetByIdAsync(seccionAActualizar.IdTirada); | ||||||
|  |             if (actual == null) throw new KeyNotFoundException("No se encontró la sección de tirada a actualizar."); | ||||||
|  |  | ||||||
|  |             // Insertar en historial con tipo "Modificado" | ||||||
|  |             const string sqlHistorico = @"INSERT INTO dbo.bob_RegPublicaciones_H (Id_Tirada, Id_Publicacion, Id_Seccion, CantPag, Fecha, Id_Planta, Id_Usuario, FechaMod, TipoMod) | ||||||
|  |                                       VALUES (@IdTirada, @IdPublicacion, @IdSeccion, @CantPag, @Fecha, @IdPlanta, @IdUsuario, GETDATE(), 'Modificado');"; | ||||||
|  |             await transaction.Connection!.ExecuteAsync(sqlHistorico, new | ||||||
|  |             { | ||||||
|  |                 actual.IdTirada, | ||||||
|  |                 actual.IdPublicacion, | ||||||
|  |                 actual.IdSeccion, | ||||||
|  |                 actual.CantPag, | ||||||
|  |                 actual.Fecha, | ||||||
|  |                 actual.IdPlanta, | ||||||
|  |                 IdUsuario = idUsuario | ||||||
|  |             }, transaction); | ||||||
|  |  | ||||||
|  |             // Actualizar el registro | ||||||
|  |             const string sqlUpdate = "UPDATE dbo.bob_RegPublicaciones SET CantPag = @CantPag WHERE Id_Tirada = @IdTirada"; | ||||||
|  |             var rowsAffected = await transaction.Connection!.ExecuteAsync(sqlUpdate, seccionAActualizar, transaction); | ||||||
|  |             return rowsAffected == 1; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public async Task<bool> DeleteByIdAsync(int idTirada, int idUsuario, IDbTransaction transaction) | ||||||
|  |         { | ||||||
|  |             // Obtener estado PREVIO para historial | ||||||
|  |             var actual = await GetByIdAsync(idTirada); | ||||||
|  |             if (actual == null) return false; // Ya no existe, no hacemos nada. | ||||||
|  |  | ||||||
|  |             // Insertar en historial con tipo "Eliminado" | ||||||
|  |             const string sqlHistorico = @"INSERT INTO dbo.bob_RegPublicaciones_H (Id_Tirada, Id_Publicacion, Id_Seccion, CantPag, Fecha, Id_Planta, Id_Usuario, FechaMod, TipoMod) | ||||||
|  |                                       VALUES (@IdTirada, @IdPublicacion, @IdSeccion, @CantPag, @Fecha, @IdPlanta, @IdUsuario, GETDATE(), 'Eliminado');"; | ||||||
|  |             await transaction.Connection!.ExecuteAsync(sqlHistorico, new | ||||||
|  |             { | ||||||
|  |                 actual.IdTirada, | ||||||
|  |                 actual.IdPublicacion, | ||||||
|  |                 actual.IdSeccion, | ||||||
|  |                 actual.CantPag, | ||||||
|  |                 actual.Fecha, | ||||||
|  |                 actual.IdPlanta, | ||||||
|  |                 IdUsuario = idUsuario | ||||||
|  |             }, transaction); | ||||||
|  |  | ||||||
|  |             // Eliminar el registro | ||||||
|  |             const string sqlDelete = "DELETE FROM dbo.bob_RegPublicaciones WHERE Id_Tirada = @IdTirada"; | ||||||
|  |             var rowsAffected = await transaction.Connection!.ExecuteAsync(sqlDelete, new { IdTirada = idTirada }, transaction); | ||||||
|  |             return rowsAffected == 1; | ||||||
|  |         } | ||||||
|  |  | ||||||
|         public async Task<bool> DeleteByFechaPublicacionPlantaAsync(DateTime fecha, int idPublicacion, int idPlanta, int idUsuario, IDbTransaction transaction) |         public async Task<bool> DeleteByFechaPublicacionPlantaAsync(DateTime fecha, int idPublicacion, int idPlanta, int idUsuario, IDbTransaction transaction) | ||||||
|         { |         { | ||||||
|             var seccionesAEliminar = await GetByFechaPublicacionPlantaAsync(fecha.Date, idPublicacion, idPlanta); // No necesita TX, es SELECT |             var seccionesAEliminar = await GetByFechaPublicacionPlantaAsync(fecha.Date, idPublicacion, idPlanta); // No necesita TX, es SELECT | ||||||
|   | |||||||
| @@ -61,13 +61,13 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion | |||||||
|             } |             } | ||||||
|             if (fechaDesde.HasValue) |             if (fechaDesde.HasValue) | ||||||
|             { |             { | ||||||
|                 sqlBuilder.Append(" AND sb.FechaRemito >= @FechaDesdeParam"); // O FechaEstado según el contexto del filtro |                 sqlBuilder.Append(" AND sb.FechaRemito >= @FechaDesdeParam"); | ||||||
|                 parameters.Add("FechaDesdeParam", fechaDesde.Value.Date); |                 parameters.Add("FechaDesdeParam", fechaDesde.Value.Date); | ||||||
|             } |             } | ||||||
|             if (fechaHasta.HasValue) |             if (fechaHasta.HasValue) | ||||||
|             { |             { | ||||||
|                 sqlBuilder.Append(" AND sb.FechaRemito <= @FechaHastaParam"); // O FechaEstado |                 sqlBuilder.Append(" AND sb.FechaRemito <= @FechaHastaParam"); | ||||||
|                 parameters.Add("FechaHastaParam", fechaHasta.Value.Date.AddDays(1).AddTicks(-1)); // Hasta el final del día |                 parameters.Add("FechaHastaParam", fechaHasta.Value.Date); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             sqlBuilder.Append(" ORDER BY sb.FechaRemito DESC, sb.NroBobina;"); |             sqlBuilder.Append(" ORDER BY sb.FechaRemito DESC, sb.NroBobina;"); | ||||||
| @@ -224,14 +224,12 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion | |||||||
|  |  | ||||||
|             if (actual == null) throw new KeyNotFoundException("Bobina no encontrada para eliminar."); |             if (actual == null) throw new KeyNotFoundException("Bobina no encontrada para eliminar."); | ||||||
|  |  | ||||||
|             // --- INICIO DE CAMBIO EN VALIDACIÓN --- |  | ||||||
|             // Permitir eliminar si está Disponible (1) o Dañada (3) |             // Permitir eliminar si está Disponible (1) o Dañada (3) | ||||||
|             if (actual.IdEstadoBobina != 1 && actual.IdEstadoBobina != 3) |             if (actual.IdEstadoBobina != 1 && actual.IdEstadoBobina != 3) | ||||||
|             { |             { | ||||||
|                 _logger.LogWarning("Intento de eliminar bobina {IdBobina} que no está en estado 'Disponible' o 'Dañada'. Estado actual: {EstadoActual}", idBobina, actual.IdEstadoBobina); |                 _logger.LogWarning("Intento de eliminar bobina {IdBobina} que no está en estado 'Disponible' o 'Dañada'. Estado actual: {EstadoActual}", idBobina, actual.IdEstadoBobina); | ||||||
|                 return false; // Devolver false si no cumple la condición para ser eliminada |                 return false; // Devolver false si no cumple la condición para ser eliminada | ||||||
|             } |             } | ||||||
|             // --- FIN DE CAMBIO EN VALIDACIÓN --- |  | ||||||
|  |  | ||||||
|             const string sqlDelete = "DELETE FROM dbo.bob_StockBobinas WHERE Id_Bobina = @IdBobinaParam"; |             const string sqlDelete = "DELETE FROM dbo.bob_StockBobinas WHERE Id_Bobina = @IdBobinaParam"; | ||||||
|             const string sqlInsertHistorico = @" |             const string sqlInsertHistorico = @" | ||||||
|   | |||||||
| @@ -44,6 +44,24 @@ namespace GestionIntegral.Api.Data.Repositories.Impresion | |||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         public async Task<IEnumerable<TipoBobina>> GetAllDropdownAsync() | ||||||
|  |         { | ||||||
|  |             var sqlBuilder = new StringBuilder("SELECT Id_TipoBobina AS IdTipoBobina, Denominacion FROM dbo.bob_dtBobinas WHERE 1=1"); | ||||||
|  |              | ||||||
|  |             sqlBuilder.Append(" ORDER BY Denominacion;"); | ||||||
|  |  | ||||||
|  |             try | ||||||
|  |             { | ||||||
|  |                 using var connection = _connectionFactory.CreateConnection(); | ||||||
|  |                 return await connection.QueryAsync<TipoBobina>(sqlBuilder.ToString()); | ||||||
|  |             } | ||||||
|  |             catch (Exception ex) | ||||||
|  |             { | ||||||
|  |                 _logger.LogError(ex, "Error al obtener todos los Tipos de Bobina."); | ||||||
|  |                 return Enumerable.Empty<TipoBobina>(); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|         public async Task<TipoBobina?> GetByIdAsync(int id) |         public async Task<TipoBobina?> GetByIdAsync(int id) | ||||||
|         { |         { | ||||||
|             const string sql = "SELECT Id_TipoBobina AS IdTipoBobina, Denominacion FROM dbo.bob_dtBobinas WHERE Id_TipoBobina = @Id"; |             const string sql = "SELECT Id_TipoBobina AS IdTipoBobina, Denominacion FROM dbo.bob_dtBobinas WHERE Id_TipoBobina = @Id"; | ||||||
|   | |||||||
| @@ -45,5 +45,8 @@ namespace GestionIntegral.Api.Data.Repositories.Reportes | |||||||
|         Task<IEnumerable<LiquidacionCanillaGananciaDto>> GetLiquidacionCanillaGananciasAsync(DateTime fecha, int idCanilla); |         Task<IEnumerable<LiquidacionCanillaGananciaDto>> GetLiquidacionCanillaGananciasAsync(DateTime fecha, int idCanilla); | ||||||
|         Task<IEnumerable<ListadoDistCanMensualDiariosDto>> GetReporteMensualDiariosAsync(DateTime fechaDesde, DateTime fechaHasta, bool esAccionista); |         Task<IEnumerable<ListadoDistCanMensualDiariosDto>> GetReporteMensualDiariosAsync(DateTime fechaDesde, DateTime fechaHasta, bool esAccionista); | ||||||
|         Task<IEnumerable<ListadoDistCanMensualPubDto>> GetReporteMensualPorPublicacionAsync(DateTime fechaDesde, DateTime fechaHasta, bool esAccionista); |         Task<IEnumerable<ListadoDistCanMensualPubDto>> GetReporteMensualPorPublicacionAsync(DateTime fechaDesde, DateTime fechaHasta, bool esAccionista); | ||||||
|  |         Task<IEnumerable<FacturasParaReporteDto>> GetDatosReportePublicidadAsync(string periodo); | ||||||
|  |         Task<IEnumerable<DistribucionSuscripcionDto>> GetDistribucionSuscripcionesActivasAsync(DateTime fechaDesde, DateTime fechaHasta); | ||||||
|  |         Task<IEnumerable<DistribucionSuscripcionDto>> GetDistribucionSuscripcionesBajasAsync(DateTime fechaDesde, DateTime fechaHasta); | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -547,5 +547,111 @@ namespace GestionIntegral.Api.Data.Repositories.Reportes | |||||||
|                 commandType: CommandType.StoredProcedure, commandTimeout: 120 |                 commandType: CommandType.StoredProcedure, commandTimeout: 120 | ||||||
|             ); |             ); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         public async Task<IEnumerable<FacturasParaReporteDto>> GetDatosReportePublicidadAsync(string periodo) | ||||||
|  |         { | ||||||
|  |             // Esta consulta une todas las tablas necesarias para obtener los datos del reporte | ||||||
|  |             const string sql = @" | ||||||
|  |               SELECT  | ||||||
|  |                   f.IdFactura, | ||||||
|  |                   f.Periodo, | ||||||
|  |                   s.NombreCompleto AS NombreSuscriptor, | ||||||
|  |                   s.TipoDocumento, | ||||||
|  |                   s.NroDocumento, | ||||||
|  |                   f.ImporteFinal, | ||||||
|  |                   e.Id_Empresa AS IdEmpresa, | ||||||
|  |                   e.Nombre AS NombreEmpresa | ||||||
|  |               FROM dbo.susc_Facturas f | ||||||
|  |               JOIN dbo.susc_Suscriptores s ON f.IdSuscriptor = s.IdSuscriptor | ||||||
|  |               -- Usamos una subconsulta para obtener la empresa de forma segura | ||||||
|  |               JOIN ( | ||||||
|  |                   SELECT DISTINCT  | ||||||
|  |                       fd.IdFactura,  | ||||||
|  |                       p.Id_Empresa | ||||||
|  |                   FROM dbo.susc_FacturaDetalles fd | ||||||
|  |                   JOIN dbo.susc_Suscripciones sub ON fd.IdSuscripcion = sub.IdSuscripcion | ||||||
|  |                   JOIN dbo.dist_dtPublicaciones p ON sub.IdPublicacion = p.Id_Publicacion | ||||||
|  |               ) AS FacturaEmpresa ON f.IdFactura = FacturaEmpresa.IdFactura | ||||||
|  |               JOIN dbo.dist_dtEmpresas e ON FacturaEmpresa.Id_Empresa = e.Id_Empresa | ||||||
|  |               WHERE  | ||||||
|  |                   f.Periodo = @Periodo | ||||||
|  |                   AND f.EstadoPago = 'Pagada' | ||||||
|  |                   AND f.EstadoFacturacion = 'Pendiente de Facturar' | ||||||
|  |               ORDER BY  | ||||||
|  |                   e.Nombre, s.NombreCompleto; | ||||||
|  |           "; | ||||||
|  |  | ||||||
|  |             try | ||||||
|  |             { | ||||||
|  |                 using var connection = _dbConnectionFactory.CreateConnection(); | ||||||
|  |                 return await connection.QueryAsync<FacturasParaReporteDto>(sql, new { Periodo = periodo }); | ||||||
|  |             } | ||||||
|  |             catch (Exception ex) | ||||||
|  |             { | ||||||
|  |                 _logger.LogError(ex, "Error al ejecutar la consulta para el Reporte de Publicidad para el período {Periodo}", periodo); | ||||||
|  |                 return Enumerable.Empty<FacturasParaReporteDto>(); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public async Task<IEnumerable<DistribucionSuscripcionDto>> GetDistribucionSuscripcionesActivasAsync(DateTime fechaDesde, DateTime fechaHasta) | ||||||
|  |         { | ||||||
|  |             const string sql = @" | ||||||
|  |                 SELECT  | ||||||
|  |                     e.Nombre AS NombreEmpresa, p.Nombre AS NombrePublicacion, | ||||||
|  |                     sus.NombreCompleto AS NombreSuscriptor, sus.Direccion, sus.Telefono, | ||||||
|  |                     s.FechaInicio, s.FechaFin, s.DiasEntrega, s.Observaciones | ||||||
|  |                 FROM dbo.susc_Suscripciones s | ||||||
|  |                 JOIN dbo.susc_Suscriptores sus ON s.IdSuscriptor = sus.IdSuscriptor | ||||||
|  |                 JOIN dbo.dist_dtPublicaciones p ON s.IdPublicacion = p.Id_Publicacion | ||||||
|  |                 JOIN dbo.dist_dtEmpresas e ON p.Id_Empresa = e.Id_Empresa | ||||||
|  |                 WHERE  | ||||||
|  |                     -- --- INICIO DE LA CORRECCIÓN --- | ||||||
|  |                     -- Se asegura de que SOLO se incluyan suscripciones y suscriptores ACTIVOS. | ||||||
|  |                     s.Estado = 'Activa' AND sus.Activo = 1 | ||||||
|  |                     -- --- FIN DE LA CORRECCIÓN --- | ||||||
|  |                     AND s.FechaInicio <= @FechaHasta | ||||||
|  |                     AND (s.FechaFin IS NULL OR s.FechaFin >= @FechaDesde) | ||||||
|  |                 ORDER BY e.Nombre, p.Nombre, sus.NombreCompleto;"; | ||||||
|  |  | ||||||
|  |             try | ||||||
|  |             { | ||||||
|  |                 using var connection = _dbConnectionFactory.CreateConnection(); | ||||||
|  |                 return await connection.QueryAsync<DistribucionSuscripcionDto>(sql, new { FechaDesde = fechaDesde, FechaHasta = fechaHasta }); | ||||||
|  |             } | ||||||
|  |             catch (Exception ex) | ||||||
|  |             { | ||||||
|  |                 _logger.LogError(ex, "Error al obtener datos para Reporte de Distribución (Activas)."); | ||||||
|  |                 return Enumerable.Empty<DistribucionSuscripcionDto>(); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public async Task<IEnumerable<DistribucionSuscripcionDto>> GetDistribucionSuscripcionesBajasAsync(DateTime fechaDesde, DateTime fechaHasta) | ||||||
|  |         { | ||||||
|  |             const string sql = @" | ||||||
|  |                 SELECT  | ||||||
|  |                     e.Nombre AS NombreEmpresa, p.Nombre AS NombrePublicacion, | ||||||
|  |                     sus.NombreCompleto AS NombreSuscriptor, sus.Direccion, sus.Telefono, | ||||||
|  |                     s.FechaInicio, s.FechaFin, s.DiasEntrega, s.Observaciones | ||||||
|  |                 FROM dbo.susc_Suscripciones s | ||||||
|  |                 JOIN dbo.susc_Suscriptores sus ON s.IdSuscriptor = sus.IdSuscriptor | ||||||
|  |                 JOIN dbo.dist_dtPublicaciones p ON s.IdPublicacion = p.Id_Publicacion | ||||||
|  |                 JOIN dbo.dist_dtEmpresas e ON p.Id_Empresa = e.Id_Empresa | ||||||
|  |                 WHERE  | ||||||
|  |                     -- La lógica aquí es correcta: buscamos cualquier suscripción cuya fecha de fin | ||||||
|  |                     -- caiga dentro del rango de fechas seleccionado. | ||||||
|  |                     s.FechaFin BETWEEN @FechaDesde AND @FechaHasta | ||||||
|  |                 ORDER BY e.Nombre, p.Nombre, s.FechaFin, sus.NombreCompleto;"; | ||||||
|  |  | ||||||
|  |             try | ||||||
|  |             { | ||||||
|  |                 using var connection = _dbConnectionFactory.CreateConnection(); | ||||||
|  |                 return await connection.QueryAsync<DistribucionSuscripcionDto>(sql, new { FechaDesde = fechaDesde, FechaHasta = fechaHasta }); | ||||||
|  |             } | ||||||
|  |             catch (Exception ex) | ||||||
|  |             { | ||||||
|  |                 _logger.LogError(ex, "Error al obtener datos para Reporte de Distribución (Bajas)."); | ||||||
|  |                 return Enumerable.Empty<DistribucionSuscripcionDto>(); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -0,0 +1,139 @@ | |||||||
|  | using Dapper; | ||||||
|  | using GestionIntegral.Api.Models.Suscripciones; | ||||||
|  | using System.Data; | ||||||
|  | using System.Text; | ||||||
|  |  | ||||||
|  | namespace GestionIntegral.Api.Data.Repositories.Suscripciones | ||||||
|  | { | ||||||
|  |     public class AjusteRepository : IAjusteRepository | ||||||
|  |     { | ||||||
|  |         private readonly DbConnectionFactory _connectionFactory; | ||||||
|  |         private readonly ILogger<AjusteRepository> _logger; | ||||||
|  |  | ||||||
|  |         public AjusteRepository(DbConnectionFactory factory, ILogger<AjusteRepository> logger) | ||||||
|  |         { | ||||||
|  |             _connectionFactory = factory; | ||||||
|  |             _logger = logger; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public async Task<bool> UpdateAsync(Ajuste ajuste, IDbTransaction transaction) | ||||||
|  |         { | ||||||
|  |             const string sql = @" | ||||||
|  |                 UPDATE dbo.susc_Ajustes SET | ||||||
|  |                     IdEmpresa = @IdEmpresa, | ||||||
|  |                     FechaAjuste = @FechaAjuste, | ||||||
|  |                     TipoAjuste = @TipoAjuste, | ||||||
|  |                     Monto = @Monto, | ||||||
|  |                     Motivo = @Motivo | ||||||
|  |                 WHERE IdAjuste = @IdAjuste AND Estado = 'Pendiente';"; | ||||||
|  |             if (transaction?.Connection == null) | ||||||
|  |             { | ||||||
|  |                 throw new ArgumentNullException(nameof(transaction.Connection), "La conexión de la transacción no puede ser nula."); | ||||||
|  |             } | ||||||
|  |             var rows = await transaction.Connection.ExecuteAsync(sql, ajuste, transaction); | ||||||
|  |             return rows == 1; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public async Task<Ajuste?> CreateAsync(Ajuste nuevoAjuste, IDbTransaction transaction) | ||||||
|  |         { | ||||||
|  |             const string sql = @" | ||||||
|  |                 INSERT INTO dbo.susc_Ajustes (IdSuscriptor, IdEmpresa, FechaAjuste, TipoAjuste, Monto, Motivo, Estado, IdUsuarioAlta, FechaAlta) | ||||||
|  |                 OUTPUT INSERTED.* | ||||||
|  |                 VALUES (@IdSuscriptor, @IdEmpresa, @FechaAjuste, @TipoAjuste, @Monto, @Motivo, 'Pendiente', @IdUsuarioAlta, GETDATE());"; | ||||||
|  |             if (transaction?.Connection == null) | ||||||
|  |             { | ||||||
|  |                 throw new ArgumentNullException(nameof(transaction.Connection), "La conexión de la transacción no puede ser nula."); | ||||||
|  |             } | ||||||
|  |             return await transaction.Connection.QuerySingleOrDefaultAsync<Ajuste>(sql, nuevoAjuste, transaction); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public async Task<IEnumerable<Ajuste>> GetAjustesPorSuscriptorAsync(int idSuscriptor, DateTime? fechaDesde, DateTime? fechaHasta) | ||||||
|  |         { | ||||||
|  |             var sqlBuilder = new StringBuilder("SELECT * FROM dbo.susc_Ajustes WHERE IdSuscriptor = @IdSuscriptor"); | ||||||
|  |             var parameters = new DynamicParameters(); | ||||||
|  |             parameters.Add("IdSuscriptor", idSuscriptor); | ||||||
|  |  | ||||||
|  |             if (fechaDesde.HasValue) | ||||||
|  |             { | ||||||
|  |                 sqlBuilder.Append(" AND FechaAjuste >= @FechaDesde"); | ||||||
|  |                 parameters.Add("FechaDesde", fechaDesde.Value.Date); | ||||||
|  |             } | ||||||
|  |             if (fechaHasta.HasValue) | ||||||
|  |             { | ||||||
|  |                 sqlBuilder.Append(" AND FechaAjuste <= @FechaHasta"); | ||||||
|  |                 parameters.Add("FechaHasta", fechaHasta.Value.Date); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             sqlBuilder.Append(" ORDER BY FechaAlta DESC;"); | ||||||
|  |  | ||||||
|  |             using var connection = _connectionFactory.CreateConnection(); | ||||||
|  |             return await connection.QueryAsync<Ajuste>(sqlBuilder.ToString(), parameters); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public async Task<IEnumerable<Ajuste>> GetAjustesPendientesHastaFechaAsync(int idSuscriptor, int idEmpresa, DateTime fechaHasta, IDbTransaction transaction) | ||||||
|  |         { | ||||||
|  |             const string sql = @" | ||||||
|  |                 SELECT * FROM dbo.susc_Ajustes  | ||||||
|  |                 WHERE IdSuscriptor = @IdSuscriptor  | ||||||
|  |                 AND IdEmpresa = @IdEmpresa | ||||||
|  |                 AND Estado = 'Pendiente' | ||||||
|  |                 AND FechaAjuste <= @FechaHasta;"; | ||||||
|  |              | ||||||
|  |             if (transaction?.Connection == null) | ||||||
|  |             { | ||||||
|  |                 throw new ArgumentNullException(nameof(transaction.Connection), "La conexión de la transacción no puede ser nula."); | ||||||
|  |             } | ||||||
|  |             return await transaction.Connection.QueryAsync<Ajuste>(sql, new { idSuscriptor, idEmpresa, FechaHasta = fechaHasta }, transaction); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public async Task<bool> MarcarAjustesComoAplicadosAsync(IEnumerable<int> idsAjustes, int idFactura, IDbTransaction transaction) | ||||||
|  |         { | ||||||
|  |             if (!idsAjustes.Any()) return true; | ||||||
|  |  | ||||||
|  |             const string sql = @" | ||||||
|  |                 UPDATE dbo.susc_Ajustes SET | ||||||
|  |                     Estado = 'Aplicado', | ||||||
|  |                     IdFacturaAplicado = @IdFactura | ||||||
|  |                 WHERE IdAjuste IN @IdsAjustes;"; | ||||||
|  |  | ||||||
|  |             if (transaction?.Connection == null) | ||||||
|  |             { | ||||||
|  |                 throw new ArgumentNullException(nameof(transaction.Connection), "La conexión de la transacción no puede ser nula."); | ||||||
|  |             } | ||||||
|  |             var rowsAffected = await transaction.Connection.ExecuteAsync(sql, new { IdsAjustes = idsAjustes, IdFactura = idFactura }, transaction); | ||||||
|  |             return rowsAffected == idsAjustes.Count(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public async Task<Ajuste?> GetByIdAsync(int idAjuste) | ||||||
|  |         { | ||||||
|  |             const string sql = "SELECT * FROM dbo.susc_Ajustes WHERE IdAjuste = @IdAjuste;"; | ||||||
|  |             using var connection = _connectionFactory.CreateConnection(); | ||||||
|  |             return await connection.QuerySingleOrDefaultAsync<Ajuste>(sql, new { idAjuste }); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public async Task<bool> AnularAjusteAsync(int idAjuste, int idUsuario, IDbTransaction transaction) | ||||||
|  |         { | ||||||
|  |             const string sql = @" | ||||||
|  |                 UPDATE dbo.susc_Ajustes SET | ||||||
|  |                     Estado = 'Anulado', | ||||||
|  |                     IdUsuarioAnulo = @IdUsuario, | ||||||
|  |                     FechaAnulacion = GETDATE() | ||||||
|  |                 WHERE IdAjuste = @IdAjuste AND Estado = 'Pendiente';"; | ||||||
|  |  | ||||||
|  |             if (transaction?.Connection == null) | ||||||
|  |             { | ||||||
|  |                 throw new ArgumentNullException(nameof(transaction.Connection), "La conexión de la transacción no puede ser nula."); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             var rows = await transaction.Connection.ExecuteAsync(sql, new { IdAjuste = idAjuste, IdUsuario = idUsuario }, transaction); | ||||||
|  |             return rows == 1; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public async Task<IEnumerable<Ajuste>> GetAjustesPorIdFacturaAsync(int idFactura) | ||||||
|  |         { | ||||||
|  |             const string sql = "SELECT * FROM dbo.susc_Ajustes WHERE IdFacturaAplicado = @IdFactura;"; | ||||||
|  |             using var connection = _connectionFactory.CreateConnection(); | ||||||
|  |             return await connection.QueryAsync<Ajuste>(sql, new { IdFactura = idFactura }); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,58 @@ | |||||||
|  | using Dapper; | ||||||
|  | using System.Data; | ||||||
|  |  | ||||||
|  | namespace GestionIntegral.Api.Data.Repositories.Suscripciones | ||||||
|  | { | ||||||
|  |     public class FacturaDetalleRepository : IFacturaDetalleRepository | ||||||
|  |     { | ||||||
|  |         private readonly DbConnectionFactory _connectionFactory; | ||||||
|  |         private readonly ILogger<FacturaDetalleRepository> _logger; | ||||||
|  |  | ||||||
|  |         public FacturaDetalleRepository(DbConnectionFactory connectionFactory, ILogger<FacturaDetalleRepository> logger) | ||||||
|  |         { | ||||||
|  |             _connectionFactory = connectionFactory; | ||||||
|  |             _logger = logger; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public async Task<FacturaDetalle?> CreateAsync(FacturaDetalle nuevoDetalle, IDbTransaction transaction) | ||||||
|  |         { | ||||||
|  |             if (transaction == null || transaction.Connection == null) | ||||||
|  |             { | ||||||
|  |                 throw new ArgumentNullException(nameof(transaction), "La transacción o su conexión no pueden ser nulas."); | ||||||
|  |             } | ||||||
|  |             const string sqlInsert = @" | ||||||
|  |                 INSERT INTO dbo.susc_FacturaDetalles (IdFactura, IdSuscripcion, Descripcion, ImporteBruto, DescuentoAplicado, ImporteNeto) | ||||||
|  |                 OUTPUT INSERTED.* | ||||||
|  |                 VALUES (@IdFactura, @IdSuscripcion, @Descripcion, @ImporteBruto, @DescuentoAplicado, @ImporteNeto);"; | ||||||
|  |              | ||||||
|  |             return await transaction.Connection.QuerySingleOrDefaultAsync<FacturaDetalle>(sqlInsert, nuevoDetalle, transaction); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public async Task<IEnumerable<FacturaDetalle>> GetDetallesPorFacturaIdAsync(int idFactura) | ||||||
|  |         { | ||||||
|  |             const string sql = "SELECT * FROM dbo.susc_FacturaDetalles WHERE IdFactura = @IdFactura;"; | ||||||
|  |             using var connection = _connectionFactory.CreateConnection(); | ||||||
|  |             return await connection.QueryAsync<FacturaDetalle>(sql, new { IdFactura = idFactura }); | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         public async Task<IEnumerable<FacturaDetalle>> GetDetallesPorPeriodoAsync(string periodo) | ||||||
|  |         { | ||||||
|  |             const string sql = @" | ||||||
|  |                 SELECT fd.*  | ||||||
|  |                 FROM dbo.susc_FacturaDetalles fd | ||||||
|  |                 JOIN dbo.susc_Facturas f ON fd.IdFactura = f.IdFactura | ||||||
|  |                 WHERE f.Periodo = @Periodo;"; | ||||||
|  |              | ||||||
|  |             try | ||||||
|  |             { | ||||||
|  |                 using var connection = _connectionFactory.CreateConnection(); | ||||||
|  |                 return await connection.QueryAsync<FacturaDetalle>(sql, new { Periodo = periodo }); | ||||||
|  |             } | ||||||
|  |             catch (Exception ex) | ||||||
|  |             { | ||||||
|  |                 _logger.LogError(ex, "Error al obtener los detalles de factura para el período {Periodo}", periodo); | ||||||
|  |                 return Enumerable.Empty<FacturaDetalle>(); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,259 @@ | |||||||
|  | using Dapper; | ||||||
|  | using GestionIntegral.Api.Data; | ||||||
|  | using GestionIntegral.Api.Models.Suscripciones; | ||||||
|  | using Microsoft.Extensions.Logging; | ||||||
|  | using System; | ||||||
|  | using System.Collections.Generic; | ||||||
|  | using System.Data; | ||||||
|  | using System.Linq; | ||||||
|  | using System.Text; | ||||||
|  | using System.Threading.Tasks; | ||||||
|  |  | ||||||
|  | namespace GestionIntegral.Api.Data.Repositories.Suscripciones | ||||||
|  | { | ||||||
|  |     public class FacturaRepository : IFacturaRepository | ||||||
|  |     { | ||||||
|  |         private readonly DbConnectionFactory _connectionFactory; | ||||||
|  |         private readonly ILogger<FacturaRepository> _logger; | ||||||
|  |  | ||||||
|  |         public FacturaRepository(DbConnectionFactory connectionFactory, ILogger<FacturaRepository> logger) | ||||||
|  |         { | ||||||
|  |             _connectionFactory = connectionFactory; | ||||||
|  |             _logger = logger; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public async Task<Factura?> GetByIdAsync(int idFactura) | ||||||
|  |         { | ||||||
|  |             const string sql = "SELECT * FROM dbo.susc_Facturas WHERE IdFactura = @idFactura;"; | ||||||
|  |             using var connection = _connectionFactory.CreateConnection(); | ||||||
|  |             return await connection.QuerySingleOrDefaultAsync<Factura>(sql, new { idFactura }); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public async Task<IEnumerable<Factura>> GetByPeriodoAsync(string periodo) | ||||||
|  |         { | ||||||
|  |             const string sql = "SELECT * FROM dbo.susc_Facturas WHERE Periodo = @Periodo ORDER BY IdFactura;"; | ||||||
|  |             using var connection = _connectionFactory.CreateConnection(); | ||||||
|  |             return await connection.QueryAsync<Factura>(sql, new { Periodo = periodo }); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public async Task<Factura?> GetBySuscriptorYPeriodoAsync(int idSuscriptor, string periodo, IDbTransaction transaction) | ||||||
|  |         { | ||||||
|  |             const string sql = "SELECT TOP 1 * FROM dbo.susc_Facturas WHERE IdSuscriptor = @IdSuscriptor AND Periodo = @Periodo;"; | ||||||
|  |             if (transaction == null || transaction.Connection == null) | ||||||
|  |             { | ||||||
|  |                 throw new ArgumentNullException(nameof(transaction), "La transacción o su conexión no pueden ser nulas."); | ||||||
|  |             } | ||||||
|  |             return await transaction.Connection.QuerySingleOrDefaultAsync<Factura>(sql, new { idSuscriptor, Periodo = periodo }, transaction); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public async Task<IEnumerable<Factura>> GetListBySuscriptorYPeriodoAsync(int idSuscriptor, string periodo) | ||||||
|  |         { | ||||||
|  |             const string sql = "SELECT * FROM dbo.susc_Facturas WHERE IdSuscriptor = @IdSuscriptor AND Periodo = @Periodo;"; | ||||||
|  |             using var connection = _connectionFactory.CreateConnection(); | ||||||
|  |             return await connection.QueryAsync<Factura>(sql, new { idSuscriptor, Periodo = periodo }); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public async Task<Factura?> CreateAsync(Factura nuevaFactura, IDbTransaction transaction) | ||||||
|  |         { | ||||||
|  |             if (transaction == null || transaction.Connection == null) | ||||||
|  |             { | ||||||
|  |                 throw new ArgumentNullException(nameof(transaction), "La transacción o su conexión no pueden ser nulas."); | ||||||
|  |             } | ||||||
|  |             const string sqlInsert = @" | ||||||
|  |                 INSERT INTO dbo.susc_Facturas (IdSuscriptor, Periodo, FechaEmision, FechaVencimiento, ImporteBruto, DescuentoAplicado, ImporteFinal, EstadoPago, EstadoFacturacion) | ||||||
|  |                 OUTPUT INSERTED.* | ||||||
|  |                 VALUES (@IdSuscriptor, @Periodo, @FechaEmision, @FechaVencimiento, @ImporteBruto, @DescuentoAplicado, @ImporteFinal, @EstadoPago, @EstadoFacturacion);"; | ||||||
|  |  | ||||||
|  |             return await transaction.Connection.QuerySingleAsync<Factura>(sqlInsert, nuevaFactura, transaction); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public async Task<bool> UpdateEstadoPagoAsync(int idFactura, string nuevoEstadoPago, IDbTransaction transaction) | ||||||
|  |         { | ||||||
|  |             if (transaction == null || transaction.Connection == null) | ||||||
|  |             { | ||||||
|  |                 throw new ArgumentNullException(nameof(transaction), "La transacción o su conexión no pueden ser nulas."); | ||||||
|  |             } | ||||||
|  |             const string sql = "UPDATE dbo.susc_Facturas SET EstadoPago = @NuevoEstadoPago WHERE IdFactura = @IdFactura;"; | ||||||
|  |             var rowsAffected = await transaction.Connection.ExecuteAsync(sql, new { NuevoEstadoPago = nuevoEstadoPago, idFactura }, transaction); | ||||||
|  |             return rowsAffected == 1; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public async Task<bool> UpdateNumeroFacturaAsync(int idFactura, string numeroFactura, IDbTransaction transaction) | ||||||
|  |         { | ||||||
|  |             if (transaction == null || transaction.Connection == null) | ||||||
|  |             { | ||||||
|  |                 throw new ArgumentNullException(nameof(transaction), "La transacción o su conexión no pueden ser nulas."); | ||||||
|  |             } | ||||||
|  |             const string sql = @" | ||||||
|  |                 UPDATE dbo.susc_Facturas SET  | ||||||
|  |                     NumeroFactura = @NumeroFactura,  | ||||||
|  |                     EstadoFacturacion = 'Facturado'  | ||||||
|  |                 WHERE IdFactura = @IdFactura;"; | ||||||
|  |             var rowsAffected = await transaction.Connection.ExecuteAsync(sql, new { NumeroFactura = numeroFactura, idFactura }, transaction); | ||||||
|  |             return rowsAffected == 1; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public async Task<bool> UpdateLoteDebitoAsync(IEnumerable<int> idsFacturas, int idLoteDebito, IDbTransaction transaction) | ||||||
|  |         { | ||||||
|  |             if (transaction == null || transaction.Connection == null) | ||||||
|  |             { | ||||||
|  |                 throw new ArgumentNullException(nameof(transaction), "La transacción o su conexión no pueden ser nulas."); | ||||||
|  |             } | ||||||
|  |             const string sql = "UPDATE dbo.susc_Facturas SET IdLoteDebito = @IdLoteDebito WHERE IdFactura IN @IdsFacturas;"; | ||||||
|  |             var rowsAffected = await transaction.Connection.ExecuteAsync(sql, new { IdLoteDebito = idLoteDebito, IdsFacturas = idsFacturas }, transaction); | ||||||
|  |             return rowsAffected == idsFacturas.Count(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public async Task<IEnumerable<(Factura Factura, string NombreSuscriptor, int IdEmpresa, decimal TotalPagado)>> GetByPeriodoEnrichedAsync(string periodo, string? nombreSuscriptor, string? estadoPago, string? estadoFacturacion) | ||||||
|  |         { | ||||||
|  |             var sqlBuilder = new StringBuilder(@" | ||||||
|  |                 WITH FacturaConEmpresa AS ( | ||||||
|  |                     -- Esta subconsulta obtiene el IdEmpresa para cada factura basándose en la primera suscripción que encuentra en sus detalles. | ||||||
|  |                     -- Esto es seguro porque nuestra lógica de negocio asegura que todos los detalles de una factura pertenecen a la misma empresa. | ||||||
|  |                     SELECT  | ||||||
|  |                         f.IdFactura, | ||||||
|  |                         (SELECT TOP 1 p.Id_Empresa  | ||||||
|  |                          FROM dbo.susc_FacturaDetalles fd | ||||||
|  |                          JOIN dbo.susc_Suscripciones s ON fd.IdSuscripcion = s.IdSuscripcion | ||||||
|  |                          JOIN dbo.dist_dtPublicaciones p ON s.IdPublicacion = p.Id_Publicacion | ||||||
|  |                          WHERE fd.IdFactura = f.IdFactura) AS IdEmpresa | ||||||
|  |                     FROM dbo.susc_Facturas f | ||||||
|  |                     WHERE f.Periodo = @Periodo | ||||||
|  |                 ) | ||||||
|  |                 SELECT  | ||||||
|  |                     f.*,  | ||||||
|  |                     s.NombreCompleto AS NombreSuscriptor,  | ||||||
|  |                     fce.IdEmpresa, | ||||||
|  |                     (SELECT ISNULL(SUM(Monto), 0) FROM dbo.susc_Pagos pg WHERE pg.IdFactura = f.IdFactura AND pg.Estado = 'Aprobado') AS TotalPagado | ||||||
|  |                 FROM dbo.susc_Facturas f | ||||||
|  |                 JOIN dbo.susc_Suscriptores s ON f.IdSuscriptor = s.IdSuscriptor | ||||||
|  |                 JOIN FacturaConEmpresa fce ON f.IdFactura = fce.IdFactura | ||||||
|  |                 WHERE f.Periodo = @Periodo"); | ||||||
|  |  | ||||||
|  |             var parameters = new DynamicParameters(); | ||||||
|  |             parameters.Add("Periodo", periodo); | ||||||
|  |  | ||||||
|  |             if (!string.IsNullOrWhiteSpace(nombreSuscriptor)) | ||||||
|  |             { | ||||||
|  |                 sqlBuilder.Append(" AND s.NombreCompleto LIKE @NombreSuscriptor"); | ||||||
|  |                 parameters.Add("NombreSuscriptor", $"%{nombreSuscriptor}%"); | ||||||
|  |             } | ||||||
|  |             if (!string.IsNullOrWhiteSpace(estadoPago)) | ||||||
|  |             { | ||||||
|  |                 sqlBuilder.Append(" AND f.EstadoPago = @EstadoPago"); | ||||||
|  |                 parameters.Add("EstadoPago", estadoPago); | ||||||
|  |             } | ||||||
|  |             if (!string.IsNullOrWhiteSpace(estadoFacturacion)) | ||||||
|  |             { | ||||||
|  |                 sqlBuilder.Append(" AND f.EstadoFacturacion = @EstadoFacturacion"); | ||||||
|  |                 parameters.Add("EstadoFacturacion", estadoFacturacion); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             sqlBuilder.Append(" ORDER BY s.NombreCompleto, f.IdFactura;"); | ||||||
|  |  | ||||||
|  |             try | ||||||
|  |             { | ||||||
|  |                 using var connection = _connectionFactory.CreateConnection(); | ||||||
|  |                 var result = await connection.QueryAsync<Factura, string, int, decimal, (Factura, string, int, decimal)>( | ||||||
|  |                     sqlBuilder.ToString(), | ||||||
|  |                     (factura, suscriptor, idEmpresa, totalPagado) => (factura, suscriptor, idEmpresa, totalPagado), | ||||||
|  |                     parameters, | ||||||
|  |                     splitOn: "NombreSuscriptor,IdEmpresa,TotalPagado" | ||||||
|  |                 ); | ||||||
|  |                 return result; | ||||||
|  |             } | ||||||
|  |             catch (Exception ex) | ||||||
|  |             { | ||||||
|  |                 _logger.LogError(ex, "Error al obtener facturas enriquecidas para el período {Periodo}", periodo); | ||||||
|  |                 return Enumerable.Empty<(Factura, string, int, decimal)>(); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public async Task<bool> UpdateEstadoYMotivoAsync(int idFactura, string nuevoEstadoPago, string? motivoRechazo, IDbTransaction transaction) | ||||||
|  |         { | ||||||
|  |             if (transaction == null || transaction.Connection == null) | ||||||
|  |             { | ||||||
|  |                 throw new ArgumentNullException(nameof(transaction), "La transacción o su conexión no pueden ser nulas."); | ||||||
|  |             } | ||||||
|  |             const string sql = @" | ||||||
|  |                 UPDATE dbo.susc_Facturas SET | ||||||
|  |                     EstadoPago = @NuevoEstadoPago, | ||||||
|  |                     MotivoRechazo = @MotivoRechazo | ||||||
|  |                 WHERE IdFactura = @IdFactura;"; | ||||||
|  |             var rowsAffected = await transaction.Connection.ExecuteAsync(sql, new { NuevoEstadoPago = nuevoEstadoPago, MotivoRechazo = motivoRechazo, idFactura }, transaction); | ||||||
|  |             return rowsAffected == 1; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public async Task<string?> GetUltimoPeriodoFacturadoAsync() | ||||||
|  |         { | ||||||
|  |             const string sql = "SELECT TOP 1 Periodo FROM dbo.susc_Facturas ORDER BY Periodo DESC;"; | ||||||
|  |             using var connection = _connectionFactory.CreateConnection(); | ||||||
|  |             return await connection.QuerySingleOrDefaultAsync<string>(sql); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public async Task<IEnumerable<(Factura Factura, string NombreEmpresa)>> GetFacturasConEmpresaAsync(int idSuscriptor, string periodo) | ||||||
|  |         { | ||||||
|  |             // Esta consulta es más robusta y eficiente. Obtiene la factura y el nombre de la empresa en una sola llamada. | ||||||
|  |             const string sql = @" | ||||||
|  |             SELECT f.*, e.Nombre AS NombreEmpresa | ||||||
|  |             FROM dbo.susc_Facturas f | ||||||
|  |             OUTER APPLY ( | ||||||
|  |                 SELECT TOP 1 emp.Nombre | ||||||
|  |                 FROM dbo.susc_FacturaDetalles fd | ||||||
|  |                 JOIN dbo.susc_Suscripciones s ON fd.IdSuscripcion = s.IdSuscripcion | ||||||
|  |                 JOIN dbo.dist_dtPublicaciones p ON s.IdPublicacion = p.Id_Publicacion | ||||||
|  |                 JOIN dbo.dist_dtEmpresas emp ON p.Id_Empresa = emp.Id_Empresa | ||||||
|  |                 WHERE fd.IdFactura = f.IdFactura | ||||||
|  |             ) e | ||||||
|  |             WHERE f.IdSuscriptor = @IdSuscriptor AND f.Periodo = @Periodo;"; | ||||||
|  |  | ||||||
|  |             try | ||||||
|  |             { | ||||||
|  |                 using var connection = _connectionFactory.CreateConnection(); | ||||||
|  |                 var result = await connection.QueryAsync<Factura, string, (Factura, string)>( | ||||||
|  |                     sql, | ||||||
|  |                     (factura, nombreEmpresa) => (factura, nombreEmpresa ?? "N/A"), // Asignamos "N/A" si no encuentra empresa | ||||||
|  |                     new { IdSuscriptor = idSuscriptor, Periodo = periodo }, | ||||||
|  |                     splitOn: "NombreEmpresa" | ||||||
|  |                 ); | ||||||
|  |                 return result; | ||||||
|  |             } | ||||||
|  |             catch (Exception ex) | ||||||
|  |             { | ||||||
|  |                 _logger.LogError(ex, "Error al obtener facturas con empresa para suscriptor {IdSuscriptor} y período {Periodo}", idSuscriptor, periodo); | ||||||
|  |                 return Enumerable.Empty<(Factura, string)>(); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public async Task<IEnumerable<Factura>> GetFacturasPagadasPendientesDeFacturar(string periodo) | ||||||
|  |         { | ||||||
|  |             // Consulta simplificada pero robusta. | ||||||
|  |             const string sql = @" | ||||||
|  |               SELECT * FROM dbo.susc_Facturas  | ||||||
|  |               WHERE Periodo = @Periodo  | ||||||
|  |               AND EstadoPago = 'Pagada'  | ||||||
|  |               AND EstadoFacturacion = 'Pendiente de Facturar';"; | ||||||
|  |             try | ||||||
|  |             { | ||||||
|  |                 using var connection = _connectionFactory.CreateConnection(); | ||||||
|  |                 return await connection.QueryAsync<Factura>(sql, new { Periodo = periodo }); | ||||||
|  |             } | ||||||
|  |             catch (Exception ex) | ||||||
|  |             { | ||||||
|  |                 _logger.LogError(ex, "Error al obtener facturas pagadas pendientes de facturar para el período {Periodo}", periodo); | ||||||
|  |                 return Enumerable.Empty<Factura>(); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public async Task<IEnumerable<Factura>> GetByIdsAsync(IEnumerable<int> ids) | ||||||
|  |         { | ||||||
|  |             if (ids == null || !ids.Any()) | ||||||
|  |             { | ||||||
|  |                 return Enumerable.Empty<Factura>(); | ||||||
|  |             } | ||||||
|  |             const string sql = "SELECT * FROM dbo.susc_Facturas WHERE IdFactura IN @Ids;"; | ||||||
|  |             using var connection = _connectionFactory.CreateConnection(); | ||||||
|  |             return await connection.QueryAsync<Factura>(sql, new { Ids = ids }); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,54 @@ | |||||||
|  | using Dapper; | ||||||
|  | using GestionIntegral.Api.Models.Suscripciones; | ||||||
|  |  | ||||||
|  | namespace GestionIntegral.Api.Data.Repositories.Suscripciones | ||||||
|  | { | ||||||
|  |     public class FormaPagoRepository : IFormaPagoRepository | ||||||
|  |     { | ||||||
|  |         private readonly DbConnectionFactory _connectionFactory; | ||||||
|  |         private readonly ILogger<FormaPagoRepository> _logger; | ||||||
|  |  | ||||||
|  |         public FormaPagoRepository(DbConnectionFactory connectionFactory, ILogger<FormaPagoRepository> logger) | ||||||
|  |         { | ||||||
|  |             _connectionFactory = connectionFactory; | ||||||
|  |             _logger = logger; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public async Task<IEnumerable<FormaPago>> GetAllAsync() | ||||||
|  |         { | ||||||
|  |             const string sql = @" | ||||||
|  |                 SELECT IdFormaPago, Nombre, RequiereCBU, Activo  | ||||||
|  |                 FROM dbo.susc_FormasDePago  | ||||||
|  |                 WHERE Activo = 1  | ||||||
|  |                 ORDER BY Nombre;"; | ||||||
|  |             try | ||||||
|  |             { | ||||||
|  |                 using var connection = _connectionFactory.CreateConnection(); | ||||||
|  |                 return await connection.QueryAsync<FormaPago>(sql); | ||||||
|  |             } | ||||||
|  |             catch (Exception ex) | ||||||
|  |             { | ||||||
|  |                 _logger.LogError(ex, "Error al obtener todas las Formas de Pago activas."); | ||||||
|  |                 return Enumerable.Empty<FormaPago>(); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public async Task<FormaPago?> GetByIdAsync(int id) | ||||||
|  |         { | ||||||
|  |             const string sql = @" | ||||||
|  |                 SELECT IdFormaPago, Nombre, RequiereCBU, Activo  | ||||||
|  |                 FROM dbo.susc_FormasDePago  | ||||||
|  |                 WHERE IdFormaPago = @Id;"; | ||||||
|  |             try | ||||||
|  |             { | ||||||
|  |                 using var connection = _connectionFactory.CreateConnection(); | ||||||
|  |                 return await connection.QuerySingleOrDefaultAsync<FormaPago>(sql, new { Id = id }); | ||||||
|  |             } | ||||||
|  |             catch (Exception ex) | ||||||
|  |             { | ||||||
|  |                 _logger.LogError(ex, "Error al obtener Forma de Pago por ID: {IdFormaPago}", id); | ||||||
|  |                 return null; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,22 @@ | |||||||
|  | // Archivo: GestionIntegral.Api/Data/Repositories/Suscripciones/IAjusteRepository.cs | ||||||
|  |  | ||||||
|  | using GestionIntegral.Api.Models.Suscripciones; | ||||||
|  | using System; | ||||||
|  | using System.Collections.Generic; | ||||||
|  | using System.Data; | ||||||
|  | using System.Threading.Tasks; | ||||||
|  |  | ||||||
|  | namespace GestionIntegral.Api.Data.Repositories.Suscripciones | ||||||
|  | { | ||||||
|  |     public interface IAjusteRepository | ||||||
|  |     { | ||||||
|  |         Task<Ajuste?> GetByIdAsync(int idAjuste); | ||||||
|  |         Task<Ajuste?> CreateAsync(Ajuste nuevoAjuste, IDbTransaction transaction); | ||||||
|  |         Task<bool> UpdateAsync(Ajuste ajuste, IDbTransaction transaction); | ||||||
|  |         Task<bool> AnularAjusteAsync(int idAjuste, int idUsuario, IDbTransaction transaction); | ||||||
|  |         Task<IEnumerable<Ajuste>> GetAjustesPorSuscriptorAsync(int idSuscriptor, DateTime? fechaDesde, DateTime? fechaHasta); | ||||||
|  |         Task<IEnumerable<Ajuste>> GetAjustesPendientesHastaFechaAsync(int idSuscriptor, int idEmpresa, DateTime fechaHasta, IDbTransaction transaction); | ||||||
|  |         Task<bool> MarcarAjustesComoAplicadosAsync(IEnumerable<int> idsAjustes, int idFactura, IDbTransaction transaction); | ||||||
|  |         Task<IEnumerable<Ajuste>> GetAjustesPorIdFacturaAsync(int idFactura); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,22 @@ | |||||||
|  | using System.Data; | ||||||
|  |  | ||||||
|  | namespace GestionIntegral.Api.Data.Repositories.Suscripciones | ||||||
|  | { | ||||||
|  |     public interface IFacturaDetalleRepository | ||||||
|  |     { | ||||||
|  |         /// <summary> | ||||||
|  |         /// Crea un nuevo registro de detalle de factura. | ||||||
|  |         /// </summary> | ||||||
|  |         Task<FacturaDetalle?> CreateAsync(FacturaDetalle nuevoDetalle, IDbTransaction transaction); | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Obtiene todos los detalles de una factura específica. | ||||||
|  |         /// </summary> | ||||||
|  |         Task<IEnumerable<FacturaDetalle>> GetDetallesPorFacturaIdAsync(int idFactura); | ||||||
|  |          | ||||||
|  |         /// <summary> | ||||||
|  |         /// Obtiene de forma eficiente todos los detalles de todas las facturas de un período específico. | ||||||
|  |         /// </summary> | ||||||
|  |         Task<IEnumerable<FacturaDetalle>> GetDetallesPorPeriodoAsync(string periodo); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,23 @@ | |||||||
|  | using GestionIntegral.Api.Models.Suscripciones; | ||||||
|  | using System.Data; | ||||||
|  |  | ||||||
|  | namespace GestionIntegral.Api.Data.Repositories.Suscripciones | ||||||
|  | { | ||||||
|  |     public interface IFacturaRepository | ||||||
|  |     { | ||||||
|  |         Task<Factura?> GetByIdAsync(int idFactura); | ||||||
|  |         Task<IEnumerable<Factura>> GetByIdsAsync(IEnumerable<int> ids); | ||||||
|  |         Task<IEnumerable<Factura>> GetByPeriodoAsync(string periodo); | ||||||
|  |         Task<Factura?> GetBySuscriptorYPeriodoAsync(int idSuscriptor, string periodo, IDbTransaction transaction); | ||||||
|  |         Task<IEnumerable<Factura>> GetListBySuscriptorYPeriodoAsync(int idSuscriptor, string periodo); | ||||||
|  |         Task<IEnumerable<(Factura Factura, string NombreEmpresa)>> GetFacturasConEmpresaAsync(int idSuscriptor, string periodo); | ||||||
|  |         Task<Factura?> CreateAsync(Factura nuevaFactura, IDbTransaction transaction); | ||||||
|  |         Task<bool> UpdateEstadoPagoAsync(int idFactura, string nuevoEstadoPago, IDbTransaction transaction); | ||||||
|  |         Task<bool> UpdateNumeroFacturaAsync(int idFactura, string numeroFactura, IDbTransaction transaction); | ||||||
|  |         Task<bool> UpdateLoteDebitoAsync(IEnumerable<int> idsFacturas, int idLoteDebito, IDbTransaction transaction); | ||||||
|  |         Task<IEnumerable<(Factura Factura, string NombreSuscriptor, int IdEmpresa, decimal TotalPagado)>> GetByPeriodoEnrichedAsync(string periodo, string? nombreSuscriptor, string? estadoPago, string? estadoFacturacion); | ||||||
|  |         Task<bool> UpdateEstadoYMotivoAsync(int idFactura, string nuevoEstadoPago, string? motivoRechazo, IDbTransaction transaction);    | ||||||
|  |         Task<string?> GetUltimoPeriodoFacturadoAsync(); | ||||||
|  |         Task<IEnumerable<Factura>> GetFacturasPagadasPendientesDeFacturar(string periodo); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,10 @@ | |||||||
|  | using GestionIntegral.Api.Models.Suscripciones; | ||||||
|  |  | ||||||
|  | namespace GestionIntegral.Api.Data.Repositories.Suscripciones | ||||||
|  | { | ||||||
|  |     public interface IFormaPagoRepository | ||||||
|  |     { | ||||||
|  |         Task<IEnumerable<FormaPago>> GetAllAsync(); | ||||||
|  |         Task<FormaPago?> GetByIdAsync(int id); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,10 @@ | |||||||
|  | using GestionIntegral.Api.Models.Suscripciones; | ||||||
|  | using System.Data; | ||||||
|  |  | ||||||
|  | namespace GestionIntegral.Api.Data.Repositories.Suscripciones | ||||||
|  | { | ||||||
|  |   public interface ILoteDebitoRepository | ||||||
|  |   { | ||||||
|  |     Task<LoteDebito?> CreateAsync(LoteDebito nuevoLote, IDbTransaction transaction); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -0,0 +1,12 @@ | |||||||
|  | using GestionIntegral.Api.Models.Suscripciones; | ||||||
|  | using System.Data; | ||||||
|  |  | ||||||
|  | namespace GestionIntegral.Api.Data.Repositories.Suscripciones | ||||||
|  | { | ||||||
|  |     public interface IPagoRepository | ||||||
|  |     { | ||||||
|  |         Task<IEnumerable<Pago>> GetByFacturaIdAsync(int idFactura); | ||||||
|  |         Task<Pago?> CreateAsync(Pago nuevoPago, IDbTransaction transaction); | ||||||
|  |         Task<decimal> GetTotalPagadoAprobadoAsync(int idFactura, IDbTransaction transaction); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,15 @@ | |||||||
|  | using GestionIntegral.Api.Models.Suscripciones; | ||||||
|  | using System.Data; | ||||||
|  |  | ||||||
|  | namespace GestionIntegral.Api.Data.Repositories.Suscripciones | ||||||
|  | { | ||||||
|  |     public interface IPromocionRepository | ||||||
|  |     { | ||||||
|  |         Task<IEnumerable<Promocion>> GetAllAsync(bool soloActivas); | ||||||
|  |         Task<Promocion?> GetByIdAsync(int id); | ||||||
|  |         Task<Promocion?> CreateAsync(Promocion nuevaPromocion, IDbTransaction transaction); | ||||||
|  |         Task<bool> UpdateAsync(Promocion promocion, IDbTransaction transaction); | ||||||
|  |         Task<IEnumerable<Promocion>> GetPromocionesActivasParaSuscripcion(int idSuscripcion, DateTime fechaPeriodo, IDbTransaction transaction); | ||||||
|  |         Task<IEnumerable<Promocion>> GetPromocionesActivasParaSuscripcion(int idSuscripcion, DateTime fechaPeriodo); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,17 @@ | |||||||
|  | using GestionIntegral.Api.Models.Suscripciones; | ||||||
|  | using System.Data; | ||||||
|  |  | ||||||
|  | namespace GestionIntegral.Api.Data.Repositories.Suscripciones | ||||||
|  | { | ||||||
|  |     public interface ISuscripcionRepository | ||||||
|  |     { | ||||||
|  |         Task<Suscripcion?> GetByIdAsync(int idSuscripcion); | ||||||
|  |         Task<IEnumerable<Suscripcion>> GetBySuscriptorIdAsync(int idSuscriptor); | ||||||
|  |         Task<IEnumerable<Suscripcion>> GetAllActivasParaFacturacion(string periodo, IDbTransaction transaction); | ||||||
|  |         Task<Suscripcion?> CreateAsync(Suscripcion nuevaSuscripcion, IDbTransaction transaction); | ||||||
|  |         Task<bool> UpdateAsync(Suscripcion suscripcionAActualizar, IDbTransaction transaction); | ||||||
|  |         Task<IEnumerable<(SuscripcionPromocion Asignacion, Promocion Promocion)>> GetPromocionesAsignadasBySuscripcionIdAsync(int idSuscripcion); | ||||||
|  |         Task AsignarPromocionAsync(SuscripcionPromocion asignacion, IDbTransaction transaction); | ||||||
|  |         Task<bool> QuitarPromocionAsync(int idSuscripcion, int idPromocion, IDbTransaction transaction); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,16 @@ | |||||||
|  | using GestionIntegral.Api.Models.Suscripciones; | ||||||
|  | using System.Data; | ||||||
|  |  | ||||||
|  | namespace GestionIntegral.Api.Data.Repositories.Suscripciones | ||||||
|  | { | ||||||
|  |     public interface ISuscriptorRepository | ||||||
|  |     { | ||||||
|  |         Task<IEnumerable<Suscriptor>> GetAllAsync(string? nombreFilter, string? nroDocFilter, bool soloActivos); | ||||||
|  |         Task<Suscriptor?> GetByIdAsync(int id); | ||||||
|  |         Task<Suscriptor?> CreateAsync(Suscriptor nuevoSuscriptor, IDbTransaction transaction); | ||||||
|  |         Task<bool> UpdateAsync(Suscriptor suscriptorAActualizar, IDbTransaction transaction); | ||||||
|  |         Task<bool> ToggleActivoAsync(int id, bool activar, int idUsuario, IDbTransaction transaction); | ||||||
|  |         Task<bool> ExistsByDocumentoAsync(string tipoDocumento, string nroDocumento, int? excludeId = null); | ||||||
|  |         Task<bool> IsInUseAsync(int id); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,43 @@ | |||||||
|  | using Dapper; | ||||||
|  | using GestionIntegral.Api.Models.Suscripciones; | ||||||
|  | using System.Data; | ||||||
|  |  | ||||||
|  | namespace GestionIntegral.Api.Data.Repositories.Suscripciones | ||||||
|  | { | ||||||
|  |     public class LoteDebitoRepository : ILoteDebitoRepository | ||||||
|  |     { | ||||||
|  |         private readonly DbConnectionFactory _connectionFactory; | ||||||
|  |         private readonly ILogger<LoteDebitoRepository> _logger; | ||||||
|  |  | ||||||
|  |         public LoteDebitoRepository(DbConnectionFactory connectionFactory, ILogger<LoteDebitoRepository> logger) | ||||||
|  |         { | ||||||
|  |             _connectionFactory = connectionFactory; | ||||||
|  |             _logger = logger; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public async Task<LoteDebito?> CreateAsync(LoteDebito nuevoLote, IDbTransaction transaction) | ||||||
|  |         { | ||||||
|  |             if (transaction == null || transaction.Connection == null) | ||||||
|  |             { | ||||||
|  |                 throw new ArgumentNullException(nameof(transaction), "La transacción o su conexión no pueden ser nulas."); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             const string sqlInsert = @" | ||||||
|  |                 INSERT INTO dbo.susc_LotesDebito | ||||||
|  |                     (FechaGeneracion, Periodo, NombreArchivo, ImporteTotal, CantidadRegistros, IdUsuarioGeneracion) | ||||||
|  |                 OUTPUT INSERTED.* | ||||||
|  |                 VALUES | ||||||
|  |                     (GETDATE(), @Periodo, @NombreArchivo, @ImporteTotal, @CantidadRegistros, @IdUsuarioGeneracion);"; | ||||||
|  |  | ||||||
|  |             try | ||||||
|  |             { | ||||||
|  |                 return await transaction.Connection.QuerySingleAsync<LoteDebito>(sqlInsert, nuevoLote, transaction); | ||||||
|  |             } | ||||||
|  |             catch (Exception ex) | ||||||
|  |             { | ||||||
|  |                 _logger.LogError(ex, "Error al crear el registro de LoteDebito para el período {Periodo}", nuevoLote.Periodo); | ||||||
|  |                 return null; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,68 @@ | |||||||
|  | using Dapper; | ||||||
|  | using GestionIntegral.Api.Models.Suscripciones; | ||||||
|  | using System.Data; | ||||||
|  |  | ||||||
|  | namespace GestionIntegral.Api.Data.Repositories.Suscripciones | ||||||
|  | { | ||||||
|  |     public class PagoRepository : IPagoRepository | ||||||
|  |     { | ||||||
|  |         private readonly DbConnectionFactory _connectionFactory; | ||||||
|  |         private readonly ILogger<PagoRepository> _logger; | ||||||
|  |  | ||||||
|  |         public PagoRepository(DbConnectionFactory connectionFactory, ILogger<PagoRepository> logger) | ||||||
|  |         { | ||||||
|  |             _connectionFactory = connectionFactory; | ||||||
|  |             _logger = logger; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public async Task<IEnumerable<Pago>> GetByFacturaIdAsync(int idFactura) | ||||||
|  |         { | ||||||
|  |             const string sql = "SELECT * FROM dbo.susc_Pagos WHERE IdFactura = @IdFactura ORDER BY FechaPago DESC;"; | ||||||
|  |             try | ||||||
|  |             { | ||||||
|  |                 using var connection = _connectionFactory.CreateConnection(); | ||||||
|  |                 return await connection.QueryAsync<Pago>(sql, new { idFactura }); | ||||||
|  |             } | ||||||
|  |             catch (Exception ex) | ||||||
|  |             { | ||||||
|  |                 _logger.LogError(ex, "Error al obtener pagos para la factura ID: {IdFactura}", idFactura); | ||||||
|  |                 return Enumerable.Empty<Pago>(); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public async Task<Pago?> CreateAsync(Pago nuevoPago, IDbTransaction transaction) | ||||||
|  |         { | ||||||
|  |             if (transaction == null || transaction.Connection == null) | ||||||
|  |             { | ||||||
|  |                 throw new ArgumentNullException(nameof(transaction), "La transacción o su conexión no pueden ser nulas."); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             const string sqlInsert = @" | ||||||
|  |                 INSERT INTO dbo.susc_Pagos | ||||||
|  |                     (IdFactura, FechaPago, IdFormaPago, Monto, Estado, Referencia, Observaciones, IdUsuarioRegistro) | ||||||
|  |                 OUTPUT INSERTED.* | ||||||
|  |                 VALUES | ||||||
|  |                     (@IdFactura, @FechaPago, @IdFormaPago, @Monto, @Estado, @Referencia, @Observaciones, @IdUsuarioRegistro);"; | ||||||
|  |  | ||||||
|  |             try | ||||||
|  |             { | ||||||
|  |                 return await transaction.Connection.QuerySingleAsync<Pago>(sqlInsert, nuevoPago, transaction); | ||||||
|  |             } | ||||||
|  |             catch (Exception ex) | ||||||
|  |             { | ||||||
|  |                 _logger.LogError(ex, "Error al registrar un nuevo Pago para la Factura ID: {IdFactura}", nuevoPago.IdFactura); | ||||||
|  |                 return null; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public async Task<decimal> GetTotalPagadoAprobadoAsync(int idFactura, IDbTransaction transaction) | ||||||
|  |         { | ||||||
|  |             if (transaction == null || transaction.Connection == null) | ||||||
|  |             { | ||||||
|  |                 throw new ArgumentNullException(nameof(transaction), "La transacción o su conexión no pueden ser nulas."); | ||||||
|  |             } | ||||||
|  |             const string sql = "SELECT ISNULL(SUM(Monto), 0) FROM dbo.susc_Pagos WHERE IdFactura = @IdFactura AND Estado = 'Aprobado';"; | ||||||
|  |             return await transaction.Connection.ExecuteScalarAsync<decimal>(sql, new { idFactura }, transaction); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,118 @@ | |||||||
|  | using Dapper; | ||||||
|  | using GestionIntegral.Api.Models.Suscripciones; | ||||||
|  | using System.Data; | ||||||
|  | using System.Text; | ||||||
|  |  | ||||||
|  | namespace GestionIntegral.Api.Data.Repositories.Suscripciones | ||||||
|  | { | ||||||
|  |     public class PromocionRepository : IPromocionRepository | ||||||
|  |     { | ||||||
|  |         private readonly DbConnectionFactory _connectionFactory; | ||||||
|  |         private readonly ILogger<PromocionRepository> _logger; | ||||||
|  |  | ||||||
|  |         public PromocionRepository(DbConnectionFactory factory, ILogger<PromocionRepository> logger) | ||||||
|  |         { | ||||||
|  |             _connectionFactory = factory; | ||||||
|  |             _logger = logger; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public async Task<IEnumerable<Promocion>> GetAllAsync(bool soloActivas) | ||||||
|  |         { | ||||||
|  |             var sql = new StringBuilder("SELECT * FROM dbo.susc_Promociones"); | ||||||
|  |             if (soloActivas) | ||||||
|  |             { | ||||||
|  |                 sql.Append(" WHERE Activa = 1"); | ||||||
|  |             } | ||||||
|  |             sql.Append(" ORDER BY FechaInicio DESC;"); | ||||||
|  |  | ||||||
|  |             using var connection = _connectionFactory.CreateConnection(); | ||||||
|  |             return await connection.QueryAsync<Promocion>(sql.ToString()); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public async Task<Promocion?> GetByIdAsync(int id) | ||||||
|  |         { | ||||||
|  |             const string sql = "SELECT * FROM dbo.susc_Promociones WHERE IdPromocion = @Id;"; | ||||||
|  |             using var connection = _connectionFactory.CreateConnection(); | ||||||
|  |             return await connection.QuerySingleOrDefaultAsync<Promocion>(sql, new { Id = id }); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public async Task<Promocion?> CreateAsync(Promocion nuevaPromocion, IDbTransaction transaction) | ||||||
|  |         { | ||||||
|  |             const string sql = @" | ||||||
|  |                 INSERT INTO dbo.susc_Promociones | ||||||
|  |                     (Descripcion, TipoEfecto, ValorEfecto, TipoCondicion, ValorCondicion, | ||||||
|  |                     FechaInicio, FechaFin, Activa, IdUsuarioAlta, FechaAlta) | ||||||
|  |                 OUTPUT INSERTED.* | ||||||
|  |                 VALUES (@Descripcion, @TipoEfecto, @ValorEfecto, @TipoCondicion, | ||||||
|  |                         @ValorCondicion, @FechaInicio, @FechaFin, @Activa, @IdUsuarioAlta, GETDATE());"; | ||||||
|  |             if (transaction?.Connection == null) | ||||||
|  |             { | ||||||
|  |                 throw new ArgumentNullException(nameof(transaction.Connection), "La conexión de la transacción no puede ser nula."); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             return await transaction.Connection.QuerySingleAsync<Promocion>(sql, nuevaPromocion, transaction); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public async Task<bool> UpdateAsync(Promocion promocion, IDbTransaction transaction) | ||||||
|  |         { | ||||||
|  |             const string sql = @" | ||||||
|  |                 UPDATE dbo.susc_Promociones SET | ||||||
|  |                     Descripcion = @Descripcion, | ||||||
|  |                     TipoPromocion = @TipoPromocion, | ||||||
|  |                     Valor = @Valor, | ||||||
|  |                     FechaInicio = @FechaInicio, | ||||||
|  |                     FechaFin = @FechaFin, | ||||||
|  |                     Activa = @Activa | ||||||
|  |                 WHERE IdPromocion = @IdPromocion;"; | ||||||
|  |  | ||||||
|  |             if (transaction?.Connection == null) | ||||||
|  |             { | ||||||
|  |                 throw new ArgumentNullException(nameof(transaction.Connection), "La conexión de la transacción no puede ser nula."); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             var rows = await transaction.Connection.ExecuteAsync(sql, promocion, transaction); | ||||||
|  |             return rows == 1; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public async Task<IEnumerable<Promocion>> GetPromocionesActivasParaSuscripcion(int idSuscripcion, DateTime fechaPeriodo, IDbTransaction transaction) | ||||||
|  |         { | ||||||
|  |             // Esta consulta ahora es más compleja para respetar ambas vigencias. | ||||||
|  |             const string sql = @" | ||||||
|  |                 SELECT p.*  | ||||||
|  |                 FROM dbo.susc_Promociones p | ||||||
|  |                 JOIN dbo.susc_SuscripcionPromociones sp ON p.IdPromocion = sp.IdPromocion | ||||||
|  |                 WHERE sp.IdSuscripcion = @IdSuscripcion | ||||||
|  |                 AND p.Activa = 1 | ||||||
|  |                 -- 1. La promoción general debe estar activa en el período | ||||||
|  |                 AND p.FechaInicio <= @FechaPeriodo | ||||||
|  |                 AND (p.FechaFin IS NULL OR p.FechaFin >= @FechaPeriodo) | ||||||
|  |                 -- 2. La asignación específica al cliente debe estar activa en el período | ||||||
|  |                 AND sp.VigenciaDesde <= @FechaPeriodo | ||||||
|  |                 AND (sp.VigenciaHasta IS NULL OR sp.VigenciaHasta >= @FechaPeriodo);"; | ||||||
|  |             if (transaction?.Connection == null) | ||||||
|  |             { | ||||||
|  |                 throw new ArgumentNullException(nameof(transaction.Connection), "La conexión de la transacción no puede ser nula."); | ||||||
|  |             } | ||||||
|  |             return await transaction.Connection.QueryAsync<Promocion>(sql, new { IdSuscripcion = idSuscripcion, FechaPeriodo = fechaPeriodo }, transaction); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Versión SIN transacción, para solo lectura | ||||||
|  |         public async Task<IEnumerable<Promocion>> GetPromocionesActivasParaSuscripcion(int idSuscripcion, DateTime fechaPeriodo) | ||||||
|  |         { | ||||||
|  |             const string sql = @" | ||||||
|  |                 SELECT p.*  | ||||||
|  |                 FROM dbo.susc_Promociones p | ||||||
|  |                 JOIN dbo.susc_SuscripcionPromociones sp ON p.IdPromocion = sp.IdPromocion | ||||||
|  |                 WHERE sp.IdSuscripcion = @IdSuscripcion | ||||||
|  |                 AND p.Activa = 1 | ||||||
|  |                 -- 1. La promoción general debe estar activa en el período | ||||||
|  |                 AND p.FechaInicio <= @FechaPeriodo | ||||||
|  |                 AND (p.FechaFin IS NULL OR p.FechaFin >= @FechaPeriodo) | ||||||
|  |                 -- 2. La asignación específica al cliente debe estar activa en el período | ||||||
|  |                 AND sp.VigenciaDesde <= @FechaPeriodo | ||||||
|  |                 AND (sp.VigenciaHasta IS NULL OR sp.VigenciaHasta >= @FechaPeriodo);"; | ||||||
|  |             using var connection = _connectionFactory.CreateConnection(); | ||||||
|  |             return await connection.QueryAsync<Promocion>(sql, new { idSuscripcion, FechaPeriodo = fechaPeriodo }); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,156 @@ | |||||||
|  | using Dapper; | ||||||
|  | using GestionIntegral.Api.Models.Suscripciones; | ||||||
|  | using System.Data; | ||||||
|  |  | ||||||
|  | namespace GestionIntegral.Api.Data.Repositories.Suscripciones | ||||||
|  | { | ||||||
|  |     public class SuscripcionRepository : ISuscripcionRepository | ||||||
|  |     { | ||||||
|  |         private readonly DbConnectionFactory _connectionFactory; | ||||||
|  |         private readonly ILogger<SuscripcionRepository> _logger; | ||||||
|  |  | ||||||
|  |         public SuscripcionRepository(DbConnectionFactory connectionFactory, ILogger<SuscripcionRepository> logger) | ||||||
|  |         { | ||||||
|  |             _connectionFactory = connectionFactory; | ||||||
|  |             _logger = logger; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public async Task<Suscripcion?> GetByIdAsync(int idSuscripcion) | ||||||
|  |         { | ||||||
|  |             const string sql = "SELECT * FROM dbo.susc_Suscripciones WHERE IdSuscripcion = @IdSuscripcion;"; | ||||||
|  |             try | ||||||
|  |             { | ||||||
|  |                 using var connection = _connectionFactory.CreateConnection(); | ||||||
|  |                 return await connection.QuerySingleOrDefaultAsync<Suscripcion>(sql, new { IdSuscripcion = idSuscripcion }); | ||||||
|  |             } | ||||||
|  |             catch (Exception ex) | ||||||
|  |             { | ||||||
|  |                 _logger.LogError(ex, "Error al obtener Suscripción por ID: {IdSuscripcion}", idSuscripcion); | ||||||
|  |                 return null; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public async Task<IEnumerable<Suscripcion>> GetBySuscriptorIdAsync(int idSuscriptor) | ||||||
|  |         { | ||||||
|  |             const string sql = "SELECT * FROM dbo.susc_Suscripciones WHERE IdSuscriptor = @IdSuscriptor ORDER BY FechaInicio DESC;"; | ||||||
|  |             try | ||||||
|  |             { | ||||||
|  |                 using var connection = _connectionFactory.CreateConnection(); | ||||||
|  |                 return await connection.QueryAsync<Suscripcion>(sql, new { IdSuscriptor = idSuscriptor }); | ||||||
|  |             } | ||||||
|  |             catch (Exception ex) | ||||||
|  |             { | ||||||
|  |                 _logger.LogError(ex, "Error al obtener suscripciones para el suscriptor ID: {IdSuscriptor}", idSuscriptor); | ||||||
|  |                 return Enumerable.Empty<Suscripcion>(); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         public async Task<IEnumerable<Suscripcion>> GetAllActivasParaFacturacion(string periodo, IDbTransaction transaction) | ||||||
|  |         { | ||||||
|  |             var year = int.Parse(periodo.Split('-')[0]); | ||||||
|  |             var month = int.Parse(periodo.Split('-')[1]); | ||||||
|  |             var primerDiaMes = new DateTime(year, month, 1); | ||||||
|  |             var ultimoDiaMes = primerDiaMes.AddMonths(1).AddDays(-1); | ||||||
|  |  | ||||||
|  |             const string sql = @" | ||||||
|  |                 SELECT s.* | ||||||
|  |                 FROM dbo.susc_Suscripciones s | ||||||
|  |                 JOIN dbo.susc_Suscriptores su ON s.IdSuscriptor = su.IdSuscriptor | ||||||
|  |                 WHERE s.Estado = 'Activa' | ||||||
|  |                   AND su.Activo = 1 | ||||||
|  |                   AND s.FechaInicio <= @UltimoDiaMes | ||||||
|  |                   AND (s.FechaFin IS NULL OR s.FechaFin >= @PrimerDiaMes);"; | ||||||
|  |              | ||||||
|  |             if (transaction == null || transaction.Connection == null) | ||||||
|  |             { | ||||||
|  |                 throw new ArgumentNullException(nameof(transaction), "La transacción o su conexión no pueden ser nulas."); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             return await transaction.Connection.QueryAsync<Suscripcion>(sql, new { PrimerDiaMes = primerDiaMes, UltimoDiaMes = ultimoDiaMes }, transaction); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public async Task<Suscripcion?> CreateAsync(Suscripcion nuevaSuscripcion, IDbTransaction transaction) | ||||||
|  |         { | ||||||
|  |             if (transaction == null || transaction.Connection == null) | ||||||
|  |             { | ||||||
|  |                 throw new ArgumentNullException(nameof(transaction), "La transacción o su conexión no pueden ser nulas."); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             const string sqlInsert = @" | ||||||
|  |                 INSERT INTO dbo.susc_Suscripciones | ||||||
|  |                     (IdSuscriptor, IdPublicacion, FechaInicio, FechaFin, Estado, DiasEntrega,  | ||||||
|  |                      Observaciones, IdUsuarioAlta, FechaAlta) | ||||||
|  |                 OUTPUT INSERTED.* | ||||||
|  |                 VALUES | ||||||
|  |                     (@IdSuscriptor, @IdPublicacion, @FechaInicio, @FechaFin, @Estado, @DiasEntrega,  | ||||||
|  |                      @Observaciones, @IdUsuarioAlta, GETDATE());"; | ||||||
|  |              | ||||||
|  |             return await transaction.Connection.QuerySingleAsync<Suscripcion>(sqlInsert, nuevaSuscripcion, transaction); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public async Task<bool> UpdateAsync(Suscripcion suscripcion, IDbTransaction transaction) | ||||||
|  |         { | ||||||
|  |             if (transaction == null || transaction.Connection == null) | ||||||
|  |             { | ||||||
|  |                 throw new ArgumentNullException(nameof(transaction), "La transacción o su conexión no pueden ser nulas."); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             const string sqlUpdate = @" | ||||||
|  |                 UPDATE dbo.susc_Suscripciones SET | ||||||
|  |                     IdPublicacion = @IdPublicacion, | ||||||
|  |                     FechaInicio = @FechaInicio, | ||||||
|  |                     FechaFin = @FechaFin, | ||||||
|  |                     Estado = @Estado, | ||||||
|  |                     DiasEntrega = @DiasEntrega, | ||||||
|  |                     Observaciones = @Observaciones, | ||||||
|  |                     IdUsuarioMod = @IdUsuarioMod, | ||||||
|  |                     FechaMod = @FechaMod | ||||||
|  |                 WHERE IdSuscripcion = @IdSuscripcion;"; | ||||||
|  |  | ||||||
|  |             var rowsAffected = await transaction.Connection.ExecuteAsync(sqlUpdate, suscripcion, transaction); | ||||||
|  |             return rowsAffected == 1; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public async Task<IEnumerable<(SuscripcionPromocion Asignacion, Promocion Promocion)>> GetPromocionesAsignadasBySuscripcionIdAsync(int idSuscripcion) | ||||||
|  |         { | ||||||
|  |             const string sql = @" | ||||||
|  |                 SELECT sp.*, p.*  | ||||||
|  |                 FROM dbo.susc_SuscripcionPromociones sp | ||||||
|  |                 JOIN dbo.susc_Promociones p ON sp.IdPromocion = p.IdPromocion | ||||||
|  |                 WHERE sp.IdSuscripcion = @IdSuscripcion;"; | ||||||
|  |              | ||||||
|  |             using var connection = _connectionFactory.CreateConnection(); | ||||||
|  |             var result = await connection.QueryAsync<SuscripcionPromocion, Promocion, (SuscripcionPromocion, Promocion)>( | ||||||
|  |                 sql,  | ||||||
|  |                 (asignacion, promocion) => (asignacion, promocion), | ||||||
|  |                 new { IdSuscripcion = idSuscripcion }, | ||||||
|  |                 splitOn: "IdPromocion" | ||||||
|  |             ); | ||||||
|  |             return result; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public async Task AsignarPromocionAsync(SuscripcionPromocion asignacion, IDbTransaction transaction) | ||||||
|  |         { | ||||||
|  |             if (transaction == null || transaction.Connection == null) | ||||||
|  |             { | ||||||
|  |                 throw new ArgumentNullException(nameof(transaction), "La transacción o su conexión no pueden ser nulas."); | ||||||
|  |             } | ||||||
|  |             const string sql = @" | ||||||
|  |                 INSERT INTO dbo.susc_SuscripcionPromociones (IdSuscripcion, IdPromocion, IdUsuarioAsigno, VigenciaDesde, VigenciaHasta, FechaAsignacion) | ||||||
|  |                 VALUES (@IdSuscripcion, @IdPromocion, @IdUsuarioAsigno, @VigenciaDesde, @VigenciaHasta, GETDATE());"; | ||||||
|  |              | ||||||
|  |             await transaction.Connection.ExecuteAsync(sql, asignacion, transaction); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public async Task<bool> QuitarPromocionAsync(int idSuscripcion, int idPromocion, IDbTransaction transaction) | ||||||
|  |         { | ||||||
|  |             if (transaction == null || transaction.Connection == null) | ||||||
|  |             { | ||||||
|  |                 throw new ArgumentNullException(nameof(transaction), "La transacción o su conexión no pueden ser nulas."); | ||||||
|  |             } | ||||||
|  |             const string sql = "DELETE FROM dbo.susc_SuscripcionPromociones WHERE IdSuscripcion = @IdSuscripcion AND IdPromocion = @IdPromocion;"; | ||||||
|  |             var rows = await transaction.Connection.ExecuteAsync(sql, new { idSuscripcion, idPromocion }, transaction); | ||||||
|  |             return rows == 1; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,195 @@ | |||||||
|  | using Dapper; | ||||||
|  | using GestionIntegral.Api.Models.Suscripciones; | ||||||
|  | using System.Data; | ||||||
|  | using System.Text; | ||||||
|  |  | ||||||
|  | namespace GestionIntegral.Api.Data.Repositories.Suscripciones | ||||||
|  | { | ||||||
|  |     public class SuscriptorRepository : ISuscriptorRepository | ||||||
|  |     { | ||||||
|  |         private readonly DbConnectionFactory _connectionFactory; | ||||||
|  |         private readonly ILogger<SuscriptorRepository> _logger; | ||||||
|  |  | ||||||
|  |         public SuscriptorRepository(DbConnectionFactory connectionFactory, ILogger<SuscriptorRepository> logger) | ||||||
|  |         { | ||||||
|  |             _connectionFactory = connectionFactory; | ||||||
|  |             _logger = logger; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public async Task<IEnumerable<Suscriptor>> GetAllAsync(string? nombreFilter, string? nroDocFilter, bool soloActivos) | ||||||
|  |         { | ||||||
|  |             var sqlBuilder = new StringBuilder(@" | ||||||
|  |                 SELECT IdSuscriptor, NombreCompleto, Email, Telefono, Direccion, TipoDocumento,  | ||||||
|  |                        NroDocumento, CBU, IdFormaPagoPreferida, Observaciones, Activo,  | ||||||
|  |                        IdUsuarioAlta, FechaAlta, IdUsuarioMod, FechaMod | ||||||
|  |                 FROM dbo.susc_Suscriptores WHERE 1=1"); | ||||||
|  |              | ||||||
|  |             var parameters = new DynamicParameters(); | ||||||
|  |  | ||||||
|  |             if (soloActivos) | ||||||
|  |             { | ||||||
|  |                 sqlBuilder.Append(" AND Activo = 1"); | ||||||
|  |             } | ||||||
|  |             if (!string.IsNullOrWhiteSpace(nombreFilter)) | ||||||
|  |             { | ||||||
|  |                 sqlBuilder.Append(" AND NombreCompleto LIKE @NombreFilter"); | ||||||
|  |                 parameters.Add("NombreFilter", $"%{nombreFilter}%"); | ||||||
|  |             } | ||||||
|  |             if (!string.IsNullOrWhiteSpace(nroDocFilter)) | ||||||
|  |             { | ||||||
|  |                 sqlBuilder.Append(" AND NroDocumento LIKE @NroDocFilter"); | ||||||
|  |                 parameters.Add("NroDocFilter", $"%{nroDocFilter}%"); | ||||||
|  |             } | ||||||
|  |             sqlBuilder.Append(" ORDER BY NombreCompleto;"); | ||||||
|  |  | ||||||
|  |             try | ||||||
|  |             { | ||||||
|  |                 using var connection = _connectionFactory.CreateConnection(); | ||||||
|  |                 return await connection.QueryAsync<Suscriptor>(sqlBuilder.ToString(), parameters); | ||||||
|  |             } | ||||||
|  |             catch (Exception ex) | ||||||
|  |             { | ||||||
|  |                 _logger.LogError(ex, "Error al obtener todos los Suscriptores."); | ||||||
|  |                 return Enumerable.Empty<Suscriptor>(); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public async Task<Suscriptor?> GetByIdAsync(int id) | ||||||
|  |         { | ||||||
|  |             const string sql = @" | ||||||
|  |                 SELECT IdSuscriptor, NombreCompleto, Email, Telefono, Direccion, TipoDocumento,  | ||||||
|  |                        NroDocumento, CBU, IdFormaPagoPreferida, Observaciones, Activo,  | ||||||
|  |                        IdUsuarioAlta, FechaAlta, IdUsuarioMod, FechaMod | ||||||
|  |                 FROM dbo.susc_Suscriptores WHERE IdSuscriptor = @Id;"; | ||||||
|  |             try | ||||||
|  |             { | ||||||
|  |                 using var connection = _connectionFactory.CreateConnection(); | ||||||
|  |                 return await connection.QuerySingleOrDefaultAsync<Suscriptor>(sql, new { Id = id }); | ||||||
|  |             } | ||||||
|  |             catch (Exception ex) | ||||||
|  |             { | ||||||
|  |                 _logger.LogError(ex, "Error al obtener Suscriptor por ID: {IdSuscriptor}", id); | ||||||
|  |                 return null; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         public async Task<bool> ExistsByDocumentoAsync(string tipoDocumento, string nroDocumento, int? excludeId = null) | ||||||
|  |         { | ||||||
|  |             var sqlBuilder = new StringBuilder("SELECT COUNT(1) FROM dbo.susc_Suscriptores WHERE TipoDocumento = @TipoDocumento AND NroDocumento = @NroDocumento"); | ||||||
|  |             var parameters = new DynamicParameters(); | ||||||
|  |             parameters.Add("TipoDocumento", tipoDocumento); | ||||||
|  |             parameters.Add("NroDocumento", nroDocumento); | ||||||
|  |  | ||||||
|  |             if (excludeId.HasValue) | ||||||
|  |             { | ||||||
|  |                 sqlBuilder.Append(" AND IdSuscriptor != @ExcludeId"); | ||||||
|  |                 parameters.Add("ExcludeId", excludeId.Value); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             try | ||||||
|  |             { | ||||||
|  |                 using var connection = _connectionFactory.CreateConnection(); | ||||||
|  |                 return await connection.ExecuteScalarAsync<bool>(sqlBuilder.ToString(), parameters); | ||||||
|  |             } | ||||||
|  |             catch (Exception ex) | ||||||
|  |             { | ||||||
|  |                 _logger.LogError(ex, "Error en ExistsByDocumentoAsync para Suscriptor."); | ||||||
|  |                 return true; // Asumir que existe si hay error para prevenir duplicados. | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public async Task<bool> IsInUseAsync(int id) | ||||||
|  |         { | ||||||
|  |             // Un suscriptor está en uso si tiene suscripciones activas. | ||||||
|  |             const string sql = "SELECT TOP 1 1 FROM dbo.susc_Suscripciones WHERE IdSuscriptor = @Id AND Estado = 'Activa'"; | ||||||
|  |             try | ||||||
|  |             { | ||||||
|  |                 using var connection = _connectionFactory.CreateConnection(); | ||||||
|  |                 var inUse = await connection.ExecuteScalarAsync<int?>(sql, new { Id = id }); | ||||||
|  |                 return inUse.HasValue && inUse.Value == 1; | ||||||
|  |             } | ||||||
|  |             catch (Exception ex) | ||||||
|  |             { | ||||||
|  |                 _logger.LogError(ex, "Error en IsInUseAsync para Suscriptor ID: {IdSuscriptor}", id); | ||||||
|  |                 return true; // Asumir en uso si hay error. | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public async Task<Suscriptor?> CreateAsync(Suscriptor nuevoSuscriptor, IDbTransaction transaction) | ||||||
|  |         { | ||||||
|  |             if (transaction == null || transaction.Connection == null) | ||||||
|  |             { | ||||||
|  |                 throw new ArgumentNullException(nameof(transaction), "La transacción o su conexión no pueden ser nulas."); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             const string sqlInsert = @" | ||||||
|  |                 INSERT INTO dbo.susc_Suscriptores  | ||||||
|  |                     (NombreCompleto, Email, Telefono, Direccion, TipoDocumento, NroDocumento,  | ||||||
|  |                      CBU, IdFormaPagoPreferida, Observaciones, Activo, IdUsuarioAlta, FechaAlta) | ||||||
|  |                 OUTPUT INSERTED.* | ||||||
|  |                 VALUES  | ||||||
|  |                     (@NombreCompleto, @Email, @Telefono, @Direccion, @TipoDocumento, @NroDocumento,  | ||||||
|  |                      @CBU, @IdFormaPagoPreferida, @Observaciones, 1, @IdUsuarioAlta, GETDATE());"; | ||||||
|  |  | ||||||
|  |             var suscriptorCreado = await transaction.Connection.QuerySingleAsync<Suscriptor>( | ||||||
|  |                 sqlInsert, | ||||||
|  |                 nuevoSuscriptor, | ||||||
|  |                 transaction: transaction | ||||||
|  |             ); | ||||||
|  |  | ||||||
|  |             // No se necesita historial para la creación, ya que la propia tabla tiene campos de auditoría. | ||||||
|  |             // Si se necesitara una tabla _H, aquí iría el INSERT a esa tabla. | ||||||
|  |  | ||||||
|  |             return suscriptorCreado; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public async Task<bool> UpdateAsync(Suscriptor suscriptor, IDbTransaction transaction) | ||||||
|  |         { | ||||||
|  |             // El servicio ya ha poblado IdUsuarioMod y FechaMod en la entidad. | ||||||
|  |             const string sqlUpdate = @" | ||||||
|  |                 UPDATE dbo.susc_Suscriptores SET | ||||||
|  |                     NombreCompleto = @NombreCompleto, | ||||||
|  |                     Email = @Email, | ||||||
|  |                     Telefono = @Telefono, | ||||||
|  |                     Direccion = @Direccion, | ||||||
|  |                     TipoDocumento = @TipoDocumento, | ||||||
|  |                     NroDocumento = @NroDocumento, | ||||||
|  |                     CBU = @CBU, | ||||||
|  |                     IdFormaPagoPreferida = @IdFormaPagoPreferida, | ||||||
|  |                     Observaciones = @Observaciones, | ||||||
|  |                     IdUsuarioMod = @IdUsuarioMod, | ||||||
|  |                     FechaMod = @FechaMod | ||||||
|  |                 WHERE IdSuscriptor = @IdSuscriptor;"; | ||||||
|  |  | ||||||
|  |             if (transaction == null || transaction.Connection == null) | ||||||
|  |             { | ||||||
|  |                 throw new ArgumentNullException(nameof(transaction), "La transacción o su conexión no pueden ser nulas."); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             var rowsAffected = await transaction.Connection.ExecuteAsync(sqlUpdate, suscriptor, transaction: transaction); | ||||||
|  |             return rowsAffected == 1; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public async Task<bool> ToggleActivoAsync(int id, bool activar, int idUsuario, IDbTransaction transaction) | ||||||
|  |         { | ||||||
|  |             const string sqlToggle = @" | ||||||
|  |                 UPDATE dbo.susc_Suscriptores SET | ||||||
|  |                     Activo = @Activar, | ||||||
|  |                     IdUsuarioMod = @IdUsuario, | ||||||
|  |                     FechaMod = GETDATE() | ||||||
|  |                 WHERE IdSuscriptor = @Id;"; | ||||||
|  |  | ||||||
|  |             if (transaction == null || transaction.Connection == null) | ||||||
|  |             { | ||||||
|  |                 throw new ArgumentNullException(nameof(transaction), "La transacción o su conexión no pueden ser nulas."); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             var rowsAffected = await transaction.Connection.ExecuteAsync( | ||||||
|  |                 sqlToggle,  | ||||||
|  |                 new { Activar = activar, IdUsuario = idUsuario, Id = id },  | ||||||
|  |                 transaction: transaction | ||||||
|  |             ); | ||||||
|  |             return rowsAffected == 1; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,3 +1,5 @@ | |||||||
|  | // Archivo: GestionIntegral.Api/Data/Repositories/Usuarios/IUsuarioRepository.cs | ||||||
|  |  | ||||||
| using GestionIntegral.Api.Models.Usuarios; // Para Usuario | using GestionIntegral.Api.Models.Usuarios; // Para Usuario | ||||||
| using GestionIntegral.Api.Dtos.Usuarios.Auditoria; | using GestionIntegral.Api.Dtos.Usuarios.Auditoria; | ||||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||||
| @@ -10,6 +12,7 @@ namespace GestionIntegral.Api.Data.Repositories.Usuarios | |||||||
|     { |     { | ||||||
|         Task<IEnumerable<Usuario>> GetAllAsync(string? userFilter, string? nombreFilter); |         Task<IEnumerable<Usuario>> GetAllAsync(string? userFilter, string? nombreFilter); | ||||||
|         Task<Usuario?> GetByIdAsync(int id); |         Task<Usuario?> GetByIdAsync(int id); | ||||||
|  |         Task<IEnumerable<Usuario>> GetByIdsAsync(IEnumerable<int> ids); | ||||||
|         Task<Usuario?> GetByUsernameAsync(string username); // Ya existe en IAuthRepository, pero lo duplicamos para cohesión del CRUD |         Task<Usuario?> GetByUsernameAsync(string username); // Ya existe en IAuthRepository, pero lo duplicamos para cohesión del CRUD | ||||||
|         Task<Usuario?> CreateAsync(Usuario nuevoUsuario, int idUsuarioCreador, IDbTransaction transaction); |         Task<Usuario?> CreateAsync(Usuario nuevoUsuario, int idUsuarioCreador, IDbTransaction transaction); | ||||||
|         Task<bool> UpdateAsync(Usuario usuarioAActualizar, int idUsuarioModificador, IDbTransaction transaction); |         Task<bool> UpdateAsync(Usuario usuarioAActualizar, int idUsuarioModificador, IDbTransaction transaction); | ||||||
| @@ -17,7 +20,6 @@ namespace GestionIntegral.Api.Data.Repositories.Usuarios | |||||||
|         // Task<bool> DeleteAsync(int id, int idUsuarioModificador, IDbTransaction transaction); |         // Task<bool> DeleteAsync(int id, int idUsuarioModificador, IDbTransaction transaction); | ||||||
|         Task<bool> SetPasswordAsync(int userId, string newHash, string newSalt, bool debeCambiarClave, int idUsuarioModificador, IDbTransaction transaction); |         Task<bool> SetPasswordAsync(int userId, string newHash, string newSalt, bool debeCambiarClave, int idUsuarioModificador, IDbTransaction transaction); | ||||||
|         Task<bool> UserExistsAsync(string username, int? excludeId = null); |         Task<bool> UserExistsAsync(string username, int? excludeId = null); | ||||||
|         // Para el DTO de listado |  | ||||||
|         Task<IEnumerable<(Usuario Usuario, string NombrePerfil)>> GetAllWithProfileNameAsync(string? userFilter, string? nombreFilter); |         Task<IEnumerable<(Usuario Usuario, string NombrePerfil)>> GetAllWithProfileNameAsync(string? userFilter, string? nombreFilter); | ||||||
|         Task<(Usuario? Usuario, string? NombrePerfil)> GetByIdWithProfileNameAsync(int id); |         Task<(Usuario? Usuario, string? NombrePerfil)> GetByIdWithProfileNameAsync(int id); | ||||||
|         Task<IEnumerable<UsuarioHistorialDto>> GetHistorialByUsuarioIdAsync(int idUsuarioAfectado, DateTime? fechaDesde, DateTime? fechaHasta); |         Task<IEnumerable<UsuarioHistorialDto>> GetHistorialByUsuarioIdAsync(int idUsuarioAfectado, DateTime? fechaDesde, DateTime? fechaHasta); | ||||||
|   | |||||||
| @@ -1,12 +1,8 @@ | |||||||
| using Dapper; | using Dapper; | ||||||
| using GestionIntegral.Api.Models.Usuarios; | using GestionIntegral.Api.Models.Usuarios; | ||||||
| using GestionIntegral.Api.Dtos.Usuarios.Auditoria; | using GestionIntegral.Api.Dtos.Usuarios.Auditoria; | ||||||
| using Microsoft.Extensions.Logging; |  | ||||||
| using System.Collections.Generic; |  | ||||||
| using System.Data; | using System.Data; | ||||||
| using System.Linq; |  | ||||||
| using System.Text; | using System.Text; | ||||||
| using System.Threading.Tasks; |  | ||||||
|  |  | ||||||
| namespace GestionIntegral.Api.Data.Repositories.Usuarios | namespace GestionIntegral.Api.Data.Repositories.Usuarios | ||||||
| { | { | ||||||
| @@ -88,7 +84,6 @@ namespace GestionIntegral.Api.Data.Repositories.Usuarios | |||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |  | ||||||
|         public async Task<Usuario?> GetByIdAsync(int id) |         public async Task<Usuario?> GetByIdAsync(int id) | ||||||
|         { |         { | ||||||
|             const string sql = "SELECT * FROM dbo.gral_Usuarios WHERE Id = @Id"; |             const string sql = "SELECT * FROM dbo.gral_Usuarios WHERE Id = @Id"; | ||||||
| @@ -103,6 +98,33 @@ namespace GestionIntegral.Api.Data.Repositories.Usuarios | |||||||
|                 return null; |                 return null; | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         public async Task<IEnumerable<Usuario>> GetByIdsAsync(IEnumerable<int> ids) | ||||||
|  |         { | ||||||
|  |             // 1. Validar si la lista de IDs está vacía para evitar una consulta innecesaria a la BD. | ||||||
|  |             if (ids == null || !ids.Any()) | ||||||
|  |             { | ||||||
|  |                 return Enumerable.Empty<Usuario>(); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             // 2. Definir la consulta. Dapper manejará la expansión de la cláusula IN de forma segura. | ||||||
|  |             const string sql = "SELECT * FROM dbo.gral_Usuarios WHERE Id IN @Ids"; | ||||||
|  |  | ||||||
|  |             try | ||||||
|  |             { | ||||||
|  |                 // 3. Crear conexión y ejecutar la consulta. | ||||||
|  |                 using var connection = _connectionFactory.CreateConnection(); | ||||||
|  |                 // 4. Pasar la colección de IDs como parámetro. El nombre 'Ids' debe coincidir con el placeholder '@Ids'. | ||||||
|  |                 return await connection.QueryAsync<Usuario>(sql, new { Ids = ids }); | ||||||
|  |             } | ||||||
|  |             catch (Exception ex) | ||||||
|  |             { | ||||||
|  |                 // 5. Registrar el error y devolver una lista vacía en caso de fallo para no romper la aplicación. | ||||||
|  |                 _logger.LogError(ex, "Error al obtener Usuarios por lista de IDs."); | ||||||
|  |                 return Enumerable.Empty<Usuario>(); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|         public async Task<(Usuario? Usuario, string? NombrePerfil)> GetByIdWithProfileNameAsync(int id) |         public async Task<(Usuario? Usuario, string? NombrePerfil)> GetByIdWithProfileNameAsync(int id) | ||||||
|         { |         { | ||||||
|             const string sql = @" |             const string sql = @" | ||||||
| @@ -128,7 +150,6 @@ namespace GestionIntegral.Api.Data.Repositories.Usuarios | |||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |  | ||||||
|         public async Task<Usuario?> GetByUsernameAsync(string username) |         public async Task<Usuario?> GetByUsernameAsync(string username) | ||||||
|         { |         { | ||||||
|             // Esta es la misma que en AuthRepository, si se unifican, se puede eliminar una. |             // Esta es la misma que en AuthRepository, si se unifican, se puede eliminar una. | ||||||
|   | |||||||
| @@ -9,6 +9,8 @@ | |||||||
|   <ItemGroup> |   <ItemGroup> | ||||||
|     <PackageReference Include="AspNetCore.HealthChecks.SqlServer" Version="9.0.0" /> |     <PackageReference Include="AspNetCore.HealthChecks.SqlServer" Version="9.0.0" /> | ||||||
|     <PackageReference Include="Dapper" Version="2.1.66" /> |     <PackageReference Include="Dapper" Version="2.1.66" /> | ||||||
|  |     <PackageReference Include="DotNetEnv" Version="3.1.1" /> | ||||||
|  |     <PackageReference Include="MailKit" Version="4.13.0" /> | ||||||
|     <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.4" /> |     <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.4" /> | ||||||
|     <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.3" /> |     <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.3" /> | ||||||
|     <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" /> |     <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" /> | ||||||
|   | |||||||
| @@ -0,0 +1,16 @@ | |||||||
|  | namespace GestionIntegral.Api.Models.Comunicaciones | ||||||
|  | { | ||||||
|  |     public class EmailLog | ||||||
|  |     { | ||||||
|  |         public int IdEmailLog { get; set; } | ||||||
|  |         public DateTime FechaEnvio { get; set; } | ||||||
|  |         public string DestinatarioEmail { get; set; } = string.Empty; | ||||||
|  |         public string Asunto { get; set; } = string.Empty; | ||||||
|  |         public string Estado { get; set; } = string.Empty; | ||||||
|  |         public string? Error { get; set; } | ||||||
|  |         public int? IdUsuarioDisparo { get; set; } | ||||||
|  |         public string? Origen { get; set; } | ||||||
|  |         public string? ReferenciaId { get; set; } | ||||||
|  |         public int? IdLoteDeEnvio { get; set; } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,16 @@ | |||||||
|  | namespace GestionIntegral.Api.Models.Comunicaciones | ||||||
|  | { | ||||||
|  |     public class LoteDeEnvio | ||||||
|  |     { | ||||||
|  |         public int IdLoteDeEnvio { get; set; } | ||||||
|  |         public DateTime FechaInicio { get; set; } | ||||||
|  |         public DateTime? FechaFin { get; set; } | ||||||
|  |         public string Periodo { get; set; } = string.Empty; | ||||||
|  |         public string Origen { get; set; } = string.Empty; | ||||||
|  |         public string Estado { get; set; } = string.Empty; | ||||||
|  |         public int TotalCorreos { get; set; } | ||||||
|  |         public int TotalEnviados { get; set; } | ||||||
|  |         public int TotalFallidos { get; set; } | ||||||
|  |         public int IdUsuarioDisparo { get; set; } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,12 @@ | |||||||
|  | namespace GestionIntegral.Api.Models.Comunicaciones | ||||||
|  | { | ||||||
|  |     public class MailSettings | ||||||
|  |     { | ||||||
|  |         public string SmtpHost { get; set; } = string.Empty; | ||||||
|  |         public int SmtpPort { get; set; } | ||||||
|  |         public string SenderName { get; set; } = string.Empty; | ||||||
|  |         public string SenderEmail { get; set; } = string.Empty; | ||||||
|  |         public string SmtpUser { get; set; } = string.Empty; | ||||||
|  |         public string SmtpPass { get; set; } = string.Empty; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,24 @@ | |||||||
|  | using System; | ||||||
|  |  | ||||||
|  | namespace GestionIntegral.Api.Dtos.Anomalia | ||||||
|  | { | ||||||
|  |     public class AlertaGenericaDto | ||||||
|  |     { | ||||||
|  |         public int IdAlerta { get; set; } | ||||||
|  |         public DateTime FechaDeteccion { get; set; } | ||||||
|  |         public string TipoAlerta { get; set; } = string.Empty; | ||||||
|  |         public string Entidad { get; set; } = string.Empty; | ||||||
|  |         public int IdEntidad { get; set; } | ||||||
|  |         public string Mensaje { get; set; } = string.Empty; | ||||||
|  |         public DateTime FechaAnomalia { get; set; } | ||||||
|  |         public bool Leida { get; set; } | ||||||
|  |          | ||||||
|  |         // Propiedades que pueden ser nulas porque no aplican a todos los tipos de alerta | ||||||
|  |         public int? CantidadEnviada { get; set; } | ||||||
|  |         public int? CantidadDevuelta { get; set; } | ||||||
|  |         public decimal? PorcentajeDevolucion { get; set; } | ||||||
|  |          | ||||||
|  |         // Podríamos añadir más propiedades opcionales en el futuro | ||||||
|  |         // public string? NombreEntidad { get; set; } // Por ejemplo, el nombre del canillita | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,20 @@ | |||||||
|  | namespace GestionIntegral.Api.Dtos.Comunicaciones | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     /// Representa un registro de historial de envío de correo para ser mostrado en la interfaz de usuario. | ||||||
|  |     /// </summary> | ||||||
|  |     public class EmailLogDto | ||||||
|  |     { | ||||||
|  |         public DateTime FechaEnvio { get; set; } | ||||||
|  |         public string Estado { get; set; } = string.Empty; | ||||||
|  |         public string Asunto { get; set; } = string.Empty; | ||||||
|  |         public string DestinatarioEmail { get; set; } = string.Empty; | ||||||
|  |         public string? Error { get; set; } | ||||||
|  |          | ||||||
|  |         /// <summary> | ||||||
|  |         /// Nombre del usuario que inició la acción de envío (ej. "Juan Pérez"). | ||||||
|  |         /// Puede ser "Sistema" si el envío fue automático (ej. Cierre Mensual). | ||||||
|  |         /// </summary> | ||||||
|  |         public string? NombreUsuarioDisparo { get; set; } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,26 @@ | |||||||
|  | namespace GestionIntegral.Api.Dtos.Comunicaciones | ||||||
|  | { | ||||||
|  |     // DTO para el feedback inmediato | ||||||
|  |     public class LoteDeEnvioResumenDto | ||||||
|  |     { | ||||||
|  |         public int IdLoteDeEnvio { get; set; } | ||||||
|  |         public string Periodo { get; set; } = string.Empty; | ||||||
|  |         public int TotalCorreos { get; set; } | ||||||
|  |         public int TotalEnviados { get; set; } | ||||||
|  |         public int TotalFallidos { get; set; } | ||||||
|  |         public List<EmailLogDto> ErroresDetallados { get; set; } = new(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // DTO para la tabla de historial | ||||||
|  |     public class LoteDeEnvioHistorialDto | ||||||
|  |     { | ||||||
|  |         public int IdLoteDeEnvio { get; set; } | ||||||
|  |         public DateTime FechaInicio { get; set; } | ||||||
|  |         public string Periodo { get; set; } = string.Empty; | ||||||
|  |         public string Estado { get; set; } = string.Empty; | ||||||
|  |         public int TotalCorreos { get; set; } | ||||||
|  |         public int TotalEnviados { get; set; } | ||||||
|  |         public int TotalFallidos { get; set; } | ||||||
|  |         public string NombreUsuarioDisparo { get; set; } = string.Empty; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,11 @@ | |||||||
|  | using GestionIntegral.Api.Dtos.Comunicaciones; | ||||||
|  |  | ||||||
|  | public class LoteDeEnvioResumenDto | ||||||
|  | { | ||||||
|  |     public int IdLoteDeEnvio { get; set; } | ||||||
|  |     public required string Periodo { get; set; } | ||||||
|  |     public int TotalCorreos { get; set; } | ||||||
|  |     public int TotalEnviados { get; set; } | ||||||
|  |     public int TotalFallidos { get; set; } | ||||||
|  |     public List<EmailLogDto> ErroresDetallados { get; set; } = new(); | ||||||
|  | } | ||||||
| @@ -0,0 +1,9 @@ | |||||||
|  | namespace GestionIntegral.Api.Dtos.Distribucion | ||||||
|  | { | ||||||
|  |     public class CanillaDropdownDto | ||||||
|  |     { | ||||||
|  |         public int IdCanilla { get; set; } | ||||||
|  |         public int? Legajo { get; set; } | ||||||
|  |         public string NomApe { get; set; } = string.Empty; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,8 @@ | |||||||
|  | namespace GestionIntegral.Api.Dtos.Distribucion | ||||||
|  | { | ||||||
|  |     public class OtroDestinoDropdownDto | ||||||
|  |     { | ||||||
|  |         public int IdDestino { get; set; } | ||||||
|  |         public string Nombre { get; set; } = string.Empty; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -4,6 +4,7 @@ namespace GestionIntegral.Api.Dtos.Distribucion | |||||||
|     { |     { | ||||||
|         public int IdPublicacion { get; set; } |         public int IdPublicacion { get; set; } | ||||||
|         public string Nombre { get; set; } = string.Empty; |         public string Nombre { get; set; } = string.Empty; | ||||||
|         public bool Habilitada { get; set; } // Simplificamos a bool, el backend manejará el default si es null |         public string NombreEmpresa { get; set; } = string.Empty; | ||||||
|  |         public bool? Habilitada { get; set; } | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -8,6 +8,6 @@ namespace GestionIntegral.Api.Dtos.Distribucion | |||||||
|         public int IdEmpresa { get; set; } |         public int IdEmpresa { get; set; } | ||||||
|         public string NombreEmpresa { get; set; } = string.Empty; // Para mostrar en UI |         public string NombreEmpresa { get; set; } = string.Empty; // Para mostrar en UI | ||||||
|         public bool CtrlDevoluciones { get; set; } |         public bool CtrlDevoluciones { get; set; } | ||||||
|         public bool Habilitada { get; set; } // Simplificamos a bool, el backend manejará el default si es null |         public bool? Habilitada { get; set; }  | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -0,0 +1,8 @@ | |||||||
|  | namespace GestionIntegral.Api.Dtos.Impresion | ||||||
|  | { | ||||||
|  |     public class EstadoBobinaDropdownDto | ||||||
|  |     { | ||||||
|  |         public int IdEstadoBobina { get; set; } | ||||||
|  |         public string Denominacion { get; set; } = string.Empty; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,17 @@ | |||||||
|  | using System.ComponentModel.DataAnnotations; | ||||||
|  |  | ||||||
|  | namespace GestionIntegral.Api.Dtos.Impresion | ||||||
|  | { | ||||||
|  |     public class UpdateDetalleSeccionTiradaDto | ||||||
|  |     { | ||||||
|  |         // ID del registro en bob_RegPublicaciones.  | ||||||
|  |         // Si es 0 o null, es una nueva sección a añadir. | ||||||
|  |         public int IdRegPublicacionSeccion { get; set; } | ||||||
|  |  | ||||||
|  |         [Required] | ||||||
|  |         public int IdSeccion { get; set; } | ||||||
|  |  | ||||||
|  |         [Required, Range(1, 1000)] | ||||||
|  |         public int CantPag { get; set; } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,15 @@ | |||||||
|  | using System.ComponentModel.DataAnnotations; | ||||||
|  |  | ||||||
|  | namespace GestionIntegral.Api.Dtos.Impresion | ||||||
|  | { | ||||||
|  |     public class UpdateTiradaRequestDto | ||||||
|  |     { | ||||||
|  |         [Required, Range(1, int.MaxValue)] | ||||||
|  |         public int Ejemplares { get; set; } | ||||||
|  |  | ||||||
|  |         [Required] | ||||||
|  |         // No necesitamos MinLength(1), ya que una tirada podría quedar sin secciones si el usuario las borra todas. | ||||||
|  |         // Por ahora lo quitamos para más flexibilidad. | ||||||
|  |         public List<UpdateDetalleSeccionTiradaDto> Secciones { get; set; } = new List<UpdateDetalleSeccionTiradaDto>(); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,15 @@ | |||||||
|  | namespace GestionIntegral.Api.Dtos.Reportes | ||||||
|  | { | ||||||
|  |     public class DistribucionSuscripcionDto | ||||||
|  |     { | ||||||
|  |         public string NombreEmpresa { get; set; } = string.Empty; | ||||||
|  |         public string NombrePublicacion { get; set; } = string.Empty; | ||||||
|  |         public string NombreSuscriptor { get; set; } = string.Empty; | ||||||
|  |         public string Direccion { get; set; } = string.Empty; | ||||||
|  |         public string? Telefono { get; set; } | ||||||
|  |         public DateTime FechaInicio { get; set; } | ||||||
|  |         public DateTime? FechaFin { get; set; } | ||||||
|  |         public string DiasEntrega { get; set; } = string.Empty; | ||||||
|  |         public string? Observaciones { get; set; } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,14 @@ | |||||||
|  | namespace GestionIntegral.Api.Dtos.Reportes | ||||||
|  | { | ||||||
|  |     public class FacturasParaReporteDto | ||||||
|  |     { | ||||||
|  |         public int IdFactura { get; set; } | ||||||
|  |         public string Periodo { get; set; } = string.Empty; | ||||||
|  |         public string NombreSuscriptor { get; set; } = string.Empty; | ||||||
|  |         public string TipoDocumento { get; set; } = string.Empty; | ||||||
|  |         public string NroDocumento { get; set; } = string.Empty; | ||||||
|  |         public decimal ImporteFinal { get; set; } | ||||||
|  |         public int IdEmpresa { get; set; } | ||||||
|  |         public string NombreEmpresa { get; set; } = string.Empty; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,55 @@ | |||||||
|  | namespace GestionIntegral.Api.Dtos.Reportes.ViewModels | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     /// Representa una agrupación de suscripciones por publicación para el reporte. | ||||||
|  |     /// </summary> | ||||||
|  |     public class GrupoPublicacion | ||||||
|  |     { | ||||||
|  |         public string NombrePublicacion { get; set; } = string.Empty; | ||||||
|  |         public IEnumerable<DistribucionSuscripcionDto> Suscripciones { get; set; } = Enumerable.Empty<DistribucionSuscripcionDto>(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Representa una agrupación de publicaciones por empresa para el reporte. | ||||||
|  |     /// </summary> | ||||||
|  |     public class GrupoEmpresa | ||||||
|  |     { | ||||||
|  |         public string NombreEmpresa { get; set; } = string.Empty; | ||||||
|  |         public IEnumerable<GrupoPublicacion> Publicaciones { get; set; } = Enumerable.Empty<GrupoPublicacion>(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public class DistribucionSuscripcionesViewModel | ||||||
|  |     { | ||||||
|  |         public IEnumerable<GrupoEmpresa> DatosAgrupadosAltas { get; } | ||||||
|  |         public IEnumerable<GrupoEmpresa> DatosAgrupadosBajas { get; } | ||||||
|  |         public string FechaDesde { get; set; } = string.Empty; | ||||||
|  |         public string FechaHasta { get; set; } = string.Empty; | ||||||
|  |         public string FechaGeneracion { get; set; } = string.Empty; | ||||||
|  |  | ||||||
|  |         public DistribucionSuscripcionesViewModel(IEnumerable<DistribucionSuscripcionDto> altas, IEnumerable<DistribucionSuscripcionDto> bajas) | ||||||
|  |         { | ||||||
|  |             // Función local para evitar repetir el código de agrupación | ||||||
|  |             Func<IEnumerable<DistribucionSuscripcionDto>, IEnumerable<GrupoEmpresa>> agruparDatos = (suscripciones) => | ||||||
|  |             { | ||||||
|  |                 return suscripciones | ||||||
|  |                     .GroupBy(s => s.NombreEmpresa) | ||||||
|  |                     .Select(gEmpresa => new GrupoEmpresa | ||||||
|  |                     { | ||||||
|  |                         NombreEmpresa = gEmpresa.Key, | ||||||
|  |                         Publicaciones = gEmpresa | ||||||
|  |                             .GroupBy(s => s.NombrePublicacion) | ||||||
|  |                             .Select(gPub => new GrupoPublicacion | ||||||
|  |                             { | ||||||
|  |                                 NombrePublicacion = gPub.Key, | ||||||
|  |                                 Suscripciones = gPub.OrderBy(s => s.NombreSuscriptor).ToList() | ||||||
|  |                             }) | ||||||
|  |                             .OrderBy(p => p.NombrePublicacion) | ||||||
|  |                     }) | ||||||
|  |                     .OrderBy(e => e.NombreEmpresa); | ||||||
|  |             }; | ||||||
|  |  | ||||||
|  |             DatosAgrupadosAltas = agruparDatos(altas); | ||||||
|  |             DatosAgrupadosBajas = agruparDatos(bajas); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,18 @@ | |||||||
|  | namespace GestionIntegral.Api.Dtos.Reportes.ViewModels | ||||||
|  | { | ||||||
|  |     // Esta clase anidada representará los datos de una empresa | ||||||
|  |     public class DatosEmpresaViewModel | ||||||
|  |     { | ||||||
|  |         public string NombreEmpresa { get; set; } = string.Empty; | ||||||
|  |         public IEnumerable<FacturasParaReporteDto> Facturas { get; set; } = new List<FacturasParaReporteDto>(); | ||||||
|  |         public decimal TotalEmpresa => Facturas.Sum(f => f.ImporteFinal); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public class FacturasPublicidadViewModel | ||||||
|  |     { | ||||||
|  |         public IEnumerable<DatosEmpresaViewModel> DatosPorEmpresa { get; set; } = new List<DatosEmpresaViewModel>(); | ||||||
|  |         public string Periodo { get; set; } = string.Empty; | ||||||
|  |         public string FechaGeneracion { get; set; } = string.Empty; | ||||||
|  |         public decimal TotalGeneral => DatosPorEmpresa.Sum(e => e.TotalEmpresa); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -14,8 +14,6 @@ namespace GestionIntegral.Api.Dtos.Reportes.ViewModels | |||||||
|         public string MesConsultado { get; set; } = string.Empty; |         public string MesConsultado { get; set; } = string.Empty; | ||||||
|         public string FechaReporte { get; set; } = DateTime.Now.ToString("dd/MM/yyyy"); |         public string FechaReporte { get; set; } = DateTime.Now.ToString("dd/MM/yyyy"); | ||||||
|  |  | ||||||
|         // --- PROPIEDAD PARA LOS TOTALES GENERALES DE PROMEDIOS --- |  | ||||||
|         // Esta propiedad calcula los promedios generales basados en los datos del resumen mensual. |  | ||||||
|         public ListadoDistribucionGeneralPromedioDiaDto? PromedioGeneral |         public ListadoDistribucionGeneralPromedioDiaDto? PromedioGeneral | ||||||
|         { |         { | ||||||
|             get |             get | ||||||
| @@ -24,21 +22,30 @@ namespace GestionIntegral.Api.Dtos.Reportes.ViewModels | |||||||
|                 { |                 { | ||||||
|                     return null; |                     return null; | ||||||
|                 } |                 } | ||||||
|  |                  | ||||||
|  |                 // 1. Filtrar solo los días con actividad para no diluir el promedio. | ||||||
|  |                 var diasActivos = ResumenMensual.Where(r => r.CantidadTirada > 0).ToList(); | ||||||
|  |  | ||||||
|                 // Contar solo los días con tirada > 0 para promediar correctamente |                 if (!diasActivos.Any()) | ||||||
|                 var diasConTirada = ResumenMensual.Count(d => d.CantidadTirada > 0); |                 { | ||||||
|                 if (diasConTirada == 0) return null; |                     return null; // No hay días con actividad, no se puede calcular el promedio. | ||||||
|  |                 } | ||||||
|  |                  | ||||||
|  |                 // 2. Usar el conteo de días activos como divisor. | ||||||
|  |                 var totalDiasActivos = diasActivos.Count; | ||||||
|  |  | ||||||
|                 return new ListadoDistribucionGeneralPromedioDiaDto |                 return new ListadoDistribucionGeneralPromedioDiaDto | ||||||
|                 { |                 { | ||||||
|                     Dia = "General", |                     Dia = "General", | ||||||
|                     CantidadDias = diasConTirada, |                     CantidadDias = totalDiasActivos, | ||||||
|                     PromedioTirada = (int)ResumenMensual.Average(r => r.CantidadTirada), |                     // 3. Calcular el promedio real: Suma de valores / Cantidad de días activos. | ||||||
|                     PromedioSinCargo = (int)ResumenMensual.Average(r => r.SinCargo), |                     // Se usa división entera para que coincida con el formato sin decimales. | ||||||
|                     PromedioPerdidos = (int)ResumenMensual.Average(r => r.Perdidos), |                     PromedioTirada = diasActivos.Sum(r => r.CantidadTirada) / totalDiasActivos, | ||||||
|                     PromedioLlevados = (int)ResumenMensual.Average(r => r.Llevados), |                     PromedioSinCargo = diasActivos.Sum(r => r.SinCargo) / totalDiasActivos, | ||||||
|                     PromedioDevueltos = (int)ResumenMensual.Average(r => r.Devueltos), |                     PromedioPerdidos = diasActivos.Sum(r => r.Perdidos) / totalDiasActivos, | ||||||
|                     PromedioVendidos = (int)ResumenMensual.Average(r => r.Vendidos) |                     PromedioLlevados = diasActivos.Sum(r => r.Llevados) / totalDiasActivos, | ||||||
|  |                     PromedioDevueltos = diasActivos.Sum(r => r.Devueltos) / totalDiasActivos, | ||||||
|  |                     PromedioVendidos = diasActivos.Sum(r => r.Vendidos) / totalDiasActivos | ||||||
|                 }; |                 }; | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -0,0 +1,19 @@ | |||||||
|  | namespace GestionIntegral.Api.Dtos.Suscripciones | ||||||
|  | { | ||||||
|  |     public class AjusteDto | ||||||
|  |     { | ||||||
|  |         public int IdAjuste { get; set; } | ||||||
|  |         public int IdSuscriptor { get; set; } | ||||||
|  |         public int IdEmpresa { get; set; } | ||||||
|  |         public string? NombreEmpresa { get; set; } | ||||||
|  |         public string FechaAjuste { get; set; } = string.Empty; | ||||||
|  |         public string TipoAjuste { get; set; } = string.Empty; | ||||||
|  |         public decimal Monto { get; set; } | ||||||
|  |         public string Motivo { get; set; } = string.Empty; | ||||||
|  |         public string Estado { get; set; } = string.Empty; | ||||||
|  |         public int? IdFacturaAplicado { get; set; } | ||||||
|  |         public string? NumeroFacturaAplicado { get; set; } | ||||||
|  |         public string FechaAlta { get; set; } = string.Empty; | ||||||
|  |         public string NombreUsuarioAlta { get; set; } = string.Empty; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,13 @@ | |||||||
|  | using System.ComponentModel.DataAnnotations; | ||||||
|  |  | ||||||
|  | namespace GestionIntegral.Api.Dtos.Suscripciones | ||||||
|  | { | ||||||
|  |     public class AsignarPromocionDto | ||||||
|  |     { | ||||||
|  |         [Required] | ||||||
|  |         public int IdPromocion { get; set; } | ||||||
|  |         [Required] | ||||||
|  |         public DateTime VigenciaDesde { get; set; } | ||||||
|  |         public DateTime? VigenciaHasta { get; set; } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,28 @@ | |||||||
|  | using System.ComponentModel.DataAnnotations; | ||||||
|  |  | ||||||
|  | namespace GestionIntegral.Api.Dtos.Suscripciones | ||||||
|  | { | ||||||
|  |     public class CreateAjusteDto | ||||||
|  |     { | ||||||
|  |         [Required] | ||||||
|  |         public int IdSuscriptor { get; set; } | ||||||
|  |          | ||||||
|  |         [Required] | ||||||
|  |         public int IdEmpresa { get; set; } | ||||||
|  |  | ||||||
|  |         [Required] | ||||||
|  |         public DateTime FechaAjuste { get; set; } | ||||||
|  |          | ||||||
|  |         [Required] | ||||||
|  |         [RegularExpression("^(Credito|Debito)$", ErrorMessage = "El tipo de ajuste debe ser 'Credito' o 'Debito'.")] | ||||||
|  |         public string TipoAjuste { get; set; } = string.Empty; | ||||||
|  |  | ||||||
|  |         [Required] | ||||||
|  |         [Range(0.01, 999999.99, ErrorMessage = "El monto debe ser un valor positivo.")] | ||||||
|  |         public decimal Monto { get; set; } | ||||||
|  |  | ||||||
|  |         [Required(ErrorMessage = "El motivo es obligatorio.")] | ||||||
|  |         [StringLength(250)] | ||||||
|  |         public string Motivo { get; set; } = string.Empty; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,29 @@ | |||||||
|  | // Archivo: GestionIntegral.Api/Dtos/Suscripciones/CreatePagoDto.cs | ||||||
|  |  | ||||||
|  | using System; | ||||||
|  | using System.ComponentModel.DataAnnotations; | ||||||
|  |  | ||||||
|  | namespace GestionIntegral.Api.Dtos.Suscripciones | ||||||
|  | { | ||||||
|  |     public class CreatePagoDto | ||||||
|  |     { | ||||||
|  |         [Required] | ||||||
|  |         public int IdFactura { get; set; } | ||||||
|  |  | ||||||
|  |         [Required] | ||||||
|  |         public DateTime FechaPago { get; set; } | ||||||
|  |  | ||||||
|  |         [Required(ErrorMessage = "Debe seleccionar una forma de pago.")] | ||||||
|  |         public int IdFormaPago { get; set; } | ||||||
|  |  | ||||||
|  |         [Required(ErrorMessage = "El monto es obligatorio.")] | ||||||
|  |         [Range(0.01, 99999999.99, ErrorMessage = "El monto debe ser un valor positivo.")] | ||||||
|  |         public decimal Monto { get; set; } | ||||||
|  |  | ||||||
|  |         [StringLength(100)] | ||||||
|  |         public string? Referencia { get; set; } // Nro. de comprobante, etc. | ||||||
|  |  | ||||||
|  |         [StringLength(250)] | ||||||
|  |         public string? Observaciones { get; set; } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,31 @@ | |||||||
|  | // Archivo: GestionIntegral.Api/Dtos/Suscripciones/CreatePromocionDto.cs | ||||||
|  |  | ||||||
|  | using System; | ||||||
|  | using System.ComponentModel.DataAnnotations; | ||||||
|  |  | ||||||
|  | namespace GestionIntegral.Api.Dtos.Suscripciones | ||||||
|  | { | ||||||
|  |     public class CreatePromocionDto | ||||||
|  |     { | ||||||
|  |         [Required] | ||||||
|  |         [StringLength(200)] | ||||||
|  |         public string Descripcion { get; set; } = string.Empty; | ||||||
|  |  | ||||||
|  |         [Required] | ||||||
|  |         public string TipoEfecto { get; set; } = string.Empty; // Corregido | ||||||
|  |  | ||||||
|  |         [Required] | ||||||
|  |         [Range(0, 99999999.99)] // Se permite 0 para bonificaciones | ||||||
|  |         public decimal ValorEfecto { get; set; } // Corregido | ||||||
|  |  | ||||||
|  |         [Required] | ||||||
|  |         public string TipoCondicion { get; set; } = string.Empty; | ||||||
|  |          | ||||||
|  |         public int? ValorCondicion { get; set; } | ||||||
|  |  | ||||||
|  |         [Required] | ||||||
|  |         public DateTime FechaInicio { get; set; } | ||||||
|  |         public DateTime? FechaFin { get; set; } | ||||||
|  |         public bool Activa { get; set; } = true; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,30 @@ | |||||||
|  | using System.ComponentModel.DataAnnotations; | ||||||
|  |  | ||||||
|  | namespace GestionIntegral.Api.Dtos.Suscripciones | ||||||
|  | { | ||||||
|  |     public class CreateSuscripcionDto | ||||||
|  |     { | ||||||
|  |         [Required] | ||||||
|  |         public int IdSuscriptor { get; set; } | ||||||
|  |          | ||||||
|  |         [Required] | ||||||
|  |         public int IdPublicacion { get; set; } | ||||||
|  |  | ||||||
|  |         [Required] | ||||||
|  |         public DateTime FechaInicio { get; set; } | ||||||
|  |          | ||||||
|  |         public DateTime? FechaFin { get; set; } | ||||||
|  |  | ||||||
|  |         [Required] | ||||||
|  |         public string Estado { get; set; } = "Activa"; | ||||||
|  |  | ||||||
|  |         [Required(ErrorMessage = "Debe especificar los días de entrega.")] | ||||||
|  |         public List<string> DiasEntrega { get; set; } = new List<string>(); // "L", "M", "X"... | ||||||
|  |  | ||||||
|  |         [StringLength(250)] | ||||||
|  |         public string? Observaciones { get; set; } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Nota: Por ahora, el DTO de actualización puede ser similar al de creación. | ||||||
|  | // Si se necesita una lógica diferente, se crearía un UpdateSuscripcionDto. | ||||||
| @@ -0,0 +1,44 @@ | |||||||
|  | // Archivo: GestionIntegral.Api/Dtos/Suscripciones/CreateSuscriptorDto.cs | ||||||
|  |  | ||||||
|  | using System.ComponentModel.DataAnnotations; | ||||||
|  |  | ||||||
|  | namespace GestionIntegral.Api.Dtos.Suscripciones | ||||||
|  | { | ||||||
|  |     public class CreateSuscriptorDto | ||||||
|  |     { | ||||||
|  |         [Required(ErrorMessage = "El nombre completo es obligatorio.")] | ||||||
|  |         [StringLength(150)] | ||||||
|  |         public string NombreCompleto { get; set; } = string.Empty; | ||||||
|  |  | ||||||
|  |         [EmailAddress(ErrorMessage = "El formato del email no es válido.")] | ||||||
|  |         [StringLength(100)] | ||||||
|  |         public string? Email { get; set; } | ||||||
|  |  | ||||||
|  |         [StringLength(50)] | ||||||
|  |         [RegularExpression(@"^[0-9\s\+\-\(\)]*$", ErrorMessage = "El teléfono solo puede contener números y los símbolos +, -, () y espacios.")] | ||||||
|  |         public string? Telefono { get; set; } | ||||||
|  |  | ||||||
|  |         [Required(ErrorMessage = "La dirección es obligatoria.")] | ||||||
|  |         [StringLength(200)] | ||||||
|  |         public string Direccion { get; set; } = string.Empty; | ||||||
|  |  | ||||||
|  |         [Required(ErrorMessage = "El tipo de documento es obligatorio.")] | ||||||
|  |         [StringLength(4)] | ||||||
|  |         public string TipoDocumento { get; set; } = string.Empty; | ||||||
|  |  | ||||||
|  |         [Required(ErrorMessage = "El número de documento es obligatorio.")] | ||||||
|  |         [StringLength(11)] | ||||||
|  |         [RegularExpression("^[0-9]*$", ErrorMessage = "El número de documento solo puede contener números.")] | ||||||
|  |         public string NroDocumento { get; set; } = string.Empty; | ||||||
|  |  | ||||||
|  |         [StringLength(22, MinimumLength = 22, ErrorMessage = "El CBU debe tener 22 dígitos.")] | ||||||
|  |         [RegularExpression("^[0-9]*$", ErrorMessage = "El CBU solo puede contener números.")] | ||||||
|  |         public string? CBU { get; set; } | ||||||
|  |  | ||||||
|  |         [Required(ErrorMessage = "La forma de pago es obligatoria.")] | ||||||
|  |         public int IdFormaPagoPreferida { get; set; } | ||||||
|  |  | ||||||
|  |         [StringLength(250)] | ||||||
|  |         public string? Observaciones { get; set; } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,13 @@ | |||||||
|  | namespace GestionIntegral.Api.Dtos.Suscripciones | ||||||
|  | { | ||||||
|  |     public class FacturaConsolidadaDto | ||||||
|  |     { | ||||||
|  |         public int IdFactura { get; set; } | ||||||
|  |         public string NombreEmpresa { get; set; } = string.Empty; | ||||||
|  |         public decimal ImporteFinal { get; set; } | ||||||
|  |         public string EstadoPago { get; set; } = string.Empty; | ||||||
|  |         public string EstadoFacturacion { get; set; } = string.Empty; | ||||||
|  |         public string? NumeroFactura { get; set; } | ||||||
|  |         public List<FacturaDetalleDto> Detalles { get; set; } = new List<FacturaDetalleDto>(); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,25 @@ | |||||||
|  | namespace GestionIntegral.Api.Dtos.Suscripciones | ||||||
|  | { | ||||||
|  |     public class FacturaDetalleDto | ||||||
|  |     { | ||||||
|  |         public string Descripcion { get; set; } = string.Empty; | ||||||
|  |         public decimal ImporteNeto { get; set; } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public class FacturaDto | ||||||
|  |     { | ||||||
|  |         public int IdFactura { get; set; } | ||||||
|  |         public int IdSuscriptor { get; set; } | ||||||
|  |         public string Periodo { get; set; } = string.Empty; | ||||||
|  |         public string FechaEmision { get; set; } = string.Empty; | ||||||
|  |         public string FechaVencimiento { get; set; } = string.Empty; | ||||||
|  |         public decimal ImporteFinal { get; set; } | ||||||
|  |         public decimal TotalPagado { get; set; } | ||||||
|  |         public decimal SaldoPendiente { get; set; } | ||||||
|  |         public string EstadoPago { get; set; } = string.Empty; | ||||||
|  |         public string EstadoFacturacion { get; set; } = string.Empty; | ||||||
|  |         public string? NumeroFactura { get; set; } | ||||||
|  |         public string NombreSuscriptor { get; set; } = string.Empty; | ||||||
|  |         public List<FacturaDetalleDto> Detalles { get; set; } = new List<FacturaDetalleDto>(); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,9 @@ | |||||||
|  | namespace GestionIntegral.Api.Dtos.Suscripciones | ||||||
|  | { | ||||||
|  |     public class FormaPagoDto | ||||||
|  |     { | ||||||
|  |         public int IdFormaPago { get; set; } | ||||||
|  |         public string Nombre { get; set; } = string.Empty; | ||||||
|  |         public bool RequiereCBU { get; set; } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,17 @@ | |||||||
|  | namespace GestionIntegral.Api.Dtos.Suscripciones | ||||||
|  | { | ||||||
|  |     public class PagoDto | ||||||
|  |     { | ||||||
|  |         public int IdPago { get; set; } | ||||||
|  |         public int IdFactura { get; set; } | ||||||
|  |         public string FechaPago { get; set; } = string.Empty; // "yyyy-MM-dd" | ||||||
|  |         public int IdFormaPago { get; set; } | ||||||
|  |         public string NombreFormaPago { get; set; } = string.Empty; // Enriquecido | ||||||
|  |         public decimal Monto { get; set; } | ||||||
|  |         public string Estado { get; set; } = string.Empty; // "Aprobado", "Rechazado" | ||||||
|  |         public string? Referencia { get; set; } | ||||||
|  |         public string? Observaciones { get; set; } | ||||||
|  |         public int IdUsuarioRegistro { get; set; } | ||||||
|  |         public string NombreUsuarioRegistro { get; set; } = string.Empty; // Enriquecido | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,11 @@ | |||||||
|  | namespace GestionIntegral.Api.Dtos.Suscripciones | ||||||
|  | { | ||||||
|  |     public class ProcesamientoLoteResponseDto | ||||||
|  |     { | ||||||
|  |         public int TotalRegistrosLeidos { get; set; } | ||||||
|  |         public int PagosAprobados { get; set; } | ||||||
|  |         public int PagosRechazados { get; set; } | ||||||
|  |         public List<string> Errores { get; set; } = new List<string>(); | ||||||
|  |         public string MensajeResumen { get; set; } = string.Empty; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,8 @@ | |||||||
|  | namespace GestionIntegral.Api.Dtos.Suscripciones | ||||||
|  | { | ||||||
|  |     public class PromocionAsignadaDto : PromocionDto | ||||||
|  |     { | ||||||
|  |         public string VigenciaDesdeAsignacion { get; set; } = string.Empty; | ||||||
|  |         public string? VigenciaHastaAsignacion { get; set; } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,15 @@ | |||||||
|  | namespace GestionIntegral.Api.Dtos.Suscripciones | ||||||
|  | { | ||||||
|  |     public class PromocionDto | ||||||
|  |     { | ||||||
|  |         public int IdPromocion { get; set; } | ||||||
|  |         public string Descripcion { get; set; } = string.Empty; | ||||||
|  |         public string TipoEfecto { get; set; } = string.Empty; | ||||||
|  |         public decimal ValorEfecto { get; set; } | ||||||
|  |         public string TipoCondicion { get; set; } = string.Empty; | ||||||
|  |         public int? ValorCondicion { get; set; } | ||||||
|  |         public string FechaInicio { get; set; } = string.Empty; | ||||||
|  |         public string? FechaFin { get; set; } | ||||||
|  |         public bool Activa { get; set; } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,11 @@ | |||||||
|  | namespace GestionIntegral.Api.Dtos.Suscripciones | ||||||
|  | { | ||||||
|  |     public class ResumenCuentaSuscriptorDto | ||||||
|  |     { | ||||||
|  |         public int IdSuscriptor { get; set; } | ||||||
|  |         public string NombreSuscriptor { get; set; } = string.Empty; | ||||||
|  |         public decimal SaldoPendienteTotal { get; set; } | ||||||
|  |         public decimal ImporteTotal { get; set; } | ||||||
|  |         public List<FacturaConsolidadaDto> Facturas { get; set; } = new List<FacturaConsolidadaDto>(); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,15 @@ | |||||||
|  | namespace GestionIntegral.Api.Dtos.Suscripciones | ||||||
|  | { | ||||||
|  |     public class SuscripcionDto | ||||||
|  |     { | ||||||
|  |         public int IdSuscripcion { get; set; } | ||||||
|  |         public int IdSuscriptor { get; set; } | ||||||
|  |         public int IdPublicacion { get; set; } | ||||||
|  |         public string NombrePublicacion { get; set; } = string.Empty; // Para UI | ||||||
|  |         public string FechaInicio { get; set; } = string.Empty; // Formato "yyyy-MM-dd" | ||||||
|  |         public string? FechaFin { get; set; } | ||||||
|  |         public string Estado { get; set; } = string.Empty; | ||||||
|  |         public string DiasEntrega { get; set; } = string.Empty; | ||||||
|  |         public string? Observaciones { get; set; } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,19 @@ | |||||||
|  | namespace GestionIntegral.Api.Dtos.Suscripciones | ||||||
|  | { | ||||||
|  |     // DTO para mostrar la información de un suscriptor | ||||||
|  |     public class SuscriptorDto | ||||||
|  |     { | ||||||
|  |         public int IdSuscriptor { get; set; } | ||||||
|  |         public string NombreCompleto { get; set; } = string.Empty; | ||||||
|  |         public string? Email { get; set; } | ||||||
|  |         public string? Telefono { get; set; } | ||||||
|  |         public string Direccion { get; set; } = string.Empty; | ||||||
|  |         public string TipoDocumento { get; set; } = string.Empty; | ||||||
|  |         public string NroDocumento { get; set; } = string.Empty; | ||||||
|  |         public string? CBU { get; set; } | ||||||
|  |         public int IdFormaPagoPreferida { get; set; } | ||||||
|  |         public string NombreFormaPagoPreferida { get; set; } = string.Empty; // Para UI | ||||||
|  |         public string? Observaciones { get; set; } | ||||||
|  |         public bool Activo { get; set; } | ||||||
|  |     } | ||||||
|  | } | ||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user