using GestionIntegral.Api.Data; using GestionIntegral.Api.Data.Repositories.Suscripciones; using GestionIntegral.Api.Models.Suscripciones; using System.Data; using System.Text; using GestionIntegral.Api.Dtos.Suscripciones; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using System.IO; namespace GestionIntegral.Api.Services.Suscripciones { public class DebitoAutomaticoService : IDebitoAutomaticoService { private readonly IFacturaRepository _facturaRepository; private readonly ISuscriptorRepository _suscriptorRepository; private readonly ILoteDebitoRepository _loteDebitoRepository; private readonly IFormaPagoRepository _formaPagoRepository; private readonly IPagoRepository _pagoRepository; private readonly DbConnectionFactory _connectionFactory; private readonly ILogger _logger; 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, ILoteDebitoRepository loteDebitoRepository, IFormaPagoRepository formaPagoRepository, IPagoRepository pagoRepository, DbConnectionFactory connectionFactory, ILogger logger) { _facturaRepository = facturaRepository; _suscriptorRepository = suscriptorRepository; _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 { 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"; 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."); 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)); 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> GetFacturasParaDebito(string periodo, IDbTransaction transaction) { var facturasDelPeriodo = await _facturaRepository.GetByPeriodoAsync(periodo); var resultado = new List<(Factura, Suscriptor)>(); foreach (var f in facturasDelPeriodo.Where(fa => fa.EstadoPago == "Pendiente")) { var suscriptor = await _suscriptorRepository.GetByIdAsync(f.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 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) { if (linea.Length < 20) continue; respuesta.TotalRegistrosLeidos++; var referencia = linea.Substring(0, 15).Trim(); var estadoProceso = linea.Substring(15, 2).Trim(); var motivoRechazo = linea.Substring(17, 3).Trim(); 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; } 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, IdFormaPago = 1, Monto = factura.ImporteFinal, IdUsuarioRegistro = idUsuario, Referencia = $"Lote {factura.IdLoteDebito} - Banco" }; if (estadoProceso == "AP") { nuevoPago.Estado = "Aprobado"; await _pagoRepository.CreateAsync(nuevoPago, transaction); await _facturaRepository.UpdateEstadoPagoAsync(idFactura, "Pagada", transaction); respuesta.PagosAprobados++; } else { nuevoPago.Estado = "Rechazado"; await _pagoRepository.CreateAsync(nuevoPago, transaction); 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; } } }