@stackforge-eu/factur-x 1.4.3 → 1.4.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,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.4.4] — 2026-11-09
11
+
12
+ - **Fixed header reference element order.** `buildXml` emitted
13
+ `input.references` in input order, so an `order` reference listed before a
14
+ `seller-order` (or `contract`) reference produced XML that failed XSD
15
+ validation. References are now emitted in the CII sequence (SellerOrder →
16
+ BuyerOrder → Contract → SpecifiedProcuringProject); input order is only
17
+ kept among references of the same type. Reported by @SCPAU100 (#23).
18
+ - **Fixed `project` references failing XSD validation.** `SpecifiedProcuringProject`
19
+ requires a `ram:Name` child that was never emitted. A new optional `name`
20
+ field on `DocumentReferenceInput` fills it, defaulting to `"Project"`.
21
+ - **`validateInput` rejects duplicate single-occurrence references.** The
22
+ schema allows `order`, `contract`, `seller-order`, `project` and `despatch`
23
+ references at most once each, and `delivery.despatchAdviceReference` and a
24
+ `despatch` reference target the same element. Both cases used to surface
25
+ only as an XSD error after building.
26
+
10
27
  ## [1.4.3] — 2026-09-28
11
28
 
12
29
  - **Fixed half-cent rounding.** `Math.round(n * 100) / 100` turns
@@ -274,6 +291,7 @@ This element is not expected`. Reordered the three blocks in
274
291
  - Deno compatibility test suite
275
292
 
276
293
  [Unreleased]: https://github.com/StackForge-EU/factur-x/commits/main
294
+ [1.4.4]: https://github.com/StackForge-EU/factur-x/compare/v1.4.3...v1.4.4
277
295
  [1.4.3]: https://github.com/StackForge-EU/factur-x/compare/v1.4.2...v1.4.3
278
296
  [1.4.2]: https://github.com/StackForge-EU/factur-x/compare/v1.4.1...v1.4.2
279
297
  [1.4.1]: https://github.com/StackForge-EU/factur-x/compare/v1.4.0...v1.4.1
package/dist/index.cjs CHANGED
@@ -570,27 +570,21 @@ function buildXml(input, profile, flavor) {
570
570
  buildParty(input.sellerTaxRepresentative, profile)
571
571
  );
572
572
  if (input.references && atLeast(profile, "BASIC_WL" /* BASIC_WL */)) {
573
- for (const ref of input.references) {
574
- const refType = ref.type ?? "order";
575
- if (refType === "preceding" || refType === "despatch") continue;
576
- const inner = tag("ram:IssuerAssignedID", escapeXml(ref.id)) + (ref.issueDate ? dateEl("ram:FormattedIssueDateTime", ref.issueDate) : "");
577
- switch (refType) {
578
- case "order":
579
- agree += tag("ram:BuyerOrderReferencedDocument", inner);
580
- break;
581
- case "contract":
582
- agree += tag("ram:ContractReferencedDocument", inner);
583
- break;
584
- case "seller-order":
585
- if (atLeast(profile, "EN16931" /* EN16931 */))
586
- agree += tag("ram:SellerOrderReferencedDocument", inner);
587
- break;
588
- case "project":
589
- if (atLeast(profile, "EN16931" /* EN16931 */))
590
- agree += tag("ram:SpecifiedProcuringProject", tag("ram:ID", escapeXml(ref.id)));
591
- break;
592
- }
593
- }
573
+ const refsOfType = (type) => input.references.filter((ref) => (ref.type ?? "order") === type);
574
+ const refDoc = (ref) => tag("ram:IssuerAssignedID", escapeXml(ref.id)) + (ref.issueDate ? dateEl("ram:FormattedIssueDateTime", ref.issueDate) : "");
575
+ if (atLeast(profile, "EN16931" /* EN16931 */))
576
+ for (const ref of refsOfType("seller-order"))
577
+ agree += tag("ram:SellerOrderReferencedDocument", refDoc(ref));
578
+ for (const ref of refsOfType("order"))
579
+ agree += tag("ram:BuyerOrderReferencedDocument", refDoc(ref));
580
+ for (const ref of refsOfType("contract"))
581
+ agree += tag("ram:ContractReferencedDocument", refDoc(ref));
582
+ if (atLeast(profile, "EN16931" /* EN16931 */))
583
+ for (const ref of refsOfType("project"))
584
+ agree += tag(
585
+ "ram:SpecifiedProcuringProject",
586
+ tag("ram:ID", escapeXml(ref.id)) + tag("ram:Name", escapeXml(ref.name ?? "Project"))
587
+ );
594
588
  }
595
589
  tx += tag("ram:ApplicableHeaderTradeAgreement", agree);
596
590
  let del = "";
@@ -1036,6 +1030,28 @@ function validateInput(input, profile, flavor) {
1036
1030
  }
1037
1031
  }
1038
1032
  }
1033
+ if (req("BASIC_WL" /* BASIC_WL */) && input.references) {
1034
+ const singleOccurrence = ["order", "contract", "seller-order", "project", "despatch"];
1035
+ for (const type of singleOccurrence) {
1036
+ const count = input.references.filter((ref) => (ref.type ?? "order") === type).length;
1037
+ if (count > 1) {
1038
+ addError(
1039
+ errors,
1040
+ "references",
1041
+ `Only one "${type}" reference is allowed, got ${count}.`,
1042
+ "BASIC_WL" /* BASIC_WL */
1043
+ );
1044
+ }
1045
+ }
1046
+ if (input.delivery?.despatchAdviceReference && input.references.some((ref) => ref.type === "despatch")) {
1047
+ addError(
1048
+ errors,
1049
+ "references",
1050
+ 'Use either delivery.despatchAdviceReference or a "despatch" reference, not both.',
1051
+ "BASIC_WL" /* BASIC_WL */
1052
+ );
1053
+ }
1054
+ }
1039
1055
  if (flavor === "xrechnung" /* XRECHNUNG */) {
1040
1056
  if (!input.document?.buyerReference) {
1041
1057
  addError(
package/dist/index.d.cts CHANGED
@@ -947,6 +947,12 @@ interface DocumentReferenceInput {
947
947
  type?: "order" | "contract" | "despatch" | "preceding" | "seller-order" | "project";
948
948
  /** Issue date of the referenced document */
949
949
  issueDate?: string;
950
+ /**
951
+ * Project name, only used for `"project"` references. The CII schema
952
+ * requires a `ram:Name` next to the project ID (BT-11); falls back to
953
+ * `"Project"` when omitted.
954
+ */
955
+ name?: string;
950
956
  }
951
957
  /**
952
958
  * Billing/invoicing period.
package/dist/index.d.ts CHANGED
@@ -947,6 +947,12 @@ interface DocumentReferenceInput {
947
947
  type?: "order" | "contract" | "despatch" | "preceding" | "seller-order" | "project";
948
948
  /** Issue date of the referenced document */
949
949
  issueDate?: string;
950
+ /**
951
+ * Project name, only used for `"project"` references. The CII schema
952
+ * requires a `ram:Name` next to the project ID (BT-11); falls back to
953
+ * `"Project"` when omitted.
954
+ */
955
+ name?: string;
950
956
  }
951
957
  /**
952
958
  * Billing/invoicing period.
package/dist/index.js CHANGED
@@ -540,27 +540,21 @@ function buildXml(input, profile, flavor) {
540
540
  buildParty(input.sellerTaxRepresentative, profile)
541
541
  );
542
542
  if (input.references && atLeast(profile, "BASIC_WL" /* BASIC_WL */)) {
543
- for (const ref of input.references) {
544
- const refType = ref.type ?? "order";
545
- if (refType === "preceding" || refType === "despatch") continue;
546
- const inner = tag("ram:IssuerAssignedID", escapeXml(ref.id)) + (ref.issueDate ? dateEl("ram:FormattedIssueDateTime", ref.issueDate) : "");
547
- switch (refType) {
548
- case "order":
549
- agree += tag("ram:BuyerOrderReferencedDocument", inner);
550
- break;
551
- case "contract":
552
- agree += tag("ram:ContractReferencedDocument", inner);
553
- break;
554
- case "seller-order":
555
- if (atLeast(profile, "EN16931" /* EN16931 */))
556
- agree += tag("ram:SellerOrderReferencedDocument", inner);
557
- break;
558
- case "project":
559
- if (atLeast(profile, "EN16931" /* EN16931 */))
560
- agree += tag("ram:SpecifiedProcuringProject", tag("ram:ID", escapeXml(ref.id)));
561
- break;
562
- }
563
- }
543
+ const refsOfType = (type) => input.references.filter((ref) => (ref.type ?? "order") === type);
544
+ const refDoc = (ref) => tag("ram:IssuerAssignedID", escapeXml(ref.id)) + (ref.issueDate ? dateEl("ram:FormattedIssueDateTime", ref.issueDate) : "");
545
+ if (atLeast(profile, "EN16931" /* EN16931 */))
546
+ for (const ref of refsOfType("seller-order"))
547
+ agree += tag("ram:SellerOrderReferencedDocument", refDoc(ref));
548
+ for (const ref of refsOfType("order"))
549
+ agree += tag("ram:BuyerOrderReferencedDocument", refDoc(ref));
550
+ for (const ref of refsOfType("contract"))
551
+ agree += tag("ram:ContractReferencedDocument", refDoc(ref));
552
+ if (atLeast(profile, "EN16931" /* EN16931 */))
553
+ for (const ref of refsOfType("project"))
554
+ agree += tag(
555
+ "ram:SpecifiedProcuringProject",
556
+ tag("ram:ID", escapeXml(ref.id)) + tag("ram:Name", escapeXml(ref.name ?? "Project"))
557
+ );
564
558
  }
565
559
  tx += tag("ram:ApplicableHeaderTradeAgreement", agree);
566
560
  let del = "";
@@ -1006,6 +1000,28 @@ function validateInput(input, profile, flavor) {
1006
1000
  }
1007
1001
  }
1008
1002
  }
1003
+ if (req("BASIC_WL" /* BASIC_WL */) && input.references) {
1004
+ const singleOccurrence = ["order", "contract", "seller-order", "project", "despatch"];
1005
+ for (const type of singleOccurrence) {
1006
+ const count = input.references.filter((ref) => (ref.type ?? "order") === type).length;
1007
+ if (count > 1) {
1008
+ addError(
1009
+ errors,
1010
+ "references",
1011
+ `Only one "${type}" reference is allowed, got ${count}.`,
1012
+ "BASIC_WL" /* BASIC_WL */
1013
+ );
1014
+ }
1015
+ }
1016
+ if (input.delivery?.despatchAdviceReference && input.references.some((ref) => ref.type === "despatch")) {
1017
+ addError(
1018
+ errors,
1019
+ "references",
1020
+ 'Use either delivery.despatchAdviceReference or a "despatch" reference, not both.',
1021
+ "BASIC_WL" /* BASIC_WL */
1022
+ );
1023
+ }
1024
+ }
1009
1025
  if (flavor === "xrechnung" /* XRECHNUNG */) {
1010
1026
  if (!input.document?.buyerReference) {
1011
1027
  addError(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackforge-eu/factur-x",
3
- "version": "1.4.3",
3
+ "version": "1.4.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": [