Compare commits
	
		
			7 Commits
		
	
	
		
			main
			...
			b66d00c92d
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| b66d00c92d | |||
| 72c2f7ee31 | |||
| 1456ccd723 | |||
| 229f657685 | |||
| 13ab496727 | |||
| 5adc1e6d46 | |||
| 1ec21741cc | 
| @@ -14,7 +14,7 @@ jobs: | |||||||
|         run: | |         run: | | ||||||
|           set -e |           set -e | ||||||
|            |            | ||||||
|           # Configura SSH (sin cambios) |           # Configura SSH | ||||||
|           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,7 +26,7 @@ jobs: | |||||||
|             set -e |             set -e | ||||||
|             echo "--- INICIO DEL DESPLIEGUE OPTIMIZADO ---" |             echo "--- INICIO DEL DESPLIEGUE OPTIMIZADO ---" | ||||||
|              |              | ||||||
|             # 1. Preparar entorno |             # 1. Preparar entorno (sin cambios) | ||||||
|             TEMP_DIR=$(mktemp -d) |             TEMP_DIR=$(mktemp -d) | ||||||
|             REPO_OWNER="dmolinari" |             REPO_OWNER="dmolinari" | ||||||
|             REPO_NAME="gestionintegralweb" |             REPO_NAME="gestionintegralweb" | ||||||
| @@ -37,7 +37,7 @@ jobs: | |||||||
|             cd "$TEMP_DIR" |             cd "$TEMP_DIR" | ||||||
|             git checkout "${{ gitea.sha }}" |             git checkout "${{ gitea.sha }}" | ||||||
|              |              | ||||||
|             # 2. Construcción paralela |             # 2. Construcción paralela (sin cambios) | ||||||
|             build_image() { |             build_image() { | ||||||
|               local dockerfile=$1 |               local dockerfile=$1 | ||||||
|               local image_name=$2 |               local image_name=$2 | ||||||
| @@ -55,17 +55,31 @@ jobs: | |||||||
|             (build_image "Frontend/Dockerfile" "dmolinari/gestionintegralweb-frontend:latest" ".") & |             (build_image "Frontend/Dockerfile" "dmolinari/gestionintegralweb-frontend:latest" ".") & | ||||||
|             wait |             wait | ||||||
|              |              | ||||||
|             # 3. Despliegue con Docker Compose |             # Copiamos la versión actualizada del docker-compose.yml al directorio de despliegue. | ||||||
|             cd /opt/gestion-integral |             echo "Copiando el archivo docker-compose.yml actualizado..." | ||||||
|             export DB_SA_PASSWORD='${{ secrets.DB_SA_PASSWORD_SECRET }}' |             cp "$TEMP_DIR/docker-compose.yml" /opt/gestion-integral/docker-compose.yml | ||||||
|              |              | ||||||
|             echo "Recreando servicios de la aplicación..." |             # (Opcional pero recomendado) Verificamos que el archivo se copió bien | ||||||
|             docker compose up -d --force-recreate |             echo "--- Verificando contenido del docker-compose.yml que se usará ---" | ||||||
|  |             cat /opt/gestion-integral/docker-compose.yml | head -n 5 | ||||||
|  |             echo "------------------------------------------------------------------" | ||||||
|              |              | ||||||
|             # 4. Limpieza |             # 3. Crear/Actualizar los Docker Secrets (sin cambios) | ||||||
|  |             # ... (tus comandos docker secret create) ... | ||||||
|  |             printf "%s" '${{ secrets.JWT_KEY }}' | docker secret create jwt_key - 2>/dev/null || (printf "%s" '${{ secrets.JWT_KEY }}' | docker secret rm jwt_key && printf "%s" '${{ secrets.JWT_KEY }}' | docker secret create jwt_key -) | ||||||
|  |             printf "%s" '${{ secrets.DB_SA_PASSWORD_SECRET }}' | docker secret create db_password - 2>/dev/null || (printf "%s" '${{ secrets.DB_SA_PASSWORD_SECRET }}' | docker secret rm db_password && printf "%s" '${{ secrets.DB_SA_PASSWORD_SECRET }}' | docker secret create db_password -) | ||||||
|  |              | ||||||
|  |             # 4. Desplegar el Stack | ||||||
|  |             echo "Desplegando el stack..." | ||||||
|  |             docker stack deploy \ | ||||||
|  |               -c /opt/gestion-integral/docker-compose.yml \ | ||||||
|  |               --with-registry-auth \ | ||||||
|  |               gestion-integral | ||||||
|  |              | ||||||
|  |             # 5. Limpieza (sin cambios) | ||||||
|             echo "Realizando limpieza..." |             echo "Realizando limpieza..." | ||||||
|             rm -rf "$TEMP_DIR" |             rm -rf "$TEMP_DIR" | ||||||
|             docker image prune -f --filter "dangling=true" |             docker image prune -af --filter "until=24h" | ||||||
|              |              | ||||||
|             echo "--- DESPLIEGUE COMPLETADO CON ÉXITO ---" |             echo "--- DESPLIEGUE COMPLETADO CON ÉXITO ---" | ||||||
|           EOSSH |           EOSSH | ||||||
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -19,6 +19,9 @@ 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 | ||||||
|   | |||||||
| @@ -1,73 +0,0 @@ | |||||||
| 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; } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,40 +0,0 @@ | |||||||
| 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,19 +50,10 @@ 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); |             var canillitas = await _canillaService.ObtenerTodosAsync(nomApe, legajo, soloActivos, esAccionista); // <<-- Pasa el parámetro | ||||||
|             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,23 +64,6 @@ 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) |         public async Task<IActionResult> GetAllPublicaciones([FromQuery] string? nombre, [FromQuery] int? idEmpresa, [FromQuery] bool? soloHabilitadas = true) | ||||||
|         { |         { | ||||||
|             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) |         public async Task<IActionResult> GetPublicacionesForDropdown([FromQuery] bool soloHabilitadas = true) | ||||||
|         { |         { | ||||||
|             try |             try | ||||||
|             { |             { | ||||||
|   | |||||||
| @@ -67,23 +67,6 @@ 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,25 +62,6 @@ 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,8 +22,7 @@ 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"; |         private const string PermisoEliminarTirada = "IT003"; // Asumo que se refiere a eliminar una tirada completa (cabecera y detalles) | ||||||
|         private const string PermisoModificarTirada = "IT004"; |  | ||||||
|  |  | ||||||
|         public TiradasController(ITiradaService tiradaService, ILogger<TiradasController> logger) |         public TiradasController(ITiradaService tiradaService, ILogger<TiradasController> logger) | ||||||
|         { |         { | ||||||
| @@ -84,43 +83,6 @@ 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,9 +1,12 @@ | |||||||
|  | // --- 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 | ||||||
| { | { | ||||||
|   | |||||||
| @@ -1,152 +0,0 @@ | |||||||
| 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 ?? "-"); |  | ||||||
|                     } |  | ||||||
|                 }); |  | ||||||
|             }); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,121 +0,0 @@ | |||||||
| 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(60); |                     columns.ConstantColumn(40); | ||||||
|                     columns.RelativeColumn(); |                     columns.RelativeColumn(); | ||||||
|                     columns.RelativeColumn(); |                     columns.RelativeColumn(); | ||||||
|                     columns.RelativeColumn(); |                     columns.RelativeColumn(); | ||||||
| @@ -89,20 +89,9 @@ 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($"{dayAbbreviations[item.Fecha.DayOfWeek]} {item.Fecha.Day}"); |                     table.Cell().Border(1).Padding(3).Text(item.Fecha.Day.ToString()); | ||||||
|                     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,8 +1,15 @@ | |||||||
| 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; | ||||||
| @@ -38,9 +45,6 @@ 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"; |  | ||||||
|         private const string PermisoVerReportesSecretaria = "RR012"; |  | ||||||
|  |  | ||||||
|         public ReportesController( |         public ReportesController( | ||||||
|             IReportesService reportesService, |             IReportesService reportesService, | ||||||
| @@ -527,7 +531,7 @@ namespace GestionIntegral.Api.Controllers | |||||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] |         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||||
|         public async Task<IActionResult> GetVentaMensualSecretariaElDia([FromQuery] DateTime fechaDesde, [FromQuery] DateTime fechaHasta) |         public async Task<IActionResult> GetVentaMensualSecretariaElDia([FromQuery] DateTime fechaDesde, [FromQuery] DateTime fechaHasta) | ||||||
|         { |         { | ||||||
|             if (!TienePermiso(PermisoVerReportesSecretaria)) return Forbid(); // Asumiendo RR002 para todos estos |             if (!TienePermiso(PermisoVerListadoDistribucion)) return Forbid(); // Asumiendo RR002 para todos estos | ||||||
|             var (data, error) = await _reportesService.ObtenerVentaMensualSecretariaElDiaAsync(fechaDesde, fechaHasta); |             var (data, error) = await _reportesService.ObtenerVentaMensualSecretariaElDiaAsync(fechaDesde, fechaHasta); | ||||||
|             if (error != null) return BadRequest(new { message = error }); |             if (error != null) return BadRequest(new { message = error }); | ||||||
|             if (data == null || !data.Any()) return NotFound(new { message = "No hay datos para el reporte de ventas 'El Día'." }); |             if (data == null || !data.Any()) return NotFound(new { message = "No hay datos para el reporte de ventas 'El Día'." }); | ||||||
| @@ -541,7 +545,7 @@ namespace GestionIntegral.Api.Controllers | |||||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] |         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||||
|         public async Task<IActionResult> GetVentaMensualSecretariaElDiaPdf([FromQuery] DateTime fechaDesde, [FromQuery] DateTime fechaHasta) |         public async Task<IActionResult> GetVentaMensualSecretariaElDiaPdf([FromQuery] DateTime fechaDesde, [FromQuery] DateTime fechaHasta) | ||||||
|         { |         { | ||||||
|             if (!TienePermiso(PermisoVerReportesSecretaria)) return Forbid(); |             if (!TienePermiso(PermisoVerListadoDistribucion)) return Forbid(); | ||||||
|  |  | ||||||
|             var (data, error) = await _reportesService.ObtenerVentaMensualSecretariaElDiaAsync(fechaDesde, fechaHasta); |             var (data, error) = await _reportesService.ObtenerVentaMensualSecretariaElDiaAsync(fechaDesde, fechaHasta); | ||||||
|  |  | ||||||
| @@ -578,7 +582,7 @@ namespace GestionIntegral.Api.Controllers | |||||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] |         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||||
|         public async Task<IActionResult> GetVentaMensualSecretariaElPlata([FromQuery] DateTime fechaDesde, [FromQuery] DateTime fechaHasta) |         public async Task<IActionResult> GetVentaMensualSecretariaElPlata([FromQuery] DateTime fechaDesde, [FromQuery] DateTime fechaHasta) | ||||||
|         { |         { | ||||||
|             if (!TienePermiso(PermisoVerReportesSecretaria)) return Forbid(); // Asumiendo RR002 |             if (!TienePermiso(PermisoVerListadoDistribucion)) return Forbid(); // Asumiendo RR002 | ||||||
|             var (data, error) = await _reportesService.ObtenerVentaMensualSecretariaElPlataAsync(fechaDesde, fechaHasta); |             var (data, error) = await _reportesService.ObtenerVentaMensualSecretariaElPlataAsync(fechaDesde, fechaHasta); | ||||||
|             if (error != null) return BadRequest(new { message = error }); |             if (error != null) return BadRequest(new { message = error }); | ||||||
|             if (data == null || !data.Any()) return NotFound(new { message = "No hay datos para el reporte de ventas 'El Plata'." }); |             if (data == null || !data.Any()) return NotFound(new { message = "No hay datos para el reporte de ventas 'El Plata'." }); | ||||||
| @@ -592,7 +596,7 @@ namespace GestionIntegral.Api.Controllers | |||||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] |         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||||
|         public async Task<IActionResult> GetVentaMensualSecretariaElPlataPdf([FromQuery] DateTime fechaDesde, [FromQuery] DateTime fechaHasta) |         public async Task<IActionResult> GetVentaMensualSecretariaElPlataPdf([FromQuery] DateTime fechaDesde, [FromQuery] DateTime fechaHasta) | ||||||
|         { |         { | ||||||
|             if (!TienePermiso(PermisoVerReportesSecretaria)) return Forbid(); |             if (!TienePermiso(PermisoVerListadoDistribucion)) return Forbid(); | ||||||
|  |  | ||||||
|             var (data, error) = await _reportesService.ObtenerVentaMensualSecretariaElPlataAsync(fechaDesde, fechaHasta); |             var (data, error) = await _reportesService.ObtenerVentaMensualSecretariaElPlataAsync(fechaDesde, fechaHasta); | ||||||
|  |  | ||||||
| @@ -629,7 +633,7 @@ namespace GestionIntegral.Api.Controllers | |||||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] |         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||||
|         public async Task<IActionResult> GetVentaMensualSecretariaTirDevo([FromQuery] DateTime fechaDesde, [FromQuery] DateTime fechaHasta) |         public async Task<IActionResult> GetVentaMensualSecretariaTirDevo([FromQuery] DateTime fechaDesde, [FromQuery] DateTime fechaHasta) | ||||||
|         { |         { | ||||||
|             if (!TienePermiso(PermisoVerReportesSecretaria)) return Forbid(); // Asumiendo RR002 |             if (!TienePermiso(PermisoVerListadoDistribucion)) return Forbid(); // Asumiendo RR002 | ||||||
|             var (data, error) = await _reportesService.ObtenerVentaMensualSecretariaTirDevoAsync(fechaDesde, fechaHasta); |             var (data, error) = await _reportesService.ObtenerVentaMensualSecretariaTirDevoAsync(fechaDesde, fechaHasta); | ||||||
|             if (error != null) return BadRequest(new { message = error }); |             if (error != null) return BadRequest(new { message = error }); | ||||||
|             if (data == null || !data.Any()) return NotFound(new { message = "No hay datos para el reporte de tirada/devolución." }); |             if (data == null || !data.Any()) return NotFound(new { message = "No hay datos para el reporte de tirada/devolución." }); | ||||||
| @@ -643,7 +647,7 @@ namespace GestionIntegral.Api.Controllers | |||||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] |         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||||
|         public async Task<IActionResult> GetVentaMensualSecretariaTirDevoPdf([FromQuery] DateTime fechaDesde, [FromQuery] DateTime fechaHasta) |         public async Task<IActionResult> GetVentaMensualSecretariaTirDevoPdf([FromQuery] DateTime fechaDesde, [FromQuery] DateTime fechaHasta) | ||||||
|         { |         { | ||||||
|             if (!TienePermiso(PermisoVerReportesSecretaria)) return Forbid(); |             if (!TienePermiso(PermisoVerListadoDistribucion)) return Forbid(); | ||||||
|  |  | ||||||
|             var (data, error) = await _reportesService.ObtenerVentaMensualSecretariaTirDevoAsync(fechaDesde, fechaHasta); |             var (data, error) = await _reportesService.ObtenerVentaMensualSecretariaTirDevoAsync(fechaDesde, fechaHasta); | ||||||
|  |  | ||||||
| @@ -1672,88 +1676,5 @@ 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."); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -1,94 +0,0 @@ | |||||||
| 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(); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,93 +0,0 @@ | |||||||
| // 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); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,126 +0,0 @@ | |||||||
| 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, |  | ||||||
|         [FromQuery] string? tipoFactura) |  | ||||||
|         { |  | ||||||
|             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, tipoFactura); |  | ||||||
|             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); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,27 +0,0 @@ | |||||||
| 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); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,69 +0,0 @@ | |||||||
| 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); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,90 +0,0 @@ | |||||||
| 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(); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,138 +0,0 @@ | |||||||
| // 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(); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,153 +0,0 @@ | |||||||
| 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(); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,65 +0,0 @@ | |||||||
| 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>(); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -1,26 +0,0 @@ | |||||||
| 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); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,12 +0,0 @@ | |||||||
| 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); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,69 +0,0 @@ | |||||||
| 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,5 +1,4 @@ | |||||||
| 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 | ||||||
| @@ -26,7 +25,7 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion | |||||||
|     string? nomApeFilter, |     string? nomApeFilter, | ||||||
|     int? legajoFilter, |     int? legajoFilter, | ||||||
|     bool? esAccionista, |     bool? esAccionista, | ||||||
|     bool? soloActivos) |     bool? soloActivos) // <<-- Parámetro aquí | ||||||
|         { |         { | ||||||
|             using var connection = _connectionFactory.CreateConnection(); |             using var connection = _connectionFactory.CreateConnection(); | ||||||
|             var sqlBuilder = new System.Text.StringBuilder(@" |             var sqlBuilder = new System.Text.StringBuilder(@" | ||||||
| @@ -74,37 +73,6 @@ 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,14 +2,12 @@ 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,14 +2,12 @@ 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,11 +9,10 @@ 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); |         Task<bool> DeleteByPublicacionIdAsync(int idPublicacion, int idUsuarioAuditoria, IDbTransaction transaction); // Ya existe | ||||||
|         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,5 +1,4 @@ | |||||||
| 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; | ||||||
| @@ -45,21 +44,6 @@ 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,32 +169,6 @@ 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,6 +1,5 @@ | |||||||
| 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; | ||||||
| @@ -46,25 +45,6 @@ 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,14 +2,12 @@ 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,5 +1,3 @@ | |||||||
| // --- 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; | ||||||
| @@ -8,12 +6,11 @@ using System.Threading.Tasks; | |||||||
|  |  | ||||||
| namespace GestionIntegral.Api.Data.Repositories.Impresion | namespace GestionIntegral.Api.Data.Repositories.Impresion | ||||||
| { | { | ||||||
|     public interface IRegTiradaRepository |     public interface IRegTiradaRepository // Para bob_RegTiradas | ||||||
|     { |     { | ||||||
|         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); | ||||||
| @@ -30,11 +27,9 @@ 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,7 +8,6 @@ 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,42 +83,6 @@ 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 | ||||||
| @@ -312,66 +276,6 @@ 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"); |                 sqlBuilder.Append(" AND sb.FechaRemito >= @FechaDesdeParam"); // O FechaEstado según el contexto del filtro | ||||||
|                 parameters.Add("FechaDesdeParam", fechaDesde.Value.Date); |                 parameters.Add("FechaDesdeParam", fechaDesde.Value.Date); | ||||||
|             } |             } | ||||||
|             if (fechaHasta.HasValue) |             if (fechaHasta.HasValue) | ||||||
|             { |             { | ||||||
|                 sqlBuilder.Append(" AND sb.FechaRemito <= @FechaHastaParam"); |                 sqlBuilder.Append(" AND sb.FechaRemito <= @FechaHastaParam"); // O FechaEstado | ||||||
|                 parameters.Add("FechaHastaParam", fechaHasta.Value.Date); |                 parameters.Add("FechaHastaParam", fechaHasta.Value.Date.AddDays(1).AddTicks(-1)); // Hasta el final del día | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             sqlBuilder.Append(" ORDER BY sb.FechaRemito DESC, sb.NroBobina;"); |             sqlBuilder.Append(" ORDER BY sb.FechaRemito DESC, sb.NroBobina;"); | ||||||
| @@ -224,12 +224,14 @@ 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,24 +44,6 @@ 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,8 +45,5 @@ 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,111 +547,5 @@ 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>(); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -1,139 +0,0 @@ | |||||||
| 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 }); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,58 +0,0 @@ | |||||||
| 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>(); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,271 +0,0 @@ | |||||||
| 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, TipoFactura) |  | ||||||
|                 OUTPUT INSERTED.* |  | ||||||
|                 VALUES  |  | ||||||
|                     (@IdSuscriptor, @Periodo, @FechaEmision, @FechaVencimiento, @ImporteBruto,  |  | ||||||
|                      @DescuentoAplicado, @ImporteFinal, @EstadoPago, @EstadoFacturacion, @TipoFactura);"; |  | ||||||
|  |  | ||||||
|             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, string? tipoFactura) |  | ||||||
|         { |  | ||||||
|             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); |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             if (!string.IsNullOrWhiteSpace(tipoFactura)) |  | ||||||
|             { |  | ||||||
|                 sqlBuilder.Append(" AND f.TipoFactura = @TipoFactura"); |  | ||||||
|                 parameters.Add("TipoFactura", tipoFactura); |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             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 }); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,54 +0,0 @@ | |||||||
| 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; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,22 +0,0 @@ | |||||||
| // 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); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,22 +0,0 @@ | |||||||
| 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); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,24 +0,0 @@ | |||||||
| 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, string? tipoFactura); |  | ||||||
|         Task<bool> UpdateEstadoYMotivoAsync(int idFactura, string nuevoEstadoPago, string? motivoRechazo, IDbTransaction transaction);    |  | ||||||
|         Task<string?> GetUltimoPeriodoFacturadoAsync(); |  | ||||||
|         Task<IEnumerable<Factura>> GetFacturasPagadasPendientesDeFacturar(string periodo); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,10 +0,0 @@ | |||||||
| using GestionIntegral.Api.Models.Suscripciones; |  | ||||||
|  |  | ||||||
| namespace GestionIntegral.Api.Data.Repositories.Suscripciones |  | ||||||
| { |  | ||||||
|     public interface IFormaPagoRepository |  | ||||||
|     { |  | ||||||
|         Task<IEnumerable<FormaPago>> GetAllAsync(); |  | ||||||
|         Task<FormaPago?> GetByIdAsync(int id); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,10 +0,0 @@ | |||||||
| using GestionIntegral.Api.Models.Suscripciones; |  | ||||||
| using System.Data; |  | ||||||
|  |  | ||||||
| namespace GestionIntegral.Api.Data.Repositories.Suscripciones |  | ||||||
| { |  | ||||||
|   public interface ILoteDebitoRepository |  | ||||||
|   { |  | ||||||
|     Task<LoteDebito?> CreateAsync(LoteDebito nuevoLote, IDbTransaction transaction); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -1,12 +0,0 @@ | |||||||
| 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); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,15 +0,0 @@ | |||||||
| 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); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,17 +0,0 @@ | |||||||
| 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); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,16 +0,0 @@ | |||||||
| 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); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,43 +0,0 @@ | |||||||
| 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; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,68 +0,0 @@ | |||||||
| 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); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,118 +0,0 @@ | |||||||
| 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 }); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,156 +0,0 @@ | |||||||
| 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; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,195 +0,0 @@ | |||||||
| 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,5 +1,3 @@ | |||||||
| // 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; | ||||||
| @@ -12,7 +10,6 @@ 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); | ||||||
| @@ -20,6 +17,7 @@ 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,8 +1,12 @@ | |||||||
| 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 | ||||||
| { | { | ||||||
| @@ -84,6 +88,7 @@ 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"; | ||||||
| @@ -98,33 +103,6 @@ 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 = @" | ||||||
| @@ -150,6 +128,7 @@ 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,7 +9,6 @@ | |||||||
|   <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="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" /> | ||||||
|   | |||||||
| @@ -1,16 +0,0 @@ | |||||||
| 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; } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,16 +0,0 @@ | |||||||
| 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; } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,12 +0,0 @@ | |||||||
| 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; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,24 +0,0 @@ | |||||||
| 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 |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,20 +0,0 @@ | |||||||
| 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; } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,26 +0,0 @@ | |||||||
| 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; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,11 +0,0 @@ | |||||||
| 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(); |  | ||||||
| } |  | ||||||
| @@ -1,9 +0,0 @@ | |||||||
| 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; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,8 +0,0 @@ | |||||||
| namespace GestionIntegral.Api.Dtos.Distribucion |  | ||||||
| { |  | ||||||
|     public class OtroDestinoDropdownDto |  | ||||||
|     { |  | ||||||
|         public int IdDestino { get; set; } |  | ||||||
|         public string Nombre { get; set; } = string.Empty; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -4,7 +4,6 @@ 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 string NombreEmpresa { get; set; } = string.Empty; |         public bool Habilitada { get; set; } // Simplificamos a bool, el backend manejará el default si es null | ||||||
|         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; }  |         public bool Habilitada { get; set; } // Simplificamos a bool, el backend manejará el default si es null | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -1,8 +0,0 @@ | |||||||
| namespace GestionIntegral.Api.Dtos.Impresion |  | ||||||
| { |  | ||||||
|     public class EstadoBobinaDropdownDto |  | ||||||
|     { |  | ||||||
|         public int IdEstadoBobina { get; set; } |  | ||||||
|         public string Denominacion { get; set; } = string.Empty; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,17 +0,0 @@ | |||||||
| 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; } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,15 +0,0 @@ | |||||||
| 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>(); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,15 +0,0 @@ | |||||||
| 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; } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,14 +0,0 @@ | |||||||
| 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; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,55 +0,0 @@ | |||||||
| 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); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,18 +0,0 @@ | |||||||
| 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,6 +14,8 @@ 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 | ||||||
| @@ -23,29 +25,20 @@ namespace GestionIntegral.Api.Dtos.Reportes.ViewModels | |||||||
|                     return null; |                     return null; | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 // 1. Filtrar solo los días con actividad para no diluir el promedio. |                 // Contar solo los días con tirada > 0 para promediar correctamente | ||||||
|                 var diasActivos = ResumenMensual.Where(r => r.CantidadTirada > 0).ToList(); |                 var diasConTirada = ResumenMensual.Count(d => d.CantidadTirada > 0); | ||||||
|  |                 if (diasConTirada == 0) return null; | ||||||
|                 if (!diasActivos.Any()) |  | ||||||
|                 { |  | ||||||
|                     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 = totalDiasActivos, |                     CantidadDias = diasConTirada, | ||||||
|                     // 3. Calcular el promedio real: Suma de valores / Cantidad de días activos. |                     PromedioTirada = (int)ResumenMensual.Average(r => r.CantidadTirada), | ||||||
|                     // Se usa división entera para que coincida con el formato sin decimales. |                     PromedioSinCargo = (int)ResumenMensual.Average(r => r.SinCargo), | ||||||
|                     PromedioTirada = diasActivos.Sum(r => r.CantidadTirada) / totalDiasActivos, |                     PromedioPerdidos = (int)ResumenMensual.Average(r => r.Perdidos), | ||||||
|                     PromedioSinCargo = diasActivos.Sum(r => r.SinCargo) / totalDiasActivos, |                     PromedioLlevados = (int)ResumenMensual.Average(r => r.Llevados), | ||||||
|                     PromedioPerdidos = diasActivos.Sum(r => r.Perdidos) / totalDiasActivos, |                     PromedioDevueltos = (int)ResumenMensual.Average(r => r.Devueltos), | ||||||
|                     PromedioLlevados = diasActivos.Sum(r => r.Llevados) / totalDiasActivos, |                     PromedioVendidos = (int)ResumenMensual.Average(r => r.Vendidos) | ||||||
|                     PromedioDevueltos = diasActivos.Sum(r => r.Devueltos) / totalDiasActivos, |  | ||||||
|                     PromedioVendidos = diasActivos.Sum(r => r.Vendidos) / totalDiasActivos |  | ||||||
|                 }; |                 }; | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -1,19 +0,0 @@ | |||||||
| 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; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,13 +0,0 @@ | |||||||
| 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; } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,28 +0,0 @@ | |||||||
| 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; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,29 +0,0 @@ | |||||||
| // 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; } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,31 +0,0 @@ | |||||||
| // 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; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,30 +0,0 @@ | |||||||
| 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. |  | ||||||
| @@ -1,44 +0,0 @@ | |||||||
| // 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; } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,16 +0,0 @@ | |||||||
| 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 decimal TotalPagado { get; set; } |  | ||||||
|         public string TipoFactura { get; set; } = string.Empty; |  | ||||||
|         public int IdSuscriptor { get; set; } |  | ||||||
|         public List<FacturaDetalleDto> Detalles { get; set; } = new List<FacturaDetalleDto>(); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,25 +0,0 @@ | |||||||
| 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>(); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,9 +0,0 @@ | |||||||
| 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; } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,17 +0,0 @@ | |||||||
| 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 |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,11 +0,0 @@ | |||||||
| 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; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,8 +0,0 @@ | |||||||
| namespace GestionIntegral.Api.Dtos.Suscripciones |  | ||||||
| { |  | ||||||
|     public class PromocionAsignadaDto : PromocionDto |  | ||||||
|     { |  | ||||||
|         public string VigenciaDesdeAsignacion { get; set; } = string.Empty; |  | ||||||
|         public string? VigenciaHastaAsignacion { get; set; } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,15 +0,0 @@ | |||||||
| 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; } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,11 +0,0 @@ | |||||||
| 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>(); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,15 +0,0 @@ | |||||||
| 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; } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,19 +0,0 @@ | |||||||
| 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; } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,23 +0,0 @@ | |||||||
| using System.ComponentModel.DataAnnotations; |  | ||||||
|  |  | ||||||
| namespace GestionIntegral.Api.Dtos.Suscripciones; |  | ||||||
| public class UpdateAjusteDto |  | ||||||
| { |  | ||||||
|   [Required] |  | ||||||
|   public int IdEmpresa { get; set; } |  | ||||||
|  |  | ||||||
|   [Required] |  | ||||||
|   public DateTime FechaAjuste { get; set; } |  | ||||||
|  |  | ||||||
|   [Required] |  | ||||||
|   [RegularExpression("^(Credito|Debito)$")] |  | ||||||
|   public string TipoAjuste { get; set; } = string.Empty; |  | ||||||
|  |  | ||||||
|   [Required] |  | ||||||
|   [Range(0.01, 999999.99)] |  | ||||||
|   public decimal Monto { get; set; } |  | ||||||
|  |  | ||||||
|   [Required] |  | ||||||
|   [StringLength(250)] |  | ||||||
|   public string Motivo { get; set; } = string.Empty; |  | ||||||
| } |  | ||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user