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 _logger; public FacturacionService( ISuscripcionRepository suscripcionRepository, IFacturaRepository facturaRepository, IPrecioRepository precioRepository, IPromocionRepository promocionRepository, IRecargoZonaRepository recargoZonaRepository, ISuscriptorRepository suscriptorRepository, DbConnectionFactory connectionFactory, IEmailService emailService, ILogger 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 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> 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 = $@"

Hola {suscriptor.NombreCompleto},

Te adjuntamos los detalles de tu factura para el período {factura.Periodo}.

  • Número de Factura: {factura.NumeroFactura}
  • Importe Total: ${factura.ImporteFinal:N2}
  • Fecha de Vencimiento: {factura.FechaVencimiento:dd/MM/yyyy}

Gracias por ser parte de nuestra comunidad de lectores.

Diario El Día

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