@stackforge-eu/factur-x 1.4.4 → 1.4.5

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.5] — 2026-11-14
11
+
12
+ - **Fixed derived line net amount ignoring `basisQuantity`.** When a line
13
+ omits `lineTotal`, `buildXml` and the BR-CO-10 check in `validateInput`
14
+ computed it as `quantity * unitPrice`, so a price per 100 units produced a
15
+ line total 100× too high and a correct `totals.lineTotal` was rejected. The
16
+ amount is now `quantity * unitPrice / basisQuantity` (EN 16931 BT-131),
17
+ rounded per line in both places. Reported by @trackoor (#26).
18
+ - **`validateInput` rejects a `basisQuantity` that is not greater than zero.**
19
+
10
20
  ## [1.4.4] — 2026-11-09
11
21
 
12
22
  - **Fixed header reference element order.** `buildXml` emitted
@@ -291,6 +301,7 @@ This element is not expected`. Reordered the three blocks in
291
301
  - Deno compatibility test suite
292
302
 
293
303
  [Unreleased]: https://github.com/StackForge-EU/factur-x/commits/main
304
+ [1.4.5]: https://github.com/StackForge-EU/factur-x/compare/v1.4.4...v1.4.5
294
305
  [1.4.4]: https://github.com/StackForge-EU/factur-x/compare/v1.4.3...v1.4.4
295
306
  [1.4.3]: https://github.com/StackForge-EU/factur-x/compare/v1.4.2...v1.4.3
296
307
  [1.4.2]: https://github.com/StackForge-EU/factur-x/compare/v1.4.1...v1.4.2
package/dist/index.cjs CHANGED
@@ -289,6 +289,9 @@ function toCents(n) {
289
289
  function roundHalfUp(n) {
290
290
  return toCents(n) / 100;
291
291
  }
292
+ function deriveLineNet(line) {
293
+ return roundHalfUp(line.quantity * line.unitPrice / (line.basisQuantity ?? 1));
294
+ }
292
295
 
293
296
  // src/core/xml-builder.ts
294
297
  var PROFILE_LEVEL = {
@@ -451,7 +454,7 @@ function buildLineItem(line, profile) {
451
454
  tx += tag("ram:RateApplicablePercent", line.vatRatePercent.toString());
452
455
  settle += tag("ram:ApplicableTradeTax", tx);
453
456
  }
454
- const lineTotal = line.lineTotal ?? roundHalfUp(line.quantity * line.unitPrice);
457
+ const lineTotal = line.lineTotal ?? deriveLineNet(line);
455
458
  settle += tag(
456
459
  "ram:SpecifiedTradeSettlementLineMonetarySummation",
457
460
  tag("ram:LineTotalAmount", fmtAmt(lineTotal))
@@ -879,6 +882,14 @@ function validateInput(input, profile, flavor) {
879
882
  if (line.unitPrice == null) {
880
883
  addError(errors, `lines[${i}].unitPrice`, "Line unit price is required", "BASIC" /* BASIC */);
881
884
  }
885
+ if (line.basisQuantity != null && !(line.basisQuantity > 0)) {
886
+ addError(
887
+ errors,
888
+ `lines[${i}].basisQuantity`,
889
+ "Line price base quantity (BT-149) must be greater than zero",
890
+ "BASIC" /* BASIC */
891
+ );
892
+ }
882
893
  });
883
894
  }
884
895
  }
@@ -975,8 +986,8 @@ function validateInput(input, profile, flavor) {
975
986
  }
976
987
  if (req("BASIC" /* BASIC */) && input.totals && input.lines) {
977
988
  const sumLineNets = input.lines.reduce((sum, l) => {
978
- const lineNet = l.lineTotal ?? (typeof l.quantity === "number" && typeof l.unitPrice === "number" ? l.quantity * l.unitPrice : 0);
979
- return sum + lineNet;
989
+ const lineNet = l.lineTotal ?? deriveLineNet(l);
990
+ return sum + (Number.isFinite(lineNet) ? lineNet : 0);
980
991
  }, 0);
981
992
  if (input.totals.lineTotal != null) {
982
993
  if (amountsDiffer(input.totals.lineTotal, sumLineNets)) {
package/dist/index.d.cts CHANGED
@@ -596,7 +596,8 @@ interface InvoiceLineInput {
596
596
  */
597
597
  basisQuantityUnitCode?: UnitCode | (string & Record<never, never>);
598
598
  /**
599
- * Line total (net) — can be derived from `quantity * unitPrice`.
599
+ * Line total (net). Derived as `quantity * unitPrice / (basisQuantity ?? 1)`,
600
+ * rounded to two decimals, when omitted.
600
601
  * @see EN 16931 BT-131
601
602
  */
602
603
  lineTotal?: number;
package/dist/index.d.ts CHANGED
@@ -596,7 +596,8 @@ interface InvoiceLineInput {
596
596
  */
597
597
  basisQuantityUnitCode?: UnitCode | (string & Record<never, never>);
598
598
  /**
599
- * Line total (net) — can be derived from `quantity * unitPrice`.
599
+ * Line total (net). Derived as `quantity * unitPrice / (basisQuantity ?? 1)`,
600
+ * rounded to two decimals, when omitted.
600
601
  * @see EN 16931 BT-131
601
602
  */
602
603
  lineTotal?: number;
package/dist/index.js CHANGED
@@ -259,6 +259,9 @@ function toCents(n) {
259
259
  function roundHalfUp(n) {
260
260
  return toCents(n) / 100;
261
261
  }
262
+ function deriveLineNet(line) {
263
+ return roundHalfUp(line.quantity * line.unitPrice / (line.basisQuantity ?? 1));
264
+ }
262
265
 
263
266
  // src/core/xml-builder.ts
264
267
  var PROFILE_LEVEL = {
@@ -421,7 +424,7 @@ function buildLineItem(line, profile) {
421
424
  tx += tag("ram:RateApplicablePercent", line.vatRatePercent.toString());
422
425
  settle += tag("ram:ApplicableTradeTax", tx);
423
426
  }
424
- const lineTotal = line.lineTotal ?? roundHalfUp(line.quantity * line.unitPrice);
427
+ const lineTotal = line.lineTotal ?? deriveLineNet(line);
425
428
  settle += tag(
426
429
  "ram:SpecifiedTradeSettlementLineMonetarySummation",
427
430
  tag("ram:LineTotalAmount", fmtAmt(lineTotal))
@@ -849,6 +852,14 @@ function validateInput(input, profile, flavor) {
849
852
  if (line.unitPrice == null) {
850
853
  addError(errors, `lines[${i}].unitPrice`, "Line unit price is required", "BASIC" /* BASIC */);
851
854
  }
855
+ if (line.basisQuantity != null && !(line.basisQuantity > 0)) {
856
+ addError(
857
+ errors,
858
+ `lines[${i}].basisQuantity`,
859
+ "Line price base quantity (BT-149) must be greater than zero",
860
+ "BASIC" /* BASIC */
861
+ );
862
+ }
852
863
  });
853
864
  }
854
865
  }
@@ -945,8 +956,8 @@ function validateInput(input, profile, flavor) {
945
956
  }
946
957
  if (req("BASIC" /* BASIC */) && input.totals && input.lines) {
947
958
  const sumLineNets = input.lines.reduce((sum, l) => {
948
- const lineNet = l.lineTotal ?? (typeof l.quantity === "number" && typeof l.unitPrice === "number" ? l.quantity * l.unitPrice : 0);
949
- return sum + lineNet;
959
+ const lineNet = l.lineTotal ?? deriveLineNet(l);
960
+ return sum + (Number.isFinite(lineNet) ? lineNet : 0);
950
961
  }, 0);
951
962
  if (input.totals.lineTotal != null) {
952
963
  if (amountsDiffer(input.totals.lineTotal, sumLineNets)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackforge-eu/factur-x",
3
- "version": "1.4.4",
3
+ "version": "1.4.5",
4
4
  "type": "module",
5
5
  "description": "Embed Factur-X, ZUGFeRD, and XRechnung structured invoice XML into PDF/A-3 documents",
6
6
  "keywords": [