@stackforge-eu/factur-x 1.2.1 → 1.3.0

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,10 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.3.0] — 2026-09-01
11
+
12
+ - **Added item price base quantity (BT-149/BT-150).** New optional
13
+ `basisQuantity` and `basisQuantityUnitCode` fields on `InvoiceLineInput`
14
+ emit `ram:BasisQuantity` inside the net (and, when present, gross) price
15
+ element, so a unit price can state the quantity it refers to — e.g.
16
+ `12.50` per 10 m². The unit code defaults to the line's `unitCode`, as
17
+ EN 16931 requires BT-150 to match BT-130. Omitting the field keeps the
18
+ output byte-for-byte identical (#12).
19
+ - CI now requires every PR to add a `CHANGELOG.md` entry (or carry the
20
+ `skip-changelog` label), so contributor fixes always reach the release notes.
21
+
10
22
  ## [1.2.1] — 2026-08-27
11
23
 
24
+ - **Fixed XSD validation on Windows.** Schema `<xsd:import>` resolution built a
25
+ malformed `file://` URL from backslash paths and keyed the schema lookup by
26
+ a percent-encoded href that libxml2 normalises differently. Now uses
27
+ `pathToFileURL()` and matches imported schemas by filename. Windows added
28
+ to the CI matrix (thanks @muratkorkmaztr-cmyk, #6).
29
+ - **Embedded `factur-x.xml` now uses the `application/xml` MIME type** instead
30
+ of the deprecated `text/xml` (RFC 7303), which current Factur-X conformance
31
+ checkers flag (thanks @muratkorkmaztr-cmyk, #7).
12
32
  - Expanded npm `keywords` (e.g. `e-rechnung`, `pdfa`, `chorus-pro`, `un-cefact`,
13
- spelling variants) so the package is easier to find on npm. No code changes.
33
+ spelling variants) so the package is easier to find on npm (#8).
14
34
 
15
35
  ## [1.2.0] — 2026-07-11
16
36
 
@@ -204,6 +224,7 @@ This element is not expected`. Reordered the three blocks in
204
224
  - Deno compatibility test suite
205
225
 
206
226
  [Unreleased]: https://github.com/StackForge-EU/factur-x/commits/main
227
+ [1.3.0]: https://github.com/StackForge-EU/factur-x/compare/v1.2.1...v1.3.0
207
228
  [1.2.1]: https://github.com/StackForge-EU/factur-x/compare/v1.2.0...v1.2.1
208
229
  [1.2.0]: https://github.com/StackForge-EU/factur-x/compare/v1.1.0...v1.2.0
209
230
  [1.1.0]: https://github.com/StackForge-EU/factur-x/compare/v1.0.8...v1.1.0
package/dist/index.cjs CHANGED
@@ -402,13 +402,19 @@ function buildLineItem(line, profile) {
402
402
  "ram:BuyerOrderReferencedDocument",
403
403
  tag("ram:LineID", escapeXml(line.buyerOrderLineId))
404
404
  );
405
+ const unitCode = line.unitCode ?? "C62" /* UNIT */;
406
+ const basisQuantity = line.basisQuantity !== void 0 ? tag("ram:BasisQuantity", line.basisQuantity.toString(), {
407
+ unitCode: line.basisQuantityUnitCode ?? unitCode
408
+ }) : "";
405
409
  if (line.grossUnitPrice !== void 0)
406
410
  agree += tag(
407
411
  "ram:GrossPriceProductTradePrice",
408
- tag("ram:ChargeAmount", fmtAmt(line.grossUnitPrice))
412
+ tag("ram:ChargeAmount", fmtAmt(line.grossUnitPrice)) + basisQuantity
409
413
  );
410
- agree += tag("ram:NetPriceProductTradePrice", tag("ram:ChargeAmount", fmtAmt(line.unitPrice)));
411
- const unitCode = line.unitCode ?? "C62" /* UNIT */;
414
+ agree += tag(
415
+ "ram:NetPriceProductTradePrice",
416
+ tag("ram:ChargeAmount", fmtAmt(line.unitPrice)) + basisQuantity
417
+ );
412
418
  const delivery = tag("ram:BilledQuantity", line.quantity.toString(), {
413
419
  unitCode
414
420
  });
@@ -509,10 +515,7 @@ function buildXml(input, profile, flavor) {
509
515
  tag("ram:ID", escapeXml(doc.businessProcessId))
510
516
  );
511
517
  const profileUrn = fl === "xrechnung" /* XRECHNUNG */ ? XRECHNUNG_PROFILE_URN : PROFILE_URNS[profile];
512
- ctx += tag(
513
- "ram:GuidelineSpecifiedDocumentContextParameter",
514
- tag("ram:ID", profileUrn)
515
- );
518
+ ctx += tag("ram:GuidelineSpecifiedDocumentContextParameter", tag("ram:ID", profileUrn));
516
519
  let edoc = tag("ram:ID", escapeXml(doc.id));
517
520
  if (doc.name && atLeast(profile, "EXTENDED" /* EXTENDED */)) edoc += tag("ram:Name", escapeXml(doc.name));
518
521
  edoc += tag("ram:TypeCode", typeCode);
@@ -1178,7 +1181,9 @@ function resolveXmpConformanceLevel(profile, flavor) {
1178
1181
  }
1179
1182
  function ensureIdTrailer(pdfDoc, pdfContentBytes) {
1180
1183
  const existingIdTrailer = pdfDoc.context.trailerInfo.ID;
1181
- const currentContentHash = pdfLib.PDFHexString.of(crypto.createHash("md5").update(pdfContentBytes).digest("hex"));
1184
+ const currentContentHash = pdfLib.PDFHexString.of(
1185
+ crypto.createHash("md5").update(pdfContentBytes).digest("hex")
1186
+ );
1182
1187
  const hasValidIdTrailer = existingIdTrailer instanceof pdfLib.PDFArray && existingIdTrailer.size() === 2;
1183
1188
  if (hasValidIdTrailer) {
1184
1189
  const existingFirstId = existingIdTrailer.lookup(0);
@@ -1207,11 +1212,7 @@ function ensureOutputIntent(pdfDoc, iccBytes, identifier) {
1207
1212
  function findUnembeddedFonts(pdfDoc) {
1208
1213
  const seen = /* @__PURE__ */ new Set();
1209
1214
  const unembedded = /* @__PURE__ */ new Set();
1210
- const FONT_FILE_KEYS = [
1211
- pdfLib.PDFName.of("FontFile"),
1212
- pdfLib.PDFName.of("FontFile2"),
1213
- pdfLib.PDFName.of("FontFile3")
1214
- ];
1215
+ const FONT_FILE_KEYS = [pdfLib.PDFName.of("FontFile"), pdfLib.PDFName.of("FontFile2"), pdfLib.PDFName.of("FontFile3")];
1215
1216
  const inspectFontDict = (font) => {
1216
1217
  const subtype = font.lookup(pdfLib.PDFName.of("Subtype"));
1217
1218
  const baseFont = font.lookup(pdfLib.PDFName.of("BaseFont"));
package/dist/index.d.cts CHANGED
@@ -573,6 +573,20 @@ interface InvoiceLineInput {
573
573
  * @see EN 16931 BT-148
574
574
  */
575
575
  grossUnitPrice?: number;
576
+ /**
577
+ * Item price base quantity — the quantity the unit price (BT-146)
578
+ * applies to, e.g. a price per 10 square metres or per 1000 prints.
579
+ * @see EN 16931 BT-149
580
+ */
581
+ basisQuantity?: number;
582
+ /**
583
+ * Unit of measure for `basisQuantity` (UN/CEFACT Rec 20).
584
+ *
585
+ * @default the line's `unitCode` when omitted, as EN 16931 requires
586
+ * BT-150 to match the invoiced quantity's unit (BT-130)
587
+ * @see EN 16931 BT-150
588
+ */
589
+ basisQuantityUnitCode?: UnitCode | (string & Record<never, never>);
576
590
  /**
577
591
  * Line total (net) — can be derived from `quantity * unitPrice`.
578
592
  * @see EN 16931 BT-131
package/dist/index.d.ts CHANGED
@@ -573,6 +573,20 @@ interface InvoiceLineInput {
573
573
  * @see EN 16931 BT-148
574
574
  */
575
575
  grossUnitPrice?: number;
576
+ /**
577
+ * Item price base quantity — the quantity the unit price (BT-146)
578
+ * applies to, e.g. a price per 10 square metres or per 1000 prints.
579
+ * @see EN 16931 BT-149
580
+ */
581
+ basisQuantity?: number;
582
+ /**
583
+ * Unit of measure for `basisQuantity` (UN/CEFACT Rec 20).
584
+ *
585
+ * @default the line's `unitCode` when omitted, as EN 16931 requires
586
+ * BT-150 to match the invoiced quantity's unit (BT-130)
587
+ * @see EN 16931 BT-150
588
+ */
589
+ basisQuantityUnitCode?: UnitCode | (string & Record<never, never>);
576
590
  /**
577
591
  * Line total (net) — can be derived from `quantity * unitPrice`.
578
592
  * @see EN 16931 BT-131
package/dist/index.js CHANGED
@@ -372,13 +372,19 @@ function buildLineItem(line, profile) {
372
372
  "ram:BuyerOrderReferencedDocument",
373
373
  tag("ram:LineID", escapeXml(line.buyerOrderLineId))
374
374
  );
375
+ const unitCode = line.unitCode ?? "C62" /* UNIT */;
376
+ const basisQuantity = line.basisQuantity !== void 0 ? tag("ram:BasisQuantity", line.basisQuantity.toString(), {
377
+ unitCode: line.basisQuantityUnitCode ?? unitCode
378
+ }) : "";
375
379
  if (line.grossUnitPrice !== void 0)
376
380
  agree += tag(
377
381
  "ram:GrossPriceProductTradePrice",
378
- tag("ram:ChargeAmount", fmtAmt(line.grossUnitPrice))
382
+ tag("ram:ChargeAmount", fmtAmt(line.grossUnitPrice)) + basisQuantity
379
383
  );
380
- agree += tag("ram:NetPriceProductTradePrice", tag("ram:ChargeAmount", fmtAmt(line.unitPrice)));
381
- const unitCode = line.unitCode ?? "C62" /* UNIT */;
384
+ agree += tag(
385
+ "ram:NetPriceProductTradePrice",
386
+ tag("ram:ChargeAmount", fmtAmt(line.unitPrice)) + basisQuantity
387
+ );
382
388
  const delivery = tag("ram:BilledQuantity", line.quantity.toString(), {
383
389
  unitCode
384
390
  });
@@ -479,10 +485,7 @@ function buildXml(input, profile, flavor) {
479
485
  tag("ram:ID", escapeXml(doc.businessProcessId))
480
486
  );
481
487
  const profileUrn = fl === "xrechnung" /* XRECHNUNG */ ? XRECHNUNG_PROFILE_URN : PROFILE_URNS[profile];
482
- ctx += tag(
483
- "ram:GuidelineSpecifiedDocumentContextParameter",
484
- tag("ram:ID", profileUrn)
485
- );
488
+ ctx += tag("ram:GuidelineSpecifiedDocumentContextParameter", tag("ram:ID", profileUrn));
486
489
  let edoc = tag("ram:ID", escapeXml(doc.id));
487
490
  if (doc.name && atLeast(profile, "EXTENDED" /* EXTENDED */)) edoc += tag("ram:Name", escapeXml(doc.name));
488
491
  edoc += tag("ram:TypeCode", typeCode);
@@ -1148,7 +1151,9 @@ function resolveXmpConformanceLevel(profile, flavor) {
1148
1151
  }
1149
1152
  function ensureIdTrailer(pdfDoc, pdfContentBytes) {
1150
1153
  const existingIdTrailer = pdfDoc.context.trailerInfo.ID;
1151
- const currentContentHash = PDFHexString.of(createHash("md5").update(pdfContentBytes).digest("hex"));
1154
+ const currentContentHash = PDFHexString.of(
1155
+ createHash("md5").update(pdfContentBytes).digest("hex")
1156
+ );
1152
1157
  const hasValidIdTrailer = existingIdTrailer instanceof PDFArray && existingIdTrailer.size() === 2;
1153
1158
  if (hasValidIdTrailer) {
1154
1159
  const existingFirstId = existingIdTrailer.lookup(0);
@@ -1177,11 +1182,7 @@ function ensureOutputIntent(pdfDoc, iccBytes, identifier) {
1177
1182
  function findUnembeddedFonts(pdfDoc) {
1178
1183
  const seen = /* @__PURE__ */ new Set();
1179
1184
  const unembedded = /* @__PURE__ */ new Set();
1180
- const FONT_FILE_KEYS = [
1181
- PDFName.of("FontFile"),
1182
- PDFName.of("FontFile2"),
1183
- PDFName.of("FontFile3")
1184
- ];
1185
+ const FONT_FILE_KEYS = [PDFName.of("FontFile"), PDFName.of("FontFile2"), PDFName.of("FontFile3")];
1185
1186
  const inspectFontDict = (font) => {
1186
1187
  const subtype = font.lookup(PDFName.of("Subtype"));
1187
1188
  const baseFont = font.lookup(PDFName.of("BaseFont"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackforge-eu/factur-x",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
4
4
  "type": "module",
5
5
  "description": "Embed Factur-X, ZUGFeRD, and XRechnung structured invoice XML into PDF/A-3 documents",
6
6
  "keywords": [