@stackforge-eu/factur-x 1.0.1 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.0.4] — 2026-05-05
11
+
12
+ ### Fixed
13
+
14
+ - Release workflow `publish-npm` job: bump `node-version` to `24.15.0` (current Node LTS) and roll back the npm upgrade step from `corepack prepare npm@latest --activate` to `npm install -g npm@latest`. The corepack route in v1.0.3 silently broke OIDC trusted publishing against the npm registry (sigstore provenance signing still worked, but the registry `PUT` went out unauthenticated and was rejected with a 404), so v1.0.2 and v1.0.3 never reached `registry.npmjs.org` — `dist-tags.latest` there is still `1.0.1`.
15
+
16
+ ## [1.0.3] — 2026-05-05
17
+
18
+ ### Fixed
19
+
20
+ - Release workflow: upgrade npm via `corepack prepare npm@latest --activate` instead of `npm install -g npm@latest`, which crashed on the GitHub-hosted Node 22.22.2 toolcache where the bundled npm was missing its own `promise-retry` transitive dependency
21
+ - `src/core/embed.ts`: import `console` from `node:console` so the new PDF/A-3 warnings type-check under the project's `lib: ["ES2022"]` tsconfig (no DOM lib)
22
+
23
+ ## [1.0.2] — 2026-05-05
24
+
25
+ ### Added
26
+
27
+ - `EmbedOptions.rgbIccProfile` and `EmbedOptions.outputIntentIdentifier`: inject a PDF/A-3 `/OutputIntents` entry referencing an sRGB ICC profile when the input PDF lacks one (ISO 19005-3 §6.2.4.3)
28
+ - `EmbedOptions.unembeddedFonts` (`"warn"` | `"throw"` | `"ignore"`, default `"warn"`): detect and report fonts whose `FontDescriptor` carries no `FontFile`/`FontFile2`/`FontFile3` stream, including Type 0 descendant fonts and the standard 14 base fonts (ISO 19005-3 §6.2.11.4.1)
29
+ - Exported `XRECHNUNG_PROFILE_URN` (`urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0`)
30
+ - `validateInput` now accepts an optional `flavor` argument; passing `Flavor.XRECHNUNG` enables BR-DE / PEPPOL-EN16931 rules on top of the profile checks
31
+ - Pre-XML business-rule validation: BR-27 (non-negative line unit price), BR-CO-10 / BR-CO-13 / BR-CO-14 / BR-CO-15 / BR-CO-17, BR-62 / FX-SCH-A-000158 (URIID schemeID), BR-FX-EN-04 (delivery date or billing period), and XRechnung-only BR-DE-2, BR-DE-15, BR-DE-19 (IBAN mod-97), PEPPOL-EN16931-R001 / R010 / R020
32
+
33
+ ### Changed
34
+
35
+ - **BREAKING:** `TradePartyInput.electronicAddress` is now `{ value: string; schemeID: string }` instead of a bare `string`. Required by BR-62 / FX-SCH-A-000158 — the URIID element must carry an EAS `@schemeID` (e.g. `"EM"`, `"0088"`, `"9930"`)
36
+ - **BREAKING:** Profile URNs in `PROFILE_URNS` updated to the EN 16931 conformance form accepted by the shipped schematron — `BASIC` → `urn:cen.eu:en16931:2017#compliant#urn:factur-x.eu:1p0:basic`, `EN16931` → `urn:cen.eu:en16931:2017`, `EXTENDED` → `urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended` (`MINIMUM` and `BASIC_WL` unchanged)
37
+ - XRechnung output now writes the XRechnung CIUS URN as the GuidelineSpecifiedDocumentContextParameter (BR-DE-21) instead of the underlying EN 16931 URN
38
+ - Line-total fallback (`quantity × unitPrice`) is rounded to currency precision so values like `0.1 × 3` no longer drift to `0.30000000000000004`
39
+ - `detectProfile` in the extractor now tests longer/more-specific URNs before short forms so XRechnung CIUS and conformance URNs are recognised correctly
40
+
41
+ ### Fixed
42
+
43
+ - Always emit `ram:ActualDeliverySupplyChainEvent` (defaulting to `document.issueDate`) for non-prepayment BASIC+ invoices so the schematron's two-conjunct delivery rule (BR-FX-EN-04 / PEPPOL-EN16931-R008) is satisfied even when only a billing period is supplied
44
+ - `toXRechnung` now validates the business-process-enriched input rather than the raw input, so the auto-injected `businessProcessId` no longer trips PEPPOL-EN16931-R001 in strict mode
45
+ - `URIUniversalCommunication/URIID` is now emitted with the required `@schemeID` attribute
46
+
10
47
  ## [1.0.1] — 2026-03-01
11
48
 
12
49
  ### Added
@@ -30,4 +67,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
30
67
  - Deno compatibility test suite
31
68
 
32
69
  [Unreleased]: https://github.com/StackForge-EU/factur-x/commits/main
70
+ [1.0.4]: https://github.com/StackForge-EU/factur-x/compare/v1.0.3...v1.0.4
71
+ [1.0.3]: https://github.com/StackForge-EU/factur-x/compare/v1.0.2...v1.0.3
72
+ [1.0.2]: https://github.com/StackForge-EU/factur-x/compare/v1.0.1...v1.0.2
33
73
  [1.0.1]: https://github.com/StackForge-EU/factur-x/compare/v1.0.0...v1.0.1
package/README.md CHANGED
@@ -4,8 +4,8 @@
4
4
  [![License: EUPL-1.2](https://img.shields.io/badge/License-EUPL--1.2-blue.svg)](LICENSE)
5
5
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.3+-3178c6.svg?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
6
6
  [![Node.js](https://img.shields.io/node/v/@stackforge-eu/factur-x?logo=node.js)](https://nodejs.org)
7
- ![Deno](https://img.shields.io/badge/deno-%3E%3D2.0.0-brightgreen?logo=deno)
8
- ![JSR Version](https://img.shields.io/jsr/v/@stackforge-eu/factur-x?logo=jsr)
7
+ [![Deno](https://img.shields.io/badge/deno-%3E%3D2.0.0-brightgreen?logo=deno)](https://deno.com)
8
+ [![JSR Version](https://img.shields.io/jsr/v/@stackforge-eu/factur-x?logo=jsr)](https://jsr.io/@stackforge-eu/factur-x)
9
9
  [![npm version](https://img.shields.io/npm/v/@stackforge-eu/factur-x?logo=npm&color=cb3837)](https://www.npmjs.com/package/@stackforge-eu/factur-x)
10
10
 
11
11
  **Generate and embed Factur-X, ZUGFeRD, and XRechnung compliant CII XML into PDF/A-3 invoices.**
package/dist/index.cjs CHANGED
@@ -3,8 +3,11 @@
3
3
  var fs = require('fs');
4
4
  var path = require('path');
5
5
  var url = require('url');
6
+ var console = require('console');
6
7
  var pdfLib = require('pdf-lib');
7
8
 
9
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
10
+
8
11
  function _interopNamespace(e) {
9
12
  if (e && e.__esModule) return e;
10
13
  var n = Object.create(null);
@@ -25,6 +28,7 @@ function _interopNamespace(e) {
25
28
 
26
29
  var fs__namespace = /*#__PURE__*/_interopNamespace(fs);
27
30
  var path__namespace = /*#__PURE__*/_interopNamespace(path);
31
+ var console__default = /*#__PURE__*/_interopDefault(console);
28
32
 
29
33
  // node_modules/tsup/assets/cjs_shims.js
30
34
  var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
@@ -175,10 +179,11 @@ var Flavor = /* @__PURE__ */ ((Flavor2) => {
175
179
  var PROFILE_URNS = {
176
180
  MINIMUM: "urn:factur-x.eu:1p0:minimum",
177
181
  BASIC_WL: "urn:factur-x.eu:1p0:basicwl",
178
- BASIC: "urn:factur-x.eu:1p0:basic",
179
- EN16931: "urn:factur-x.eu:1p0:en16931",
180
- EXTENDED: "urn:factur-x.eu:1p0:extended"
182
+ BASIC: "urn:cen.eu:en16931:2017#compliant#urn:factur-x.eu:1p0:basic",
183
+ EN16931: "urn:cen.eu:en16931:2017",
184
+ EXTENDED: "urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended"
181
185
  };
186
+ var XRECHNUNG_PROFILE_URN = "urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0";
182
187
  var FLAVOR_DEFAULT_TYPE_CODES = {
183
188
  ["factur-x" /* FACTUR_X */]: "380" /* COMMERCIAL_INVOICE */,
184
189
  ["zugferd" /* ZUGFERD */]: "380" /* COMMERCIAL_INVOICE */,
@@ -348,7 +353,12 @@ function buildParty(p, profile) {
348
353
  if (p.contact && atLeast(profile, "EN16931" /* EN16931 */)) x += buildContact(p.contact);
349
354
  if (p.address && atLeast(profile, "BASIC_WL" /* BASIC_WL */)) x += buildAddress(p.address);
350
355
  if (p.electronicAddress)
351
- x += tag("ram:URIUniversalCommunication", tag("ram:URIID", escapeXml(p.electronicAddress)));
356
+ x += tag(
357
+ "ram:URIUniversalCommunication",
358
+ tag("ram:URIID", escapeXml(p.electronicAddress.value), {
359
+ schemeID: p.electronicAddress.schemeID
360
+ })
361
+ );
352
362
  if (p.taxRegistrations) {
353
363
  for (const tr of p.taxRegistrations) {
354
364
  x += tag(
@@ -394,7 +404,7 @@ function buildLineItem(line, profile) {
394
404
  tx += tag("ram:RateApplicablePercent", line.vatRatePercent.toString());
395
405
  settle += tag("ram:ApplicableTradeTax", tx);
396
406
  }
397
- const lineTotal = line.lineTotal ?? line.quantity * line.unitPrice;
407
+ const lineTotal = line.lineTotal ?? Math.round(line.quantity * line.unitPrice * 100) / 100;
398
408
  settle += tag(
399
409
  "ram:SpecifiedTradeSettlementLineMonetarySummation",
400
410
  tag("ram:LineTotalAmount", fmtAmt(lineTotal))
@@ -477,9 +487,10 @@ function buildXml(input, profile, flavor) {
477
487
  "ram:BusinessProcessSpecifiedDocumentContextParameter",
478
488
  tag("ram:ID", escapeXml(doc.businessProcessId))
479
489
  );
490
+ const profileUrn = fl === "xrechnung" /* XRECHNUNG */ ? XRECHNUNG_PROFILE_URN : PROFILE_URNS[profile];
480
491
  ctx += tag(
481
492
  "ram:GuidelineSpecifiedDocumentContextParameter",
482
- tag("ram:ID", PROFILE_URNS[profile])
493
+ tag("ram:ID", profileUrn)
483
494
  );
484
495
  let edoc = tag("ram:ID", escapeXml(doc.id));
485
496
  if (doc.name && atLeast(profile, "EXTENDED" /* EXTENDED */)) edoc += tag("ram:Name", escapeXml(doc.name));
@@ -532,6 +543,7 @@ function buildXml(input, profile, flavor) {
532
543
  }
533
544
  tx += tag("ram:ApplicableHeaderTradeAgreement", agree);
534
545
  let del = "";
546
+ let hasActualDelivery = false;
535
547
  if (input.delivery && atLeast(profile, "BASIC_WL" /* BASIC_WL */)) {
536
548
  if (input.delivery.partyName || input.delivery.location) {
537
549
  let sp = "";
@@ -539,11 +551,13 @@ function buildXml(input, profile, flavor) {
539
551
  if (input.delivery.location) sp += buildAddress(input.delivery.location);
540
552
  del += tag("ram:ShipToTradeParty", sp);
541
553
  }
542
- if (input.delivery.date)
554
+ if (input.delivery.date) {
543
555
  del += tag(
544
556
  "ram:ActualDeliverySupplyChainEvent",
545
557
  dateEl("ram:OccurrenceDateTime", input.delivery.date)
546
558
  );
559
+ hasActualDelivery = true;
560
+ }
547
561
  if (input.delivery.despatchAdviceReference)
548
562
  del += tag(
549
563
  "ram:DespatchAdviceReferencedDocument",
@@ -559,6 +573,12 @@ function buildXml(input, profile, flavor) {
559
573
  }
560
574
  }
561
575
  }
576
+ if (atLeast(profile, "BASIC" /* BASIC */) && !hasActualDelivery && typeCode !== "386" /* PREPAYMENT_INVOICE */) {
577
+ del += tag(
578
+ "ram:ActualDeliverySupplyChainEvent",
579
+ dateEl("ram:OccurrenceDateTime", doc.issueDate)
580
+ );
581
+ }
562
582
  tx += tag("ram:ApplicableHeaderTradeDelivery", del);
563
583
  let sett = "";
564
584
  if (payment?.creditorReference && atLeast(profile, "BASIC_WL" /* BASIC_WL */))
@@ -636,7 +656,7 @@ var PROFILE_LEVEL2 = {
636
656
  function addError(errors, field, message, profile) {
637
657
  errors.push({ field, message, profile });
638
658
  }
639
- function validateInput(input, profile) {
659
+ function validateInput(input, profile, flavor) {
640
660
  const errors = [];
641
661
  const level = PROFILE_LEVEL2[profile];
642
662
  const req = (minProfile) => level >= PROFILE_LEVEL2[minProfile];
@@ -823,11 +843,193 @@ function validateInput(input, profile) {
823
843
  }
824
844
  });
825
845
  }
846
+ if (req("BASIC_WL" /* BASIC_WL */)) {
847
+ for (const role of ["seller", "buyer"]) {
848
+ const ea = input[role]?.electronicAddress;
849
+ if (ea && !ea.schemeID) {
850
+ addError(
851
+ errors,
852
+ `${role}.electronicAddress.schemeID`,
853
+ `${role.charAt(0).toUpperCase() + role.slice(1)} electronic address must include schemeID (BR-62 / FX-SCH-A-000158).`,
854
+ "BASIC_WL" /* BASIC_WL */
855
+ );
856
+ }
857
+ }
858
+ }
859
+ if (req("BASIC" /* BASIC */) && input.lines) {
860
+ input.lines.forEach((line, i) => {
861
+ if (typeof line.unitPrice === "number" && line.unitPrice < 0) {
862
+ addError(
863
+ errors,
864
+ `lines[${i}].unitPrice`,
865
+ "Line unit price must not be negative (BR-27). Use allowancesCharges for discounts.",
866
+ "BASIC" /* BASIC */
867
+ );
868
+ }
869
+ });
870
+ }
871
+ if (req("BASIC_WL" /* BASIC_WL */) && input.totals) {
872
+ const t = input.totals;
873
+ if (t.taxBasisTotal != null && t.taxTotal != null && t.grandTotal != null) {
874
+ const expected = round2(t.taxBasisTotal + t.taxTotal);
875
+ if (Math.abs(round2(t.grandTotal) - expected) > 0.01) {
876
+ addError(
877
+ errors,
878
+ "totals.grandTotal",
879
+ `Grand total ${t.grandTotal} does not equal taxBasisTotal + taxTotal (${expected}) (BR-CO-15).`,
880
+ "BASIC_WL" /* BASIC_WL */
881
+ );
882
+ }
883
+ }
884
+ }
885
+ if (req("BASIC_WL" /* BASIC_WL */) && input.totals) {
886
+ const t = input.totals;
887
+ if (t.lineTotal != null && t.taxBasisTotal != null) {
888
+ const allowance = t.allowanceTotal ?? 0;
889
+ const charge = t.chargeTotal ?? 0;
890
+ const expected = round2(t.lineTotal - allowance + charge);
891
+ if (Math.abs(round2(t.taxBasisTotal) - expected) > 0.01) {
892
+ addError(
893
+ errors,
894
+ "totals.taxBasisTotal",
895
+ `taxBasisTotal ${t.taxBasisTotal} does not equal lineTotal - allowanceTotal + chargeTotal (${expected}) (BR-CO-13).`,
896
+ "BASIC_WL" /* BASIC_WL */
897
+ );
898
+ }
899
+ }
900
+ }
901
+ if (req("BASIC" /* BASIC */) && input.totals && input.lines) {
902
+ const sumLineNets = input.lines.reduce((sum, l) => {
903
+ const lineNet = l.lineTotal ?? (typeof l.quantity === "number" && typeof l.unitPrice === "number" ? l.quantity * l.unitPrice : 0);
904
+ return sum + lineNet;
905
+ }, 0);
906
+ if (input.totals.lineTotal != null) {
907
+ if (Math.abs(round2(input.totals.lineTotal) - round2(sumLineNets)) > 0.01) {
908
+ addError(
909
+ errors,
910
+ "totals.lineTotal",
911
+ `lineTotal ${input.totals.lineTotal} does not equal \u03A3 line nets (${round2(sumLineNets)}) (BR-CO-10).`,
912
+ "BASIC" /* BASIC */
913
+ );
914
+ }
915
+ }
916
+ }
917
+ if (req("BASIC_WL" /* BASIC_WL */) && input.totals?.taxTotal != null && input.vatBreakdown) {
918
+ const sumVat = input.vatBreakdown.reduce((s, v) => s + (v.taxAmount ?? 0), 0);
919
+ if (Math.abs(round2(input.totals.taxTotal) - round2(sumVat)) > 0.01) {
920
+ addError(
921
+ errors,
922
+ "totals.taxTotal",
923
+ `taxTotal ${input.totals.taxTotal} does not equal \u03A3 vatBreakdown.taxAmount (${round2(sumVat)}) (BR-CO-14).`,
924
+ "BASIC_WL" /* BASIC_WL */
925
+ );
926
+ }
927
+ }
928
+ if (req("BASIC_WL" /* BASIC_WL */) && input.vatBreakdown) {
929
+ input.vatBreakdown.forEach((vb, i) => {
930
+ if (vb.taxableAmount != null && vb.taxAmount != null && vb.ratePercent != null) {
931
+ const expected = round2(vb.taxableAmount * vb.ratePercent / 100);
932
+ if (Math.abs(round2(vb.taxAmount) - expected) > 0.01) {
933
+ addError(
934
+ errors,
935
+ `vatBreakdown[${i}].taxAmount`,
936
+ `taxAmount ${vb.taxAmount} does not equal round(taxableAmount \xD7 ratePercent / 100) = ${expected} (BR-CO-17).`,
937
+ "BASIC_WL" /* BASIC_WL */
938
+ );
939
+ }
940
+ }
941
+ });
942
+ }
943
+ if (req("BASIC" /* BASIC */) && input.document) {
944
+ const isPrepayment = input.document.typeCode === "386";
945
+ if (!isPrepayment) {
946
+ const hasDeliveryDate = !!input.delivery?.date;
947
+ const hasBillingPeriod = !!input.billingPeriod;
948
+ if (!hasDeliveryDate && !hasBillingPeriod) {
949
+ addError(
950
+ errors,
951
+ "delivery.date",
952
+ "Provide delivery.date or billingPeriod (BR-FX-EN-04).",
953
+ "BASIC" /* BASIC */
954
+ );
955
+ }
956
+ }
957
+ }
958
+ if (flavor === "xrechnung" /* XRECHNUNG */) {
959
+ if (!input.document?.buyerReference) {
960
+ addError(
961
+ errors,
962
+ "document.buyerReference",
963
+ "BuyerReference (BT-10) is required for XRechnung (BR-DE-15).",
964
+ "EN16931" /* EN16931 */
965
+ );
966
+ }
967
+ if (!input.document?.businessProcessId) {
968
+ addError(
969
+ errors,
970
+ "document.businessProcessId",
971
+ "Business process (BT-23) is required for XRechnung (PEPPOL-EN16931-R001).",
972
+ "EN16931" /* EN16931 */
973
+ );
974
+ }
975
+ if (!input.seller?.contact || !input.seller.contact.name && !input.seller.contact.email && !input.seller.contact.phone) {
976
+ addError(
977
+ errors,
978
+ "seller.contact",
979
+ "Seller contact (BG-6) with name, email or phone is required for XRechnung (BR-DE-2).",
980
+ "EN16931" /* EN16931 */
981
+ );
982
+ }
983
+ if (!input.seller?.electronicAddress?.value || !input.seller.electronicAddress.schemeID) {
984
+ addError(
985
+ errors,
986
+ "seller.electronicAddress",
987
+ "Seller electronic address (BT-34) with non-empty schemeID is required for XRechnung (PEPPOL-EN16931-R020 / BR-62).",
988
+ "EN16931" /* EN16931 */
989
+ );
990
+ }
991
+ if (!input.buyer?.electronicAddress?.value || !input.buyer.electronicAddress.schemeID) {
992
+ addError(
993
+ errors,
994
+ "buyer.electronicAddress",
995
+ "Buyer electronic address (BT-49) with non-empty schemeID is required for XRechnung (PEPPOL-EN16931-R010 / BR-62).",
996
+ "EN16931" /* EN16931 */
997
+ );
998
+ }
999
+ if (input.payment?.meansCode === "58" && input.payment.iban) {
1000
+ if (!isValidIban(input.payment.iban)) {
1001
+ addError(
1002
+ errors,
1003
+ "payment.iban",
1004
+ "IBAN is not syntactically valid (BR-DE-19).",
1005
+ "EN16931" /* EN16931 */
1006
+ );
1007
+ }
1008
+ }
1009
+ }
826
1010
  return {
827
1011
  valid: errors.length === 0,
828
1012
  errors
829
1013
  };
830
1014
  }
1015
+ function round2(n) {
1016
+ return Math.round(n * 100) / 100;
1017
+ }
1018
+ function isValidIban(iban) {
1019
+ const normalized = iban.replace(/\s+/g, "").toUpperCase();
1020
+ if (!/^[A-Z]{2}\d{2}[A-Z0-9]{1,30}$/.test(normalized)) return false;
1021
+ const rearranged = normalized.slice(4) + normalized.slice(0, 4);
1022
+ let expanded = "";
1023
+ for (const ch of rearranged) {
1024
+ expanded += /[A-Z]/.test(ch) ? String(ch.charCodeAt(0) - 55) : ch;
1025
+ }
1026
+ let remainder = 0;
1027
+ for (let i = 0; i < expanded.length; i += 7) {
1028
+ const chunk = remainder.toString() + expanded.slice(i, i + 7);
1029
+ remainder = parseInt(chunk, 10) % 97;
1030
+ }
1031
+ return remainder === 1;
1032
+ }
831
1033
  function findSchemaDir(startDir) {
832
1034
  let dir = startDir;
833
1035
  for (let i = 0; i < 6; i++) {
@@ -909,6 +1111,71 @@ var AF_RELATIONSHIP_MAP = {
909
1111
  Data: pdfLib.AFRelationship.Data,
910
1112
  Source: pdfLib.AFRelationship.Source
911
1113
  };
1114
+ function ensureOutputIntent(pdfDoc, iccBytes, identifier) {
1115
+ const existing = pdfDoc.catalog.lookup(pdfLib.PDFName.of("OutputIntents"));
1116
+ if (existing instanceof pdfLib.PDFArray && existing.size() > 0) return;
1117
+ const iccStream = pdfDoc.context.flateStream(iccBytes, { N: 3 });
1118
+ const iccRef = pdfDoc.context.register(iccStream);
1119
+ const intent = pdfDoc.context.obj({
1120
+ Type: "OutputIntent",
1121
+ S: "GTS_PDFA1",
1122
+ OutputConditionIdentifier: pdfLib.PDFString.of(identifier),
1123
+ RegistryName: pdfLib.PDFString.of("http://www.color.org"),
1124
+ Info: pdfLib.PDFString.of(identifier),
1125
+ DestOutputProfile: iccRef
1126
+ });
1127
+ const intents = pdfLib.PDFArray.withContext(pdfDoc.context);
1128
+ intents.push(intent);
1129
+ pdfDoc.catalog.set(pdfLib.PDFName.of("OutputIntents"), intents);
1130
+ }
1131
+ function findUnembeddedFonts(pdfDoc) {
1132
+ const seen = /* @__PURE__ */ new Set();
1133
+ const unembedded = /* @__PURE__ */ new Set();
1134
+ const FONT_FILE_KEYS = [
1135
+ pdfLib.PDFName.of("FontFile"),
1136
+ pdfLib.PDFName.of("FontFile2"),
1137
+ pdfLib.PDFName.of("FontFile3")
1138
+ ];
1139
+ const inspectFontDict = (font) => {
1140
+ const subtype = font.lookup(pdfLib.PDFName.of("Subtype"));
1141
+ const baseFont = font.lookup(pdfLib.PDFName.of("BaseFont"));
1142
+ const baseFontName = baseFont instanceof pdfLib.PDFName ? baseFont.asString() : "<unknown>";
1143
+ const subtypeName = subtype instanceof pdfLib.PDFName ? subtype.asString() : "";
1144
+ if (subtypeName === "/Type0") {
1145
+ const desc = font.lookup(pdfLib.PDFName.of("DescendantFonts"));
1146
+ if (desc instanceof pdfLib.PDFArray) {
1147
+ for (let i = 0; i < desc.size(); i += 1) {
1148
+ const child = desc.lookup(i);
1149
+ if (child instanceof pdfLib.PDFDict) inspectFontDict(child);
1150
+ }
1151
+ }
1152
+ return;
1153
+ }
1154
+ const descriptor = font.lookup(pdfLib.PDFName.of("FontDescriptor"));
1155
+ if (!(descriptor instanceof pdfLib.PDFDict)) {
1156
+ unembedded.add(baseFontName);
1157
+ return;
1158
+ }
1159
+ const hasEmbedded = FONT_FILE_KEYS.some((k) => descriptor.has(k));
1160
+ if (!hasEmbedded) unembedded.add(baseFontName);
1161
+ };
1162
+ for (const page of pdfDoc.getPages()) {
1163
+ const resources = page.node.Resources();
1164
+ if (!resources) continue;
1165
+ const fonts = resources.lookup(pdfLib.PDFName.of("Font"));
1166
+ if (!(fonts instanceof pdfLib.PDFDict)) continue;
1167
+ for (const [, value] of fonts.entries()) {
1168
+ const ref = value instanceof pdfLib.PDFRef ? value : void 0;
1169
+ if (ref) {
1170
+ if (seen.has(ref)) continue;
1171
+ seen.add(ref);
1172
+ }
1173
+ const font = ref ? pdfDoc.context.lookup(ref) : value;
1174
+ if (font instanceof pdfLib.PDFDict) inspectFontDict(font);
1175
+ }
1176
+ }
1177
+ return Array.from(unembedded);
1178
+ }
912
1179
  function resolveAfRelationship(profile, override) {
913
1180
  if (override) return AF_RELATIONSHIP_MAP[override];
914
1181
  switch (profile) {
@@ -1025,6 +1292,9 @@ async function embedFacturX(options) {
1025
1292
  validateBeforeEmbed = true,
1026
1293
  validateXsd: runXsdValidation = false,
1027
1294
  addPdfA3Metadata = true,
1295
+ rgbIccProfile,
1296
+ outputIntentIdentifier = "sRGB IEC61966-2.1",
1297
+ unembeddedFonts = "warn",
1028
1298
  meta
1029
1299
  } = options;
1030
1300
  if (!options.xml && !options.input) {
@@ -1038,7 +1308,7 @@ async function embedFacturX(options) {
1038
1308
  if (!xml) {
1039
1309
  const input = options.input;
1040
1310
  if (validateBeforeEmbed) {
1041
- validation = validateInput(input, profile);
1311
+ validation = validateInput(input, profile, flavor);
1042
1312
  if (!validation.valid) {
1043
1313
  throw new Error(
1044
1314
  `Input validation failed for profile "${profile}":
@@ -1079,6 +1349,22 @@ async function embedFacturX(options) {
1079
1349
  pdfDoc.setCreationDate(now);
1080
1350
  pdfDoc.setModificationDate(now);
1081
1351
  if (addPdfA3Metadata) {
1352
+ if (rgbIccProfile) {
1353
+ const iccBytes = rgbIccProfile instanceof Uint8Array ? rgbIccProfile : new Uint8Array(rgbIccProfile);
1354
+ ensureOutputIntent(pdfDoc, iccBytes, outputIntentIdentifier);
1355
+ } else if (!(pdfDoc.catalog.lookup(pdfLib.PDFName.of("OutputIntents")) instanceof pdfLib.PDFArray)) {
1356
+ console__default.default.warn(
1357
+ "[factur-x] addPdfA3Metadata is enabled but no rgbIccProfile was supplied and the input PDF has no /OutputIntents. The resulting PDF will not be PDF/A-3 conformant."
1358
+ );
1359
+ }
1360
+ if (unembeddedFonts !== "ignore") {
1361
+ const missing = findUnembeddedFonts(pdfDoc);
1362
+ if (missing.length > 0) {
1363
+ const message = `[factur-x] PDF/A-3 requires every font program to be embedded; the following fonts are not embedded: ${missing.join(", ")}. Configure your PDF generator to embed all fonts.`;
1364
+ if (unembeddedFonts === "throw") throw new Error(message);
1365
+ console__default.default.warn(message);
1366
+ }
1367
+ }
1082
1368
  const isoDate = now.toISOString();
1083
1369
  const xmpXml = buildXmpMetadata({
1084
1370
  title,
@@ -1125,18 +1411,22 @@ function resolve(doc, obj) {
1125
1411
  return obj;
1126
1412
  }
1127
1413
  function detectProfile(xml) {
1128
- const profileMap = {
1129
- "urn:factur-x.eu:1p0:minimum": "MINIMUM" /* MINIMUM */,
1130
- "urn:factur-x.eu:1p0:basicwl": "BASIC_WL" /* BASIC_WL */,
1131
- "urn:factur-x.eu:1p0:basic": "BASIC" /* BASIC */,
1132
- "urn:factur-x.eu:1p0:en16931": "EN16931" /* EN16931 */,
1133
- "urn:factur-x.eu:1p0:extended": "EXTENDED" /* EXTENDED */,
1134
- "urn:cen.eu:en16931:2017": "EN16931" /* EN16931 */,
1135
- "urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0": "EN16931" /* EN16931 */,
1136
- "urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_2.3": "EN16931" /* EN16931 */,
1137
- "urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_2.2": "EN16931" /* EN16931 */
1138
- };
1139
- for (const [urn, profile] of Object.entries(profileMap)) {
1414
+ const profileMap = [
1415
+ ["urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended", "EXTENDED" /* EXTENDED */],
1416
+ ["urn:cen.eu:en16931:2017#conformant#urn:zugferd.de:2p0:extended", "EXTENDED" /* EXTENDED */],
1417
+ ["urn:cen.eu:en16931:2017#compliant#urn:factur-x.eu:1p0:basic", "BASIC" /* BASIC */],
1418
+ ["urn:cen.eu:en16931:2017#compliant#urn:zugferd.de:2p0:basic", "BASIC" /* BASIC */],
1419
+ ["urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0", "EN16931" /* EN16931 */],
1420
+ ["urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_2.3", "EN16931" /* EN16931 */],
1421
+ ["urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_2.2", "EN16931" /* EN16931 */],
1422
+ ["urn:cen.eu:en16931:2017", "EN16931" /* EN16931 */],
1423
+ ["urn:factur-x.eu:1p0:minimum", "MINIMUM" /* MINIMUM */],
1424
+ ["urn:factur-x.eu:1p0:basicwl", "BASIC_WL" /* BASIC_WL */],
1425
+ ["urn:factur-x.eu:1p0:basic", "BASIC" /* BASIC */],
1426
+ ["urn:factur-x.eu:1p0:en16931", "EN16931" /* EN16931 */],
1427
+ ["urn:factur-x.eu:1p0:extended", "EXTENDED" /* EXTENDED */]
1428
+ ];
1429
+ for (const [urn, profile] of profileMap) {
1140
1430
  if (xml.includes(urn)) return profile;
1141
1431
  }
1142
1432
  return void 0;
@@ -1204,9 +1494,16 @@ function toXRechnung(input, options) {
1204
1494
  const profile = options?.profile ?? "EN16931" /* EN16931 */;
1205
1495
  const validate = options?.validate ?? true;
1206
1496
  validateFlavorProfile("xrechnung" /* XRECHNUNG */, profile);
1497
+ const enrichedInput = {
1498
+ ...input,
1499
+ document: {
1500
+ ...input.document,
1501
+ businessProcessId: resolveBusinessProcessUrn(input, "xrechnung" /* XRECHNUNG */)
1502
+ }
1503
+ };
1207
1504
  let validation;
1208
1505
  if (validate) {
1209
- validation = validateInput(input, profile);
1506
+ validation = validateInput(enrichedInput, profile, "xrechnung" /* XRECHNUNG */);
1210
1507
  if (!validation.valid) {
1211
1508
  throw new Error(
1212
1509
  `Input validation failed for XRechnung (profile "${profile}"):
@@ -1214,13 +1511,6 @@ function toXRechnung(input, options) {
1214
1511
  );
1215
1512
  }
1216
1513
  }
1217
- const enrichedInput = {
1218
- ...input,
1219
- document: {
1220
- ...input.document,
1221
- businessProcessId: resolveBusinessProcessUrn(input, "xrechnung" /* XRECHNUNG */)
1222
- }
1223
- };
1224
1514
  const xml = buildXml(enrichedInput, profile, "xrechnung" /* XRECHNUNG */);
1225
1515
  return {
1226
1516
  xml,
@@ -1326,6 +1616,7 @@ exports.PROFILE_URNS = PROFILE_URNS;
1326
1616
  exports.Profile = Profile;
1327
1617
  exports.UnitCode = UnitCode;
1328
1618
  exports.VatCategoryCode = VatCategoryCode;
1619
+ exports.XRECHNUNG_PROFILE_URN = XRECHNUNG_PROFILE_URN;
1329
1620
  exports.buildXml = buildXml;
1330
1621
  exports.embedFacturX = embedFacturX;
1331
1622
  exports.escapeXml = escapeXml;
package/dist/index.d.cts CHANGED
@@ -480,10 +480,22 @@ interface TradePartyInput {
480
480
  */
481
481
  contact?: ContactInput;
482
482
  /**
483
- * Electronic address (e.g. email or URI for e-delivery).
483
+ * Electronic address (e.g. PEPPOL participant ID, email).
484
+ *
485
+ * `value` is the address itself (e.g. `"info@stack-forge.eu"`,
486
+ * `"0088:1234567890123"`).
487
+ * `schemeID` is the EAS code list value (e.g. `"EM"` for email,
488
+ * `"9930"` for German VAT, `"0088"` for GLN). Required by BR-62 /
489
+ * FX-SCH-A-000158 — a URIID without `@schemeID` is rejected by every
490
+ * conforming validator (Mustang, KoSIT, veraPDF).
491
+ *
484
492
  * @see EN 16931 BT-34 / BT-49
493
+ * @see https://docs.peppol.eu/poacc/billing/3.0/codelist/eas/
485
494
  */
486
- electronicAddress?: string;
495
+ electronicAddress?: {
496
+ value: string;
497
+ schemeID: string;
498
+ };
487
499
  /**
488
500
  * Tax registrations (VAT ID, fiscal number).
489
501
  * @see EN 16931 BT-31 / BT-48
@@ -970,8 +982,20 @@ declare enum Flavor {
970
982
  XRECHNUNG = "xrechnung",
971
983
  CHRONO_PRO = "chrono-pro"
972
984
  }
973
- /** Profile URNs (GuidelineSpecifiedDocumentContextParameter) */
985
+ /**
986
+ * Profile URNs (GuidelineSpecifiedDocumentContextParameter / BT-24).
987
+ *
988
+ * BASIC, EN16931, and EXTENDED use the EN 16931 conformance prefix because
989
+ * those profiles are CIUS / extension claims against the European norm. The
990
+ * exact strings are the only values accepted by the schematron shipped under
991
+ * `schema/{basic,en16931,extended}/` (the codedb cl id="1" enumeration).
992
+ */
974
993
  declare const PROFILE_URNS: Record<Profile, string>;
994
+ /**
995
+ * XRechnung CIUS URN (BR-DE-21). Used as the profile URN when emitting
996
+ * XRechnung instead of pure Factur-X / ZUGFeRD.
997
+ */
998
+ declare const XRECHNUNG_PROFILE_URN = "urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0";
975
999
 
976
1000
  /**
977
1001
  * Default document type code per flavor.
@@ -1099,11 +1123,13 @@ interface ValidationResult {
1099
1123
  * Validates that the input contains all required fields for the given profile.
1100
1124
  * Requirements are cumulative: each profile includes all requirements of lower levels.
1101
1125
  *
1102
- * @param input - Invoice input to validate
1126
+ * @param input - Invoice input to validate
1103
1127
  * @param profile - Target profile (MINIMUM, BASIC_WL, BASIC, EN16931, or EXTENDED)
1128
+ * @param flavor - Optional flavor; when {@link Flavor.XRECHNUNG}, additional
1129
+ * BR-DE / PEPPOL-EN16931 rules are applied on top of the profile rules.
1104
1130
  * @returns Validation result with valid flag and any errors
1105
1131
  */
1106
- declare function validateInput(input: FacturXInvoiceInput, profile: Profile): ValidationResult;
1132
+ declare function validateInput(input: FacturXInvoiceInput, profile: Profile, flavor?: Flavor): ValidationResult;
1107
1133
 
1108
1134
  /**
1109
1135
  * XSD Schema Validation
@@ -1203,6 +1229,36 @@ interface EmbedOptions {
1203
1229
  validateXsd?: boolean;
1204
1230
  /** Add PDF/A-3 metadata (default: `true`). */
1205
1231
  addPdfA3Metadata?: boolean;
1232
+ /**
1233
+ * RGB ICC profile bytes used to inject the PDF/A required `/OutputIntents`
1234
+ * entry when the input PDF does not already have one.
1235
+ *
1236
+ * PDF/A-3 forbids DeviceRGB without an OutputIntent that references an
1237
+ * RGB destination profile (ISO 19005-3 §6.2.4.3). pdf-lib does not produce
1238
+ * PDF/A by default, so this library will add the entry on save when this
1239
+ * profile is supplied.
1240
+ *
1241
+ * Drop in any sRGB ICC v2 profile compatible with your licensing needs
1242
+ * (e.g. the ICC.org public profile, or one bundled with Argyll CMS / PDFBox).
1243
+ * When omitted and {@link addPdfA3Metadata} is `true`, the library logs a
1244
+ * warning and the resulting PDF will not be PDF/A-3 conformant.
1245
+ */
1246
+ rgbIccProfile?: Uint8Array | Buffer;
1247
+ /**
1248
+ * Output intent identifier written into the OutputIntent dictionary.
1249
+ * Defaults to `"sRGB IEC61966-2.1"` which matches the standard sRGB profile.
1250
+ */
1251
+ outputIntentIdentifier?: string;
1252
+ /**
1253
+ * How to react when the input PDF references fonts that are not embedded.
1254
+ * PDF/A-3 requires every font to be embedded (ISO 19005-3 §6.2.11.4.1) —
1255
+ * this library cannot fix that post-hoc, so it surfaces the gap.
1256
+ *
1257
+ * - `"warn"` (default): log a warning to `console.warn` and continue.
1258
+ * - `"throw"`: reject the PDF with an error.
1259
+ * - `"ignore"`: skip the check entirely.
1260
+ */
1261
+ unembeddedFonts?: "warn" | "throw" | "ignore";
1206
1262
  /**
1207
1263
  * Override the AF relationship for the embedded XML attachment.
1208
1264
  *
@@ -1379,4 +1435,4 @@ interface XRechnungResult {
1379
1435
  */
1380
1436
  declare function toXRechnung(input: FacturXInvoiceInput, options?: XRechnungOptions): XRechnungResult;
1381
1437
 
1382
- export { type AddressInput, type AllowanceChargeInput, type BillingPeriodInput, type ContactInput, type DeliveryInput, type DocumentReferenceInput, DocumentTypeCode, type EmbedOptions, type EmbedResult, type ExtractOptions, type ExtractResult, FLAVOR_CONFIGS, FLAVOR_DEFAULT_TYPE_CODES, type FacturXInvoiceInput, Flavor, type FlavorConfig, type InvoiceDocumentInput, type InvoiceLineInput, type InvoiceTotalsInput, type LegalOrganizationInput, type NoteInput, PROFILE_MAIN_XSD, PROFILE_SCHEMA_DIRS, PROFILE_URNS, type PaymentInput, Profile, type TaxRegistrationInput, type TradePartyInput, UnitCode, type ValidationError, type ValidationResult, type VatBreakdownInput, VatCategoryCode, type XRechnungOptions, type XRechnungResult, type XsdValidateOptions, type XsdValidationError, type XsdValidationResult, buildXml, embedFacturX, escapeXml, extractXml, getFlavorConfig, resolveBusinessProcessUrn, resolveTypeCode, toXRechnung, validateFlavorProfile, validateInput, validateXsd };
1438
+ export { type AddressInput, type AllowanceChargeInput, type BillingPeriodInput, type ContactInput, type DeliveryInput, type DocumentReferenceInput, DocumentTypeCode, type EmbedOptions, type EmbedResult, type ExtractOptions, type ExtractResult, FLAVOR_CONFIGS, FLAVOR_DEFAULT_TYPE_CODES, type FacturXInvoiceInput, Flavor, type FlavorConfig, type InvoiceDocumentInput, type InvoiceLineInput, type InvoiceTotalsInput, type LegalOrganizationInput, type NoteInput, PROFILE_MAIN_XSD, PROFILE_SCHEMA_DIRS, PROFILE_URNS, type PaymentInput, Profile, type TaxRegistrationInput, type TradePartyInput, UnitCode, type ValidationError, type ValidationResult, type VatBreakdownInput, VatCategoryCode, XRECHNUNG_PROFILE_URN, type XRechnungOptions, type XRechnungResult, type XsdValidateOptions, type XsdValidationError, type XsdValidationResult, buildXml, embedFacturX, escapeXml, extractXml, getFlavorConfig, resolveBusinessProcessUrn, resolveTypeCode, toXRechnung, validateFlavorProfile, validateInput, validateXsd };
package/dist/index.d.ts CHANGED
@@ -480,10 +480,22 @@ interface TradePartyInput {
480
480
  */
481
481
  contact?: ContactInput;
482
482
  /**
483
- * Electronic address (e.g. email or URI for e-delivery).
483
+ * Electronic address (e.g. PEPPOL participant ID, email).
484
+ *
485
+ * `value` is the address itself (e.g. `"info@stack-forge.eu"`,
486
+ * `"0088:1234567890123"`).
487
+ * `schemeID` is the EAS code list value (e.g. `"EM"` for email,
488
+ * `"9930"` for German VAT, `"0088"` for GLN). Required by BR-62 /
489
+ * FX-SCH-A-000158 — a URIID without `@schemeID` is rejected by every
490
+ * conforming validator (Mustang, KoSIT, veraPDF).
491
+ *
484
492
  * @see EN 16931 BT-34 / BT-49
493
+ * @see https://docs.peppol.eu/poacc/billing/3.0/codelist/eas/
485
494
  */
486
- electronicAddress?: string;
495
+ electronicAddress?: {
496
+ value: string;
497
+ schemeID: string;
498
+ };
487
499
  /**
488
500
  * Tax registrations (VAT ID, fiscal number).
489
501
  * @see EN 16931 BT-31 / BT-48
@@ -970,8 +982,20 @@ declare enum Flavor {
970
982
  XRECHNUNG = "xrechnung",
971
983
  CHRONO_PRO = "chrono-pro"
972
984
  }
973
- /** Profile URNs (GuidelineSpecifiedDocumentContextParameter) */
985
+ /**
986
+ * Profile URNs (GuidelineSpecifiedDocumentContextParameter / BT-24).
987
+ *
988
+ * BASIC, EN16931, and EXTENDED use the EN 16931 conformance prefix because
989
+ * those profiles are CIUS / extension claims against the European norm. The
990
+ * exact strings are the only values accepted by the schematron shipped under
991
+ * `schema/{basic,en16931,extended}/` (the codedb cl id="1" enumeration).
992
+ */
974
993
  declare const PROFILE_URNS: Record<Profile, string>;
994
+ /**
995
+ * XRechnung CIUS URN (BR-DE-21). Used as the profile URN when emitting
996
+ * XRechnung instead of pure Factur-X / ZUGFeRD.
997
+ */
998
+ declare const XRECHNUNG_PROFILE_URN = "urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0";
975
999
 
976
1000
  /**
977
1001
  * Default document type code per flavor.
@@ -1099,11 +1123,13 @@ interface ValidationResult {
1099
1123
  * Validates that the input contains all required fields for the given profile.
1100
1124
  * Requirements are cumulative: each profile includes all requirements of lower levels.
1101
1125
  *
1102
- * @param input - Invoice input to validate
1126
+ * @param input - Invoice input to validate
1103
1127
  * @param profile - Target profile (MINIMUM, BASIC_WL, BASIC, EN16931, or EXTENDED)
1128
+ * @param flavor - Optional flavor; when {@link Flavor.XRECHNUNG}, additional
1129
+ * BR-DE / PEPPOL-EN16931 rules are applied on top of the profile rules.
1104
1130
  * @returns Validation result with valid flag and any errors
1105
1131
  */
1106
- declare function validateInput(input: FacturXInvoiceInput, profile: Profile): ValidationResult;
1132
+ declare function validateInput(input: FacturXInvoiceInput, profile: Profile, flavor?: Flavor): ValidationResult;
1107
1133
 
1108
1134
  /**
1109
1135
  * XSD Schema Validation
@@ -1203,6 +1229,36 @@ interface EmbedOptions {
1203
1229
  validateXsd?: boolean;
1204
1230
  /** Add PDF/A-3 metadata (default: `true`). */
1205
1231
  addPdfA3Metadata?: boolean;
1232
+ /**
1233
+ * RGB ICC profile bytes used to inject the PDF/A required `/OutputIntents`
1234
+ * entry when the input PDF does not already have one.
1235
+ *
1236
+ * PDF/A-3 forbids DeviceRGB without an OutputIntent that references an
1237
+ * RGB destination profile (ISO 19005-3 §6.2.4.3). pdf-lib does not produce
1238
+ * PDF/A by default, so this library will add the entry on save when this
1239
+ * profile is supplied.
1240
+ *
1241
+ * Drop in any sRGB ICC v2 profile compatible with your licensing needs
1242
+ * (e.g. the ICC.org public profile, or one bundled with Argyll CMS / PDFBox).
1243
+ * When omitted and {@link addPdfA3Metadata} is `true`, the library logs a
1244
+ * warning and the resulting PDF will not be PDF/A-3 conformant.
1245
+ */
1246
+ rgbIccProfile?: Uint8Array | Buffer;
1247
+ /**
1248
+ * Output intent identifier written into the OutputIntent dictionary.
1249
+ * Defaults to `"sRGB IEC61966-2.1"` which matches the standard sRGB profile.
1250
+ */
1251
+ outputIntentIdentifier?: string;
1252
+ /**
1253
+ * How to react when the input PDF references fonts that are not embedded.
1254
+ * PDF/A-3 requires every font to be embedded (ISO 19005-3 §6.2.11.4.1) —
1255
+ * this library cannot fix that post-hoc, so it surfaces the gap.
1256
+ *
1257
+ * - `"warn"` (default): log a warning to `console.warn` and continue.
1258
+ * - `"throw"`: reject the PDF with an error.
1259
+ * - `"ignore"`: skip the check entirely.
1260
+ */
1261
+ unembeddedFonts?: "warn" | "throw" | "ignore";
1206
1262
  /**
1207
1263
  * Override the AF relationship for the embedded XML attachment.
1208
1264
  *
@@ -1379,4 +1435,4 @@ interface XRechnungResult {
1379
1435
  */
1380
1436
  declare function toXRechnung(input: FacturXInvoiceInput, options?: XRechnungOptions): XRechnungResult;
1381
1437
 
1382
- export { type AddressInput, type AllowanceChargeInput, type BillingPeriodInput, type ContactInput, type DeliveryInput, type DocumentReferenceInput, DocumentTypeCode, type EmbedOptions, type EmbedResult, type ExtractOptions, type ExtractResult, FLAVOR_CONFIGS, FLAVOR_DEFAULT_TYPE_CODES, type FacturXInvoiceInput, Flavor, type FlavorConfig, type InvoiceDocumentInput, type InvoiceLineInput, type InvoiceTotalsInput, type LegalOrganizationInput, type NoteInput, PROFILE_MAIN_XSD, PROFILE_SCHEMA_DIRS, PROFILE_URNS, type PaymentInput, Profile, type TaxRegistrationInput, type TradePartyInput, UnitCode, type ValidationError, type ValidationResult, type VatBreakdownInput, VatCategoryCode, type XRechnungOptions, type XRechnungResult, type XsdValidateOptions, type XsdValidationError, type XsdValidationResult, buildXml, embedFacturX, escapeXml, extractXml, getFlavorConfig, resolveBusinessProcessUrn, resolveTypeCode, toXRechnung, validateFlavorProfile, validateInput, validateXsd };
1438
+ export { type AddressInput, type AllowanceChargeInput, type BillingPeriodInput, type ContactInput, type DeliveryInput, type DocumentReferenceInput, DocumentTypeCode, type EmbedOptions, type EmbedResult, type ExtractOptions, type ExtractResult, FLAVOR_CONFIGS, FLAVOR_DEFAULT_TYPE_CODES, type FacturXInvoiceInput, Flavor, type FlavorConfig, type InvoiceDocumentInput, type InvoiceLineInput, type InvoiceTotalsInput, type LegalOrganizationInput, type NoteInput, PROFILE_MAIN_XSD, PROFILE_SCHEMA_DIRS, PROFILE_URNS, type PaymentInput, Profile, type TaxRegistrationInput, type TradePartyInput, UnitCode, type ValidationError, type ValidationResult, type VatBreakdownInput, VatCategoryCode, XRECHNUNG_PROFILE_URN, type XRechnungOptions, type XRechnungResult, type XsdValidateOptions, type XsdValidationError, type XsdValidationResult, buildXml, embedFacturX, escapeXml, extractXml, getFlavorConfig, resolveBusinessProcessUrn, resolveTypeCode, toXRechnung, validateFlavorProfile, validateInput, validateXsd };
package/dist/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
  import { fileURLToPath } from 'url';
4
- import { AFRelationship, PDFDocument, PDFName, PDFDict, PDFStream, PDFRawStream, decodePDFRawStream, PDFArray, PDFRef, PDFString, PDFHexString } from 'pdf-lib';
4
+ import console from 'console';
5
+ import { AFRelationship, PDFDocument, PDFName, PDFArray, PDFDict, PDFStream, PDFRawStream, decodePDFRawStream, PDFString, PDFRef, PDFHexString } from 'pdf-lib';
5
6
 
6
7
  // src/types/input.ts
7
8
  var DocumentTypeCode = /* @__PURE__ */ ((DocumentTypeCode2) => {
@@ -148,10 +149,11 @@ var Flavor = /* @__PURE__ */ ((Flavor2) => {
148
149
  var PROFILE_URNS = {
149
150
  MINIMUM: "urn:factur-x.eu:1p0:minimum",
150
151
  BASIC_WL: "urn:factur-x.eu:1p0:basicwl",
151
- BASIC: "urn:factur-x.eu:1p0:basic",
152
- EN16931: "urn:factur-x.eu:1p0:en16931",
153
- EXTENDED: "urn:factur-x.eu:1p0:extended"
152
+ BASIC: "urn:cen.eu:en16931:2017#compliant#urn:factur-x.eu:1p0:basic",
153
+ EN16931: "urn:cen.eu:en16931:2017",
154
+ EXTENDED: "urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended"
154
155
  };
156
+ var XRECHNUNG_PROFILE_URN = "urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0";
155
157
  var FLAVOR_DEFAULT_TYPE_CODES = {
156
158
  ["factur-x" /* FACTUR_X */]: "380" /* COMMERCIAL_INVOICE */,
157
159
  ["zugferd" /* ZUGFERD */]: "380" /* COMMERCIAL_INVOICE */,
@@ -321,7 +323,12 @@ function buildParty(p, profile) {
321
323
  if (p.contact && atLeast(profile, "EN16931" /* EN16931 */)) x += buildContact(p.contact);
322
324
  if (p.address && atLeast(profile, "BASIC_WL" /* BASIC_WL */)) x += buildAddress(p.address);
323
325
  if (p.electronicAddress)
324
- x += tag("ram:URIUniversalCommunication", tag("ram:URIID", escapeXml(p.electronicAddress)));
326
+ x += tag(
327
+ "ram:URIUniversalCommunication",
328
+ tag("ram:URIID", escapeXml(p.electronicAddress.value), {
329
+ schemeID: p.electronicAddress.schemeID
330
+ })
331
+ );
325
332
  if (p.taxRegistrations) {
326
333
  for (const tr of p.taxRegistrations) {
327
334
  x += tag(
@@ -367,7 +374,7 @@ function buildLineItem(line, profile) {
367
374
  tx += tag("ram:RateApplicablePercent", line.vatRatePercent.toString());
368
375
  settle += tag("ram:ApplicableTradeTax", tx);
369
376
  }
370
- const lineTotal = line.lineTotal ?? line.quantity * line.unitPrice;
377
+ const lineTotal = line.lineTotal ?? Math.round(line.quantity * line.unitPrice * 100) / 100;
371
378
  settle += tag(
372
379
  "ram:SpecifiedTradeSettlementLineMonetarySummation",
373
380
  tag("ram:LineTotalAmount", fmtAmt(lineTotal))
@@ -450,9 +457,10 @@ function buildXml(input, profile, flavor) {
450
457
  "ram:BusinessProcessSpecifiedDocumentContextParameter",
451
458
  tag("ram:ID", escapeXml(doc.businessProcessId))
452
459
  );
460
+ const profileUrn = fl === "xrechnung" /* XRECHNUNG */ ? XRECHNUNG_PROFILE_URN : PROFILE_URNS[profile];
453
461
  ctx += tag(
454
462
  "ram:GuidelineSpecifiedDocumentContextParameter",
455
- tag("ram:ID", PROFILE_URNS[profile])
463
+ tag("ram:ID", profileUrn)
456
464
  );
457
465
  let edoc = tag("ram:ID", escapeXml(doc.id));
458
466
  if (doc.name && atLeast(profile, "EXTENDED" /* EXTENDED */)) edoc += tag("ram:Name", escapeXml(doc.name));
@@ -505,6 +513,7 @@ function buildXml(input, profile, flavor) {
505
513
  }
506
514
  tx += tag("ram:ApplicableHeaderTradeAgreement", agree);
507
515
  let del = "";
516
+ let hasActualDelivery = false;
508
517
  if (input.delivery && atLeast(profile, "BASIC_WL" /* BASIC_WL */)) {
509
518
  if (input.delivery.partyName || input.delivery.location) {
510
519
  let sp = "";
@@ -512,11 +521,13 @@ function buildXml(input, profile, flavor) {
512
521
  if (input.delivery.location) sp += buildAddress(input.delivery.location);
513
522
  del += tag("ram:ShipToTradeParty", sp);
514
523
  }
515
- if (input.delivery.date)
524
+ if (input.delivery.date) {
516
525
  del += tag(
517
526
  "ram:ActualDeliverySupplyChainEvent",
518
527
  dateEl("ram:OccurrenceDateTime", input.delivery.date)
519
528
  );
529
+ hasActualDelivery = true;
530
+ }
520
531
  if (input.delivery.despatchAdviceReference)
521
532
  del += tag(
522
533
  "ram:DespatchAdviceReferencedDocument",
@@ -532,6 +543,12 @@ function buildXml(input, profile, flavor) {
532
543
  }
533
544
  }
534
545
  }
546
+ if (atLeast(profile, "BASIC" /* BASIC */) && !hasActualDelivery && typeCode !== "386" /* PREPAYMENT_INVOICE */) {
547
+ del += tag(
548
+ "ram:ActualDeliverySupplyChainEvent",
549
+ dateEl("ram:OccurrenceDateTime", doc.issueDate)
550
+ );
551
+ }
535
552
  tx += tag("ram:ApplicableHeaderTradeDelivery", del);
536
553
  let sett = "";
537
554
  if (payment?.creditorReference && atLeast(profile, "BASIC_WL" /* BASIC_WL */))
@@ -609,7 +626,7 @@ var PROFILE_LEVEL2 = {
609
626
  function addError(errors, field, message, profile) {
610
627
  errors.push({ field, message, profile });
611
628
  }
612
- function validateInput(input, profile) {
629
+ function validateInput(input, profile, flavor) {
613
630
  const errors = [];
614
631
  const level = PROFILE_LEVEL2[profile];
615
632
  const req = (minProfile) => level >= PROFILE_LEVEL2[minProfile];
@@ -796,11 +813,193 @@ function validateInput(input, profile) {
796
813
  }
797
814
  });
798
815
  }
816
+ if (req("BASIC_WL" /* BASIC_WL */)) {
817
+ for (const role of ["seller", "buyer"]) {
818
+ const ea = input[role]?.electronicAddress;
819
+ if (ea && !ea.schemeID) {
820
+ addError(
821
+ errors,
822
+ `${role}.electronicAddress.schemeID`,
823
+ `${role.charAt(0).toUpperCase() + role.slice(1)} electronic address must include schemeID (BR-62 / FX-SCH-A-000158).`,
824
+ "BASIC_WL" /* BASIC_WL */
825
+ );
826
+ }
827
+ }
828
+ }
829
+ if (req("BASIC" /* BASIC */) && input.lines) {
830
+ input.lines.forEach((line, i) => {
831
+ if (typeof line.unitPrice === "number" && line.unitPrice < 0) {
832
+ addError(
833
+ errors,
834
+ `lines[${i}].unitPrice`,
835
+ "Line unit price must not be negative (BR-27). Use allowancesCharges for discounts.",
836
+ "BASIC" /* BASIC */
837
+ );
838
+ }
839
+ });
840
+ }
841
+ if (req("BASIC_WL" /* BASIC_WL */) && input.totals) {
842
+ const t = input.totals;
843
+ if (t.taxBasisTotal != null && t.taxTotal != null && t.grandTotal != null) {
844
+ const expected = round2(t.taxBasisTotal + t.taxTotal);
845
+ if (Math.abs(round2(t.grandTotal) - expected) > 0.01) {
846
+ addError(
847
+ errors,
848
+ "totals.grandTotal",
849
+ `Grand total ${t.grandTotal} does not equal taxBasisTotal + taxTotal (${expected}) (BR-CO-15).`,
850
+ "BASIC_WL" /* BASIC_WL */
851
+ );
852
+ }
853
+ }
854
+ }
855
+ if (req("BASIC_WL" /* BASIC_WL */) && input.totals) {
856
+ const t = input.totals;
857
+ if (t.lineTotal != null && t.taxBasisTotal != null) {
858
+ const allowance = t.allowanceTotal ?? 0;
859
+ const charge = t.chargeTotal ?? 0;
860
+ const expected = round2(t.lineTotal - allowance + charge);
861
+ if (Math.abs(round2(t.taxBasisTotal) - expected) > 0.01) {
862
+ addError(
863
+ errors,
864
+ "totals.taxBasisTotal",
865
+ `taxBasisTotal ${t.taxBasisTotal} does not equal lineTotal - allowanceTotal + chargeTotal (${expected}) (BR-CO-13).`,
866
+ "BASIC_WL" /* BASIC_WL */
867
+ );
868
+ }
869
+ }
870
+ }
871
+ if (req("BASIC" /* BASIC */) && input.totals && input.lines) {
872
+ const sumLineNets = input.lines.reduce((sum, l) => {
873
+ const lineNet = l.lineTotal ?? (typeof l.quantity === "number" && typeof l.unitPrice === "number" ? l.quantity * l.unitPrice : 0);
874
+ return sum + lineNet;
875
+ }, 0);
876
+ if (input.totals.lineTotal != null) {
877
+ if (Math.abs(round2(input.totals.lineTotal) - round2(sumLineNets)) > 0.01) {
878
+ addError(
879
+ errors,
880
+ "totals.lineTotal",
881
+ `lineTotal ${input.totals.lineTotal} does not equal \u03A3 line nets (${round2(sumLineNets)}) (BR-CO-10).`,
882
+ "BASIC" /* BASIC */
883
+ );
884
+ }
885
+ }
886
+ }
887
+ if (req("BASIC_WL" /* BASIC_WL */) && input.totals?.taxTotal != null && input.vatBreakdown) {
888
+ const sumVat = input.vatBreakdown.reduce((s, v) => s + (v.taxAmount ?? 0), 0);
889
+ if (Math.abs(round2(input.totals.taxTotal) - round2(sumVat)) > 0.01) {
890
+ addError(
891
+ errors,
892
+ "totals.taxTotal",
893
+ `taxTotal ${input.totals.taxTotal} does not equal \u03A3 vatBreakdown.taxAmount (${round2(sumVat)}) (BR-CO-14).`,
894
+ "BASIC_WL" /* BASIC_WL */
895
+ );
896
+ }
897
+ }
898
+ if (req("BASIC_WL" /* BASIC_WL */) && input.vatBreakdown) {
899
+ input.vatBreakdown.forEach((vb, i) => {
900
+ if (vb.taxableAmount != null && vb.taxAmount != null && vb.ratePercent != null) {
901
+ const expected = round2(vb.taxableAmount * vb.ratePercent / 100);
902
+ if (Math.abs(round2(vb.taxAmount) - expected) > 0.01) {
903
+ addError(
904
+ errors,
905
+ `vatBreakdown[${i}].taxAmount`,
906
+ `taxAmount ${vb.taxAmount} does not equal round(taxableAmount \xD7 ratePercent / 100) = ${expected} (BR-CO-17).`,
907
+ "BASIC_WL" /* BASIC_WL */
908
+ );
909
+ }
910
+ }
911
+ });
912
+ }
913
+ if (req("BASIC" /* BASIC */) && input.document) {
914
+ const isPrepayment = input.document.typeCode === "386";
915
+ if (!isPrepayment) {
916
+ const hasDeliveryDate = !!input.delivery?.date;
917
+ const hasBillingPeriod = !!input.billingPeriod;
918
+ if (!hasDeliveryDate && !hasBillingPeriod) {
919
+ addError(
920
+ errors,
921
+ "delivery.date",
922
+ "Provide delivery.date or billingPeriod (BR-FX-EN-04).",
923
+ "BASIC" /* BASIC */
924
+ );
925
+ }
926
+ }
927
+ }
928
+ if (flavor === "xrechnung" /* XRECHNUNG */) {
929
+ if (!input.document?.buyerReference) {
930
+ addError(
931
+ errors,
932
+ "document.buyerReference",
933
+ "BuyerReference (BT-10) is required for XRechnung (BR-DE-15).",
934
+ "EN16931" /* EN16931 */
935
+ );
936
+ }
937
+ if (!input.document?.businessProcessId) {
938
+ addError(
939
+ errors,
940
+ "document.businessProcessId",
941
+ "Business process (BT-23) is required for XRechnung (PEPPOL-EN16931-R001).",
942
+ "EN16931" /* EN16931 */
943
+ );
944
+ }
945
+ if (!input.seller?.contact || !input.seller.contact.name && !input.seller.contact.email && !input.seller.contact.phone) {
946
+ addError(
947
+ errors,
948
+ "seller.contact",
949
+ "Seller contact (BG-6) with name, email or phone is required for XRechnung (BR-DE-2).",
950
+ "EN16931" /* EN16931 */
951
+ );
952
+ }
953
+ if (!input.seller?.electronicAddress?.value || !input.seller.electronicAddress.schemeID) {
954
+ addError(
955
+ errors,
956
+ "seller.electronicAddress",
957
+ "Seller electronic address (BT-34) with non-empty schemeID is required for XRechnung (PEPPOL-EN16931-R020 / BR-62).",
958
+ "EN16931" /* EN16931 */
959
+ );
960
+ }
961
+ if (!input.buyer?.electronicAddress?.value || !input.buyer.electronicAddress.schemeID) {
962
+ addError(
963
+ errors,
964
+ "buyer.electronicAddress",
965
+ "Buyer electronic address (BT-49) with non-empty schemeID is required for XRechnung (PEPPOL-EN16931-R010 / BR-62).",
966
+ "EN16931" /* EN16931 */
967
+ );
968
+ }
969
+ if (input.payment?.meansCode === "58" && input.payment.iban) {
970
+ if (!isValidIban(input.payment.iban)) {
971
+ addError(
972
+ errors,
973
+ "payment.iban",
974
+ "IBAN is not syntactically valid (BR-DE-19).",
975
+ "EN16931" /* EN16931 */
976
+ );
977
+ }
978
+ }
979
+ }
799
980
  return {
800
981
  valid: errors.length === 0,
801
982
  errors
802
983
  };
803
984
  }
985
+ function round2(n) {
986
+ return Math.round(n * 100) / 100;
987
+ }
988
+ function isValidIban(iban) {
989
+ const normalized = iban.replace(/\s+/g, "").toUpperCase();
990
+ if (!/^[A-Z]{2}\d{2}[A-Z0-9]{1,30}$/.test(normalized)) return false;
991
+ const rearranged = normalized.slice(4) + normalized.slice(0, 4);
992
+ let expanded = "";
993
+ for (const ch of rearranged) {
994
+ expanded += /[A-Z]/.test(ch) ? String(ch.charCodeAt(0) - 55) : ch;
995
+ }
996
+ let remainder = 0;
997
+ for (let i = 0; i < expanded.length; i += 7) {
998
+ const chunk = remainder.toString() + expanded.slice(i, i + 7);
999
+ remainder = parseInt(chunk, 10) % 97;
1000
+ }
1001
+ return remainder === 1;
1002
+ }
804
1003
  function findSchemaDir(startDir) {
805
1004
  let dir = startDir;
806
1005
  for (let i = 0; i < 6; i++) {
@@ -882,6 +1081,71 @@ var AF_RELATIONSHIP_MAP = {
882
1081
  Data: AFRelationship.Data,
883
1082
  Source: AFRelationship.Source
884
1083
  };
1084
+ function ensureOutputIntent(pdfDoc, iccBytes, identifier) {
1085
+ const existing = pdfDoc.catalog.lookup(PDFName.of("OutputIntents"));
1086
+ if (existing instanceof PDFArray && existing.size() > 0) return;
1087
+ const iccStream = pdfDoc.context.flateStream(iccBytes, { N: 3 });
1088
+ const iccRef = pdfDoc.context.register(iccStream);
1089
+ const intent = pdfDoc.context.obj({
1090
+ Type: "OutputIntent",
1091
+ S: "GTS_PDFA1",
1092
+ OutputConditionIdentifier: PDFString.of(identifier),
1093
+ RegistryName: PDFString.of("http://www.color.org"),
1094
+ Info: PDFString.of(identifier),
1095
+ DestOutputProfile: iccRef
1096
+ });
1097
+ const intents = PDFArray.withContext(pdfDoc.context);
1098
+ intents.push(intent);
1099
+ pdfDoc.catalog.set(PDFName.of("OutputIntents"), intents);
1100
+ }
1101
+ function findUnembeddedFonts(pdfDoc) {
1102
+ const seen = /* @__PURE__ */ new Set();
1103
+ const unembedded = /* @__PURE__ */ new Set();
1104
+ const FONT_FILE_KEYS = [
1105
+ PDFName.of("FontFile"),
1106
+ PDFName.of("FontFile2"),
1107
+ PDFName.of("FontFile3")
1108
+ ];
1109
+ const inspectFontDict = (font) => {
1110
+ const subtype = font.lookup(PDFName.of("Subtype"));
1111
+ const baseFont = font.lookup(PDFName.of("BaseFont"));
1112
+ const baseFontName = baseFont instanceof PDFName ? baseFont.asString() : "<unknown>";
1113
+ const subtypeName = subtype instanceof PDFName ? subtype.asString() : "";
1114
+ if (subtypeName === "/Type0") {
1115
+ const desc = font.lookup(PDFName.of("DescendantFonts"));
1116
+ if (desc instanceof PDFArray) {
1117
+ for (let i = 0; i < desc.size(); i += 1) {
1118
+ const child = desc.lookup(i);
1119
+ if (child instanceof PDFDict) inspectFontDict(child);
1120
+ }
1121
+ }
1122
+ return;
1123
+ }
1124
+ const descriptor = font.lookup(PDFName.of("FontDescriptor"));
1125
+ if (!(descriptor instanceof PDFDict)) {
1126
+ unembedded.add(baseFontName);
1127
+ return;
1128
+ }
1129
+ const hasEmbedded = FONT_FILE_KEYS.some((k) => descriptor.has(k));
1130
+ if (!hasEmbedded) unembedded.add(baseFontName);
1131
+ };
1132
+ for (const page of pdfDoc.getPages()) {
1133
+ const resources = page.node.Resources();
1134
+ if (!resources) continue;
1135
+ const fonts = resources.lookup(PDFName.of("Font"));
1136
+ if (!(fonts instanceof PDFDict)) continue;
1137
+ for (const [, value] of fonts.entries()) {
1138
+ const ref = value instanceof PDFRef ? value : void 0;
1139
+ if (ref) {
1140
+ if (seen.has(ref)) continue;
1141
+ seen.add(ref);
1142
+ }
1143
+ const font = ref ? pdfDoc.context.lookup(ref) : value;
1144
+ if (font instanceof PDFDict) inspectFontDict(font);
1145
+ }
1146
+ }
1147
+ return Array.from(unembedded);
1148
+ }
885
1149
  function resolveAfRelationship(profile, override) {
886
1150
  if (override) return AF_RELATIONSHIP_MAP[override];
887
1151
  switch (profile) {
@@ -998,6 +1262,9 @@ async function embedFacturX(options) {
998
1262
  validateBeforeEmbed = true,
999
1263
  validateXsd: runXsdValidation = false,
1000
1264
  addPdfA3Metadata = true,
1265
+ rgbIccProfile,
1266
+ outputIntentIdentifier = "sRGB IEC61966-2.1",
1267
+ unembeddedFonts = "warn",
1001
1268
  meta
1002
1269
  } = options;
1003
1270
  if (!options.xml && !options.input) {
@@ -1011,7 +1278,7 @@ async function embedFacturX(options) {
1011
1278
  if (!xml) {
1012
1279
  const input = options.input;
1013
1280
  if (validateBeforeEmbed) {
1014
- validation = validateInput(input, profile);
1281
+ validation = validateInput(input, profile, flavor);
1015
1282
  if (!validation.valid) {
1016
1283
  throw new Error(
1017
1284
  `Input validation failed for profile "${profile}":
@@ -1052,6 +1319,22 @@ async function embedFacturX(options) {
1052
1319
  pdfDoc.setCreationDate(now);
1053
1320
  pdfDoc.setModificationDate(now);
1054
1321
  if (addPdfA3Metadata) {
1322
+ if (rgbIccProfile) {
1323
+ const iccBytes = rgbIccProfile instanceof Uint8Array ? rgbIccProfile : new Uint8Array(rgbIccProfile);
1324
+ ensureOutputIntent(pdfDoc, iccBytes, outputIntentIdentifier);
1325
+ } else if (!(pdfDoc.catalog.lookup(PDFName.of("OutputIntents")) instanceof PDFArray)) {
1326
+ console.warn(
1327
+ "[factur-x] addPdfA3Metadata is enabled but no rgbIccProfile was supplied and the input PDF has no /OutputIntents. The resulting PDF will not be PDF/A-3 conformant."
1328
+ );
1329
+ }
1330
+ if (unembeddedFonts !== "ignore") {
1331
+ const missing = findUnembeddedFonts(pdfDoc);
1332
+ if (missing.length > 0) {
1333
+ const message = `[factur-x] PDF/A-3 requires every font program to be embedded; the following fonts are not embedded: ${missing.join(", ")}. Configure your PDF generator to embed all fonts.`;
1334
+ if (unembeddedFonts === "throw") throw new Error(message);
1335
+ console.warn(message);
1336
+ }
1337
+ }
1055
1338
  const isoDate = now.toISOString();
1056
1339
  const xmpXml = buildXmpMetadata({
1057
1340
  title,
@@ -1098,18 +1381,22 @@ function resolve(doc, obj) {
1098
1381
  return obj;
1099
1382
  }
1100
1383
  function detectProfile(xml) {
1101
- const profileMap = {
1102
- "urn:factur-x.eu:1p0:minimum": "MINIMUM" /* MINIMUM */,
1103
- "urn:factur-x.eu:1p0:basicwl": "BASIC_WL" /* BASIC_WL */,
1104
- "urn:factur-x.eu:1p0:basic": "BASIC" /* BASIC */,
1105
- "urn:factur-x.eu:1p0:en16931": "EN16931" /* EN16931 */,
1106
- "urn:factur-x.eu:1p0:extended": "EXTENDED" /* EXTENDED */,
1107
- "urn:cen.eu:en16931:2017": "EN16931" /* EN16931 */,
1108
- "urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0": "EN16931" /* EN16931 */,
1109
- "urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_2.3": "EN16931" /* EN16931 */,
1110
- "urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_2.2": "EN16931" /* EN16931 */
1111
- };
1112
- for (const [urn, profile] of Object.entries(profileMap)) {
1384
+ const profileMap = [
1385
+ ["urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended", "EXTENDED" /* EXTENDED */],
1386
+ ["urn:cen.eu:en16931:2017#conformant#urn:zugferd.de:2p0:extended", "EXTENDED" /* EXTENDED */],
1387
+ ["urn:cen.eu:en16931:2017#compliant#urn:factur-x.eu:1p0:basic", "BASIC" /* BASIC */],
1388
+ ["urn:cen.eu:en16931:2017#compliant#urn:zugferd.de:2p0:basic", "BASIC" /* BASIC */],
1389
+ ["urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0", "EN16931" /* EN16931 */],
1390
+ ["urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_2.3", "EN16931" /* EN16931 */],
1391
+ ["urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_2.2", "EN16931" /* EN16931 */],
1392
+ ["urn:cen.eu:en16931:2017", "EN16931" /* EN16931 */],
1393
+ ["urn:factur-x.eu:1p0:minimum", "MINIMUM" /* MINIMUM */],
1394
+ ["urn:factur-x.eu:1p0:basicwl", "BASIC_WL" /* BASIC_WL */],
1395
+ ["urn:factur-x.eu:1p0:basic", "BASIC" /* BASIC */],
1396
+ ["urn:factur-x.eu:1p0:en16931", "EN16931" /* EN16931 */],
1397
+ ["urn:factur-x.eu:1p0:extended", "EXTENDED" /* EXTENDED */]
1398
+ ];
1399
+ for (const [urn, profile] of profileMap) {
1113
1400
  if (xml.includes(urn)) return profile;
1114
1401
  }
1115
1402
  return void 0;
@@ -1177,9 +1464,16 @@ function toXRechnung(input, options) {
1177
1464
  const profile = options?.profile ?? "EN16931" /* EN16931 */;
1178
1465
  const validate = options?.validate ?? true;
1179
1466
  validateFlavorProfile("xrechnung" /* XRECHNUNG */, profile);
1467
+ const enrichedInput = {
1468
+ ...input,
1469
+ document: {
1470
+ ...input.document,
1471
+ businessProcessId: resolveBusinessProcessUrn(input, "xrechnung" /* XRECHNUNG */)
1472
+ }
1473
+ };
1180
1474
  let validation;
1181
1475
  if (validate) {
1182
- validation = validateInput(input, profile);
1476
+ validation = validateInput(enrichedInput, profile, "xrechnung" /* XRECHNUNG */);
1183
1477
  if (!validation.valid) {
1184
1478
  throw new Error(
1185
1479
  `Input validation failed for XRechnung (profile "${profile}"):
@@ -1187,13 +1481,6 @@ function toXRechnung(input, options) {
1187
1481
  );
1188
1482
  }
1189
1483
  }
1190
- const enrichedInput = {
1191
- ...input,
1192
- document: {
1193
- ...input.document,
1194
- businessProcessId: resolveBusinessProcessUrn(input, "xrechnung" /* XRECHNUNG */)
1195
- }
1196
- };
1197
1484
  const xml = buildXml(enrichedInput, profile, "xrechnung" /* XRECHNUNG */);
1198
1485
  return {
1199
1486
  xml,
@@ -1289,4 +1576,4 @@ function toXRechnung(input, options) {
1289
1576
  * ```
1290
1577
  */
1291
1578
 
1292
- export { DocumentTypeCode, FLAVOR_CONFIGS, FLAVOR_DEFAULT_TYPE_CODES, Flavor, PROFILE_MAIN_XSD, PROFILE_SCHEMA_DIRS, PROFILE_URNS, Profile, UnitCode, VatCategoryCode, buildXml, embedFacturX, escapeXml, extractXml, getFlavorConfig, resolveBusinessProcessUrn, resolveTypeCode, toXRechnung, validateFlavorProfile, validateInput, validateXsd };
1579
+ export { DocumentTypeCode, FLAVOR_CONFIGS, FLAVOR_DEFAULT_TYPE_CODES, Flavor, PROFILE_MAIN_XSD, PROFILE_SCHEMA_DIRS, PROFILE_URNS, Profile, UnitCode, VatCategoryCode, XRECHNUNG_PROFILE_URN, buildXml, embedFacturX, escapeXml, extractXml, getFlavorConfig, resolveBusinessProcessUrn, resolveTypeCode, toXRechnung, validateFlavorProfile, validateInput, validateXsd };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackforge-eu/factur-x",
3
- "version": "1.0.1",
3
+ "version": "1.0.4",
4
4
  "type": "module",
5
5
  "description": "Embed Factur-X, ZUGFeRD, and XRechnung structured invoice XML into PDF/A-3 documents",
6
6
  "keywords": [