Compare commits
	
		
			57 Commits
		
	
	
		
			Suscripcio
			...
			8101756638
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 8101756638 | |||
| b66d00c92d | |||
| 72c2f7ee31 | |||
| 1456ccd723 | |||
| 229f657685 | |||
| 13ab496727 | |||
| 5adc1e6d46 | |||
| 1ec21741cc | |||
| b33dd4f94f | |||
| ba9bef2364 | |||
| 856d7ac5c1 | |||
| d8dc41222c | |||
| a434640456 | |||
| fcc2b90f15 | |||
| 0d7614ef4c | |||
| ae54ef3fe5 | |||
| c361842a91 | |||
| de079d8bd4 | |||
| 401e61e0eb | |||
| 4b208793ce | |||
| d60dd8dc9f | |||
| 8156df9f90 | |||
| 55ca8bffa7 | |||
| 02265a46e7 | |||
| 767cd081dc | |||
| 97b6a9241f | |||
| b68ac1fed1 | |||
| 2e83c2e373 | |||
| 1e8d5fd308 | |||
| a1dfd0d089 | |||
| eff65921e6 | |||
| b3de8dba3a | |||
| f62fc4b507 | |||
| 66686fc548 | |||
| c0900e07e6 | |||
| a63b40471b | |||
| 12decefc1b | |||
| bb79ccf64c | |||
| b3e70a3988 | |||
| 55640c394f | |||
| 338cd8579f | |||
| 69620c5607 | |||
| 8ba18ed687 | |||
| 30570beaca | |||
| db10fa0254 | |||
| 1d664672b2 | |||
| 969c78a567 | |||
| 0b884197fb | |||
| 0d2c30ad94 | |||
| f1591bd572 | |||
| 24e4769f78 | |||
| 76d48cc310 | |||
| b072e31385 | |||
| 65be2bcbaa | |||
| 0fb3cb7aef | |||
| 541625bf66 | |||
| cbe313b59d | 
							
								
								
									
										82
									
								
								.drone.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								.drone.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,82 @@ | ||||
| kind: pipeline | ||||
| type: docker | ||||
| name: Build y Deploy | ||||
|  | ||||
| trigger: | ||||
|   branch: | ||||
|   - main | ||||
|   event: | ||||
|   - push | ||||
|  | ||||
| steps: | ||||
| - name: build-and-publish-backend | ||||
|   image: plugins/kaniko | ||||
|   settings: | ||||
|     repo: host.docker.internal:5000/${DRONE_REPO_OWNER}/${DRONE_REPO_NAME,,}-backend | ||||
|     tags: | ||||
|       - latest | ||||
|       - ${DRONE_COMMIT_SHA:0:8} | ||||
|     dockerfile: Backend/GestionIntegral.Api/Dockerfile | ||||
|     context: . | ||||
|     username: | ||||
|       from_secret: GITEA_USER | ||||
|     password: | ||||
|       from_secret: ACTIONS_PAT | ||||
|     insecure: true | ||||
|  | ||||
| - name: build-and-publish-frontend | ||||
|   image: plugins/kaniko | ||||
|   settings: | ||||
|     repo: host.docker.internal:5000/${DRONE_REPO_OWNER}/${DRONE_REPO_NAME,,}-frontend | ||||
|     tags: | ||||
|       - latest | ||||
|       - ${DRONE_COMMIT_SHA:0:8} | ||||
|     dockerfile: Frontend/Dockerfile | ||||
|     context: . | ||||
|     username: | ||||
|       from_secret: GITEA_USER | ||||
|     password: | ||||
|       from_secret: ACTIONS_PAT | ||||
|     insecure: true | ||||
|   depends_on: | ||||
|     - build-and-publish-backend | ||||
|  | ||||
| - name: deploy-to-production | ||||
|   image: alpine:latest | ||||
|   environment: | ||||
|     SSH_KEY: | ||||
|       from_secret: PROD_SERVER_SSH_KEY | ||||
|     PROD_HOST: | ||||
|       from_secret: PROD_SERVER_HOST | ||||
|     PROD_USER: | ||||
|       from_secret: PROD_SERVER_USER | ||||
|     DB_PASSWORD: | ||||
|       from_secret: DB_SA_PASSWORD_SECRET | ||||
|     JWT_KEY: | ||||
|       from_secret: JWT_KEY_SECRET | ||||
|     REGISTRY: | ||||
|       from_secret: REGISTRY_URL | ||||
|     GITEA_USER: | ||||
|       from_secret: GITEA_USER | ||||
|     GITEA_PAT: | ||||
|       from_secret: ACTIONS_PAT | ||||
|   commands: | ||||
|     - apk add --no-cache openssh-client | ||||
|     - mkdir -p ~/.ssh | ||||
|     - echo "$SSH_KEY" > ~/.ssh/id_rsa | ||||
|     - chmod 600 ~/.ssh/id_rsa | ||||
|     - ssh-keyscan -H $PROD_HOST >> ~/.ssh/known_hosts | ||||
|     - | | ||||
|       ssh $PROD_USER@$PROD_HOST << 'EOF' | ||||
|         echo "--- CONECTADO AL SERVIDOR DE PRODUCCIÓN ---" | ||||
|         cd /opt/gestion-integral | ||||
|         export DB_SA_PASSWORD="${DB_PASSWORD}" | ||||
|         export JWT_KEY="${JWT_KEY}" | ||||
|         docker login ${REGISTRY} -u ${GITEA_USER} -p ${GITEA_PAT} | ||||
|         docker compose pull | ||||
|         docker compose up -d | ||||
|         docker image prune -af | ||||
|         echo "--- DESPLIEGUE COMPLETADO ---" | ||||
|       EOF | ||||
|   depends_on: | ||||
|     - build-and-publish-frontend | ||||
| @@ -14,7 +14,7 @@ jobs: | ||||
|         run: | | ||||
|           set -e | ||||
|            | ||||
|           # Configura SSH (sin cambios) | ||||
|           # Configura SSH | ||||
|           apt-get update -qq && apt-get install -y openssh-client git | ||||
|           mkdir -p ~/.ssh | ||||
|           echo "${{ secrets.PROD_SERVER_SSH_KEY }}" > ~/.ssh/id_rsa | ||||
| @@ -26,13 +26,7 @@ jobs: | ||||
|             set -e | ||||
|             echo "--- INICIO DEL DESPLIEGUE OPTIMIZADO ---" | ||||
|              | ||||
|             # --- Asegurar que el Stack de la Base de Datos esté corriendo --- | ||||
|             echo "Asegurando que el stack de la base de datos esté activo..." | ||||
|             cd /opt/shared-services/database | ||||
|             # El comando 'up -d' es idempotente. Si ya está corriendo, no hace nada. | ||||
|             docker compose up -d | ||||
|              | ||||
|             # 1. Preparar entorno | ||||
|             # 1. Preparar entorno (sin cambios) | ||||
|             TEMP_DIR=$(mktemp -d) | ||||
|             REPO_OWNER="dmolinari" | ||||
|             REPO_NAME="gestionintegralweb" | ||||
| @@ -43,7 +37,7 @@ jobs: | ||||
|             cd "$TEMP_DIR" | ||||
|             git checkout "${{ gitea.sha }}" | ||||
|              | ||||
|             # 2. Construcción paralela | ||||
|             # 2. Construcción paralela (sin cambios) | ||||
|             build_image() { | ||||
|               local dockerfile=$1 | ||||
|               local image_name=$2 | ||||
| @@ -61,17 +55,31 @@ jobs: | ||||
|             (build_image "Frontend/Dockerfile" "dmolinari/gestionintegralweb-frontend:latest" ".") & | ||||
|             wait | ||||
|              | ||||
|             # 3. Despliegue con Docker Compose | ||||
|             cd /opt/gestion-integral | ||||
|             export DB_SA_PASSWORD='${{ secrets.DB_SA_PASSWORD_SECRET }}' | ||||
|             # Copiamos la versión actualizada del docker-compose.yml al directorio de despliegue. | ||||
|             echo "Copiando el archivo docker-compose.yml actualizado..." | ||||
|             cp "$TEMP_DIR/docker-compose.yml" /opt/gestion-integral/docker-compose.yml | ||||
|              | ||||
|             echo "Recreando servicios de la aplicación..." | ||||
|             docker compose up -d --force-recreate | ||||
|             # (Opcional pero recomendado) Verificamos que el archivo se copió bien | ||||
|             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..." | ||||
|             rm -rf "$TEMP_DIR" | ||||
|             docker image prune -f --filter "dangling=true" | ||||
|             docker image prune -af --filter "until=24h" | ||||
|              | ||||
|             echo "--- DESPLIEGUE COMPLETADO CON ÉXITO ---" | ||||
|           EOSSH | ||||
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -19,6 +19,9 @@ lerna-debug.log* | ||||
|  | ||||
| # 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.development.local | ||||
| .env.test.local | ||||
|   | ||||
| @@ -1,12 +0,0 @@ | ||||
| # ================================================ | ||||
| # VARIABLES DE ENTORNO PARA LA CONFIGURACIÓN DE CORREO | ||||
| # ================================================ | ||||
| # El separador de doble guion bajo (__) se usa para mapear la jerarquía del JSON. | ||||
| # MailSettings:SmtpHost se convierte en MailSettings__SmtpHost | ||||
|  | ||||
| MailSettings__SmtpHost="mail.eldia.com" | ||||
| MailSettings__SmtpPort=587 | ||||
| MailSettings__SenderName="Club - Diario El Día" | ||||
| MailSettings__SenderEmail="alertas@eldia.com" | ||||
| MailSettings__SmtpUser="alertas@eldia.com" | ||||
| MailSettings__SmtpPass="@Alertas713550@" | ||||
| @@ -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) | ||||
|         { | ||||
|             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); | ||||
|         } | ||||
|  | ||||
|         [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} | ||||
|         [HttpGet("{id:int}", Name = "GetCanillaById")] | ||||
|         [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} | ||||
|         [HttpGet("{id:int}", Name = "GetOtroDestinoById")] | ||||
|         [ProducesResponseType(typeof(OtroDestinoDto), StatusCodes.Status200OK)] | ||||
|   | ||||
| @@ -42,7 +42,7 @@ namespace GestionIntegral.Api.Controllers.Distribucion | ||||
|         [HttpGet] | ||||
|         [ProducesResponseType(typeof(IEnumerable<PublicacionDto>), StatusCodes.Status200OK)] | ||||
|         [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(); | ||||
|             var publicaciones = await _publicacionService.ObtenerTodasAsync(nombre, idEmpresa, soloHabilitadas); | ||||
| @@ -54,7 +54,7 @@ namespace GestionIntegral.Api.Controllers.Distribucion | ||||
|         [ProducesResponseType(typeof(IEnumerable<PublicacionDropdownDto>), StatusCodes.Status200OK)] | ||||
|         [ProducesResponseType(StatusCodes.Status500InternalServerError)] | ||||
|         // 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 | ||||
|             { | ||||
|   | ||||
| @@ -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} | ||||
|         [HttpGet("{id:int}", Name = "GetEstadoBobinaById")] | ||||
|         [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} | ||||
|         // Permiso: IB006 (Ver Tipos Bobinas) | ||||
|         [HttpGet("{id:int}", Name = "GetTipoBobinaById")] | ||||
|   | ||||
| @@ -22,8 +22,7 @@ namespace GestionIntegral.Api.Controllers.Impresion | ||||
|         // Permisos para Tiradas (IT001 a IT003) | ||||
|         private const string PermisoVerTiradas = "IT001"; | ||||
|         private const string PermisoRegistrarTirada = "IT002"; | ||||
|         private const string PermisoEliminarTirada = "IT003"; | ||||
|         private const string PermisoModificarTirada = "IT004"; | ||||
|         private const string PermisoEliminarTirada = "IT003"; // Asumo que se refiere a eliminar una tirada completa (cabecera y detalles) | ||||
|  | ||||
|         public TiradasController(ITiradaService tiradaService, ILogger<TiradasController> logger) | ||||
|         { | ||||
| @@ -84,43 +83,6 @@ namespace GestionIntegral.Api.Controllers.Impresion | ||||
|             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 | ||||
|         // Se identifica la tirada a eliminar por su combinación única de Fecha, IdPublicacion, IdPlanta | ||||
|         [HttpDelete] | ||||
|   | ||||
| @@ -1,9 +1,12 @@ | ||||
| // --- REEMPLAZAR ARCHIVO: Controllers/Reportes/PdfTemplates/DistribucionCanillasDocument.cs --- | ||||
| using GestionIntegral.Api.Dtos.Reportes; | ||||
| using GestionIntegral.Api.Dtos.Reportes.ViewModels; | ||||
| using QuestPDF.Fluent; | ||||
| using QuestPDF.Helpers; | ||||
| using QuestPDF.Infrastructure; | ||||
| using System.Collections.Generic; | ||||
| using System.Globalization; | ||||
| using System.Linq; | ||||
|  | ||||
| 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 => | ||||
|                 { | ||||
|                     columns.ConstantColumn(60); | ||||
|                     columns.ConstantColumn(40); | ||||
|                     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"); | ||||
|                 }); | ||||
|  | ||||
|                 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)) | ||||
|                 { | ||||
|                     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.SinCargo.ToString("N0")); | ||||
|                     table.Cell().Border(1).Padding(3).AlignRight().Text(item.Perdidos.ToString("N0")); | ||||
|   | ||||
| @@ -1,8 +1,15 @@ | ||||
| using GestionIntegral.Api.Services.Reportes; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| 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.Data.Repositories.Impresion; | ||||
| using System.IO; | ||||
| using System.Linq; | ||||
| using GestionIntegral.Api.Data.Repositories.Distribucion; | ||||
| using GestionIntegral.Api.Services.Distribucion; | ||||
| using GestionIntegral.Api.Services.Pdf; | ||||
| @@ -38,8 +45,6 @@ namespace GestionIntegral.Api.Controllers | ||||
|         private const string PermisoVerReporteConsumoBobinas = "RR007"; | ||||
|         private const string PermisoVerReporteNovedadesCanillas = "RR004"; | ||||
|         private const string PermisoVerReporteListadoDistMensual = "RR009"; | ||||
|         private const string PermisoVerReporteFacturasPublicidad = "RR010"; | ||||
|         private const string PermisoVerReporteDistSuscripciones = "RR011"; | ||||
|  | ||||
|         public ReportesController( | ||||
|             IReportesService reportesService, | ||||
| @@ -1671,88 +1676,5 @@ namespace GestionIntegral.Api.Controllers | ||||
|                 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,125 +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) | ||||
|         { | ||||
|             if (!TienePermiso(PermisoGestionarFacturacion)) return Forbid(); | ||||
|             if (anio < 2020 || mes < 1 || mes > 12) return BadRequest(new { message = "El período no es válido." }); | ||||
|  | ||||
|             var resumenes = await _facturacionService.ObtenerResumenesDeCuentaPorPeriodo(anio, mes, nombreSuscriptor, estadoPago, estadoFacturacion); | ||||
|             return Ok(resumenes); | ||||
|         } | ||||
|  | ||||
|         [HttpPost("{anio:int}/{mes:int}")] | ||||
|         public async Task<IActionResult> GenerarFacturacion(int anio, int mes) | ||||
|         { | ||||
|             if (!TienePermiso(PermisoGestionarFacturacion)) return Forbid(); | ||||
|             var userId = GetCurrentUserId(); | ||||
|             if (userId == null) return Unauthorized(); | ||||
|             if (anio < 2020 || mes < 1 || mes > 12) return BadRequest(new { message = "El año y el mes proporcionados no son válidos." }); | ||||
|  | ||||
|             var (exito, mensaje, resultadoEnvio) = await _facturacionService.GenerarFacturacionMensual(anio, mes, userId.Value); | ||||
|  | ||||
|             if (!exito) return StatusCode(StatusCodes.Status500InternalServerError, new { message = mensaje }); | ||||
|  | ||||
|             return Ok(new { message = mensaje, resultadoEnvio }); | ||||
|         } | ||||
|  | ||||
|         [HttpGet("historial-lotes-envio")] | ||||
|         [ProducesResponseType(typeof(IEnumerable<LoteDeEnvioHistorialDto>), StatusCodes.Status200OK)] | ||||
|         public async Task<IActionResult> GetHistorialLotesEnvio([FromQuery] int? anio, [FromQuery] int? mes) | ||||
|         { | ||||
|             if (!TienePermiso("SU006")) return Forbid(); | ||||
|             var historial = await _facturacionService.ObtenerHistorialLotesEnvio(anio, mes); | ||||
|             return Ok(historial); | ||||
|         } | ||||
|  | ||||
|         // Endpoint para el historial de envíos de una factura individual | ||||
|         [HttpGet("{idFactura:int}/historial-envios")] | ||||
|         [ProducesResponseType(typeof(IEnumerable<EmailLogDto>), StatusCodes.Status200OK)] | ||||
|         public async Task<IActionResult> GetHistorialEnvios(int idFactura) | ||||
|         { | ||||
|             if (!TienePermiso(PermisoGestionarFacturacion)) return Forbid(); // Reutilizamos el permiso | ||||
|  | ||||
|             // Construimos la referencia que se guarda en el log | ||||
|             string referencia = $"Factura-{idFactura}"; | ||||
|             var historial = await _emailLogService.ObtenerHistorialPorReferencia(referencia); | ||||
|  | ||||
|             return Ok(historial); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -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 GestionIntegral.Api.Dtos.Distribucion; | ||||
| using GestionIntegral.Api.Models.Distribucion; | ||||
| using Microsoft.Extensions.Logging; | ||||
| using System; // Para Exception | ||||
| @@ -26,7 +25,7 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion | ||||
|     string? nomApeFilter, | ||||
|     int? legajoFilter, | ||||
|     bool? esAccionista, | ||||
|     bool? soloActivos) | ||||
|     bool? soloActivos) // <<-- Parámetro aquí | ||||
|         { | ||||
|             using var connection = _connectionFactory.CreateConnection(); | ||||
|             var sqlBuilder = new System.Text.StringBuilder(@" | ||||
| @@ -74,37 +73,6 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion | ||||
|             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) | ||||
|         { | ||||
|             const string sql = @" | ||||
|   | ||||
| @@ -2,14 +2,12 @@ using GestionIntegral.Api.Models.Distribucion; | ||||
| using System.Collections.Generic; | ||||
| using System.Threading.Tasks; | ||||
| using System.Data; | ||||
| using GestionIntegral.Api.Dtos.Distribucion; | ||||
|  | ||||
| namespace GestionIntegral.Api.Data.Repositories.Distribucion | ||||
| { | ||||
|     public interface ICanillaRepository | ||||
|     { | ||||
|         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?> GetByIdSimpleAsync(int id); // Para obtener solo la entidad Canilla | ||||
|         Task<Canilla?> CreateAsync(Canilla nuevoCanilla, int idUsuario, IDbTransaction transaction); | ||||
|   | ||||
| @@ -2,14 +2,12 @@ using GestionIntegral.Api.Models.Distribucion; | ||||
| using System.Collections.Generic; | ||||
| using System.Threading.Tasks; | ||||
| using System.Data; | ||||
| using GestionIntegral.Api.Dtos.Distribucion; | ||||
|  | ||||
| namespace GestionIntegral.Api.Data.Repositories.Distribucion | ||||
| { | ||||
|     public interface IOtroDestinoRepository | ||||
|     { | ||||
|         Task<IEnumerable<OtroDestino>> GetAllAsync(string? nombreFilter); | ||||
|         Task<IEnumerable<OtroDestinoDropdownDto>> GetAllDropdownAsync(); | ||||
|         Task<OtroDestino?> GetByIdAsync(int id); | ||||
|         Task<OtroDestino?> CreateAsync(OtroDestino nuevoDestino, 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<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<bool> UpdateAsync(PubliSeccion seccionAActualizar, 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> IsInUseAsync(int idSeccion); // Verificar en bob_RegPublicaciones, bob_StockBobinas | ||||
|         Task<IEnumerable<(PubliSeccionHistorico Historial, string NombreUsuarioModifico)>> GetHistorialAsync( | ||||
|   | ||||
| @@ -1,5 +1,4 @@ | ||||
| using Dapper; | ||||
| using GestionIntegral.Api.Dtos.Distribucion; | ||||
| using GestionIntegral.Api.Models.Distribucion; | ||||
| using Microsoft.Extensions.Logging; | ||||
| 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) | ||||
|         { | ||||
|             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; | ||||
|         } | ||||
|  | ||||
|         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) | ||||
|         { | ||||
|             var actual = await transaction.Connection!.QuerySingleOrDefaultAsync<PubliSeccion>( | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| using Dapper; | ||||
| using GestionIntegral.Api.Data.Repositories.Impresion; | ||||
| using GestionIntegral.Api.Dtos.Impresion; | ||||
| using GestionIntegral.Api.Models.Impresion; | ||||
| using Microsoft.Extensions.Logging; | ||||
| 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) | ||||
|         { | ||||
|             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.Threading.Tasks; | ||||
| using System.Data; | ||||
| using GestionIntegral.Api.Dtos.Impresion; | ||||
|  | ||||
| namespace GestionIntegral.Api.Data.Repositories.Impresion | ||||
| { | ||||
|     public interface IEstadoBobinaRepository | ||||
|     { | ||||
|         Task<IEnumerable<EstadoBobina>> GetAllAsync(string? denominacionFilter); | ||||
|         Task<IEnumerable<EstadoBobinaDropdownDto>> GetAllDropdownAsync(); | ||||
|         Task<EstadoBobina?> GetByIdAsync(int id); | ||||
|         Task<EstadoBobina?> CreateAsync(EstadoBobina nuevoEstadoBobina, 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 System; | ||||
| using System.Collections.Generic; | ||||
| @@ -8,12 +6,11 @@ using System.Threading.Tasks; | ||||
|  | ||||
| namespace GestionIntegral.Api.Data.Repositories.Impresion | ||||
| { | ||||
|     public interface IRegTiradaRepository | ||||
|     public interface IRegTiradaRepository // Para bob_RegTiradas | ||||
|     { | ||||
|         Task<RegTirada?> GetByIdAsync(int idRegistro); | ||||
|         Task<IEnumerable<RegTirada>> GetByCriteriaAsync(DateTime? fecha, int? idPublicacion, int? idPlanta); | ||||
|         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> DeleteByFechaPublicacionPlantaAsync(DateTime fecha, int idPublicacion, int idPlanta, int idUsuario, IDbTransaction transaction); | ||||
|         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 | ||||
|     { | ||||
|         Task<RegPublicacionSeccion?> GetByIdAsync(int idTirada); | ||||
|         Task<IEnumerable<RegPublicacionSeccion>> GetByFechaPublicacionPlantaAsync(DateTime fecha, int idPublicacion, int idPlanta); | ||||
|         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); | ||||
|         // 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 | ||||
|     { | ||||
|         Task<IEnumerable<TipoBobina>> GetAllAsync(string? denominacionFilter); | ||||
|         Task<IEnumerable<TipoBobina>> GetAllDropdownAsync(); | ||||
|         Task<TipoBobina?> GetByIdAsync(int id); | ||||
|         Task<TipoBobina?> CreateAsync(TipoBobina nuevoTipoBobina, 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; | ||||
|         } | ||||
|  | ||||
|         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) | ||||
|         { | ||||
|             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; | ||||
|         } | ||||
|  | ||||
|         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) | ||||
|         { | ||||
|             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) | ||||
|             { | ||||
|                 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); | ||||
|             } | ||||
|             if (fechaHasta.HasValue) | ||||
|             { | ||||
|                 sqlBuilder.Append(" AND sb.FechaRemito <= @FechaHastaParam"); | ||||
|                 parameters.Add("FechaHastaParam", fechaHasta.Value.Date); | ||||
|                 sqlBuilder.Append(" AND sb.FechaRemito <= @FechaHastaParam"); // O FechaEstado | ||||
|                 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;"); | ||||
| @@ -224,12 +224,14 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion | ||||
|  | ||||
|             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) | ||||
|             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); | ||||
|                 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 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) | ||||
|         { | ||||
|             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<ListadoDistCanMensualDiariosDto>> GetReporteMensualDiariosAsync(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 | ||||
|             ); | ||||
|         } | ||||
|  | ||||
|         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,259 +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) | ||||
|                 OUTPUT INSERTED.* | ||||
|                 VALUES (@IdSuscriptor, @Periodo, @FechaEmision, @FechaVencimiento, @ImporteBruto, @DescuentoAplicado, @ImporteFinal, @EstadoPago, @EstadoFacturacion);"; | ||||
|  | ||||
|             return await transaction.Connection.QuerySingleAsync<Factura>(sqlInsert, nuevaFactura, transaction); | ||||
|         } | ||||
|  | ||||
|         public async Task<bool> UpdateEstadoPagoAsync(int idFactura, string nuevoEstadoPago, IDbTransaction transaction) | ||||
|         { | ||||
|             if (transaction == null || transaction.Connection == null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(transaction), "La transacción o su conexión no pueden ser nulas."); | ||||
|             } | ||||
|             const string sql = "UPDATE dbo.susc_Facturas SET EstadoPago = @NuevoEstadoPago WHERE IdFactura = @IdFactura;"; | ||||
|             var rowsAffected = await transaction.Connection.ExecuteAsync(sql, new { NuevoEstadoPago = nuevoEstadoPago, idFactura }, transaction); | ||||
|             return rowsAffected == 1; | ||||
|         } | ||||
|  | ||||
|         public async Task<bool> UpdateNumeroFacturaAsync(int idFactura, string numeroFactura, IDbTransaction transaction) | ||||
|         { | ||||
|             if (transaction == null || transaction.Connection == null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(transaction), "La transacción o su conexión no pueden ser nulas."); | ||||
|             } | ||||
|             const string sql = @" | ||||
|                 UPDATE dbo.susc_Facturas SET  | ||||
|                     NumeroFactura = @NumeroFactura,  | ||||
|                     EstadoFacturacion = 'Facturado'  | ||||
|                 WHERE IdFactura = @IdFactura;"; | ||||
|             var rowsAffected = await transaction.Connection.ExecuteAsync(sql, new { NumeroFactura = numeroFactura, idFactura }, transaction); | ||||
|             return rowsAffected == 1; | ||||
|         } | ||||
|  | ||||
|         public async Task<bool> UpdateLoteDebitoAsync(IEnumerable<int> idsFacturas, int idLoteDebito, IDbTransaction transaction) | ||||
|         { | ||||
|             if (transaction == null || transaction.Connection == null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(transaction), "La transacción o su conexión no pueden ser nulas."); | ||||
|             } | ||||
|             const string sql = "UPDATE dbo.susc_Facturas SET IdLoteDebito = @IdLoteDebito WHERE IdFactura IN @IdsFacturas;"; | ||||
|             var rowsAffected = await transaction.Connection.ExecuteAsync(sql, new { IdLoteDebito = idLoteDebito, IdsFacturas = idsFacturas }, transaction); | ||||
|             return rowsAffected == idsFacturas.Count(); | ||||
|         } | ||||
|  | ||||
|         public async Task<IEnumerable<(Factura Factura, string NombreSuscriptor, int IdEmpresa, decimal TotalPagado)>> GetByPeriodoEnrichedAsync(string periodo, string? nombreSuscriptor, string? estadoPago, string? estadoFacturacion) | ||||
|         { | ||||
|             var sqlBuilder = new StringBuilder(@" | ||||
|                 WITH FacturaConEmpresa AS ( | ||||
|                     -- Esta subconsulta obtiene el IdEmpresa para cada factura basándose en la primera suscripción que encuentra en sus detalles. | ||||
|                     -- Esto es seguro porque nuestra lógica de negocio asegura que todos los detalles de una factura pertenecen a la misma empresa. | ||||
|                     SELECT  | ||||
|                         f.IdFactura, | ||||
|                         (SELECT TOP 1 p.Id_Empresa  | ||||
|                          FROM dbo.susc_FacturaDetalles fd | ||||
|                          JOIN dbo.susc_Suscripciones s ON fd.IdSuscripcion = s.IdSuscripcion | ||||
|                          JOIN dbo.dist_dtPublicaciones p ON s.IdPublicacion = p.Id_Publicacion | ||||
|                          WHERE fd.IdFactura = f.IdFactura) AS IdEmpresa | ||||
|                     FROM dbo.susc_Facturas f | ||||
|                     WHERE f.Periodo = @Periodo | ||||
|                 ) | ||||
|                 SELECT  | ||||
|                     f.*,  | ||||
|                     s.NombreCompleto AS NombreSuscriptor,  | ||||
|                     fce.IdEmpresa, | ||||
|                     (SELECT ISNULL(SUM(Monto), 0) FROM dbo.susc_Pagos pg WHERE pg.IdFactura = f.IdFactura AND pg.Estado = 'Aprobado') AS TotalPagado | ||||
|                 FROM dbo.susc_Facturas f | ||||
|                 JOIN dbo.susc_Suscriptores s ON f.IdSuscriptor = s.IdSuscriptor | ||||
|                 JOIN FacturaConEmpresa fce ON f.IdFactura = fce.IdFactura | ||||
|                 WHERE f.Periodo = @Periodo"); | ||||
|  | ||||
|             var parameters = new DynamicParameters(); | ||||
|             parameters.Add("Periodo", periodo); | ||||
|  | ||||
|             if (!string.IsNullOrWhiteSpace(nombreSuscriptor)) | ||||
|             { | ||||
|                 sqlBuilder.Append(" AND s.NombreCompleto LIKE @NombreSuscriptor"); | ||||
|                 parameters.Add("NombreSuscriptor", $"%{nombreSuscriptor}%"); | ||||
|             } | ||||
|             if (!string.IsNullOrWhiteSpace(estadoPago)) | ||||
|             { | ||||
|                 sqlBuilder.Append(" AND f.EstadoPago = @EstadoPago"); | ||||
|                 parameters.Add("EstadoPago", estadoPago); | ||||
|             } | ||||
|             if (!string.IsNullOrWhiteSpace(estadoFacturacion)) | ||||
|             { | ||||
|                 sqlBuilder.Append(" AND f.EstadoFacturacion = @EstadoFacturacion"); | ||||
|                 parameters.Add("EstadoFacturacion", estadoFacturacion); | ||||
|             } | ||||
|  | ||||
|             sqlBuilder.Append(" ORDER BY s.NombreCompleto, f.IdFactura;"); | ||||
|  | ||||
|             try | ||||
|             { | ||||
|                 using var connection = _connectionFactory.CreateConnection(); | ||||
|                 var result = await connection.QueryAsync<Factura, string, int, decimal, (Factura, string, int, decimal)>( | ||||
|                     sqlBuilder.ToString(), | ||||
|                     (factura, suscriptor, idEmpresa, totalPagado) => (factura, suscriptor, idEmpresa, totalPagado), | ||||
|                     parameters, | ||||
|                     splitOn: "NombreSuscriptor,IdEmpresa,TotalPagado" | ||||
|                 ); | ||||
|                 return result; | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 _logger.LogError(ex, "Error al obtener facturas enriquecidas para el período {Periodo}", periodo); | ||||
|                 return Enumerable.Empty<(Factura, string, int, decimal)>(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public async Task<bool> UpdateEstadoYMotivoAsync(int idFactura, string nuevoEstadoPago, string? motivoRechazo, IDbTransaction transaction) | ||||
|         { | ||||
|             if (transaction == null || transaction.Connection == null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(transaction), "La transacción o su conexión no pueden ser nulas."); | ||||
|             } | ||||
|             const string sql = @" | ||||
|                 UPDATE dbo.susc_Facturas SET | ||||
|                     EstadoPago = @NuevoEstadoPago, | ||||
|                     MotivoRechazo = @MotivoRechazo | ||||
|                 WHERE IdFactura = @IdFactura;"; | ||||
|             var rowsAffected = await transaction.Connection.ExecuteAsync(sql, new { NuevoEstadoPago = nuevoEstadoPago, MotivoRechazo = motivoRechazo, idFactura }, transaction); | ||||
|             return rowsAffected == 1; | ||||
|         } | ||||
|  | ||||
|         public async Task<string?> GetUltimoPeriodoFacturadoAsync() | ||||
|         { | ||||
|             const string sql = "SELECT TOP 1 Periodo FROM dbo.susc_Facturas ORDER BY Periodo DESC;"; | ||||
|             using var connection = _connectionFactory.CreateConnection(); | ||||
|             return await connection.QuerySingleOrDefaultAsync<string>(sql); | ||||
|         } | ||||
|  | ||||
|         public async Task<IEnumerable<(Factura Factura, string NombreEmpresa)>> GetFacturasConEmpresaAsync(int idSuscriptor, string periodo) | ||||
|         { | ||||
|             // Esta consulta es más robusta y eficiente. Obtiene la factura y el nombre de la empresa en una sola llamada. | ||||
|             const string sql = @" | ||||
|             SELECT f.*, e.Nombre AS NombreEmpresa | ||||
|             FROM dbo.susc_Facturas f | ||||
|             OUTER APPLY ( | ||||
|                 SELECT TOP 1 emp.Nombre | ||||
|                 FROM dbo.susc_FacturaDetalles fd | ||||
|                 JOIN dbo.susc_Suscripciones s ON fd.IdSuscripcion = s.IdSuscripcion | ||||
|                 JOIN dbo.dist_dtPublicaciones p ON s.IdPublicacion = p.Id_Publicacion | ||||
|                 JOIN dbo.dist_dtEmpresas emp ON p.Id_Empresa = emp.Id_Empresa | ||||
|                 WHERE fd.IdFactura = f.IdFactura | ||||
|             ) e | ||||
|             WHERE f.IdSuscriptor = @IdSuscriptor AND f.Periodo = @Periodo;"; | ||||
|  | ||||
|             try | ||||
|             { | ||||
|                 using var connection = _connectionFactory.CreateConnection(); | ||||
|                 var result = await connection.QueryAsync<Factura, string, (Factura, string)>( | ||||
|                     sql, | ||||
|                     (factura, nombreEmpresa) => (factura, nombreEmpresa ?? "N/A"), // Asignamos "N/A" si no encuentra empresa | ||||
|                     new { IdSuscriptor = idSuscriptor, Periodo = periodo }, | ||||
|                     splitOn: "NombreEmpresa" | ||||
|                 ); | ||||
|                 return result; | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 _logger.LogError(ex, "Error al obtener facturas con empresa para suscriptor {IdSuscriptor} y período {Periodo}", idSuscriptor, periodo); | ||||
|                 return Enumerable.Empty<(Factura, string)>(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public async Task<IEnumerable<Factura>> GetFacturasPagadasPendientesDeFacturar(string periodo) | ||||
|         { | ||||
|             // Consulta simplificada pero robusta. | ||||
|             const string sql = @" | ||||
|               SELECT * FROM dbo.susc_Facturas  | ||||
|               WHERE Periodo = @Periodo  | ||||
|               AND EstadoPago = 'Pagada'  | ||||
|               AND EstadoFacturacion = 'Pendiente de Facturar';"; | ||||
|             try | ||||
|             { | ||||
|                 using var connection = _connectionFactory.CreateConnection(); | ||||
|                 return await connection.QueryAsync<Factura>(sql, new { Periodo = periodo }); | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 _logger.LogError(ex, "Error al obtener facturas pagadas pendientes de facturar para el período {Periodo}", periodo); | ||||
|                 return Enumerable.Empty<Factura>(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public async Task<IEnumerable<Factura>> GetByIdsAsync(IEnumerable<int> ids) | ||||
|         { | ||||
|             if (ids == null || !ids.Any()) | ||||
|             { | ||||
|                 return Enumerable.Empty<Factura>(); | ||||
|             } | ||||
|             const string sql = "SELECT * FROM dbo.susc_Facturas WHERE IdFactura IN @Ids;"; | ||||
|             using var connection = _connectionFactory.CreateConnection(); | ||||
|             return await connection.QueryAsync<Factura>(sql, new { Ids = ids }); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -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,23 +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); | ||||
|         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.Dtos.Usuarios.Auditoria; | ||||
| using System.Collections.Generic; | ||||
| @@ -12,7 +10,6 @@ namespace GestionIntegral.Api.Data.Repositories.Usuarios | ||||
|     { | ||||
|         Task<IEnumerable<Usuario>> GetAllAsync(string? userFilter, string? nombreFilter); | ||||
|         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?> CreateAsync(Usuario nuevoUsuario, int idUsuarioCreador, 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> SetPasswordAsync(int userId, string newHash, string newSalt, bool debeCambiarClave, int idUsuarioModificador, IDbTransaction transaction); | ||||
|         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<(Usuario? Usuario, string? NombrePerfil)> GetByIdWithProfileNameAsync(int id); | ||||
|         Task<IEnumerable<UsuarioHistorialDto>> GetHistorialByUsuarioIdAsync(int idUsuarioAfectado, DateTime? fechaDesde, DateTime? fechaHasta); | ||||
|   | ||||
| @@ -1,8 +1,12 @@ | ||||
| using Dapper; | ||||
| using GestionIntegral.Api.Models.Usuarios; | ||||
| using GestionIntegral.Api.Dtos.Usuarios.Auditoria; | ||||
| using Microsoft.Extensions.Logging; | ||||
| using System.Collections.Generic; | ||||
| using System.Data; | ||||
| using System.Linq; | ||||
| using System.Text; | ||||
| using System.Threading.Tasks; | ||||
|  | ||||
| namespace GestionIntegral.Api.Data.Repositories.Usuarios | ||||
| { | ||||
| @@ -84,6 +88,7 @@ namespace GestionIntegral.Api.Data.Repositories.Usuarios | ||||
|             } | ||||
|         } | ||||
|  | ||||
|  | ||||
|         public async Task<Usuario?> GetByIdAsync(int id) | ||||
|         { | ||||
|             const string sql = "SELECT * FROM dbo.gral_Usuarios WHERE Id = @Id"; | ||||
| @@ -98,33 +103,6 @@ namespace GestionIntegral.Api.Data.Repositories.Usuarios | ||||
|                 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) | ||||
|         { | ||||
|             const string sql = @" | ||||
| @@ -150,6 +128,7 @@ namespace GestionIntegral.Api.Data.Repositories.Usuarios | ||||
|             } | ||||
|         } | ||||
|  | ||||
|  | ||||
|         public async Task<Usuario?> GetByUsernameAsync(string username) | ||||
|         { | ||||
|             // Esta es la misma que en AuthRepository, si se unifican, se puede eliminar una. | ||||
|   | ||||
| @@ -1,29 +1,37 @@ | ||||
| # --- Etapa 1: Build --- | ||||
| # Usamos el SDK de .NET 9 (o el que corresponda) para compilar. | ||||
| FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build | ||||
| WORKDIR /src | ||||
|  | ||||
| # Copia solo los archivos de proyecto primero para cachear dependencias | ||||
| # Copiamos los archivos de proyecto (.sln y .csproj) y restauramos las dependencias. | ||||
| # Esto es una optimización de caché de Docker. | ||||
| COPY GestionIntegralWeb.sln . | ||||
| COPY Backend/GestionIntegral.Api/GestionIntegral.Api.csproj Backend/GestionIntegral.Api/ | ||||
|  | ||||
| # Restaura dependencias | ||||
| # Restauramos los paquetes NuGet. | ||||
| RUN dotnet restore "GestionIntegralWeb.sln" | ||||
|  | ||||
| # Copia el resto del código | ||||
| # Copiamos todo el resto del código fuente. | ||||
| COPY . . | ||||
|  | ||||
| # Construye el proyecto | ||||
| # Nos movemos al directorio del proyecto y lo construimos en modo Release. | ||||
| WORKDIR "/src/Backend/GestionIntegral.Api" | ||||
| RUN dotnet build "GestionIntegral.Api.csproj" -c Release -o /app/build | ||||
|  | ||||
| # --- Etapa 2: Publish --- | ||||
| # Publicamos la aplicación, lo que genera los artefactos listos para producción. | ||||
| FROM build AS publish | ||||
| RUN dotnet publish "GestionIntegral.Api.csproj" -c Release -o /app/publish /p:UseAppHost=false | ||||
|  | ||||
| # --- Etapa 3: Final --- | ||||
| # Usamos la imagen de runtime de ASP.NET, que es mucho más ligera que el SDK. | ||||
| FROM mcr.microsoft.com/dotnet/aspnet:9.0 | ||||
| WORKDIR /app | ||||
| COPY --from=publish /app/publish . | ||||
|  | ||||
| # El puerto en el que la API escuchará DENTRO del contenedor. | ||||
| # Usaremos 8080 para evitar conflictos si en el futuro corres algo en el puerto 80. | ||||
| EXPOSE 8080 | ||||
|  | ||||
| # El comando para iniciar la API cuando el contenedor arranque. | ||||
| ENTRYPOINT ["dotnet", "GestionIntegral.Api.dll"] | ||||
| @@ -9,8 +9,6 @@ | ||||
|   <ItemGroup> | ||||
|     <PackageReference Include="AspNetCore.HealthChecks.SqlServer" Version="9.0.0" /> | ||||
|     <PackageReference Include="Dapper" Version="2.1.66" /> | ||||
|     <PackageReference Include="DotNetEnv" Version="3.1.1" /> | ||||
|     <PackageReference Include="MailKit" Version="4.13.0" /> | ||||
|     <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.4" /> | ||||
|     <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.3" /> | ||||
|     <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 string Nombre { get; set; } = string.Empty; | ||||
|         public string NombreEmpresa { get; set; } = string.Empty; | ||||
|         public bool? Habilitada { get; set; } | ||||
|         public bool Habilitada { get; set; } // Simplificamos a bool, el backend manejará el default si es null | ||||
|     } | ||||
| } | ||||
| @@ -8,6 +8,6 @@ namespace GestionIntegral.Api.Dtos.Distribucion | ||||
|         public int IdEmpresa { get; set; } | ||||
|         public string NombreEmpresa { get; set; } = string.Empty; // Para mostrar en UI | ||||
|         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 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 | ||||
|         { | ||||
|             get | ||||
| @@ -23,29 +25,20 @@ namespace GestionIntegral.Api.Dtos.Reportes.ViewModels | ||||
|                     return null; | ||||
|                 } | ||||
|  | ||||
|                 // 1. Filtrar solo los días con actividad para no diluir el promedio. | ||||
|                 var diasActivos = ResumenMensual.Where(r => r.CantidadTirada > 0).ToList(); | ||||
|  | ||||
|                 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; | ||||
|                 // Contar solo los días con tirada > 0 para promediar correctamente | ||||
|                 var diasConTirada = ResumenMensual.Count(d => d.CantidadTirada > 0); | ||||
|                 if (diasConTirada == 0) return null; | ||||
|  | ||||
|                 return new ListadoDistribucionGeneralPromedioDiaDto | ||||
|                 { | ||||
|                     Dia = "General", | ||||
|                     CantidadDias = totalDiasActivos, | ||||
|                     // 3. Calcular el promedio real: Suma de valores / Cantidad de días activos. | ||||
|                     // Se usa división entera para que coincida con el formato sin decimales. | ||||
|                     PromedioTirada = diasActivos.Sum(r => r.CantidadTirada) / totalDiasActivos, | ||||
|                     PromedioSinCargo = diasActivos.Sum(r => r.SinCargo) / totalDiasActivos, | ||||
|                     PromedioPerdidos = diasActivos.Sum(r => r.Perdidos) / totalDiasActivos, | ||||
|                     PromedioLlevados = diasActivos.Sum(r => r.Llevados) / totalDiasActivos, | ||||
|                     PromedioDevueltos = diasActivos.Sum(r => r.Devueltos) / totalDiasActivos, | ||||
|                     PromedioVendidos = diasActivos.Sum(r => r.Vendidos) / totalDiasActivos | ||||
|                     CantidadDias = diasConTirada, | ||||
|                     PromedioTirada = (int)ResumenMensual.Average(r => r.CantidadTirada), | ||||
|                     PromedioSinCargo = (int)ResumenMensual.Average(r => r.SinCargo), | ||||
|                     PromedioPerdidos = (int)ResumenMensual.Average(r => r.Perdidos), | ||||
|                     PromedioLlevados = (int)ResumenMensual.Average(r => r.Llevados), | ||||
|                     PromedioDevueltos = (int)ResumenMensual.Average(r => r.Devueltos), | ||||
|                     PromedioVendidos = (int)ResumenMensual.Average(r => r.Vendidos) | ||||
|                 }; | ||||
|             } | ||||
|         } | ||||
|   | ||||
| @@ -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,13 +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 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>(); | ||||
|     } | ||||
| } | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user