Browse Source

initial xml sending implementation

Matthew Trejo 1 month ago
parent
commit
a5c0ec29e3

+ 323 - 17
package-lock.json

@@ -27,6 +27,7 @@
         "react": "19.2.0",
         "react-dom": "19.2.0",
         "react-dropzone": "^14.3.8",
+        "soap": "^1.6.0",
         "sonner": "^2.0.7",
         "tailwind-merge": "^3.3.1",
         "zod": "^4.1.12"
@@ -36,6 +37,7 @@
         "@types/node": "^20",
         "@types/react": "^19",
         "@types/react-dom": "^19",
+        "@types/soap": "^0.18.0",
         "eslint": "^9",
         "eslint-config-next": "16.0.1",
         "prisma": "^6.18.0",
@@ -1225,6 +1227,18 @@
         "node": ">= 10"
       }
     },
+    "node_modules/@noble/hashes": {
+      "version": "1.8.0",
+      "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
+      "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
+      "license": "MIT",
+      "engines": {
+        "node": "^14.21.3 || >=16"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
     "node_modules/@nodelib/fs.scandir": {
       "version": "2.1.5",
       "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -1273,6 +1287,15 @@
         "node": ">=12.4.0"
       }
     },
+    "node_modules/@paralleldrive/cuid2": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz",
+      "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==",
+      "license": "MIT",
+      "dependencies": {
+        "@noble/hashes": "^1.1.5"
+      }
+    },
     "node_modules/@prisma/client": {
       "version": "6.18.0",
       "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.18.0.tgz",
@@ -2509,6 +2532,16 @@
         "@types/react": "^19.2.0"
       }
     },
+    "node_modules/@types/soap": {
+      "version": "0.18.0",
+      "resolved": "https://registry.npmjs.org/@types/soap/-/soap-0.18.0.tgz",
+      "integrity": "sha512-D1jn9hj2e+9xNaxgPSSUNVKnzdm1LzSHlbAfdJa3VUMprbp7fvb3QxDkK7HaNdsWomWECzJAFXEpfN+D+EHheA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
     "node_modules/@typescript-eslint/eslint-plugin": {
       "version": "8.46.2",
       "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.2.tgz",
@@ -3079,6 +3112,24 @@
         "win32"
       ]
     },
+    "node_modules/@xmldom/is-dom-node": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@xmldom/is-dom-node/-/is-dom-node-1.0.1.tgz",
+      "integrity": "sha512-CJDxIgE5I0FH+ttq/Fxy6nRpxP70+e2O048EPe85J2use3XKdatVM7dDVvFNjQudd9B49NPoZ+8PG49zj4Er8Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 16"
+      }
+    },
+    "node_modules/@xmldom/xmldom": {
+      "version": "0.8.11",
+      "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz",
+      "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
     "node_modules/acorn": {
       "version": "8.15.0",
       "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
@@ -3324,6 +3375,12 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/asap": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
+      "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
+      "license": "MIT"
+    },
     "node_modules/ast-types-flow": {
       "version": "0.0.8",
       "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz",
@@ -3341,6 +3398,12 @@
         "node": ">= 0.4"
       }
     },
+    "node_modules/asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+      "license": "MIT"
+    },
     "node_modules/attr-accept": {
       "version": "2.2.5",
       "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz",
@@ -3376,6 +3439,29 @@
         "node": ">=4"
       }
     },
+    "node_modules/axios": {
+      "version": "1.13.1",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.1.tgz",
+      "integrity": "sha512-hU4EGxxt+j7TQijx1oYdAjw4xuIp1wRQSsbMFwSthCWeBQur1eF+qJ5iQ5sN3Tw8YRzQNKb8jszgBdMDVqwJcw==",
+      "license": "MIT",
+      "dependencies": {
+        "follow-redirects": "^1.15.6",
+        "form-data": "^4.0.4",
+        "proxy-from-env": "^1.1.0"
+      }
+    },
+    "node_modules/axios-ntlm": {
+      "version": "1.4.6",
+      "resolved": "https://registry.npmjs.org/axios-ntlm/-/axios-ntlm-1.4.6.tgz",
+      "integrity": "sha512-4nR5cbVEBfPMTFkd77FEDpDuaR205JKibmrkaQyNwGcCx0szWNpRZaL0jZyMx4+mVY2PXHjRHuJafv9Oipl0Kg==",
+      "license": "MIT",
+      "dependencies": {
+        "axios": "^1.12.2",
+        "des.js": "^1.1.0",
+        "dev-null": "^0.1.1",
+        "js-md4": "^0.3.2"
+      }
+    },
     "node_modules/axobject-query": {
       "version": "4.1.0",
       "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
@@ -3513,7 +3599,6 @@
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
       "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "es-errors": "^1.3.0",
@@ -3660,6 +3745,18 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/combined-stream": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+      "license": "MIT",
+      "dependencies": {
+        "delayed-stream": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
     "node_modules/concat-map": {
       "version": "0.0.1",
       "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -3778,7 +3875,6 @@
       "version": "4.4.3",
       "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
       "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "ms": "^2.1.3"
@@ -3852,6 +3948,25 @@
       "devOptional": true,
       "license": "MIT"
     },
+    "node_modules/delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/des.js": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz",
+      "integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==",
+      "license": "MIT",
+      "dependencies": {
+        "inherits": "^2.0.1",
+        "minimalistic-assert": "^1.0.0"
+      }
+    },
     "node_modules/destr": {
       "version": "2.0.5",
       "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz",
@@ -3875,6 +3990,22 @@
       "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
       "license": "MIT"
     },
+    "node_modules/dev-null": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/dev-null/-/dev-null-0.1.1.tgz",
+      "integrity": "sha512-nMNZG0zfMgmdv8S5O0TM5cpwNbGKRGPCxVsr0SmA3NZZy9CYBbuNLL0PD3Acx9e5LIUgwONXtM9kM6RlawPxEQ==",
+      "license": "MIT"
+    },
+    "node_modules/dezalgo": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz",
+      "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==",
+      "license": "ISC",
+      "dependencies": {
+        "asap": "^2.0.0",
+        "wrappy": "1"
+      }
+    },
     "node_modules/doctrine": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
@@ -3905,7 +4036,6 @@
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
       "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "call-bind-apply-helpers": "^1.0.1",
@@ -4048,7 +4178,6 @@
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
       "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">= 0.4"
@@ -4058,7 +4187,6 @@
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
       "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">= 0.4"
@@ -4096,7 +4224,6 @@
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
       "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "es-errors": "^1.3.0"
@@ -4109,7 +4236,6 @@
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
       "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "es-errors": "^1.3.0",
@@ -4784,6 +4910,26 @@
       "dev": true,
       "license": "ISC"
     },
+    "node_modules/follow-redirects": {
+      "version": "1.15.11",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
+      "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/RubenVerborgh"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=4.0"
+      },
+      "peerDependenciesMeta": {
+        "debug": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/for-each": {
       "version": "0.3.5",
       "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
@@ -4800,11 +4946,43 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/form-data": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
+      "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
+      "license": "MIT",
+      "dependencies": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.8",
+        "es-set-tostringtag": "^2.1.0",
+        "hasown": "^2.0.2",
+        "mime-types": "^2.1.12"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/formidable": {
+      "version": "3.5.4",
+      "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz",
+      "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==",
+      "license": "MIT",
+      "dependencies": {
+        "@paralleldrive/cuid2": "^2.2.2",
+        "dezalgo": "^1.0.4",
+        "once": "^1.4.0"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      },
+      "funding": {
+        "url": "https://ko-fi.com/tunnckoCore/commissions"
+      }
+    },
     "node_modules/function-bind": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
       "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
-      "dev": true,
       "license": "MIT",
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
@@ -4865,7 +5043,6 @@
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
       "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "call-bind-apply-helpers": "^1.0.2",
@@ -4899,7 +5076,6 @@
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
       "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "dunder-proto": "^1.0.1",
@@ -4909,6 +5085,18 @@
         "node": ">= 0.4"
       }
     },
+    "node_modules/get-stream": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
+      "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/get-symbol-description": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz",
@@ -5005,7 +5193,6 @@
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
       "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">= 0.4"
@@ -5084,7 +5271,6 @@
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
       "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">= 0.4"
@@ -5097,7 +5283,6 @@
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
       "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "has-symbols": "^1.0.3"
@@ -5113,7 +5298,6 @@
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
       "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "function-bind": "^1.1.2"
@@ -5176,6 +5360,12 @@
         "node": ">=0.8.19"
       }
     },
+    "node_modules/inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+      "license": "ISC"
+    },
     "node_modules/internal-slot": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
@@ -5648,6 +5838,12 @@
         "jiti": "lib/jiti-cli.mjs"
       }
     },
+    "node_modules/js-md4": {
+      "version": "0.3.2",
+      "resolved": "https://registry.npmjs.org/js-md4/-/js-md4-0.3.2.tgz",
+      "integrity": "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA==",
+      "license": "MIT"
+    },
     "node_modules/js-tokens": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -6051,6 +6247,12 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/lodash": {
+      "version": "4.17.21",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+      "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+      "license": "MIT"
+    },
     "node_modules/lodash.merge": {
       "version": "4.6.2",
       "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@@ -6103,7 +6305,6 @@
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
       "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">= 0.4"
@@ -6133,6 +6334,33 @@
         "node": ">=8.6"
       }
     },
+    "node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/minimalistic-assert": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
+      "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
+      "license": "ISC"
+    },
     "node_modules/minimatch": {
       "version": "3.1.2",
       "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -6160,7 +6388,6 @@
       "version": "2.1.3",
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
       "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
-      "dev": true,
       "license": "MIT"
     },
     "node_modules/nanoid": {
@@ -6466,6 +6693,15 @@
       "devOptional": true,
       "license": "MIT"
     },
+    "node_modules/once": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+      "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+      "license": "ISC",
+      "dependencies": {
+        "wrappy": "1"
+      }
+    },
     "node_modules/optionator": {
       "version": "0.9.4",
       "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -6705,6 +6941,12 @@
         "react-is": "^16.13.1"
       }
     },
+    "node_modules/proxy-from-env": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+      "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+      "license": "MIT"
+    },
     "node_modules/punycode": {
       "version": "2.3.1",
       "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -7066,6 +7308,12 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/sax": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz",
+      "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==",
+      "license": "ISC"
+    },
     "node_modules/scheduler": {
       "version": "0.27.0",
       "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
@@ -7286,6 +7534,27 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/soap": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/soap/-/soap-1.6.0.tgz",
+      "integrity": "sha512-koOlNMAONSSVP38WakXEWz3WaYFupJJ08eicwrIvQsv9k2Qwz5JLLS6COqJVpIVCwffcqf8InMs+NYPw1bLOjA==",
+      "license": "MIT",
+      "dependencies": {
+        "axios": "^1.12.2",
+        "axios-ntlm": "^1.4.6",
+        "debug": "^4.4.3",
+        "formidable": "^3.5.4",
+        "get-stream": "^6.0.1",
+        "lodash": "^4.17.21",
+        "sax": "^1.4.1",
+        "strip-bom": "^3.0.0",
+        "whatwg-mimetype": "4.0.0",
+        "xml-crypto": "^6.1.2"
+      },
+      "engines": {
+        "node": ">=14.17.0"
+      }
+    },
     "node_modules/sonner": {
       "version": "2.0.7",
       "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz",
@@ -7443,7 +7712,6 @@
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
       "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=4"
@@ -7951,6 +8219,15 @@
         }
       }
     },
+    "node_modules/whatwg-mimetype": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
+      "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      }
+    },
     "node_modules/which": {
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -8066,6 +8343,35 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/wrappy": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+      "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+      "license": "ISC"
+    },
+    "node_modules/xml-crypto": {
+      "version": "6.1.2",
+      "resolved": "https://registry.npmjs.org/xml-crypto/-/xml-crypto-6.1.2.tgz",
+      "integrity": "sha512-leBOVQdVi8FvPJrMYoum7Ici9qyxfE4kVi+AkpUoYCSXaQF4IlBm1cneTK9oAxR61LpYxTx7lNcsnBIeRpGW2w==",
+      "license": "MIT",
+      "dependencies": {
+        "@xmldom/is-dom-node": "^1.0.1",
+        "@xmldom/xmldom": "^0.8.10",
+        "xpath": "^0.0.33"
+      },
+      "engines": {
+        "node": ">=16"
+      }
+    },
+    "node_modules/xpath": {
+      "version": "0.0.33",
+      "resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.33.tgz",
+      "integrity": "sha512-NNXnzrkDrAzalLhIUc01jO2mOzXGXh1JwPgkihcLLzw98c0WgYDmmjSh1Kl3wzaxSVWMuA+fe0WTWOBDWCBmNA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.6.0"
+      }
+    },
     "node_modules/yallist": {
       "version": "3.1.1",
       "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",

+ 2 - 0
package.json

@@ -28,6 +28,7 @@
     "react": "19.2.0",
     "react-dom": "19.2.0",
     "react-dropzone": "^14.3.8",
+    "soap": "^1.6.0",
     "sonner": "^2.0.7",
     "tailwind-merge": "^3.3.1",
     "zod": "^4.1.12"
@@ -37,6 +38,7 @@
     "@types/node": "^20",
     "@types/react": "^19",
     "@types/react-dom": "^19",
+    "@types/soap": "^0.18.0",
     "eslint": "^9",
     "eslint-config-next": "16.0.1",
     "prisma": "^6.18.0",

+ 70 - 0
src/app/api/poll-sri/route.ts

@@ -0,0 +1,70 @@
+import { NextRequest, NextResponse } from 'next/server'
+import { longPollDoc } from '@/lib/sri-utils'
+
+export async function POST(request: NextRequest) {
+  try {
+    const body = await request.json()
+    const { accessKey, ambiente } = body
+
+    if (!accessKey) {
+      return NextResponse.json(
+        { error: 'Clave de acceso es requerida' },
+        { status: 400 }
+      )
+    }
+
+    // Validar ambiente
+    const ambienteValido = ambiente === 'produccion' ? 'produccion' : 'pruebas'
+
+    // Verificar el estado del documento en el SRI
+    // longPollDoc hace polling hasta que el documento sea procesado
+    const result = await longPollDoc({
+      accessKey,
+      ambiente: ambienteValido
+    })
+
+    // Obtener la autorización del resultado
+    const autorizacion = result.RespuestaAutorizacionComprobante.autorizaciones?.autorizacion?.[0]
+
+    // Determinar el estado basado en la respuesta
+    let estado: string = 'verificando'
+
+    if (autorizacion) {
+      if (autorizacion.estado === 'AUTORIZADO') {
+        estado = 'autorizado'
+      } else if (autorizacion.estado === 'DEVUELTA') {
+        estado = 'devuelto'
+      } else if (autorizacion.estado === 'NO AUTORIZADO') {
+        estado = 'no_autorizado'
+      }
+    }
+
+    // Formatear mensajes
+    const mensajes = autorizacion?.mensajes?.mensaje?.map(m => ({
+      identificador: m.identificador,
+      mensaje: m.mensaje,
+      tipo: m.tipo as "INFORMATIVO" | "ADVERTENCIA" | "ERROR",
+      informacionAdicional: m.informacionAdicional
+    })) || []
+
+    return NextResponse.json({
+      success: true,
+      estado,
+      numeroAutorizacion: autorizacion?.numeroAutorizacion,
+      fechaAutorizacion: autorizacion?.fechaAutorizacion,
+      ambiente: autorizacion?.ambiente,
+      comprobante: autorizacion?.comprobante,
+      mensajes
+    })
+
+  } catch (error) {
+    console.error('Error verificando estado en el SRI:', error)
+    return NextResponse.json(
+      {
+        error: 'Error al verificar el estado en el SRI',
+        details: error instanceof Error ? error.message : 'Error desconocido'
+      },
+      { status: 500 }
+    )
+  }
+}

+ 39 - 0
src/app/api/send-to-sri/route.ts

@@ -0,0 +1,39 @@
+import { NextRequest, NextResponse } from 'next/server'
+import { sendDocToSRI } from '@/lib/sri-utils'
+
+export async function POST(request: NextRequest) {
+  try {
+    const body = await request.json()
+    const { signedXml, ambiente } = body
+
+    if (!signedXml) {
+      return NextResponse.json(
+        { error: 'XML firmado es requerido' },
+        { status: 400 }
+      )
+    }
+
+    // Validar ambiente
+    const ambienteValido = ambiente === 'produccion' ? 'produccion' : 'pruebas'
+
+    // Enviar el documento firmado al SRI
+    const result = await sendDocToSRI(signedXml, ambienteValido)
+
+    return NextResponse.json({
+      success: true,
+      estado: result.RespuestaRecepcionComprobante.estado,
+      mensaje: 'Documento enviado al SRI exitosamente',
+      respuesta: result
+    })
+
+  } catch (error) {
+    console.error('Error enviando documento al SRI:', error)
+    return NextResponse.json(
+      {
+        error: 'Error al enviar el documento al SRI',
+        details: error instanceof Error ? error.message : 'Error desconocido'
+      },
+      { status: 500 }
+    )
+  }
+}

+ 71 - 0
src/app/enviar-sri/page.tsx

@@ -0,0 +1,71 @@
+"use client"
+
+import { EnviarHeader } from "@/components/envio-sri/EnviarHeader"
+import { ParametrosSRIForm } from "@/components/envio-sri/ParametrosSRIForm"
+import { EnvioStatus } from "@/components/envio-sri/EnvioStatus"
+import { EnvioActions } from "@/components/envio-sri/EnvioActions"
+import { useEnvioSRI } from "@/hooks/envio-sri/useEnvioSRI"
+
+export default function EnviarSRIPage() {
+  const {
+    xmlFile,
+    p12File,
+    password,
+    signedXml,
+    accessKey,
+    ambiente,
+    isLoading,
+    estadoEnvio,
+    respuestaAutorizacion,
+    setXmlFile,
+    setP12File,
+    setPassword,
+    setAccessKey,
+    handleSign,
+    handleEnviarSRI,
+    handleVerificarEstado,
+    handleLoadSignedXml,
+    handleDownloadSigned,
+    handleDownloadAutorizado,
+    handleReset,
+  } = useEnvioSRI()
+
+  return (
+    <div className="container mx-auto max-w-5xl py-8 space-y-8">
+      <EnviarHeader />
+
+      <ParametrosSRIForm
+        xmlFile={xmlFile}
+        p12File={p12File}
+        password={password}
+        accessKey={accessKey}
+        ambiente={ambiente}
+        signedXml={signedXml}
+        isLoading={isLoading}
+        onXmlFileChange={setXmlFile}
+        onP12FileChange={setP12File}
+        onPasswordChange={setPassword}
+        onAccessKeyChange={setAccessKey}
+        onLoadSignedXml={handleLoadSignedXml}
+      />
+
+      <EnvioActions
+        xmlFile={xmlFile}
+        p12File={p12File}
+        password={password}
+        signedXml={signedXml}
+        accessKey={accessKey}
+        estadoEnvio={estadoEnvio}
+        isLoading={isLoading}
+        onSign={handleSign}
+        onEnviarSRI={handleEnviarSRI}
+        onVerificarEstado={handleVerificarEstado}
+        onDownloadSigned={handleDownloadSigned}
+        onDownloadAutorizado={handleDownloadAutorizado}
+        onReset={handleReset}
+      />
+
+      <EnvioStatus estadoEnvio={estadoEnvio} respuestaAutorizacion={respuestaAutorizacion} />
+    </div>
+  )
+}

+ 1 - 1
src/app/layout.tsx

@@ -19,7 +19,7 @@ const geistMono = Geist_Mono({
 
 export const metadata: Metadata = {
   title: "Sumire - shadcn/ui Demo",
-  description: "Proyecto demo de Next.js con shadcn/ui",
+  description: "yoshizawa se merecía mejor",
 };
 
 export default function RootLayout({

+ 7 - 2
src/components/app-sidebar.tsx

@@ -1,6 +1,6 @@
 "use client"
 
-import { ChevronUp, Home, FileText, Settings, FileSignature } from "lucide-react"
+import { ChevronUp, Home, FileText, Settings, FileSignature, Send } from "lucide-react"
 
 import {
   DropdownMenu,
@@ -41,6 +41,11 @@ const items = [
     url: "/firmar",
     icon: FileSignature,
   },
+  {
+    title: "Enviar al SRI (SOAP)",
+    url: "/enviar-sri",
+    icon: Send,
+  },
   {
     title: "Configuración",
     url: "/configuracion",
@@ -55,7 +60,7 @@ export function AppSidebar() {
         <SidebarMenu>
           <SidebarMenuItem>
             <SidebarMenuButton size="lg" className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground">
-              <div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
+              <div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-red-500 text-sidebar-primary-foreground">
                 <FileText className="size-4" />
               </div>
               <div className="grid flex-1 text-left text-sm leading-tight">

+ 19 - 0
src/components/envio-sri/EnviarHeader.tsx

@@ -0,0 +1,19 @@
+import { Send } from "lucide-react"
+
+export function EnviarHeader() {
+  return (
+    <div className="space-y-2">
+      <div className="flex items-center gap-3">
+        {/* <div className="rounded-lg bg-primary/10 p-2">
+          <Send className="h-6 w-6 text-primary" />
+        </div> */}
+        <div>
+          <h1 className="text-3xl font-bold">Enviar al SRI</h1>
+          <p className="text-muted-foreground">
+            Envía tus documentos electrónicos al Servicio de Rentas Internas
+          </p>
+        </div>
+      </div>
+    </div>
+  )
+}

+ 118 - 0
src/components/envio-sri/EnvioActions.tsx

@@ -0,0 +1,118 @@
+import { Button } from "@/components/ui/button"
+import { Download, Send, CheckCircle, RotateCcw, FileSignature } from "lucide-react"
+import type { EstadoEnvio } from "@/types/envio-sri"
+
+interface EnvioActionsProps {
+  xmlFile: File | null
+  p12File: File | null
+  password: string
+  signedXml: string | null
+  accessKey: string
+  estadoEnvio: EstadoEnvio
+  isLoading: boolean
+  onSign: () => Promise<boolean>
+  onEnviarSRI: () => Promise<boolean>
+  onVerificarEstado: () => Promise<boolean>
+  onDownloadSigned: () => void
+  onDownloadAutorizado: () => void
+  onReset: () => void
+}
+
+export function EnvioActions({
+  xmlFile,
+  p12File,
+  password,
+  signedXml,
+  accessKey,
+  estadoEnvio,
+  isLoading,
+  onSign,
+  onEnviarSRI,
+  onVerificarEstado,
+  onDownloadSigned,
+  onDownloadAutorizado,
+  onReset,
+}: EnvioActionsProps) {
+  const canSign = xmlFile && p12File && password && !signedXml
+  const canSend = signedXml && accessKey.length === 49
+  const canVerify = accessKey.length === 49
+  const isAutorizado = estadoEnvio === "autorizado"
+
+  return (
+    <div className="flex flex-wrap gap-3">
+      {/* Firmar XML */}
+      {!signedXml && (
+        <Button
+          onClick={onSign}
+          disabled={!canSign || isLoading}
+          className="flex items-center gap-2"
+        >
+          Firmar XML
+        </Button>
+      )}
+
+      {/* Enviar al SRI */}
+      {signedXml && estadoEnvio !== "autorizado" && (
+        <Button
+          onClick={onEnviarSRI}
+          disabled={!canSend || isLoading || estadoEnvio === "verificando" || estadoEnvio === "enviando"}
+          className="flex items-center gap-2"
+        >
+          Enviar al SRI
+        </Button>
+      )}
+
+      {/* Verificar Estado */}
+      {accessKey && estadoEnvio !== "idle" && estadoEnvio !== "autorizado" && (
+        <Button
+          variant="outline"
+          onClick={onVerificarEstado}
+          disabled={!canVerify || isLoading}
+          className="flex items-center gap-2"
+        >
+          <CheckCircle className="h-4 w-4" />
+          Verificar Estado
+        </Button>
+      )}
+
+      {/* Descargar XML Firmado */}
+      {/* {signedXml && (
+        <Button
+          variant="outline"
+          onClick={onDownloadSigned}
+          disabled={isLoading}
+          className="flex items-center gap-2"
+        >
+          <Download className="h-4 w-4" />
+          Descargar Firmado
+        </Button>
+      )} */}
+
+      {/* Descargar Comprobante Autorizado */}
+      {isAutorizado && (
+        <Button
+          variant="default"
+          onClick={onDownloadAutorizado}
+          disabled={isLoading}
+          className="flex items-center gap-2"
+        >
+          <Download className="h-4 w-4" />
+          Descargar Autorizado
+        </Button>
+      )}
+
+      {/* Reset */}
+      {(signedXml || estadoEnvio !== "idle") && (
+        <Button
+          variant="ghost"
+          onClick={onReset}
+          disabled={isLoading}
+          className="flex items-center gap-2 ml-auto"
+        >
+          <RotateCcw className="h-4 w-4" />
+          Probar con otro archivo
+        </Button>
+      )}
+    </div>
+  )
+}

+ 171 - 0
src/components/envio-sri/EnvioStatus.tsx

@@ -0,0 +1,171 @@
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
+import { Badge } from "@/components/ui/badge"
+import { CheckCircle2, XCircle, AlertCircle, Clock, Send, Loader2 } from "lucide-react"
+import type { EstadoEnvio, RespuestaVerificacionSRI } from "@/types/envio-sri"
+import { Alert, AlertDescription } from "@/components/ui/alert"
+
+interface EnvioStatusProps {
+  estadoEnvio: EstadoEnvio
+  respuestaAutorizacion: RespuestaVerificacionSRI | null
+}
+
+export function EnvioStatus({ estadoEnvio, respuestaAutorizacion }: EnvioStatusProps) {
+  if (estadoEnvio === "idle") return null
+
+  const getEstadoInfo = () => {
+    switch (estadoEnvio) {
+      case "preparando":
+        return {
+          icon: <Loader2 className="h-5 w-5 animate-spin text-blue-500" />,
+          title: "Preparando documento",
+          description: "Firmando el XML...",
+          variant: "default" as const,
+        }
+      case "enviando":
+        return {
+          icon: <Send className="h-5 w-5 text-blue-500" />,
+          title: "Enviando al SRI",
+          description: "Enviando documento a los servidores del SRI...",
+          variant: "default" as const,
+        }
+      case "verificando":
+        return {
+          icon: <Clock className="h-5 w-5 text-yellow-500" />,
+          title: "Verificando estado",
+          description: "Consultando el estado del documento en el SRI...",
+          variant: "default" as const,
+        }
+      case "autorizado":
+        return {
+          icon: <CheckCircle2 className="h-5 w-5 text-green-500" />,
+          title: "Documento Autorizado",
+          description: "El documento ha sido autorizado exitosamente por el SRI",
+          variant: "default" as const,
+        }
+      case "devuelto":
+        return {
+          icon: <AlertCircle className="h-5 w-5 text-orange-500" />,
+          title: "Documento Devuelto",
+          description: "El documento fue devuelto por el SRI. Revisa los mensajes.",
+          variant: "destructive" as const,
+        }
+      case "no_autorizado":
+        return {
+          icon: <XCircle className="h-5 w-5 text-red-500" />,
+          title: "Documento No Autorizado",
+          description: "El documento no fue autorizado por el SRI",
+          variant: "destructive" as const,
+        }
+      case "error":
+        return {
+          icon: <XCircle className="h-5 w-5 text-red-500" />,
+          title: "Error",
+          description: "Ocurrió un error durante el proceso",
+          variant: "destructive" as const,
+        }
+      default:
+        return {
+          icon: <Clock className="h-5 w-5" />,
+          title: "Procesando",
+          description: "Procesando solicitud...",
+          variant: "default" as const,
+        }
+    }
+  }
+
+  const estadoInfo = getEstadoInfo()
+
+  return (
+    <Card>
+      <CardHeader>
+        <CardTitle className="flex items-center gap-2">
+          {estadoInfo.icon}
+          {estadoInfo.title}
+        </CardTitle>
+        <CardDescription>{estadoInfo.description}</CardDescription>
+      </CardHeader>
+      <CardContent className="space-y-4">
+        {/* Información de autorización */}
+        {respuestaAutorizacion && (
+          <div className="space-y-3">
+            {respuestaAutorizacion.numeroAutorizacion && (
+              <div className="space-y-1">
+                <p className="text-sm font-medium">Número de Autorización</p>
+                <p className="text-sm font-mono bg-muted p-2 rounded">
+                  {respuestaAutorizacion.numeroAutorizacion}
+                </p>
+              </div>
+            )}
+
+            {respuestaAutorizacion.fechaAutorizacion && (
+              <div className="space-y-1">
+                <p className="text-sm font-medium">Fecha de Autorización</p>
+                <p className="text-sm text-muted-foreground">
+                  {respuestaAutorizacion.fechaAutorizacion}
+                </p>
+              </div>
+            )}
+
+            {respuestaAutorizacion.ambiente && (
+              <div className="space-y-1">
+                <p className="text-sm font-medium">Ambiente</p>
+                <Badge variant={respuestaAutorizacion.ambiente === "PRODUCCIÓN" ? "default" : "secondary"}>
+                  {respuestaAutorizacion.ambiente}
+                </Badge>
+              </div>
+            )}
+
+            {/* Mensajes del SRI */}
+            {respuestaAutorizacion.mensajes && respuestaAutorizacion.mensajes.length > 0 && (
+              <div className="space-y-2">
+                <p className="text-sm font-medium">Mensajes del SRI</p>
+                <div className="space-y-2">
+                  {respuestaAutorizacion.mensajes.map((mensaje, index) => (
+                    <Alert
+                      key={index}
+                      variant={mensaje.tipo === "ERROR" ? "destructive" : "default"}
+                    >
+                      <AlertDescription className="space-y-1">
+                        <div className="flex items-start gap-2">
+                          <Badge
+                            variant={
+                              mensaje.tipo === "ERROR"
+                                ? "destructive"
+                                : mensaje.tipo === "ADVERTENCIA"
+                                ? "secondary"
+                                : "outline"
+                            }
+                            className="text-xs"
+                          >
+                            {mensaje.tipo}
+                          </Badge>
+                          <div className="flex-1 space-y-1">
+                            <p className="text-sm font-medium">
+                              {mensaje.identificador}: {mensaje.mensaje}
+                            </p>
+                            {mensaje.informacionAdicional && (
+                              <p className="text-xs text-muted-foreground">
+                                {mensaje.informacionAdicional}
+                              </p>
+                            )}
+                          </div>
+                        </div>
+                      </AlertDescription>
+                    </Alert>
+                  ))}
+                </div>
+              </div>
+            )}
+
+            {/* Error */}
+            {respuestaAutorizacion.error && (
+              <Alert variant="destructive">
+                <AlertDescription>{respuestaAutorizacion.error}</AlertDescription>
+              </Alert>
+            )}
+          </div>
+        )}
+      </CardContent>
+    </Card>
+  )
+}

+ 124 - 0
src/components/envio-sri/ParametrosSRIForm.tsx

@@ -0,0 +1,124 @@
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
+import { Label } from "@/components/ui/label"
+import { Input } from "@/components/ui/input"
+import { Dropzone } from "@/components/ui/shadcn-io/dropzone"
+import { FileText, FileKey, Check, Upload, Key } from "lucide-react"
+import { toast } from "sonner"
+import { Separator } from "@/components/ui/separator"
+import { Button } from "@/components/ui/button"
+import { Badge } from "@/components/ui/badge"
+
+interface ParametrosSRIFormProps {
+  xmlFile: File | null
+  p12File: File | null
+  password: string
+  accessKey: string
+  ambiente: '1' | '2'
+  signedXml: string | null
+  isLoading: boolean
+  onXmlFileChange: (file: File | null) => void
+  onP12FileChange: (file: File | null) => void
+  onPasswordChange: (password: string) => void
+  onAccessKeyChange: (key: string) => void
+  onLoadSignedXml: (file: File) => void
+}
+
+export function ParametrosSRIForm({
+  xmlFile,
+  p12File,
+  password,
+  accessKey,
+  ambiente,
+  signedXml,
+  isLoading,
+  onXmlFileChange,
+  onP12FileChange,
+  onPasswordChange,
+  onAccessKeyChange,
+  onLoadSignedXml,
+}: ParametrosSRIFormProps) {
+  return (
+    <Card>
+      <CardHeader>
+        <CardTitle>Documentos y Parámetros</CardTitle>
+        <CardDescription>
+          Carga tu XML firmado y parámetros necesarios se llenan automáticamente
+        </CardDescription>
+      </CardHeader>
+      <CardContent className="space-y-6">
+        {/* Archivo XML Firmado */}
+        <div className="space-y-2">
+          <Dropzone
+            accept={{
+              "text/xml": [".xml"],
+              "application/xml": [".xml"],
+            }}
+            maxFiles={1}
+            onDrop={(acceptedFiles) => {
+              if (acceptedFiles.length > 0) {
+                onLoadSignedXml(acceptedFiles[0])
+              }
+            }}
+            disabled={isLoading}
+            className="border-2 border-dashed rounded-lg p-6"
+          >
+            <div className="flex flex-col items-center gap-2 text-center">
+              <div className="rounded-full bg-primary/10 p-3">
+                <Upload className="h-5 w-5 text-primary" />
+              </div>
+              <div className="space-y-1">
+                <p className="text-sm font-medium">
+                  {signedXml && xmlFile ? (
+                    <span className="flex items-center gap-2">
+                      <Check className="h-4 w-4 text-green-500" />
+                      {xmlFile.name}
+                    </span>
+                  ) : (
+                    "Arrastra un XML firmado aquí"
+                  )}
+                </p>
+              </div>
+            </div>
+          </Dropzone>
+        </div>
+        <Separator />
+
+        {/* Información del XML (solo lectura) */}
+        {signedXml && accessKey && (
+          <div className="space-y-4 p-4 bg-muted/50 rounded-lg">
+            <div className="space-y-2">
+              <Label htmlFor="accessKey" className="flex items-center gap-2">
+                <Key className="h-4 w-4" />
+                Clave de Acceso (extraída del XML)
+              </Label>
+              <Input
+                id="accessKey"
+                type="text"
+                value={accessKey}
+                disabled
+                className="font-mono bg-background"
+              />
+              <p className="text-xs text-muted-foreground">
+                La clave de acceso fue extraída automáticamente del XML
+              </p>
+            </div>
+
+            <div className="space-y-2">
+              <Label className="flex items-center gap-2">
+                Ambiente
+              </Label>
+              <div className="flex items-center gap-2">
+                <Badge variant={ambiente === '2' ? 'default' : 'secondary'}>
+                  {ambiente === '1' ? 'Pruebas' : 'Producción'}
+                </Badge>
+                <p className="text-xs text-muted-foreground">
+                  Detectado del XML
+                </p>
+              </div>
+            </div>
+          </div>
+        )}
+      </CardContent>
+    </Card>
+  )
+}

+ 2 - 2
src/components/factura/InfoTributariaForm.tsx

@@ -57,13 +57,13 @@ export function InfoTributariaForm({ infoTributaria, onChange }: InfoTributariaF
                   ))}
                 </SelectContent>
               </Select>
-              <Button
+              {/* <Button
                 variant="outline"
                 size="sm"
                 onClick={() => window.open('/configuracion', '_blank')}
               >
                 <ExternalLink className="w-4 h-4" />
-              </Button>
+              </Button> */}
             </div>
           )}
         </div>

+ 58 - 0
src/components/ui/alert.tsx

@@ -0,0 +1,58 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+import { cn } from "@/lib/utils"
+
+const alertVariants = cva(
+  "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
+  {
+    variants: {
+      variant: {
+        default: "bg-background text-foreground",
+        destructive:
+          "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
+      },
+    },
+    defaultVariants: {
+      variant: "default",
+    },
+  }
+)
+
+const Alert = React.forwardRef<
+  HTMLDivElement,
+  React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
+>(({ className, variant, ...props }, ref) => (
+  <div
+    ref={ref}
+    role="alert"
+    className={cn(alertVariants({ variant }), className)}
+    {...props}
+  />
+))
+Alert.displayName = "Alert"
+
+const AlertTitle = React.forwardRef<
+  HTMLParagraphElement,
+  React.HTMLAttributes<HTMLHeadingElement>
+>(({ className, ...props }, ref) => (
+  <h5
+    ref={ref}
+    className={cn("mb-1 font-medium leading-none tracking-tight", className)}
+    {...props}
+  />
+))
+AlertTitle.displayName = "AlertTitle"
+
+const AlertDescription = React.forwardRef<
+  HTMLParagraphElement,
+  React.HTMLAttributes<HTMLParagraphElement>
+>(({ className, ...props }, ref) => (
+  <div
+    ref={ref}
+    className={cn("text-sm [&_p]:leading-relaxed", className)}
+    {...props}
+  />
+))
+AlertDescription.displayName = "AlertDescription"
+
+export { Alert, AlertTitle, AlertDescription }

+ 255 - 0
src/hooks/envio-sri/useEnvioSRI.ts

@@ -0,0 +1,255 @@
+import { useState } from "react"
+import { toast } from "sonner"
+import type { EstadoEnvio, RespuestaVerificacionSRI } from "@/types/envio-sri"
+import { extractInfoFromXml } from "@/lib/xml-utils"
+
+export function useEnvioSRI() {
+  const [xmlFile, setXmlFile] = useState<File | null>(null)
+  const [p12File, setP12File] = useState<File | null>(null)
+  const [password, setPassword] = useState("")
+  const [signedXml, setSignedXml] = useState<string | null>(null)
+  const [accessKey, setAccessKey] = useState<string>("")
+  const [ambiente, setAmbiente] = useState<'1' | '2'>('1') // 1=Pruebas, 2=Producción
+
+  const [isLoading, setIsLoading] = useState(false)
+  const [estadoEnvio, setEstadoEnvio] = useState<EstadoEnvio>("idle")
+  const [respuestaAutorizacion, setRespuestaAutorizacion] = useState<RespuestaVerificacionSRI | null>(null)
+
+  // Paso 1: Firmar el XML (opcional si ya viene firmado)
+  const handleSign = async () => {
+    if (!xmlFile || !p12File || !password) {
+      toast.error("Por favor completa todos los campos para firmar")
+      return false
+    }
+
+    setIsLoading(true)
+    setSignedXml(null)
+
+    try {
+      const formData = new FormData()
+      formData.append("xml", xmlFile)
+      formData.append("p12", p12File)
+      formData.append("password", password)
+
+      const response = await fetch("/api/sign-invoice", {
+        method: "POST",
+        body: formData,
+      })
+
+      const data = await response.json()
+
+      if (!response.ok) {
+        throw new Error(data.details || data.error || "Error al firmar")
+      }
+
+      setSignedXml(data.signedXml)
+      toast.success("XML firmado exitosamente")
+      return true
+    } catch (error) {
+      console.error(error)
+      toast.error(error instanceof Error ? error.message : "Error al firmar el XML")
+      return false
+    } finally {
+      setIsLoading(false)
+    }
+  }
+
+  // Paso 2: Enviar al SRI
+  const handleEnviarSRI = async () => {
+    if (!signedXml) {
+      toast.error("Primero debes firmar el XML o cargar uno firmado")
+      return false
+    }
+
+    if (!accessKey.trim()) {
+      toast.error("Debes ingresar la clave de acceso del documento")
+      return false
+    }
+
+    setIsLoading(true)
+    setEstadoEnvio("enviando")
+
+    try {
+      const response = await fetch("/api/send-to-sri", {
+        method: "POST",
+        headers: {
+          "Content-Type": "application/json",
+        },
+        body: JSON.stringify({ signedXml }),
+      })
+
+      const data = await response.json()
+
+      if (!response.ok) {
+        throw new Error(data.details || data.error || "Error al enviar")
+      }
+
+      toast.success("Documento enviado al SRI, verificando estado...")
+      setEstadoEnvio("verificando")
+
+      // Automáticamente verificar el estado
+      await handleVerificarEstado()
+      return true
+    } catch (error) {
+      console.error(error)
+      toast.error(error instanceof Error ? error.message : "Error al enviar al SRI")
+      setEstadoEnvio("error")
+      return false
+    } finally {
+      setIsLoading(false)
+    }
+  }
+
+  // Paso 3: Verificar estado
+  const handleVerificarEstado = async () => {
+    if (!accessKey.trim()) {
+      toast.error("Debes ingresar la clave de acceso")
+      return false
+    }
+
+    setIsLoading(true)
+    setEstadoEnvio("verificando")
+
+    try {
+      const response = await fetch("/api/poll-sri", {
+        method: "POST",
+        headers: {
+          "Content-Type": "application/json",
+        },
+        body: JSON.stringify({ accessKey }),
+      })
+
+      const data: RespuestaVerificacionSRI = await response.json()
+
+      if (!response.ok) {
+        throw new Error(data.details || data.error || "Error al verificar")
+      }
+
+      setRespuestaAutorizacion(data)
+      setEstadoEnvio(data.estado)
+
+      if (data.estado === "autorizado") {
+        toast.success("¡Documento autorizado por el SRI!")
+      } else if (data.estado === "devuelto") {
+        toast.warning("Documento devuelto por el SRI")
+      } else if (data.estado === "no_autorizado") {
+        toast.error("Documento no autorizado por el SRI")
+      }
+
+      return true
+    } catch (error) {
+      console.error(error)
+      toast.error(error instanceof Error ? error.message : "Error al verificar estado")
+      setEstadoEnvio("error")
+      return false
+    } finally {
+      setIsLoading(false)
+    }
+  }
+
+  // Cargar un XML firmado directamente
+  const handleLoadSignedXml = async (file: File) => {
+    try {
+      const content = await file.text()
+
+      // Extraer información del XML automáticamente
+      const info = extractInfoFromXml(content)
+
+      if (!info.claveAcceso) {
+        toast.error("No se pudo encontrar la clave de acceso en el XML")
+        return
+      }
+
+      // Validar que la clave de acceso tenga 49 dígitos
+      if (info.claveAcceso.length !== 49) {
+        toast.error("La clave de acceso debe tener 49 dígitos")
+        return
+      }
+
+      setSignedXml(content)
+      setXmlFile(file)
+      setAccessKey(info.claveAcceso)
+
+      // Establecer el ambiente si se encuentra en el XML
+      if (info.ambiente) {
+        setAmbiente(info.ambiente)
+      }
+
+      toast.success(`XML cargado: ${file.name}`)
+      toast.info(`Clave de acceso extraída: ${info.claveAcceso.substring(0, 10)}...`)
+    } catch (error) {
+      toast.error("Error al leer el archivo XML")
+    }
+  }
+
+  // Descargar XML firmado
+  const handleDownloadSigned = () => {
+    if (!signedXml) return
+
+    const blob = new Blob([signedXml], { type: "application/xml" })
+    const url = URL.createObjectURL(blob)
+    const a = document.createElement("a")
+    a.href = url
+    a.download = `factura_firmada_${new Date().getTime()}.xml`
+    document.body.appendChild(a)
+    a.click()
+    document.body.removeChild(a)
+    URL.revokeObjectURL(url)
+    toast.success("XML firmado descargado")
+  }
+
+  // Descargar comprobante autorizado
+  const handleDownloadAutorizado = () => {
+    if (!respuestaAutorizacion?.comprobante) return
+
+    const blob = new Blob([respuestaAutorizacion.comprobante], { type: "application/xml" })
+    const url = URL.createObjectURL(blob)
+    const a = document.createElement("a")
+    a.href = url
+    a.download = `factura_autorizada_${accessKey}.xml`
+    document.body.appendChild(a)
+    a.click()
+    document.body.removeChild(a)
+    URL.revokeObjectURL(url)
+    toast.success("Comprobante autorizado descargado")
+  }
+
+  // Reset
+  const handleReset = () => {
+    setXmlFile(null)
+    setP12File(null)
+    setPassword("")
+    setSignedXml(null)
+    setAccessKey("")
+    setEstadoEnvio("idle")
+    setRespuestaAutorizacion(null)
+  }
+
+  return {
+    // State
+    xmlFile,
+    p12File,
+    password,
+    signedXml,
+    accessKey,
+    ambiente,
+    isLoading,
+    estadoEnvio,
+    respuestaAutorizacion,
+
+    // Setters
+    setXmlFile,
+    setP12File,
+    setPassword,
+    setAccessKey,
+
+    // Actions
+    handleSign,
+    handleEnviarSRI,
+    handleVerificarEstado,
+    handleLoadSignedXml,
+    handleDownloadSigned,
+    handleDownloadAutorizado,
+    handleReset,
+  }
+}

+ 213 - 0
src/lib/sri-utils.ts

@@ -0,0 +1,213 @@
+import * as soap from 'soap'
+
+/**
+ * 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 RespuestaAutorizacion {
+  RespuestaAutorizacionComprobante: {
+    claveAccesoConsultada: string
+    numeroComprobantes: string
+    autorizaciones?: {
+      autorizacion: Array<{
+        estado: string
+        numeroAutorizacion?: string
+        fechaAutorizacion?: string
+        ambiente?: string
+        comprobante?: string
+        mensajes?: {
+          mensaje: Array<{
+            identificador: string
+            mensaje: string
+            informacionAdicional?: string
+            tipo: string
+          }>
+        }
+      }>
+    }
+  }
+}
+
+/**
+ * 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<RespuestaRecepcion> {
+  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))
+                reject(
+                  new Error(
+                    `El documento no fue recibido por el SRI. Estado: ${estado || 'DESCONOCIDO'}`
+                  )
+                )
+              } 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<RespuestaAutorizacion> {
+  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')
+              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<RespuestaAutorizacion> {
+  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)
+      const autorizacion = result.RespuestaAutorizacionComprobante.autorizaciones?.autorizacion?.[0]
+
+      if (autorizacion) {
+        const estado = autorizacion.estado
+
+        // Estados finales
+        if (estado === 'AUTORIZADO' || estado === 'NO AUTORIZADO' || estado === 'DEVUELTA') {
+          console.log(`Estado final alcanzado: ${estado}`)
+          return result
+        }
+      }
+
+      // 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`
+  )
+}

+ 52 - 0
src/lib/xml-utils.ts

@@ -0,0 +1,52 @@
+/**
+ * Extrae la clave de acceso de un XML de factura electrónica
+ * @param xmlContent Contenido del XML
+ * @returns Clave de acceso (49 dígitos) o null si no se encuentra
+ */
+export function extractAccessKeyFromXml(xmlContent: string): string | null {
+  try {
+    // Buscar el tag <claveAcceso> en el XML
+    const claveAccesoMatch = xmlContent.match(/<claveAcceso>(\d{49})<\/claveAcceso>/)
+
+    if (claveAccesoMatch && claveAccesoMatch[1]) {
+      return claveAccesoMatch[1]
+    }
+
+    return null
+  } catch (error) {
+    console.error('Error extrayendo clave de acceso:', error)
+    return null
+  }
+}
+
+/**
+ * Extrae el ambiente del XML (1=Pruebas, 2=Producción)
+ * @param xmlContent Contenido del XML
+ * @returns Ambiente o null si no se encuentra
+ */
+export function extractAmbienteFromXml(xmlContent: string): '1' | '2' | null {
+  try {
+    const ambienteMatch = xmlContent.match(/<ambiente>([12])<\/ambiente>/)
+
+    if (ambienteMatch && ambienteMatch[1]) {
+      return ambienteMatch[1] as '1' | '2'
+    }
+
+    return null
+  } catch (error) {
+    console.error('Error extrayendo ambiente:', error)
+    return null
+  }
+}
+
+/**
+ * Extrae información tributaria básica del XML
+ * @param xmlContent Contenido del XML
+ * @returns Objeto con información extraída
+ */
+export function extractInfoFromXml(xmlContent: string) {
+  return {
+    claveAcceso: extractAccessKeyFromXml(xmlContent),
+    ambiente: extractAmbienteFromXml(xmlContent),
+  }
+}

+ 60 - 0
src/types/envio-sri.ts

@@ -0,0 +1,60 @@
+export interface ParametrosEnvioSRI {
+  // Datos del documento
+  fecha: string // DD/MM/YYYY
+  tipoDocumento: string // "01" para factura
+  ruc: string
+  ambiente: "1" | "2" // 1=Pruebas, 2=Producción
+  establecimiento: string // 3 dígitos
+  puntoEmision: string // 3 dígitos
+  secuencial: string // 9 dígitos
+  codigoNumerico: string // 8 dígitos (generado aleatoriamente)
+  tipoEmision: "1" | "2" // 1=Normal, 2=Contingencia
+}
+
+export interface ClaveAcceso {
+  claveCompleta: string // 49 dígitos
+  digitoVerificador: string
+}
+
+export type EstadoEnvio =
+  | "idle"
+  | "preparando"
+  | "enviando"
+  | "verificando"
+  | "autorizado"
+  | "error"
+  | "devuelto"
+  | "no_autorizado"
+
+export interface RespuestaEnvioSRI {
+  success: boolean
+  estado?: string
+  claveAcceso?: string
+  numeroAutorizacion?: string
+  fechaAutorizacion?: string
+  mensajes?: Array<{
+    identificador: string
+    mensaje: string
+    tipo: "INFORMATIVO" | "ADVERTENCIA" | "ERROR"
+    informacionAdicional?: string
+  }>
+  error?: string
+  details?: string
+}
+
+export interface RespuestaVerificacionSRI {
+  success: boolean
+  estado: EstadoEnvio
+  numeroAutorizacion?: string
+  fechaAutorizacion?: string
+  ambiente?: string
+  comprobante?: string // XML del comprobante autorizado
+  mensajes?: Array<{
+    identificador: string
+    mensaje: string
+    tipo: "INFORMATIVO" | "ADVERTENCIA" | "ERROR"
+    informacionAdicional?: string
+  }>
+  error?: string
+  details?: string
+}