@stackforge-eu/factur-x 1.0.1 → 1.0.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 +54 -0
- package/README.md +2 -2
- package/dist/index.cjs +321 -30
- package/dist/index.d.cts +62 -6
- package/dist/index.d.ts +62 -6
- package/dist/index.js +319 -32
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,56 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.0.5] — 2026-05-12
|
|
11
|
+
|
|
12
|
+
- **`ChargeIndicator`'s inner `Indicator` element was emitted in the wrong XML
|
|
13
|
+
namespace.** The XSD requires `udt:Indicator` (UnqualifiedDataType:100) but
|
|
14
|
+
the builder emitted `ram:Indicator` (ReusableAggregateBusinessInformationEntity:100).
|
|
15
|
+
The XSD rejected every invoice carrying a `SpecifiedTradeAllowanceCharge`,
|
|
16
|
+
and the BASIC schematron's `udt:Indicator='true'/'false'` predicates silently
|
|
17
|
+
matched nothing — so every downstream sum over allowances/charges (BR-S-08,
|
|
18
|
+
BR-CO-11, BR-CO-14) computed zero on the allowance/charge side and tripped.
|
|
19
|
+
Affects every caller that uses `allowancesCharges`; previously masked because
|
|
20
|
+
the test suite only exercised allowance presence, never XSD round-trip with
|
|
21
|
+
one. Regression test added in `tests/xsd-validator.test.ts`.
|
|
22
|
+
|
|
23
|
+
## [1.0.4] — 2026-05-05
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- Release workflow `publish-npm` job: bump `node-version` to `24.15.0` (current Node LTS) and roll back the npm upgrade step from `corepack prepare npm@latest --activate` to `npm install -g npm@latest`. The corepack route in v1.0.3 silently broke OIDC trusted publishing against the npm registry (sigstore provenance signing still worked, but the registry `PUT` went out unauthenticated and was rejected with a 404), so v1.0.2 and v1.0.3 never reached `registry.npmjs.org` — `dist-tags.latest` there is still `1.0.1`.
|
|
28
|
+
|
|
29
|
+
## [1.0.3] — 2026-05-05
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- Release workflow: upgrade npm via `corepack prepare npm@latest --activate` instead of `npm install -g npm@latest`, which crashed on the GitHub-hosted Node 22.22.2 toolcache where the bundled npm was missing its own `promise-retry` transitive dependency
|
|
34
|
+
- `src/core/embed.ts`: import `console` from `node:console` so the new PDF/A-3 warnings type-check under the project's `lib: ["ES2022"]` tsconfig (no DOM lib)
|
|
35
|
+
|
|
36
|
+
## [1.0.2] — 2026-05-05
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
|
|
40
|
+
- `EmbedOptions.rgbIccProfile` and `EmbedOptions.outputIntentIdentifier`: inject a PDF/A-3 `/OutputIntents` entry referencing an sRGB ICC profile when the input PDF lacks one (ISO 19005-3 §6.2.4.3)
|
|
41
|
+
- `EmbedOptions.unembeddedFonts` (`"warn"` | `"throw"` | `"ignore"`, default `"warn"`): detect and report fonts whose `FontDescriptor` carries no `FontFile`/`FontFile2`/`FontFile3` stream, including Type 0 descendant fonts and the standard 14 base fonts (ISO 19005-3 §6.2.11.4.1)
|
|
42
|
+
- Exported `XRECHNUNG_PROFILE_URN` (`urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0`)
|
|
43
|
+
- `validateInput` now accepts an optional `flavor` argument; passing `Flavor.XRECHNUNG` enables BR-DE / PEPPOL-EN16931 rules on top of the profile checks
|
|
44
|
+
- Pre-XML business-rule validation: BR-27 (non-negative line unit price), BR-CO-10 / BR-CO-13 / BR-CO-14 / BR-CO-15 / BR-CO-17, BR-62 / FX-SCH-A-000158 (URIID schemeID), BR-FX-EN-04 (delivery date or billing period), and XRechnung-only BR-DE-2, BR-DE-15, BR-DE-19 (IBAN mod-97), PEPPOL-EN16931-R001 / R010 / R020
|
|
45
|
+
|
|
46
|
+
### Changed
|
|
47
|
+
|
|
48
|
+
- **BREAKING:** `TradePartyInput.electronicAddress` is now `{ value: string; schemeID: string }` instead of a bare `string`. Required by BR-62 / FX-SCH-A-000158 — the URIID element must carry an EAS `@schemeID` (e.g. `"EM"`, `"0088"`, `"9930"`)
|
|
49
|
+
- **BREAKING:** Profile URNs in `PROFILE_URNS` updated to the EN 16931 conformance form accepted by the shipped schematron — `BASIC` → `urn:cen.eu:en16931:2017#compliant#urn:factur-x.eu:1p0:basic`, `EN16931` → `urn:cen.eu:en16931:2017`, `EXTENDED` → `urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended` (`MINIMUM` and `BASIC_WL` unchanged)
|
|
50
|
+
- XRechnung output now writes the XRechnung CIUS URN as the GuidelineSpecifiedDocumentContextParameter (BR-DE-21) instead of the underlying EN 16931 URN
|
|
51
|
+
- Line-total fallback (`quantity × unitPrice`) is rounded to currency precision so values like `0.1 × 3` no longer drift to `0.30000000000000004`
|
|
52
|
+
- `detectProfile` in the extractor now tests longer/more-specific URNs before short forms so XRechnung CIUS and conformance URNs are recognised correctly
|
|
53
|
+
|
|
54
|
+
### Fixed
|
|
55
|
+
|
|
56
|
+
- Always emit `ram:ActualDeliverySupplyChainEvent` (defaulting to `document.issueDate`) for non-prepayment BASIC+ invoices so the schematron's two-conjunct delivery rule (BR-FX-EN-04 / PEPPOL-EN16931-R008) is satisfied even when only a billing period is supplied
|
|
57
|
+
- `toXRechnung` now validates the business-process-enriched input rather than the raw input, so the auto-injected `businessProcessId` no longer trips PEPPOL-EN16931-R001 in strict mode
|
|
58
|
+
- `URIUniversalCommunication/URIID` is now emitted with the required `@schemeID` attribute
|
|
59
|
+
|
|
10
60
|
## [1.0.1] — 2026-03-01
|
|
11
61
|
|
|
12
62
|
### Added
|
|
@@ -30,4 +80,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
30
80
|
- Deno compatibility test suite
|
|
31
81
|
|
|
32
82
|
[Unreleased]: https://github.com/StackForge-EU/factur-x/commits/main
|
|
83
|
+
[1.0.5]: https://github.com/StackForge-EU/factur-x/compare/v1.0.4...v1.0.5
|
|
84
|
+
[1.0.4]: https://github.com/StackForge-EU/factur-x/compare/v1.0.3...v1.0.4
|
|
85
|
+
[1.0.3]: https://github.com/StackForge-EU/factur-x/compare/v1.0.2...v1.0.3
|
|
86
|
+
[1.0.2]: https://github.com/StackForge-EU/factur-x/compare/v1.0.1...v1.0.2
|
|
33
87
|
[1.0.1]: https://github.com/StackForge-EU/factur-x/compare/v1.0.0...v1.0.1
|
package/README.md
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
[](https://www.typescriptlang.org/)
|
|
6
6
|
[](https://nodejs.org)
|
|
7
|
-

|
|
8
|
-

|
|
7
|
+
[](https://deno.com)
|
|
8
|
+
[](https://jsr.io/@stackforge-eu/factur-x)
|
|
9
9
|
[](https://www.npmjs.com/package/@stackforge-eu/factur-x)
|
|
10
10
|
|
|
11
11
|
**Generate and embed Factur-X, ZUGFeRD, and XRechnung compliant CII XML into PDF/A-3 invoices.**
|
package/dist/index.cjs
CHANGED
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
var fs = require('fs');
|
|
4
4
|
var path = require('path');
|
|
5
5
|
var url = require('url');
|
|
6
|
+
var console = require('console');
|
|
6
7
|
var pdfLib = require('pdf-lib');
|
|
7
8
|
|
|
9
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
10
|
+
|
|
8
11
|
function _interopNamespace(e) {
|
|
9
12
|
if (e && e.__esModule) return e;
|
|
10
13
|
var n = Object.create(null);
|
|
@@ -25,6 +28,7 @@ function _interopNamespace(e) {
|
|
|
25
28
|
|
|
26
29
|
var fs__namespace = /*#__PURE__*/_interopNamespace(fs);
|
|
27
30
|
var path__namespace = /*#__PURE__*/_interopNamespace(path);
|
|
31
|
+
var console__default = /*#__PURE__*/_interopDefault(console);
|
|
28
32
|
|
|
29
33
|
// node_modules/tsup/assets/cjs_shims.js
|
|
30
34
|
var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
|
|
@@ -175,10 +179,11 @@ var Flavor = /* @__PURE__ */ ((Flavor2) => {
|
|
|
175
179
|
var PROFILE_URNS = {
|
|
176
180
|
MINIMUM: "urn:factur-x.eu:1p0:minimum",
|
|
177
181
|
BASIC_WL: "urn:factur-x.eu:1p0:basicwl",
|
|
178
|
-
BASIC: "urn:factur-x.eu:1p0:basic",
|
|
179
|
-
EN16931: "urn:
|
|
180
|
-
EXTENDED: "urn:factur-x.eu:1p0:extended"
|
|
182
|
+
BASIC: "urn:cen.eu:en16931:2017#compliant#urn:factur-x.eu:1p0:basic",
|
|
183
|
+
EN16931: "urn:cen.eu:en16931:2017",
|
|
184
|
+
EXTENDED: "urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended"
|
|
181
185
|
};
|
|
186
|
+
var XRECHNUNG_PROFILE_URN = "urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0";
|
|
182
187
|
var FLAVOR_DEFAULT_TYPE_CODES = {
|
|
183
188
|
["factur-x" /* FACTUR_X */]: "380" /* COMMERCIAL_INVOICE */,
|
|
184
189
|
["zugferd" /* ZUGFERD */]: "380" /* COMMERCIAL_INVOICE */,
|
|
@@ -348,7 +353,12 @@ function buildParty(p, profile) {
|
|
|
348
353
|
if (p.contact && atLeast(profile, "EN16931" /* EN16931 */)) x += buildContact(p.contact);
|
|
349
354
|
if (p.address && atLeast(profile, "BASIC_WL" /* BASIC_WL */)) x += buildAddress(p.address);
|
|
350
355
|
if (p.electronicAddress)
|
|
351
|
-
x += tag(
|
|
356
|
+
x += tag(
|
|
357
|
+
"ram:URIUniversalCommunication",
|
|
358
|
+
tag("ram:URIID", escapeXml(p.electronicAddress.value), {
|
|
359
|
+
schemeID: p.electronicAddress.schemeID
|
|
360
|
+
})
|
|
361
|
+
);
|
|
352
362
|
if (p.taxRegistrations) {
|
|
353
363
|
for (const tr of p.taxRegistrations) {
|
|
354
364
|
x += tag(
|
|
@@ -394,7 +404,7 @@ function buildLineItem(line, profile) {
|
|
|
394
404
|
tx += tag("ram:RateApplicablePercent", line.vatRatePercent.toString());
|
|
395
405
|
settle += tag("ram:ApplicableTradeTax", tx);
|
|
396
406
|
}
|
|
397
|
-
const lineTotal = line.lineTotal ?? line.quantity * line.unitPrice;
|
|
407
|
+
const lineTotal = line.lineTotal ?? Math.round(line.quantity * line.unitPrice * 100) / 100;
|
|
398
408
|
settle += tag(
|
|
399
409
|
"ram:SpecifiedTradeSettlementLineMonetarySummation",
|
|
400
410
|
tag("ram:LineTotalAmount", fmtAmt(lineTotal))
|
|
@@ -405,7 +415,7 @@ function buildLineItem(line, profile) {
|
|
|
405
415
|
);
|
|
406
416
|
}
|
|
407
417
|
function buildAllowanceCharge(ac) {
|
|
408
|
-
let x = tag("ram:ChargeIndicator", tag("
|
|
418
|
+
let x = tag("ram:ChargeIndicator", tag("udt:Indicator", ac.isCharge ? "true" : "false"));
|
|
409
419
|
if (ac.percent !== void 0) x += tag("ram:CalculationPercent", ac.percent.toString());
|
|
410
420
|
if (ac.baseAmount !== void 0) x += tag("ram:BasisAmount", fmtAmt(ac.baseAmount));
|
|
411
421
|
x += tag("ram:ActualAmount", fmtAmt(ac.amount));
|
|
@@ -477,9 +487,10 @@ function buildXml(input, profile, flavor) {
|
|
|
477
487
|
"ram:BusinessProcessSpecifiedDocumentContextParameter",
|
|
478
488
|
tag("ram:ID", escapeXml(doc.businessProcessId))
|
|
479
489
|
);
|
|
490
|
+
const profileUrn = fl === "xrechnung" /* XRECHNUNG */ ? XRECHNUNG_PROFILE_URN : PROFILE_URNS[profile];
|
|
480
491
|
ctx += tag(
|
|
481
492
|
"ram:GuidelineSpecifiedDocumentContextParameter",
|
|
482
|
-
tag("ram:ID",
|
|
493
|
+
tag("ram:ID", profileUrn)
|
|
483
494
|
);
|
|
484
495
|
let edoc = tag("ram:ID", escapeXml(doc.id));
|
|
485
496
|
if (doc.name && atLeast(profile, "EXTENDED" /* EXTENDED */)) edoc += tag("ram:Name", escapeXml(doc.name));
|
|
@@ -532,6 +543,7 @@ function buildXml(input, profile, flavor) {
|
|
|
532
543
|
}
|
|
533
544
|
tx += tag("ram:ApplicableHeaderTradeAgreement", agree);
|
|
534
545
|
let del = "";
|
|
546
|
+
let hasActualDelivery = false;
|
|
535
547
|
if (input.delivery && atLeast(profile, "BASIC_WL" /* BASIC_WL */)) {
|
|
536
548
|
if (input.delivery.partyName || input.delivery.location) {
|
|
537
549
|
let sp = "";
|
|
@@ -539,11 +551,13 @@ function buildXml(input, profile, flavor) {
|
|
|
539
551
|
if (input.delivery.location) sp += buildAddress(input.delivery.location);
|
|
540
552
|
del += tag("ram:ShipToTradeParty", sp);
|
|
541
553
|
}
|
|
542
|
-
if (input.delivery.date)
|
|
554
|
+
if (input.delivery.date) {
|
|
543
555
|
del += tag(
|
|
544
556
|
"ram:ActualDeliverySupplyChainEvent",
|
|
545
557
|
dateEl("ram:OccurrenceDateTime", input.delivery.date)
|
|
546
558
|
);
|
|
559
|
+
hasActualDelivery = true;
|
|
560
|
+
}
|
|
547
561
|
if (input.delivery.despatchAdviceReference)
|
|
548
562
|
del += tag(
|
|
549
563
|
"ram:DespatchAdviceReferencedDocument",
|
|
@@ -559,6 +573,12 @@ function buildXml(input, profile, flavor) {
|
|
|
559
573
|
}
|
|
560
574
|
}
|
|
561
575
|
}
|
|
576
|
+
if (atLeast(profile, "BASIC" /* BASIC */) && !hasActualDelivery && typeCode !== "386" /* PREPAYMENT_INVOICE */) {
|
|
577
|
+
del += tag(
|
|
578
|
+
"ram:ActualDeliverySupplyChainEvent",
|
|
579
|
+
dateEl("ram:OccurrenceDateTime", doc.issueDate)
|
|
580
|
+
);
|
|
581
|
+
}
|
|
562
582
|
tx += tag("ram:ApplicableHeaderTradeDelivery", del);
|
|
563
583
|
let sett = "";
|
|
564
584
|
if (payment?.creditorReference && atLeast(profile, "BASIC_WL" /* BASIC_WL */))
|
|
@@ -636,7 +656,7 @@ var PROFILE_LEVEL2 = {
|
|
|
636
656
|
function addError(errors, field, message, profile) {
|
|
637
657
|
errors.push({ field, message, profile });
|
|
638
658
|
}
|
|
639
|
-
function validateInput(input, profile) {
|
|
659
|
+
function validateInput(input, profile, flavor) {
|
|
640
660
|
const errors = [];
|
|
641
661
|
const level = PROFILE_LEVEL2[profile];
|
|
642
662
|
const req = (minProfile) => level >= PROFILE_LEVEL2[minProfile];
|
|
@@ -823,11 +843,193 @@ function validateInput(input, profile) {
|
|
|
823
843
|
}
|
|
824
844
|
});
|
|
825
845
|
}
|
|
846
|
+
if (req("BASIC_WL" /* BASIC_WL */)) {
|
|
847
|
+
for (const role of ["seller", "buyer"]) {
|
|
848
|
+
const ea = input[role]?.electronicAddress;
|
|
849
|
+
if (ea && !ea.schemeID) {
|
|
850
|
+
addError(
|
|
851
|
+
errors,
|
|
852
|
+
`${role}.electronicAddress.schemeID`,
|
|
853
|
+
`${role.charAt(0).toUpperCase() + role.slice(1)} electronic address must include schemeID (BR-62 / FX-SCH-A-000158).`,
|
|
854
|
+
"BASIC_WL" /* BASIC_WL */
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
if (req("BASIC" /* BASIC */) && input.lines) {
|
|
860
|
+
input.lines.forEach((line, i) => {
|
|
861
|
+
if (typeof line.unitPrice === "number" && line.unitPrice < 0) {
|
|
862
|
+
addError(
|
|
863
|
+
errors,
|
|
864
|
+
`lines[${i}].unitPrice`,
|
|
865
|
+
"Line unit price must not be negative (BR-27). Use allowancesCharges for discounts.",
|
|
866
|
+
"BASIC" /* BASIC */
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
if (req("BASIC_WL" /* BASIC_WL */) && input.totals) {
|
|
872
|
+
const t = input.totals;
|
|
873
|
+
if (t.taxBasisTotal != null && t.taxTotal != null && t.grandTotal != null) {
|
|
874
|
+
const expected = round2(t.taxBasisTotal + t.taxTotal);
|
|
875
|
+
if (Math.abs(round2(t.grandTotal) - expected) > 0.01) {
|
|
876
|
+
addError(
|
|
877
|
+
errors,
|
|
878
|
+
"totals.grandTotal",
|
|
879
|
+
`Grand total ${t.grandTotal} does not equal taxBasisTotal + taxTotal (${expected}) (BR-CO-15).`,
|
|
880
|
+
"BASIC_WL" /* BASIC_WL */
|
|
881
|
+
);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
if (req("BASIC_WL" /* BASIC_WL */) && input.totals) {
|
|
886
|
+
const t = input.totals;
|
|
887
|
+
if (t.lineTotal != null && t.taxBasisTotal != null) {
|
|
888
|
+
const allowance = t.allowanceTotal ?? 0;
|
|
889
|
+
const charge = t.chargeTotal ?? 0;
|
|
890
|
+
const expected = round2(t.lineTotal - allowance + charge);
|
|
891
|
+
if (Math.abs(round2(t.taxBasisTotal) - expected) > 0.01) {
|
|
892
|
+
addError(
|
|
893
|
+
errors,
|
|
894
|
+
"totals.taxBasisTotal",
|
|
895
|
+
`taxBasisTotal ${t.taxBasisTotal} does not equal lineTotal - allowanceTotal + chargeTotal (${expected}) (BR-CO-13).`,
|
|
896
|
+
"BASIC_WL" /* BASIC_WL */
|
|
897
|
+
);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
if (req("BASIC" /* BASIC */) && input.totals && input.lines) {
|
|
902
|
+
const sumLineNets = input.lines.reduce((sum, l) => {
|
|
903
|
+
const lineNet = l.lineTotal ?? (typeof l.quantity === "number" && typeof l.unitPrice === "number" ? l.quantity * l.unitPrice : 0);
|
|
904
|
+
return sum + lineNet;
|
|
905
|
+
}, 0);
|
|
906
|
+
if (input.totals.lineTotal != null) {
|
|
907
|
+
if (Math.abs(round2(input.totals.lineTotal) - round2(sumLineNets)) > 0.01) {
|
|
908
|
+
addError(
|
|
909
|
+
errors,
|
|
910
|
+
"totals.lineTotal",
|
|
911
|
+
`lineTotal ${input.totals.lineTotal} does not equal \u03A3 line nets (${round2(sumLineNets)}) (BR-CO-10).`,
|
|
912
|
+
"BASIC" /* BASIC */
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
if (req("BASIC_WL" /* BASIC_WL */) && input.totals?.taxTotal != null && input.vatBreakdown) {
|
|
918
|
+
const sumVat = input.vatBreakdown.reduce((s, v) => s + (v.taxAmount ?? 0), 0);
|
|
919
|
+
if (Math.abs(round2(input.totals.taxTotal) - round2(sumVat)) > 0.01) {
|
|
920
|
+
addError(
|
|
921
|
+
errors,
|
|
922
|
+
"totals.taxTotal",
|
|
923
|
+
`taxTotal ${input.totals.taxTotal} does not equal \u03A3 vatBreakdown.taxAmount (${round2(sumVat)}) (BR-CO-14).`,
|
|
924
|
+
"BASIC_WL" /* BASIC_WL */
|
|
925
|
+
);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
if (req("BASIC_WL" /* BASIC_WL */) && input.vatBreakdown) {
|
|
929
|
+
input.vatBreakdown.forEach((vb, i) => {
|
|
930
|
+
if (vb.taxableAmount != null && vb.taxAmount != null && vb.ratePercent != null) {
|
|
931
|
+
const expected = round2(vb.taxableAmount * vb.ratePercent / 100);
|
|
932
|
+
if (Math.abs(round2(vb.taxAmount) - expected) > 0.01) {
|
|
933
|
+
addError(
|
|
934
|
+
errors,
|
|
935
|
+
`vatBreakdown[${i}].taxAmount`,
|
|
936
|
+
`taxAmount ${vb.taxAmount} does not equal round(taxableAmount \xD7 ratePercent / 100) = ${expected} (BR-CO-17).`,
|
|
937
|
+
"BASIC_WL" /* BASIC_WL */
|
|
938
|
+
);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
if (req("BASIC" /* BASIC */) && input.document) {
|
|
944
|
+
const isPrepayment = input.document.typeCode === "386";
|
|
945
|
+
if (!isPrepayment) {
|
|
946
|
+
const hasDeliveryDate = !!input.delivery?.date;
|
|
947
|
+
const hasBillingPeriod = !!input.billingPeriod;
|
|
948
|
+
if (!hasDeliveryDate && !hasBillingPeriod) {
|
|
949
|
+
addError(
|
|
950
|
+
errors,
|
|
951
|
+
"delivery.date",
|
|
952
|
+
"Provide delivery.date or billingPeriod (BR-FX-EN-04).",
|
|
953
|
+
"BASIC" /* BASIC */
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
if (flavor === "xrechnung" /* XRECHNUNG */) {
|
|
959
|
+
if (!input.document?.buyerReference) {
|
|
960
|
+
addError(
|
|
961
|
+
errors,
|
|
962
|
+
"document.buyerReference",
|
|
963
|
+
"BuyerReference (BT-10) is required for XRechnung (BR-DE-15).",
|
|
964
|
+
"EN16931" /* EN16931 */
|
|
965
|
+
);
|
|
966
|
+
}
|
|
967
|
+
if (!input.document?.businessProcessId) {
|
|
968
|
+
addError(
|
|
969
|
+
errors,
|
|
970
|
+
"document.businessProcessId",
|
|
971
|
+
"Business process (BT-23) is required for XRechnung (PEPPOL-EN16931-R001).",
|
|
972
|
+
"EN16931" /* EN16931 */
|
|
973
|
+
);
|
|
974
|
+
}
|
|
975
|
+
if (!input.seller?.contact || !input.seller.contact.name && !input.seller.contact.email && !input.seller.contact.phone) {
|
|
976
|
+
addError(
|
|
977
|
+
errors,
|
|
978
|
+
"seller.contact",
|
|
979
|
+
"Seller contact (BG-6) with name, email or phone is required for XRechnung (BR-DE-2).",
|
|
980
|
+
"EN16931" /* EN16931 */
|
|
981
|
+
);
|
|
982
|
+
}
|
|
983
|
+
if (!input.seller?.electronicAddress?.value || !input.seller.electronicAddress.schemeID) {
|
|
984
|
+
addError(
|
|
985
|
+
errors,
|
|
986
|
+
"seller.electronicAddress",
|
|
987
|
+
"Seller electronic address (BT-34) with non-empty schemeID is required for XRechnung (PEPPOL-EN16931-R020 / BR-62).",
|
|
988
|
+
"EN16931" /* EN16931 */
|
|
989
|
+
);
|
|
990
|
+
}
|
|
991
|
+
if (!input.buyer?.electronicAddress?.value || !input.buyer.electronicAddress.schemeID) {
|
|
992
|
+
addError(
|
|
993
|
+
errors,
|
|
994
|
+
"buyer.electronicAddress",
|
|
995
|
+
"Buyer electronic address (BT-49) with non-empty schemeID is required for XRechnung (PEPPOL-EN16931-R010 / BR-62).",
|
|
996
|
+
"EN16931" /* EN16931 */
|
|
997
|
+
);
|
|
998
|
+
}
|
|
999
|
+
if (input.payment?.meansCode === "58" && input.payment.iban) {
|
|
1000
|
+
if (!isValidIban(input.payment.iban)) {
|
|
1001
|
+
addError(
|
|
1002
|
+
errors,
|
|
1003
|
+
"payment.iban",
|
|
1004
|
+
"IBAN is not syntactically valid (BR-DE-19).",
|
|
1005
|
+
"EN16931" /* EN16931 */
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
826
1010
|
return {
|
|
827
1011
|
valid: errors.length === 0,
|
|
828
1012
|
errors
|
|
829
1013
|
};
|
|
830
1014
|
}
|
|
1015
|
+
function round2(n) {
|
|
1016
|
+
return Math.round(n * 100) / 100;
|
|
1017
|
+
}
|
|
1018
|
+
function isValidIban(iban) {
|
|
1019
|
+
const normalized = iban.replace(/\s+/g, "").toUpperCase();
|
|
1020
|
+
if (!/^[A-Z]{2}\d{2}[A-Z0-9]{1,30}$/.test(normalized)) return false;
|
|
1021
|
+
const rearranged = normalized.slice(4) + normalized.slice(0, 4);
|
|
1022
|
+
let expanded = "";
|
|
1023
|
+
for (const ch of rearranged) {
|
|
1024
|
+
expanded += /[A-Z]/.test(ch) ? String(ch.charCodeAt(0) - 55) : ch;
|
|
1025
|
+
}
|
|
1026
|
+
let remainder = 0;
|
|
1027
|
+
for (let i = 0; i < expanded.length; i += 7) {
|
|
1028
|
+
const chunk = remainder.toString() + expanded.slice(i, i + 7);
|
|
1029
|
+
remainder = parseInt(chunk, 10) % 97;
|
|
1030
|
+
}
|
|
1031
|
+
return remainder === 1;
|
|
1032
|
+
}
|
|
831
1033
|
function findSchemaDir(startDir) {
|
|
832
1034
|
let dir = startDir;
|
|
833
1035
|
for (let i = 0; i < 6; i++) {
|
|
@@ -909,6 +1111,71 @@ var AF_RELATIONSHIP_MAP = {
|
|
|
909
1111
|
Data: pdfLib.AFRelationship.Data,
|
|
910
1112
|
Source: pdfLib.AFRelationship.Source
|
|
911
1113
|
};
|
|
1114
|
+
function ensureOutputIntent(pdfDoc, iccBytes, identifier) {
|
|
1115
|
+
const existing = pdfDoc.catalog.lookup(pdfLib.PDFName.of("OutputIntents"));
|
|
1116
|
+
if (existing instanceof pdfLib.PDFArray && existing.size() > 0) return;
|
|
1117
|
+
const iccStream = pdfDoc.context.flateStream(iccBytes, { N: 3 });
|
|
1118
|
+
const iccRef = pdfDoc.context.register(iccStream);
|
|
1119
|
+
const intent = pdfDoc.context.obj({
|
|
1120
|
+
Type: "OutputIntent",
|
|
1121
|
+
S: "GTS_PDFA1",
|
|
1122
|
+
OutputConditionIdentifier: pdfLib.PDFString.of(identifier),
|
|
1123
|
+
RegistryName: pdfLib.PDFString.of("http://www.color.org"),
|
|
1124
|
+
Info: pdfLib.PDFString.of(identifier),
|
|
1125
|
+
DestOutputProfile: iccRef
|
|
1126
|
+
});
|
|
1127
|
+
const intents = pdfLib.PDFArray.withContext(pdfDoc.context);
|
|
1128
|
+
intents.push(intent);
|
|
1129
|
+
pdfDoc.catalog.set(pdfLib.PDFName.of("OutputIntents"), intents);
|
|
1130
|
+
}
|
|
1131
|
+
function findUnembeddedFonts(pdfDoc) {
|
|
1132
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1133
|
+
const unembedded = /* @__PURE__ */ new Set();
|
|
1134
|
+
const FONT_FILE_KEYS = [
|
|
1135
|
+
pdfLib.PDFName.of("FontFile"),
|
|
1136
|
+
pdfLib.PDFName.of("FontFile2"),
|
|
1137
|
+
pdfLib.PDFName.of("FontFile3")
|
|
1138
|
+
];
|
|
1139
|
+
const inspectFontDict = (font) => {
|
|
1140
|
+
const subtype = font.lookup(pdfLib.PDFName.of("Subtype"));
|
|
1141
|
+
const baseFont = font.lookup(pdfLib.PDFName.of("BaseFont"));
|
|
1142
|
+
const baseFontName = baseFont instanceof pdfLib.PDFName ? baseFont.asString() : "<unknown>";
|
|
1143
|
+
const subtypeName = subtype instanceof pdfLib.PDFName ? subtype.asString() : "";
|
|
1144
|
+
if (subtypeName === "/Type0") {
|
|
1145
|
+
const desc = font.lookup(pdfLib.PDFName.of("DescendantFonts"));
|
|
1146
|
+
if (desc instanceof pdfLib.PDFArray) {
|
|
1147
|
+
for (let i = 0; i < desc.size(); i += 1) {
|
|
1148
|
+
const child = desc.lookup(i);
|
|
1149
|
+
if (child instanceof pdfLib.PDFDict) inspectFontDict(child);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
const descriptor = font.lookup(pdfLib.PDFName.of("FontDescriptor"));
|
|
1155
|
+
if (!(descriptor instanceof pdfLib.PDFDict)) {
|
|
1156
|
+
unembedded.add(baseFontName);
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
const hasEmbedded = FONT_FILE_KEYS.some((k) => descriptor.has(k));
|
|
1160
|
+
if (!hasEmbedded) unembedded.add(baseFontName);
|
|
1161
|
+
};
|
|
1162
|
+
for (const page of pdfDoc.getPages()) {
|
|
1163
|
+
const resources = page.node.Resources();
|
|
1164
|
+
if (!resources) continue;
|
|
1165
|
+
const fonts = resources.lookup(pdfLib.PDFName.of("Font"));
|
|
1166
|
+
if (!(fonts instanceof pdfLib.PDFDict)) continue;
|
|
1167
|
+
for (const [, value] of fonts.entries()) {
|
|
1168
|
+
const ref = value instanceof pdfLib.PDFRef ? value : void 0;
|
|
1169
|
+
if (ref) {
|
|
1170
|
+
if (seen.has(ref)) continue;
|
|
1171
|
+
seen.add(ref);
|
|
1172
|
+
}
|
|
1173
|
+
const font = ref ? pdfDoc.context.lookup(ref) : value;
|
|
1174
|
+
if (font instanceof pdfLib.PDFDict) inspectFontDict(font);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
return Array.from(unembedded);
|
|
1178
|
+
}
|
|
912
1179
|
function resolveAfRelationship(profile, override) {
|
|
913
1180
|
if (override) return AF_RELATIONSHIP_MAP[override];
|
|
914
1181
|
switch (profile) {
|
|
@@ -1025,6 +1292,9 @@ async function embedFacturX(options) {
|
|
|
1025
1292
|
validateBeforeEmbed = true,
|
|
1026
1293
|
validateXsd: runXsdValidation = false,
|
|
1027
1294
|
addPdfA3Metadata = true,
|
|
1295
|
+
rgbIccProfile,
|
|
1296
|
+
outputIntentIdentifier = "sRGB IEC61966-2.1",
|
|
1297
|
+
unembeddedFonts = "warn",
|
|
1028
1298
|
meta
|
|
1029
1299
|
} = options;
|
|
1030
1300
|
if (!options.xml && !options.input) {
|
|
@@ -1038,7 +1308,7 @@ async function embedFacturX(options) {
|
|
|
1038
1308
|
if (!xml) {
|
|
1039
1309
|
const input = options.input;
|
|
1040
1310
|
if (validateBeforeEmbed) {
|
|
1041
|
-
validation = validateInput(input, profile);
|
|
1311
|
+
validation = validateInput(input, profile, flavor);
|
|
1042
1312
|
if (!validation.valid) {
|
|
1043
1313
|
throw new Error(
|
|
1044
1314
|
`Input validation failed for profile "${profile}":
|
|
@@ -1079,6 +1349,22 @@ async function embedFacturX(options) {
|
|
|
1079
1349
|
pdfDoc.setCreationDate(now);
|
|
1080
1350
|
pdfDoc.setModificationDate(now);
|
|
1081
1351
|
if (addPdfA3Metadata) {
|
|
1352
|
+
if (rgbIccProfile) {
|
|
1353
|
+
const iccBytes = rgbIccProfile instanceof Uint8Array ? rgbIccProfile : new Uint8Array(rgbIccProfile);
|
|
1354
|
+
ensureOutputIntent(pdfDoc, iccBytes, outputIntentIdentifier);
|
|
1355
|
+
} else if (!(pdfDoc.catalog.lookup(pdfLib.PDFName.of("OutputIntents")) instanceof pdfLib.PDFArray)) {
|
|
1356
|
+
console__default.default.warn(
|
|
1357
|
+
"[factur-x] addPdfA3Metadata is enabled but no rgbIccProfile was supplied and the input PDF has no /OutputIntents. The resulting PDF will not be PDF/A-3 conformant."
|
|
1358
|
+
);
|
|
1359
|
+
}
|
|
1360
|
+
if (unembeddedFonts !== "ignore") {
|
|
1361
|
+
const missing = findUnembeddedFonts(pdfDoc);
|
|
1362
|
+
if (missing.length > 0) {
|
|
1363
|
+
const message = `[factur-x] PDF/A-3 requires every font program to be embedded; the following fonts are not embedded: ${missing.join(", ")}. Configure your PDF generator to embed all fonts.`;
|
|
1364
|
+
if (unembeddedFonts === "throw") throw new Error(message);
|
|
1365
|
+
console__default.default.warn(message);
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1082
1368
|
const isoDate = now.toISOString();
|
|
1083
1369
|
const xmpXml = buildXmpMetadata({
|
|
1084
1370
|
title,
|
|
@@ -1125,18 +1411,22 @@ function resolve(doc, obj) {
|
|
|
1125
1411
|
return obj;
|
|
1126
1412
|
}
|
|
1127
1413
|
function detectProfile(xml) {
|
|
1128
|
-
const profileMap =
|
|
1129
|
-
"urn:factur-x.eu:1p0:
|
|
1130
|
-
"urn:
|
|
1131
|
-
"urn:factur-x.eu:1p0:basic"
|
|
1132
|
-
"urn:
|
|
1133
|
-
"urn:
|
|
1134
|
-
"urn:cen.eu:en16931:2017"
|
|
1135
|
-
"urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:
|
|
1136
|
-
"urn:cen.eu:en16931:2017
|
|
1137
|
-
"urn:
|
|
1138
|
-
|
|
1139
|
-
|
|
1414
|
+
const profileMap = [
|
|
1415
|
+
["urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended", "EXTENDED" /* EXTENDED */],
|
|
1416
|
+
["urn:cen.eu:en16931:2017#conformant#urn:zugferd.de:2p0:extended", "EXTENDED" /* EXTENDED */],
|
|
1417
|
+
["urn:cen.eu:en16931:2017#compliant#urn:factur-x.eu:1p0:basic", "BASIC" /* BASIC */],
|
|
1418
|
+
["urn:cen.eu:en16931:2017#compliant#urn:zugferd.de:2p0:basic", "BASIC" /* BASIC */],
|
|
1419
|
+
["urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0", "EN16931" /* EN16931 */],
|
|
1420
|
+
["urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_2.3", "EN16931" /* EN16931 */],
|
|
1421
|
+
["urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_2.2", "EN16931" /* EN16931 */],
|
|
1422
|
+
["urn:cen.eu:en16931:2017", "EN16931" /* EN16931 */],
|
|
1423
|
+
["urn:factur-x.eu:1p0:minimum", "MINIMUM" /* MINIMUM */],
|
|
1424
|
+
["urn:factur-x.eu:1p0:basicwl", "BASIC_WL" /* BASIC_WL */],
|
|
1425
|
+
["urn:factur-x.eu:1p0:basic", "BASIC" /* BASIC */],
|
|
1426
|
+
["urn:factur-x.eu:1p0:en16931", "EN16931" /* EN16931 */],
|
|
1427
|
+
["urn:factur-x.eu:1p0:extended", "EXTENDED" /* EXTENDED */]
|
|
1428
|
+
];
|
|
1429
|
+
for (const [urn, profile] of profileMap) {
|
|
1140
1430
|
if (xml.includes(urn)) return profile;
|
|
1141
1431
|
}
|
|
1142
1432
|
return void 0;
|
|
@@ -1204,9 +1494,16 @@ function toXRechnung(input, options) {
|
|
|
1204
1494
|
const profile = options?.profile ?? "EN16931" /* EN16931 */;
|
|
1205
1495
|
const validate = options?.validate ?? true;
|
|
1206
1496
|
validateFlavorProfile("xrechnung" /* XRECHNUNG */, profile);
|
|
1497
|
+
const enrichedInput = {
|
|
1498
|
+
...input,
|
|
1499
|
+
document: {
|
|
1500
|
+
...input.document,
|
|
1501
|
+
businessProcessId: resolveBusinessProcessUrn(input, "xrechnung" /* XRECHNUNG */)
|
|
1502
|
+
}
|
|
1503
|
+
};
|
|
1207
1504
|
let validation;
|
|
1208
1505
|
if (validate) {
|
|
1209
|
-
validation = validateInput(
|
|
1506
|
+
validation = validateInput(enrichedInput, profile, "xrechnung" /* XRECHNUNG */);
|
|
1210
1507
|
if (!validation.valid) {
|
|
1211
1508
|
throw new Error(
|
|
1212
1509
|
`Input validation failed for XRechnung (profile "${profile}"):
|
|
@@ -1214,13 +1511,6 @@ function toXRechnung(input, options) {
|
|
|
1214
1511
|
);
|
|
1215
1512
|
}
|
|
1216
1513
|
}
|
|
1217
|
-
const enrichedInput = {
|
|
1218
|
-
...input,
|
|
1219
|
-
document: {
|
|
1220
|
-
...input.document,
|
|
1221
|
-
businessProcessId: resolveBusinessProcessUrn(input, "xrechnung" /* XRECHNUNG */)
|
|
1222
|
-
}
|
|
1223
|
-
};
|
|
1224
1514
|
const xml = buildXml(enrichedInput, profile, "xrechnung" /* XRECHNUNG */);
|
|
1225
1515
|
return {
|
|
1226
1516
|
xml,
|
|
@@ -1326,6 +1616,7 @@ exports.PROFILE_URNS = PROFILE_URNS;
|
|
|
1326
1616
|
exports.Profile = Profile;
|
|
1327
1617
|
exports.UnitCode = UnitCode;
|
|
1328
1618
|
exports.VatCategoryCode = VatCategoryCode;
|
|
1619
|
+
exports.XRECHNUNG_PROFILE_URN = XRECHNUNG_PROFILE_URN;
|
|
1329
1620
|
exports.buildXml = buildXml;
|
|
1330
1621
|
exports.embedFacturX = embedFacturX;
|
|
1331
1622
|
exports.escapeXml = escapeXml;
|
package/dist/index.d.cts
CHANGED
|
@@ -480,10 +480,22 @@ interface TradePartyInput {
|
|
|
480
480
|
*/
|
|
481
481
|
contact?: ContactInput;
|
|
482
482
|
/**
|
|
483
|
-
* Electronic address (e.g.
|
|
483
|
+
* Electronic address (e.g. PEPPOL participant ID, email).
|
|
484
|
+
*
|
|
485
|
+
* `value` is the address itself (e.g. `"info@stack-forge.eu"`,
|
|
486
|
+
* `"0088:1234567890123"`).
|
|
487
|
+
* `schemeID` is the EAS code list value (e.g. `"EM"` for email,
|
|
488
|
+
* `"9930"` for German VAT, `"0088"` for GLN). Required by BR-62 /
|
|
489
|
+
* FX-SCH-A-000158 — a URIID without `@schemeID` is rejected by every
|
|
490
|
+
* conforming validator (Mustang, KoSIT, veraPDF).
|
|
491
|
+
*
|
|
484
492
|
* @see EN 16931 BT-34 / BT-49
|
|
493
|
+
* @see https://docs.peppol.eu/poacc/billing/3.0/codelist/eas/
|
|
485
494
|
*/
|
|
486
|
-
electronicAddress?:
|
|
495
|
+
electronicAddress?: {
|
|
496
|
+
value: string;
|
|
497
|
+
schemeID: string;
|
|
498
|
+
};
|
|
487
499
|
/**
|
|
488
500
|
* Tax registrations (VAT ID, fiscal number).
|
|
489
501
|
* @see EN 16931 BT-31 / BT-48
|
|
@@ -970,8 +982,20 @@ declare enum Flavor {
|
|
|
970
982
|
XRECHNUNG = "xrechnung",
|
|
971
983
|
CHRONO_PRO = "chrono-pro"
|
|
972
984
|
}
|
|
973
|
-
/**
|
|
985
|
+
/**
|
|
986
|
+
* Profile URNs (GuidelineSpecifiedDocumentContextParameter / BT-24).
|
|
987
|
+
*
|
|
988
|
+
* BASIC, EN16931, and EXTENDED use the EN 16931 conformance prefix because
|
|
989
|
+
* those profiles are CIUS / extension claims against the European norm. The
|
|
990
|
+
* exact strings are the only values accepted by the schematron shipped under
|
|
991
|
+
* `schema/{basic,en16931,extended}/` (the codedb cl id="1" enumeration).
|
|
992
|
+
*/
|
|
974
993
|
declare const PROFILE_URNS: Record<Profile, string>;
|
|
994
|
+
/**
|
|
995
|
+
* XRechnung CIUS URN (BR-DE-21). Used as the profile URN when emitting
|
|
996
|
+
* XRechnung instead of pure Factur-X / ZUGFeRD.
|
|
997
|
+
*/
|
|
998
|
+
declare const XRECHNUNG_PROFILE_URN = "urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0";
|
|
975
999
|
|
|
976
1000
|
/**
|
|
977
1001
|
* Default document type code per flavor.
|
|
@@ -1099,11 +1123,13 @@ interface ValidationResult {
|
|
|
1099
1123
|
* Validates that the input contains all required fields for the given profile.
|
|
1100
1124
|
* Requirements are cumulative: each profile includes all requirements of lower levels.
|
|
1101
1125
|
*
|
|
1102
|
-
* @param input
|
|
1126
|
+
* @param input - Invoice input to validate
|
|
1103
1127
|
* @param profile - Target profile (MINIMUM, BASIC_WL, BASIC, EN16931, or EXTENDED)
|
|
1128
|
+
* @param flavor - Optional flavor; when {@link Flavor.XRECHNUNG}, additional
|
|
1129
|
+
* BR-DE / PEPPOL-EN16931 rules are applied on top of the profile rules.
|
|
1104
1130
|
* @returns Validation result with valid flag and any errors
|
|
1105
1131
|
*/
|
|
1106
|
-
declare function validateInput(input: FacturXInvoiceInput, profile: Profile): ValidationResult;
|
|
1132
|
+
declare function validateInput(input: FacturXInvoiceInput, profile: Profile, flavor?: Flavor): ValidationResult;
|
|
1107
1133
|
|
|
1108
1134
|
/**
|
|
1109
1135
|
* XSD Schema Validation
|
|
@@ -1203,6 +1229,36 @@ interface EmbedOptions {
|
|
|
1203
1229
|
validateXsd?: boolean;
|
|
1204
1230
|
/** Add PDF/A-3 metadata (default: `true`). */
|
|
1205
1231
|
addPdfA3Metadata?: boolean;
|
|
1232
|
+
/**
|
|
1233
|
+
* RGB ICC profile bytes used to inject the PDF/A required `/OutputIntents`
|
|
1234
|
+
* entry when the input PDF does not already have one.
|
|
1235
|
+
*
|
|
1236
|
+
* PDF/A-3 forbids DeviceRGB without an OutputIntent that references an
|
|
1237
|
+
* RGB destination profile (ISO 19005-3 §6.2.4.3). pdf-lib does not produce
|
|
1238
|
+
* PDF/A by default, so this library will add the entry on save when this
|
|
1239
|
+
* profile is supplied.
|
|
1240
|
+
*
|
|
1241
|
+
* Drop in any sRGB ICC v2 profile compatible with your licensing needs
|
|
1242
|
+
* (e.g. the ICC.org public profile, or one bundled with Argyll CMS / PDFBox).
|
|
1243
|
+
* When omitted and {@link addPdfA3Metadata} is `true`, the library logs a
|
|
1244
|
+
* warning and the resulting PDF will not be PDF/A-3 conformant.
|
|
1245
|
+
*/
|
|
1246
|
+
rgbIccProfile?: Uint8Array | Buffer;
|
|
1247
|
+
/**
|
|
1248
|
+
* Output intent identifier written into the OutputIntent dictionary.
|
|
1249
|
+
* Defaults to `"sRGB IEC61966-2.1"` which matches the standard sRGB profile.
|
|
1250
|
+
*/
|
|
1251
|
+
outputIntentIdentifier?: string;
|
|
1252
|
+
/**
|
|
1253
|
+
* How to react when the input PDF references fonts that are not embedded.
|
|
1254
|
+
* PDF/A-3 requires every font to be embedded (ISO 19005-3 §6.2.11.4.1) —
|
|
1255
|
+
* this library cannot fix that post-hoc, so it surfaces the gap.
|
|
1256
|
+
*
|
|
1257
|
+
* - `"warn"` (default): log a warning to `console.warn` and continue.
|
|
1258
|
+
* - `"throw"`: reject the PDF with an error.
|
|
1259
|
+
* - `"ignore"`: skip the check entirely.
|
|
1260
|
+
*/
|
|
1261
|
+
unembeddedFonts?: "warn" | "throw" | "ignore";
|
|
1206
1262
|
/**
|
|
1207
1263
|
* Override the AF relationship for the embedded XML attachment.
|
|
1208
1264
|
*
|
|
@@ -1379,4 +1435,4 @@ interface XRechnungResult {
|
|
|
1379
1435
|
*/
|
|
1380
1436
|
declare function toXRechnung(input: FacturXInvoiceInput, options?: XRechnungOptions): XRechnungResult;
|
|
1381
1437
|
|
|
1382
|
-
export { type AddressInput, type AllowanceChargeInput, type BillingPeriodInput, type ContactInput, type DeliveryInput, type DocumentReferenceInput, DocumentTypeCode, type EmbedOptions, type EmbedResult, type ExtractOptions, type ExtractResult, FLAVOR_CONFIGS, FLAVOR_DEFAULT_TYPE_CODES, type FacturXInvoiceInput, Flavor, type FlavorConfig, type InvoiceDocumentInput, type InvoiceLineInput, type InvoiceTotalsInput, type LegalOrganizationInput, type NoteInput, PROFILE_MAIN_XSD, PROFILE_SCHEMA_DIRS, PROFILE_URNS, type PaymentInput, Profile, type TaxRegistrationInput, type TradePartyInput, UnitCode, type ValidationError, type ValidationResult, type VatBreakdownInput, VatCategoryCode, type XRechnungOptions, type XRechnungResult, type XsdValidateOptions, type XsdValidationError, type XsdValidationResult, buildXml, embedFacturX, escapeXml, extractXml, getFlavorConfig, resolveBusinessProcessUrn, resolveTypeCode, toXRechnung, validateFlavorProfile, validateInput, validateXsd };
|
|
1438
|
+
export { type AddressInput, type AllowanceChargeInput, type BillingPeriodInput, type ContactInput, type DeliveryInput, type DocumentReferenceInput, DocumentTypeCode, type EmbedOptions, type EmbedResult, type ExtractOptions, type ExtractResult, FLAVOR_CONFIGS, FLAVOR_DEFAULT_TYPE_CODES, type FacturXInvoiceInput, Flavor, type FlavorConfig, type InvoiceDocumentInput, type InvoiceLineInput, type InvoiceTotalsInput, type LegalOrganizationInput, type NoteInput, PROFILE_MAIN_XSD, PROFILE_SCHEMA_DIRS, PROFILE_URNS, type PaymentInput, Profile, type TaxRegistrationInput, type TradePartyInput, UnitCode, type ValidationError, type ValidationResult, type VatBreakdownInput, VatCategoryCode, XRECHNUNG_PROFILE_URN, type XRechnungOptions, type XRechnungResult, type XsdValidateOptions, type XsdValidationError, type XsdValidationResult, buildXml, embedFacturX, escapeXml, extractXml, getFlavorConfig, resolveBusinessProcessUrn, resolveTypeCode, toXRechnung, validateFlavorProfile, validateInput, validateXsd };
|
package/dist/index.d.ts
CHANGED
|
@@ -480,10 +480,22 @@ interface TradePartyInput {
|
|
|
480
480
|
*/
|
|
481
481
|
contact?: ContactInput;
|
|
482
482
|
/**
|
|
483
|
-
* Electronic address (e.g.
|
|
483
|
+
* Electronic address (e.g. PEPPOL participant ID, email).
|
|
484
|
+
*
|
|
485
|
+
* `value` is the address itself (e.g. `"info@stack-forge.eu"`,
|
|
486
|
+
* `"0088:1234567890123"`).
|
|
487
|
+
* `schemeID` is the EAS code list value (e.g. `"EM"` for email,
|
|
488
|
+
* `"9930"` for German VAT, `"0088"` for GLN). Required by BR-62 /
|
|
489
|
+
* FX-SCH-A-000158 — a URIID without `@schemeID` is rejected by every
|
|
490
|
+
* conforming validator (Mustang, KoSIT, veraPDF).
|
|
491
|
+
*
|
|
484
492
|
* @see EN 16931 BT-34 / BT-49
|
|
493
|
+
* @see https://docs.peppol.eu/poacc/billing/3.0/codelist/eas/
|
|
485
494
|
*/
|
|
486
|
-
electronicAddress?:
|
|
495
|
+
electronicAddress?: {
|
|
496
|
+
value: string;
|
|
497
|
+
schemeID: string;
|
|
498
|
+
};
|
|
487
499
|
/**
|
|
488
500
|
* Tax registrations (VAT ID, fiscal number).
|
|
489
501
|
* @see EN 16931 BT-31 / BT-48
|
|
@@ -970,8 +982,20 @@ declare enum Flavor {
|
|
|
970
982
|
XRECHNUNG = "xrechnung",
|
|
971
983
|
CHRONO_PRO = "chrono-pro"
|
|
972
984
|
}
|
|
973
|
-
/**
|
|
985
|
+
/**
|
|
986
|
+
* Profile URNs (GuidelineSpecifiedDocumentContextParameter / BT-24).
|
|
987
|
+
*
|
|
988
|
+
* BASIC, EN16931, and EXTENDED use the EN 16931 conformance prefix because
|
|
989
|
+
* those profiles are CIUS / extension claims against the European norm. The
|
|
990
|
+
* exact strings are the only values accepted by the schematron shipped under
|
|
991
|
+
* `schema/{basic,en16931,extended}/` (the codedb cl id="1" enumeration).
|
|
992
|
+
*/
|
|
974
993
|
declare const PROFILE_URNS: Record<Profile, string>;
|
|
994
|
+
/**
|
|
995
|
+
* XRechnung CIUS URN (BR-DE-21). Used as the profile URN when emitting
|
|
996
|
+
* XRechnung instead of pure Factur-X / ZUGFeRD.
|
|
997
|
+
*/
|
|
998
|
+
declare const XRECHNUNG_PROFILE_URN = "urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0";
|
|
975
999
|
|
|
976
1000
|
/**
|
|
977
1001
|
* Default document type code per flavor.
|
|
@@ -1099,11 +1123,13 @@ interface ValidationResult {
|
|
|
1099
1123
|
* Validates that the input contains all required fields for the given profile.
|
|
1100
1124
|
* Requirements are cumulative: each profile includes all requirements of lower levels.
|
|
1101
1125
|
*
|
|
1102
|
-
* @param input
|
|
1126
|
+
* @param input - Invoice input to validate
|
|
1103
1127
|
* @param profile - Target profile (MINIMUM, BASIC_WL, BASIC, EN16931, or EXTENDED)
|
|
1128
|
+
* @param flavor - Optional flavor; when {@link Flavor.XRECHNUNG}, additional
|
|
1129
|
+
* BR-DE / PEPPOL-EN16931 rules are applied on top of the profile rules.
|
|
1104
1130
|
* @returns Validation result with valid flag and any errors
|
|
1105
1131
|
*/
|
|
1106
|
-
declare function validateInput(input: FacturXInvoiceInput, profile: Profile): ValidationResult;
|
|
1132
|
+
declare function validateInput(input: FacturXInvoiceInput, profile: Profile, flavor?: Flavor): ValidationResult;
|
|
1107
1133
|
|
|
1108
1134
|
/**
|
|
1109
1135
|
* XSD Schema Validation
|
|
@@ -1203,6 +1229,36 @@ interface EmbedOptions {
|
|
|
1203
1229
|
validateXsd?: boolean;
|
|
1204
1230
|
/** Add PDF/A-3 metadata (default: `true`). */
|
|
1205
1231
|
addPdfA3Metadata?: boolean;
|
|
1232
|
+
/**
|
|
1233
|
+
* RGB ICC profile bytes used to inject the PDF/A required `/OutputIntents`
|
|
1234
|
+
* entry when the input PDF does not already have one.
|
|
1235
|
+
*
|
|
1236
|
+
* PDF/A-3 forbids DeviceRGB without an OutputIntent that references an
|
|
1237
|
+
* RGB destination profile (ISO 19005-3 §6.2.4.3). pdf-lib does not produce
|
|
1238
|
+
* PDF/A by default, so this library will add the entry on save when this
|
|
1239
|
+
* profile is supplied.
|
|
1240
|
+
*
|
|
1241
|
+
* Drop in any sRGB ICC v2 profile compatible with your licensing needs
|
|
1242
|
+
* (e.g. the ICC.org public profile, or one bundled with Argyll CMS / PDFBox).
|
|
1243
|
+
* When omitted and {@link addPdfA3Metadata} is `true`, the library logs a
|
|
1244
|
+
* warning and the resulting PDF will not be PDF/A-3 conformant.
|
|
1245
|
+
*/
|
|
1246
|
+
rgbIccProfile?: Uint8Array | Buffer;
|
|
1247
|
+
/**
|
|
1248
|
+
* Output intent identifier written into the OutputIntent dictionary.
|
|
1249
|
+
* Defaults to `"sRGB IEC61966-2.1"` which matches the standard sRGB profile.
|
|
1250
|
+
*/
|
|
1251
|
+
outputIntentIdentifier?: string;
|
|
1252
|
+
/**
|
|
1253
|
+
* How to react when the input PDF references fonts that are not embedded.
|
|
1254
|
+
* PDF/A-3 requires every font to be embedded (ISO 19005-3 §6.2.11.4.1) —
|
|
1255
|
+
* this library cannot fix that post-hoc, so it surfaces the gap.
|
|
1256
|
+
*
|
|
1257
|
+
* - `"warn"` (default): log a warning to `console.warn` and continue.
|
|
1258
|
+
* - `"throw"`: reject the PDF with an error.
|
|
1259
|
+
* - `"ignore"`: skip the check entirely.
|
|
1260
|
+
*/
|
|
1261
|
+
unembeddedFonts?: "warn" | "throw" | "ignore";
|
|
1206
1262
|
/**
|
|
1207
1263
|
* Override the AF relationship for the embedded XML attachment.
|
|
1208
1264
|
*
|
|
@@ -1379,4 +1435,4 @@ interface XRechnungResult {
|
|
|
1379
1435
|
*/
|
|
1380
1436
|
declare function toXRechnung(input: FacturXInvoiceInput, options?: XRechnungOptions): XRechnungResult;
|
|
1381
1437
|
|
|
1382
|
-
export { type AddressInput, type AllowanceChargeInput, type BillingPeriodInput, type ContactInput, type DeliveryInput, type DocumentReferenceInput, DocumentTypeCode, type EmbedOptions, type EmbedResult, type ExtractOptions, type ExtractResult, FLAVOR_CONFIGS, FLAVOR_DEFAULT_TYPE_CODES, type FacturXInvoiceInput, Flavor, type FlavorConfig, type InvoiceDocumentInput, type InvoiceLineInput, type InvoiceTotalsInput, type LegalOrganizationInput, type NoteInput, PROFILE_MAIN_XSD, PROFILE_SCHEMA_DIRS, PROFILE_URNS, type PaymentInput, Profile, type TaxRegistrationInput, type TradePartyInput, UnitCode, type ValidationError, type ValidationResult, type VatBreakdownInput, VatCategoryCode, type XRechnungOptions, type XRechnungResult, type XsdValidateOptions, type XsdValidationError, type XsdValidationResult, buildXml, embedFacturX, escapeXml, extractXml, getFlavorConfig, resolveBusinessProcessUrn, resolveTypeCode, toXRechnung, validateFlavorProfile, validateInput, validateXsd };
|
|
1438
|
+
export { type AddressInput, type AllowanceChargeInput, type BillingPeriodInput, type ContactInput, type DeliveryInput, type DocumentReferenceInput, DocumentTypeCode, type EmbedOptions, type EmbedResult, type ExtractOptions, type ExtractResult, FLAVOR_CONFIGS, FLAVOR_DEFAULT_TYPE_CODES, type FacturXInvoiceInput, Flavor, type FlavorConfig, type InvoiceDocumentInput, type InvoiceLineInput, type InvoiceTotalsInput, type LegalOrganizationInput, type NoteInput, PROFILE_MAIN_XSD, PROFILE_SCHEMA_DIRS, PROFILE_URNS, type PaymentInput, Profile, type TaxRegistrationInput, type TradePartyInput, UnitCode, type ValidationError, type ValidationResult, type VatBreakdownInput, VatCategoryCode, XRECHNUNG_PROFILE_URN, type XRechnungOptions, type XRechnungResult, type XsdValidateOptions, type XsdValidationError, type XsdValidationResult, buildXml, embedFacturX, escapeXml, extractXml, getFlavorConfig, resolveBusinessProcessUrn, resolveTypeCode, toXRechnung, validateFlavorProfile, validateInput, validateXsd };
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
|
-
import
|
|
4
|
+
import console from 'console';
|
|
5
|
+
import { AFRelationship, PDFDocument, PDFName, PDFArray, PDFDict, PDFStream, PDFRawStream, decodePDFRawStream, PDFString, PDFRef, PDFHexString } from 'pdf-lib';
|
|
5
6
|
|
|
6
7
|
// src/types/input.ts
|
|
7
8
|
var DocumentTypeCode = /* @__PURE__ */ ((DocumentTypeCode2) => {
|
|
@@ -148,10 +149,11 @@ var Flavor = /* @__PURE__ */ ((Flavor2) => {
|
|
|
148
149
|
var PROFILE_URNS = {
|
|
149
150
|
MINIMUM: "urn:factur-x.eu:1p0:minimum",
|
|
150
151
|
BASIC_WL: "urn:factur-x.eu:1p0:basicwl",
|
|
151
|
-
BASIC: "urn:factur-x.eu:1p0:basic",
|
|
152
|
-
EN16931: "urn:
|
|
153
|
-
EXTENDED: "urn:factur-x.eu:1p0:extended"
|
|
152
|
+
BASIC: "urn:cen.eu:en16931:2017#compliant#urn:factur-x.eu:1p0:basic",
|
|
153
|
+
EN16931: "urn:cen.eu:en16931:2017",
|
|
154
|
+
EXTENDED: "urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended"
|
|
154
155
|
};
|
|
156
|
+
var XRECHNUNG_PROFILE_URN = "urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0";
|
|
155
157
|
var FLAVOR_DEFAULT_TYPE_CODES = {
|
|
156
158
|
["factur-x" /* FACTUR_X */]: "380" /* COMMERCIAL_INVOICE */,
|
|
157
159
|
["zugferd" /* ZUGFERD */]: "380" /* COMMERCIAL_INVOICE */,
|
|
@@ -321,7 +323,12 @@ function buildParty(p, profile) {
|
|
|
321
323
|
if (p.contact && atLeast(profile, "EN16931" /* EN16931 */)) x += buildContact(p.contact);
|
|
322
324
|
if (p.address && atLeast(profile, "BASIC_WL" /* BASIC_WL */)) x += buildAddress(p.address);
|
|
323
325
|
if (p.electronicAddress)
|
|
324
|
-
x += tag(
|
|
326
|
+
x += tag(
|
|
327
|
+
"ram:URIUniversalCommunication",
|
|
328
|
+
tag("ram:URIID", escapeXml(p.electronicAddress.value), {
|
|
329
|
+
schemeID: p.electronicAddress.schemeID
|
|
330
|
+
})
|
|
331
|
+
);
|
|
325
332
|
if (p.taxRegistrations) {
|
|
326
333
|
for (const tr of p.taxRegistrations) {
|
|
327
334
|
x += tag(
|
|
@@ -367,7 +374,7 @@ function buildLineItem(line, profile) {
|
|
|
367
374
|
tx += tag("ram:RateApplicablePercent", line.vatRatePercent.toString());
|
|
368
375
|
settle += tag("ram:ApplicableTradeTax", tx);
|
|
369
376
|
}
|
|
370
|
-
const lineTotal = line.lineTotal ?? line.quantity * line.unitPrice;
|
|
377
|
+
const lineTotal = line.lineTotal ?? Math.round(line.quantity * line.unitPrice * 100) / 100;
|
|
371
378
|
settle += tag(
|
|
372
379
|
"ram:SpecifiedTradeSettlementLineMonetarySummation",
|
|
373
380
|
tag("ram:LineTotalAmount", fmtAmt(lineTotal))
|
|
@@ -378,7 +385,7 @@ function buildLineItem(line, profile) {
|
|
|
378
385
|
);
|
|
379
386
|
}
|
|
380
387
|
function buildAllowanceCharge(ac) {
|
|
381
|
-
let x = tag("ram:ChargeIndicator", tag("
|
|
388
|
+
let x = tag("ram:ChargeIndicator", tag("udt:Indicator", ac.isCharge ? "true" : "false"));
|
|
382
389
|
if (ac.percent !== void 0) x += tag("ram:CalculationPercent", ac.percent.toString());
|
|
383
390
|
if (ac.baseAmount !== void 0) x += tag("ram:BasisAmount", fmtAmt(ac.baseAmount));
|
|
384
391
|
x += tag("ram:ActualAmount", fmtAmt(ac.amount));
|
|
@@ -450,9 +457,10 @@ function buildXml(input, profile, flavor) {
|
|
|
450
457
|
"ram:BusinessProcessSpecifiedDocumentContextParameter",
|
|
451
458
|
tag("ram:ID", escapeXml(doc.businessProcessId))
|
|
452
459
|
);
|
|
460
|
+
const profileUrn = fl === "xrechnung" /* XRECHNUNG */ ? XRECHNUNG_PROFILE_URN : PROFILE_URNS[profile];
|
|
453
461
|
ctx += tag(
|
|
454
462
|
"ram:GuidelineSpecifiedDocumentContextParameter",
|
|
455
|
-
tag("ram:ID",
|
|
463
|
+
tag("ram:ID", profileUrn)
|
|
456
464
|
);
|
|
457
465
|
let edoc = tag("ram:ID", escapeXml(doc.id));
|
|
458
466
|
if (doc.name && atLeast(profile, "EXTENDED" /* EXTENDED */)) edoc += tag("ram:Name", escapeXml(doc.name));
|
|
@@ -505,6 +513,7 @@ function buildXml(input, profile, flavor) {
|
|
|
505
513
|
}
|
|
506
514
|
tx += tag("ram:ApplicableHeaderTradeAgreement", agree);
|
|
507
515
|
let del = "";
|
|
516
|
+
let hasActualDelivery = false;
|
|
508
517
|
if (input.delivery && atLeast(profile, "BASIC_WL" /* BASIC_WL */)) {
|
|
509
518
|
if (input.delivery.partyName || input.delivery.location) {
|
|
510
519
|
let sp = "";
|
|
@@ -512,11 +521,13 @@ function buildXml(input, profile, flavor) {
|
|
|
512
521
|
if (input.delivery.location) sp += buildAddress(input.delivery.location);
|
|
513
522
|
del += tag("ram:ShipToTradeParty", sp);
|
|
514
523
|
}
|
|
515
|
-
if (input.delivery.date)
|
|
524
|
+
if (input.delivery.date) {
|
|
516
525
|
del += tag(
|
|
517
526
|
"ram:ActualDeliverySupplyChainEvent",
|
|
518
527
|
dateEl("ram:OccurrenceDateTime", input.delivery.date)
|
|
519
528
|
);
|
|
529
|
+
hasActualDelivery = true;
|
|
530
|
+
}
|
|
520
531
|
if (input.delivery.despatchAdviceReference)
|
|
521
532
|
del += tag(
|
|
522
533
|
"ram:DespatchAdviceReferencedDocument",
|
|
@@ -532,6 +543,12 @@ function buildXml(input, profile, flavor) {
|
|
|
532
543
|
}
|
|
533
544
|
}
|
|
534
545
|
}
|
|
546
|
+
if (atLeast(profile, "BASIC" /* BASIC */) && !hasActualDelivery && typeCode !== "386" /* PREPAYMENT_INVOICE */) {
|
|
547
|
+
del += tag(
|
|
548
|
+
"ram:ActualDeliverySupplyChainEvent",
|
|
549
|
+
dateEl("ram:OccurrenceDateTime", doc.issueDate)
|
|
550
|
+
);
|
|
551
|
+
}
|
|
535
552
|
tx += tag("ram:ApplicableHeaderTradeDelivery", del);
|
|
536
553
|
let sett = "";
|
|
537
554
|
if (payment?.creditorReference && atLeast(profile, "BASIC_WL" /* BASIC_WL */))
|
|
@@ -609,7 +626,7 @@ var PROFILE_LEVEL2 = {
|
|
|
609
626
|
function addError(errors, field, message, profile) {
|
|
610
627
|
errors.push({ field, message, profile });
|
|
611
628
|
}
|
|
612
|
-
function validateInput(input, profile) {
|
|
629
|
+
function validateInput(input, profile, flavor) {
|
|
613
630
|
const errors = [];
|
|
614
631
|
const level = PROFILE_LEVEL2[profile];
|
|
615
632
|
const req = (minProfile) => level >= PROFILE_LEVEL2[minProfile];
|
|
@@ -796,11 +813,193 @@ function validateInput(input, profile) {
|
|
|
796
813
|
}
|
|
797
814
|
});
|
|
798
815
|
}
|
|
816
|
+
if (req("BASIC_WL" /* BASIC_WL */)) {
|
|
817
|
+
for (const role of ["seller", "buyer"]) {
|
|
818
|
+
const ea = input[role]?.electronicAddress;
|
|
819
|
+
if (ea && !ea.schemeID) {
|
|
820
|
+
addError(
|
|
821
|
+
errors,
|
|
822
|
+
`${role}.electronicAddress.schemeID`,
|
|
823
|
+
`${role.charAt(0).toUpperCase() + role.slice(1)} electronic address must include schemeID (BR-62 / FX-SCH-A-000158).`,
|
|
824
|
+
"BASIC_WL" /* BASIC_WL */
|
|
825
|
+
);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
if (req("BASIC" /* BASIC */) && input.lines) {
|
|
830
|
+
input.lines.forEach((line, i) => {
|
|
831
|
+
if (typeof line.unitPrice === "number" && line.unitPrice < 0) {
|
|
832
|
+
addError(
|
|
833
|
+
errors,
|
|
834
|
+
`lines[${i}].unitPrice`,
|
|
835
|
+
"Line unit price must not be negative (BR-27). Use allowancesCharges for discounts.",
|
|
836
|
+
"BASIC" /* BASIC */
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
if (req("BASIC_WL" /* BASIC_WL */) && input.totals) {
|
|
842
|
+
const t = input.totals;
|
|
843
|
+
if (t.taxBasisTotal != null && t.taxTotal != null && t.grandTotal != null) {
|
|
844
|
+
const expected = round2(t.taxBasisTotal + t.taxTotal);
|
|
845
|
+
if (Math.abs(round2(t.grandTotal) - expected) > 0.01) {
|
|
846
|
+
addError(
|
|
847
|
+
errors,
|
|
848
|
+
"totals.grandTotal",
|
|
849
|
+
`Grand total ${t.grandTotal} does not equal taxBasisTotal + taxTotal (${expected}) (BR-CO-15).`,
|
|
850
|
+
"BASIC_WL" /* BASIC_WL */
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
if (req("BASIC_WL" /* BASIC_WL */) && input.totals) {
|
|
856
|
+
const t = input.totals;
|
|
857
|
+
if (t.lineTotal != null && t.taxBasisTotal != null) {
|
|
858
|
+
const allowance = t.allowanceTotal ?? 0;
|
|
859
|
+
const charge = t.chargeTotal ?? 0;
|
|
860
|
+
const expected = round2(t.lineTotal - allowance + charge);
|
|
861
|
+
if (Math.abs(round2(t.taxBasisTotal) - expected) > 0.01) {
|
|
862
|
+
addError(
|
|
863
|
+
errors,
|
|
864
|
+
"totals.taxBasisTotal",
|
|
865
|
+
`taxBasisTotal ${t.taxBasisTotal} does not equal lineTotal - allowanceTotal + chargeTotal (${expected}) (BR-CO-13).`,
|
|
866
|
+
"BASIC_WL" /* BASIC_WL */
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
if (req("BASIC" /* BASIC */) && input.totals && input.lines) {
|
|
872
|
+
const sumLineNets = input.lines.reduce((sum, l) => {
|
|
873
|
+
const lineNet = l.lineTotal ?? (typeof l.quantity === "number" && typeof l.unitPrice === "number" ? l.quantity * l.unitPrice : 0);
|
|
874
|
+
return sum + lineNet;
|
|
875
|
+
}, 0);
|
|
876
|
+
if (input.totals.lineTotal != null) {
|
|
877
|
+
if (Math.abs(round2(input.totals.lineTotal) - round2(sumLineNets)) > 0.01) {
|
|
878
|
+
addError(
|
|
879
|
+
errors,
|
|
880
|
+
"totals.lineTotal",
|
|
881
|
+
`lineTotal ${input.totals.lineTotal} does not equal \u03A3 line nets (${round2(sumLineNets)}) (BR-CO-10).`,
|
|
882
|
+
"BASIC" /* BASIC */
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
if (req("BASIC_WL" /* BASIC_WL */) && input.totals?.taxTotal != null && input.vatBreakdown) {
|
|
888
|
+
const sumVat = input.vatBreakdown.reduce((s, v) => s + (v.taxAmount ?? 0), 0);
|
|
889
|
+
if (Math.abs(round2(input.totals.taxTotal) - round2(sumVat)) > 0.01) {
|
|
890
|
+
addError(
|
|
891
|
+
errors,
|
|
892
|
+
"totals.taxTotal",
|
|
893
|
+
`taxTotal ${input.totals.taxTotal} does not equal \u03A3 vatBreakdown.taxAmount (${round2(sumVat)}) (BR-CO-14).`,
|
|
894
|
+
"BASIC_WL" /* BASIC_WL */
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
if (req("BASIC_WL" /* BASIC_WL */) && input.vatBreakdown) {
|
|
899
|
+
input.vatBreakdown.forEach((vb, i) => {
|
|
900
|
+
if (vb.taxableAmount != null && vb.taxAmount != null && vb.ratePercent != null) {
|
|
901
|
+
const expected = round2(vb.taxableAmount * vb.ratePercent / 100);
|
|
902
|
+
if (Math.abs(round2(vb.taxAmount) - expected) > 0.01) {
|
|
903
|
+
addError(
|
|
904
|
+
errors,
|
|
905
|
+
`vatBreakdown[${i}].taxAmount`,
|
|
906
|
+
`taxAmount ${vb.taxAmount} does not equal round(taxableAmount \xD7 ratePercent / 100) = ${expected} (BR-CO-17).`,
|
|
907
|
+
"BASIC_WL" /* BASIC_WL */
|
|
908
|
+
);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
if (req("BASIC" /* BASIC */) && input.document) {
|
|
914
|
+
const isPrepayment = input.document.typeCode === "386";
|
|
915
|
+
if (!isPrepayment) {
|
|
916
|
+
const hasDeliveryDate = !!input.delivery?.date;
|
|
917
|
+
const hasBillingPeriod = !!input.billingPeriod;
|
|
918
|
+
if (!hasDeliveryDate && !hasBillingPeriod) {
|
|
919
|
+
addError(
|
|
920
|
+
errors,
|
|
921
|
+
"delivery.date",
|
|
922
|
+
"Provide delivery.date or billingPeriod (BR-FX-EN-04).",
|
|
923
|
+
"BASIC" /* BASIC */
|
|
924
|
+
);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
if (flavor === "xrechnung" /* XRECHNUNG */) {
|
|
929
|
+
if (!input.document?.buyerReference) {
|
|
930
|
+
addError(
|
|
931
|
+
errors,
|
|
932
|
+
"document.buyerReference",
|
|
933
|
+
"BuyerReference (BT-10) is required for XRechnung (BR-DE-15).",
|
|
934
|
+
"EN16931" /* EN16931 */
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
if (!input.document?.businessProcessId) {
|
|
938
|
+
addError(
|
|
939
|
+
errors,
|
|
940
|
+
"document.businessProcessId",
|
|
941
|
+
"Business process (BT-23) is required for XRechnung (PEPPOL-EN16931-R001).",
|
|
942
|
+
"EN16931" /* EN16931 */
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
if (!input.seller?.contact || !input.seller.contact.name && !input.seller.contact.email && !input.seller.contact.phone) {
|
|
946
|
+
addError(
|
|
947
|
+
errors,
|
|
948
|
+
"seller.contact",
|
|
949
|
+
"Seller contact (BG-6) with name, email or phone is required for XRechnung (BR-DE-2).",
|
|
950
|
+
"EN16931" /* EN16931 */
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
if (!input.seller?.electronicAddress?.value || !input.seller.electronicAddress.schemeID) {
|
|
954
|
+
addError(
|
|
955
|
+
errors,
|
|
956
|
+
"seller.electronicAddress",
|
|
957
|
+
"Seller electronic address (BT-34) with non-empty schemeID is required for XRechnung (PEPPOL-EN16931-R020 / BR-62).",
|
|
958
|
+
"EN16931" /* EN16931 */
|
|
959
|
+
);
|
|
960
|
+
}
|
|
961
|
+
if (!input.buyer?.electronicAddress?.value || !input.buyer.electronicAddress.schemeID) {
|
|
962
|
+
addError(
|
|
963
|
+
errors,
|
|
964
|
+
"buyer.electronicAddress",
|
|
965
|
+
"Buyer electronic address (BT-49) with non-empty schemeID is required for XRechnung (PEPPOL-EN16931-R010 / BR-62).",
|
|
966
|
+
"EN16931" /* EN16931 */
|
|
967
|
+
);
|
|
968
|
+
}
|
|
969
|
+
if (input.payment?.meansCode === "58" && input.payment.iban) {
|
|
970
|
+
if (!isValidIban(input.payment.iban)) {
|
|
971
|
+
addError(
|
|
972
|
+
errors,
|
|
973
|
+
"payment.iban",
|
|
974
|
+
"IBAN is not syntactically valid (BR-DE-19).",
|
|
975
|
+
"EN16931" /* EN16931 */
|
|
976
|
+
);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
799
980
|
return {
|
|
800
981
|
valid: errors.length === 0,
|
|
801
982
|
errors
|
|
802
983
|
};
|
|
803
984
|
}
|
|
985
|
+
function round2(n) {
|
|
986
|
+
return Math.round(n * 100) / 100;
|
|
987
|
+
}
|
|
988
|
+
function isValidIban(iban) {
|
|
989
|
+
const normalized = iban.replace(/\s+/g, "").toUpperCase();
|
|
990
|
+
if (!/^[A-Z]{2}\d{2}[A-Z0-9]{1,30}$/.test(normalized)) return false;
|
|
991
|
+
const rearranged = normalized.slice(4) + normalized.slice(0, 4);
|
|
992
|
+
let expanded = "";
|
|
993
|
+
for (const ch of rearranged) {
|
|
994
|
+
expanded += /[A-Z]/.test(ch) ? String(ch.charCodeAt(0) - 55) : ch;
|
|
995
|
+
}
|
|
996
|
+
let remainder = 0;
|
|
997
|
+
for (let i = 0; i < expanded.length; i += 7) {
|
|
998
|
+
const chunk = remainder.toString() + expanded.slice(i, i + 7);
|
|
999
|
+
remainder = parseInt(chunk, 10) % 97;
|
|
1000
|
+
}
|
|
1001
|
+
return remainder === 1;
|
|
1002
|
+
}
|
|
804
1003
|
function findSchemaDir(startDir) {
|
|
805
1004
|
let dir = startDir;
|
|
806
1005
|
for (let i = 0; i < 6; i++) {
|
|
@@ -882,6 +1081,71 @@ var AF_RELATIONSHIP_MAP = {
|
|
|
882
1081
|
Data: AFRelationship.Data,
|
|
883
1082
|
Source: AFRelationship.Source
|
|
884
1083
|
};
|
|
1084
|
+
function ensureOutputIntent(pdfDoc, iccBytes, identifier) {
|
|
1085
|
+
const existing = pdfDoc.catalog.lookup(PDFName.of("OutputIntents"));
|
|
1086
|
+
if (existing instanceof PDFArray && existing.size() > 0) return;
|
|
1087
|
+
const iccStream = pdfDoc.context.flateStream(iccBytes, { N: 3 });
|
|
1088
|
+
const iccRef = pdfDoc.context.register(iccStream);
|
|
1089
|
+
const intent = pdfDoc.context.obj({
|
|
1090
|
+
Type: "OutputIntent",
|
|
1091
|
+
S: "GTS_PDFA1",
|
|
1092
|
+
OutputConditionIdentifier: PDFString.of(identifier),
|
|
1093
|
+
RegistryName: PDFString.of("http://www.color.org"),
|
|
1094
|
+
Info: PDFString.of(identifier),
|
|
1095
|
+
DestOutputProfile: iccRef
|
|
1096
|
+
});
|
|
1097
|
+
const intents = PDFArray.withContext(pdfDoc.context);
|
|
1098
|
+
intents.push(intent);
|
|
1099
|
+
pdfDoc.catalog.set(PDFName.of("OutputIntents"), intents);
|
|
1100
|
+
}
|
|
1101
|
+
function findUnembeddedFonts(pdfDoc) {
|
|
1102
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1103
|
+
const unembedded = /* @__PURE__ */ new Set();
|
|
1104
|
+
const FONT_FILE_KEYS = [
|
|
1105
|
+
PDFName.of("FontFile"),
|
|
1106
|
+
PDFName.of("FontFile2"),
|
|
1107
|
+
PDFName.of("FontFile3")
|
|
1108
|
+
];
|
|
1109
|
+
const inspectFontDict = (font) => {
|
|
1110
|
+
const subtype = font.lookup(PDFName.of("Subtype"));
|
|
1111
|
+
const baseFont = font.lookup(PDFName.of("BaseFont"));
|
|
1112
|
+
const baseFontName = baseFont instanceof PDFName ? baseFont.asString() : "<unknown>";
|
|
1113
|
+
const subtypeName = subtype instanceof PDFName ? subtype.asString() : "";
|
|
1114
|
+
if (subtypeName === "/Type0") {
|
|
1115
|
+
const desc = font.lookup(PDFName.of("DescendantFonts"));
|
|
1116
|
+
if (desc instanceof PDFArray) {
|
|
1117
|
+
for (let i = 0; i < desc.size(); i += 1) {
|
|
1118
|
+
const child = desc.lookup(i);
|
|
1119
|
+
if (child instanceof PDFDict) inspectFontDict(child);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
const descriptor = font.lookup(PDFName.of("FontDescriptor"));
|
|
1125
|
+
if (!(descriptor instanceof PDFDict)) {
|
|
1126
|
+
unembedded.add(baseFontName);
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
const hasEmbedded = FONT_FILE_KEYS.some((k) => descriptor.has(k));
|
|
1130
|
+
if (!hasEmbedded) unembedded.add(baseFontName);
|
|
1131
|
+
};
|
|
1132
|
+
for (const page of pdfDoc.getPages()) {
|
|
1133
|
+
const resources = page.node.Resources();
|
|
1134
|
+
if (!resources) continue;
|
|
1135
|
+
const fonts = resources.lookup(PDFName.of("Font"));
|
|
1136
|
+
if (!(fonts instanceof PDFDict)) continue;
|
|
1137
|
+
for (const [, value] of fonts.entries()) {
|
|
1138
|
+
const ref = value instanceof PDFRef ? value : void 0;
|
|
1139
|
+
if (ref) {
|
|
1140
|
+
if (seen.has(ref)) continue;
|
|
1141
|
+
seen.add(ref);
|
|
1142
|
+
}
|
|
1143
|
+
const font = ref ? pdfDoc.context.lookup(ref) : value;
|
|
1144
|
+
if (font instanceof PDFDict) inspectFontDict(font);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
return Array.from(unembedded);
|
|
1148
|
+
}
|
|
885
1149
|
function resolveAfRelationship(profile, override) {
|
|
886
1150
|
if (override) return AF_RELATIONSHIP_MAP[override];
|
|
887
1151
|
switch (profile) {
|
|
@@ -998,6 +1262,9 @@ async function embedFacturX(options) {
|
|
|
998
1262
|
validateBeforeEmbed = true,
|
|
999
1263
|
validateXsd: runXsdValidation = false,
|
|
1000
1264
|
addPdfA3Metadata = true,
|
|
1265
|
+
rgbIccProfile,
|
|
1266
|
+
outputIntentIdentifier = "sRGB IEC61966-2.1",
|
|
1267
|
+
unembeddedFonts = "warn",
|
|
1001
1268
|
meta
|
|
1002
1269
|
} = options;
|
|
1003
1270
|
if (!options.xml && !options.input) {
|
|
@@ -1011,7 +1278,7 @@ async function embedFacturX(options) {
|
|
|
1011
1278
|
if (!xml) {
|
|
1012
1279
|
const input = options.input;
|
|
1013
1280
|
if (validateBeforeEmbed) {
|
|
1014
|
-
validation = validateInput(input, profile);
|
|
1281
|
+
validation = validateInput(input, profile, flavor);
|
|
1015
1282
|
if (!validation.valid) {
|
|
1016
1283
|
throw new Error(
|
|
1017
1284
|
`Input validation failed for profile "${profile}":
|
|
@@ -1052,6 +1319,22 @@ async function embedFacturX(options) {
|
|
|
1052
1319
|
pdfDoc.setCreationDate(now);
|
|
1053
1320
|
pdfDoc.setModificationDate(now);
|
|
1054
1321
|
if (addPdfA3Metadata) {
|
|
1322
|
+
if (rgbIccProfile) {
|
|
1323
|
+
const iccBytes = rgbIccProfile instanceof Uint8Array ? rgbIccProfile : new Uint8Array(rgbIccProfile);
|
|
1324
|
+
ensureOutputIntent(pdfDoc, iccBytes, outputIntentIdentifier);
|
|
1325
|
+
} else if (!(pdfDoc.catalog.lookup(PDFName.of("OutputIntents")) instanceof PDFArray)) {
|
|
1326
|
+
console.warn(
|
|
1327
|
+
"[factur-x] addPdfA3Metadata is enabled but no rgbIccProfile was supplied and the input PDF has no /OutputIntents. The resulting PDF will not be PDF/A-3 conformant."
|
|
1328
|
+
);
|
|
1329
|
+
}
|
|
1330
|
+
if (unembeddedFonts !== "ignore") {
|
|
1331
|
+
const missing = findUnembeddedFonts(pdfDoc);
|
|
1332
|
+
if (missing.length > 0) {
|
|
1333
|
+
const message = `[factur-x] PDF/A-3 requires every font program to be embedded; the following fonts are not embedded: ${missing.join(", ")}. Configure your PDF generator to embed all fonts.`;
|
|
1334
|
+
if (unembeddedFonts === "throw") throw new Error(message);
|
|
1335
|
+
console.warn(message);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1055
1338
|
const isoDate = now.toISOString();
|
|
1056
1339
|
const xmpXml = buildXmpMetadata({
|
|
1057
1340
|
title,
|
|
@@ -1098,18 +1381,22 @@ function resolve(doc, obj) {
|
|
|
1098
1381
|
return obj;
|
|
1099
1382
|
}
|
|
1100
1383
|
function detectProfile(xml) {
|
|
1101
|
-
const profileMap =
|
|
1102
|
-
"urn:factur-x.eu:1p0:
|
|
1103
|
-
"urn:
|
|
1104
|
-
"urn:factur-x.eu:1p0:basic"
|
|
1105
|
-
"urn:
|
|
1106
|
-
"urn:
|
|
1107
|
-
"urn:cen.eu:en16931:2017"
|
|
1108
|
-
"urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:
|
|
1109
|
-
"urn:cen.eu:en16931:2017
|
|
1110
|
-
"urn:
|
|
1111
|
-
|
|
1112
|
-
|
|
1384
|
+
const profileMap = [
|
|
1385
|
+
["urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended", "EXTENDED" /* EXTENDED */],
|
|
1386
|
+
["urn:cen.eu:en16931:2017#conformant#urn:zugferd.de:2p0:extended", "EXTENDED" /* EXTENDED */],
|
|
1387
|
+
["urn:cen.eu:en16931:2017#compliant#urn:factur-x.eu:1p0:basic", "BASIC" /* BASIC */],
|
|
1388
|
+
["urn:cen.eu:en16931:2017#compliant#urn:zugferd.de:2p0:basic", "BASIC" /* BASIC */],
|
|
1389
|
+
["urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0", "EN16931" /* EN16931 */],
|
|
1390
|
+
["urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_2.3", "EN16931" /* EN16931 */],
|
|
1391
|
+
["urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_2.2", "EN16931" /* EN16931 */],
|
|
1392
|
+
["urn:cen.eu:en16931:2017", "EN16931" /* EN16931 */],
|
|
1393
|
+
["urn:factur-x.eu:1p0:minimum", "MINIMUM" /* MINIMUM */],
|
|
1394
|
+
["urn:factur-x.eu:1p0:basicwl", "BASIC_WL" /* BASIC_WL */],
|
|
1395
|
+
["urn:factur-x.eu:1p0:basic", "BASIC" /* BASIC */],
|
|
1396
|
+
["urn:factur-x.eu:1p0:en16931", "EN16931" /* EN16931 */],
|
|
1397
|
+
["urn:factur-x.eu:1p0:extended", "EXTENDED" /* EXTENDED */]
|
|
1398
|
+
];
|
|
1399
|
+
for (const [urn, profile] of profileMap) {
|
|
1113
1400
|
if (xml.includes(urn)) return profile;
|
|
1114
1401
|
}
|
|
1115
1402
|
return void 0;
|
|
@@ -1177,9 +1464,16 @@ function toXRechnung(input, options) {
|
|
|
1177
1464
|
const profile = options?.profile ?? "EN16931" /* EN16931 */;
|
|
1178
1465
|
const validate = options?.validate ?? true;
|
|
1179
1466
|
validateFlavorProfile("xrechnung" /* XRECHNUNG */, profile);
|
|
1467
|
+
const enrichedInput = {
|
|
1468
|
+
...input,
|
|
1469
|
+
document: {
|
|
1470
|
+
...input.document,
|
|
1471
|
+
businessProcessId: resolveBusinessProcessUrn(input, "xrechnung" /* XRECHNUNG */)
|
|
1472
|
+
}
|
|
1473
|
+
};
|
|
1180
1474
|
let validation;
|
|
1181
1475
|
if (validate) {
|
|
1182
|
-
validation = validateInput(
|
|
1476
|
+
validation = validateInput(enrichedInput, profile, "xrechnung" /* XRECHNUNG */);
|
|
1183
1477
|
if (!validation.valid) {
|
|
1184
1478
|
throw new Error(
|
|
1185
1479
|
`Input validation failed for XRechnung (profile "${profile}"):
|
|
@@ -1187,13 +1481,6 @@ function toXRechnung(input, options) {
|
|
|
1187
1481
|
);
|
|
1188
1482
|
}
|
|
1189
1483
|
}
|
|
1190
|
-
const enrichedInput = {
|
|
1191
|
-
...input,
|
|
1192
|
-
document: {
|
|
1193
|
-
...input.document,
|
|
1194
|
-
businessProcessId: resolveBusinessProcessUrn(input, "xrechnung" /* XRECHNUNG */)
|
|
1195
|
-
}
|
|
1196
|
-
};
|
|
1197
1484
|
const xml = buildXml(enrichedInput, profile, "xrechnung" /* XRECHNUNG */);
|
|
1198
1485
|
return {
|
|
1199
1486
|
xml,
|
|
@@ -1289,4 +1576,4 @@ function toXRechnung(input, options) {
|
|
|
1289
1576
|
* ```
|
|
1290
1577
|
*/
|
|
1291
1578
|
|
|
1292
|
-
export { DocumentTypeCode, FLAVOR_CONFIGS, FLAVOR_DEFAULT_TYPE_CODES, Flavor, PROFILE_MAIN_XSD, PROFILE_SCHEMA_DIRS, PROFILE_URNS, Profile, UnitCode, VatCategoryCode, buildXml, embedFacturX, escapeXml, extractXml, getFlavorConfig, resolveBusinessProcessUrn, resolveTypeCode, toXRechnung, validateFlavorProfile, validateInput, validateXsd };
|
|
1579
|
+
export { DocumentTypeCode, FLAVOR_CONFIGS, FLAVOR_DEFAULT_TYPE_CODES, Flavor, PROFILE_MAIN_XSD, PROFILE_SCHEMA_DIRS, PROFILE_URNS, Profile, UnitCode, VatCategoryCode, XRECHNUNG_PROFILE_URN, buildXml, embedFacturX, escapeXml, extractXml, getFlavorConfig, resolveBusinessProcessUrn, resolveTypeCode, toXRechnung, validateFlavorProfile, validateInput, validateXsd };
|