sri-utils.ts 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. import * as soap from 'soap'
  2. import { formatSRIError, isCriticalError, isRecoverableError } from './sri-error-codes'
  3. /**
  4. * Endpoints del SRI
  5. * Ambiente de pruebas (celcer): Para testing
  6. * Ambiente de producción: Para facturas reales
  7. */
  8. const SRI_ENDPOINTS = {
  9. pruebas: {
  10. recepcion: 'https://celcer.sri.gob.ec/comprobantes-electronicos-ws/RecepcionComprobantesOffline?wsdl',
  11. autorizacion: 'https://celcer.sri.gob.ec/comprobantes-electronicos-ws/AutorizacionComprobantesOffline?wsdl',
  12. },
  13. produccion: {
  14. recepcion: 'https://cel.sri.gob.ec/comprobantes-electronicos-ws/RecepcionComprobantesOffline?wsdl',
  15. autorizacion: 'https://cel.sri.gob.ec/comprobantes-electronicos-ws/AutorizacionComprobantesOffline?wsdl',
  16. },
  17. }
  18. export interface RespuestaRecepcion {
  19. RespuestaRecepcionComprobante: {
  20. estado: string
  21. comprobantes?: {
  22. comprobante: Array<{
  23. claveAcceso: string
  24. mensajes?: {
  25. mensaje: Array<{
  26. identificador: string
  27. mensaje: string
  28. informacionAdicional?: string
  29. tipo: string
  30. }>
  31. }
  32. }>
  33. }
  34. }
  35. }
  36. export interface AutorizacionItem {
  37. estado: string
  38. numeroAutorizacion?: string
  39. fechaAutorizacion?: string
  40. ambiente?: string
  41. comprobante?: string
  42. mensajes?: {
  43. mensaje: {
  44. identificador: string
  45. mensaje: string
  46. informacionAdicional?: string
  47. tipo: string
  48. } | Array<{
  49. identificador: string
  50. mensaje: string
  51. informacionAdicional?: string
  52. tipo: string
  53. }>
  54. }
  55. }
  56. export interface RespuestaAutorizacion {
  57. RespuestaAutorizacionComprobante: {
  58. claveAccesoConsultada: string
  59. numeroComprobantes: string
  60. autorizaciones?: {
  61. autorizacion: AutorizacionItem | AutorizacionItem[]
  62. }
  63. }
  64. }
  65. /**
  66. * Envía un documento XML firmado al SRI
  67. * @param signedDoc XML del documento firmado
  68. * @param ambiente 'pruebas' o 'produccion'
  69. * @returns Respuesta del SRI
  70. */
  71. export async function sendDocToSRI(
  72. signedDoc: string,
  73. ambiente: 'pruebas' | 'produccion' = 'pruebas'
  74. ): Promise<RespuestaRecepcion> {
  75. console.log('Enviando documento al SRI...')
  76. const url = SRI_ENDPOINTS[ambiente].recepcion
  77. return new Promise((resolve, reject) => {
  78. soap.createClient(
  79. url,
  80. { wsdl_options: { timeout: 10000 } },
  81. function (err, client) {
  82. if (err) {
  83. console.error('Error creando cliente SOAP:', err)
  84. reject(new Error(`Error de conexión con el SRI: ${err.message}`))
  85. } else {
  86. // Codificar el XML a base64
  87. const args = { xml: Buffer.from(signedDoc).toString('base64') }
  88. client.validarComprobante(args, function (err: any, result: RespuestaRecepcion) {
  89. if (err) {
  90. console.error('Error validando comprobante:', err)
  91. reject(new Error(`Error al validar comprobante: ${err.message}`))
  92. } else {
  93. const estado = result?.RespuestaRecepcionComprobante?.estado
  94. if (estado !== 'RECIBIDA') {
  95. console.error('Documento no recibido:', JSON.stringify(result, null, 2))
  96. // Extraer y formatear mensajes de error
  97. const mensajes = result?.RespuestaRecepcionComprobante?.comprobantes?.comprobante
  98. if (mensajes) {
  99. const mensajesArray = Array.isArray(mensajes) ? mensajes : [mensajes]
  100. mensajesArray.forEach((comp: any) => {
  101. const msgs = comp.mensajes?.mensaje
  102. if (msgs) {
  103. const mensajesMsgArray = Array.isArray(msgs) ? msgs : [msgs]
  104. mensajesMsgArray.forEach((msg: any) => {
  105. const errorFormateado = formatSRIError(
  106. msg.identificador,
  107. msg.mensaje,
  108. msg.informacionAdicional
  109. )
  110. console.error('\n' + errorFormateado + '\n')
  111. if (isCriticalError(msg.identificador)) {
  112. console.error('⚠️ Este es un error crítico que requiere intervención administrativa')
  113. } else if (isRecoverableError(msg.identificador)) {
  114. console.error('🔧 Este error puede solucionarse corrigiendo el documento')
  115. }
  116. })
  117. }
  118. })
  119. }
  120. // En lugar de rechazar, resolvemos para que el endpoint pueda manejar el estado
  121. console.log(`⚠️ Documento con estado: ${estado} - El endpoint decidirá cómo manejarlo`)
  122. resolve(result)
  123. } else {
  124. console.log('✅ Documento recibido exitosamente')
  125. resolve(result)
  126. }
  127. }
  128. })
  129. }
  130. }
  131. )
  132. })
  133. }
  134. /**
  135. * Consulta el estado de autorización de un documento en el SRI
  136. * @param accessKey Clave de acceso del documento (49 dígitos)
  137. * @param ambiente 'pruebas' o 'produccion'
  138. * @returns Respuesta de autorización del SRI
  139. */
  140. export async function checkAuthorizationStatus(
  141. accessKey: string,
  142. ambiente: 'pruebas' | 'produccion' = 'pruebas'
  143. ): Promise<RespuestaAutorizacion> {
  144. console.log('Consultando estado de autorización...')
  145. const url = SRI_ENDPOINTS[ambiente].autorizacion
  146. return new Promise((resolve, reject) => {
  147. soap.createClient(
  148. url,
  149. { wsdl_options: { timeout: 10000 } },
  150. function (err, client) {
  151. if (err) {
  152. console.error('Error creando cliente SOAP:', err)
  153. reject(new Error(`Error de conexión con el SRI: ${err.message}`))
  154. } else {
  155. const args = { claveAccesoComprobante: accessKey }
  156. client.autorizacionComprobante(args, function (err: any, result: RespuestaAutorizacion) {
  157. if (err) {
  158. console.error('Error consultando autorización:', err)
  159. reject(new Error(`Error al consultar autorización: ${err.message}`))
  160. } else {
  161. console.log('Estado consultado exitosamente')
  162. console.log('Respuesta completa del SRI:', JSON.stringify(result, null, 2))
  163. resolve(result)
  164. }
  165. })
  166. }
  167. }
  168. )
  169. })
  170. }
  171. /**
  172. * Hace polling del estado de autorización hasta que el documento sea procesado
  173. * @param accessKey Clave de acceso del documento
  174. * @param ambiente 'pruebas' o 'produccion'
  175. * @param maxAttempts Número máximo de intentos
  176. * @param delayMs Delay entre intentos en milisegundos
  177. * @returns Respuesta final de autorización
  178. */
  179. export async function longPollDoc({
  180. accessKey,
  181. ambiente = 'pruebas',
  182. maxAttempts = 10,
  183. delayMs = 3000,
  184. }: {
  185. accessKey: string
  186. ambiente?: 'pruebas' | 'produccion'
  187. maxAttempts?: number
  188. delayMs?: number
  189. }): Promise<RespuestaAutorizacion> {
  190. console.log(`Iniciando polling para clave de acceso: ${accessKey}`)
  191. for (let attempt = 1; attempt <= maxAttempts; attempt++) {
  192. console.log(`Intento ${attempt} de ${maxAttempts}...`)
  193. try {
  194. const result = await checkAuthorizationStatus(accessKey, ambiente)
  195. // La autorización puede venir como objeto o array
  196. let autorizacion = result.RespuestaAutorizacionComprobante.autorizaciones?.autorizacion
  197. if (Array.isArray(autorizacion)) {
  198. autorizacion = autorizacion[0]
  199. }
  200. if (autorizacion) {
  201. const estado = autorizacion.estado
  202. console.log(`Estado actual del documento: ${estado}`)
  203. // Si hay mensajes, mostrarlos con formato mejorado
  204. if (autorizacion.mensajes?.mensaje) {
  205. const mensajes = Array.isArray(autorizacion.mensajes.mensaje)
  206. ? autorizacion.mensajes.mensaje
  207. : [autorizacion.mensajes.mensaje]
  208. console.log('\n📨 Mensajes del SRI:')
  209. mensajes.forEach((msg: any, idx: number) => {
  210. console.log(`\n--- Mensaje ${idx + 1} ---`)
  211. // Formatear error con información detallada
  212. const errorFormateado = formatSRIError(
  213. msg.identificador,
  214. msg.mensaje,
  215. msg.informacionAdicional
  216. )
  217. console.log(errorFormateado)
  218. // Indicar tipo de error
  219. if (isCriticalError(msg.identificador)) {
  220. console.log('\n⚠️ ERROR CRÍTICO: Requiere intervención administrativa del SRI')
  221. } else if (isRecoverableError(msg.identificador)) {
  222. console.log('\n🔧 ERROR RECUPERABLE: Puedes corregir el documento y volver a enviarlo')
  223. }
  224. })
  225. console.log('\n' + '─'.repeat(50) + '\n')
  226. }
  227. // Estados finales
  228. if (estado === 'AUTORIZADO' || estado === 'NO AUTORIZADO' || estado === 'DEVUELTA') {
  229. if (estado === 'AUTORIZADO') {
  230. console.log(`✅ Estado final alcanzado: ${estado}`)
  231. if (autorizacion.numeroAutorizacion) {
  232. console.log(`🎫 Número de autorización: ${autorizacion.numeroAutorizacion}`)
  233. }
  234. } else {
  235. console.log(`Estado final alcanzado: ${estado}`)
  236. }
  237. return result
  238. } else {
  239. console.log(`⏳ Documento en procesamiento (estado: ${estado})`)
  240. }
  241. } else {
  242. console.log('⚠️ No se encontró información de autorización en la respuesta')
  243. console.log('Número de comprobantes:', result.RespuestaAutorizacionComprobante.numeroComprobantes)
  244. }
  245. // Esperar antes del siguiente intento
  246. if (attempt < maxAttempts) {
  247. console.log(`Esperando ${delayMs}ms antes del siguiente intento...`)
  248. await new Promise((resolve) => setTimeout(resolve, delayMs))
  249. }
  250. } catch (error) {
  251. console.error(`Error en intento ${attempt}:`, error)
  252. // Si es el último intento, lanzar el error
  253. if (attempt === maxAttempts) {
  254. throw error
  255. }
  256. // Esperar antes de reintentar
  257. await new Promise((resolve) => setTimeout(resolve, delayMs))
  258. }
  259. }
  260. throw new Error(
  261. `No se pudo obtener el estado del documento después de ${maxAttempts} intentos`
  262. )
  263. }