Exchange and samples
After the simulation, the certification asks for two more things. The exchange: the SII sends your company a set of documents addressed to it, and you answer with three signed files. The printed samples: the PDF of every document the sets and the simulation issued, uploaded as a review the SII's automatic validator checks. runCertification does both; this page covers how, and the functions beneath if you chain them by hand.
Nothing here has been run against the two portals yet. Every wire and
wording is still unproven; the exchange answers validate against the
official schemas in the tests, and the printed samples have not been reviewed
by the SII. The first enrolled run should keep every artefact the store
writes under intercambio/ and muestras/ as fixtures. See
Status.
Basic usage#
import { readFile } from 'node:fs/promises'
import { runCertification, createExchangeAnswers, EXCHANGE_SET_ARTEFACT } from 'dte-sdk/portal'
import { readPrintedSamples, writePrintedSamples, findEnvelopeFiles } from 'dte-sdk/pdf'
// After the simulation the run downloads the exchange set from the exchange portal, answers it and confirms it.
// Then the samples are rendered from the envelopes the store kept and uploaded to the samples portal.
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' once the samples are submitted, or 'rejected' with the SII's reasons in run.detail
// The set could not be downloaded: run.stopped is 'exchange' and run.detail names intercambio/set-intercambio.xml.
// Save the set by hand from the exchange portal, then either pass it...
await runCertification(deps, { exchangeSetXml: await readFile('set-intercambio.xml', 'utf8') })
// ...or put it in that artefact of the store and run again
await deps.store.writeArtefact(EXCHANGE_SET_ARTEFACT, await readFile('set-intercambio.xml', 'utf8'))
// The answers alone, for a set in hand
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 // signed, one line, UTF-8, no XML declarationThe exchange#
The set#
The exchange set is the SII's set of documents addressed to the company, an EnvioDTE. parseExchangeSet(setXml) reads it loosely, because the SII's own test companies do not always have valid RUTs and the set is not under your control:
- The XML is not checked for well-formedness, and tags are read by their full name, so an envelope with a namespace prefix is no envelope. The root is
EnvioDTE,EnvioDTEB,EnvioDTETrasladoorEnvioBOLETA. - The set id is the
IDattribute, else anIDchild, elseSetDTE. The cover's issuer, sender and receiver RUTs are read; per document, its type (TipoDTEorTpoDoc), folio, issue date, issuer and receiver (the cover's when the document names none) and total. - A number that does not parse is 0 (
1e3reads as 1000), a missing date isnull, and a missing RUT is empty. RUTs are not validated: only their dots are removed and their check letter is upper-cased. - Only a plain
Documentois read, or one nested inside a secondDTE; any other kind reads as empty. - It throws
XML_PARSE_FAILEDwhen the parser gives up or there is no envelope root, andENVELOPE_EMPTYwhen the set holds no document. Nothing else fails.
The three answers#
createExchangeAnswers(setXml, { companyRut, certificate, contact?, now? }) builds and signs the three answers the certification expects. Each is one line of UTF-8 with no XML declaration, and each travels under the file name the portal's upload expects (EXCHANGE_ANSWER_FILES):
| Key | File | Content |
|---|---|---|
reception | respuesta-recepcion-envio.xml | The reception response: the envelope received, and each document received when its receiver is your company, otherwise marked with the SII's "error en RUT receptor" |
receipts | envio-recibos.xml | The receipts of Ley 19.983: one Recibo per document, each signed by the certificate holder, inside a signed EnvioRecibos |
approval | respuesta-aprobacion-comercial.xml | The commercial approval: each document accepted when its receiver is your company, otherwise rejected with CodRchDsc -1 |
They validate against the official schemas in the tests. The ids carry the clock in milliseconds, and every timestamp is Chile's local time. The contact name is sanitized; its phone number appears in the receipts only. The result also carries set, the exchange set as read.
It throws the codes of parseExchangeSet, and INVALID_RUT (field companyRut).
The exchange step#
runExchange(deps, state, { setXml? }) is the exchange stage. In order:
- The set. The one you gave (
setXml, which is saved as the artefact), else the one kept from an earlier run, else it is downloaded from the exchange portal and kept asintercambio/set-intercambio.xml. - The answers are generated and kept as artefacts under
intercambio/. - One stage on the exchange portal (one warm-up): the user is validated; if the company is in the exchange stage, the three answers are uploaded in order, and the exchange is confirmed. Every portal answer is kept under
intercambio/. - The state records the exchange and moves the stage to
printedSamples.
| Outcome | Meaning |
|---|---|
done | The answers were uploaded and the exchange confirmed |
alreadyDone | The state says it was confirmed, or the portal says the exchange was completed (state P90) |
setUnavailable | The set could not be downloaded. detail names the artefact to fill by hand |
notInExchange | The portal says the company is not at the exchange stage. detail is its sentence |
A set that cannot be downloaded, for any reason, does not throw: it stops the run with exchange and asks for the set by hand. The page that came back, when there is one, is kept as intercambio/pfe-download-error.html. Save the set into the EXCHANGE_SET_ARTEFACT artefact (or pass exchangeSetXml) and run again.
The answers are generated and uploaded again on every run that reaches the portal; only the set is kept.
The exchange portal operations#
All take the PortalClient, an input with an optional companyRut (the client's by default) and an optional stage, and optional call overrides.
| Operation | What it does |
|---|---|
downloadExchangeSet | A warm-up, then the download with the company in the query and an empty multipart body. Returns { xml }. The download does not change the application. Throws SII_PORTAL_UNEXPECTED_PAGE (with the redacted page in details.page) when the answer is no set |
validateExchangeUser | Validates the user with the company and the certificate holder. Returns inExchange, state, completed, message, the hashes used and the raw body. //OK is not required: the answer says whether the company stands in the exchange stage |
uploadExchangeAnswers | Uploads reception, receipts and approval, in that order, each as one multipart file part. A refused page stops the sequence (SII_PORTAL_REJECTED with exchangeUploadRefused and the file); anything not refused counts as uploaded, because the success page has never been recorded |
confirmExchange | Sends the set validation (its answer is kept, not judged), then three confirmations (follow-up ids 40, 41 and 43), one per uploaded answer, each of which must answer //OK. The three confirmations are what moves the application to the printed samples |
withExchangeStage | Opens a stage for you to chain the operations by hand (see The portals) |
Printed samples#
Writing the PDFs#
writePrintedSamples (from dte-sdk/pdf) renders every document of the envelopes the run kept into SET-PRUEBAS and SET-SIMULACION folders, and readPrintedSamples(dir) reads every .pdf under a directory, the test set folder first, as PrintedSampleFile[] (fileName, bytes). findEnvelopeFiles picks the set and simulation envelopes out of the store directory. The PDF itself is described in Printed samples.
The samples step#
uploadPrintedSamples(deps, state, { samples }) runs the whole stage on the samples portal as one stage with one warm-up: reading, creating, every PDF, submitting, reading again, and the detail. It throws MISSING_FIELD (samples) when you give no PDF. In order:
- Read the review. A sentence with no state means the application is not at the printed samples stage (or no test set was requested): outcome
blocked, with the portal's sentence. - A review already sent (any state other than
INGRESO,RECHAZADOorAPROBADO) ends here asalreadySubmitted, and the stage moves tocompliance. - Resume or create. A review left in
INGRESO(a run cut off while uploading), with a readable id and token, is resumed: every PDF is uploaded again. Otherwise a new review is created. The portal only creates one when there is none or the current one isRECHAZADOorAPROBADO. - Upload every PDF in order, saving each name in the state as it goes.
- Submit the review and read its state again. A read that fails is logged and leaves the state unknown, because the review was sent anyway.
| Outcome | Meaning |
|---|---|
submitted | The review was sent. detail is its state, or submitted when it could not be read. The stage moves to compliance |
alreadySubmitted | A review was already sent. detail is its state |
blocked | The portal says the application is not at this stage |
rejected | The SII rejected the review at once. detail carries the SII's reasons (see below) |
A review the SII rejects comes back as rejected, with the state saved. The step then reads how the SII filed each PDF (readSamplesDetail) and summarizes it in one sentence: how many documents it recognised as the test set, with their cases, and how many as the simulation, by document type. A set whose documents the SII filed as simulation was not the envelope the SII registered for that set.
The review states the portal names are INGRESO, POR REVISAR, EN REVISION, ENVIADO AL SII, APROBADO and RECHAZADO. Every portal answer is kept under muestras/.
The samples portal operations#
| Operation | What it does |
|---|---|
readSamplesReview | Reads the company's review: its state, a blocked sentence when the portal names no state, the review id and token, and when it was sent. By default any HTTP status but 200 is unexpected; anyStatus: true reads whatever came, as the step does after a submission |
createSamplesReview | Creates a review with the company as sender and provider. Throws SII_PORTAL_REJECTED (samplesReviewRefused) with the SII's sentence |
uploadSample | Uploads one PDF (revisionId, fileName, bytes). HTTP 200 is success. A 5xx is retried, up to four attempts in all, waiting 2, 4 and 6 s (a 503 on one file once left a review in INGRESO). A 4xx is refused at once (sampleUploadRefused). A name holding a CR, an LF or a double quote throws INVALID_OPTION |
submitSamplesReview | Submits a review by its token. The answer is the same whether the SII accepts or rejects the samples: read the state afterwards |
readSamplesDetail | Reads how the SII filed each PDF of a rejected review. A failure is logged and gives an empty detail: the rejection stands without it |
withSamplesStage | Opens a stage for you to chain the operations by hand |
The GWT-RPC portals#
The exchange portal and the samples portal are web applications that talk GWT-RPC. The SDK includes the codec and the stage mechanics, because the portals have no other API.
One stage, one warm-up. The run enters each portal once per stage of work: once for the download of the exchange set, once for the validation, the three uploads and the confirmation, and once for the whole of the printed samples. A stage makes sure of the session (a cached one is checked and replaced when dead), warms the portal up with a request to its base (following one redirect), and takes the hashes once. The cookies the warm-up sets live only for that stage: they neither join the shared session nor travel to another portal. withExchangeStage and withSamplesStage open a stage for a caller that chains the operations by hand (pass it as stage in their input); alone, each operation opens its own.
These calls are sent once and wait as long as it takes. No retry (but for a PDF upload the portal answers with a 5xx) and no time-out of their own, so a portal that does not answer holds the call. Pass timeoutMs in the call overrides to bound them. The answers are read as UTF-8. Inside a stage each call reads its own answer whatever its status: the client neither throws on a 502, 503 or 504 nor looks for the firewall's page there, so an error on the user validation or on the set validation does not stop the exchange. Each portal receives only the headers of its stage (a browser-like User-Agent, Accept and keep-alive, and for the samples portal the origin and referer of the portal itself) plus what each call names.
The hashes. A GWT portal requires two hashes on every call: the permutation (X-GWT-Permutation) and the serialization policy. The SII changes both when it redeploys a portal. The exchange portal is called with the pair known to work (EXCHANGE_PORTAL.known), never looked up. The samples portal's hashes are discovered once on the way in, from the module's script, with the known pair as the fallback when anything fails. Pass gwt: { permutation, policy } in the input of any operation to override either: it wins over the known pair and over any lookup.
The codec#
| Function | What it does |
|---|---|
gwtBody(call) | The request body of a GwtCall (moduleBase, policy, service, method, parameters): the protocol header, the string table, then the body, every item followed by a pipe. Parameters are strings, boxed integers and longs, booleans, nulls, or objects with their fields in serialization order |
gwtAnswer(body) | Reads an answer: ok (the body starts with //OK), the string table, the first message that reads as a sentence to the user, and the longs (every single-quoted token decoded) |
encodeGwtLong(value) | A number in the GWT base 64 alphabet, most significant digit first (264872 is BAqo) |
decodeGwtLong(token) | The number a token holds, or null when a character is not in the alphabet |
discoverGwtHashes(context, input) | Looks the hashes of a portal up from its module script: the known pair stands when its permutation is listed, otherwise the first one listed is the permutation and its cache page gives the policy. Any failure keeps the known pair. The stage uses it; you rarely need it |
You need the codec only to call a GWT method the SDK does not wrap. The portal operations above already do.
Catalogs#
| Export | What it holds |
|---|---|
EXCHANGE_PORTAL, SAMPLES_PORTAL | A GwtPortal each: the module base, RPC service path and interface, module name, and the known hash pair |
EXCHANGE_ANSWER_KEYS | reception, receipts, approval, in upload order |
EXCHANGE_ANSWER_FILES | The file name of each answer |
EXCHANGE_SET_ARTEFACT | intercambio/set-intercambio.xml, the artefact that holds the exchange set |
Next steps#
- Make the declaration: The runner.
- Understand the PDFs the SII reviews: Printed samples.
- The certification of boletas is a separate application: Receipts.