// 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 _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 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> 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 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; } } }