Compare commits

..

57 Commits

Author SHA1 Message Date
8101756638 Merge branch 'backup-ci-cd-attempts'
Some checks failed
Optimized Build and Deploy / remote-build-and-deploy (push) Failing after 1m25s
# Conflicts:
#	Backend/GestionIntegral.Api/Dockerfile
#	Backend/GestionIntegral.Api/appsettings.json
#	Frontend/Dockerfile
2025-06-26 23:21:05 -03:00
b66d00c92d Retry 2309
Some checks failed
Optimized Build and Deploy / remote-build-and-deploy (push) Failing after 1m19s
2025-06-26 23:09:33 -03:00
72c2f7ee31 Retry 2244
Some checks failed
Optimized Build and Deploy / remote-build-and-deploy (push) Failing after 1m25s
2025-06-26 22:44:44 -03:00
1456ccd723 Retry 2239
Some checks failed
Optimized Build and Deploy / remote-build-and-deploy (push) Failing after 1m40s
2025-06-26 22:39:09 -03:00
229f657685 Retry 2221
All checks were successful
Optimized Build and Deploy / remote-build-and-deploy (push) Successful in 1m59s
2025-06-26 22:21:47 -03:00
13ab496727 Retry 2211
All checks were successful
Optimized Build and Deploy / remote-build-and-deploy (push) Successful in 4m31s
2025-06-26 22:11:59 -03:00
5adc1e6d46 Se realiza fix y se añade depuración al deply
All checks were successful
Optimized Build and Deploy / remote-build-and-deploy (push) Successful in 1m55s
2025-06-26 21:55:46 -03:00
1ec21741cc Retry Secret JWT Key
Some checks failed
Optimized Build and Deploy / remote-build-and-deploy (push) Failing after 4m10s
2025-06-26 21:46:57 -03:00
b33dd4f94f Cambio por host 2025-06-16 19:06:11 -03:00
ba9bef2364 Cambio de imagen kaniko 2025-06-16 18:58:49 -03:00
856d7ac5c1 Retest 2025-06-16 18:52:48 -03:00
d8dc41222c Fix yml 2025-06-16 18:50:31 -03:00
a434640456 Test con Kaniko 2025-06-16 18:44:51 -03:00
fcc2b90f15 Added: privileged: true 2025-06-16 18:28:08 -03:00
0d7614ef4c Webhook gitea Enable. 2025-06-16 18:21:59 -03:00
ae54ef3fe5 Retry 1807 2025-06-16 18:07:22 -03:00
c361842a91 Retry 1806 2025-06-16 18:06:14 -03:00
de079d8bd4 Cabio de Variable por valor directo. 2025-06-16 13:12:14 -03:00
401e61e0eb Se añade la URL del registro al nombre del repo 2025-06-16 13:02:38 -03:00
4b208793ce Todos los registros de contenedores (Docker Hub, Gitea, etc.) exigen que los nombres de las imágenes de Docker estén en minúsculas. El pipeline está intentando crear una imagen llamada dmolinari/GestionIntegralWeb-backend, pero la parte GestionIntegralWeb contiene mayúsculas.
Esto ocurre porque la variable de Drone ${DRONE_REPO_NAME} toma el nombre directamente de Gitea, que en este caso es GestionIntegralWeb. La sintaxis es: ${VARIABLE,,}.
2025-06-16 12:57:22 -03:00
d60dd8dc9f Linter: duplicate step name 2025-06-16 12:49:35 -03:00
8156df9f90 Fix indentación. Y Trusted en Drone. 2025-06-16 12:48:22 -03:00
55ca8bffa7 Prueba: Usar el Socket de Docker del Host 2025-06-16 12:41:32 -03:00
02265a46e7 Cambio de enfoque. Parametro para MTU (Maximum Transmission Unit) añadidos. 2025-06-16 12:18:21 -03:00
767cd081dc Se agregan daemon_dns para poder resolver los dominios de nuget desde los contenedores de despliegue. 2025-06-16 12:05:49 -03:00
97b6a9241f Se agregan debug-network para verificar valores. 2025-06-16 11:57:42 -03:00
b68ac1fed1 Test Webhook 2025-06-16 03:07:17 -03:00
2e83c2e373 Retry 0142 2025-06-16 01:42:57 -03:00
1e8d5fd308 Forzar la network. 2025-06-16 01:34:16 -03:00
a1dfd0d089 Fix: Configure docker plugin for insecure registry 2025-06-16 01:30:15 -03:00
eff65921e6 Nuevo test 2025-06-15 23:10:37 -03:00
b3de8dba3a Retry mil 2025-06-15 23:06:51 -03:00
f62fc4b507 Va 2025-06-15 22:57:24 -03:00
66686fc548 Retry yml 2025-06-15 22:52:26 -03:00
c0900e07e6 Fix problema de indentación. 2025-06-15 22:46:25 -03:00
a63b40471b Retry .drone.yml 2025-06-15 22:42:58 -03:00
12decefc1b Fix de yml para Drone. 2025-06-15 22:38:22 -03:00
bb79ccf64c Cambio de Enfoque para CI/CD. Se intenta uso de Drone. 2025-06-15 22:18:23 -03:00
b3e70a3988 Nuevo 2025-06-15 21:29:03 -03:00
55640c394f A ver... 2025-06-15 21:15:41 -03:00
338cd8579f Final? 2025-06-15 19:46:31 -03:00
69620c5607 Este 2025-06-15 19:40:15 -03:00
8ba18ed687 Va 2025-06-15 19:18:13 -03:00
30570beaca Reversion 1.24.0 2025-06-15 19:00:31 -03:00
db10fa0254 Test con Gitea 1.21.11 2025-06-15 12:16:46 -03:00
1d664672b2 No puede resolver gitea. Debe usar el secreto REGISTRY_URL. 2025-06-15 11:28:46 -03:00
969c78a567 Sin sh-runner. 2025-06-15 11:22:02 -03:00
0b884197fb Reversión antes de cambio de foco. 2025-06-15 11:19:21 -03:00
0d2c30ad94 Test con sh-runner 2025-06-15 11:01:10 -03:00
f1591bd572 Reversión 3 2025-06-15 10:39:12 -03:00
24e4769f78 Fix 2025-06-15 10:32:18 -03:00
76d48cc310 Reversión 2. 2025-06-15 10:26:55 -03:00
b072e31385 Nuevo testeo de CI/CD. 2025-06-15 04:06:27 -03:00
65be2bcbaa Reversión. 2025-06-14 23:45:47 -03:00
0fb3cb7aef Reseteo de estado por fallas del deply. 2025-06-14 23:13:05 -03:00
541625bf66 fix: Run jobs on runner host to resolve network issues 2025-06-14 23:01:28 -03:00
cbe313b59d Ajustes de CI/CD. 2025-06-14 22:11:02 -03:00
251 changed files with 1147 additions and 12971 deletions

82
.drone.yml Normal file
View 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

View File

@@ -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
View File

@@ -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

View File

@@ -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@"

View File

@@ -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; }
}
}

View File

@@ -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);
}
}
}

View File

@@ -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)]

View File

@@ -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)]

View File

@@ -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
{

View File

@@ -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)]

View File

@@ -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")]

View File

@@ -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]

View File

@@ -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
{

View File

@@ -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 ?? "-");
}
});
});
}
}
}

View File

@@ -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();
});
}
}
}

View File

@@ -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"));

View File

@@ -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.");
}
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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>();
}
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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 });
}
}
}

View File

@@ -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 = @"

View File

@@ -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);

View File

@@ -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);

View File

@@ -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(

View File

@@ -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";

View File

@@ -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>(

View File

@@ -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";

View File

@@ -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);

View File

@@ -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
}
}

View File

@@ -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);

View File

@@ -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

View File

@@ -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 = @"

View File

@@ -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";

View File

@@ -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);
}
}

View File

@@ -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>();
}
}
}
}

View File

@@ -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 });
}
}
}

View File

@@ -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>();
}
}
}
}

View File

@@ -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 });
}
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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 });
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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);

View File

@@ -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.

View File

@@ -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"]

View File

@@ -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" />

View File

@@ -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; }
}
}

View File

@@ -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; }
}
}

View File

@@ -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;
}
}

View File

@@ -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
}
}

View File

@@ -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; }
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}

View File

@@ -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;
}
}

View File

@@ -1,8 +0,0 @@
namespace GestionIntegral.Api.Dtos.Distribucion
{
public class OtroDestinoDropdownDto
{
public int IdDestino { get; set; }
public string Nombre { get; set; } = string.Empty;
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -1,8 +0,0 @@
namespace GestionIntegral.Api.Dtos.Impresion
{
public class EstadoBobinaDropdownDto
{
public int IdEstadoBobina { get; set; }
public string Denominacion { get; set; } = string.Empty;
}
}

View File

@@ -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; }
}
}

View File

@@ -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>();
}
}

View File

@@ -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; }
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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
@@ -22,30 +24,21 @@ 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)
};
}
}

View File

@@ -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;
}
}

View File

@@ -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; }
}
}

View File

@@ -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;
}
}

View File

@@ -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; }
}
}

View File

@@ -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;
}
}

View File

@@ -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.

View File

@@ -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; }
}
}

View File

@@ -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>();
}
}

View File

@@ -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>();
}
}

View File

@@ -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; }
}
}

View File

@@ -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
}
}

View File

@@ -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;
}
}

View File

@@ -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; }
}
}

View File

@@ -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; }
}
}

View File

@@ -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