@fin.cx/einvoice 6.1.1 → 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.
Files changed (44) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/einvoice.d.ts +1 -0
  3. package/dist_ts/einvoice.js +23 -2
  4. package/dist_ts/formats/base/base.decoder.d.ts +10 -0
  5. package/dist_ts/formats/base/base.decoder.js +72 -1
  6. package/dist_ts/formats/cii/cii.decoder.d.ts +4 -0
  7. package/dist_ts/formats/cii/cii.decoder.js +14 -1
  8. package/dist_ts/formats/cii/cii.validator.js +4 -2
  9. package/dist_ts/formats/cii/facturx/facturx.decoder.js +6 -5
  10. package/dist_ts/formats/factories/decoder.factory.js +12 -2
  11. package/dist_ts/formats/factories/validator.factory.js +12 -3
  12. package/dist_ts/formats/pdf/extractors/associated.extractor.js +8 -4
  13. package/dist_ts/formats/pdf/extractors/standard.extractor.js +4 -3
  14. package/dist_ts/formats/ubl/ubl.decoder.d.ts +4 -0
  15. package/dist_ts/formats/ubl/ubl.decoder.js +22 -4
  16. package/dist_ts/formats/ubl/ubl.types.d.ts +2 -0
  17. package/dist_ts/formats/ubl/ubl.types.js +4 -2
  18. package/dist_ts/formats/ubl/ubl.validator.js +7 -3
  19. package/dist_ts/formats/ubl/xrechnung/xrechnung.decoder.js +6 -34
  20. package/dist_ts/formats/utils/xml.syntax.d.ts +5 -0
  21. package/dist_ts/formats/utils/xml.syntax.js +36 -0
  22. package/dist_ts/vendor/xmldom.d.ts +6 -1
  23. package/dist_ts/vendor/xmldom.js +2 -2
  24. package/package.json +9 -10
  25. package/readme.hints.md +14 -3
  26. package/readme.md +8 -1
  27. package/readme.plan.md +17 -1
  28. package/ts/00_commitinfo_data.ts +1 -1
  29. package/ts/einvoice.ts +40 -1
  30. package/ts/formats/base/base.decoder.ts +120 -0
  31. package/ts/formats/cii/cii.decoder.ts +18 -0
  32. package/ts/formats/cii/cii.validator.ts +5 -1
  33. package/ts/formats/cii/facturx/facturx.decoder.ts +12 -4
  34. package/ts/formats/factories/decoder.factory.ts +12 -1
  35. package/ts/formats/factories/validator.factory.ts +11 -2
  36. package/ts/formats/pdf/extractors/associated.extractor.ts +16 -3
  37. package/ts/formats/pdf/extractors/standard.extractor.ts +12 -2
  38. package/ts/formats/ubl/ubl.decoder.ts +32 -3
  39. package/ts/formats/ubl/ubl.types.ts +3 -1
  40. package/ts/formats/ubl/ubl.validator.ts +8 -2
  41. package/ts/formats/ubl/xrechnung/xrechnung.decoder.ts +13 -34
  42. package/ts/formats/utils/xml.syntax.ts +47 -0
  43. package/ts/readme.md +3 -1
  44. package/ts/vendor/xmldom.ts +13 -2
@@ -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 (!root || root.nodeName !== 'rsm:CrossIndustryInvoice') {
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.getText('//rsm:ExchangedDocument/ram:ID');
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 issueDateStr = this.getText('//ram:IssueDateTime/udt:DateTimeString');
51
- const issueDateFormat = this.getText('//ram:IssueDateTime/udt:DateTimeString/@format');
52
- const issueDate = this.parseCIIDate(issueDateStr, issueDateFormat);
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
- return new XRechnungValidator(xml, parsedDocument);
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 { PDFDocument, PDFDict, PDFName, PDFRawStream, PDFArray, PDFString } = await loadPdfLib();
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 fileNameObj = fileSpec.lookup(PDFName.of('F')) || fileSpec.lookup(PDFName.of('UF'));
36
- if (!(fileNameObj instanceof PDFString)) {
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 { PDFDocument, PDFDict, PDFName, PDFRawStream, PDFArray, PDFString } = await loadPdfLib();
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
- if (!(fileNameObj instanceof PDFString) || !(fileSpecObj instanceof PDFDict)) {
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 rootName = this.doc.documentElement.nodeName;
59
+ const root = this.doc.documentElement;
59
60
 
60
- if (rootName === UBLDocumentType.CREDIT_NOTE) {
61
+ if (
62
+ root.localName === UBLDocumentType.CREDIT_NOTE &&
63
+ root.namespaceURI === UBL_NAMESPACES.CREDIT_NOTE
64
+ ) {
61
65
  return UBLDocumentType.CREDIT_NOTE;
62
- } else {
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
- if (!root || (root.nodeName !== UBLDocumentType.INVOICE && root.nodeName !== UBLDocumentType.CREDIT_NOTE)) {
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
- // Default values
49
- const invoiceId = this.getText('//cbc:ID', this.doc) || `INV-${Date.now()}`;
50
- const issueDateText = this.getText('//cbc:IssueDate', this.doc);
51
- const issueDate = issueDateText ? new Date(issueDateText).getTime() : Date.now();
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
- // Re-throw validation errors
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-based profile |
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.
@@ -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 {