feat(dte): implement buildDteXml, applyTimbre and signDte for types 33 and 39 - #35
Open
nbisquertt wants to merge 1 commit into
Open
feat(dte): implement buildDteXml, applyTimbre and signDte for types 33 and 39#35nbisquertt wants to merge 1 commit into
nbisquertt wants to merge 1 commit into
Conversation
…3 and 39
buildDteXml now produces the real <DTE>/<Documento ID="F{folio}T{tipo}">
XML for Factura Electronica (33) and Boleta Electronica (39):
- Type-specific header element names: RznSoc/GiroEmis/Acteco for 33,
RznSocEmisor/GiroEmisor (no Acteco) for 39. Facturas fail fast when
receptor giro/direccion/comuna are missing (the SII rejects them).
- Totals: when montoNeto/iva are omitted they are derived from montoTotal
with IVA computed as the exact remainder (never a second rounding), so
MntNeto + IVA + MntExe === MntTotal always holds. Caller-provided totals
are validated within ±1.
- Line amounts convention (design decision, see resolveLineAmounts):
items[].montoItem is always GROSS (IVA-included) for both types, since
DteItemSchema has no gross/net flag and boletas require gross lines.
For type 33 lines are converted to net (round(monto/1.19)) and the
per-line rounding drift is absorbed into the last affected line so the
detail sums to the header MntNeto EXACTLY — the invariant the SII
validates with an otherwise opaque "amounts don't match" rejection.
- New optional DteDocumentSchema.indServicio (1|2|3), emitted only for
boletas and only when explicitly provided.
- UTF-8 prolog so declared encoding matches transmitted bytes.
applyTimbre builds a real TED: compact <DD> block signed RSA-SHA1 with
the CAF private key, embedding the ORIGINAL <CAF> block verbatim.
BREAKING-ish design decision: the previous (xml, cafPrivateKey: string)
signature could never produce a valid TED, because the SII's <FRMA>
signature inside the CAF cannot be reconstructed from parsed fields. The
function now takes (xml, caf: FolioRange) and FolioRangeSchema gained an
optional cafXml field carrying the raw CAF XML; the range metadata also
lets applyTimbre validate the folio/type against the authorization.
TSTED/TmstFirma use America/Santiago wall time (UTC would date nighttime
documents on the wrong day and desynchronize the tax period).
signDte applies XMLDSig over <Documento> (Reference URI="#F{folio}T{tipo}"
with C14N transform — unlike the auth seed's URI="" + enveloped pattern),
inserting <Signature> as sibling of <Documento> inside <DTE>. It must run
AFTER applyTimbre so the digest covers the TED. Digest/signature are
computed over the canonical form (in-scope namespaces rendered), following
the same manual string-building style as auth/xml-dsig.ts.
Tests replace the old "throws Not implemented" stub with hand-verified
values, a real in-memory P12 (generateTestP12) and a new synthetic CAF
helper; TED and XMLDSig signatures are verified cryptographically
(round-trip with the corresponding public keys).
Out of scope: uploadDte, generateDtePdf, folios parsing (parseCaf), and
the remaining DTE types (34/41/52/56/61/...) stay unimplemented.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
packages/engine/src/dte/index.tshadbuildDteXml,signDte, andapplyTimbrefully stubbed (throw new Error("Not implemented")) — running the README's own Quick Start example would throw. This PR implements the full pipeline for Factura Electrónica (33) and Boleta Electrónica (39):buildDteXml— produces the real<DTE>/<Documento>XML per type, with correct per-type header field names (RznSoc/GiroEmis/Actecofor 33 vsRznSocEmisor/GiroEmisorfor 39), receptor validation for facturas (giro/dirección/comuna required), and derived totals (neto/ivacomputed frommontoTotalwhen omitted, withivaas the exact remainder to avoid double-rounding).items[].montoItemis treated as gross for both types; for type 33 lines are converted to net with per-line rounding drift absorbed into the last affected line so the detail sums to the headerMntNetoexactly — this is the invariant the SII validates and rejects with an otherwise opaque "amounts don't match" error. Documented as an explicit design decision inresolveLineAmountssinceDteItemSchemahas no gross/net flag.applyTimbre— builds a real TED (compact<DD>block, RSA-SHA1-signed with the CAF private key, embedding the original<CAF>block verbatim).signDte— XMLDSig over<Documento>(Reference URI="#F{folio}T{tipo}"+ C14N, distinct from the auth seed's enveloped pattern), following the repo's existing manual string-building style inauth/xml-dsig.ts(no new XML-DSIG dependency).America/Santiagowall time, not UTC (a document emitted at night would otherwise be dated "tomorrow").Design decision worth flagging for review
The old
applyTimbre(xml, cafPrivateKey: string)signature could never produce a valid TED: the SII's own<FRMA>signature inside the CAF authorization can't be reconstructed from parsed fields — it has to be re-embedded verbatim from the original CAF XML. I added an optionalcafXmlfield toFolioRangeSchema(backward-compatible) and changedapplyTimbreto take(xml, caf: FolioRange)instead, so it also validates the folio/type against the authorization range. Happy to adjust this if you'd prefer a different shape (e.g. a separatecafXmlparam instead of extendingFolioRange).Out of scope
uploadDte,generateDtePdf,parseCaf/loadCafFromFile, and the remaining DTE types (34/41/43/46/52/56/61/110/112) are still unimplemented — this PR focuses on the two most common types to get the core pipeline working end-to-end first.Test plan
pnpm build— cleanpnpm lint(tsc --noEmit) — cleanpnpm test:run— 96 passed / 3 skipped (pre-existing skips), all greenMntNeto1679 with line split [839, 840] proving the last-line rounding adjustment) and real cryptographic round-trips: TEDFRMTverified against the CAF public key, XMLDSigSignatureValueverified against the P12 certificate's public key.changeset/tidy-dtes-sign.md, minor bump) per CONTRIBUTING.md