Saltar al contenido
dte-sdk

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#

TypeScript
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 XML

El 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, EnvioDTETraslado o EnvioBOLETA.
  • El id del set es el atributo ID, si no un hijo ID, si no SetDTE. Se leen los RUT de emisor, enviador y receptor de la carátula; y por documento su tipo (TipoDTE o TpoDoc), 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 (1e3 se lee como 1000), una fecha ausente es null y 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 Documento simple, o uno anidado dentro de un segundo DTE; cualquier otro tipo se lee como vacío.
  • Lanza XML_PARSE_FAILED cuando el parser se rinde o no hay raíz de sobre, y ENVELOPE_EMPTY cuando 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):

ClaveArchivoContenido
receptionrespuesta-recepcion-envio.xmlLa 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
receiptsenvio-recibos.xmlLos recibos de la Ley 19.983: un Recibo por documento, cada uno firmado por el titular del certificado, dentro de un EnvioRecibos firmado
approvalrespuesta-aprobacion-comercial.xmlLa 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:

  1. 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 como intercambio/set-intercambio.xml.
  2. Las respuestas se generan y se guardan como artefactos bajo intercambio/.
  3. 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/.
  4. El estado registra el intercambio y pasa la etapa a printedSamples.
ResultadoSignificado
doneLas respuestas se subieron y el intercambio se confirmó
alreadyDoneEl estado dice que se confirmó, o el portal dice que el intercambio ya se completó (estado P90)
setUnavailableNo se pudo descargar el set. detail nombra el artefacto que hay que llenar a mano
notInExchangeEl 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ónQué hace
downloadExchangeSetUn 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
validateExchangeUserValida 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
uploadExchangeAnswersSube 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
confirmExchangeEnví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
withExchangeStageAbre 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:

  1. 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.
  2. Una revisión ya enviada (cualquier estado distinto de INGRESO, RECHAZADO o APROBADO) termina aquí como alreadySubmitted, y la etapa pasa a compliance.
  3. 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á RECHAZADO o APROBADO.
  4. Subir cada PDF en orden, guardando cada nombre en el estado a medida que avanza.
  5. 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.
ResultadoSignificado
submittedLa revisión se envió. detail es su estado, o submitted cuando no se pudo leer. La etapa pasa a compliance
alreadySubmittedYa se había enviado una revisión. detail es su estado
blockedEl portal dice que la postulación no está en esta etapa
rejectedEl 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ónQué hace
readSamplesReviewLee 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
createSamplesReviewCrea una revisión con la empresa como remitente y proveedor. Lanza SII_PORTAL_REJECTED (samplesReviewRefused) con la frase del SII
uploadSampleSube 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
submitSamplesReviewEnví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
readSamplesDetailLee 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
withSamplesStageAbre 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ónQué 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#

ExportQué contiene
EXCHANGE_PORTAL, SAMPLES_PORTALUn 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_KEYSreception, receipts, approval, en orden de subida
EXCHANGE_ANSWER_FILESEl nombre de archivo de cada respuesta
EXCHANGE_SET_ARTEFACTintercambio/set-intercambio.xml, el artefacto que guarda el set de intercambio

Próximos pasos#