Test sets
The deterministic half of the certification. You give it the text the SII generated for your company and your CAFs and certificate; it gives you the signed envelopes and books the SII asks for. It sends nothing and waits for nothing: sending, declaring and waiting belong to the runner, which calls these same functions.
Use it on its own to see what a test set will issue before touching a portal, to issue a set by hand after the SII rejected it, or to build your own flow.
The parser was written without a real test set text at hand, and the tests use
synthetic texts with invented data. Keep your first download (the runner
saves it as set-texto.txt) as a fixture, and open an issue if the parser
misreads a line. Nothing here has been run against the SII yet: see
Status.
Basic usage#
import { readFileSync } from 'node:fs'
import { loadCertificateFromPkcs12, parseCaf } from 'dte-sdk'
import {
parseTestSetText,
planCertificationSets,
createFolioAllocator,
issueTestSet,
issueSimulation,
createCertificationSalesBook,
createCertificationPurchaseBook,
createCertificationGuideBook,
} from 'dte-sdk/portal'
const certificate = loadCertificateFromPkcs12(readFileSync('firma.pfx'), process.env.PFX_PASSWORD!)
// The text the SII generated for the applicant
const plan = planCertificationSets(parseTestSetText(readFileSync('set-texto.txt', 'utf8')))
plan.basic?.folios // { 33: 3, 61: 3, 56: 2 }: the folios the basic set needs, by document type
const issuer = {
rut: '76543210-3',
businessName: 'Comercial Andina SpA',
businessActivity: 'Actividades de programacion informatica',
address: 'Av. Los Aromos 1540',
commune: 'Santiago',
activityCode: 620200,
}
const receiver = {
rut: '66666666-6',
businessName: 'CLIENTE TEST',
businessActivity: 'ACTIVIDADES VARIAS',
address: 'AVENIDA PRINCIPAL 123',
commune: 'Santiago',
city: 'Santiago',
}
const resolution = { number: 0, date: '2026-01-15' }
// One allocator per set: it hands out the folios of the CAFs you give it
const cafs = ['caf-33.xml', 'caf-61.xml', 'caf-56.xml'].map((file) => parseCaf(readFileSync(file, 'utf8')))
const options = { issuer, receiver, certificate, folios: createFolioAllocator(cafs), resolution }
const basic = await issueTestSet(plan.basic!, options)
basic.cases.map((c) => [c.caseId, c.type, c.folio]) // [['2410011-1', 33, 1], …]
basic.envelope.xml // the signed EnvioDTE, ready to upload
const common = {
period: '2026-11',
issuerRut: issuer.rut,
senderRut: certificate.rut!,
resolution,
certificate,
}
const salesBook = createCertificationSalesBook(basic, {
...common,
receiver: { rut: receiver.rut, name: receiver.businessName },
})
const purchaseBook = createCertificationPurchaseBook(plan.purchaseBook!, common)
// The simulation reissues every case of the four sets, in one envelope, with CAFs other than the sets'
const freshCafs = ['caf-33-b.xml', 'caf-34.xml', 'caf-52.xml', 'caf-46.xml', 'caf-61-b.xml', 'caf-56-b.xml'].map((file) =>
parseCaf(readFileSync(file, 'utf8')),
)
const simulation = await issueSimulation(plan, { ...options, folios: createFolioAllocator(freshCafs) })
simulation.envelope.xmlEach issued set and each book is a signed XML string plus the data around it. You upload them with uploadEnvelope and uploadBook, or let the runner do it.
parseTestSetText#
parseTestSetText(text) parses the text the SII generates for the applicant into TestSet[]. The text is the portal page with its tags turned into line breaks and its entities decoded, or a file holding the same (downloadTestSet already returns it that way). The function never throws: lines the grammar does not know are ignored, and a set of unknown kind is other.
SET BASICO - NUMERO DE ATENCION: 2410011
==========================================
CASO 2410011-1
==========================================
DOCUMENTO FACTURA ELECTRONICA
ITEM CANTIDAD PRECIO UNITARIO
Cajon de manzanas 130 2800
Relleno de pollo 90 1700
--------------------------------------------------
CASO 2410011-4
DOCUMENTO NOTA DE CREDITO ELECTRONICA
REFERENCIA CASO 2410011-1
RAZON REFERENCIA DEVOLUCION DE MERCADERIAS
ITEM CANTIDAD
Cajon de manzanas 62This is a synthetic fragment with invented data, not an SII text.
What it reads#
A TestSet has name, attentionNumber (the digits after NUMERO DE ATENCION), kind, cases, bookDocuments and proportionalityFactor. A TestCase has:
| Field | Source line |
|---|---|
id, setId, number | The CASO <setId>-<n> header, for example 2410011-1 |
documentText, documentType | DOCUMENTO; the type is detected from the text, or null |
items | The item rows under ITEM |
referenceText, referencedCase | REFERENCIA; the CASO n-m found in it, or null |
reasonText | RAZON REFERENCIA |
motive, transferBy | MOTIVO and TRASLADO POR (guides) |
globalDiscountPercent | DESCUENTO GLOBAL … n% |
currency, exportFields | MONEDA DE LA OPERACION and the export-only lines (parsed, never issued) |
raw | Every line of the case after its header, trimmed |
An item row is split on tabs or runs of two or more spaces. With four columns it is name, quantity, unit and price, or name, quantity, price and discount (10%); with three it is name, quantity and price; with two it is a name and a quantity, or, when the reason says MODIFICA MONTO, a name and a new price. A missing quantity is 1. A line is not an item when it holds a colon, when DOCUMENTO, REFERENCIA or RAZON opens it, or when ITEM, CANTIDAD or UNITARIO is in its first part (the header row).
These words, and EXENTO and ANULA further down, are read in capitals only, as the SII writes them. Matching is case-insensitive elsewhere, but not here: an item named "Razones varias" is an item, and the value of a two-part row under a reason "modifica monto" is a quantity, not a price.
Kinds of set#
The kind comes from the name after SET, first match wins (TEST_SET_KINDS):
| The name contains | kind |
|---|---|
LIBRO DE COMPRAS PARA EXENTOS | exemptPurchaseBook |
LIBRO DE COMPRAS | purchaseBook |
LIBRO DE VENTAS | salesBook |
LIBRO DE GUIAS | guideBook |
GUIA | guide |
FACTURA EXENTA or NO AFECTA | exempt |
EXPORTACION | export |
LIQUIDACION | settlement |
FACTURA DE COMPRA or FACTURA COMPRA | purchase |
BASICO | basic |
| anything else | other |
In the purchase book sets, the supplier documents are not cases: each one (FACTURA ELECTRONICA 234, NOTA DE CREDITO 12…) becomes a TestBookDocument with its folio, exempt and net affected amounts, the observation line and three flags read from it: IVA USO COMUN, ENTREGA GRATUITA (non-recoverable VAT code 4) and RETENCION TOTAL. The FACTOR DE PROPORCIONALIDAD line gives the proportionality factor.
planCertificationSets#
planCertificationSets(sets) turns the parsed sets into a CertificationPlan: what each set issues, ready for issueTestSet, and the folios each document type needs. For each kind the last set of the text wins, and a kind the text lacks is absent from the plan.
| Plan key | Type | Issues |
|---|---|---|
basic | BasicSetPlan | Invoices 33, credit notes 61, debit notes 56 |
exempt | ExemptSetPlan | Invoices 34, credit notes 61, debit notes 56; every line exempt |
guide | GuideSetPlan | One guide 52 per case |
purchase | PurchaseSetPlan | One purchase invoice 46, at most one 61 and one 56 |
purchaseBook | PurchaseBookPlan | Supplier documents of the purchase book |
exemptPurchaseBook | PurchaseBookPlan | The same, for exempt taxpayers |
The sales book and guide book sets have no plan: those books come from the basic and guide sets once issued. Export and settlement sets are parsed but never issued, and unknown sets are left out.
Every plan carries its attentionNumber and, for the issuable sets, folios: how many documents of each type it will issue (SetFolios, a record from document type to count). The runner sums these to know how many folios to have. A PurchaseBookPlan carries documents and proportionalityFactor, which is 0.6 when the set gives none.
Notes carry their referenceCode (CodRef), derived from the reason text: a reason that is empty or says ANULA is 1 (cancels), CORRIGE with GIRO or TEXTO is 2 (corrects text), DEVOLUCION or MODIFICA is 3 (corrects amounts), anything else is 1. A guide carries its transferReason (IndTraslado) from the motive, 1 when empty or unknown (TRASLADO between warehouses is 5, DEVOLUCION 7, CONSIGNACION 3, GRATUITA 4, export sale 9, a sale to be made 2, plain VENTA 1) and its dispatchType from TRASLADO POR: EMISOR is 2 (by the issuer), CLIENTE is 1.
createFolioAllocator#
createFolioAllocator(cafs, { usedFolios }) is a FolioSource over one or several parsed CAFs: one counter per CAF, the first CAF of the type with a folio left wins, a folio is never given twice, and the CAF returned is the one whose range holds the folio, so it stamps with its own key. When every CAF of the type is exhausted take throws FOLIO_OUT_OF_RANGE with { type }. usedFolios makes it start after the highest used folio of each CAF.
A FolioSource is just { take(type): Promise<{ caf, folio }> }. The allocator is in memory; the runner passes its own, backed by the folio registry so two runs never take the same folio. Implement your own if you keep folios elsewhere.
issueTestSet#
issueTestSet(plan, options) issues every document of one set plan and signs its envelope. It accepts the basic, exempt, guide and purchase plans.
| Option | Meaning |
|---|---|
issuer | The issuer, as for issueDocument |
receiver | The customer every document names. Purchase invoices and internal transfers name the issuer instead |
certificate | The signing certificate; its holder is the sender of the envelope |
folios | A FolioSource |
resolution | { number, date }; number 0 in certification |
now | Clock, read once per document and once for the envelope |
It returns an IssuedSet: kind, attentionNumber, cases (each with caseId, type, folio and the signed document) and the signed envelope (EnvioDTE, with SetDTE/@ID DTE_SetDoc, the SII as receiver and the certificate holder as sender; the issuer when the certificate names none). Invoices (or guides) go first, then credit notes, then debit notes.
It throws MISSING_FIELD (invoices or guides on an empty plan, referencedCase on a note whose case was not issued before it), FOLIO_OUT_OF_RANGE from the folio source, and the usual document issuing errors.
How the documents are written#
The test sets follow the rules the SII's review expects, and a few details matter because the SII compares them:
- The
SETreference. Every document carries a first reference with document typeSET, folio 1, the issue date and the reasonCASO <id>(TEST_SET_REFERENCE_TYPE). A note adds a second reference to the document of the case it refers to, with the set's reference code and reason. - Returned goods at the original price. A note that corrects amounts (code 3) and gives items with no price takes the price, and the discount, of the item with the same name in the original document. A return in the exempt set takes every item of the original, exempt, at its original price.
- Cancellations take the cancelled document's items. A basic-set credit note whose reason says
ANULA(in capitals; a lower-case "anula" keeps the note's own items) takes the items of the document it cancels. In the exempt set a note always keeps its own items, written as exempt lines, even when its reason saysANULA. - The issuer as receiver of purchase invoices and notes, and of internal transfers (guides with transfer reason 5). Purchase documents carry the VAT withholding. An internal transfer totals 0 and keeps the VAT rate next to that zero total.
- Notes that correct a text (code 2) get one line named after the reason, with a zero price. In the basic and purchase sets such a document totals 0 and carries no VAT rate; in the exempt set and in guides the rate stays next to the zero total. A basic-set note that ends up with no items gets a
SIN MONTOline. - Exempt items in the basic set. An item of a basic invoice whose name has
EXENTOis issued exempt. "Servicio exento", in lower case, is not. - Basic invoices carry
TpoTranVenta1 and the case's global discount, described asDESCUENTO GLOBAL ITEMES AFECTOS. - Every case of the guide set is a guide, whatever its text was read as, with its transfer reason and dispatch type.
- A receiver's line of business is cut at 40 characters instead of being refused. The issuer's keeps its own length, except where the issuer is the receiver of its own documents.
How the dates, stamps and prices are written is also deliberate, and different from the simulation:
- The issue date is the UTC day the set starts on.
TSTED,TmstFirmaandTmstFirmaEnvare the UTC instant with milliseconds and no zone. A run after 21:00 in Chile (20:00 in winter) therefore dates the documents with the next day. - Unit prices are written with six decimals on priced lines. Invoice and note lines with no price carry no price, no quantity and no unit.
- A document whose lines add up to nothing has no VAT rate (see above).
issueSimulation#
issueSimulation(plan, options) issues every case of the four sets again, with fresh folios, in the order basic, exempt, guide and purchase, and signs one envelope with all of them. The folio source should hold CAFs other than the sets'. It takes the same options as issueTestSet and returns an IssuedSimulation: cases, envelope, types (document types in order of first appearance) and fewerThanRecommended (true with fewer than the ten documents the SII asks for).
It throws MISSING_FIELD (cases) when the plan has no case at all.
The simulation is written differently from the test sets:
- No
SETreference. A note carries one reference to the document it refers to, when that case is in the simulation. - The date is the local day of the process.
FchEmisis the local date,TSTEDandTmstFirmaare the UTC instant to the second, the envelope id is the local time, and onlyTmstFirmaEnvis Chile time. Run the process withTZ=America/Santiago. - Unit prices are plain numbers, not six decimals.
- A guide carries the transfer reason and no
TpoDespacho. An internal transfer names the issuer as receiver. - The global discount has no description.
- A note with no items gets one line named after its reason. A purchase note item with no price takes the price of the same-named item of the original, or 1.
- A document whose lines add up to nothing totals 0 with no VAT rate; a guide keeps it.
- The parties carry a fixed list of fields: whatever else you give the issuer or the receiver (phone, e-mail, branch, contact) stays out of the documents.
The books#
The three functions build and sign the books. All take a CertificationBookInput:
| Field | Meaning |
|---|---|
period | Tax period YYYY-MM. The runner chooses it against the portal's history |
issuerRut, senderRut | The company and the authorized sender, normally the certificate holder |
resolution | { number, date } |
certificate | The signing certificate |
now | Clock for the book id and the signature time |
submission | total (default) or adjustment, for a period the SII already holds |
previousTotals | Period totals of the earlier submissions, carried into an adjustment of the sales or purchase book; the guide book ignores them |
createCertificationSalesBook(set, input)takes the issued basic set (or{ kind: 'basic', cases }, as a resumed run holds them) andreceiver: { rut, name }. Only the basic set's invoices and notes go in, dated the 15th of the period, with the customer as counterparty, every amount present (0 when absent) and the 19 VAT rate on every line.creditNoteSignispositive(default) ornegative. It throwsINVALID_OPTION(fieldset) for any other set.createCertificationPurchaseBook(plan, input)takes aPurchaseBookPlan(purchaseBookorexemptPurchaseBook). One line per supplier document, with the SII's own RUT and a placeholder name as counterparty (CERTIFICATION_COUNTERPARTY, nobody's personal data), VAT at 19 % of the affected amount, common-use VAT, non-recoverable VAT for free deliveries, the withholding of a purchase invoice, and the referenced folio of a note taken from its observation. For the exempt taxpayers' book, non-recoverable VAT on every affected line, the paper exempt invoice as type 32, and notes that inherit the amounts of the document they reference. The VAT withholding rate travels as a fraction (0.19) while every other rate is a percentage. The summary rows carry the credit of common-use VAT, rounded document by document and then summed, also across the rows of earlier submissions.createCertificationGuideBook(set, input)takes the issued guide set (or{ kind: 'guide', cases }) andreceiver. By position, the second guide is flaggedmodifiedand the thirdvoided(written as annulled, total 0 and nothing else), unlesscaseFlagssays otherwise. A sale carries the customer, the net, the VAT rate and the VAT; every guide its total.notificationFoliois 3 by default. An adjustment marks only the modified guide.
Sales and purchase books are monthly books sent as a total submission. The guide book is a special book. All three return a signed book with its xml (the sales and purchase books also its periodTotals).
Catalogs#
| Export | What it holds |
|---|---|
TEST_SET_KINDS | The kinds of set, by their English name |
TEST_SET_PORTAL_NAMES | The names the portal lists the sets and books under (SET BASICO, SET GUIA DE DESPACHO, LIBRO DE VENTAS…) |
TEST_SET_REFERENCE_TYPE | SET, the document type of the first reference of every set document |
TEST_SET_ENVELOPE_ID | DTE_SetDoc, the SetDTE/@ID of the test set envelopes |
CERTIFICATION_COUNTERPARTY | The counterparty written in the certification purchase books |
Next steps#
- Send and declare what you issued with the runner.
- Build the PDFs the SII asks for in Printed samples.
- The books as the SDK models them: Books.