@fin.cx/einvoice 6.1.0 → 6.2.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/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/einvoice.d.ts +1 -0
- package/dist_ts/einvoice.js +23 -2
- package/dist_ts/formats/base/base.decoder.d.ts +10 -0
- package/dist_ts/formats/base/base.decoder.js +72 -1
- package/dist_ts/formats/cii/cii.decoder.d.ts +4 -0
- package/dist_ts/formats/cii/cii.decoder.js +14 -1
- package/dist_ts/formats/cii/cii.validator.js +4 -2
- package/dist_ts/formats/cii/facturx/facturx.decoder.js +6 -5
- package/dist_ts/formats/factories/decoder.factory.js +12 -2
- package/dist_ts/formats/factories/validator.factory.js +12 -3
- package/dist_ts/formats/pdf/extractors/associated.extractor.js +8 -4
- package/dist_ts/formats/pdf/extractors/standard.extractor.js +4 -3
- package/dist_ts/formats/ubl/ubl.decoder.d.ts +4 -0
- package/dist_ts/formats/ubl/ubl.decoder.js +22 -4
- package/dist_ts/formats/ubl/ubl.types.d.ts +2 -0
- package/dist_ts/formats/ubl/ubl.types.js +4 -2
- package/dist_ts/formats/ubl/ubl.validator.js +7 -3
- package/dist_ts/formats/ubl/xrechnung/xrechnung.decoder.js +6 -34
- package/dist_ts/formats/utils/xml.syntax.d.ts +5 -0
- package/dist_ts/formats/utils/xml.syntax.js +36 -0
- package/dist_ts/vendor/pdflib.browser.d.ts +2 -0
- package/dist_ts/vendor/pdflib.browser.js +6 -0
- package/dist_ts/vendor/xmldom.d.ts +6 -1
- package/dist_ts/vendor/xmldom.js +2 -2
- package/package.json +11 -11
- package/readme.hints.md +14 -3
- package/readme.md +8 -1
- package/readme.plan.md +17 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/einvoice.ts +40 -1
- package/ts/formats/base/base.decoder.ts +120 -0
- package/ts/formats/cii/cii.decoder.ts +18 -0
- package/ts/formats/cii/cii.validator.ts +5 -1
- package/ts/formats/cii/facturx/facturx.decoder.ts +12 -4
- package/ts/formats/factories/decoder.factory.ts +12 -1
- package/ts/formats/factories/validator.factory.ts +11 -2
- package/ts/formats/pdf/extractors/associated.extractor.ts +16 -3
- package/ts/formats/pdf/extractors/standard.extractor.ts +12 -2
- package/ts/formats/ubl/ubl.decoder.ts +32 -3
- package/ts/formats/ubl/ubl.types.ts +3 -1
- package/ts/formats/ubl/ubl.validator.ts +8 -2
- package/ts/formats/ubl/xrechnung/xrechnung.decoder.ts +13 -34
- package/ts/formats/utils/xml.syntax.ts +47 -0
- package/ts/readme.md +3 -1
- package/ts/vendor/pdflib.browser.ts +8 -0
- package/ts/vendor/xmldom.ts +13 -2
package/ts/einvoice.ts
CHANGED
|
@@ -23,6 +23,15 @@ import { ValidatorFactory } from './formats/factories/validator.factory.js';
|
|
|
23
23
|
// Import format detector
|
|
24
24
|
import { FormatDetector } from './formats/utils/format.detector.js';
|
|
25
25
|
|
|
26
|
+
import {
|
|
27
|
+
FACTURX_ATTACHMENT_DESCRIPTION,
|
|
28
|
+
FACTURX_ATTACHMENT_FILENAME,
|
|
29
|
+
} from './formats/cii/facturx/facturx.types.js';
|
|
30
|
+
import {
|
|
31
|
+
ZUGFERD_ATTACHMENT_DESCRIPTION,
|
|
32
|
+
ZUGFERD_ATTACHMENT_FILENAME,
|
|
33
|
+
} from './formats/cii/zugferd/zugferd.types.js';
|
|
34
|
+
|
|
26
35
|
// Import enhanced validators
|
|
27
36
|
import { EN16931Validator } from './formats/validation/en16931.validator.js';
|
|
28
37
|
import { EN16931BusinessRulesValidator } from './formats/validation/en16931.business-rules.validator.js';
|
|
@@ -613,7 +622,13 @@ export class EInvoice implements TInvoice {
|
|
|
613
622
|
try {
|
|
614
623
|
const xmlString = await this.toXmlString(format);
|
|
615
624
|
const pdfEmbedder = await this.getPdfEmbedder();
|
|
616
|
-
const
|
|
625
|
+
const attachment = this.getPdfAttachmentMetadata(format);
|
|
626
|
+
const embedResult = await pdfEmbedder.embedXml(
|
|
627
|
+
pdfBuffer,
|
|
628
|
+
xmlString,
|
|
629
|
+
attachment.filename,
|
|
630
|
+
attachment.description,
|
|
631
|
+
);
|
|
617
632
|
if (!embedResult.success) {
|
|
618
633
|
throw new EInvoicePDFError('Failed to embed XML in PDF', 'embed', { format });
|
|
619
634
|
}
|
|
@@ -624,6 +639,30 @@ export class EInvoice implements TInvoice {
|
|
|
624
639
|
}
|
|
625
640
|
}
|
|
626
641
|
|
|
642
|
+
private getPdfAttachmentMetadata(format: ExportFormat): {
|
|
643
|
+
filename: string;
|
|
644
|
+
description: string;
|
|
645
|
+
} {
|
|
646
|
+
if (format === 'facturx') {
|
|
647
|
+
return {
|
|
648
|
+
filename: FACTURX_ATTACHMENT_FILENAME,
|
|
649
|
+
description: FACTURX_ATTACHMENT_DESCRIPTION,
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
if (format === 'zugferd') {
|
|
654
|
+
return {
|
|
655
|
+
filename: ZUGFERD_ATTACHMENT_FILENAME,
|
|
656
|
+
description: ZUGFERD_ATTACHMENT_DESCRIPTION,
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
return {
|
|
661
|
+
filename: 'invoice.xml',
|
|
662
|
+
description: `${format} XML Invoice`,
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
|
|
627
666
|
/**
|
|
628
667
|
* Saves the invoice to a file
|
|
629
668
|
* @param filePath The path to save to
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { TInvoice } from '../../interfaces/common.js';
|
|
2
2
|
import { ValidationLevel } from '../../interfaces/common.js';
|
|
3
3
|
import type { ValidationResult } from '../../interfaces/common.js';
|
|
4
|
+
import { EInvoiceParsingError } from '../../errors.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Base decoder class that defines common decoding functionality
|
|
@@ -72,4 +73,123 @@ export abstract class BaseDecoder {
|
|
|
72
73
|
const parsed = Date.parse(dateStr);
|
|
73
74
|
return isNaN(parsed) ? Date.now() : parsed;
|
|
74
75
|
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Parses a required CII date without inventing a fallback value.
|
|
79
|
+
*/
|
|
80
|
+
protected parseRequiredCIIDate(dateStr: string, format?: string): number {
|
|
81
|
+
const value = dateStr.trim();
|
|
82
|
+
if (!value) {
|
|
83
|
+
throw new EInvoiceParsingError('Missing required CII invoice issue date', {
|
|
84
|
+
format: 'cii',
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (format === '102') {
|
|
89
|
+
const match = /^(\d{4})(\d{2})(\d{2})$/.exec(value);
|
|
90
|
+
if (!match) {
|
|
91
|
+
throw this.createInvalidDateError(value, 'CII format 102');
|
|
92
|
+
}
|
|
93
|
+
return this.createStrictUtcTimestamp(
|
|
94
|
+
Number(match[1]),
|
|
95
|
+
Number(match[2]),
|
|
96
|
+
Number(match[3]),
|
|
97
|
+
value,
|
|
98
|
+
'CII format 102',
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (format === '610') {
|
|
103
|
+
const match = /^(\d{4})(\d{2})$/.exec(value);
|
|
104
|
+
if (!match) {
|
|
105
|
+
throw this.createInvalidDateError(value, 'CII format 610');
|
|
106
|
+
}
|
|
107
|
+
return this.createStrictUtcTimestamp(
|
|
108
|
+
Number(match[1]),
|
|
109
|
+
Number(match[2]),
|
|
110
|
+
1,
|
|
111
|
+
value,
|
|
112
|
+
'CII format 610',
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const parsed = Date.parse(value);
|
|
117
|
+
if (Number.isNaN(parsed)) {
|
|
118
|
+
throw this.createInvalidDateError(value, 'CII');
|
|
119
|
+
}
|
|
120
|
+
return parsed;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Parses a required UBL xsd:date value strictly and deterministically.
|
|
125
|
+
*/
|
|
126
|
+
protected parseRequiredUblDate(dateStr: string): number {
|
|
127
|
+
const value = dateStr.trim();
|
|
128
|
+
const match =
|
|
129
|
+
/^(\d{4})-(\d{2})-(\d{2})(?:(Z)|([+-])(\d{2}):(\d{2}))?$/.exec(value);
|
|
130
|
+
if (!match) {
|
|
131
|
+
throw this.createInvalidDateError(value, 'UBL xsd:date');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const timestamp = this.createStrictUtcTimestamp(
|
|
135
|
+
Number(match[1]),
|
|
136
|
+
Number(match[2]),
|
|
137
|
+
Number(match[3]),
|
|
138
|
+
value,
|
|
139
|
+
'UBL xsd:date',
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
if (!match[5]) {
|
|
143
|
+
return timestamp;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const offsetHours = Number(match[6]);
|
|
147
|
+
const offsetMinutes = Number(match[7]);
|
|
148
|
+
if (
|
|
149
|
+
offsetHours > 14 ||
|
|
150
|
+
offsetMinutes > 59 ||
|
|
151
|
+
(offsetHours === 14 && offsetMinutes !== 0)
|
|
152
|
+
) {
|
|
153
|
+
throw this.createInvalidDateError(value, 'UBL xsd:date');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const direction = match[5] === '+' ? 1 : -1;
|
|
157
|
+
return timestamp - direction * (offsetHours * 60 + offsetMinutes) * 60_000;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private createStrictUtcTimestamp(
|
|
161
|
+
year: number,
|
|
162
|
+
month: number,
|
|
163
|
+
day: number,
|
|
164
|
+
sourceValue: string,
|
|
165
|
+
format: string,
|
|
166
|
+
): number {
|
|
167
|
+
if (year < 1 || month < 1 || month > 12 || day < 1 || day > 31) {
|
|
168
|
+
throw this.createInvalidDateError(sourceValue, format);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const date = new Date(0);
|
|
172
|
+
date.setUTCHours(0, 0, 0, 0);
|
|
173
|
+
date.setUTCFullYear(year, month - 1, day);
|
|
174
|
+
|
|
175
|
+
if (
|
|
176
|
+
date.getUTCFullYear() !== year ||
|
|
177
|
+
date.getUTCMonth() !== month - 1 ||
|
|
178
|
+
date.getUTCDate() !== day
|
|
179
|
+
) {
|
|
180
|
+
throw this.createInvalidDateError(sourceValue, format);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return date.getTime();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private createInvalidDateError(
|
|
187
|
+
value: string,
|
|
188
|
+
format: string,
|
|
189
|
+
): EInvoiceParsingError {
|
|
190
|
+
return new EInvoiceParsingError(
|
|
191
|
+
`Invalid required invoice issue date "${value}" for ${format}`,
|
|
192
|
+
{ format },
|
|
193
|
+
);
|
|
194
|
+
}
|
|
75
195
|
}
|
|
@@ -2,6 +2,7 @@ import { BaseDecoder } from '../base/base.decoder.js';
|
|
|
2
2
|
import type { TInvoice, TCreditNote, TDebitNote } from '../../interfaces/common.js';
|
|
3
3
|
import { CII_NAMESPACES, CIIProfile } from './cii.types.js';
|
|
4
4
|
import { DOMParser, xpath } from '../../plugins.js';
|
|
5
|
+
import { EInvoiceParsingError } from '../../errors.js';
|
|
5
6
|
|
|
6
7
|
const ciiParser = new DOMParser();
|
|
7
8
|
const ciiNamespaces = {
|
|
@@ -106,6 +107,23 @@ export abstract class CIIBaseDecoder extends BaseDecoder {
|
|
|
106
107
|
return node ? (node.textContent || '') : '';
|
|
107
108
|
}
|
|
108
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Gets a required CII text value and rejects missing content.
|
|
112
|
+
*/
|
|
113
|
+
protected getRequiredText(
|
|
114
|
+
xpathExpr: string,
|
|
115
|
+
fieldName: string,
|
|
116
|
+
context?: Node,
|
|
117
|
+
): string {
|
|
118
|
+
const value = this.getText(xpathExpr, context).trim();
|
|
119
|
+
if (!value) {
|
|
120
|
+
throw new EInvoiceParsingError(`Missing required CII ${fieldName}`, {
|
|
121
|
+
format: 'cii',
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
return value;
|
|
125
|
+
}
|
|
126
|
+
|
|
109
127
|
/**
|
|
110
128
|
* Gets a number value from an XPath expression
|
|
111
129
|
* @param xpath XPath expression
|
|
@@ -89,7 +89,11 @@ export abstract class CIIBaseValidator extends BaseValidator {
|
|
|
89
89
|
|
|
90
90
|
// Check for root element
|
|
91
91
|
const root = this.doc.documentElement;
|
|
92
|
-
if (
|
|
92
|
+
if (
|
|
93
|
+
!root ||
|
|
94
|
+
root.localName !== 'CrossIndustryInvoice' ||
|
|
95
|
+
root.namespaceURI !== CII_NAMESPACES.RSM
|
|
96
|
+
) {
|
|
93
97
|
this.addError('CII-SCHEMA-1', 'Root element must be rsm:CrossIndustryInvoice', '/');
|
|
94
98
|
return false;
|
|
95
99
|
}
|
|
@@ -44,12 +44,20 @@ export class FacturXDecoder extends CIIBaseDecoder {
|
|
|
44
44
|
*/
|
|
45
45
|
private async extractCommonData(): Promise<Partial<TInvoice>> {
|
|
46
46
|
// Extract invoice ID
|
|
47
|
-
const invoiceId = this.
|
|
47
|
+
const invoiceId = this.getRequiredText(
|
|
48
|
+
'/rsm:CrossIndustryInvoice/rsm:ExchangedDocument/ram:ID',
|
|
49
|
+
'invoice identifier',
|
|
50
|
+
);
|
|
48
51
|
|
|
49
52
|
// Extract issue date
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
+
const issueDatePath =
|
|
54
|
+
'/rsm:CrossIndustryInvoice/rsm:ExchangedDocument/ram:IssueDateTime/udt:DateTimeString';
|
|
55
|
+
const issueDateStr = this.getRequiredText(
|
|
56
|
+
issueDatePath,
|
|
57
|
+
'invoice issue date',
|
|
58
|
+
);
|
|
59
|
+
const issueDateFormat = this.getText(`${issueDatePath}/@format`).trim();
|
|
60
|
+
const issueDate = this.parseRequiredCIIDate(issueDateStr, issueDateFormat);
|
|
53
61
|
|
|
54
62
|
// Extract seller information
|
|
55
63
|
const seller = this.extractParty('//ram:SellerTradeParty');
|
|
@@ -7,6 +7,7 @@ import { XRechnungDecoder } from '../ubl/xrechnung/xrechnung.decoder.js';
|
|
|
7
7
|
import { FacturXDecoder } from '../cii/facturx/facturx.decoder.js';
|
|
8
8
|
import { ZUGFeRDDecoder } from '../cii/zugferd/zugferd.decoder.js';
|
|
9
9
|
import { ZUGFeRDV1Decoder } from '../cii/zugferd/zugferd.v1.decoder.js';
|
|
10
|
+
import { detectInvoiceXmlSyntax } from '../utils/xml.syntax.js';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Factory to create the appropriate decoder based on the XML format
|
|
@@ -28,9 +29,19 @@ export class DecoderFactory {
|
|
|
28
29
|
|
|
29
30
|
switch (format) {
|
|
30
31
|
case InvoiceFormat.UBL:
|
|
31
|
-
case InvoiceFormat.XRECHNUNG:
|
|
32
32
|
return new XRechnungDecoder(xml, skipValidation);
|
|
33
33
|
|
|
34
|
+
case InvoiceFormat.XRECHNUNG: {
|
|
35
|
+
const syntax = detectInvoiceXmlSyntax(xml);
|
|
36
|
+
if (syntax === 'cii') {
|
|
37
|
+
return new FacturXDecoder(xml, skipValidation);
|
|
38
|
+
}
|
|
39
|
+
if (syntax === 'ubl') {
|
|
40
|
+
return new XRechnungDecoder(xml, skipValidation);
|
|
41
|
+
}
|
|
42
|
+
throw new Error('Unsupported or malformed XRechnung XML syntax');
|
|
43
|
+
}
|
|
44
|
+
|
|
34
45
|
case InvoiceFormat.CII:
|
|
35
46
|
// For now, use Factur-X decoder for generic CII
|
|
36
47
|
return new FacturXDecoder(xml, skipValidation);
|
|
@@ -8,6 +8,7 @@ import { EN16931UBLValidator } from '../ubl/en16931.ubl.validator.js';
|
|
|
8
8
|
import { XRechnungValidator } from '../ubl/xrechnung.validator.js';
|
|
9
9
|
import { FacturXValidator } from '../cii/facturx/facturx.validator.js';
|
|
10
10
|
import { ZUGFeRDValidator } from '../cii/zugferd/zugferd.validator.js';
|
|
11
|
+
import { detectInvoiceXmlSyntax } from '../utils/xml.syntax.js';
|
|
11
12
|
|
|
12
13
|
// The EN16931UBLValidator handles all UBL-based formats with proper business rules
|
|
13
14
|
// No need for legacy validator implementations here
|
|
@@ -75,8 +76,16 @@ export class ValidatorFactory {
|
|
|
75
76
|
case InvoiceFormat.UBL:
|
|
76
77
|
return new EN16931UBLValidator(xml, parsedDocument);
|
|
77
78
|
|
|
78
|
-
case InvoiceFormat.XRECHNUNG:
|
|
79
|
-
|
|
79
|
+
case InvoiceFormat.XRECHNUNG: {
|
|
80
|
+
const syntax = detectInvoiceXmlSyntax(xml);
|
|
81
|
+
if (syntax === 'cii') {
|
|
82
|
+
return new FacturXValidator(xml, parsedDocument);
|
|
83
|
+
}
|
|
84
|
+
if (syntax === 'ubl') {
|
|
85
|
+
return new XRechnungValidator(xml, parsedDocument);
|
|
86
|
+
}
|
|
87
|
+
return new GenericValidator(xml, format);
|
|
88
|
+
}
|
|
80
89
|
|
|
81
90
|
case InvoiceFormat.CII:
|
|
82
91
|
// For now, use Factur-X validator for generic CII
|
|
@@ -14,7 +14,15 @@ export class AssociatedFilesExtractor extends BaseXMLExtractor {
|
|
|
14
14
|
*/
|
|
15
15
|
public async extractXml(pdfBuffer: Uint8Array | Buffer): Promise<string | null> {
|
|
16
16
|
try {
|
|
17
|
-
const {
|
|
17
|
+
const {
|
|
18
|
+
PDFDocument,
|
|
19
|
+
PDFDict,
|
|
20
|
+
PDFName,
|
|
21
|
+
PDFRawStream,
|
|
22
|
+
PDFArray,
|
|
23
|
+
PDFString,
|
|
24
|
+
PDFHexString,
|
|
25
|
+
} = await loadPdfLib();
|
|
18
26
|
const pdfDoc = await PDFDocument.load(pdfBuffer);
|
|
19
27
|
|
|
20
28
|
// Try to find associated files via the AF entry in the catalog
|
|
@@ -32,8 +40,13 @@ export class AssociatedFilesExtractor extends BaseXMLExtractor {
|
|
|
32
40
|
}
|
|
33
41
|
|
|
34
42
|
// Get the file name
|
|
35
|
-
const
|
|
36
|
-
|
|
43
|
+
const asciiFileName = fileSpec.lookup(PDFName.of('F'));
|
|
44
|
+
const unicodeFileName = fileSpec.lookup(PDFName.of('UF'));
|
|
45
|
+
const fileNameObj =
|
|
46
|
+
asciiFileName instanceof PDFString || asciiFileName instanceof PDFHexString
|
|
47
|
+
? asciiFileName
|
|
48
|
+
: unicodeFileName;
|
|
49
|
+
if (!(fileNameObj instanceof PDFString) && !(fileNameObj instanceof PDFHexString)) {
|
|
37
50
|
continue;
|
|
38
51
|
}
|
|
39
52
|
|
|
@@ -13,7 +13,15 @@ export class StandardXMLExtractor extends BaseXMLExtractor {
|
|
|
13
13
|
*/
|
|
14
14
|
public async extractXml(pdfBuffer: Uint8Array | Buffer): Promise<string | null> {
|
|
15
15
|
try {
|
|
16
|
-
const {
|
|
16
|
+
const {
|
|
17
|
+
PDFDocument,
|
|
18
|
+
PDFDict,
|
|
19
|
+
PDFName,
|
|
20
|
+
PDFRawStream,
|
|
21
|
+
PDFArray,
|
|
22
|
+
PDFString,
|
|
23
|
+
PDFHexString,
|
|
24
|
+
} = await loadPdfLib();
|
|
17
25
|
const pdfDoc = await PDFDocument.load(pdfBuffer);
|
|
18
26
|
|
|
19
27
|
// Get the document's metadata dictionary
|
|
@@ -42,7 +50,9 @@ export class StandardXMLExtractor extends BaseXMLExtractor {
|
|
|
42
50
|
const fileNameObj = filesSpecObj.lookup(i);
|
|
43
51
|
const fileSpecObj = filesSpecObj.lookup(i + 1);
|
|
44
52
|
|
|
45
|
-
|
|
53
|
+
const isFileNameString =
|
|
54
|
+
fileNameObj instanceof PDFString || fileNameObj instanceof PDFHexString;
|
|
55
|
+
if (!isFileNameString || !(fileSpecObj instanceof PDFDict)) {
|
|
46
56
|
continue;
|
|
47
57
|
}
|
|
48
58
|
|
|
@@ -2,6 +2,7 @@ import { BaseDecoder } from '../base/base.decoder.js';
|
|
|
2
2
|
import type { TInvoice, TCreditNote, TDebitNote } from '../../interfaces/common.js';
|
|
3
3
|
import { UBLDocumentType, UBL_NAMESPACES } from './ubl.types.js';
|
|
4
4
|
import { DOMParser, xpath } from '../../plugins.js';
|
|
5
|
+
import { EInvoiceParsingError } from '../../errors.js';
|
|
5
6
|
|
|
6
7
|
const ublParser = new DOMParser();
|
|
7
8
|
const ublNamespaces = {
|
|
@@ -55,13 +56,24 @@ export abstract class UBLBaseDecoder extends BaseDecoder {
|
|
|
55
56
|
* @returns UBL document type
|
|
56
57
|
*/
|
|
57
58
|
protected getDocumentType(): UBLDocumentType {
|
|
58
|
-
const
|
|
59
|
+
const root = this.doc.documentElement;
|
|
59
60
|
|
|
60
|
-
if (
|
|
61
|
+
if (
|
|
62
|
+
root.localName === UBLDocumentType.CREDIT_NOTE &&
|
|
63
|
+
root.namespaceURI === UBL_NAMESPACES.CREDIT_NOTE
|
|
64
|
+
) {
|
|
61
65
|
return UBLDocumentType.CREDIT_NOTE;
|
|
62
|
-
}
|
|
66
|
+
}
|
|
67
|
+
if (
|
|
68
|
+
root.localName === UBLDocumentType.INVOICE &&
|
|
69
|
+
root.namespaceURI === UBL_NAMESPACES.INVOICE
|
|
70
|
+
) {
|
|
63
71
|
return UBLDocumentType.INVOICE;
|
|
64
72
|
}
|
|
73
|
+
|
|
74
|
+
throw new EInvoiceParsingError('Unsupported UBL document root', {
|
|
75
|
+
format: 'ubl',
|
|
76
|
+
});
|
|
65
77
|
}
|
|
66
78
|
|
|
67
79
|
/**
|
|
@@ -88,6 +100,23 @@ export abstract class UBLBaseDecoder extends BaseDecoder {
|
|
|
88
100
|
return node ? (node.textContent || '') : '';
|
|
89
101
|
}
|
|
90
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Gets a required UBL text value and rejects missing content.
|
|
105
|
+
*/
|
|
106
|
+
protected getRequiredText(
|
|
107
|
+
xpathExpr: string,
|
|
108
|
+
fieldName: string,
|
|
109
|
+
context?: Node,
|
|
110
|
+
): string {
|
|
111
|
+
const value = this.getText(xpathExpr, context).trim();
|
|
112
|
+
if (!value) {
|
|
113
|
+
throw new EInvoiceParsingError(`Missing required UBL ${fieldName}`, {
|
|
114
|
+
format: 'ubl',
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return value;
|
|
118
|
+
}
|
|
119
|
+
|
|
91
120
|
/**
|
|
92
121
|
* Gets a number value from an XPath expression
|
|
93
122
|
* @param xpath XPath expression
|
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
export const UBL_NAMESPACES = {
|
|
7
7
|
CBC: 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2',
|
|
8
8
|
CAC: 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2',
|
|
9
|
-
UBL: 'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2'
|
|
9
|
+
UBL: 'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2',
|
|
10
|
+
INVOICE: 'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2',
|
|
11
|
+
CREDIT_NOTE: 'urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2',
|
|
10
12
|
};
|
|
11
13
|
|
|
12
14
|
// UBL document types
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { BaseValidator } from '../base/base.validator.js';
|
|
2
2
|
import { ValidationLevel } from '../../interfaces/common.js';
|
|
3
3
|
import type { ValidationResult } from '../../interfaces/common.js';
|
|
4
|
-
import { UBLDocumentType } from './ubl.types.js';
|
|
4
|
+
import { UBLDocumentType, UBL_NAMESPACES } from './ubl.types.js';
|
|
5
5
|
import { DOMParser, xpath } from '../../plugins.js';
|
|
6
6
|
|
|
7
7
|
const ublValidatorParser = new DOMParser();
|
|
@@ -84,7 +84,13 @@ export abstract class UBLBaseValidator extends BaseValidator {
|
|
|
84
84
|
|
|
85
85
|
// Check for root element
|
|
86
86
|
const root = this.doc.documentElement;
|
|
87
|
-
|
|
87
|
+
const isInvoice =
|
|
88
|
+
root?.localName === UBLDocumentType.INVOICE &&
|
|
89
|
+
root.namespaceURI === UBL_NAMESPACES.INVOICE;
|
|
90
|
+
const isCreditNote =
|
|
91
|
+
root?.localName === UBLDocumentType.CREDIT_NOTE &&
|
|
92
|
+
root.namespaceURI === UBL_NAMESPACES.CREDIT_NOTE;
|
|
93
|
+
if (!isInvoice && !isCreditNote) {
|
|
88
94
|
this.addError('UBL-SCHEMA-1', `Root element must be ${UBLDocumentType.INVOICE} or ${UBLDocumentType.CREDIT_NOTE}`, '/');
|
|
89
95
|
return false;
|
|
90
96
|
}
|
|
@@ -45,10 +45,18 @@ export class XRechnungDecoder extends UBLBaseDecoder {
|
|
|
45
45
|
*/
|
|
46
46
|
private async extractCommonData(): Promise<Partial<TInvoice>> {
|
|
47
47
|
try {
|
|
48
|
-
|
|
49
|
-
const invoiceId = this.
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
const root = this.doc.documentElement;
|
|
49
|
+
const invoiceId = this.getRequiredText(
|
|
50
|
+
'./cbc:ID',
|
|
51
|
+
'invoice identifier',
|
|
52
|
+
root,
|
|
53
|
+
);
|
|
54
|
+
const issueDateText = this.getRequiredText(
|
|
55
|
+
'./cbc:IssueDate',
|
|
56
|
+
'invoice issue date',
|
|
57
|
+
root,
|
|
58
|
+
);
|
|
59
|
+
const issueDate = this.parseRequiredUblDate(issueDateText);
|
|
52
60
|
const currencyCode = this.getText('//cbc:DocumentCurrencyCode', this.doc) || 'EUR';
|
|
53
61
|
|
|
54
62
|
// Extract payment terms
|
|
@@ -267,36 +275,7 @@ export class XRechnungDecoder extends UBLBaseDecoder {
|
|
|
267
275
|
|
|
268
276
|
return invoiceData;
|
|
269
277
|
} catch (error) {
|
|
270
|
-
|
|
271
|
-
if (error instanceof Error && error.message.includes('EN16931 validation failed')) {
|
|
272
|
-
throw error;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
console.error('Error extracting common data:', error);
|
|
276
|
-
// Return default data
|
|
277
|
-
return {
|
|
278
|
-
type: 'accounting-doc' as const,
|
|
279
|
-
accountingDocType: 'invoice' as const,
|
|
280
|
-
id: `INV-${Date.now()}`,
|
|
281
|
-
accountingDocId: `INV-${Date.now()}`,
|
|
282
|
-
date: Date.now(),
|
|
283
|
-
accountingDocStatus: 'issued' as const,
|
|
284
|
-
versionInfo: {
|
|
285
|
-
type: 'final',
|
|
286
|
-
version: '1.0.0'
|
|
287
|
-
},
|
|
288
|
-
language: 'en',
|
|
289
|
-
incidenceId: `INV-${Date.now()}`,
|
|
290
|
-
from: this.createEmptyContact(),
|
|
291
|
-
to: this.createEmptyContact(),
|
|
292
|
-
subject: 'Invoice',
|
|
293
|
-
items: [],
|
|
294
|
-
dueInDays: 30,
|
|
295
|
-
reverseCharge: false,
|
|
296
|
-
currency: 'EUR',
|
|
297
|
-
notes: [],
|
|
298
|
-
objectActions: []
|
|
299
|
-
};
|
|
278
|
+
throw error;
|
|
300
279
|
}
|
|
301
280
|
}
|
|
302
281
|
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { DOMParser } from '../../plugins.js';
|
|
2
|
+
import { CII_NAMESPACES } from '../cii/cii.types.js';
|
|
3
|
+
import { UBL_NAMESPACES } from '../ubl/ubl.types.js';
|
|
4
|
+
|
|
5
|
+
export type TInvoiceXmlSyntax = 'cii' | 'ubl' | 'unknown';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Detects the XML syntax independently from the invoice profile.
|
|
9
|
+
*/
|
|
10
|
+
export const detectInvoiceXmlSyntax = (xml: string): TInvoiceXmlSyntax => {
|
|
11
|
+
const parseDiagnostics: string[] = [];
|
|
12
|
+
const parser = new DOMParser({
|
|
13
|
+
onError: (level, message) =>
|
|
14
|
+
parseDiagnostics.push(`${level}: ${message}`),
|
|
15
|
+
});
|
|
16
|
+
let document: Document;
|
|
17
|
+
try {
|
|
18
|
+
document = parser.parseFromString(xml, 'application/xml');
|
|
19
|
+
} catch {
|
|
20
|
+
return 'unknown';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (parseDiagnostics.length > 0) {
|
|
24
|
+
return 'unknown';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const root = document.documentElement;
|
|
28
|
+
if (!root) {
|
|
29
|
+
return 'unknown';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (
|
|
33
|
+
root.localName === 'CrossIndustryInvoice' &&
|
|
34
|
+
root.namespaceURI === CII_NAMESPACES.RSM
|
|
35
|
+
) {
|
|
36
|
+
return 'cii';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const isUblInvoice =
|
|
40
|
+
root.localName === 'Invoice' &&
|
|
41
|
+
root.namespaceURI === UBL_NAMESPACES.INVOICE;
|
|
42
|
+
const isUblCreditNote =
|
|
43
|
+
root.localName === 'CreditNote' &&
|
|
44
|
+
root.namespaceURI === UBL_NAMESPACES.CREDIT_NOTE;
|
|
45
|
+
|
|
46
|
+
return isUblInvoice || isUblCreditNote ? 'ubl' : 'unknown';
|
|
47
|
+
};
|
package/ts/readme.md
CHANGED
|
@@ -36,7 +36,7 @@ const xml = await invoice.exportXml('facturx');
|
|
|
36
36
|
| Format | Detect | Import | Export | Notes |
|
|
37
37
|
| --- | --- | --- | --- | --- |
|
|
38
38
|
| `ubl` | Yes | Yes | Yes | Generic UBL flow |
|
|
39
|
-
| `xrechnung` | Yes | Yes | Yes | UBL
|
|
39
|
+
| `xrechnung` | Yes | Yes | Yes | UBL export; UBL and CII import |
|
|
40
40
|
| `cii` | Yes | Yes | Yes | Generic export currently routes through the Factur-X encoder path |
|
|
41
41
|
| `facturx` | Yes | Yes | Yes | Main CII generation path |
|
|
42
42
|
| `zugferd` | Yes | Yes | Yes | Input supports v1 and v2+ |
|
|
@@ -45,6 +45,8 @@ const xml = await invoice.exportXml('facturx');
|
|
|
45
45
|
## Important implementation notes
|
|
46
46
|
|
|
47
47
|
- `peppol` is not a root-level XML export target.
|
|
48
|
+
- XRechnung import routes UBL and CII by the namespace-qualified XML root.
|
|
49
|
+
Missing or invalid document-level invoice IDs and issue dates are rejected.
|
|
48
50
|
- `FatturaPA` is not fully implemented for import/export.
|
|
49
51
|
- PDF support means extracting or embedding XML into existing PDFs, not generating invoice PDFs from scratch.
|
|
50
52
|
- Validation is useful and extensive, but should not be documented as blanket certification.
|
package/ts/vendor/xmldom.ts
CHANGED
|
@@ -1,11 +1,22 @@
|
|
|
1
|
-
import * as xmldomRuntime from 'xmldom';
|
|
1
|
+
import * as xmldomRuntime from '@xmldom/xmldom';
|
|
2
2
|
|
|
3
3
|
export interface IDOMParser {
|
|
4
4
|
parseFromString(source: string, mimeType: string): Document;
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
+
export type TDOMParserErrorLevel = 'warning' | 'error' | 'fatalError';
|
|
8
|
+
|
|
9
|
+
export type TDOMParserErrorHandler = (
|
|
10
|
+
level: TDOMParserErrorLevel,
|
|
11
|
+
message: string,
|
|
12
|
+
) => void;
|
|
13
|
+
|
|
14
|
+
export interface IDOMParserOptions {
|
|
15
|
+
onError?: TDOMParserErrorHandler;
|
|
16
|
+
}
|
|
17
|
+
|
|
7
18
|
export interface IDOMParserConstructor {
|
|
8
|
-
new (): IDOMParser;
|
|
19
|
+
new (options?: IDOMParserOptions): IDOMParser;
|
|
9
20
|
}
|
|
10
21
|
|
|
11
22
|
export interface IXMLSerializer {
|