Parsing, validating and serializing
The pipeline has four steps, each in its own package: parse() turns XML into the model, validate() checks a document against a ruleset, createInvoice() builds one, and serialize() writes XML. Only validate() checks rules.
Parse
import { parse } from '@facturometro/einvoice';
import { xml } from '../first-invoice/create.ts';
const { document, diagnostics, ok } = parse(xml);
const kind = document.kind; // 'Invoice'
const payable = document.legalMonetaryTotal?.payableAmount?.value; // '3500.00': text, as writtenparse() detects whether the document is an invoice or a credit note; parseInvoice() and parseCreditNote() narrow the result. The document is always returned, however broken, so one parse and one validation report every problem at once. Values stay text, exactly as written: the parser never trims or converts them.
diagnostics lists what is wrong with the XML's structure, under the EN 16931 syntax rule ids (UBL-CR-* for elements the Peppol subset does not use, UBL-SR-* for elements that occur too often, UBL-DT-* for attributes that are not allowed) or, where no rule covers it, under this library's EINV-XML-* codes. ok is false when one of them is fatal.
Three errors are thrown instead, because they are about the call rather than the document:
| Error | When |
|---|---|
XmlSyntaxError | The text is not well-formed XML. |
UnsupportedDocumentError | The root element is neither a UBL Invoice nor a UBL CreditNote. |
DocumentKindError | parseInvoice() got a credit note, or parseCreditNote() an invoice. |
Validate
import type { Invoice } from '@facturometro/einvoice';
import { assertValid, parseInvoice, validate } from '@facturometro/einvoice';
import { xml } from '../first-invoice/create.ts';
import { document } from './parse.ts';
const result = validate(document);
if (result.ok) {
// result.document is a canonical Invoice | CreditNote from here on
console.log(result.document.legalMonetaryTotal.payableAmount.value);
} else {
for (const issue of result.fatal) {
console.error(`${issue.id} at ${issue.xpath}: ${issue.message}`);
}
}validate() takes a parsed or a built document and returns every issue: issues in evaluation order, split into fatal and warnings. ok is true when nothing is fatal, and only then does result.document hold the document, typed as the canonical Invoice or CreditNote.
Rule failures are never thrown. A rule that crashes becomes one fatal EINV-RULE-ERROR issue, so a run always completes. Without a ruleset option, the rules come from the customization id; see which rules run by default.
A decimal, boolean or date the UBL schema would reject (12,50, TRUE, 2017-13-01) is reported once, as a fatal EINV-XSD-LEXICAL issue, and the rules that would read the value are skipped for it. That mirrors the official pipeline, which validates against the XSD before it runs the rules.
To fail fast instead, assertValid() returns the canonical document or throws a ValidationError whose issues list what was found:
function readInvoice(text: string): Invoice {
return assertValid(parseInvoice(text).document); // throws a ValidationError when not valid
}Parse and validate in one call
import { validateXml } from '@facturometro/einvoice';
import { xml } from '../first-invoice/create.ts';
const result = validateXml(xml);
const valid = result.ok; // no fatal issue from the parser or from a rule
const report = result.issues.map((issue) => `${issue.severity} ${issue.id} at ${issue.xpath}`);
const parserDiagnostics = result.diagnostics; // what the parser reported, unfilteredvalidateXml() from @facturometro/einvoice parses the XML and validates the document against the same composition validate() would pick. The parser's diagnostics come first in issues and count towards ok, so a document with a fatal structural problem is not valid even when every rule passes. Your ruleset's disable and override of structural ids apply to them; diagnostics keeps the parser's report unfiltered.
Serialize
import { serialize } from '@facturometro/einvoice';
import { invoice } from '../first-invoice/create.ts';
const pretty = serialize(invoice); // 2-space indent, with the XML declaration
const tabs = serialize(invoice, { indent: '\t' });
const compact = serialize(invoice, { indent: false, declaration: false }); // one lineserialize() accepts a canonical or a parsed document and writes the elements in schema order. It applies no defaults and computes nothing: pass the document through createInvoice() or createCreditNote() for that. A value that is not a string where the schema expects text, such as a number, throws a TypeError naming the element's XPath.