import * as soap from 'soap' import { formatSRIError, isCriticalError, isRecoverableError } from './sri-error-codes' /** * Endpoints del SRI * Ambiente de pruebas (celcer): Para testing * Ambiente de producción: Para facturas reales */ const SRI_ENDPOINTS = { pruebas: { recepcion: 'https://celcer.sri.gob.ec/comprobantes-electronicos-ws/RecepcionComprobantesOffline?wsdl', autorizacion: 'https://celcer.sri.gob.ec/comprobantes-electronicos-ws/AutorizacionComprobantesOffline?wsdl', }, produccion: { recepcion: 'https://cel.sri.gob.ec/comprobantes-electronicos-ws/RecepcionComprobantesOffline?wsdl', autorizacion: 'https://cel.sri.gob.ec/comprobantes-electronicos-ws/AutorizacionComprobantesOffline?wsdl', }, } export interface RespuestaRecepcion { RespuestaRecepcionComprobante: { estado: string comprobantes?: { comprobante: Array<{ claveAcceso: string mensajes?: { mensaje: Array<{ identificador: string mensaje: string informacionAdicional?: string tipo: string }> } }> } } } export interface AutorizacionItem { estado: string numeroAutorizacion?: string fechaAutorizacion?: string ambiente?: string comprobante?: string mensajes?: { mensaje: { identificador: string mensaje: string informacionAdicional?: string tipo: string } | Array<{ identificador: string mensaje: string informacionAdicional?: string tipo: string }> } } export interface RespuestaAutorizacion { RespuestaAutorizacionComprobante: { claveAccesoConsultada: string numeroComprobantes: string autorizaciones?: { autorizacion: AutorizacionItem | AutorizacionItem[] } } } /** * Envía un documento XML firmado al SRI * @param signedDoc XML del documento firmado * @param ambiente 'pruebas' o 'produccion' * @returns Respuesta del SRI */ export async function sendDocToSRI( signedDoc: string, ambiente: 'pruebas' | 'produccion' = 'pruebas' ): Promise { console.log('Enviando documento al SRI...') const url = SRI_ENDPOINTS[ambiente].recepcion return new Promise((resolve, reject) => { soap.createClient( url, { wsdl_options: { timeout: 10000 } }, function (err, client) { if (err) { console.error('Error creando cliente SOAP:', err) reject(new Error(`Error de conexión con el SRI: ${err.message}`)) } else { // Codificar el XML a base64 const args = { xml: Buffer.from(signedDoc).toString('base64') } client.validarComprobante(args, function (err: any, result: RespuestaRecepcion) { if (err) { console.error('Error validando comprobante:', err) reject(new Error(`Error al validar comprobante: ${err.message}`)) } else { const estado = result?.RespuestaRecepcionComprobante?.estado if (estado !== 'RECIBIDA') { console.error('Documento no recibido:', JSON.stringify(result, null, 2)) // Extraer y formatear mensajes de error const mensajes = result?.RespuestaRecepcionComprobante?.comprobantes?.comprobante if (mensajes) { const mensajesArray = Array.isArray(mensajes) ? mensajes : [mensajes] mensajesArray.forEach((comp: any) => { const msgs = comp.mensajes?.mensaje if (msgs) { const mensajesMsgArray = Array.isArray(msgs) ? msgs : [msgs] mensajesMsgArray.forEach((msg: any) => { const errorFormateado = formatSRIError( msg.identificador, msg.mensaje, msg.informacionAdicional ) console.error('\n' + errorFormateado + '\n') if (isCriticalError(msg.identificador)) { console.error('⚠️ Este es un error crítico que requiere intervención administrativa') } else if (isRecoverableError(msg.identificador)) { console.error('🔧 Este error puede solucionarse corrigiendo el documento') } }) } }) } // En lugar de rechazar, resolvemos para que el endpoint pueda manejar el estado console.log(`⚠️ Documento con estado: ${estado} - El endpoint decidirá cómo manejarlo`) resolve(result) } else { console.log('✅ Documento recibido exitosamente') resolve(result) } } }) } } ) }) } /** * Consulta el estado de autorización de un documento en el SRI * @param accessKey Clave de acceso del documento (49 dígitos) * @param ambiente 'pruebas' o 'produccion' * @returns Respuesta de autorización del SRI */ export async function checkAuthorizationStatus( accessKey: string, ambiente: 'pruebas' | 'produccion' = 'pruebas' ): Promise { console.log('Consultando estado de autorización...') const url = SRI_ENDPOINTS[ambiente].autorizacion return new Promise((resolve, reject) => { soap.createClient( url, { wsdl_options: { timeout: 10000 } }, function (err, client) { if (err) { console.error('Error creando cliente SOAP:', err) reject(new Error(`Error de conexión con el SRI: ${err.message}`)) } else { const args = { claveAccesoComprobante: accessKey } client.autorizacionComprobante(args, function (err: any, result: RespuestaAutorizacion) { if (err) { console.error('Error consultando autorización:', err) reject(new Error(`Error al consultar autorización: ${err.message}`)) } else { console.log('Estado consultado exitosamente') console.log('Respuesta completa del SRI:', JSON.stringify(result, null, 2)) resolve(result) } }) } } ) }) } /** * Hace polling del estado de autorización hasta que el documento sea procesado * @param accessKey Clave de acceso del documento * @param ambiente 'pruebas' o 'produccion' * @param maxAttempts Número máximo de intentos * @param delayMs Delay entre intentos en milisegundos * @returns Respuesta final de autorización */ export async function longPollDoc({ accessKey, ambiente = 'pruebas', maxAttempts = 10, delayMs = 3000, }: { accessKey: string ambiente?: 'pruebas' | 'produccion' maxAttempts?: number delayMs?: number }): Promise { console.log(`Iniciando polling para clave de acceso: ${accessKey}`) for (let attempt = 1; attempt <= maxAttempts; attempt++) { console.log(`Intento ${attempt} de ${maxAttempts}...`) try { const result = await checkAuthorizationStatus(accessKey, ambiente) // La autorización puede venir como objeto o array let autorizacion = result.RespuestaAutorizacionComprobante.autorizaciones?.autorizacion if (Array.isArray(autorizacion)) { autorizacion = autorizacion[0] } if (autorizacion) { const estado = autorizacion.estado console.log(`Estado actual del documento: ${estado}`) // Si hay mensajes, mostrarlos con formato mejorado if (autorizacion.mensajes?.mensaje) { const mensajes = Array.isArray(autorizacion.mensajes.mensaje) ? autorizacion.mensajes.mensaje : [autorizacion.mensajes.mensaje] console.log('\n📨 Mensajes del SRI:') mensajes.forEach((msg: any, idx: number) => { console.log(`\n--- Mensaje ${idx + 1} ---`) // Formatear error con información detallada const errorFormateado = formatSRIError( msg.identificador, msg.mensaje, msg.informacionAdicional ) console.log(errorFormateado) // Indicar tipo de error if (isCriticalError(msg.identificador)) { console.log('\n⚠️ ERROR CRÍTICO: Requiere intervención administrativa del SRI') } else if (isRecoverableError(msg.identificador)) { console.log('\n🔧 ERROR RECUPERABLE: Puedes corregir el documento y volver a enviarlo') } }) console.log('\n' + '─'.repeat(50) + '\n') } // Estados finales if (estado === 'AUTORIZADO' || estado === 'NO AUTORIZADO' || estado === 'DEVUELTA') { if (estado === 'AUTORIZADO') { console.log(`✅ Estado final alcanzado: ${estado}`) if (autorizacion.numeroAutorizacion) { console.log(`🎫 Número de autorización: ${autorizacion.numeroAutorizacion}`) } } else { console.log(`Estado final alcanzado: ${estado}`) } return result } else { console.log(`⏳ Documento en procesamiento (estado: ${estado})`) } } else { console.log('⚠️ No se encontró información de autorización en la respuesta') console.log('Número de comprobantes:', result.RespuestaAutorizacionComprobante.numeroComprobantes) } // Esperar antes del siguiente intento if (attempt < maxAttempts) { console.log(`Esperando ${delayMs}ms antes del siguiente intento...`) await new Promise((resolve) => setTimeout(resolve, delayMs)) } } catch (error) { console.error(`Error en intento ${attempt}:`, error) // Si es el último intento, lanzar el error if (attempt === maxAttempts) { throw error } // Esperar antes de reintentar await new Promise((resolve) => setTimeout(resolve, delayMs)) } } throw new Error( `No se pudo obtener el estado del documento después de ${maxAttempts} intentos` ) }