@stackforge-eu/factur-x 1.3.1 → 1.4.1

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,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.4.1] — 2026-09-14
11
+
12
+ - **Added the rounding amount (BT-114).** A new optional `roundingAmount`
13
+ field on `InvoiceTotalsInput` emits `ram:RoundingAmount` between the tax
14
+ total and the grand total, so an invoice can state the amount that takes
15
+ the grand total (BT-112) to the payable figure (BT-115). Emitted for
16
+ EN 16931 and EXTENDED only, as the lower-profile XSDs do not define the
17
+ element. `validateInput` now also checks BR-CO-16
18
+ (`duePayableAmount = grandTotal - prepaidAmount + roundingAmount`), which
19
+ the schematron already enforced. Omitting the field keeps the output
20
+ byte-for-byte identical (#17).
21
+
22
+ ## [1.4.0] — 2026-09-03
23
+
24
+ - **Added the item price discount (BT-147).** A new optional `priceDiscount`
25
+ field on `InvoiceLineInput` emits `ram:AppliedTradeAllowanceCharge` inside
26
+ the gross price element, so a line can state the discount that connects the
27
+ gross unit price (BT-148) to the net unit price (BT-146) instead of leaving
28
+ it implied by their difference. Requires `grossUnitPrice` to be set; the
29
+ amount keeps up to four decimals for sub-cent discounts. Omitting the field
30
+ keeps the output byte-for-byte identical (#15).
31
+
10
32
  ## [1.3.1] — 2026-09-01
11
33
 
12
34
  - **Dropped the unused `schema/cii-d22b/` code-list XSDs from the npm and JSR
@@ -235,6 +257,8 @@ This element is not expected`. Reordered the three blocks in
235
257
  - Deno compatibility test suite
236
258
 
237
259
  [Unreleased]: https://github.com/StackForge-EU/factur-x/commits/main
260
+ [1.4.1]: https://github.com/StackForge-EU/factur-x/compare/v1.4.0...v1.4.1
261
+ [1.4.0]: https://github.com/StackForge-EU/factur-x/compare/v1.3.1...v1.4.0
238
262
  [1.3.1]: https://github.com/StackForge-EU/factur-x/compare/v1.3.0...v1.3.1
239
263
  [1.3.0]: https://github.com/StackForge-EU/factur-x/compare/v1.2.1...v1.3.0
240
264
  [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
@@ -480,6 +496,8 @@ function buildMonetarySummation(t, profile) {
480
496
  x += tag("ram:TaxTotalAmount", fmtAmt(t.taxTotal), {
481
497
  currencyID: t.currency
482
498
  });
499
+ if (atLeast(profile, "EN16931" /* EN16931 */) && t.roundingAmount !== void 0)
500
+ x += tag("ram:RoundingAmount", fmtAmt(t.roundingAmount));
483
501
  x += tag("ram:GrandTotalAmount", fmtAmt(t.grandTotal));
484
502
  if (atLeast(profile, "BASIC_WL" /* BASIC_WL */) && t.prepaidAmount !== void 0)
485
503
  x += tag("ram:TotalPrepaidAmount", fmtAmt(t.prepaidAmount));
@@ -920,6 +938,22 @@ function validateInput(input, profile, flavor) {
920
938
  }
921
939
  }
922
940
  }
941
+ if (req("BASIC_WL" /* BASIC_WL */) && input.totals) {
942
+ const t = input.totals;
943
+ if (t.grandTotal != null && t.duePayableAmount != null) {
944
+ const prepaid = t.prepaidAmount ?? 0;
945
+ const rounding = t.roundingAmount ?? 0;
946
+ const expected = round2(t.grandTotal - prepaid + rounding);
947
+ if (Math.abs(round2(t.duePayableAmount) - expected) > 0.01) {
948
+ addError(
949
+ errors,
950
+ "totals.duePayableAmount",
951
+ `duePayableAmount ${t.duePayableAmount} does not equal grandTotal - prepaidAmount + roundingAmount (${expected}) (BR-CO-16).`,
952
+ "BASIC_WL" /* BASIC_WL */
953
+ );
954
+ }
955
+ }
956
+ }
923
957
  if (req("BASIC_WL" /* BASIC_WL */) && input.totals) {
924
958
  const t = input.totals;
925
959
  if (t.lineTotal != null && t.taxBasisTotal != null) {
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.
@@ -851,6 +859,14 @@ interface InvoiceTotalsInput {
851
859
  * @see EN 16931 BT-113
852
860
  */
853
861
  prepaidAmount?: number;
862
+ /**
863
+ * Rounding amount — the amount added to the grand total (BT-112) to round
864
+ * the amount due (BT-115) to a payable figure, e.g. `0.02` to take
865
+ * `29.95` to `29.97`. Emitted as `ram:RoundingAmount` for EN 16931 and
866
+ * EXTENDED only; the BASIC / BASIC WL XSDs do not define the element.
867
+ * @see EN 16931 BT-114, BR-CO-16
868
+ */
869
+ roundingAmount?: number;
854
870
  /**
855
871
  * Due payable amount.
856
872
  * @see EN 16931 BT-115
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.
@@ -851,6 +859,14 @@ interface InvoiceTotalsInput {
851
859
  * @see EN 16931 BT-113
852
860
  */
853
861
  prepaidAmount?: number;
862
+ /**
863
+ * Rounding amount — the amount added to the grand total (BT-112) to round
864
+ * the amount due (BT-115) to a payable figure, e.g. `0.02` to take
865
+ * `29.95` to `29.97`. Emitted as `ram:RoundingAmount` for EN 16931 and
866
+ * EXTENDED only; the BASIC / BASIC WL XSDs do not define the element.
867
+ * @see EN 16931 BT-114, BR-CO-16
868
+ */
869
+ roundingAmount?: number;
854
870
  /**
855
871
  * Due payable amount.
856
872
  * @see EN 16931 BT-115
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
@@ -450,6 +466,8 @@ function buildMonetarySummation(t, profile) {
450
466
  x += tag("ram:TaxTotalAmount", fmtAmt(t.taxTotal), {
451
467
  currencyID: t.currency
452
468
  });
469
+ if (atLeast(profile, "EN16931" /* EN16931 */) && t.roundingAmount !== void 0)
470
+ x += tag("ram:RoundingAmount", fmtAmt(t.roundingAmount));
453
471
  x += tag("ram:GrandTotalAmount", fmtAmt(t.grandTotal));
454
472
  if (atLeast(profile, "BASIC_WL" /* BASIC_WL */) && t.prepaidAmount !== void 0)
455
473
  x += tag("ram:TotalPrepaidAmount", fmtAmt(t.prepaidAmount));
@@ -890,6 +908,22 @@ function validateInput(input, profile, flavor) {
890
908
  }
891
909
  }
892
910
  }
911
+ if (req("BASIC_WL" /* BASIC_WL */) && input.totals) {
912
+ const t = input.totals;
913
+ if (t.grandTotal != null && t.duePayableAmount != null) {
914
+ const prepaid = t.prepaidAmount ?? 0;
915
+ const rounding = t.roundingAmount ?? 0;
916
+ const expected = round2(t.grandTotal - prepaid + rounding);
917
+ if (Math.abs(round2(t.duePayableAmount) - expected) > 0.01) {
918
+ addError(
919
+ errors,
920
+ "totals.duePayableAmount",
921
+ `duePayableAmount ${t.duePayableAmount} does not equal grandTotal - prepaidAmount + roundingAmount (${expected}) (BR-CO-16).`,
922
+ "BASIC_WL" /* BASIC_WL */
923
+ );
924
+ }
925
+ }
926
+ }
893
927
  if (req("BASIC_WL" /* BASIC_WL */) && input.totals) {
894
928
  const t = input.totals;
895
929
  if (t.lineTotal != null && t.taxBasisTotal != null) {
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.1",
4
4
  "type": "module",
5
5
  "description": "Embed Factur-X, ZUGFeRD, and XRechnung structured invoice XML into PDF/A-3 documents",
6
6
  "keywords": [