Feat: Implementa flujo completo de facturación y promociones
Este commit introduce la funcionalidad completa para la facturación mensual, la gestión de promociones y la comunicación con el cliente en el módulo de suscripciones. Backend: - Se añade el servicio de Facturación que calcula automáticamente los importes mensuales basándose en las suscripciones activas, días de entrega y precios. - Se implementa el servicio DebitoAutomaticoService, capaz de generar el archivo de texto plano para "Pago Directo Galicia" y de procesar el archivo de respuesta para la conciliación de pagos. - Se desarrolla el ABM completo para Promociones (Servicio, Repositorio, Controlador y DTOs), permitiendo la creación de descuentos por porcentaje o monto fijo. - Se implementa la lógica para asignar y desasignar promociones a suscripciones específicas. - Se añade un servicio de envío de email (EmailService) integrado con MailKit y un endpoint para notificar facturas a los clientes. - Se crea la lógica para registrar pagos manuales (efectivo, tarjeta, etc.) y actualizar el estado de las facturas. - Se añaden todos los permisos necesarios a la base de datos para segmentar el acceso a las nuevas funcionalidades. Frontend: - Se crea la página de Facturación, que permite al usuario seleccionar un período, generar la facturación, listar los resultados y generar el archivo de débito para el banco. - Se implementa la funcionalidad para subir y procesar el archivo de respuesta del banco, actualizando la UI en consecuencia. - Se añade la página completa para el ABM de Promociones. - Se integra un modal en la gestión de suscripciones para asignar y desasignar promociones a un cliente. - Se añade la opción "Enviar Email" en el menú de acciones de las facturas, conectada al nuevo endpoint del backend. - Se completan y corrigen los componentes `PagoManualModal` y `FacturacionPage` para incluir la lógica de registro de pagos y solucionar errores de TypeScript.
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
// Archivo: GestionIntegral.Api/Controllers/Suscripciones/DebitosController.cs
|
||||
|
||||
using GestionIntegral.Api.Dtos.Suscripciones;
|
||||
using GestionIntegral.Api.Services.Suscripciones;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
|
||||
namespace GestionIntegral.Api.Controllers.Suscripciones
|
||||
{
|
||||
[Route("api/debitos")]
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
public class DebitosController : ControllerBase
|
||||
{
|
||||
private readonly IDebitoAutomaticoService _debitoService;
|
||||
private readonly ILogger<DebitosController> _logger;
|
||||
|
||||
// Permiso para generar archivos de débito (a crear en BD)
|
||||
private const string PermisoGenerarDebitos = "SU007";
|
||||
|
||||
public DebitosController(IDebitoAutomaticoService debitoService, ILogger<DebitosController> logger)
|
||||
{
|
||||
_debitoService = debitoService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
private bool TienePermiso(string codAcc) => User.IsInRole("SuperAdmin") || User.HasClaim(c => c.Type == "permission" && c.Value == codAcc);
|
||||
private int? GetCurrentUserId()
|
||||
{
|
||||
if (int.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub"), out int userId)) return userId;
|
||||
return null;
|
||||
}
|
||||
|
||||
// POST: api/debitos/{anio}/{mes}/generar-archivo
|
||||
[HttpPost("{anio:int}/{mes:int}/generar-archivo")]
|
||||
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> GenerarArchivo(int anio, int mes)
|
||||
{
|
||||
if (!TienePermiso(PermisoGenerarDebitos)) return Forbid();
|
||||
|
||||
var userId = GetCurrentUserId();
|
||||
if (userId == null) return Unauthorized();
|
||||
|
||||
var (contenido, nombreArchivo, error) = await _debitoService.GenerarArchivoPagoDirecto(anio, mes, userId.Value);
|
||||
|
||||
if (error != null)
|
||||
{
|
||||
// Si el error es "No se encontraron facturas", es un 404. Otros son 400.
|
||||
if (error.Contains("No se encontraron"))
|
||||
{
|
||||
return NotFound(new { message = error });
|
||||
}
|
||||
return BadRequest(new { message = error });
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(contenido) || string.IsNullOrEmpty(nombreArchivo))
|
||||
{
|
||||
return StatusCode(500, new { message = "El servicio no pudo generar el contenido del archivo correctamente." });
|
||||
}
|
||||
|
||||
// Devolver el archivo para descarga
|
||||
var fileBytes = Encoding.UTF8.GetBytes(contenido);
|
||||
return File(fileBytes, "text/plain", nombreArchivo);
|
||||
}
|
||||
|
||||
// POST: api/debitos/procesar-respuesta
|
||||
[HttpPost("procesar-respuesta")]
|
||||
[ProducesResponseType(typeof(ProcesamientoLoteResponseDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<IActionResult> ProcesarArchivoRespuesta(IFormFile archivo)
|
||||
{
|
||||
// Usamos el mismo permiso de generar débitos para procesar la respuesta.
|
||||
if (!TienePermiso(PermisoGenerarDebitos)) return Forbid();
|
||||
|
||||
var userId = GetCurrentUserId();
|
||||
if (userId == null) return Unauthorized();
|
||||
|
||||
var resultado = await _debitoService.ProcesarArchivoRespuesta(archivo, userId.Value);
|
||||
|
||||
if (resultado.Errores.Any() && resultado.PagosAprobados == 0 && resultado.PagosRechazados == 0)
|
||||
{
|
||||
return BadRequest(resultado);
|
||||
}
|
||||
|
||||
return Ok(resultado);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
using GestionIntegral.Api.Dtos.Suscripciones;
|
||||
using GestionIntegral.Api.Services.Suscripciones;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace GestionIntegral.Api.Controllers.Suscripciones
|
||||
{
|
||||
[Route("api/facturacion")]
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
public class FacturacionController : ControllerBase
|
||||
{
|
||||
private readonly IFacturacionService _facturacionService;
|
||||
private readonly ILogger<FacturacionController> _logger;
|
||||
|
||||
// Permiso para generar facturación (a crear en la BD)
|
||||
private const string PermisoGenerarFacturacion = "SU006";
|
||||
|
||||
public FacturacionController(IFacturacionService facturacionService, ILogger<FacturacionController> logger)
|
||||
{
|
||||
_facturacionService = facturacionService;
|
||||
_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/facturacion/{anio}/{mes}
|
||||
[HttpPost("{anio:int}/{mes:int}")]
|
||||
public async Task<IActionResult> GenerarFacturacion(int anio, int mes)
|
||||
{
|
||||
if (!TienePermiso(PermisoGenerarFacturacion)) 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, facturasGeneradas) = await _facturacionService.GenerarFacturacionMensual(anio, mes, userId.Value);
|
||||
|
||||
if (!exito)
|
||||
{
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, new { message = mensaje });
|
||||
}
|
||||
|
||||
return Ok(new { message = mensaje, facturasGeneradas });
|
||||
}
|
||||
|
||||
// GET: api/facturacion/{anio}/{mes}
|
||||
[HttpGet("{anio:int}/{mes:int}")]
|
||||
[ProducesResponseType(typeof(IEnumerable<FacturaDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public async Task<IActionResult> GetFacturas(int anio, int mes)
|
||||
{
|
||||
// Usamos el permiso de generar facturación también para verlas.
|
||||
if (!TienePermiso(PermisoGenerarFacturacion)) return Forbid();
|
||||
|
||||
if (anio < 2020 || mes < 1 || mes > 12)
|
||||
{
|
||||
return BadRequest(new { message = "El período no es válido." });
|
||||
}
|
||||
|
||||
var facturas = await _facturacionService.ObtenerFacturasPorPeriodo(anio, mes);
|
||||
return Ok(facturas);
|
||||
}
|
||||
|
||||
// POST: api/facturacion/{idFactura}/enviar-email
|
||||
[HttpPost("{idFactura:int}/enviar-email")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> EnviarEmail(int idFactura)
|
||||
{
|
||||
// Usaremos un nuevo permiso para esta acción
|
||||
if (!TienePermiso("SU009")) return Forbid();
|
||||
|
||||
var (exito, error) = await _facturacionService.EnviarFacturaPorEmail(idFactura);
|
||||
|
||||
if (!exito)
|
||||
{
|
||||
return BadRequest(new { message = error });
|
||||
}
|
||||
|
||||
return Ok(new { message = "Email enviado a la cola de procesamiento." });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
using GestionIntegral.Api.Dtos.Suscripciones;
|
||||
using GestionIntegral.Api.Services.Suscripciones;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace GestionIntegral.Api.Controllers.Suscripciones
|
||||
{
|
||||
[Route("api/pagos")]
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
public class PagosController : ControllerBase
|
||||
{
|
||||
private readonly IPagoService _pagoService;
|
||||
private readonly ILogger<PagosController> _logger;
|
||||
|
||||
// Permiso para registrar pagos manuales (a crear en BD)
|
||||
private const string PermisoRegistrarPago = "SU008";
|
||||
|
||||
public PagosController(IPagoService pagoService, ILogger<PagosController> logger)
|
||||
{
|
||||
_pagoService = pagoService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
private bool TienePermiso(string codAcc) => User.IsInRole("SuperAdmin") || User.HasClaim(c => c.Type == "permission" && c.Value == codAcc);
|
||||
|
||||
private int? GetCurrentUserId()
|
||||
{
|
||||
if (int.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub"), out int userId)) return userId;
|
||||
return null;
|
||||
}
|
||||
|
||||
// GET: api/facturas/{idFactura}/pagos
|
||||
[HttpGet("~/api/facturas/{idFactura:int}/pagos")]
|
||||
[ProducesResponseType(typeof(IEnumerable<PagoDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public async Task<IActionResult> GetPagosPorFactura(int idFactura)
|
||||
{
|
||||
// Se podría usar un permiso de "Ver Facturación"
|
||||
if (!TienePermiso("SU006")) return Forbid();
|
||||
|
||||
var pagos = await _pagoService.ObtenerPagosPorFacturaId(idFactura);
|
||||
return Ok(pagos);
|
||||
}
|
||||
|
||||
// POST: api/pagos
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(PagoDto), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public async Task<IActionResult> RegistrarPago([FromBody] CreatePagoDto createDto)
|
||||
{
|
||||
if (!TienePermiso(PermisoRegistrarPago)) return Forbid();
|
||||
if (!ModelState.IsValid) return BadRequest(ModelState);
|
||||
|
||||
var userId = GetCurrentUserId();
|
||||
if (userId == null) return Unauthorized();
|
||||
|
||||
var (dto, error) = await _pagoService.RegistrarPagoManual(createDto, userId.Value);
|
||||
|
||||
if (error != null) return BadRequest(new { message = error });
|
||||
if (dto == null) return StatusCode(StatusCodes.Status500InternalServerError, "Error al registrar el pago.");
|
||||
|
||||
// No tenemos un "GetById" para pagos, así que devolvemos el objeto con un 201.
|
||||
return StatusCode(201, dto);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
using GestionIntegral.Api.Dtos.Suscripciones;
|
||||
using GestionIntegral.Api.Services.Suscripciones;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace GestionIntegral.Api.Controllers.Suscripciones
|
||||
{
|
||||
[Route("api/promociones")]
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
public class PromocionesController : ControllerBase
|
||||
{
|
||||
private readonly IPromocionService _promocionService;
|
||||
private readonly ILogger<PromocionesController> _logger;
|
||||
|
||||
// Permiso a crear en BD
|
||||
private const string PermisoGestionarPromociones = "SU010";
|
||||
|
||||
public PromocionesController(IPromocionService promocionService, ILogger<PromocionesController> logger)
|
||||
{
|
||||
_promocionService = promocionService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
private bool TienePermiso(string codAcc) => User.IsInRole("SuperAdmin") || User.HasClaim(c => c.Type == "permission" && c.Value == codAcc);
|
||||
private int? GetCurrentUserId()
|
||||
{
|
||||
if (int.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub"), out int userId)) return userId;
|
||||
return null;
|
||||
}
|
||||
|
||||
// GET: api/promociones
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetAll([FromQuery] bool soloActivas = true)
|
||||
{
|
||||
if (!TienePermiso(PermisoGestionarPromociones)) return Forbid();
|
||||
var promociones = await _promocionService.ObtenerTodas(soloActivas);
|
||||
return Ok(promociones);
|
||||
}
|
||||
|
||||
// GET: api/promociones/{id}
|
||||
[HttpGet("{id:int}", Name = "GetPromocionById")]
|
||||
public async Task<IActionResult> GetById(int id)
|
||||
{
|
||||
if (!TienePermiso(PermisoGestionarPromociones)) return Forbid();
|
||||
var promocion = await _promocionService.ObtenerPorId(id);
|
||||
if (promocion == null) return NotFound();
|
||||
return Ok(promocion);
|
||||
}
|
||||
|
||||
// POST: api/promociones
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Create([FromBody] CreatePromocionDto createDto)
|
||||
{
|
||||
if (!TienePermiso(PermisoGestionarPromociones)) return Forbid();
|
||||
if (!ModelState.IsValid) return BadRequest(ModelState);
|
||||
|
||||
var userId = GetCurrentUserId();
|
||||
if (userId == null) return Unauthorized();
|
||||
|
||||
var (dto, error) = await _promocionService.Crear(createDto, userId.Value);
|
||||
|
||||
if (error != null) return BadRequest(new { message = error });
|
||||
if (dto == null) return StatusCode(500, "Error al crear la promoción.");
|
||||
|
||||
return CreatedAtRoute("GetPromocionById", new { id = dto.IdPromocion }, dto);
|
||||
}
|
||||
|
||||
// PUT: api/promociones/{id}
|
||||
[HttpPut("{id:int}")]
|
||||
public async Task<IActionResult> Update(int id, [FromBody] UpdatePromocionDto updateDto)
|
||||
{
|
||||
if (!TienePermiso(PermisoGestionarPromociones)) return Forbid();
|
||||
if (!ModelState.IsValid) return BadRequest(ModelState);
|
||||
|
||||
var userId = GetCurrentUserId();
|
||||
if (userId == null) return Unauthorized();
|
||||
|
||||
var (exito, error) = await _promocionService.Actualizar(id, updateDto, userId.Value);
|
||||
|
||||
if (!exito)
|
||||
{
|
||||
if (error != null && error.Contains("no encontrada")) return NotFound(new { message = error });
|
||||
return BadRequest(new { message = error });
|
||||
}
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ namespace GestionIntegral.Api.Controllers.Suscripciones
|
||||
private readonly ILogger<SuscripcionesController> _logger;
|
||||
|
||||
// Permisos (nuevos, a crear en la BD)
|
||||
private const string PermisoGestionarSuscripciones = "SU005";
|
||||
private const string PermisoGestionarSuscripciones = "SU005";
|
||||
|
||||
public SuscripcionesController(ISuscripcionService suscripcionService, ILogger<SuscripcionesController> logger)
|
||||
{
|
||||
@@ -26,7 +26,7 @@ namespace GestionIntegral.Api.Controllers.Suscripciones
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -41,7 +41,7 @@ namespace GestionIntegral.Api.Controllers.Suscripciones
|
||||
{
|
||||
// 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);
|
||||
}
|
||||
@@ -62,15 +62,15 @@ namespace GestionIntegral.Api.Controllers.Suscripciones
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ namespace GestionIntegral.Api.Controllers.Suscripciones
|
||||
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 });
|
||||
@@ -93,5 +93,46 @@ namespace GestionIntegral.Api.Controllers.Suscripciones
|
||||
}
|
||||
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/{idPromocion}
|
||||
[HttpPost("{idSuscripcion:int}/promociones/{idPromocion:int}")]
|
||||
public async Task<IActionResult> AsignarPromocion(int idSuscripcion, int idPromocion)
|
||||
{
|
||||
if (!TienePermiso(PermisoGestionarSuscripciones)) return Forbid();
|
||||
var userId = GetCurrentUserId();
|
||||
if (userId == null) return Unauthorized();
|
||||
|
||||
var (exito, error) = await _suscripcionService.AsignarPromocion(idSuscripcion, idPromocion, 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,7 @@ namespace GestionIntegral.Api.Data.Repositories.Suscripciones
|
||||
public async Task<Factura?> GetBySuscripcionYPeriodoAsync(int idSuscripcion, string periodo, IDbTransaction transaction)
|
||||
{
|
||||
const string sql = "SELECT TOP 1 * FROM dbo.susc_Facturas WHERE IdSuscripcion = @IdSuscripcion AND Periodo = @Periodo;";
|
||||
if (transaction == null || transaction.Connection == null)
|
||||
if (transaction == null || transaction.Connection == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(transaction), "La transacción o su conexión no pueden ser nulas.");
|
||||
}
|
||||
@@ -55,7 +55,7 @@ namespace GestionIntegral.Api.Data.Repositories.Suscripciones
|
||||
VALUES
|
||||
(@IdSuscripcion, @Periodo, @FechaEmision, @FechaVencimiento, @ImporteBruto,
|
||||
@DescuentoAplicado, @ImporteFinal, @Estado);";
|
||||
|
||||
|
||||
return await transaction.Connection.QuerySingleAsync<Factura>(sqlInsert, nuevaFactura, transaction);
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ namespace GestionIntegral.Api.Data.Repositories.Suscripciones
|
||||
var rowsAffected = await transaction.Connection.ExecuteAsync(sql, new { NumeroFactura = numeroFactura, IdFactura = idFactura }, transaction);
|
||||
return rowsAffected == 1;
|
||||
}
|
||||
|
||||
|
||||
public async Task<bool> UpdateLoteDebitoAsync(IEnumerable<int> idsFacturas, int idLoteDebito, IDbTransaction transaction)
|
||||
{
|
||||
if (transaction == null || transaction.Connection == null)
|
||||
@@ -91,5 +91,55 @@ namespace GestionIntegral.Api.Data.Repositories.Suscripciones
|
||||
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, string NombrePublicacion)>> GetByPeriodoEnrichedAsync(string periodo)
|
||||
{
|
||||
const string sql = @"
|
||||
SELECT f.*, s.NombreCompleto AS NombreSuscriptor, p.Nombre AS NombrePublicacion
|
||||
FROM dbo.susc_Facturas f
|
||||
JOIN dbo.susc_Suscripciones sc ON f.IdSuscripcion = sc.IdSuscripcion
|
||||
JOIN dbo.susc_Suscriptores s ON sc.IdSuscriptor = s.IdSuscriptor
|
||||
JOIN dbo.dist_dtPublicaciones p ON sc.IdPublicacion = p.Id_Publicacion
|
||||
WHERE f.Periodo = @Periodo
|
||||
ORDER BY s.NombreCompleto;
|
||||
";
|
||||
try
|
||||
{
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
var result = await connection.QueryAsync<Factura, string, string, (Factura, string, string)>(
|
||||
sql,
|
||||
(factura, suscriptor, publicacion) => (factura, suscriptor, publicacion),
|
||||
new { Periodo = periodo },
|
||||
splitOn: "NombreSuscriptor,NombrePublicacion"
|
||||
);
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error al obtener facturas enriquecidas para el período {Periodo}", periodo);
|
||||
return Enumerable.Empty<(Factura, string, string)>();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateEstadoYMotivoAsync(int idFactura, string nuevoEstado, 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
|
||||
Estado = @NuevoEstado,
|
||||
MotivoRechazo = @MotivoRechazo
|
||||
WHERE IdFactura = @IdFactura;";
|
||||
|
||||
var rowsAffected = await transaction.Connection.ExecuteAsync(
|
||||
sql,
|
||||
new { NuevoEstado = nuevoEstado, MotivoRechazo = motivoRechazo, IdFactura = idFactura },
|
||||
transaction
|
||||
);
|
||||
return rowsAffected == 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,5 +12,7 @@ namespace GestionIntegral.Api.Data.Repositories.Suscripciones
|
||||
Task<bool> UpdateEstadoAsync(int idFactura, string nuevoEstado, 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, string NombrePublicacion)>> GetByPeriodoEnrichedAsync(string periodo);
|
||||
Task<bool> UpdateEstadoYMotivoAsync(int idFactura, string nuevoEstado, string? motivoRechazo, IDbTransaction transaction);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -10,5 +10,8 @@ namespace GestionIntegral.Api.Data.Repositories.Suscripciones
|
||||
Task<Suscripcion?> CreateAsync(Suscripcion nuevaSuscripcion, IDbTransaction transaction);
|
||||
Task<bool> UpdateAsync(Suscripcion suscripcionAActualizar, IDbTransaction transaction);
|
||||
Task<IEnumerable<Suscripcion>> GetAllActivasParaFacturacion(string periodo, IDbTransaction transaction);
|
||||
Task<IEnumerable<Promocion>> GetPromocionesBySuscripcionIdAsync(int idSuscripcion);
|
||||
Task AsignarPromocionAsync(int idSuscripcion, int idPromocion, int idUsuario, IDbTransaction transaction);
|
||||
Task<bool> QuitarPromocionAsync(int idSuscripcion, int idPromocion, IDbTransaction transaction);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
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, TipoPromocion, Valor, FechaInicio, FechaFin, Activa, IdUsuarioAlta, FechaAlta)
|
||||
OUTPUT INSERTED.*
|
||||
VALUES (@Descripcion, @TipoPromocion, @Valor, @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)
|
||||
{
|
||||
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
|
||||
AND p.FechaInicio <= @FechaPeriodo
|
||||
AND (p.FechaFin IS NULL OR p.FechaFin >= @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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,7 +44,7 @@ namespace GestionIntegral.Api.Data.Repositories.Suscripciones
|
||||
return Enumerable.Empty<Suscripcion>();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public async Task<IEnumerable<Suscripcion>> GetAllActivasParaFacturacion(string periodo, IDbTransaction transaction)
|
||||
{
|
||||
// Lógica para determinar el rango del período (ej. '2023-11')
|
||||
@@ -61,7 +61,7 @@ namespace GestionIntegral.Api.Data.Repositories.Suscripciones
|
||||
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.");
|
||||
@@ -85,7 +85,7 @@ namespace GestionIntegral.Api.Data.Repositories.Suscripciones
|
||||
VALUES
|
||||
(@IdSuscriptor, @IdPublicacion, @FechaInicio, @FechaFin, @Estado, @DiasEntrega,
|
||||
@Observaciones, @IdUsuarioAlta, GETDATE());";
|
||||
|
||||
|
||||
return await transaction.Connection.QuerySingleAsync<Suscripcion>(sqlInsert, nuevaSuscripcion, transaction);
|
||||
}
|
||||
|
||||
@@ -111,5 +111,42 @@ namespace GestionIntegral.Api.Data.Repositories.Suscripciones
|
||||
var rowsAffected = await transaction.Connection.ExecuteAsync(sqlUpdate, suscripcion, transaction);
|
||||
return rowsAffected == 1;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Promocion>> GetPromocionesBySuscripcionIdAsync(int idSuscripcion)
|
||||
{
|
||||
const string sql = @"
|
||||
SELECT p.* FROM dbo.susc_Promociones p
|
||||
JOIN dbo.susc_SuscripcionPromociones sp ON p.IdPromocion = sp.IdPromocion
|
||||
WHERE sp.IdSuscripcion = @IdSuscripcion;";
|
||||
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
return await connection.QueryAsync<Promocion>(sql, new { IdSuscripcion = idSuscripcion });
|
||||
}
|
||||
|
||||
public async Task AsignarPromocionAsync(int idSuscripcion, int idPromocion, int idUsuario, 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)
|
||||
VALUES (@IdSuscripcion, @IdPromocion, @IdUsuario);";
|
||||
|
||||
await transaction.Connection.ExecuteAsync(sql,
|
||||
new { IdSuscripcion = idSuscripcion, IdPromocion = idPromocion, IdUsuario = idUsuario },
|
||||
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 = idSuscripcion, IdPromocion = idPromocion }, transaction);
|
||||
return rows == 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AspNetCore.HealthChecks.SqlServer" Version="9.0.0" />
|
||||
<PackageReference Include="Dapper" Version="2.1.66" />
|
||||
<PackageReference Include="MailKit" Version="4.13.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" />
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace GestionIntegral.Api.Models.Comunicaciones
|
||||
{
|
||||
public class MailSettings
|
||||
{
|
||||
public string SmtpHost { get; set; } = string.Empty;
|
||||
public int SmtpPort { get; set; }
|
||||
public string SenderName { get; set; } = string.Empty;
|
||||
public string SenderEmail { get; set; } = string.Empty;
|
||||
public string SmtpUser { get; set; } = string.Empty;
|
||||
public string SmtpPass { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// Archivo: GestionIntegral.Api/Dtos/Suscripciones/CreatePagoDto.cs
|
||||
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace GestionIntegral.Api.Dtos.Suscripciones
|
||||
{
|
||||
public class CreatePagoDto
|
||||
{
|
||||
[Required]
|
||||
public int IdFactura { get; set; }
|
||||
|
||||
[Required]
|
||||
public DateTime FechaPago { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "Debe seleccionar una forma de pago.")]
|
||||
public int IdFormaPago { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "El monto es obligatorio.")]
|
||||
[Range(0.01, 99999999.99, ErrorMessage = "El monto debe ser un valor positivo.")]
|
||||
public decimal Monto { get; set; }
|
||||
|
||||
[StringLength(100)]
|
||||
public string? Referencia { get; set; } // Nro. de comprobante, etc.
|
||||
|
||||
[StringLength(250)]
|
||||
public string? Observaciones { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
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 TipoPromocion { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
[Range(0.01, 99999999.99)]
|
||||
public decimal Valor { get; set; }
|
||||
|
||||
[Required]
|
||||
public DateTime FechaInicio { get; set; }
|
||||
public DateTime? FechaFin { get; set; }
|
||||
public bool Activa { get; set; } = true;
|
||||
}
|
||||
}
|
||||
|
||||
// UpdatePromocionDto puede ser idéntico al de creación por ahora
|
||||
using UpdatePromocionDto = CreatePromocionDto;
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace GestionIntegral.Api.Dtos.Suscripciones
|
||||
{
|
||||
/// <summary>
|
||||
/// DTO para enviar la información de una factura generada al frontend.
|
||||
/// Incluye datos enriquecidos como nombres para facilitar su visualización en la UI.
|
||||
/// </summary>
|
||||
public class FacturaDto
|
||||
{
|
||||
public int IdFactura { get; set; }
|
||||
public int IdSuscripcion { get; set; }
|
||||
public string Periodo { get; set; } = string.Empty; // Formato "YYYY-MM"
|
||||
public string FechaEmision { get; set; } = string.Empty; // Formato "yyyy-MM-dd"
|
||||
public string FechaVencimiento { get; set; } = string.Empty; // Formato "yyyy-MM-dd"
|
||||
public decimal ImporteFinal { get; set; }
|
||||
public string Estado { get; set; } = string.Empty;
|
||||
public string? NumeroFactura { get; set; }
|
||||
|
||||
// Datos enriquecidos para la UI, poblados por el servicio
|
||||
public string NombreSuscriptor { get; set; } = string.Empty;
|
||||
public string NombrePublicacion { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace GestionIntegral.Api.Dtos.Suscripciones
|
||||
{
|
||||
public class PagoDto
|
||||
{
|
||||
public int IdPago { get; set; }
|
||||
public int IdFactura { get; set; }
|
||||
public string FechaPago { get; set; } = string.Empty; // "yyyy-MM-dd"
|
||||
public int IdFormaPago { get; set; }
|
||||
public string NombreFormaPago { get; set; } = string.Empty; // Enriquecido
|
||||
public decimal Monto { get; set; }
|
||||
public string Estado { get; set; } = string.Empty; // "Aprobado", "Rechazado"
|
||||
public string? Referencia { get; set; }
|
||||
public string? Observaciones { get; set; }
|
||||
public int IdUsuarioRegistro { get; set; }
|
||||
public string NombreUsuarioRegistro { get; set; } = string.Empty; // Enriquecido
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace GestionIntegral.Api.Dtos.Suscripciones
|
||||
{
|
||||
public class ProcesamientoLoteResponseDto
|
||||
{
|
||||
public int TotalRegistrosLeidos { get; set; }
|
||||
public int PagosAprobados { get; set; }
|
||||
public int PagosRechazados { get; set; }
|
||||
public List<string> Errores { get; set; } = new List<string>();
|
||||
public string MensajeResumen { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace GestionIntegral.Api.Dtos.Suscripciones
|
||||
{
|
||||
public class PromocionDto
|
||||
{
|
||||
public int IdPromocion { get; set; }
|
||||
public string Descripcion { get; set; } = string.Empty;
|
||||
public string TipoPromocion { get; set; } = string.Empty;
|
||||
public decimal Valor { get; set; }
|
||||
public string FechaInicio { get; set; } = string.Empty; // yyyy-MM-dd
|
||||
public string? FechaFin { get; set; }
|
||||
public bool Activa { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace GestionIntegral.Api.Models.Suscripciones
|
||||
{
|
||||
public class Promocion
|
||||
{
|
||||
public int IdPromocion { get; set; }
|
||||
public string Descripcion { get; set; } = string.Empty;
|
||||
public string TipoPromocion { get; set; } = string.Empty;
|
||||
public decimal Valor { get; set; }
|
||||
public DateTime FechaInicio { get; set; }
|
||||
public DateTime? FechaFin { get; set; }
|
||||
public bool Activa { get; set; }
|
||||
public int IdUsuarioAlta { get; set; }
|
||||
public DateTime FechaAlta { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace GestionIntegral.Api.Models.Suscripciones
|
||||
{
|
||||
public class SuscripcionPromocion
|
||||
{
|
||||
public int IdSuscripcion { get; set; }
|
||||
public int IdPromocion { get; set; }
|
||||
public DateTime FechaAsignacion { get; set; }
|
||||
public int IdUsuarioAsigno { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,8 @@ using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
using GestionIntegral.Api.Services.Anomalia;
|
||||
using GestionIntegral.Api.Data.Repositories.Suscripciones;
|
||||
using GestionIntegral.Api.Services.Suscripciones;
|
||||
using GestionIntegral.Api.Models.Comunicaciones;
|
||||
using GestionIntegral.Api.Services.Comunicaciones;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
@@ -103,18 +105,25 @@ builder.Services.AddScoped<IQuestPdfGenerator, QuestPdfGenerator>();
|
||||
builder.Services.AddScoped<IAlertaService, AlertaService>();
|
||||
|
||||
// --- Suscripciones ---
|
||||
// Repositorios
|
||||
builder.Services.AddScoped<IFormaPagoRepository, FormaPagoRepository>();
|
||||
builder.Services.AddScoped<ISuscriptorRepository, SuscriptorRepository>();
|
||||
builder.Services.AddScoped<ISuscripcionRepository, SuscripcionRepository>();
|
||||
builder.Services.AddScoped<IFacturaRepository, FacturaRepository>();
|
||||
builder.Services.AddScoped<ILoteDebitoRepository, LoteDebitoRepository>();
|
||||
builder.Services.AddScoped<IPagoRepository, PagoRepository>();
|
||||
builder.Services.AddScoped<IPromocionRepository, PromocionRepository>();
|
||||
|
||||
// Servicios
|
||||
builder.Services.AddScoped<IFormaPagoService, FormaPagoService>();
|
||||
builder.Services.AddScoped<ISuscriptorService, SuscriptorService>();
|
||||
builder.Services.AddScoped<ISuscripcionService, SuscripcionService>();
|
||||
builder.Services.AddScoped<IFacturacionService, FacturacionService>();
|
||||
builder.Services.AddScoped<IDebitoAutomaticoService, DebitoAutomaticoService>();
|
||||
builder.Services.AddScoped<IPagoService, PagoService>();
|
||||
builder.Services.AddScoped<IPromocionService, PromocionService>();
|
||||
|
||||
// --- Comunicaciones ---
|
||||
builder.Services.Configure<MailSettings>(builder.Configuration.GetSection("MailSettings"));
|
||||
builder.Services.AddTransient<IEmailService, EmailService>();
|
||||
|
||||
// --- SERVICIO DE HEALTH CHECKS ---
|
||||
// Añadimos una comprobación específica para SQL Server.
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
using GestionIntegral.Api.Models.Comunicaciones;
|
||||
using MailKit.Net.Smtp;
|
||||
using MailKit.Security;
|
||||
using Microsoft.Extensions.Options;
|
||||
using MimeKit;
|
||||
|
||||
namespace GestionIntegral.Api.Services.Comunicaciones
|
||||
{
|
||||
public class EmailService : IEmailService
|
||||
{
|
||||
private readonly MailSettings _mailSettings;
|
||||
private readonly ILogger<EmailService> _logger;
|
||||
|
||||
public EmailService(IOptions<MailSettings> mailSettings, ILogger<EmailService> logger)
|
||||
{
|
||||
_mailSettings = mailSettings.Value;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task EnviarEmailAsync(string destinatarioEmail, string destinatarioNombre, string asunto, string cuerpoHtml)
|
||||
{
|
||||
var email = new MimeMessage();
|
||||
email.Sender = new MailboxAddress(_mailSettings.SenderName, _mailSettings.SenderEmail);
|
||||
email.From.Add(email.Sender);
|
||||
email.To.Add(new MailboxAddress(destinatarioNombre, destinatarioEmail));
|
||||
email.Subject = asunto;
|
||||
|
||||
var builder = new BodyBuilder { HtmlBody = cuerpoHtml };
|
||||
email.Body = builder.ToMessageBody();
|
||||
|
||||
using var smtp = new SmtpClient();
|
||||
try
|
||||
{
|
||||
await smtp.ConnectAsync(_mailSettings.SmtpHost, _mailSettings.SmtpPort, SecureSocketOptions.StartTls);
|
||||
await smtp.AuthenticateAsync(_mailSettings.SmtpUser, _mailSettings.SmtpPass);
|
||||
await smtp.SendAsync(email);
|
||||
_logger.LogInformation("Email enviado exitosamente a {Destinatario}", destinatarioEmail);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error al enviar email a {Destinatario}", destinatarioEmail);
|
||||
throw; // Relanzar para que el servicio que lo llamó sepa que falló
|
||||
}
|
||||
finally
|
||||
{
|
||||
await smtp.DisconnectAsync(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace GestionIntegral.Api.Services.Comunicaciones
|
||||
{
|
||||
public interface IEmailService
|
||||
{
|
||||
Task EnviarEmailAsync(string destinatarioEmail, string destinatarioNombre, string asunto, string cuerpoHtml);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
// Archivo: GestionIntegral.Api/Services/Suscripciones/DebitoAutomaticoService.cs
|
||||
|
||||
using GestionIntegral.Api.Data;
|
||||
using GestionIntegral.Api.Data.Repositories.Suscripciones;
|
||||
using GestionIntegral.Api.Models.Suscripciones;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Data;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Collections.Generic;
|
||||
using GestionIntegral.Api.Dtos.Suscripciones;
|
||||
|
||||
namespace GestionIntegral.Api.Services.Suscripciones
|
||||
{
|
||||
public class DebitoAutomaticoService : IDebitoAutomaticoService
|
||||
{
|
||||
private readonly IFacturaRepository _facturaRepository;
|
||||
private readonly ISuscriptorRepository _suscriptorRepository;
|
||||
private readonly ISuscripcionRepository _suscripcionRepository;
|
||||
private readonly ILoteDebitoRepository _loteDebitoRepository;
|
||||
private readonly IFormaPagoRepository _formaPagoRepository;
|
||||
private readonly IPagoRepository _pagoRepository;
|
||||
private readonly DbConnectionFactory _connectionFactory;
|
||||
private readonly ILogger<DebitoAutomaticoService> _logger;
|
||||
|
||||
// --- CONSTANTES DEL BANCO (Mover a appsettings.json si es necesario) ---
|
||||
private const string NRO_PRESTACION = "123456"; // Nro. de prestación asignado por el banco
|
||||
private const string ORIGEN_EMPRESA = "ELDIA"; // Nombre de la empresa (7 chars)
|
||||
|
||||
public DebitoAutomaticoService(
|
||||
IFacturaRepository facturaRepository,
|
||||
ISuscriptorRepository suscriptorRepository,
|
||||
ISuscripcionRepository suscripcionRepository,
|
||||
ILoteDebitoRepository loteDebitoRepository,
|
||||
IFormaPagoRepository formaPagoRepository,
|
||||
IPagoRepository pagoRepository,
|
||||
DbConnectionFactory connectionFactory,
|
||||
ILogger<DebitoAutomaticoService> logger)
|
||||
{
|
||||
_facturaRepository = facturaRepository;
|
||||
_suscriptorRepository = suscriptorRepository;
|
||||
_suscripcionRepository = suscripcionRepository;
|
||||
_loteDebitoRepository = loteDebitoRepository;
|
||||
_formaPagoRepository = formaPagoRepository;
|
||||
_pagoRepository = pagoRepository;
|
||||
_connectionFactory = connectionFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<(string? ContenidoArchivo, string? NombreArchivo, string? Error)> GenerarArchivoPagoDirecto(int anio, int mes, int idUsuario)
|
||||
{
|
||||
var periodo = $"{anio}-{mes:D2}";
|
||||
var fechaGeneracion = DateTime.Now;
|
||||
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
await (connection as System.Data.Common.DbConnection)!.OpenAsync();
|
||||
using var transaction = connection.BeginTransaction();
|
||||
|
||||
try
|
||||
{
|
||||
// Buscamos facturas que están listas para ser enviadas al cobro.
|
||||
var facturasParaDebito = await GetFacturasParaDebito(periodo, transaction);
|
||||
|
||||
if (!facturasParaDebito.Any())
|
||||
{
|
||||
return (null, null, "No se encontraron facturas pendientes de cobro por débito automático para el período seleccionado.");
|
||||
}
|
||||
|
||||
var importeTotal = facturasParaDebito.Sum(f => f.Factura.ImporteFinal);
|
||||
var cantidadRegistros = facturasParaDebito.Count();
|
||||
var nombreArchivo = $"PD_{ORIGEN_EMPRESA.Trim()}_{fechaGeneracion:yyyyMMdd}.txt";
|
||||
|
||||
// 1. Crear el Lote de Débito
|
||||
var nuevoLote = new LoteDebito
|
||||
{
|
||||
Periodo = periodo,
|
||||
NombreArchivo = nombreArchivo,
|
||||
ImporteTotal = importeTotal,
|
||||
CantidadRegistros = cantidadRegistros,
|
||||
IdUsuarioGeneracion = idUsuario
|
||||
};
|
||||
var loteCreado = await _loteDebitoRepository.CreateAsync(nuevoLote, transaction);
|
||||
if (loteCreado == null) throw new DataException("No se pudo crear el registro del lote de débito.");
|
||||
|
||||
// 2. Generar el contenido del archivo
|
||||
var sb = new StringBuilder();
|
||||
sb.Append(CrearRegistroHeader(fechaGeneracion, importeTotal, cantidadRegistros));
|
||||
|
||||
foreach (var item in facturasParaDebito)
|
||||
{
|
||||
sb.Append(CrearRegistroDetalle(item.Factura, item.Suscriptor));
|
||||
}
|
||||
|
||||
sb.Append(CrearRegistroTrailer(fechaGeneracion, importeTotal, cantidadRegistros));
|
||||
|
||||
// 3. Actualizar las facturas con el ID del lote
|
||||
var idsFacturas = facturasParaDebito.Select(f => f.Factura.IdFactura);
|
||||
bool actualizadas = await _facturaRepository.UpdateLoteDebitoAsync(idsFacturas, loteCreado.IdLoteDebito, transaction);
|
||||
if (!actualizadas) throw new DataException("No se pudieron actualizar las facturas con la información del lote.");
|
||||
|
||||
transaction.Commit();
|
||||
_logger.LogInformation("Archivo de débito {NombreArchivo} generado exitosamente para el período {Periodo}.", nombreArchivo, periodo);
|
||||
return (sb.ToString(), nombreArchivo, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
try { transaction.Rollback(); } catch { }
|
||||
_logger.LogError(ex, "Error crítico al generar el archivo de débito para el período {Periodo}", periodo);
|
||||
return (null, null, $"Error interno: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<List<(Factura Factura, Suscriptor Suscriptor)>> GetFacturasParaDebito(string periodo, IDbTransaction transaction)
|
||||
{
|
||||
// Idealmente, esto debería estar en el repositorio para optimizar la consulta.
|
||||
// Por simplicidad del ejemplo, lo hacemos aquí.
|
||||
var facturas = await _facturaRepository.GetByPeriodoAsync(periodo);
|
||||
var resultado = new List<(Factura, Suscriptor)>();
|
||||
|
||||
foreach (var f in facturas.Where(fa => fa.Estado == "Pendiente de Cobro"))
|
||||
{
|
||||
var suscripcion = await _suscripcionRepository.GetByIdAsync(f.IdSuscripcion);
|
||||
if (suscripcion == null) continue;
|
||||
|
||||
var suscriptor = await _suscriptorRepository.GetByIdAsync(suscripcion.IdSuscriptor);
|
||||
if (suscriptor == null || string.IsNullOrWhiteSpace(suscriptor.CBU)) continue;
|
||||
|
||||
var formaPago = await _formaPagoRepository.GetByIdAsync(suscriptor.IdFormaPagoPreferida);
|
||||
if (formaPago != null && formaPago.RequiereCBU)
|
||||
{
|
||||
resultado.Add((f, suscriptor));
|
||||
}
|
||||
}
|
||||
return resultado;
|
||||
}
|
||||
|
||||
// --- Métodos de Formateo de Campos ---
|
||||
private string FormatString(string? value, int length) => (value ?? "").PadRight(length).Substring(0, length);
|
||||
private string FormatNumeric(long value, int length) => value.ToString().PadLeft(length, '0');
|
||||
|
||||
private string CrearRegistroHeader(DateTime fechaGeneracion, decimal importeTotal, int cantidadRegistros)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.Append("00"); // Tipo de Registro
|
||||
sb.Append(FormatString(NRO_PRESTACION, 6));
|
||||
sb.Append("C"); // Servicio
|
||||
sb.Append(fechaGeneracion.ToString("yyyyMMdd"));
|
||||
sb.Append("1"); // Identificación de Archivo (ej. '1' para el primer envío del día)
|
||||
sb.Append(FormatString(ORIGEN_EMPRESA, 7));
|
||||
sb.Append(FormatNumeric((long)(importeTotal * 100), 14)); // 12 enteros + 2 decimales
|
||||
sb.Append(FormatNumeric(cantidadRegistros, 7));
|
||||
sb.Append(FormatString("", 304)); // Libre
|
||||
sb.Append("\r\n");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private string CrearRegistroDetalle(Factura factura, Suscriptor suscriptor)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.Append("0101"); // Tipo de Registro
|
||||
sb.Append(FormatString(suscriptor.IdSuscriptor.ToString(), 22)); // Identificación Cliente
|
||||
sb.Append(FormatString(suscriptor.CBU, 26)); // CBU
|
||||
|
||||
// Referencia Unívoca: Usaremos ID Factura para asegurar unicidad
|
||||
sb.Append(FormatString($"SUSC-{factura.IdFactura}", 15));
|
||||
|
||||
sb.Append(factura.FechaVencimiento.ToString("yyyyMMdd")); // Fecha 1er Vto
|
||||
sb.Append(FormatNumeric((long)(factura.ImporteFinal * 100), 14)); // Importe 1er Vto
|
||||
|
||||
// Campos opcionales o con valores fijos
|
||||
sb.Append(FormatNumeric(0, 8)); // Fecha 2do Vto
|
||||
sb.Append(FormatNumeric(0, 14)); // Importe 2do Vto
|
||||
sb.Append(FormatNumeric(0, 8)); // Fecha 3er Vto
|
||||
sb.Append(FormatNumeric(0, 14)); // Importe 3er Vto
|
||||
sb.Append("0"); // Moneda (0 = Pesos)
|
||||
sb.Append(FormatString("", 3)); // Motivo Rechazo
|
||||
sb.Append(FormatString(suscriptor.TipoDocumento, 4));
|
||||
sb.Append(FormatString(suscriptor.NroDocumento, 11));
|
||||
|
||||
// El resto son campos opcionales que rellenamos con espacios/ceros
|
||||
sb.Append(FormatString("", 22)); // Nueva ID Cliente
|
||||
sb.Append(FormatNumeric(0, 26)); // Nuevo CBU
|
||||
sb.Append(FormatNumeric(0, 14)); // Importe Mínimo
|
||||
sb.Append(FormatNumeric(0, 8)); // Fecha Próximo Vto
|
||||
sb.Append(FormatString("", 22)); // ID Cuenta Anterior
|
||||
sb.Append(FormatString("", 40)); // Mensaje ATM
|
||||
sb.Append(FormatString($"Suscripcion {factura.Periodo}", 10)); // Concepto Factura
|
||||
sb.Append(FormatNumeric(0, 8)); // Fecha de Cobro
|
||||
sb.Append(FormatNumeric(0, 14)); // Importe Cobrado
|
||||
sb.Append(FormatNumeric(0, 8)); // Fecha Acreditación
|
||||
sb.Append(FormatString("", 26)); // Libre
|
||||
sb.Append("\r\n");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private string CrearRegistroTrailer(DateTime fechaGeneracion, decimal importeTotal, int cantidadRegistros)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.Append("99"); // Tipo de Registro
|
||||
sb.Append(FormatString(NRO_PRESTACION, 6));
|
||||
sb.Append("C"); // Servicio
|
||||
sb.Append(fechaGeneracion.ToString("yyyyMMdd"));
|
||||
sb.Append("1"); // Identificación de Archivo
|
||||
sb.Append(FormatString(ORIGEN_EMPRESA, 7));
|
||||
sb.Append(FormatNumeric((long)(importeTotal * 100), 14));
|
||||
sb.Append(FormatNumeric(cantidadRegistros, 7));
|
||||
sb.Append(FormatString("", 304)); // Libre
|
||||
// No se añade \r\n al final del último registro
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public async Task<ProcesamientoLoteResponseDto> ProcesarArchivoRespuesta(IFormFile archivo, int idUsuario)
|
||||
{
|
||||
var respuesta = new ProcesamientoLoteResponseDto();
|
||||
if (archivo == null || archivo.Length == 0)
|
||||
{
|
||||
respuesta.Errores.Add("No se proporcionó ningún archivo o el archivo está vacío.");
|
||||
return respuesta;
|
||||
}
|
||||
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
await (connection as System.Data.Common.DbConnection)!.OpenAsync();
|
||||
using var transaction = connection.BeginTransaction();
|
||||
|
||||
try
|
||||
{
|
||||
using var reader = new StreamReader(archivo.OpenReadStream());
|
||||
string? linea;
|
||||
while ((linea = await reader.ReadLineAsync()) != null)
|
||||
{
|
||||
// Ignorar header/trailer si los hubiera (basado en el formato real)
|
||||
if (linea.Length < 20) continue;
|
||||
|
||||
respuesta.TotalRegistrosLeidos++;
|
||||
|
||||
// =================================================================
|
||||
// === ESTA ES LA LÓGICA DE PARSEO QUE SE DEBE AJUSTAR ===
|
||||
// === CON EL FORMATO REAL DEL ARCHIVO DE RESPUESTA ===
|
||||
// =================================================================
|
||||
// Asunción: Pos 1-15: Referencia, Pos 16-17: Estado, Pos 18-20: Rechazo
|
||||
var referencia = linea.Substring(0, 15).Trim();
|
||||
var estadoProceso = linea.Substring(15, 2).Trim();
|
||||
var motivoRechazo = linea.Substring(17, 3).Trim();
|
||||
// Asumimos que podemos extraer el IdFactura de la referencia
|
||||
if (!int.TryParse(referencia.Replace("SUSC-", ""), out int idFactura))
|
||||
{
|
||||
respuesta.Errores.Add($"Línea #{respuesta.TotalRegistrosLeidos}: No se pudo extraer un ID de factura válido de la referencia '{referencia}'.");
|
||||
continue;
|
||||
}
|
||||
// =================================================================
|
||||
// === FIN DE LA LÓGICA DE PARSEO ===
|
||||
// =================================================================
|
||||
|
||||
var factura = await _facturaRepository.GetByIdAsync(idFactura);
|
||||
if (factura == null)
|
||||
{
|
||||
respuesta.Errores.Add($"Línea #{respuesta.TotalRegistrosLeidos}: La factura con ID {idFactura} no fue encontrada en el sistema.");
|
||||
continue;
|
||||
}
|
||||
|
||||
var nuevoPago = new Pago
|
||||
{
|
||||
IdFactura = idFactura,
|
||||
FechaPago = DateTime.Now.Date, // O la fecha que venga en el archivo
|
||||
IdFormaPago = 1, // 1 = Débito Automático
|
||||
Monto = factura.ImporteFinal,
|
||||
IdUsuarioRegistro = idUsuario,
|
||||
Referencia = $"Lote {factura.IdLoteDebito} - Banco"
|
||||
};
|
||||
|
||||
if (estadoProceso == "AP") // "AP" = Aprobado (Asunción)
|
||||
{
|
||||
nuevoPago.Estado = "Aprobado";
|
||||
await _pagoRepository.CreateAsync(nuevoPago, transaction);
|
||||
await _facturaRepository.UpdateEstadoAsync(idFactura, "Pagada", transaction);
|
||||
respuesta.PagosAprobados++;
|
||||
}
|
||||
else // Asumimos que cualquier otra cosa es Rechazado
|
||||
{
|
||||
nuevoPago.Estado = "Rechazado";
|
||||
await _pagoRepository.CreateAsync(nuevoPago, transaction);
|
||||
factura.Estado = "Rechazada";
|
||||
factura.MotivoRechazo = motivoRechazo;
|
||||
// Necesitamos un método en el repo para actualizar estado y motivo
|
||||
await _facturaRepository.UpdateEstadoYMotivoAsync(idFactura, "Rechazada", motivoRechazo, transaction);
|
||||
respuesta.PagosRechazados++;
|
||||
}
|
||||
}
|
||||
|
||||
transaction.Commit();
|
||||
respuesta.MensajeResumen = $"Archivo procesado. Leídos: {respuesta.TotalRegistrosLeidos}, Aprobados: {respuesta.PagosAprobados}, Rechazados: {respuesta.PagosRechazados}.";
|
||||
_logger.LogInformation(respuesta.MensajeResumen);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
try { transaction.Rollback(); } catch { }
|
||||
_logger.LogError(ex, "Error crítico al procesar archivo de respuesta de débito.");
|
||||
respuesta.Errores.Add($"Error fatal en el procesamiento: {ex.Message}");
|
||||
}
|
||||
|
||||
return respuesta;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
using GestionIntegral.Api.Data;
|
||||
using GestionIntegral.Api.Data.Repositories.Distribucion;
|
||||
using GestionIntegral.Api.Data.Repositories.Suscripciones;
|
||||
using GestionIntegral.Api.Dtos.Suscripciones;
|
||||
using GestionIntegral.Api.Models.Distribucion;
|
||||
using GestionIntegral.Api.Models.Suscripciones;
|
||||
using GestionIntegral.Api.Services.Comunicaciones;
|
||||
using System.Data;
|
||||
|
||||
namespace GestionIntegral.Api.Services.Suscripciones
|
||||
{
|
||||
public class FacturacionService : IFacturacionService
|
||||
{
|
||||
private readonly ISuscripcionRepository _suscripcionRepository;
|
||||
private readonly IFacturaRepository _facturaRepository;
|
||||
private readonly IPrecioRepository _precioRepository;
|
||||
private readonly IPromocionRepository _promocionRepository;
|
||||
private readonly IRecargoZonaRepository _recargoZonaRepository; // Para futura implementación
|
||||
private readonly ISuscriptorRepository _suscriptorRepository; // Para obtener zona del suscriptor
|
||||
private readonly DbConnectionFactory _connectionFactory;
|
||||
private readonly IEmailService _emailService;
|
||||
private readonly ILogger<FacturacionService> _logger;
|
||||
|
||||
public FacturacionService(
|
||||
ISuscripcionRepository suscripcionRepository,
|
||||
IFacturaRepository facturaRepository,
|
||||
IPrecioRepository precioRepository,
|
||||
IPromocionRepository promocionRepository,
|
||||
IRecargoZonaRepository recargoZonaRepository,
|
||||
ISuscriptorRepository suscriptorRepository,
|
||||
DbConnectionFactory connectionFactory,
|
||||
IEmailService emailService,
|
||||
ILogger<FacturacionService> logger)
|
||||
{
|
||||
_suscripcionRepository = suscripcionRepository;
|
||||
_facturaRepository = facturaRepository;
|
||||
_precioRepository = precioRepository;
|
||||
_promocionRepository = promocionRepository;
|
||||
_recargoZonaRepository = recargoZonaRepository;
|
||||
_suscriptorRepository = suscriptorRepository;
|
||||
_connectionFactory = connectionFactory;
|
||||
_emailService = emailService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<(bool Exito, string? Mensaje, int FacturasGeneradas)> GenerarFacturacionMensual(int anio, int mes, int idUsuario)
|
||||
{
|
||||
var periodo = $"{anio}-{mes:D2}";
|
||||
_logger.LogInformation("Iniciando generación de facturación para el período {Periodo} por usuario {IdUsuario}", periodo, idUsuario);
|
||||
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
await (connection as System.Data.Common.DbConnection)!.OpenAsync();
|
||||
using var transaction = connection.BeginTransaction();
|
||||
|
||||
try
|
||||
{
|
||||
var suscripcionesActivas = await _suscripcionRepository.GetAllActivasParaFacturacion(periodo, transaction);
|
||||
if (!suscripcionesActivas.Any())
|
||||
{
|
||||
return (true, "No se encontraron suscripciones activas para facturar en el período especificado.", 0);
|
||||
}
|
||||
|
||||
int facturasGeneradas = 0;
|
||||
foreach (var suscripcion in suscripcionesActivas)
|
||||
{
|
||||
var facturaExistente = await _facturaRepository.GetBySuscripcionYPeriodoAsync(suscripcion.IdSuscripcion, periodo, transaction);
|
||||
if (facturaExistente != null)
|
||||
{
|
||||
_logger.LogWarning("Ya existe una factura (ID: {IdFactura}) para la suscripción ID {IdSuscripcion} en el período {Periodo}. Se omite.", facturaExistente.IdFactura, suscripcion.IdSuscripcion, periodo);
|
||||
continue;
|
||||
}
|
||||
|
||||
// --- LÓGICA DE PROMOCIONES ---
|
||||
var primerDiaMes = new DateTime(anio, mes, 1);
|
||||
var promocionesAplicables = await _promocionRepository.GetPromocionesActivasParaSuscripcion(suscripcion.IdSuscripcion, primerDiaMes, transaction);
|
||||
|
||||
decimal importeBruto = await CalcularImporteParaSuscripcion(suscripcion, anio, mes, transaction);
|
||||
decimal descuentoTotal = 0;
|
||||
|
||||
// Aplicar promociones de descuento
|
||||
foreach (var promo in promocionesAplicables.Where(p => p.TipoPromocion == "Porcentaje"))
|
||||
{
|
||||
descuentoTotal += (importeBruto * promo.Valor) / 100;
|
||||
}
|
||||
foreach (var promo in promocionesAplicables.Where(p => p.TipoPromocion == "MontoFijo"))
|
||||
{
|
||||
descuentoTotal += promo.Valor;
|
||||
}
|
||||
// La bonificación de días se aplicaría idealmente dentro de CalcularImporteParaSuscripcion,
|
||||
// pero por simplicidad, aquí solo manejamos descuentos sobre el total.
|
||||
|
||||
if (importeBruto <= 0)
|
||||
{
|
||||
_logger.LogInformation("Suscripción ID {IdSuscripcion} no tiene importe a facturar para el período {Periodo}. Se omite.", suscripcion.IdSuscripcion, periodo);
|
||||
continue;
|
||||
}
|
||||
|
||||
var importeFinal = importeBruto - descuentoTotal;
|
||||
if (importeFinal < 0) importeFinal = 0; // El importe no puede ser negativo
|
||||
|
||||
var nuevaFactura = new Factura
|
||||
{
|
||||
IdSuscripcion = suscripcion.IdSuscripcion,
|
||||
Periodo = periodo,
|
||||
FechaEmision = DateTime.Now.Date,
|
||||
FechaVencimiento = new DateTime(anio, mes, 10).AddMonths(1),
|
||||
ImporteBruto = importeBruto,
|
||||
DescuentoAplicado = descuentoTotal,
|
||||
ImporteFinal = importeFinal,
|
||||
Estado = "Pendiente de Facturar"
|
||||
};
|
||||
|
||||
var facturaCreada = await _facturaRepository.CreateAsync(nuevaFactura, transaction);
|
||||
if (facturaCreada == null) throw new DataException($"No se pudo crear el registro de factura para la suscripción ID {suscripcion.IdSuscripcion}");
|
||||
|
||||
facturasGeneradas++;
|
||||
}
|
||||
|
||||
transaction.Commit();
|
||||
_logger.LogInformation("Finalizada la generación de facturación para {Periodo}. Total generadas: {FacturasGeneradas}", periodo, facturasGeneradas);
|
||||
return (true, $"Proceso completado. Se generaron {facturasGeneradas} facturas.", facturasGeneradas);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
try { transaction.Rollback(); } catch { }
|
||||
_logger.LogError(ex, "Error crítico durante la generación de facturación para el período {Periodo}", periodo);
|
||||
return (false, "Error interno del servidor al generar la facturación.", 0);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<decimal> CalcularImporteParaSuscripcion(Suscripcion suscripcion, int anio, int mes, IDbTransaction transaction)
|
||||
{
|
||||
decimal importeTotal = 0;
|
||||
var diasDeEntrega = suscripcion.DiasEntrega.Split(',').ToHashSet();
|
||||
var fechaActual = new DateTime(anio, mes, 1);
|
||||
|
||||
while (fechaActual.Month == mes)
|
||||
{
|
||||
// La suscripción debe estar activa en este día
|
||||
if (fechaActual.Date >= suscripcion.FechaInicio.Date &&
|
||||
(suscripcion.FechaFin == null || fechaActual.Date <= suscripcion.FechaFin.Value.Date))
|
||||
{
|
||||
var diaSemanaChar = GetCharDiaSemana(fechaActual.DayOfWeek);
|
||||
if (diasDeEntrega.Contains(diaSemanaChar))
|
||||
{
|
||||
var precioActivo = await _precioRepository.GetActiveByPublicacionAndDateAsync(suscripcion.IdPublicacion, fechaActual, transaction);
|
||||
if (precioActivo != null)
|
||||
{
|
||||
importeTotal += GetPrecioDelDia(precioActivo, fechaActual.DayOfWeek);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("No se encontró precio para la publicación ID {IdPublicacion} en la fecha {Fecha}", suscripcion.IdPublicacion, fechaActual.Date);
|
||||
}
|
||||
}
|
||||
}
|
||||
fechaActual = fechaActual.AddDays(1);
|
||||
}
|
||||
return importeTotal;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<FacturaDto>> ObtenerFacturasPorPeriodo(int anio, int mes)
|
||||
{
|
||||
var periodo = $"{anio}-{mes:D2}";
|
||||
var facturasData = await _facturaRepository.GetByPeriodoEnrichedAsync(periodo);
|
||||
|
||||
return facturasData.Select(data => new FacturaDto
|
||||
{
|
||||
IdFactura = data.Factura.IdFactura,
|
||||
IdSuscripcion = data.Factura.IdSuscripcion,
|
||||
Periodo = data.Factura.Periodo,
|
||||
FechaEmision = data.Factura.FechaEmision.ToString("yyyy-MM-dd"),
|
||||
FechaVencimiento = data.Factura.FechaVencimiento.ToString("yyyy-MM-dd"),
|
||||
ImporteFinal = data.Factura.ImporteFinal,
|
||||
Estado = data.Factura.Estado,
|
||||
NumeroFactura = data.Factura.NumeroFactura,
|
||||
NombreSuscriptor = data.NombreSuscriptor,
|
||||
NombrePublicacion = data.NombrePublicacion
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<(bool Exito, string? Error)> EnviarFacturaPorEmail(int idFactura)
|
||||
{
|
||||
var factura = await _facturaRepository.GetByIdAsync(idFactura);
|
||||
if (factura == null) return (false, "Factura no encontrada.");
|
||||
if (string.IsNullOrEmpty(factura.NumeroFactura)) return (false, "La factura aún no tiene un número asignado por ARCA.");
|
||||
|
||||
var suscripcion = await _suscripcionRepository.GetByIdAsync(factura.IdSuscripcion);
|
||||
if (suscripcion == null) return (false, "Suscripción asociada no encontrada.");
|
||||
|
||||
var suscriptor = await _suscriptorRepository.GetByIdAsync(suscripcion.IdSuscriptor);
|
||||
if (suscriptor == null) return (false, "Suscriptor asociado no encontrado.");
|
||||
if (string.IsNullOrEmpty(suscriptor.Email)) return (false, "El suscriptor no tiene una dirección de email configurada.");
|
||||
|
||||
try
|
||||
{
|
||||
var asunto = $"Tu factura del Diario El Día - Período {factura.Periodo}";
|
||||
var cuerpo = $@"
|
||||
<h1>Hola {suscriptor.NombreCompleto},</h1>
|
||||
<p>Te adjuntamos los detalles de tu factura para el período {factura.Periodo}.</p>
|
||||
<ul>
|
||||
<li><strong>Número de Factura:</strong> {factura.NumeroFactura}</li>
|
||||
<li><strong>Importe Total:</strong> ${factura.ImporteFinal:N2}</li>
|
||||
<li><strong>Fecha de Vencimiento:</strong> {factura.FechaVencimiento:dd/MM/yyyy}</li>
|
||||
</ul>
|
||||
<p>Gracias por ser parte de nuestra comunidad de lectores.</p>
|
||||
<p><em>Diario El Día</em></p>";
|
||||
|
||||
await _emailService.EnviarEmailAsync(suscriptor.Email, suscriptor.NombreCompleto, asunto, cuerpo);
|
||||
return (true, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Falló el envío de email para la factura ID {IdFactura}", idFactura);
|
||||
return (false, "Ocurrió un error al intentar enviar el email.");
|
||||
}
|
||||
}
|
||||
|
||||
private string GetCharDiaSemana(DayOfWeek dia) => dia switch
|
||||
{
|
||||
DayOfWeek.Sunday => "D",
|
||||
DayOfWeek.Monday => "L",
|
||||
DayOfWeek.Tuesday => "M",
|
||||
DayOfWeek.Wednesday => "X",
|
||||
DayOfWeek.Thursday => "J",
|
||||
DayOfWeek.Friday => "V",
|
||||
DayOfWeek.Saturday => "S",
|
||||
_ => ""
|
||||
};
|
||||
|
||||
private decimal GetPrecioDelDia(Precio precio, DayOfWeek dia) => dia switch
|
||||
{
|
||||
DayOfWeek.Sunday => precio.Domingo ?? 0,
|
||||
DayOfWeek.Monday => precio.Lunes ?? 0,
|
||||
DayOfWeek.Tuesday => precio.Martes ?? 0,
|
||||
DayOfWeek.Wednesday => precio.Miercoles ?? 0,
|
||||
DayOfWeek.Thursday => precio.Jueves ?? 0,
|
||||
DayOfWeek.Friday => precio.Viernes ?? 0,
|
||||
DayOfWeek.Saturday => precio.Sabado ?? 0,
|
||||
_ => 0
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using GestionIntegral.Api.Dtos.Suscripciones;
|
||||
|
||||
namespace GestionIntegral.Api.Services.Suscripciones
|
||||
{
|
||||
public interface IDebitoAutomaticoService
|
||||
{
|
||||
Task<(string? ContenidoArchivo, string? NombreArchivo, string? Error)> GenerarArchivoPagoDirecto(int anio, int mes, int idUsuario);
|
||||
Task<ProcesamientoLoteResponseDto> ProcesarArchivoRespuesta(IFormFile archivo, int idUsuario);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using GestionIntegral.Api.Dtos.Suscripciones;
|
||||
|
||||
namespace GestionIntegral.Api.Services.Suscripciones
|
||||
{
|
||||
public interface IFacturacionService
|
||||
{
|
||||
Task<(bool Exito, string? Mensaje, int FacturasGeneradas)> GenerarFacturacionMensual(int anio, int mes, int idUsuario);
|
||||
Task<IEnumerable<FacturaDto>> ObtenerFacturasPorPeriodo(int anio, int mes);
|
||||
Task<(bool Exito, string? Error)> EnviarFacturaPorEmail(int idFactura);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Archivo: GestionIntegral.Api/Services/Suscripciones/IPagoService.cs
|
||||
|
||||
using GestionIntegral.Api.Dtos.Suscripciones;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace GestionIntegral.Api.Services.Suscripciones
|
||||
{
|
||||
public interface IPagoService
|
||||
{
|
||||
Task<IEnumerable<PagoDto>> ObtenerPagosPorFacturaId(int idFactura);
|
||||
Task<(PagoDto? Pago, string? Error)> RegistrarPagoManual(CreatePagoDto createDto, int idUsuario);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using GestionIntegral.Api.Dtos.Suscripciones;
|
||||
|
||||
namespace GestionIntegral.Api.Services.Suscripciones
|
||||
{
|
||||
public interface IPromocionService
|
||||
{
|
||||
Task<IEnumerable<PromocionDto>> ObtenerTodas(bool soloActivas);
|
||||
Task<PromocionDto?> ObtenerPorId(int id);
|
||||
Task<(PromocionDto? Promocion, string? Error)> Crear(CreatePromocionDto createDto, int idUsuario);
|
||||
Task<(bool Exito, string? Error)> Actualizar(int id, UpdatePromocionDto updateDto, int idUsuario);
|
||||
}
|
||||
}
|
||||
@@ -8,5 +8,9 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
||||
Task<SuscripcionDto?> ObtenerPorId(int idSuscripcion);
|
||||
Task<(SuscripcionDto? Suscripcion, string? Error)> Crear(CreateSuscripcionDto createDto, int idUsuario);
|
||||
Task<(bool Exito, string? Error)> Actualizar(int idSuscripcion, UpdateSuscripcionDto updateDto, int idUsuario);
|
||||
Task<IEnumerable<PromocionDto>> ObtenerPromocionesAsignadas(int idSuscripcion);
|
||||
Task<IEnumerable<PromocionDto>> ObtenerPromocionesDisponibles(int idSuscripcion);
|
||||
Task<(bool Exito, string? Error)> AsignarPromocion(int idSuscripcion, int idPromocion, int idUsuario);
|
||||
Task<(bool Exito, string? Error)> QuitarPromocion(int idSuscripcion, int idPromocion);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
using GestionIntegral.Api.Data;
|
||||
using GestionIntegral.Api.Data.Repositories.Suscripciones;
|
||||
using GestionIntegral.Api.Data.Repositories.Usuarios;
|
||||
using GestionIntegral.Api.Dtos.Suscripciones;
|
||||
using GestionIntegral.Api.Models.Suscripciones;
|
||||
using System.Data;
|
||||
|
||||
namespace GestionIntegral.Api.Services.Suscripciones
|
||||
{
|
||||
public class PagoService : IPagoService
|
||||
{
|
||||
private readonly IPagoRepository _pagoRepository;
|
||||
private readonly IFacturaRepository _facturaRepository;
|
||||
private readonly IFormaPagoRepository _formaPagoRepository;
|
||||
private readonly IUsuarioRepository _usuarioRepository;
|
||||
private readonly DbConnectionFactory _connectionFactory;
|
||||
private readonly ILogger<PagoService> _logger;
|
||||
|
||||
public PagoService(
|
||||
IPagoRepository pagoRepository,
|
||||
IFacturaRepository facturaRepository,
|
||||
IFormaPagoRepository formaPagoRepository,
|
||||
IUsuarioRepository usuarioRepository,
|
||||
DbConnectionFactory connectionFactory,
|
||||
ILogger<PagoService> logger)
|
||||
{
|
||||
_pagoRepository = pagoRepository;
|
||||
_facturaRepository = facturaRepository;
|
||||
_formaPagoRepository = formaPagoRepository;
|
||||
_usuarioRepository = usuarioRepository;
|
||||
_connectionFactory = connectionFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
private async Task<PagoDto?> MapToDto(Pago pago)
|
||||
{
|
||||
if (pago == null) return null;
|
||||
var formaPago = await _formaPagoRepository.GetByIdAsync(pago.IdFormaPago);
|
||||
var usuario = await _usuarioRepository.GetByIdAsync(pago.IdUsuarioRegistro);
|
||||
return new PagoDto
|
||||
{
|
||||
IdPago = pago.IdPago,
|
||||
IdFactura = pago.IdFactura,
|
||||
FechaPago = pago.FechaPago.ToString("yyyy-MM-dd"),
|
||||
IdFormaPago = pago.IdFormaPago,
|
||||
NombreFormaPago = formaPago?.Nombre ?? "Desconocida",
|
||||
Monto = pago.Monto,
|
||||
Estado = pago.Estado,
|
||||
Referencia = pago.Referencia,
|
||||
Observaciones = pago.Observaciones,
|
||||
IdUsuarioRegistro = pago.IdUsuarioRegistro,
|
||||
NombreUsuarioRegistro = usuario != null ? $"{usuario.Nombre} {usuario.Apellido}" : "Usuario Desconocido"
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<PagoDto>> ObtenerPagosPorFacturaId(int idFactura)
|
||||
{
|
||||
var pagos = await _pagoRepository.GetByFacturaIdAsync(idFactura);
|
||||
var dtosTasks = pagos.Select(p => MapToDto(p));
|
||||
var dtos = await Task.WhenAll(dtosTasks);
|
||||
return dtos.Where(dto => dto != null)!;
|
||||
}
|
||||
|
||||
public async Task<(PagoDto? Pago, string? Error)> RegistrarPagoManual(CreatePagoDto createDto, int idUsuario)
|
||||
{
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
await (connection as System.Data.Common.DbConnection)!.OpenAsync();
|
||||
using var transaction = connection.BeginTransaction();
|
||||
|
||||
try
|
||||
{
|
||||
var factura = await _facturaRepository.GetByIdAsync(createDto.IdFactura);
|
||||
if (factura == null) return (null, "La factura especificada no existe.");
|
||||
if (factura.Estado == "Pagada") return (null, "La factura ya se encuentra pagada.");
|
||||
if (factura.Estado == "Anulada") return (null, "No se puede registrar un pago sobre una factura anulada.");
|
||||
|
||||
var formaPago = await _formaPagoRepository.GetByIdAsync(createDto.IdFormaPago);
|
||||
if (formaPago == null || !formaPago.Activo) return (null, "La forma de pago no es válida.");
|
||||
|
||||
var nuevoPago = new Pago
|
||||
{
|
||||
IdFactura = createDto.IdFactura,
|
||||
FechaPago = createDto.FechaPago,
|
||||
IdFormaPago = createDto.IdFormaPago,
|
||||
Monto = createDto.Monto,
|
||||
Estado = "Aprobado", // Los pagos manuales se asumen aprobados
|
||||
Referencia = createDto.Referencia,
|
||||
Observaciones = createDto.Observaciones,
|
||||
IdUsuarioRegistro = idUsuario
|
||||
};
|
||||
|
||||
// 1. Crear el registro del pago
|
||||
var pagoCreado = await _pagoRepository.CreateAsync(nuevoPago, transaction);
|
||||
if (pagoCreado == null) throw new DataException("No se pudo registrar el pago.");
|
||||
|
||||
// 2. Si el monto pagado es igual o mayor al importe de la factura, actualizar la factura
|
||||
// (Permitimos pago mayor por si hay redondeos, etc.)
|
||||
if (pagoCreado.Monto >= factura.ImporteFinal)
|
||||
{
|
||||
bool actualizado = await _facturaRepository.UpdateEstadoAsync(factura.IdFactura, "Pagada", transaction);
|
||||
if (!actualizado) throw new DataException("No se pudo actualizar el estado de la factura a 'Pagada'.");
|
||||
}
|
||||
|
||||
transaction.Commit();
|
||||
_logger.LogInformation("Pago manual ID {IdPago} registrado para Factura ID {IdFactura} por Usuario ID {IdUsuario}", pagoCreado.IdPago, pagoCreado.IdFactura, idUsuario);
|
||||
|
||||
var dto = await MapToDto(pagoCreado);
|
||||
return (dto, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
try { transaction.Rollback(); } catch { }
|
||||
_logger.LogError(ex, "Error al registrar pago manual para Factura ID {IdFactura}", createDto.IdFactura);
|
||||
return (null, $"Error interno al registrar el pago: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
using GestionIntegral.Api.Data;
|
||||
using GestionIntegral.Api.Data.Repositories.Suscripciones;
|
||||
using GestionIntegral.Api.Dtos.Suscripciones;
|
||||
using GestionIntegral.Api.Models.Suscripciones;
|
||||
using System.Data;
|
||||
|
||||
namespace GestionIntegral.Api.Services.Suscripciones
|
||||
{
|
||||
public class PromocionService : IPromocionService
|
||||
{
|
||||
private readonly IPromocionRepository _promocionRepository;
|
||||
private readonly DbConnectionFactory _connectionFactory;
|
||||
private readonly ILogger<PromocionService> _logger;
|
||||
|
||||
public PromocionService(
|
||||
IPromocionRepository promocionRepository,
|
||||
DbConnectionFactory connectionFactory,
|
||||
ILogger<PromocionService> logger)
|
||||
{
|
||||
_promocionRepository = promocionRepository;
|
||||
_connectionFactory = connectionFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
private PromocionDto MapToDto(Promocion promo)
|
||||
{
|
||||
return new PromocionDto
|
||||
{
|
||||
IdPromocion = promo.IdPromocion,
|
||||
Descripcion = promo.Descripcion,
|
||||
TipoPromocion = promo.TipoPromocion,
|
||||
Valor = promo.Valor,
|
||||
FechaInicio = promo.FechaInicio.ToString("yyyy-MM-dd"),
|
||||
FechaFin = promo.FechaFin?.ToString("yyyy-MM-dd"),
|
||||
Activa = promo.Activa
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<PromocionDto>> ObtenerTodas(bool soloActivas)
|
||||
{
|
||||
var promociones = await _promocionRepository.GetAllAsync(soloActivas);
|
||||
return promociones.Select(MapToDto);
|
||||
}
|
||||
|
||||
public async Task<PromocionDto?> ObtenerPorId(int id)
|
||||
{
|
||||
var promocion = await _promocionRepository.GetByIdAsync(id);
|
||||
return promocion != null ? MapToDto(promocion) : null;
|
||||
}
|
||||
|
||||
public async Task<(PromocionDto? Promocion, string? Error)> Crear(CreatePromocionDto createDto, int idUsuario)
|
||||
{
|
||||
if (createDto.FechaFin.HasValue && createDto.FechaFin.Value < createDto.FechaInicio)
|
||||
{
|
||||
return (null, "La fecha de fin no puede ser anterior a la fecha de inicio.");
|
||||
}
|
||||
|
||||
var nuevaPromocion = new Promocion
|
||||
{
|
||||
Descripcion = createDto.Descripcion,
|
||||
TipoPromocion = createDto.TipoPromocion,
|
||||
Valor = createDto.Valor,
|
||||
FechaInicio = createDto.FechaInicio,
|
||||
FechaFin = createDto.FechaFin,
|
||||
Activa = createDto.Activa,
|
||||
IdUsuarioAlta = idUsuario
|
||||
};
|
||||
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
await (connection as System.Data.Common.DbConnection)!.OpenAsync();
|
||||
using var transaction = connection.BeginTransaction();
|
||||
try
|
||||
{
|
||||
var promocionCreada = await _promocionRepository.CreateAsync(nuevaPromocion, transaction);
|
||||
if (promocionCreada == null) throw new DataException("Error al crear la promoción.");
|
||||
|
||||
transaction.Commit();
|
||||
_logger.LogInformation("Promoción ID {Id} creada por Usuario ID {UserId}.", promocionCreada.IdPromocion, idUsuario);
|
||||
return (MapToDto(promocionCreada), null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
try { transaction.Rollback(); } catch { }
|
||||
_logger.LogError(ex, "Error al crear promoción: {Descripcion}", createDto.Descripcion);
|
||||
return (null, $"Error interno al crear la promoción: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<(bool Exito, string? Error)> Actualizar(int id, UpdatePromocionDto updateDto, int idUsuario)
|
||||
{
|
||||
var existente = await _promocionRepository.GetByIdAsync(id);
|
||||
if (existente == null) return (false, "Promoción no encontrada.");
|
||||
|
||||
if (updateDto.FechaFin.HasValue && updateDto.FechaFin.Value < updateDto.FechaInicio)
|
||||
{
|
||||
return (false, "La fecha de fin no puede ser anterior a la fecha de inicio.");
|
||||
}
|
||||
|
||||
// Mapeo
|
||||
existente.Descripcion = updateDto.Descripcion;
|
||||
existente.TipoPromocion = updateDto.TipoPromocion;
|
||||
existente.Valor = updateDto.Valor;
|
||||
existente.FechaInicio = updateDto.FechaInicio;
|
||||
existente.FechaFin = updateDto.FechaFin;
|
||||
existente.Activa = updateDto.Activa;
|
||||
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
await (connection as System.Data.Common.DbConnection)!.OpenAsync();
|
||||
using var transaction = connection.BeginTransaction();
|
||||
try
|
||||
{
|
||||
var actualizado = await _promocionRepository.UpdateAsync(existente, transaction);
|
||||
if (!actualizado) throw new DataException("Error al actualizar la promoción.");
|
||||
|
||||
transaction.Commit();
|
||||
_logger.LogInformation("Promoción ID {Id} actualizada por Usuario ID {UserId}.", id, idUsuario);
|
||||
return (true, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
try { transaction.Rollback(); } catch { }
|
||||
_logger.LogError(ex, "Error al actualizar promoción ID: {Id}", id);
|
||||
return (false, $"Error interno al actualizar: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
||||
private readonly ISuscripcionRepository _suscripcionRepository;
|
||||
private readonly ISuscriptorRepository _suscriptorRepository;
|
||||
private readonly IPublicacionRepository _publicacionRepository;
|
||||
private readonly IPromocionRepository _promocionRepository;
|
||||
private readonly DbConnectionFactory _connectionFactory;
|
||||
private readonly ILogger<SuscripcionService> _logger;
|
||||
|
||||
@@ -19,16 +20,29 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
||||
ISuscripcionRepository suscripcionRepository,
|
||||
ISuscriptorRepository suscriptorRepository,
|
||||
IPublicacionRepository publicacionRepository,
|
||||
IPromocionRepository promocionRepository,
|
||||
DbConnectionFactory connectionFactory,
|
||||
ILogger<SuscripcionService> logger)
|
||||
{
|
||||
_suscripcionRepository = suscripcionRepository;
|
||||
_suscriptorRepository = suscriptorRepository;
|
||||
_publicacionRepository = publicacionRepository;
|
||||
_promocionRepository = promocionRepository;
|
||||
_connectionFactory = connectionFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
private PromocionDto MapPromocionToDto(Promocion promo) => new PromocionDto
|
||||
{
|
||||
IdPromocion = promo.IdPromocion,
|
||||
Descripcion = promo.Descripcion,
|
||||
TipoPromocion = promo.TipoPromocion,
|
||||
Valor = promo.Valor,
|
||||
FechaInicio = promo.FechaInicio.ToString("yyyy-MM-dd"),
|
||||
FechaFin = promo.FechaFin?.ToString("yyyy-MM-dd"),
|
||||
Activa = promo.Activa
|
||||
};
|
||||
|
||||
private async Task<SuscripcionDto?> MapToDto(Suscripcion suscripcion)
|
||||
{
|
||||
if (suscripcion == null) return null;
|
||||
@@ -91,7 +105,7 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
||||
{
|
||||
var creada = await _suscripcionRepository.CreateAsync(nuevaSuscripcion, transaction);
|
||||
if (creada == null) throw new DataException("Error al crear la suscripción.");
|
||||
|
||||
|
||||
transaction.Commit();
|
||||
_logger.LogInformation("Suscripción ID {Id} creada por Usuario ID {UserId}.", creada.IdSuscripcion, idUsuario);
|
||||
return (await MapToDto(creada), null);
|
||||
@@ -108,7 +122,7 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
||||
{
|
||||
var existente = await _suscripcionRepository.GetByIdAsync(idSuscripcion);
|
||||
if (existente == null) return (false, "Suscripción no encontrada.");
|
||||
|
||||
|
||||
if (updateDto.FechaFin.HasValue && updateDto.FechaFin.Value < updateDto.FechaInicio)
|
||||
return (false, "La fecha de fin no puede ser anterior a la fecha de inicio.");
|
||||
|
||||
@@ -139,5 +153,71 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
||||
return (false, $"Error interno: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<PromocionDto>> ObtenerPromocionesAsignadas(int idSuscripcion)
|
||||
{
|
||||
var promociones = await _suscripcionRepository.GetPromocionesBySuscripcionIdAsync(idSuscripcion);
|
||||
return promociones.Select(MapPromocionToDto);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<PromocionDto>> ObtenerPromocionesDisponibles(int idSuscripcion)
|
||||
{
|
||||
var todasLasPromosActivas = await _promocionRepository.GetAllAsync(true);
|
||||
var promosAsignadas = await _suscripcionRepository.GetPromocionesBySuscripcionIdAsync(idSuscripcion);
|
||||
var idsAsignadas = promosAsignadas.Select(p => p.IdPromocion).ToHashSet();
|
||||
|
||||
return todasLasPromosActivas
|
||||
.Where(p => !idsAsignadas.Contains(p.IdPromocion))
|
||||
.Select(MapPromocionToDto);
|
||||
}
|
||||
|
||||
public async Task<(bool Exito, string? Error)> AsignarPromocion(int idSuscripcion, int idPromocion, int idUsuario)
|
||||
{
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
await (connection as System.Data.Common.DbConnection)!.OpenAsync();
|
||||
using var transaction = connection.BeginTransaction();
|
||||
try
|
||||
{
|
||||
// Validaciones
|
||||
if (await _suscripcionRepository.GetByIdAsync(idSuscripcion) == null) return (false, "Suscripción no encontrada.");
|
||||
if (await _promocionRepository.GetByIdAsync(idPromocion) == null) return (false, "Promoción no encontrada.");
|
||||
|
||||
await _suscripcionRepository.AsignarPromocionAsync(idSuscripcion, idPromocion, idUsuario, transaction);
|
||||
transaction.Commit();
|
||||
return (true, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Capturar error de Primary Key duplicada
|
||||
if (ex.Message.Contains("PRIMARY KEY constraint"))
|
||||
{
|
||||
return (false, "Esta promoción ya está asignada a la suscripción.");
|
||||
}
|
||||
try { transaction.Rollback(); } catch { }
|
||||
_logger.LogError(ex, "Error al asignar promoción {IdPromocion} a suscripción {IdSuscripcion}", idPromocion, idSuscripcion);
|
||||
return (false, "Error interno al asignar la promoción.");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<(bool Exito, string? Error)> QuitarPromocion(int idSuscripcion, int idPromocion)
|
||||
{
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
await (connection as System.Data.Common.DbConnection)!.OpenAsync();
|
||||
using var transaction = connection.BeginTransaction();
|
||||
try
|
||||
{
|
||||
var exito = await _suscripcionRepository.QuitarPromocionAsync(idSuscripcion, idPromocion, transaction);
|
||||
if (!exito) return (false, "La promoción no estaba asignada a esta suscripción.");
|
||||
|
||||
transaction.Commit();
|
||||
return (true, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
try { transaction.Rollback(); } catch { }
|
||||
_logger.LogError(ex, "Error al quitar promoción {IdPromocion} de suscripción {IdSuscripcion}", idPromocion, idSuscripcion);
|
||||
return (false, "Error interno al quitar la promoción.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,5 +11,13 @@
|
||||
"Audience": "GestionIntegralClient",
|
||||
"DurationInHours": 8
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
"AllowedHosts": "*",
|
||||
"MailSettings": {
|
||||
"SmtpHost": "smtp.yourprovider.com",
|
||||
"SmtpPort": 587,
|
||||
"SenderName": "Diario El Día - Suscripciones",
|
||||
"SenderEmail": "suscripciones@eldia.com",
|
||||
"SmtpUser": "your-smtp-username",
|
||||
"SmtpPass": "your-smtp-password"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user