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; 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"; private const string ORIGEN_EMPRESA = "ELDIA"; 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) { // Se define la identificación del archivo. // Este número debe ser gestionado para no repetirse en archivos generados // para la misma prestación y fecha. const int identificacionArchivo = 1; 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(); // Se utiliza la variable 'identificacionArchivo' para nombrar el archivo. var nombreArchivo = $"{NRO_PRESTACION}{fechaGeneracion:yyyyMMdd}{identificacionArchivo}.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(); // Se pasa la 'identificacionArchivo' al método que crea el Header. sb.Append(CrearRegistroHeader(fechaGeneracion, importeTotal, cantidadRegistros, identificacionArchivo)); foreach (var item in facturasParaDebito) { sb.Append(CrearRegistroDetalle(item.Factura, item.Suscriptor)); } // Se pasa la 'identificacionArchivo' al método que crea el Trailer. sb.Append(CrearRegistroTrailer(fechaGeneracion, importeTotal, cantidadRegistros, identificacionArchivo)); 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 facturas = await _facturaRepository.GetByPeriodoAsync(periodo); var resultado = new List<(Factura, Suscriptor)>(); foreach (var f in facturas.Where(fa => fa.EstadoPago == "Pendiente")) { var suscriptor = await _suscriptorRepository.GetByIdAsync(f.IdSuscriptor); // Se valida que el CBU de Banelco (22 caracteres) exista antes de intentar la conversión. if (suscriptor == null || string.IsNullOrWhiteSpace(suscriptor.CBU) || suscriptor.CBU.Length != 22) { _logger.LogWarning("Suscriptor ID {IdSuscriptor} omitido del lote de débito por CBU inválido o ausente (se esperan 22 dígitos).", suscriptor?.IdSuscriptor); continue; } var formaPago = await _formaPagoRepository.GetByIdAsync(suscriptor.IdFormaPagoPreferida); if (formaPago != null && formaPago.RequiereCBU) { resultado.Add((f, suscriptor)); } } return resultado; } // Lógica de conversión de CBU. private string ConvertirCbuBanelcoASnp(string cbu22) { if (string.IsNullOrEmpty(cbu22) || cbu22.Length != 22) { _logger.LogError("Se intentó convertir un CBU inválido de {Length} caracteres. Se devolverá un campo vacío.", cbu22?.Length ?? 0); // Devolver un string de 26 espacios/ceros según la preferencia del banco para campos erróneos. return "".PadRight(26); } // El formato SNP de 26 se obtiene insertando un "0" al inicio y "000" después del 8vo caracter del CBU de 22. // Formato Banelco (22): [BBBSSSSX] [T....Y] // Posiciones: (0-7) (8-21) // Formato SNP (26): 0[BBBSSSSX]000[T....Y] try { string bloque1 = cbu22.Substring(0, 8); // Contiene código de banco, sucursal y DV del bloque 1. string bloque2 = cbu22.Substring(8); // Contiene el resto de la cadena. // Reconstruir en formato SNP de 26 dígitos según el instructivo. return $"0{bloque1}000{bloque2}"; } catch (Exception ex) { _logger.LogError(ex, "Error al parsear y convertir CBU de 22 dígitos: {CBU}", cbu22); return "".PadRight(26); } } // --- Métodos de Formateo y Mapeo --- private string FormatString(string? value, int length) => (value ?? "").PadRight(length); private string FormatNumeric(long value, int length) => value.ToString().PadLeft(length, '0'); private string MapTipoDocumento(string tipo) => tipo.ToUpper() switch { "DNI" => "0096", "CUIT" => "0080", "CUIL" => "0086", "LE" => "0089", "LC" => "0090", _ => "0000" // Tipo no especificado o C.I. Policía Federal según anexo. }; private string CrearRegistroHeader(DateTime fechaGeneracion, decimal importeTotal, int cantidadRegistros, int identificacionArchivo) { var sb = new StringBuilder(); sb.Append("00"); // Tipo de Registro Header sb.Append(FormatString(NRO_PRESTACION, 6)); sb.Append("C"); // Servicio: Sistema Nacional de Pagos sb.Append(fechaGeneracion.ToString("yyyyMMdd")); sb.Append(FormatString(identificacionArchivo.ToString(), 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)); sb.Append("\r\n"); return sb.ToString(); } private string CrearRegistroDetalle(Factura factura, Suscriptor suscriptor) { // Convertimos el CBU de 22 (Banelco) a 26 (SNP) antes de usarlo. string cbu26 = ConvertirCbuBanelcoASnp(suscriptor.CBU!); var sb = new StringBuilder(); sb.Append("0370"); // Tipo de Registro Detalle (Orden de Débito) sb.Append(FormatString(suscriptor.IdSuscriptor.ToString(), 22)); // Identificación de Cliente sb.Append(FormatString(cbu26, 26)); // CBU en formato SNP de 26 caracteres. sb.Append(FormatString($"SUSC-{factura.IdFactura}", 15)); // Referencia Unívoca de la factura. sb.Append(factura.FechaVencimiento.ToString("yyyyMMdd")); sb.Append(FormatNumeric((long)(factura.ImporteFinal * 100), 14)); sb.Append(FormatNumeric(0, 8)); // Fecha 2do Vencimiento sb.Append(FormatNumeric(0, 14)); // Importe 2do Vencimiento sb.Append(FormatNumeric(0, 8)); // Fecha 3er Vencimiento sb.Append(FormatNumeric(0, 14)); // Importe 3er Vencimiento sb.Append("0"); // Moneda (0 = Pesos) sb.Append(FormatString("", 3)); // Motivo Rechazo (vacío en el envío) sb.Append(FormatString(MapTipoDocumento(suscriptor.TipoDocumento), 4)); sb.Append(FormatString(suscriptor.NroDocumento, 11)); sb.Append(FormatString("", 22)); // Nueva ID Cliente sb.Append(FormatString("", 26)); // Nueva CBU sb.Append(FormatNumeric(0, 14)); // Importe Mínimo sb.Append(FormatNumeric(0, 8)); // Fecha Próximo Vencimiento sb.Append(FormatString("", 22)); // Identificación Cuenta Anterior sb.Append(FormatString("", 40)); // Mensaje ATM sb.Append(FormatString($"Susc.{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 de Acreditamiento sb.Append(FormatString("", 26)); // Libre sb.Append("\r\n"); return sb.ToString(); } private string CrearRegistroTrailer(DateTime fechaGeneracion, decimal importeTotal, int cantidadRegistros, int identificacionArchivo) { var sb = new StringBuilder(); sb.Append("99"); // Tipo de Registro Trailer sb.Append(FormatString(NRO_PRESTACION, 6)); sb.Append("C"); // Servicio: Sistema Nacional de Pagos sb.Append(fechaGeneracion.ToString("yyyyMMdd")); sb.Append(FormatString(identificacionArchivo.ToString(), 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)); // La última línea del archivo no lleva salto de línea (\r\n). return sb.ToString(); } public async Task ProcesarArchivoRespuesta(IFormFile archivo, int idUsuario) { // Se mantiene la lógica original para procesar el archivo de respuesta del banco. 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, // Se asume una forma de pago para el débito. 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; } } }