Compare commits
2 Commits
b14c5de1b4
...
9e248efc84
| Author | SHA1 | Date | |
|---|---|---|---|
| 9e248efc84 | |||
| 84187a66df |
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -93,5 +93,46 @@ namespace GestionIntegral.Api.Controllers.Suscripciones
|
|||||||
}
|
}
|
||||||
return NoContent();
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -91,5 +91,55 @@ namespace GestionIntegral.Api.Data.Repositories.Suscripciones
|
|||||||
var rowsAffected = await transaction.Connection.ExecuteAsync(sql, new { IdLoteDebito = idLoteDebito, IdsFacturas = idsFacturas }, transaction);
|
var rowsAffected = await transaction.Connection.ExecuteAsync(sql, new { IdLoteDebito = idLoteDebito, IdsFacturas = idsFacturas }, transaction);
|
||||||
return rowsAffected == idsFacturas.Count();
|
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> UpdateEstadoAsync(int idFactura, string nuevoEstado, IDbTransaction transaction);
|
||||||
Task<bool> UpdateNumeroFacturaAsync(int idFactura, string numeroFactura, IDbTransaction transaction);
|
Task<bool> UpdateNumeroFacturaAsync(int idFactura, string numeroFactura, IDbTransaction transaction);
|
||||||
Task<bool> UpdateLoteDebitoAsync(IEnumerable<int> idsFacturas, int idLoteDebito, 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<Suscripcion?> CreateAsync(Suscripcion nuevaSuscripcion, IDbTransaction transaction);
|
||||||
Task<bool> UpdateAsync(Suscripcion suscripcionAActualizar, IDbTransaction transaction);
|
Task<bool> UpdateAsync(Suscripcion suscripcionAActualizar, IDbTransaction transaction);
|
||||||
Task<IEnumerable<Suscripcion>> GetAllActivasParaFacturacion(string periodo, 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -111,5 +111,42 @@ namespace GestionIntegral.Api.Data.Repositories.Suscripciones
|
|||||||
var rowsAffected = await transaction.Connection.ExecuteAsync(sqlUpdate, suscripcion, transaction);
|
var rowsAffected = await transaction.Connection.ExecuteAsync(sqlUpdate, suscripcion, transaction);
|
||||||
return rowsAffected == 1;
|
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>
|
<ItemGroup>
|
||||||
<PackageReference Include="AspNetCore.HealthChecks.SqlServer" Version="9.0.0" />
|
<PackageReference Include="AspNetCore.HealthChecks.SqlServer" Version="9.0.0" />
|
||||||
<PackageReference Include="Dapper" Version="2.1.66" />
|
<PackageReference Include="Dapper" Version="2.1.66" />
|
||||||
|
<PackageReference Include="MailKit" Version="4.13.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.4" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.4" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.3" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.3" />
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" />
|
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" />
|
||||||
|
|||||||
@@ -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,28 @@
|
|||||||
|
// Archivo: GestionIntegral.Api/Dtos/Suscripciones/CreatePromocionDto.cs
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace GestionIntegral.Api.Dtos.Suscripciones
|
||||||
|
{
|
||||||
|
public class CreatePromocionDto
|
||||||
|
{
|
||||||
|
[Required(ErrorMessage = "La descripción es obligatoria.")]
|
||||||
|
[StringLength(200)]
|
||||||
|
public string Descripcion { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "El tipo de promoción es obligatorio.")]
|
||||||
|
public string TipoPromocion { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "El valor es obligatorio.")]
|
||||||
|
[Range(0.01, 99999999.99, ErrorMessage = "El valor debe ser positivo.")]
|
||||||
|
public decimal Valor { get; set; }
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "La fecha de inicio es obligatoria.")]
|
||||||
|
public DateTime FechaInicio { get; set; }
|
||||||
|
|
||||||
|
public DateTime? FechaFin { get; set; }
|
||||||
|
|
||||||
|
public bool Activa { get; set; } = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 @@
|
|||||||
|
// Archivo: GestionIntegral.Api/Dtos/Suscripciones/UpdatePromocionDto.cs
|
||||||
|
|
||||||
|
namespace GestionIntegral.Api.Dtos.Suscripciones
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// DTO para actualizar una promoción. Hereda todas las propiedades y validaciones
|
||||||
|
/// de CreatePromocionDto, ya que por ahora son idénticas.
|
||||||
|
/// </summary>
|
||||||
|
public class UpdatePromocionDto : CreatePromocionDto
|
||||||
|
{
|
||||||
|
// No se necesitan propiedades adicionales por el momento.
|
||||||
|
// Si en el futuro se necesitara una validación diferente para la actualización,
|
||||||
|
// se podrían añadir o sobrescribir propiedades aquí.
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.Services.Anomalia;
|
||||||
using GestionIntegral.Api.Data.Repositories.Suscripciones;
|
using GestionIntegral.Api.Data.Repositories.Suscripciones;
|
||||||
using GestionIntegral.Api.Services.Suscripciones;
|
using GestionIntegral.Api.Services.Suscripciones;
|
||||||
|
using GestionIntegral.Api.Models.Comunicaciones;
|
||||||
|
using GestionIntegral.Api.Services.Comunicaciones;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
@@ -103,18 +105,25 @@ builder.Services.AddScoped<IQuestPdfGenerator, QuestPdfGenerator>();
|
|||||||
builder.Services.AddScoped<IAlertaService, AlertaService>();
|
builder.Services.AddScoped<IAlertaService, AlertaService>();
|
||||||
|
|
||||||
// --- Suscripciones ---
|
// --- Suscripciones ---
|
||||||
// Repositorios
|
|
||||||
builder.Services.AddScoped<IFormaPagoRepository, FormaPagoRepository>();
|
builder.Services.AddScoped<IFormaPagoRepository, FormaPagoRepository>();
|
||||||
builder.Services.AddScoped<ISuscriptorRepository, SuscriptorRepository>();
|
builder.Services.AddScoped<ISuscriptorRepository, SuscriptorRepository>();
|
||||||
builder.Services.AddScoped<ISuscripcionRepository, SuscripcionRepository>();
|
builder.Services.AddScoped<ISuscripcionRepository, SuscripcionRepository>();
|
||||||
builder.Services.AddScoped<IFacturaRepository, FacturaRepository>();
|
builder.Services.AddScoped<IFacturaRepository, FacturaRepository>();
|
||||||
builder.Services.AddScoped<ILoteDebitoRepository, LoteDebitoRepository>();
|
builder.Services.AddScoped<ILoteDebitoRepository, LoteDebitoRepository>();
|
||||||
builder.Services.AddScoped<IPagoRepository, PagoRepository>();
|
builder.Services.AddScoped<IPagoRepository, PagoRepository>();
|
||||||
|
builder.Services.AddScoped<IPromocionRepository, PromocionRepository>();
|
||||||
|
|
||||||
// Servicios
|
|
||||||
builder.Services.AddScoped<IFormaPagoService, FormaPagoService>();
|
builder.Services.AddScoped<IFormaPagoService, FormaPagoService>();
|
||||||
builder.Services.AddScoped<ISuscriptorService, SuscriptorService>();
|
builder.Services.AddScoped<ISuscriptorService, SuscriptorService>();
|
||||||
builder.Services.AddScoped<ISuscripcionService, SuscripcionService>();
|
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 ---
|
// --- SERVICIO DE HEALTH CHECKS ---
|
||||||
// Añadimos una comprobación específica para SQL Server.
|
// 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?> ObtenerPorId(int idSuscripcion);
|
||||||
Task<(SuscripcionDto? Suscripcion, string? Error)> Crear(CreateSuscripcionDto createDto, int idUsuario);
|
Task<(SuscripcionDto? Suscripcion, string? Error)> Crear(CreateSuscripcionDto createDto, int idUsuario);
|
||||||
Task<(bool Exito, string? Error)> Actualizar(int idSuscripcion, UpdateSuscripcionDto updateDto, 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 ISuscripcionRepository _suscripcionRepository;
|
||||||
private readonly ISuscriptorRepository _suscriptorRepository;
|
private readonly ISuscriptorRepository _suscriptorRepository;
|
||||||
private readonly IPublicacionRepository _publicacionRepository;
|
private readonly IPublicacionRepository _publicacionRepository;
|
||||||
|
private readonly IPromocionRepository _promocionRepository;
|
||||||
private readonly DbConnectionFactory _connectionFactory;
|
private readonly DbConnectionFactory _connectionFactory;
|
||||||
private readonly ILogger<SuscripcionService> _logger;
|
private readonly ILogger<SuscripcionService> _logger;
|
||||||
|
|
||||||
@@ -19,16 +20,29 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
|||||||
ISuscripcionRepository suscripcionRepository,
|
ISuscripcionRepository suscripcionRepository,
|
||||||
ISuscriptorRepository suscriptorRepository,
|
ISuscriptorRepository suscriptorRepository,
|
||||||
IPublicacionRepository publicacionRepository,
|
IPublicacionRepository publicacionRepository,
|
||||||
|
IPromocionRepository promocionRepository,
|
||||||
DbConnectionFactory connectionFactory,
|
DbConnectionFactory connectionFactory,
|
||||||
ILogger<SuscripcionService> logger)
|
ILogger<SuscripcionService> logger)
|
||||||
{
|
{
|
||||||
_suscripcionRepository = suscripcionRepository;
|
_suscripcionRepository = suscripcionRepository;
|
||||||
_suscriptorRepository = suscriptorRepository;
|
_suscriptorRepository = suscriptorRepository;
|
||||||
_publicacionRepository = publicacionRepository;
|
_publicacionRepository = publicacionRepository;
|
||||||
|
_promocionRepository = promocionRepository;
|
||||||
_connectionFactory = connectionFactory;
|
_connectionFactory = connectionFactory;
|
||||||
_logger = logger;
|
_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)
|
private async Task<SuscripcionDto?> MapToDto(Suscripcion suscripcion)
|
||||||
{
|
{
|
||||||
if (suscripcion == null) return null;
|
if (suscripcion == null) return null;
|
||||||
@@ -139,5 +153,71 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
|||||||
return (false, $"Error interno: {ex.Message}");
|
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",
|
"Audience": "GestionIntegralClient",
|
||||||
"DurationInHours": 8
|
"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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
|
import { Modal, Box, Typography, Button, CircularProgress, Alert, Select, MenuItem, FormControl, InputLabel, List, ListItem, ListItemText, IconButton, Divider } from '@mui/material';
|
||||||
|
import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline';
|
||||||
|
import DeleteIcon from '@mui/icons-material/Delete';
|
||||||
|
import type { SuscripcionDto } from '../../../models/dtos/Suscripciones/SuscripcionDto';
|
||||||
|
import type { PromocionDto } from '../../../models/dtos/Suscripciones/PromocionDto';
|
||||||
|
import suscripcionService from '../../../services/Suscripciones/suscripcionService';
|
||||||
|
|
||||||
|
const modalStyle = { /* ... */ };
|
||||||
|
|
||||||
|
interface GestionarPromocionesSuscripcionModalProps {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
suscripcion: SuscripcionDto | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GestionarPromocionesSuscripcionModal: React.FC<GestionarPromocionesSuscripcionModalProps> = ({ open, onClose, suscripcion }) => {
|
||||||
|
const [asignadas, setAsignadas] = useState<PromocionDto[]>([]);
|
||||||
|
const [disponibles, setDisponibles] = useState<PromocionDto[]>([]);
|
||||||
|
const [selectedPromo, setSelectedPromo] = useState<number | string>('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const cargarDatos = useCallback(async () => {
|
||||||
|
if (!suscripcion) return;
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const [asignadasData, disponiblesData] = await Promise.all([
|
||||||
|
suscripcionService.getPromocionesAsignadas(suscripcion.idSuscripcion),
|
||||||
|
suscripcionService.getPromocionesDisponibles(suscripcion.idSuscripcion)
|
||||||
|
]);
|
||||||
|
setAsignadas(asignadasData);
|
||||||
|
setDisponibles(disponiblesData);
|
||||||
|
} catch (err) {
|
||||||
|
setError("Error al cargar las promociones.");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [suscripcion]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
cargarDatos();
|
||||||
|
}
|
||||||
|
}, [open, cargarDatos]);
|
||||||
|
|
||||||
|
const handleAsignar = async () => {
|
||||||
|
if (!suscripcion || !selectedPromo) return;
|
||||||
|
try {
|
||||||
|
await suscripcionService.asignarPromocion(suscripcion.idSuscripcion, Number(selectedPromo));
|
||||||
|
setSelectedPromo('');
|
||||||
|
cargarDatos();
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(err.response?.data?.message || "Error al asignar la promoción.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleQuitar = async (idPromocion: number) => {
|
||||||
|
if (!suscripcion) return;
|
||||||
|
try {
|
||||||
|
await suscripcionService.quitarPromocion(suscripcion.idSuscripcion, idPromocion);
|
||||||
|
cargarDatos();
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(err.response?.data?.message || "Error al quitar la promoción.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!suscripcion) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal open={open} onClose={onClose}>
|
||||||
|
<Box sx={modalStyle}>
|
||||||
|
<Typography variant="h6">Gestionar Promociones</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||||
|
Suscripción a: {suscripcion.nombrePublicacion}
|
||||||
|
</Typography>
|
||||||
|
{error && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
|
||||||
|
{loading ? <CircularProgress /> : (
|
||||||
|
<>
|
||||||
|
<Typography sx={{ mt: 2 }}>Promociones Asignadas</Typography>
|
||||||
|
<List dense>
|
||||||
|
{asignadas.length === 0 && <ListItem><ListItemText primary="No hay promociones asignadas." /></ListItem>}
|
||||||
|
{asignadas.map(p => (
|
||||||
|
<ListItem key={p.idPromocion} secondaryAction={<IconButton edge="end" onClick={() => handleQuitar(p.idPromocion)}><DeleteIcon /></IconButton>}>
|
||||||
|
<ListItemText primary={p.descripcion} secondary={`Tipo: ${p.tipoPromocion}, Valor: ${p.valor}`} />
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
<Divider sx={{ my: 2 }} />
|
||||||
|
<Typography>Asignar Nueva Promoción</Typography>
|
||||||
|
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center', mt: 1 }}>
|
||||||
|
<FormControl fullWidth size="small">
|
||||||
|
<InputLabel>Promociones Disponibles</InputLabel>
|
||||||
|
<Select value={selectedPromo} label="Promociones Disponibles" onChange={(e) => setSelectedPromo(e.target.value)}>
|
||||||
|
{disponibles.map(p => <MenuItem key={p.idPromocion} value={p.idPromocion}>{p.descripcion}</MenuItem>)}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<Button variant="contained" onClick={handleAsignar} disabled={!selectedPromo}><AddCircleOutlineIcon /></Button>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Box sx={{ mt: 3, textAlign: 'right' }}>
|
||||||
|
<Button onClick={onClose}>Cerrar</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GestionarPromocionesSuscripcionModal;
|
||||||
141
Frontend/src/components/Modals/Suscripciones/PagoManualModal.tsx
Normal file
141
Frontend/src/components/Modals/Suscripciones/PagoManualModal.tsx
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
// Archivo: Frontend/src/components/Modals/Suscripciones/PagoManualModal.tsx
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Modal, Box, Typography, TextField, Button, CircularProgress, Alert, FormControl, InputLabel, Select, MenuItem, type SelectChangeEvent, InputAdornment } from '@mui/material';
|
||||||
|
import type { FacturaDto } from '../../../models/dtos/Suscripciones/FacturaDto';
|
||||||
|
import type { CreatePagoDto } from '../../../models/dtos/Suscripciones/CreatePagoDto';
|
||||||
|
import type { FormaPagoDto } from '../../../models/dtos/Suscripciones/FormaPagoDto';
|
||||||
|
import formaPagoService from '../../../services/Suscripciones/formaPagoService';
|
||||||
|
|
||||||
|
const modalStyle = {
|
||||||
|
position: 'absolute' as 'absolute',
|
||||||
|
top: '50%',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
width: { xs: '95%', sm: '80%', md: '500px' },
|
||||||
|
bgcolor: 'background.paper',
|
||||||
|
border: '2px solid #000',
|
||||||
|
boxShadow: 24,
|
||||||
|
p: 4,
|
||||||
|
maxHeight: '90vh',
|
||||||
|
overflowY: 'auto'
|
||||||
|
};
|
||||||
|
|
||||||
|
interface PagoManualModalProps {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onSubmit: (data: CreatePagoDto) => Promise<void>;
|
||||||
|
factura: FacturaDto | null;
|
||||||
|
errorMessage?: string | null;
|
||||||
|
clearErrorMessage: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PagoManualModal: React.FC<PagoManualModalProps> = ({ open, onClose, onSubmit, factura, errorMessage, clearErrorMessage }) => {
|
||||||
|
const [formData, setFormData] = useState<Partial<CreatePagoDto>>({});
|
||||||
|
const [formasDePago, setFormasDePago] = useState<FormaPagoDto[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [loadingFormasPago, setLoadingFormasPago] = useState(false);
|
||||||
|
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchFormasDePago = async () => {
|
||||||
|
setLoadingFormasPago(true);
|
||||||
|
try {
|
||||||
|
const data = await formaPagoService.getAllFormasDePago();
|
||||||
|
setFormasDePago(data.filter(fp => !fp.requiereCBU));
|
||||||
|
} catch (error) {
|
||||||
|
setLocalErrors(prev => ({ ...prev, formasDePago: 'Error al cargar formas de pago.' }));
|
||||||
|
} finally {
|
||||||
|
setLoadingFormasPago(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (open && factura) {
|
||||||
|
fetchFormasDePago();
|
||||||
|
setFormData({
|
||||||
|
idFactura: factura.idFactura,
|
||||||
|
monto: factura.importeFinal,
|
||||||
|
fechaPago: new Date().toISOString().split('T')[0]
|
||||||
|
});
|
||||||
|
setLocalErrors({});
|
||||||
|
}
|
||||||
|
}, [open, factura]);
|
||||||
|
|
||||||
|
const validate = (): boolean => {
|
||||||
|
const errors: { [key: string]: string | null } = {};
|
||||||
|
if (!formData.idFormaPago) errors.idFormaPago = "Seleccione una forma de pago.";
|
||||||
|
if (!formData.monto || formData.monto <= 0) errors.monto = "El monto debe ser mayor a cero.";
|
||||||
|
if (!formData.fechaPago) errors.fechaPago = "La fecha de pago es obligatoria.";
|
||||||
|
setLocalErrors(errors);
|
||||||
|
return Object.keys(errors).length === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
const finalValue = name === 'monto' && value !== '' ? parseFloat(value) : value;
|
||||||
|
setFormData(prev => ({ ...prev, [name]: finalValue }));
|
||||||
|
if (localErrors[name]) setLocalErrors(prev => ({ ...prev, [name]: null }));
|
||||||
|
if (errorMessage) clearErrorMessage();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectChange = (e: SelectChangeEvent<any>) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setFormData(prev => ({ ...prev, [name]: value }));
|
||||||
|
if (localErrors[name]) setLocalErrors(prev => ({ ...prev, [name]: null }));
|
||||||
|
if (errorMessage) clearErrorMessage();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
clearErrorMessage();
|
||||||
|
if (!validate()) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
let success = false;
|
||||||
|
try {
|
||||||
|
await onSubmit(formData as CreatePagoDto);
|
||||||
|
success = true;
|
||||||
|
} catch (error) {
|
||||||
|
success = false;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
if (success) onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!factura) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal open={open} onClose={onClose}>
|
||||||
|
<Box sx={modalStyle}>
|
||||||
|
<Typography variant="h6">Registrar Pago Manual</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||||
|
Factura #{factura.idFactura} para {factura.nombreSuscriptor}
|
||||||
|
</Typography>
|
||||||
|
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 2 }}>
|
||||||
|
<TextField name="fechaPago" label="Fecha de Pago" type="date" value={formData.fechaPago || ''} onChange={handleInputChange} required fullWidth margin="dense" InputLabelProps={{ shrink: true }} error={!!localErrors.fechaPago} helperText={localErrors.fechaPago} />
|
||||||
|
<FormControl fullWidth margin="dense" error={!!localErrors.idFormaPago}>
|
||||||
|
<InputLabel id="forma-pago-label" required>Forma de Pago</InputLabel>
|
||||||
|
<Select name="idFormaPago" labelId="forma-pago-label" value={formData.idFormaPago || ''} onChange={handleSelectChange} label="Forma de Pago" disabled={loadingFormasPago}>
|
||||||
|
{formasDePago.map(fp => <MenuItem key={fp.idFormaPago} value={fp.idFormaPago}>{fp.nombre}</MenuItem>)}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<TextField name="monto" label="Monto Pagado" type="number" value={formData.monto || ''} onChange={handleInputChange} required fullWidth margin="dense" error={!!localErrors.monto} helperText={localErrors.monto} InputProps={{ startAdornment: <InputAdornment position="start">$</InputAdornment> }} inputProps={{ step: "0.01" }} />
|
||||||
|
<TextField name="referencia" label="Referencia (Opcional)" value={formData.referencia || ''} onChange={handleInputChange} fullWidth margin="dense" />
|
||||||
|
<TextField name="observaciones" label="Observaciones (Opcional)" value={formData.observaciones || ''} onChange={handleInputChange} fullWidth margin="dense" multiline rows={2} />
|
||||||
|
|
||||||
|
{errorMessage && <Alert severity="error" sx={{ mt: 2 }}>{errorMessage}</Alert>}
|
||||||
|
|
||||||
|
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
|
||||||
|
<Button onClick={onClose} color="secondary" disabled={loading}>Cancelar</Button>
|
||||||
|
<Button type="submit" variant="contained" disabled={loading || loadingFormasPago}>
|
||||||
|
{loading ? <CircularProgress size={24} /> : 'Registrar Pago'}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PagoManualModal;
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Modal, Box, Typography, TextField, Button, CircularProgress, Alert,
|
||||||
|
FormControl, InputLabel, Select, MenuItem, FormControlLabel, Checkbox,
|
||||||
|
type SelectChangeEvent, InputAdornment } from '@mui/material';
|
||||||
|
import type { PromocionDto } from '../../../models/dtos/Suscripciones/PromocionDto';
|
||||||
|
import type { CreatePromocionDto, UpdatePromocionDto } from '../../../models/dtos/Suscripciones/CreatePromocionDto';
|
||||||
|
|
||||||
|
const modalStyle = {
|
||||||
|
position: 'absolute' as 'absolute',
|
||||||
|
top: '50%',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
width: { xs: '95%', sm: '80%', md: '600px' },
|
||||||
|
bgcolor: 'background.paper',
|
||||||
|
border: '2px solid #000',
|
||||||
|
boxShadow: 24,
|
||||||
|
p: 4,
|
||||||
|
maxHeight: '90vh',
|
||||||
|
overflowY: 'auto'
|
||||||
|
};
|
||||||
|
|
||||||
|
const tiposPromocion = [
|
||||||
|
{ value: 'Porcentaje', label: 'Descuento Porcentual (%)' },
|
||||||
|
{ value: 'MontoFijo', label: 'Descuento de Monto Fijo ($)' },
|
||||||
|
// { value: 'BonificacionDias', label: 'Bonificación de Días' }, // Descomentar para futuras implementaciones
|
||||||
|
];
|
||||||
|
|
||||||
|
interface PromocionFormModalProps {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onSubmit: (data: CreatePromocionDto | UpdatePromocionDto, id?: number) => Promise<void>;
|
||||||
|
initialData?: PromocionDto | null;
|
||||||
|
errorMessage?: string | null;
|
||||||
|
clearErrorMessage: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PromocionFormModal: React.FC<PromocionFormModalProps> = ({ open, onClose, onSubmit, initialData, errorMessage, clearErrorMessage }) => {
|
||||||
|
const [formData, setFormData] = useState<Partial<CreatePromocionDto>>({});
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
|
||||||
|
|
||||||
|
const isEditing = Boolean(initialData);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
setFormData(initialData || {
|
||||||
|
descripcion: '',
|
||||||
|
tipoPromocion: 'Porcentaje',
|
||||||
|
valor: 0,
|
||||||
|
fechaInicio: new Date().toISOString().split('T')[0],
|
||||||
|
activa: true
|
||||||
|
});
|
||||||
|
setLocalErrors({});
|
||||||
|
}
|
||||||
|
}, [open, initialData]);
|
||||||
|
|
||||||
|
const validate = (): boolean => {
|
||||||
|
const errors: { [key: string]: string | null } = {};
|
||||||
|
if (!formData.descripcion?.trim()) errors.descripcion = 'La descripción es obligatoria.';
|
||||||
|
if (!formData.tipoPromocion) errors.tipoPromocion = 'El tipo de promoción es obligatorio.';
|
||||||
|
if (!formData.valor || formData.valor <= 0) errors.valor = 'El valor debe ser mayor a cero.';
|
||||||
|
if (formData.tipoPromocion === 'Porcentaje' && (formData.valor ?? 0) > 100) {
|
||||||
|
errors.valor = 'El valor para porcentaje no puede ser mayor a 100.';
|
||||||
|
}
|
||||||
|
if (!formData.fechaInicio) errors.fechaInicio = 'La fecha de inicio es obligatoria.';
|
||||||
|
if (formData.fechaFin && formData.fechaInicio && new Date(formData.fechaFin) < new Date(formData.fechaInicio)) {
|
||||||
|
errors.fechaFin = 'La fecha de fin no puede ser anterior a la de inicio.';
|
||||||
|
}
|
||||||
|
setLocalErrors(errors);
|
||||||
|
return Object.keys(errors).length === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const { name, value, type, checked } = e.target;
|
||||||
|
const finalValue = type === 'checkbox' ? checked : (type === 'number' ? parseFloat(value) : value);
|
||||||
|
setFormData(prev => ({ ...prev, [name]: finalValue }));
|
||||||
|
if (localErrors[name]) setLocalErrors(prev => ({ ...prev, [name]: null }));
|
||||||
|
if (errorMessage) clearErrorMessage();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectChange = (e: SelectChangeEvent<any>) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setFormData(prev => ({ ...prev, [name]: value }));
|
||||||
|
if (localErrors[name]) setLocalErrors(prev => ({ ...prev, [name]: null }));
|
||||||
|
if (errorMessage) clearErrorMessage();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
clearErrorMessage();
|
||||||
|
if (!validate()) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
let success = false;
|
||||||
|
try {
|
||||||
|
const dataToSubmit = {
|
||||||
|
...formData,
|
||||||
|
fechaFin: formData.fechaFin || null
|
||||||
|
} as CreatePromocionDto | UpdatePromocionDto;
|
||||||
|
|
||||||
|
await onSubmit(dataToSubmit, initialData?.idPromocion);
|
||||||
|
success = true;
|
||||||
|
} catch (error) {
|
||||||
|
success = false;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
if (success) onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal open={open} onClose={onClose}>
|
||||||
|
<Box sx={modalStyle}>
|
||||||
|
<Typography variant="h6" component="h2">{isEditing ? 'Editar Promoción' : 'Nueva Promoción'}</Typography>
|
||||||
|
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 2 }}>
|
||||||
|
<TextField name="descripcion" label="Descripción" value={formData.descripcion || ''} onChange={handleInputChange} required fullWidth margin="dense" error={!!localErrors.descripcion} helperText={localErrors.descripcion} disabled={loading} autoFocus />
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap' }}>
|
||||||
|
<FormControl fullWidth margin="dense" sx={{flex: 2}} error={!!localErrors.tipoPromocion}>
|
||||||
|
<InputLabel id="tipo-promo-label" required>Tipo</InputLabel>
|
||||||
|
<Select name="tipoPromocion" labelId="tipo-promo-label" value={formData.tipoPromocion || ''} onChange={handleSelectChange} label="Tipo" disabled={loading}>
|
||||||
|
{tiposPromocion.map(t => <MenuItem key={t.value} value={t.value}>{t.label}</MenuItem>)}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<TextField name="valor" label="Valor" type="number" value={formData.valor || ''} onChange={handleInputChange} required fullWidth margin="dense" sx={{flex: 1}} error={!!localErrors.valor} helperText={localErrors.valor} disabled={loading}
|
||||||
|
InputProps={{ startAdornment: <InputAdornment position="start">{formData.tipoPromocion === 'Porcentaje' ? '%' : '$'}</InputAdornment> }}
|
||||||
|
inputProps={{ step: "0.01" }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', gap: 2, mt: 1 }}>
|
||||||
|
<TextField name="fechaInicio" label="Fecha Inicio" type="date" value={formData.fechaInicio || ''} onChange={handleInputChange} required fullWidth margin="dense" InputLabelProps={{ shrink: true }} error={!!localErrors.fechaInicio} helperText={localErrors.fechaInicio} disabled={loading} />
|
||||||
|
<TextField name="fechaFin" label="Fecha Fin (Opcional)" type="date" value={formData.fechaFin || ''} onChange={handleInputChange} fullWidth margin="dense" InputLabelProps={{ shrink: true }} error={!!localErrors.fechaFin} helperText={localErrors.fechaFin} disabled={loading} />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<FormControlLabel control={<Checkbox name="activa" checked={formData.activa ?? true} onChange={handleInputChange} disabled={loading}/>} label="Promoción Activa" sx={{mt: 1}} />
|
||||||
|
|
||||||
|
{errorMessage && <Alert severity="error" sx={{ mt: 2 }}>{errorMessage}</Alert>}
|
||||||
|
|
||||||
|
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
|
||||||
|
<Button onClick={onClose} color="secondary" disabled={loading}>Cancelar</Button>
|
||||||
|
<Button type="submit" variant="contained" disabled={loading}>
|
||||||
|
{loading ? <CircularProgress size={24} /> : 'Guardar'}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PromocionFormModal;
|
||||||
8
Frontend/src/models/dtos/Suscripciones/CreatePagoDto.ts
Normal file
8
Frontend/src/models/dtos/Suscripciones/CreatePagoDto.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export interface CreatePagoDto {
|
||||||
|
idFactura: number;
|
||||||
|
fechaPago: string; // "yyyy-MM-dd"
|
||||||
|
idFormaPago: number;
|
||||||
|
monto: number;
|
||||||
|
referencia?: string | null;
|
||||||
|
observaciones?: string | null;
|
||||||
|
}
|
||||||
10
Frontend/src/models/dtos/Suscripciones/CreatePromocionDto.ts
Normal file
10
Frontend/src/models/dtos/Suscripciones/CreatePromocionDto.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
export interface CreatePromocionDto {
|
||||||
|
descripcion: string;
|
||||||
|
tipoPromocion: 'Porcentaje' | 'MontoFijo' | 'BonificacionDias';
|
||||||
|
valor: number;
|
||||||
|
fechaInicio: string; // "yyyy-MM-dd"
|
||||||
|
fechaFin?: string | null;
|
||||||
|
activa: boolean;
|
||||||
|
}
|
||||||
|
// UpdatePromocionDto es igual
|
||||||
|
export type UpdatePromocionDto = CreatePromocionDto;
|
||||||
14
Frontend/src/models/dtos/Suscripciones/FacturaDto.ts
Normal file
14
Frontend/src/models/dtos/Suscripciones/FacturaDto.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
export interface FacturaDto {
|
||||||
|
idFactura: number;
|
||||||
|
idSuscripcion: number;
|
||||||
|
periodo: string; // "YYYY-MM"
|
||||||
|
fechaEmision: string; // "yyyy-MM-dd"
|
||||||
|
fechaVencimiento: string; // "yyyy-MM-dd"
|
||||||
|
importeFinal: number;
|
||||||
|
estado: string;
|
||||||
|
numeroFactura?: string | null;
|
||||||
|
|
||||||
|
// Datos enriquecidos para la UI
|
||||||
|
nombreSuscriptor: string;
|
||||||
|
nombrePublicacion: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export interface GenerarFacturacionResponseDto {
|
||||||
|
message: string;
|
||||||
|
facturasGeneradas: number;
|
||||||
|
}
|
||||||
13
Frontend/src/models/dtos/Suscripciones/PagoDto.ts
Normal file
13
Frontend/src/models/dtos/Suscripciones/PagoDto.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
export interface PagoDto {
|
||||||
|
idPago: number;
|
||||||
|
idFactura: number;
|
||||||
|
fechaPago: string; // "yyyy-MM-dd"
|
||||||
|
idFormaPago: number;
|
||||||
|
nombreFormaPago: string;
|
||||||
|
monto: number;
|
||||||
|
estado: string;
|
||||||
|
referencia?: string | null;
|
||||||
|
observaciones?: string | null;
|
||||||
|
idUsuarioRegistro: number;
|
||||||
|
nombreUsuarioRegistro: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export interface ProcesamientoLoteResponseDto {
|
||||||
|
totalRegistrosLeidos: number;
|
||||||
|
pagosAprobados: number;
|
||||||
|
pagosRechazados: number;
|
||||||
|
errores: string[];
|
||||||
|
mensajeResumen: string;
|
||||||
|
}
|
||||||
9
Frontend/src/models/dtos/Suscripciones/PromocionDto.ts
Normal file
9
Frontend/src/models/dtos/Suscripciones/PromocionDto.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export interface PromocionDto {
|
||||||
|
idPromocion: number;
|
||||||
|
descripcion: string;
|
||||||
|
tipoPromocion: 'Porcentaje' | 'MontoFijo' | 'BonificacionDias';
|
||||||
|
valor: number;
|
||||||
|
fechaInicio: string; // "yyyy-MM-dd"
|
||||||
|
fechaFin?: string | null;
|
||||||
|
activa: boolean;
|
||||||
|
}
|
||||||
321
Frontend/src/pages/Suscripciones/FacturacionPage.tsx
Normal file
321
Frontend/src/pages/Suscripciones/FacturacionPage.tsx
Normal file
@@ -0,0 +1,321 @@
|
|||||||
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
|
import { Box, Typography, Button, Paper, CircularProgress, Alert, Select, MenuItem, FormControl, InputLabel, Table, TableContainer, TableHead, TableRow, TableCell, TableBody, Chip, IconButton, Menu, ListItemIcon, ListItemText } from '@mui/material';
|
||||||
|
import PlayCircleOutlineIcon from '@mui/icons-material/PlayCircleOutline';
|
||||||
|
import DownloadIcon from '@mui/icons-material/Download';
|
||||||
|
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||||
|
import PaymentIcon from '@mui/icons-material/Payment';
|
||||||
|
import EmailIcon from '@mui/icons-material/Email';
|
||||||
|
import UploadFileIcon from '@mui/icons-material/UploadFile';
|
||||||
|
import { styled } from '@mui/material/styles';
|
||||||
|
import facturacionService from '../../services/Suscripciones/facturacionService';
|
||||||
|
import { usePermissions } from '../../hooks/usePermissions';
|
||||||
|
import axios from 'axios';
|
||||||
|
import type { FacturaDto } from '../../models/dtos/Suscripciones/FacturaDto';
|
||||||
|
import PagoManualModal from '../../components/Modals/Suscripciones/PagoManualModal';
|
||||||
|
import type { CreatePagoDto } from '../../models/dtos/Suscripciones/CreatePagoDto';
|
||||||
|
|
||||||
|
const anios = Array.from({ length: 10 }, (_, i) => new Date().getFullYear() - i);
|
||||||
|
const meses = [
|
||||||
|
{ value: 1, label: 'Enero' }, { value: 2, label: 'Febrero' }, { value: 3, label: 'Marzo' },
|
||||||
|
{ value: 4, label: 'Abril' }, { value: 5, label: 'Mayo' }, { value: 6, label: 'Junio' },
|
||||||
|
{ value: 7, label: 'Julio' }, { value: 8, label: 'Agosto' }, { value: 9, label: 'Septiembre' },
|
||||||
|
{ value: 10, label: 'Octubre' }, { value: 11, label: 'Noviembre' }, { value: 12, label: 'Diciembre' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const VisuallyHiddenInput = styled('input')({
|
||||||
|
clip: 'rect(0 0 0 0)', clipPath: 'inset(50%)', height: 1, overflow: 'hidden',
|
||||||
|
position: 'absolute', bottom: 0, left: 0, whiteSpace: 'nowrap', width: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const FacturacionPage: React.FC = () => {
|
||||||
|
const [selectedAnio, setSelectedAnio] = useState<number>(new Date().getFullYear());
|
||||||
|
const [selectedMes, setSelectedMes] = useState<number>(new Date().getMonth() + 1);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [loadingArchivo, setLoadingArchivo] = useState(false);
|
||||||
|
const [loadingProceso, setLoadingProceso] = useState(false);
|
||||||
|
const [apiMessage, setApiMessage] = useState<string | null>(null);
|
||||||
|
const [apiError, setApiError] = useState<string | null>(null);
|
||||||
|
const [facturas, setFacturas] = useState<FacturaDto[]>([]);
|
||||||
|
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||||
|
const puedeGenerarFacturacion = isSuperAdmin || tienePermiso("SU006");
|
||||||
|
const puedeGenerarArchivo = isSuperAdmin || tienePermiso("SU007");
|
||||||
|
const puedeRegistrarPago = isSuperAdmin || tienePermiso("SU008");
|
||||||
|
const puedeEnviarEmail = isSuperAdmin || tienePermiso("SU009");
|
||||||
|
const [pagoModalOpen, setPagoModalOpen] = useState(false);
|
||||||
|
const [selectedFactura, setSelectedFactura] = useState<FacturaDto | null>(null);
|
||||||
|
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||||
|
const [archivoSeleccionado, setArchivoSeleccionado] = useState<File | null>(null);
|
||||||
|
|
||||||
|
const cargarFacturasDelPeriodo = useCallback(async () => {
|
||||||
|
if (!puedeGenerarFacturacion) return;
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const data = await facturacionService.getFacturasPorPeriodo(selectedAnio, selectedMes);
|
||||||
|
setFacturas(data);
|
||||||
|
} catch (err) {
|
||||||
|
setFacturas([]);
|
||||||
|
console.error(err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [selectedAnio, selectedMes, puedeGenerarFacturacion]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
cargarFacturasDelPeriodo();
|
||||||
|
}, [cargarFacturasDelPeriodo]);
|
||||||
|
|
||||||
|
const handleGenerarFacturacion = async () => {
|
||||||
|
if (!window.confirm(`¿Está seguro de que desea generar la facturación para ${meses.find(m => m.value === selectedMes)?.label} de ${selectedAnio}? Este proceso creará registros de cobro para todas las suscripciones activas.`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLoading(true);
|
||||||
|
setApiMessage(null);
|
||||||
|
setApiError(null);
|
||||||
|
try {
|
||||||
|
const response = await facturacionService.generarFacturacionMensual(selectedAnio, selectedMes);
|
||||||
|
setApiMessage(`${response.message}. Se generaron ${response.facturasGeneradas} facturas.`);
|
||||||
|
await cargarFacturasDelPeriodo();
|
||||||
|
} catch (err: any) {
|
||||||
|
const message = axios.isAxiosError(err) && err.response?.data?.message
|
||||||
|
? err.response.data.message
|
||||||
|
: 'Ocurrió un error al generar la facturación.';
|
||||||
|
setApiError(message);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGenerarArchivo = async () => {
|
||||||
|
if (!window.confirm(`Se generará el archivo de débito para las facturas del período ${meses.find(m => m.value === selectedMes)?.label}/${selectedAnio} que estén en estado 'Pendiente de Cobro'. ¿Continuar?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLoadingArchivo(true);
|
||||||
|
setApiMessage(null);
|
||||||
|
setApiError(null);
|
||||||
|
try {
|
||||||
|
const { fileContent, fileName } = await facturacionService.generarArchivoDebito(selectedAnio, selectedMes);
|
||||||
|
const url = window.URL.createObjectURL(new Blob([fileContent]));
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.setAttribute('download', fileName);
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
link.parentNode?.removeChild(link);
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
setApiMessage(`Archivo "${fileName}" generado y descargado exitosamente.`);
|
||||||
|
cargarFacturasDelPeriodo();
|
||||||
|
} catch (err: any) {
|
||||||
|
let message = 'Ocurrió un error al generar el archivo.';
|
||||||
|
if (axios.isAxiosError(err) && err.response) {
|
||||||
|
const errorText = await err.response.data.text();
|
||||||
|
try {
|
||||||
|
const errorJson = JSON.parse(errorText);
|
||||||
|
message = errorJson.message || message;
|
||||||
|
} catch { message = errorText || message; }
|
||||||
|
}
|
||||||
|
setApiError(message);
|
||||||
|
} finally {
|
||||||
|
setLoadingArchivo(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, factura: FacturaDto) => {
|
||||||
|
setAnchorEl(event.currentTarget);
|
||||||
|
setSelectedFactura(factura);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMenuClose = () => {
|
||||||
|
setAnchorEl(null);
|
||||||
|
setSelectedFactura(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenPagoModal = () => {
|
||||||
|
setPagoModalOpen(true);
|
||||||
|
handleMenuClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClosePagoModal = () => {
|
||||||
|
setPagoModalOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmitPagoModal = async (data: CreatePagoDto) => {
|
||||||
|
setApiError(null);
|
||||||
|
try {
|
||||||
|
await facturacionService.registrarPagoManual(data);
|
||||||
|
setApiMessage(`Pago para la factura #${data.idFactura} registrado exitosamente.`);
|
||||||
|
cargarFacturasDelPeriodo();
|
||||||
|
} catch (err: any) {
|
||||||
|
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al registrar el pago.';
|
||||||
|
setApiError(message);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSendEmail = async (idFactura: number) => {
|
||||||
|
if (!window.confirm(`¿Está seguro de enviar la notificación de la factura #${idFactura} por email?`)) return;
|
||||||
|
setApiMessage(null);
|
||||||
|
setApiError(null);
|
||||||
|
try {
|
||||||
|
await facturacionService.enviarFacturaPorEmail(idFactura);
|
||||||
|
setApiMessage(`El email para la factura #${idFactura} ha sido enviado a la cola de procesamiento.`);
|
||||||
|
} catch (err: any) {
|
||||||
|
setApiError(err.response?.data?.message || 'Error al intentar enviar el email.');
|
||||||
|
} finally {
|
||||||
|
handleMenuClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (event.target.files && event.target.files.length > 0) {
|
||||||
|
setArchivoSeleccionado(event.target.files[0]);
|
||||||
|
setApiMessage(null);
|
||||||
|
setApiError(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleProcesarArchivo = async () => {
|
||||||
|
if (!archivoSeleccionado) {
|
||||||
|
setApiError("Por favor, seleccione un archivo de respuesta para procesar.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLoadingProceso(true);
|
||||||
|
setApiMessage(null);
|
||||||
|
setApiError(null);
|
||||||
|
try {
|
||||||
|
const response = await facturacionService.procesarArchivoRespuesta(archivoSeleccionado);
|
||||||
|
setApiMessage(response.mensajeResumen);
|
||||||
|
if (response.errores?.length > 0) {
|
||||||
|
setApiError(`Se encontraron los siguientes problemas durante el proceso:\n${response.errores.join('\n')}`);
|
||||||
|
}
|
||||||
|
cargarFacturasDelPeriodo(); // Recargar para ver los estados finales
|
||||||
|
} catch (err: any) {
|
||||||
|
const message = axios.isAxiosError(err) && err.response?.data?.mensajeResumen
|
||||||
|
? err.response.data.mensajeResumen
|
||||||
|
: 'Ocurrió un error crítico al procesar el archivo.';
|
||||||
|
setApiError(message);
|
||||||
|
} finally {
|
||||||
|
setLoadingProceso(false);
|
||||||
|
setArchivoSeleccionado(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!puedeGenerarFacturacion) {
|
||||||
|
return <Alert severity="error">No tiene permiso para acceder a esta sección.</Alert>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ p: 1 }}>
|
||||||
|
<Typography variant="h5" gutterBottom>Facturación y Débito Automático</Typography>
|
||||||
|
<Paper sx={{ p: 2, mb: 2 }}>
|
||||||
|
<Typography variant="h6">1. Generación de Facturación</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||||
|
Este proceso calcula los importes a cobrar para todas las suscripciones activas en el período seleccionado.
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ display: 'flex', gap: 2, mb: 2, alignItems: 'center' }}>
|
||||||
|
<FormControl sx={{ minWidth: 120 }} size="small">
|
||||||
|
<InputLabel>Mes</InputLabel>
|
||||||
|
<Select value={selectedMes} label="Mes" onChange={(e) => setSelectedMes(e.target.value as number)}>{meses.map(m => <MenuItem key={m.value} value={m.value}>{m.label}</MenuItem>)}</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl sx={{ minWidth: 120 }} size="small">
|
||||||
|
<InputLabel>Año</InputLabel>
|
||||||
|
<Select value={selectedAnio} label="Año" onChange={(e) => setSelectedAnio(e.target.value as number)}>{anios.map(a => <MenuItem key={a} value={a}>{a}</MenuItem>)}</Select>
|
||||||
|
</FormControl>
|
||||||
|
</Box>
|
||||||
|
<Button variant="contained" color="primary" startIcon={loading ? <CircularProgress size={20} color="inherit" /> : <PlayCircleOutlineIcon />} onClick={handleGenerarFacturacion} disabled={loading || loadingArchivo}>Generar Facturación del Período</Button>
|
||||||
|
</Paper>
|
||||||
|
<Paper sx={{ p: 2, mb: 2 }}>
|
||||||
|
<Typography variant="h6">2. Generación de Archivo para Banco</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>Crea el archivo de texto para enviar a "Pago Directo Galicia" con todas las facturas del período que estén listas para el cobro.</Typography>
|
||||||
|
<Button variant="contained" color="secondary" startIcon={loadingArchivo ? <CircularProgress size={20} color="inherit" /> : <DownloadIcon />} onClick={handleGenerarArchivo} disabled={loading || loadingArchivo || !puedeGenerarArchivo}>Generar Archivo de Débito</Button>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
<Paper sx={{ p: 2, mb: 2 }}>
|
||||||
|
<Typography variant="h6">3. Procesar Respuesta del Banco</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||||
|
Suba aquí el archivo de respuesta de Galicia para actualizar automáticamente el estado de las facturas a "Pagada" o "Rechazada".
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center' }}>
|
||||||
|
<Button
|
||||||
|
component="label"
|
||||||
|
role={undefined}
|
||||||
|
variant="outlined"
|
||||||
|
tabIndex={-1}
|
||||||
|
startIcon={<UploadFileIcon />}
|
||||||
|
disabled={loadingProceso}
|
||||||
|
>
|
||||||
|
Seleccionar Archivo
|
||||||
|
<VisuallyHiddenInput type="file" onChange={handleFileChange} accept=".txt, text/plain" />
|
||||||
|
</Button>
|
||||||
|
{archivoSeleccionado && <Typography variant="body2">{archivoSeleccionado.name}</Typography>}
|
||||||
|
</Box>
|
||||||
|
{archivoSeleccionado && (
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="success"
|
||||||
|
sx={{ mt: 2 }}
|
||||||
|
onClick={handleProcesarArchivo}
|
||||||
|
disabled={loadingProceso}
|
||||||
|
startIcon={loadingProceso ? <CircularProgress size={20} color="inherit" /> : <PlayCircleOutlineIcon />}
|
||||||
|
>
|
||||||
|
Procesar Archivo de Respuesta
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
{apiError && <Alert severity="error" sx={{ my: 2 }}>{apiError}</Alert>}
|
||||||
|
{apiMessage && <Alert severity="success" sx={{ my: 2 }}>{apiMessage}</Alert>}
|
||||||
|
|
||||||
|
<Typography variant="h6" sx={{ mt: 4 }}>Facturas del Período</Typography>
|
||||||
|
<TableContainer component={Paper}>
|
||||||
|
<Table size="small">
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>ID</TableCell><TableCell>Suscriptor</TableCell><TableCell>Publicación</TableCell>
|
||||||
|
<TableCell align="right">Importe</TableCell><TableCell>Estado</TableCell><TableCell>Nro. Factura</TableCell>
|
||||||
|
<TableCell align="right" sx={{ fontWeight: 'bold' }}>Acciones</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{loading ? (<TableRow><TableCell colSpan={7} align="center"><CircularProgress /></TableCell></TableRow>)
|
||||||
|
: facturas.length === 0 ? (<TableRow><TableCell colSpan={7} align="center">No hay facturas para el período seleccionado.</TableCell></TableRow>)
|
||||||
|
: (facturas.map(f => (
|
||||||
|
<TableRow key={f.idFactura} hover>
|
||||||
|
<TableCell>{f.idFactura}</TableCell>
|
||||||
|
<TableCell>{f.nombreSuscriptor}</TableCell>
|
||||||
|
<TableCell>{f.nombrePublicacion}</TableCell>
|
||||||
|
<TableCell align="right">${f.importeFinal.toFixed(2)}</TableCell>
|
||||||
|
<TableCell><Chip label={f.estado} size="small" color={f.estado === 'Pagada' ? 'success' : (f.estado === 'Rechazada' ? 'error' : 'default')} /></TableCell>
|
||||||
|
<TableCell>{f.numeroFactura || '-'}</TableCell>
|
||||||
|
<TableCell align="right">
|
||||||
|
<IconButton onClick={(e) => handleMenuOpen(e, f)} disabled={f.estado === 'Pagada' || f.estado === 'Anulada'}>
|
||||||
|
<MoreVertIcon />
|
||||||
|
</IconButton>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
|
||||||
|
{selectedFactura && puedeRegistrarPago && (
|
||||||
|
<MenuItem onClick={handleOpenPagoModal}>
|
||||||
|
<ListItemIcon><PaymentIcon fontSize="small" /></ListItemIcon>
|
||||||
|
<ListItemText>Registrar Pago Manual</ListItemText>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
{selectedFactura && puedeEnviarEmail && (
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => handleSendEmail(selectedFactura.idFactura)}
|
||||||
|
disabled={!selectedFactura.numeroFactura}
|
||||||
|
>
|
||||||
|
<ListItemIcon><EmailIcon fontSize="small" /></ListItemIcon>
|
||||||
|
<ListItemText>Enviar Email</ListItemText>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
</Menu>
|
||||||
|
<PagoManualModal open={pagoModalOpen} onClose={handleClosePagoModal} onSubmit={handleSubmitPagoModal} factura={selectedFactura} errorMessage={apiError} clearErrorMessage={() => setApiError(null)} />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FacturacionPage;
|
||||||
157
Frontend/src/pages/Suscripciones/GestionarPromocionesPage.tsx
Normal file
157
Frontend/src/pages/Suscripciones/GestionarPromocionesPage.tsx
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
|
import { Box, Typography, Button, Paper, CircularProgress, Alert, Table, TableContainer, TableHead, TableRow, TableCell, TableBody, Chip, IconButton, Switch, FormControlLabel, Tooltip } from '@mui/material';
|
||||||
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
|
import EditIcon from '@mui/icons-material/Edit';
|
||||||
|
import promocionService from '../../services/Suscripciones/promocionService';
|
||||||
|
import { usePermissions } from '../../hooks/usePermissions';
|
||||||
|
import axios from 'axios';
|
||||||
|
import type { PromocionDto } from '../../models/dtos/Suscripciones/PromocionDto';
|
||||||
|
import type { CreatePromocionDto, UpdatePromocionDto } from '../../models/dtos/Suscripciones/CreatePromocionDto';
|
||||||
|
import PromocionFormModal from '../../components/Modals/Suscripciones/PromocionFormModal';
|
||||||
|
|
||||||
|
const GestionarPromocionesPage: React.FC = () => {
|
||||||
|
const [promociones, setPromociones] = useState<PromocionDto[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [filtroSoloActivas, setFiltroSoloActivas] = useState(true);
|
||||||
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
|
const [editingPromocion, setEditingPromocion] = useState<PromocionDto | null>(null);
|
||||||
|
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||||
|
const puedeGestionar = isSuperAdmin || tienePermiso("SU010");
|
||||||
|
|
||||||
|
const cargarDatos = useCallback(async () => {
|
||||||
|
if (!puedeGestionar) {
|
||||||
|
setError("No tiene permiso para gestionar promociones.");
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
setApiErrorMessage(null);
|
||||||
|
try {
|
||||||
|
const data = await promocionService.getAllPromociones(filtroSoloActivas);
|
||||||
|
setPromociones(data);
|
||||||
|
} catch (err) {
|
||||||
|
setError("Error al cargar las promociones.");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [filtroSoloActivas, puedeGestionar]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
cargarDatos();
|
||||||
|
}, [cargarDatos]);
|
||||||
|
|
||||||
|
const handleOpenModal = (promocion?: PromocionDto) => {
|
||||||
|
setEditingPromocion(promocion || null);
|
||||||
|
setApiErrorMessage(null);
|
||||||
|
setModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseModal = () => {
|
||||||
|
setModalOpen(false);
|
||||||
|
setEditingPromocion(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmitModal = async (data: CreatePromocionDto | UpdatePromocionDto, id?: number) => {
|
||||||
|
setApiErrorMessage(null);
|
||||||
|
try {
|
||||||
|
if (id && editingPromocion) {
|
||||||
|
await promocionService.updatePromocion(id, data as UpdatePromocionDto);
|
||||||
|
} else {
|
||||||
|
await promocionService.createPromocion(data as CreatePromocionDto);
|
||||||
|
}
|
||||||
|
cargarDatos();
|
||||||
|
} catch (err: any) {
|
||||||
|
const message = axios.isAxiosError(err) && err.response?.data?.message
|
||||||
|
? err.response.data.message
|
||||||
|
: 'Error al guardar la promoción.';
|
||||||
|
setApiErrorMessage(message);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (dateString?: string | null) => {
|
||||||
|
if (!dateString) return 'Indefinido';
|
||||||
|
const parts = dateString.split('-');
|
||||||
|
return `${parts[2]}/${parts[1]}/${parts[0]}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatTipo = (tipo: string) => {
|
||||||
|
if (tipo === 'MontoFijo') return 'Monto Fijo';
|
||||||
|
if (tipo === 'Porcentaje') return 'Porcentaje';
|
||||||
|
return tipo;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) return <Box sx={{ display: 'flex', justifyContent: 'center', p: 4 }}><CircularProgress /></Box>;
|
||||||
|
if (error) return <Alert severity="error" sx={{m: 2}}>{error}</Alert>;
|
||||||
|
if (!puedeGestionar) return <Alert severity="error" sx={{m: 2}}>Acceso Denegado.</Alert>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ p: 1 }}>
|
||||||
|
<Typography variant="h5" gutterBottom>Gestionar Promociones</Typography>
|
||||||
|
<Paper sx={{ p: 2, mb: 2, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<FormControlLabel control={<Switch checked={filtroSoloActivas} onChange={(e) => setFiltroSoloActivas(e.target.checked)} />} label="Ver Solo Activas" />
|
||||||
|
<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()}>
|
||||||
|
Nueva Promoción
|
||||||
|
</Button>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
{apiErrorMessage && <Alert severity="error" sx={{my: 2}}>{apiErrorMessage}</Alert>}
|
||||||
|
|
||||||
|
<TableContainer component={Paper}>
|
||||||
|
<Table size="small">
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell sx={{fontWeight: 'bold'}}>Descripción</TableCell>
|
||||||
|
<TableCell sx={{fontWeight: 'bold'}}>Tipo</TableCell>
|
||||||
|
<TableCell align="right" sx={{fontWeight: 'bold'}}>Valor</TableCell>
|
||||||
|
<TableCell sx={{fontWeight: 'bold'}}>Inicio</TableCell>
|
||||||
|
<TableCell sx={{fontWeight: 'bold'}}>Fin</TableCell>
|
||||||
|
<TableCell align="center" sx={{fontWeight: 'bold'}}>Estado</TableCell>
|
||||||
|
<TableCell align="right" sx={{fontWeight: 'bold'}}>Acciones</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{promociones.length === 0 ? (
|
||||||
|
<TableRow><TableCell colSpan={7} align="center">No se encontraron promociones.</TableCell></TableRow>
|
||||||
|
) : (
|
||||||
|
promociones.map(p => (
|
||||||
|
<TableRow key={p.idPromocion} hover>
|
||||||
|
<TableCell>{p.descripcion}</TableCell>
|
||||||
|
<TableCell>{formatTipo(p.tipoPromocion)}</TableCell>
|
||||||
|
<TableCell align="right">{p.tipoPromocion === 'Porcentaje' ? `${p.valor}%` : `$${p.valor.toFixed(2)}`}</TableCell>
|
||||||
|
<TableCell>{formatDate(p.fechaInicio)}</TableCell>
|
||||||
|
<TableCell>{formatDate(p.fechaFin)}</TableCell>
|
||||||
|
<TableCell align="center">
|
||||||
|
<Chip label={p.activa ? 'Activa' : 'Inactiva'} color={p.activa ? 'success' : 'default'} size="small" />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="right">
|
||||||
|
<Tooltip title="Editar Promoción">
|
||||||
|
<IconButton onClick={() => handleOpenModal(p)}>
|
||||||
|
<EditIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
|
||||||
|
<PromocionFormModal
|
||||||
|
open={modalOpen}
|
||||||
|
onClose={handleCloseModal}
|
||||||
|
onSubmit={handleSubmitModal}
|
||||||
|
initialData={editingPromocion}
|
||||||
|
errorMessage={apiErrorMessage}
|
||||||
|
clearErrorMessage={() => setApiErrorMessage(null)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GestionarPromocionesPage;
|
||||||
@@ -13,6 +13,8 @@ import { usePermissions } from '../../hooks/usePermissions';
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import type { CreateSuscripcionDto } from '../../models/dtos/Suscripciones/CreateSuscripcionDto';
|
import type { CreateSuscripcionDto } from '../../models/dtos/Suscripciones/CreateSuscripcionDto';
|
||||||
import type { UpdateSuscripcionDto } from '../../models/dtos/Suscripciones/UpdateSuscripcionDto';
|
import type { UpdateSuscripcionDto } from '../../models/dtos/Suscripciones/UpdateSuscripcionDto';
|
||||||
|
import LoyaltyIcon from '@mui/icons-material/Loyalty';
|
||||||
|
import GestionarPromocionesSuscripcionModal from '../../components/Modals/Suscripciones/GestionarPromocionesSuscripcionModal';
|
||||||
|
|
||||||
const GestionarSuscripcionesSuscriptorPage: React.FC = () => {
|
const GestionarSuscripcionesSuscriptorPage: React.FC = () => {
|
||||||
const { idSuscriptor: idSuscriptorStr } = useParams<{ idSuscriptor: string }>();
|
const { idSuscriptor: idSuscriptorStr } = useParams<{ idSuscriptor: string }>();
|
||||||
@@ -27,6 +29,9 @@ const GestionarSuscripcionesSuscriptorPage: React.FC = () => {
|
|||||||
const [editingSuscripcion, setEditingSuscripcion] = useState<SuscripcionDto | null>(null);
|
const [editingSuscripcion, setEditingSuscripcion] = useState<SuscripcionDto | null>(null);
|
||||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
|
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const [promocionesModalOpen, setPromocionesModalOpen] = useState(false);
|
||||||
|
const [selectedSuscripcion, setSelectedSuscripcion] = useState<SuscripcionDto | null>(null);
|
||||||
|
|
||||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||||
const puedeVer = isSuperAdmin || tienePermiso("SU001");
|
const puedeVer = isSuperAdmin || tienePermiso("SU001");
|
||||||
const puedeGestionar = isSuperAdmin || tienePermiso("SU005");
|
const puedeGestionar = isSuperAdmin || tienePermiso("SU005");
|
||||||
@@ -69,7 +74,7 @@ const GestionarSuscripcionesSuscriptorPage: React.FC = () => {
|
|||||||
const handleSubmitModal = async (data: CreateSuscripcionDto | UpdateSuscripcionDto, id?: number) => {
|
const handleSubmitModal = async (data: CreateSuscripcionDto | UpdateSuscripcionDto, id?: number) => {
|
||||||
setApiErrorMessage(null);
|
setApiErrorMessage(null);
|
||||||
try {
|
try {
|
||||||
if(id && editingSuscripcion) {
|
if (id && editingSuscripcion) {
|
||||||
await suscripcionService.updateSuscripcion(id, data as UpdateSuscripcionDto);
|
await suscripcionService.updateSuscripcion(id, data as UpdateSuscripcionDto);
|
||||||
} else {
|
} else {
|
||||||
await suscripcionService.createSuscripcion(data as CreateSuscripcionDto);
|
await suscripcionService.createSuscripcion(data as CreateSuscripcionDto);
|
||||||
@@ -82,6 +87,11 @@ const GestionarSuscripcionesSuscriptorPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleOpenPromocionesModal = (suscripcion: SuscripcionDto) => {
|
||||||
|
setSelectedSuscripcion(suscripcion);
|
||||||
|
setPromocionesModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
const formatDate = (dateString?: string | null) => {
|
const formatDate = (dateString?: string | null) => {
|
||||||
if (!dateString) return 'Indefinido';
|
if (!dateString) return 'Indefinido';
|
||||||
// Asume que la fecha viene como "yyyy-MM-dd"
|
// Asume que la fecha viene como "yyyy-MM-dd"
|
||||||
@@ -90,8 +100,8 @@ const GestionarSuscripcionesSuscriptorPage: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (loading) return <Box sx={{ display: 'flex', justifyContent: 'center', p: 4 }}><CircularProgress /></Box>;
|
if (loading) return <Box sx={{ display: 'flex', justifyContent: 'center', p: 4 }}><CircularProgress /></Box>;
|
||||||
if (error) return <Alert severity="error" sx={{m: 2}}>{error}</Alert>;
|
if (error) return <Alert severity="error" sx={{ m: 2 }}>{error}</Alert>;
|
||||||
if (!puedeVer) return <Alert severity="error" sx={{m: 2}}>Acceso Denegado.</Alert>
|
if (!puedeVer) return <Alert severity="error" sx={{ m: 2 }}>Acceso Denegado.</Alert>
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ p: 2 }}>
|
<Box sx={{ p: 2 }}>
|
||||||
@@ -105,19 +115,19 @@ const GestionarSuscripcionesSuscriptorPage: React.FC = () => {
|
|||||||
|
|
||||||
{puedeGestionar && <Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ my: 2 }}>Nueva Suscripción</Button>}
|
{puedeGestionar && <Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ my: 2 }}>Nueva Suscripción</Button>}
|
||||||
|
|
||||||
{apiErrorMessage && <Alert severity="error" sx={{my: 2}}>{apiErrorMessage}</Alert>}
|
{apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{apiErrorMessage}</Alert>}
|
||||||
|
|
||||||
<TableContainer component={Paper}>
|
<TableContainer component={Paper}>
|
||||||
<Table size="small">
|
<Table size="small">
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell sx={{fontWeight: 'bold'}}>Publicación</TableCell>
|
<TableCell sx={{ fontWeight: 'bold' }}>Publicación</TableCell>
|
||||||
<TableCell sx={{fontWeight: 'bold'}}>Estado</TableCell>
|
<TableCell sx={{ fontWeight: 'bold' }}>Estado</TableCell>
|
||||||
<TableCell sx={{fontWeight: 'bold'}}>Días Entrega</TableCell>
|
<TableCell sx={{ fontWeight: 'bold' }}>Días Entrega</TableCell>
|
||||||
<TableCell sx={{fontWeight: 'bold'}}>Inicio</TableCell>
|
<TableCell sx={{ fontWeight: 'bold' }}>Inicio</TableCell>
|
||||||
<TableCell sx={{fontWeight: 'bold'}}>Fin</TableCell>
|
<TableCell sx={{ fontWeight: 'bold' }}>Fin</TableCell>
|
||||||
<TableCell sx={{fontWeight: 'bold'}}>Observaciones</TableCell>
|
<TableCell sx={{ fontWeight: 'bold' }}>Observaciones</TableCell>
|
||||||
<TableCell align="right" sx={{fontWeight: 'bold'}}>Acciones</TableCell>
|
<TableCell align="right" sx={{ fontWeight: 'bold' }}>Acciones</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@@ -147,7 +157,16 @@ const GestionarSuscripcionesSuscriptorPage: React.FC = () => {
|
|||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
<TableCell align="right">
|
||||||
|
<Tooltip title="Editar Suscripción">
|
||||||
|
<span><IconButton onClick={() => handleOpenModal(s)} disabled={!puedeGestionar}><EditIcon /></IconButton></span>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="Gestionar Promociones">
|
||||||
|
<span><IconButton onClick={() => handleOpenPromocionesModal(s)} disabled={!puedeGestionar}><LoyaltyIcon /></IconButton></span>
|
||||||
|
</Tooltip>
|
||||||
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
@@ -165,6 +184,11 @@ const GestionarSuscripcionesSuscriptorPage: React.FC = () => {
|
|||||||
clearErrorMessage={() => setApiErrorMessage(null)}
|
clearErrorMessage={() => setApiErrorMessage(null)}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
<GestionarPromocionesSuscripcionModal
|
||||||
|
open={promocionesModalOpen}
|
||||||
|
onClose={() => setPromocionesModalOpen(false)}
|
||||||
|
suscripcion={selectedSuscripcion}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import { usePermissions } from '../../hooks/usePermissions';
|
|||||||
// Define las pestañas del módulo. Ajusta los permisos según sea necesario.
|
// Define las pestañas del módulo. Ajusta los permisos según sea necesario.
|
||||||
const suscripcionesSubModules = [
|
const suscripcionesSubModules = [
|
||||||
{ label: 'Suscriptores', path: 'suscriptores', requiredPermission: 'SU001' },
|
{ label: 'Suscriptores', path: 'suscriptores', requiredPermission: 'SU001' },
|
||||||
// { label: 'Facturación', path: 'facturacion', requiredPermission: 'SU005' },
|
{ label: 'Facturación', path: 'facturacion', requiredPermission: 'SU006' },
|
||||||
// { label: 'Promociones', path: 'promociones', requiredPermission: 'SU006' },
|
{ label: 'Promociones', path: 'promociones', requiredPermission: 'SU010' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const SuscripcionesIndexPage: React.FC = () => {
|
const SuscripcionesIndexPage: React.FC = () => {
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ import ReporteListadoDistMensualPage from '../pages/Reportes/ReporteListadoDistM
|
|||||||
import SuscripcionesIndexPage from '../pages/Suscripciones/SuscripcionesIndexPage';
|
import SuscripcionesIndexPage from '../pages/Suscripciones/SuscripcionesIndexPage';
|
||||||
import GestionarSuscriptoresPage from '../pages/Suscripciones/GestionarSuscriptoresPage';
|
import GestionarSuscriptoresPage from '../pages/Suscripciones/GestionarSuscriptoresPage';
|
||||||
import GestionarSuscripcionesSuscriptorPage from '../pages/Suscripciones/GestionarSuscripcionesSuscriptorPage';
|
import GestionarSuscripcionesSuscriptorPage from '../pages/Suscripciones/GestionarSuscripcionesSuscriptorPage';
|
||||||
|
import FacturacionPage from '../pages/Suscripciones/FacturacionPage';
|
||||||
|
import GestionarPromocionesPage from '../pages/Suscripciones/GestionarPromocionesPage';
|
||||||
|
|
||||||
// Anonalías
|
// Anonalías
|
||||||
import AlertasPage from '../pages/Anomalia/AlertasPage';
|
import AlertasPage from '../pages/Anomalia/AlertasPage';
|
||||||
@@ -203,7 +205,16 @@ const AppRoutes = () => {
|
|||||||
<GestionarSuscripcionesSuscriptorPage />
|
<GestionarSuscripcionesSuscriptorPage />
|
||||||
</SectionProtectedRoute>
|
</SectionProtectedRoute>
|
||||||
} />
|
} />
|
||||||
{/* Aquí irán las otras sub-rutas como 'facturacion', etc. */}
|
<Route path="facturacion" element={
|
||||||
|
<SectionProtectedRoute requiredPermission="SU006" sectionName="Facturación de Suscripciones">
|
||||||
|
<FacturacionPage />
|
||||||
|
</SectionProtectedRoute>
|
||||||
|
} />
|
||||||
|
<Route path="promociones" element={
|
||||||
|
<SectionProtectedRoute requiredPermission="SU010" sectionName="Promociones">
|
||||||
|
<GestionarPromocionesPage />
|
||||||
|
</SectionProtectedRoute>
|
||||||
|
} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
{/* Módulo Contable (anidado) */}
|
{/* Módulo Contable (anidado) */}
|
||||||
|
|||||||
74
Frontend/src/services/Suscripciones/facturacionService.ts
Normal file
74
Frontend/src/services/Suscripciones/facturacionService.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import apiClient from '../apiClient';
|
||||||
|
import type { FacturaDto } from '../../models/dtos/Suscripciones/FacturaDto';
|
||||||
|
import type { GenerarFacturacionResponseDto } from '../../models/dtos/Suscripciones/GenerarFacturacionResponseDto';
|
||||||
|
import type { PagoDto } from '../../models/dtos/Suscripciones/PagoDto';
|
||||||
|
import type { CreatePagoDto } from '../../models/dtos/Suscripciones/CreatePagoDto';
|
||||||
|
import type { ProcesamientoLoteResponseDto } from '../../models/dtos/Suscripciones/ProcesamientoLoteResponseDto';
|
||||||
|
|
||||||
|
const API_URL = '/facturacion';
|
||||||
|
const DEBITOS_URL = '/debitos';
|
||||||
|
const PAGOS_URL = '/pagos';
|
||||||
|
const FACTURAS_URL = '/facturas';
|
||||||
|
|
||||||
|
const procesarArchivoRespuesta = async (archivo: File): Promise<ProcesamientoLoteResponseDto> => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('archivo', archivo);
|
||||||
|
|
||||||
|
const response = await apiClient.post<ProcesamientoLoteResponseDto>(`${DEBITOS_URL}/procesar-respuesta`, formData, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFacturasPorPeriodo = async (anio: number, mes: number): Promise<FacturaDto[]> => {
|
||||||
|
const response = await apiClient.get<FacturaDto[]>(`${API_URL}/${anio}/${mes}`);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const generarFacturacionMensual = async (anio: number, mes: number): Promise<GenerarFacturacionResponseDto> => {
|
||||||
|
const response = await apiClient.post<GenerarFacturacionResponseDto>(`${API_URL}/${anio}/${mes}`);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const generarArchivoDebito = async (anio: number, mes: number): Promise<{ fileContent: Blob, fileName: string }> => {
|
||||||
|
const response = await apiClient.post(`${DEBITOS_URL}/${anio}/${mes}/generar-archivo`, {}, {
|
||||||
|
responseType: 'blob',
|
||||||
|
});
|
||||||
|
|
||||||
|
const contentDisposition = response.headers['content-disposition'];
|
||||||
|
let fileName = `debito_${anio}_${mes}.txt`;
|
||||||
|
if (contentDisposition) {
|
||||||
|
const fileNameMatch = contentDisposition.match(/filename="(.+)"/);
|
||||||
|
if (fileNameMatch && fileNameMatch.length > 1) {
|
||||||
|
fileName = fileNameMatch[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { fileContent: response.data, fileName: fileName };
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPagosPorFactura = async (idFactura: number): Promise<PagoDto[]> => {
|
||||||
|
const response = await apiClient.get<PagoDto[]>(`${FACTURAS_URL}/${idFactura}/pagos`);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const registrarPagoManual = async (data: CreatePagoDto): Promise<PagoDto> => {
|
||||||
|
const response = await apiClient.post<PagoDto>(PAGOS_URL, data);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const enviarFacturaPorEmail = async (idFactura: number): Promise<void> => {
|
||||||
|
await apiClient.post(`${API_URL}/${idFactura}/enviar-email`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
procesarArchivoRespuesta,
|
||||||
|
getFacturasPorPeriodo,
|
||||||
|
generarFacturacionMensual,
|
||||||
|
generarArchivoDebito,
|
||||||
|
getPagosPorFactura,
|
||||||
|
registrarPagoManual,
|
||||||
|
enviarFacturaPorEmail,
|
||||||
|
};
|
||||||
31
Frontend/src/services/Suscripciones/promocionService.ts
Normal file
31
Frontend/src/services/Suscripciones/promocionService.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import apiClient from '../apiClient';
|
||||||
|
import type { PromocionDto } from '../../models/dtos/Suscripciones/PromocionDto';
|
||||||
|
import type { CreatePromocionDto, UpdatePromocionDto } from '../../models/dtos/Suscripciones/CreatePromocionDto';
|
||||||
|
|
||||||
|
const API_URL = '/promociones';
|
||||||
|
|
||||||
|
const getAllPromociones = async (soloActivas: boolean = true): Promise<PromocionDto[]> => {
|
||||||
|
const response = await apiClient.get<PromocionDto[]>(`${API_URL}?soloActivas=${soloActivas}`);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPromocionById = async (id: number): Promise<PromocionDto> => {
|
||||||
|
const response = await apiClient.get<PromocionDto>(`${API_URL}/${id}`);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createPromocion = async (data: CreatePromocionDto): Promise<PromocionDto> => {
|
||||||
|
const response = await apiClient.post<PromocionDto>(API_URL, data);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatePromocion = async (id: number, data: UpdatePromocionDto): Promise<void> => {
|
||||||
|
await apiClient.put(`${API_URL}/${id}`, data);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
getAllPromociones,
|
||||||
|
getPromocionById,
|
||||||
|
createPromocion,
|
||||||
|
updatePromocion
|
||||||
|
};
|
||||||
@@ -2,6 +2,7 @@ import apiClient from '../apiClient';
|
|||||||
import type { SuscripcionDto } from '../../models/dtos/Suscripciones/SuscripcionDto';
|
import type { SuscripcionDto } from '../../models/dtos/Suscripciones/SuscripcionDto';
|
||||||
import type { CreateSuscripcionDto } from '../../models/dtos/Suscripciones/CreateSuscripcionDto';
|
import type { CreateSuscripcionDto } from '../../models/dtos/Suscripciones/CreateSuscripcionDto';
|
||||||
import type { UpdateSuscripcionDto } from '../../models/dtos/Suscripciones/UpdateSuscripcionDto';
|
import type { UpdateSuscripcionDto } from '../../models/dtos/Suscripciones/UpdateSuscripcionDto';
|
||||||
|
import type { PromocionDto } from '../../models/dtos/Suscripciones/PromocionDto';
|
||||||
|
|
||||||
const API_URL_BASE = '/suscripciones';
|
const API_URL_BASE = '/suscripciones';
|
||||||
const API_URL_BY_SUSCRIPTOR = '/suscriptores'; // Para la ruta anidada
|
const API_URL_BY_SUSCRIPTOR = '/suscriptores'; // Para la ruta anidada
|
||||||
@@ -25,9 +26,31 @@ const updateSuscripcion = async (id: number, data: UpdateSuscripcionDto): Promis
|
|||||||
await apiClient.put(`${API_URL_BASE}/${id}`, data);
|
await apiClient.put(`${API_URL_BASE}/${id}`, data);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getPromocionesAsignadas = async (idSuscripcion: number): Promise<PromocionDto[]> => {
|
||||||
|
const response = await apiClient.get<PromocionDto[]>(`${API_URL_BASE}/${idSuscripcion}/promociones`);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPromocionesDisponibles = async (idSuscripcion: number): Promise<PromocionDto[]> => {
|
||||||
|
const response = await apiClient.get<PromocionDto[]>(`${API_URL_BASE}/${idSuscripcion}/promociones-disponibles`);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const asignarPromocion = async (idSuscripcion: number, idPromocion: number): Promise<void> => {
|
||||||
|
await apiClient.post(`${API_URL_BASE}/${idSuscripcion}/promociones/${idPromocion}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const quitarPromocion = async (idSuscripcion: number, idPromocion: number): Promise<void> => {
|
||||||
|
await apiClient.delete(`${API_URL_BASE}/${idSuscripcion}/promociones/${idPromocion}`);
|
||||||
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
getSuscripcionesPorSuscriptor,
|
getSuscripcionesPorSuscriptor,
|
||||||
getSuscripcionById,
|
getSuscripcionById,
|
||||||
createSuscripcion,
|
createSuscripcion,
|
||||||
updateSuscripcion,
|
updateSuscripcion,
|
||||||
|
getPromocionesAsignadas,
|
||||||
|
getPromocionesDisponibles,
|
||||||
|
asignarPromocion,
|
||||||
|
quitarPromocion
|
||||||
};
|
};
|
||||||
Reference in New Issue
Block a user