Intercambio y muestras
Después de la simulación, la certificación pide dos cosas más. El intercambio: el SII envía a tu empresa un set de documentos dirigidos a ella, y respondes con tres archivos firmados. Las muestras impresas: el PDF de cada documento que emitieron los sets y la simulación, subido como una revisión que comprueba el validador automático del SII. runCertification hace ambas; esta página explica cómo, y las funciones que hay debajo si las encadenas a mano.
Nada de esto se ha ejecutado todavía contra los dos portales. Cada
petición y cada texto sigue sin comprobarse; las
respuestas del intercambio validan contra los esquemas oficiales en las
pruebas, y las muestras impresas aún no las ha revisado el SII. La primera
corrida con una empresa inscrita debería conservar todos los artefactos que
escribe el almacén bajo intercambio/ y muestras/ como fixtures. Ver
Estado.
Uso básico#
import { readFile } from 'node:fs/promises'
import { runCertification, createExchangeAnswers, EXCHANGE_SET_ARTEFACT } from 'dte-sdk/portal'
import { readPrintedSamples, writePrintedSamples, findEnvelopeFiles } from 'dte-sdk/pdf'
// Tras la simulación la corrida descarga el set de intercambio del portal de intercambio, lo responde y lo confirma.
// Luego las muestras se generan desde los sobres que guardó el almacén y se suben al portal de muestras.
const samplesDir = './certification/samples'
await writePrintedSamples({
xmlFiles: await findEnvelopeFiles('./certification'),
outDir: samplesDir,
transferable: true,
})
const run = await runCertification(deps, { samples: () => readPrintedSamples(samplesDir) })
// run.stopped: 'compliance' una vez enviadas las muestras, o 'rejected' con los motivos del SII en run.detail
// No se pudo descargar el set: run.stopped es 'exchange' y run.detail nombra intercambio/set-intercambio.xml.
// Guarda el set a mano desde el portal de intercambio, y luego o lo pasas...
await runCertification(deps, { exchangeSetXml: await readFile('set-intercambio.xml', 'utf8') })
// ...o lo pones en ese artefacto del almacén y corres de nuevo
await deps.store.writeArtefact(EXCHANGE_SET_ARTEFACT, await readFile('set-intercambio.xml', 'utf8'))
// Solo las respuestas, para un set que ya tienes
const answers = createExchangeAnswers(setXml, {
companyRut: '76543210-3',
certificate,
contact: { name: 'Contacto', mail: 'contacto@example.test' },
})
answers.reception.fileName // 'respuesta-recepcion-envio.xml'
answers.reception.xml // firmado, una línea, UTF-8, sin declaración XMLEl intercambio#
El set#
El set de intercambio es el conjunto de documentos que el SII dirige a la empresa, un EnvioDTE. parseExchangeSet(setXml) lo lee con tolerancia, porque las empresas de prueba del propio SII no siempre tienen RUT válidos y el set no está bajo tu control:
- No se comprueba que el XML esté bien formado, y las etiquetas se leen por su nombre completo, así que un sobre con prefijo de espacio de nombres no es un sobre. La raíz es
EnvioDTE,EnvioDTEB,EnvioDTETrasladooEnvioBOLETA. - El id del set es el atributo
ID, si no un hijoID, si noSetDTE. Se leen los RUT de emisor, enviador y receptor de la carátula; y por documento su tipo (TipoDTEoTpoDoc), folio, fecha de emisión, emisor y receptor (los de la carátula cuando el documento no los nombra) y total. - Un número que no se puede leer vale 0 (
1e3se lee como 1000), una fecha ausente esnully un RUT ausente queda vacío. Los RUT no se validan: solo se les quitan los puntos y se sube su letra a mayúscula. - Solo se lee un
Documentosimple, o uno anidado dentro de un segundoDTE; cualquier otro tipo se lee como vacío. - Lanza
XML_PARSE_FAILEDcuando el parser se rinde o no hay raíz de sobre, yENVELOPE_EMPTYcuando el set no trae ningún documento. Nada más falla.
Las tres respuestas#
createExchangeAnswers(setXml, { companyRut, certificate, contact?, now? }) arma y firma las tres respuestas que espera la certificación. Cada una es una línea de UTF-8 sin declaración XML, y cada una viaja con el nombre de archivo que espera la subida del portal (EXCHANGE_ANSWER_FILES):
| Clave | Archivo | Contenido |
|---|---|---|
reception | respuesta-recepcion-envio.xml | La respuesta de recepción: el sobre recibido, y cada documento recibido cuando su receptor es tu empresa, si no marcado con el "error en RUT receptor" del SII |
receipts | envio-recibos.xml | Los recibos de la Ley 19.983: un Recibo por documento, cada uno firmado por el titular del certificado, dentro de un EnvioRecibos firmado |
approval | respuesta-aprobacion-comercial.xml | La aprobación comercial: cada documento aceptado cuando su receptor es tu empresa, si no rechazado con CodRchDsc -1 |
Validan contra los esquemas oficiales en las pruebas. Los ids llevan el reloj en milisegundos, y toda marca de tiempo es la hora local de Chile. El nombre del contacto se sanea; su teléfono aparece solo en los recibos. El resultado trae además set, el set de intercambio tal como se leyó.
Lanza los errores de parseExchangeSet, y INVALID_RUT (campo companyRut).
El paso de intercambio#
runExchange(deps, state, { setXml? }) es la etapa de intercambio. En orden:
- El set. El que diste (
setXml, que se guarda como artefacto), si no el que quedó guardado de una corrida anterior, si no se descarga del portal de intercambio y se guarda comointercambio/set-intercambio.xml. - Las respuestas se generan y se guardan como artefactos bajo
intercambio/. - Una etapa en el portal de intercambio (un calentamiento): se valida al usuario; si la empresa está en la etapa de intercambio, se suben las tres respuestas en orden y se confirma el intercambio. Cada respuesta del portal se guarda bajo
intercambio/. - El estado registra el intercambio y pasa la etapa a
printedSamples.
| Resultado | Significado |
|---|---|
done | Las respuestas se subieron y el intercambio se confirmó |
alreadyDone | El estado dice que se confirmó, o el portal dice que el intercambio ya se completó (estado P90) |
setUnavailable | No se pudo descargar el set. detail nombra el artefacto que hay que llenar a mano |
notInExchange | El portal dice que la empresa no está en la etapa de intercambio. detail es su frase |
Un set que no se puede descargar, por la razón que sea, no lanza: detiene la corrida con exchange y pide el set a mano. La página que volvió, cuando la hay, se guarda como intercambio/pfe-download-error.html. Guarda el set en el artefacto EXCHANGE_SET_ARTEFACT (o pasa exchangeSetXml) y corre de nuevo.
Las respuestas se generan y se suben de nuevo en cada corrida que llega al portal; solo se guarda el set.
Las operaciones del portal de intercambio#
Todas reciben el PortalClient, una entrada con un companyRut opcional (por defecto el del cliente) y un stage opcional, y ajustes opcionales de la llamada.
| Operación | Qué hace |
|---|---|
downloadExchangeSet | Un calentamiento y luego la descarga con la empresa en la query y un cuerpo multipart vacío. Devuelve { xml }. La descarga no cambia la postulación. Lanza SII_PORTAL_UNEXPECTED_PAGE (con la página redactada en details.page) cuando la respuesta no es un set |
validateExchangeUser | Valida al usuario con la empresa y el titular del certificado. Devuelve inExchange, state, completed, message, los hashes usados y el body crudo. No se exige //OK: la respuesta dice si la empresa está en la etapa de intercambio |
uploadExchangeAnswers | Sube reception, receipts y approval, en ese orden, cada una como una parte de archivo multipart. Una página rechazada detiene la secuencia (SII_PORTAL_REJECTED con exchangeUploadRefused y el archivo); todo lo no rechazado cuenta como subido, porque la página de éxito nunca se ha registrado |
confirmExchange | Envía la validación del set (su respuesta se guarda, no se juzga), luego tres confirmaciones (ids de seguimiento 40, 41 y 43), una por respuesta subida, cada una de las cuales debe responder //OK. Las tres confirmaciones son lo que mueve la postulación a las muestras impresas |
withExchangeStage | Abre una etapa para que encadenes las operaciones a mano (ver Los portales) |
Muestras impresas#
Generar los PDF#
writePrintedSamples (de dte-sdk/pdf) genera cada documento de los sobres que guardó la corrida en las carpetas SET-PRUEBAS y SET-SIMULACION, y readPrintedSamples(dir) lee todo .pdf bajo un directorio, la carpeta del set de pruebas primero, como PrintedSampleFile[] (fileName, bytes). findEnvelopeFiles encuentra los sobres de los sets y de la simulación en el directorio del almacén. El PDF mismo se describe en Muestras impresas.
El paso de muestras#
uploadPrintedSamples(deps, state, { samples }) corre toda la etapa en el portal de muestras como una sola etapa con un solo calentamiento: leer, crear, cada PDF, enviar, leer de nuevo, y el detalle. Lanza MISSING_FIELD (samples) cuando no das ningún PDF. En orden:
- Leer la revisión. Una frase sin estado significa que la postulación no está en la etapa de muestras impresas (o no se pidió set de pruebas): resultado
blocked, con la frase del portal. - Una revisión ya enviada (cualquier estado distinto de
INGRESO,RECHAZADOoAPROBADO) termina aquí comoalreadySubmitted, y la etapa pasa acompliance. - Retomar o crear. Una revisión que quedó en
INGRESO(una corrida cortada mientras subía), con id y token legibles, se retoma: se suben otra vez todos los PDF. Si no, se crea una revisión nueva. El portal solo crea una cuando no hay ninguna o la actual estáRECHAZADOoAPROBADO. - Subir cada PDF en orden, guardando cada nombre en el estado a medida que avanza.
- Enviar la revisión y leer su estado de nuevo. Una lectura que falla se registra y deja el estado desconocido, porque la revisión se envió igual.
| Resultado | Significado |
|---|---|
submitted | La revisión se envió. detail es su estado, o submitted cuando no se pudo leer. La etapa pasa a compliance |
alreadySubmitted | Ya se había enviado una revisión. detail es su estado |
blocked | El portal dice que la postulación no está en esta etapa |
rejected | El SII rechazó la revisión de inmediato. detail trae los motivos del SII (ver abajo) |
Una revisión que el SII rechaza vuelve como rejected, con el estado guardado. El paso lee entonces cómo clasificó el SII cada PDF (readSamplesDetail) y lo resume en una frase: cuántos documentos reconoció como set de pruebas, con sus casos, y cuántos como simulación, por tipo de documento. Un set cuyos documentos el SII clasificó como simulación no era el sobre que el SII registró para ese set.
Los estados de revisión que nombra el portal son INGRESO, POR REVISAR, EN REVISION, ENVIADO AL SII, APROBADO y RECHAZADO. Cada respuesta del portal se guarda bajo muestras/.
Las operaciones del portal de muestras#
| Operación | Qué hace |
|---|---|
readSamplesReview | Lee la revisión de la empresa: su state, una frase en blocked cuando el portal no nombra estado, el id y el token de la revisión, y cuándo se envió. Por defecto cualquier estado HTTP distinto de 200 es inesperado; anyStatus: true lee lo que llegue, como hace el paso tras un envío |
createSamplesReview | Crea una revisión con la empresa como remitente y proveedor. Lanza SII_PORTAL_REJECTED (samplesReviewRefused) con la frase del SII |
uploadSample | Sube un PDF (revisionId, fileName, bytes). HTTP 200 es éxito. Un 5xx se reintenta, hasta cuatro intentos en total, esperando 2, 4 y 6 s (un 503 en un archivo dejó una vez una revisión en INGRESO). Un 4xx se rechaza de inmediato (sampleUploadRefused). Un nombre con un CR, un LF o una comilla doble lanza INVALID_OPTION |
submitSamplesReview | Envía una revisión por su token. La respuesta es la misma tanto si el SII acepta como si rechaza las muestras: lee el estado después |
readSamplesDetail | Lee cómo clasificó el SII cada PDF de una revisión rechazada. Una falla se registra y entrega un detalle vacío: el rechazo se mantiene sin él |
withSamplesStage | Abre una etapa para que encadenes las operaciones a mano |
Los portales GWT-RPC#
El portal de intercambio y el portal de muestras son aplicaciones web que hablan GWT-RPC. El SDK incluye el códec y la mecánica de etapas, porque los portales no tienen otra API.
Una etapa, un calentamiento. La corrida entra a cada portal una vez por etapa de trabajo: una vez para la descarga del set de intercambio, una vez para la validación, las tres subidas y la confirmación, y una vez para todas las muestras impresas. Una etapa se asegura de la sesión (una en caché se comprueba y se reemplaza si está muerta), calienta el portal con una petición a su base (siguiendo una redirección) y toma los hashes una vez. Las cookies que fija el calentamiento viven solo durante esa etapa: ni se suman a la sesión compartida ni viajan a otro portal. withExchangeStage y withSamplesStage abren una etapa para quien encadena las operaciones a mano (pásala como stage en su entrada); por sí sola, cada operación abre la suya.
Estas llamadas se envían una sola vez y esperan cuanto haga falta. Sin reintento (salvo en la subida de un PDF cuando el portal responde un 5xx) y sin timeout propio, así que un portal que no responde retiene la llamada. Pasa timeoutMs en los ajustes de la llamada para acotarlas. Las respuestas se leen como UTF-8. Dentro de una etapa cada llamada lee su propia respuesta sea cual sea su estado: el cliente ni lanza ante un 502, 503 o 504 ni busca ahí la página del firewall, así que un error en la validación del usuario o en la validación del set no detiene el intercambio. Cada portal recibe solo las cabeceras de su etapa (un User-Agent de navegador, Accept y keep-alive, y en el portal de muestras el origen y el referer del propio portal) más lo que nombra cada llamada.
Los hashes. Un portal GWT exige dos hashes en cada llamada: la permutación (X-GWT-Permutation) y la política de serialización. El SII cambia ambos al redesplegar un portal. El portal de intercambio se llama con el par que se sabe que funciona (EXCHANGE_PORTAL.known), que nunca se busca. Los hashes del portal de muestras se descubren una vez al entrar, desde el script del módulo, con el par conocido como respaldo cuando algo falla. Pasa gwt: { permutation, policy } en la entrada de cualquier operación para reemplazar cualquiera de los dos: gana sobre el par conocido y sobre toda búsqueda.
El códec#
| Función | Qué hace |
|---|---|
gwtBody(call) | El cuerpo de petición de un GwtCall (moduleBase, policy, service, method, parameters): la cabecera del protocolo, la tabla de strings y luego el cuerpo, cada ítem seguido de una barra vertical. Los parámetros son strings, enteros y longs encajonados, booleanos, nulos, u objetos con sus campos en orden de serialización |
gwtAnswer(body) | Lee una respuesta: ok (el cuerpo empieza con //OK), la table de strings, el primer message que se lee como frase para el usuario, y los longs (cada token entre comillas simples, decodificado) |
encodeGwtLong(value) | Un número en el alfabeto base 64 de GWT, el dígito más significativo primero (264872 es BAqo) |
decodeGwtLong(token) | El número que contiene un token, o null cuando un carácter no está en el alfabeto |
discoverGwtHashes(context, input) | Busca los hashes de un portal en el script de su módulo: el par conocido vale cuando su permutación está listada; si no, la primera listada es la permutación y su página de caché entrega la política. Cualquier falla deja el par conocido. Lo usa la etapa; rara vez lo necesitas |
Necesitas el códec solo para llamar a un método GWT que el SDK no envuelve. Las operaciones del portal de arriba ya lo hacen.
Catálogos#
| Export | Qué contiene |
|---|---|
EXCHANGE_PORTAL, SAMPLES_PORTAL | Un GwtPortal cada uno: la base del módulo, la ruta e interfaz del servicio RPC, el nombre del módulo y el par de hashes known |
EXCHANGE_ANSWER_KEYS | reception, receipts, approval, en orden de subida |
EXCHANGE_ANSWER_FILES | El nombre de archivo de cada respuesta |
EXCHANGE_SET_ARTEFACT | intercambio/set-intercambio.xml, el artefacto que guarda el set de intercambio |
Próximos pasos#
- Haz la declaración: El ejecutor.
- Entiende los PDF que revisa el SII: Muestras impresas.
- La certificación de boletas es una postulación aparte: Boletas.