@stackforge-eu/factur-x 1.3.1 → 1.4.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,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.4.0] — 2026-09-03
11
+
12
+ - **Added the item price discount (BT-147).** A new optional `priceDiscount`
13
+ field on `InvoiceLineInput` emits `ram:AppliedTradeAllowanceCharge` inside
14
+ the gross price element, so a line can state the discount that connects the
15
+ gross unit price (BT-148) to the net unit price (BT-146) instead of leaving
16
+ it implied by their difference. Requires `grossUnitPrice` to be set; the
17
+ amount keeps up to four decimals for sub-cent discounts. Omitting the field
18
+ keeps the output byte-for-byte identical (#15).
19
+
10
20
  ## [1.3.1] — 2026-09-01
11
21
 
12
22
  - **Dropped the unused `schema/cii-d22b/` code-list XSDs from the npm and JSR
@@ -235,6 +245,7 @@ This element is not expected`. Reordered the three blocks in
235
245
  - Deno compatibility test suite
236
246
 
237
247
  [Unreleased]: https://github.com/StackForge-EU/factur-x/commits/main
248
+ [1.4.0]: https://github.com/StackForge-EU/factur-x/compare/v1.3.1...v1.4.0
238
249
  [1.3.1]: https://github.com/StackForge-EU/factur-x/compare/v1.3.0...v1.3.1
239
250
  [1.3.0]: https://github.com/StackForge-EU/factur-x/compare/v1.2.1...v1.3.0
240
251
  [1.2.1]: https://github.com/StackForge-EU/factur-x/compare/v1.2.0...v1.2.1
package/dist/index.cjs CHANGED
@@ -307,6 +307,12 @@ function fmtAmt(n) {
307
307
  }
308
308
  return n.toFixed(2);
309
309
  }
310
+ function fmtPrice(n) {
311
+ if (!Number.isFinite(n)) {
312
+ throw new Error(`Invalid amount value: ${n}. Expected a finite number.`);
313
+ }
314
+ return n.toFixed(4).replace(/(\.\d{2}\d*?)0+$/, "$1");
315
+ }
310
316
  function tag(name, content, attrs) {
311
317
  const a = attrs ? Object.entries(attrs).map(([k, v]) => ` ${k}="${escapeXml(v)}"`).join("") : "";
312
318
  return `<${name}${a}>${content}</${name}>`;
@@ -406,11 +412,21 @@ function buildLineItem(line, profile) {
406
412
  const basisQuantity = line.basisQuantity !== void 0 ? tag("ram:BasisQuantity", line.basisQuantity.toString(), {
407
413
  unitCode: line.basisQuantityUnitCode ?? unitCode
408
414
  }) : "";
409
- if (line.grossUnitPrice !== void 0)
415
+ if (line.priceDiscount !== void 0 && line.grossUnitPrice === void 0) {
416
+ throw new Error(
417
+ `buildXml: line "${line.id}" sets "priceDiscount" (BT-147) without "grossUnitPrice" (BT-148).`
418
+ );
419
+ }
420
+ if (line.grossUnitPrice !== void 0) {
421
+ const discount = line.priceDiscount !== void 0 ? tag(
422
+ "ram:AppliedTradeAllowanceCharge",
423
+ tag("ram:ChargeIndicator", tag("udt:Indicator", "false")) + tag("ram:ActualAmount", fmtPrice(line.priceDiscount))
424
+ ) : "";
410
425
  agree += tag(
411
426
  "ram:GrossPriceProductTradePrice",
412
- tag("ram:ChargeAmount", fmtAmt(line.grossUnitPrice)) + basisQuantity
427
+ tag("ram:ChargeAmount", fmtAmt(line.grossUnitPrice)) + basisQuantity + discount
413
428
  );
429
+ }
414
430
  agree += tag(
415
431
  "ram:NetPriceProductTradePrice",
416
432
  tag("ram:ChargeAmount", fmtAmt(line.unitPrice)) + basisQuantity
package/dist/index.d.cts CHANGED
@@ -573,6 +573,14 @@ interface InvoiceLineInput {
573
573
  * @see EN 16931 BT-148
574
574
  */
575
575
  grossUnitPrice?: number;
576
+ /**
577
+ * Item price discount — the amount subtracted from `grossUnitPrice`
578
+ * (BT-148) to arrive at `unitPrice` (BT-146). Emitted as
579
+ * `ram:AppliedTradeAllowanceCharge` inside the gross price element,
580
+ * so `grossUnitPrice` must be set alongside it.
581
+ * @see EN 16931 BT-147
582
+ */
583
+ priceDiscount?: number;
576
584
  /**
577
585
  * Item price base quantity — the quantity the unit price (BT-146)
578
586
  * applies to, e.g. a price per 10 square metres or per 1000 prints.
package/dist/index.d.ts CHANGED
@@ -573,6 +573,14 @@ interface InvoiceLineInput {
573
573
  * @see EN 16931 BT-148
574
574
  */
575
575
  grossUnitPrice?: number;
576
+ /**
577
+ * Item price discount — the amount subtracted from `grossUnitPrice`
578
+ * (BT-148) to arrive at `unitPrice` (BT-146). Emitted as
579
+ * `ram:AppliedTradeAllowanceCharge` inside the gross price element,
580
+ * so `grossUnitPrice` must be set alongside it.
581
+ * @see EN 16931 BT-147
582
+ */
583
+ priceDiscount?: number;
576
584
  /**
577
585
  * Item price base quantity — the quantity the unit price (BT-146)
578
586
  * applies to, e.g. a price per 10 square metres or per 1000 prints.
package/dist/index.js CHANGED
@@ -277,6 +277,12 @@ function fmtAmt(n) {
277
277
  }
278
278
  return n.toFixed(2);
279
279
  }
280
+ function fmtPrice(n) {
281
+ if (!Number.isFinite(n)) {
282
+ throw new Error(`Invalid amount value: ${n}. Expected a finite number.`);
283
+ }
284
+ return n.toFixed(4).replace(/(\.\d{2}\d*?)0+$/, "$1");
285
+ }
280
286
  function tag(name, content, attrs) {
281
287
  const a = attrs ? Object.entries(attrs).map(([k, v]) => ` ${k}="${escapeXml(v)}"`).join("") : "";
282
288
  return `<${name}${a}>${content}</${name}>`;
@@ -376,11 +382,21 @@ function buildLineItem(line, profile) {
376
382
  const basisQuantity = line.basisQuantity !== void 0 ? tag("ram:BasisQuantity", line.basisQuantity.toString(), {
377
383
  unitCode: line.basisQuantityUnitCode ?? unitCode
378
384
  }) : "";
379
- if (line.grossUnitPrice !== void 0)
385
+ if (line.priceDiscount !== void 0 && line.grossUnitPrice === void 0) {
386
+ throw new Error(
387
+ `buildXml: line "${line.id}" sets "priceDiscount" (BT-147) without "grossUnitPrice" (BT-148).`
388
+ );
389
+ }
390
+ if (line.grossUnitPrice !== void 0) {
391
+ const discount = line.priceDiscount !== void 0 ? tag(
392
+ "ram:AppliedTradeAllowanceCharge",
393
+ tag("ram:ChargeIndicator", tag("udt:Indicator", "false")) + tag("ram:ActualAmount", fmtPrice(line.priceDiscount))
394
+ ) : "";
380
395
  agree += tag(
381
396
  "ram:GrossPriceProductTradePrice",
382
- tag("ram:ChargeAmount", fmtAmt(line.grossUnitPrice)) + basisQuantity
397
+ tag("ram:ChargeAmount", fmtAmt(line.grossUnitPrice)) + basisQuantity + discount
383
398
  );
399
+ }
384
400
  agree += tag(
385
401
  "ram:NetPriceProductTradePrice",
386
402
  tag("ram:ChargeAmount", fmtAmt(line.unitPrice)) + basisQuantity
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackforge-eu/factur-x",
3
- "version": "1.3.1",
3
+ "version": "1.4.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": [