@fin.cx/einvoice 5.1.3 → 5.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/{npmextra.json → .smartconfig.json} +12 -6
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/einvoice.d.ts +14 -5
- package/dist_ts/einvoice.js +165 -60
- package/dist_ts/errors.js +14 -4
- package/dist_ts/formats/base/base.decoder.d.ts +4 -0
- package/dist_ts/formats/base/base.decoder.js +9 -1
- package/dist_ts/formats/base/base.validator.js +3 -2
- package/dist_ts/formats/cii/cii.decoder.d.ts +1 -0
- package/dist_ts/formats/cii/cii.decoder.js +20 -10
- package/dist_ts/formats/cii/cii.encoder.js +2 -5
- package/dist_ts/formats/cii/cii.validator.d.ts +1 -1
- package/dist_ts/formats/cii/cii.validator.js +19 -12
- package/dist_ts/formats/converters/xml-to-einvoice.converter.js +3 -3
- package/dist_ts/formats/factories/decoder.factory.d.ts +3 -1
- package/dist_ts/formats/factories/decoder.factory.js +4 -3
- package/dist_ts/formats/factories/validator.factory.d.ts +4 -1
- package/dist_ts/formats/factories/validator.factory.js +11 -8
- package/dist_ts/formats/pdf/extractors/base.extractor.js +49 -51
- package/dist_ts/formats/pdf/extractors/text.extractor.js +19 -22
- package/dist_ts/formats/pdf/pdf.extractor.js +2 -2
- package/dist_ts/formats/semantic/semantic.adapter.js +42 -12
- package/dist_ts/formats/semantic/semantic.validator.js +3 -1
- package/dist_ts/formats/ubl/en16931.ubl.validator.js +5 -3
- package/dist_ts/formats/ubl/generic/ubl.encoder.js +11 -7
- package/dist_ts/formats/ubl/ubl.decoder.d.ts +1 -0
- package/dist_ts/formats/ubl/ubl.decoder.js +18 -8
- package/dist_ts/formats/ubl/ubl.validator.d.ts +1 -1
- package/dist_ts/formats/ubl/ubl.validator.js +17 -10
- package/dist_ts/formats/ubl/xrechnung/xrechnung.decoder.js +37 -29
- package/dist_ts/formats/utils/currency.calculator.decimal.js +4 -1
- package/dist_ts/formats/utils/currency.utils.js +4 -1
- package/dist_ts/formats/utils/decimal.js +9 -2
- package/dist_ts/formats/utils/format.detector.js +47 -17
- package/dist_ts/formats/validation/codelist.validator.js +2 -4
- package/dist_ts/formats/validation/conformance.harness.js +13 -6
- package/dist_ts/formats/validation/en16931.business-rules.validator.js +4 -4
- package/dist_ts/formats/validation/en16931.validator.d.ts +6 -0
- package/dist_ts/formats/validation/en16931.validator.js +18 -12
- package/dist_ts/formats/validation/facturx.validator.js +147 -148
- package/dist_ts/formats/validation/integrated.validator.js +12 -4
- package/dist_ts/formats/validation/peppol.validator.js +2 -1
- package/dist_ts/formats/validation/schematron.downloader.js +13 -6
- package/dist_ts/formats/validation/schematron.integration.js +12 -5
- package/dist_ts/formats/validation/schematron.validator.js +17 -13
- package/dist_ts/formats/validation/schematron.worker.js +6 -5
- package/dist_ts/formats/validation/vat-categories.validator.js +3 -4
- package/dist_ts/formats/validation/xrechnung.validator.js +7 -7
- package/dist_ts/index.js +3 -2
- package/dist_ts/interfaces/common.js +1 -2
- package/dist_ts/plugins.d.ts +3 -4
- package/dist_ts/plugins.js +4 -5
- package/dist_ts/vendor/pako.d.ts +5 -0
- package/dist_ts/vendor/pako.js +3 -0
- package/dist_ts/vendor/saxonjs.d.ts +22 -0
- package/dist_ts/vendor/saxonjs.js +3 -0
- package/dist_ts/vendor/xmldom.d.ts +20 -0
- package/dist_ts/vendor/xmldom.js +5 -0
- package/dist_ts_install/download-schematron.d.ts +0 -3
- package/dist_ts_install/download-schematron.js +20 -11
- package/dist_ts_install/download-test-samples.d.ts +0 -3
- package/dist_ts_install/download-test-samples.js +4 -2
- package/dist_ts_install/download-xrechnung-rules.js +2 -1
- package/dist_ts_install/index.d.ts +0 -5
- package/dist_ts_install/index.js +26 -12
- package/license +21 -0
- package/package.json +25 -23
- package/readme.hints.md +24 -8
- package/readme.md +195 -328
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/einvoice.ts +148 -26
- package/ts/formats/base/base.decoder.ts +7 -0
- package/ts/formats/cii/cii.decoder.ts +17 -8
- package/ts/formats/cii/cii.validator.ts +18 -13
- package/ts/formats/converters/xml-to-einvoice.converter.ts +4 -5
- package/ts/formats/factories/decoder.factory.ts +7 -2
- package/ts/formats/factories/validator.factory.ts +14 -8
- package/ts/formats/semantic/semantic.adapter.ts +49 -15
- package/ts/formats/ubl/en16931.ubl.validator.ts +5 -3
- package/ts/formats/ubl/generic/ubl.encoder.ts +12 -7
- package/ts/formats/ubl/ubl.decoder.ts +18 -9
- package/ts/formats/ubl/ubl.validator.ts +17 -12
- package/ts/formats/ubl/xrechnung/xrechnung.decoder.ts +41 -28
- package/ts/formats/utils/format.detector.ts +57 -19
- package/ts/formats/validation/conformance.harness.ts +6 -4
- package/ts/formats/validation/en16931.validator.ts +20 -10
- package/ts/formats/validation/integrated.validator.ts +5 -3
- package/ts/formats/validation/schematron.downloader.ts +11 -6
- package/ts/formats/validation/schematron.integration.ts +7 -4
- package/ts/formats/validation/schematron.validator.ts +12 -11
- package/ts/index.ts +2 -1
- package/ts/interfaces/common.ts +1 -3
- package/ts/plugins.ts +3 -4
- package/ts/readme.md +55 -0
- package/ts/vendor/modules.d.ts +32 -0
- package/ts/vendor/pako.ts +8 -0
- package/ts/vendor/saxonjs.ts +28 -0
- package/ts/vendor/xmldom.ts +27 -0
package/ts/einvoice.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
2
|
|
|
3
|
-
import { business, finance } from '
|
|
3
|
+
import type { business, finance } from '@tsclass/tsclass';
|
|
4
4
|
import type { TInvoice, TAccountingDocItem } from '@tsclass/tsclass/dist_ts/finance/index.js';
|
|
5
5
|
import { InvoiceFormat, ValidationLevel } from './interfaces/common.js';
|
|
6
6
|
import type { ValidationResult, ValidationError, EInvoiceOptions, IPdf, ExportFormat } from './interfaces/common.js';
|
|
@@ -28,6 +28,7 @@ import { PDFExtractor } from './formats/pdf/pdf.extractor.js';
|
|
|
28
28
|
import { FormatDetector } from './formats/utils/format.detector.js';
|
|
29
29
|
|
|
30
30
|
// Import enhanced validators
|
|
31
|
+
import { EN16931Validator } from './formats/validation/en16931.validator.js';
|
|
31
32
|
import { EN16931BusinessRulesValidator } from './formats/validation/en16931.business-rules.validator.js';
|
|
32
33
|
import { CodeListValidator } from './formats/validation/codelist.validator.js';
|
|
33
34
|
import type { ValidationOptions } from './formats/validation/validation.types.js';
|
|
@@ -41,6 +42,9 @@ import type { IEInvoiceMetadata } from './interfaces/en16931-metadata.js';
|
|
|
41
42
|
* Extends the TInvoice interface for seamless integration with existing systems
|
|
42
43
|
*/
|
|
43
44
|
export class EInvoice implements TInvoice {
|
|
45
|
+
private static sharedPdfEmbedder?: PDFEmbedder;
|
|
46
|
+
private static sharedPdfExtractor?: PDFExtractor;
|
|
47
|
+
|
|
44
48
|
/**
|
|
45
49
|
* Creates an EInvoice instance from XML string
|
|
46
50
|
* @param xmlString XML string to parse
|
|
@@ -109,8 +113,8 @@ export class EInvoice implements TInvoice {
|
|
|
109
113
|
public incidenceId: string = '';
|
|
110
114
|
public language: string = 'en';
|
|
111
115
|
public objectActions: any[] = [];
|
|
112
|
-
public pdf
|
|
113
|
-
public pdfAttachments
|
|
116
|
+
public pdf?: IPdf;
|
|
117
|
+
public pdfAttachments?: IPdf[];
|
|
114
118
|
public accentColor: string | null = null;
|
|
115
119
|
public logoUrl: string | null = null;
|
|
116
120
|
|
|
@@ -149,7 +153,13 @@ export class EInvoice implements TInvoice {
|
|
|
149
153
|
this.accountingDocType === 'creditnote' ? 'creditnote' : 'debitnote';
|
|
150
154
|
}
|
|
151
155
|
public set invoiceType(value: 'invoice' | 'creditnote' | 'debitnote') {
|
|
152
|
-
|
|
156
|
+
if (value !== 'invoice') {
|
|
157
|
+
throw new EInvoiceFormatError(
|
|
158
|
+
`Unsupported invoice type: ${value}`,
|
|
159
|
+
{ unsupportedFeatures: [`invoiceType=${value}`] }
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
this.accountingDocType = 'invoice';
|
|
153
163
|
}
|
|
154
164
|
|
|
155
165
|
// Computed properties for convenience
|
|
@@ -181,15 +191,28 @@ export class EInvoice implements TInvoice {
|
|
|
181
191
|
|
|
182
192
|
private xmlString: string = '';
|
|
183
193
|
private detectedFormat: InvoiceFormat = InvoiceFormat.UNKNOWN;
|
|
194
|
+
private parsedXmlDocument?: Document;
|
|
184
195
|
private validationErrors: ValidationError[] = [];
|
|
196
|
+
private validationCache = new Map<string, ValidationResult>();
|
|
185
197
|
private options: EInvoiceOptions = {
|
|
186
198
|
validateOnLoad: false,
|
|
187
199
|
validationLevel: ValidationLevel.SYNTAX
|
|
188
200
|
};
|
|
189
201
|
|
|
190
|
-
// PDF utilities
|
|
191
|
-
private pdfEmbedder
|
|
192
|
-
|
|
202
|
+
// PDF utilities are created lazily because most invoice workflows never touch PDF I/O.
|
|
203
|
+
private get pdfEmbedder(): PDFEmbedder {
|
|
204
|
+
if (!EInvoice.sharedPdfEmbedder) {
|
|
205
|
+
EInvoice.sharedPdfEmbedder = new PDFEmbedder();
|
|
206
|
+
}
|
|
207
|
+
return EInvoice.sharedPdfEmbedder;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private get pdfExtractor(): PDFExtractor {
|
|
211
|
+
if (!EInvoice.sharedPdfExtractor) {
|
|
212
|
+
EInvoice.sharedPdfExtractor = new PDFExtractor();
|
|
213
|
+
}
|
|
214
|
+
return EInvoice.sharedPdfExtractor;
|
|
215
|
+
}
|
|
193
216
|
|
|
194
217
|
/**
|
|
195
218
|
* Creates a new EInvoice instance
|
|
@@ -260,6 +283,7 @@ export class EInvoice implements TInvoice {
|
|
|
260
283
|
*/
|
|
261
284
|
public async fromXmlString(xmlString: string): Promise<EInvoice> {
|
|
262
285
|
try {
|
|
286
|
+
this.validationCache.clear();
|
|
263
287
|
this.xmlString = xmlString;
|
|
264
288
|
|
|
265
289
|
// Detect format
|
|
@@ -269,8 +293,13 @@ export class EInvoice implements TInvoice {
|
|
|
269
293
|
}
|
|
270
294
|
|
|
271
295
|
// Get appropriate decoder
|
|
272
|
-
const decoder = DecoderFactory.createDecoder(
|
|
296
|
+
const decoder = DecoderFactory.createDecoder(
|
|
297
|
+
xmlString,
|
|
298
|
+
!this.options.validateOnLoad,
|
|
299
|
+
this.detectedFormat,
|
|
300
|
+
);
|
|
273
301
|
const invoice = await decoder.decode();
|
|
302
|
+
this.parsedXmlDocument = decoder.getParsedDocument();
|
|
274
303
|
|
|
275
304
|
// Map the decoded invoice to our properties
|
|
276
305
|
this.mapFromTInvoice(invoice);
|
|
@@ -282,10 +311,11 @@ export class EInvoice implements TInvoice {
|
|
|
282
311
|
|
|
283
312
|
return this;
|
|
284
313
|
} catch (error) {
|
|
314
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
285
315
|
if (error instanceof EInvoiceError) {
|
|
286
316
|
throw error;
|
|
287
317
|
}
|
|
288
|
-
throw new EInvoiceParsingError(`Failed to parse XML: ${
|
|
318
|
+
throw new EInvoiceParsingError(`Failed to parse XML: ${errorMessage}`, {}, error as Error);
|
|
289
319
|
}
|
|
290
320
|
}
|
|
291
321
|
|
|
@@ -307,7 +337,8 @@ export class EInvoice implements TInvoice {
|
|
|
307
337
|
const xmlString = fileBuffer.toString('utf-8');
|
|
308
338
|
return this.fromXmlString(xmlString);
|
|
309
339
|
} catch (error) {
|
|
310
|
-
|
|
340
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
341
|
+
throw new EInvoiceError(`Failed to load file: ${errorMessage}`, 'FILE_LOAD_ERROR', { filePath });
|
|
311
342
|
}
|
|
312
343
|
}
|
|
313
344
|
|
|
@@ -343,10 +374,11 @@ export class EInvoice implements TInvoice {
|
|
|
343
374
|
|
|
344
375
|
return this.fromXmlString(extractedXml);
|
|
345
376
|
} catch (error) {
|
|
377
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
346
378
|
if (error instanceof EInvoiceError) {
|
|
347
379
|
throw error;
|
|
348
380
|
}
|
|
349
|
-
throw new EInvoicePDFError(`Failed to extract invoice from PDF: ${
|
|
381
|
+
throw new EInvoicePDFError(`Failed to extract invoice from PDF: ${errorMessage}`, 'extract', {}, error as Error);
|
|
350
382
|
}
|
|
351
383
|
}
|
|
352
384
|
|
|
@@ -423,7 +455,8 @@ export class EInvoice implements TInvoice {
|
|
|
423
455
|
|
|
424
456
|
return await encoder.encode(invoice);
|
|
425
457
|
} catch (error) {
|
|
426
|
-
|
|
458
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
459
|
+
throw new EInvoiceFormatError(`Failed to encode to ${format}: ${errorMessage}`, { targetFormat: format });
|
|
427
460
|
}
|
|
428
461
|
}
|
|
429
462
|
|
|
@@ -434,21 +467,34 @@ export class EInvoice implements TInvoice {
|
|
|
434
467
|
*/
|
|
435
468
|
public async validate(level: ValidationLevel = ValidationLevel.BUSINESS, options?: ValidationOptions): Promise<ValidationResult> {
|
|
436
469
|
try {
|
|
437
|
-
// For programmatically created invoices without XML,
|
|
470
|
+
// For programmatically created invoices without XML, validate the in-memory invoice object.
|
|
438
471
|
let result: ValidationResult;
|
|
472
|
+
const cacheKey = this.getValidationCacheKey(level, options);
|
|
473
|
+
|
|
474
|
+
if (cacheKey) {
|
|
475
|
+
const cached = this.validationCache.get(cacheKey);
|
|
476
|
+
if (cached) {
|
|
477
|
+
return this.cloneValidationResult(cached);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
439
480
|
|
|
440
481
|
if (this.xmlString && this.detectedFormat !== InvoiceFormat.UNKNOWN) {
|
|
482
|
+
if (this.shouldUseFastBusinessValidation(level, options)) {
|
|
483
|
+
result = this.validateDecodedInvoice(level);
|
|
484
|
+
} else {
|
|
441
485
|
// Use existing validator for XML-based validation
|
|
442
|
-
|
|
443
|
-
|
|
486
|
+
const validator = ValidatorFactory.createValidator(
|
|
487
|
+
this.xmlString,
|
|
488
|
+
this.detectedFormat,
|
|
489
|
+
this.parsedXmlDocument,
|
|
490
|
+
);
|
|
491
|
+
result = validator.validate(level);
|
|
492
|
+
|
|
493
|
+
// Keep the raw XML, but drop the cached DOM once validation is done.
|
|
494
|
+
this.parsedXmlDocument = undefined;
|
|
495
|
+
}
|
|
444
496
|
} else {
|
|
445
|
-
|
|
446
|
-
result = {
|
|
447
|
-
valid: true,
|
|
448
|
-
errors: [],
|
|
449
|
-
warnings: [],
|
|
450
|
-
level: level
|
|
451
|
-
};
|
|
497
|
+
result = this.validateDecodedInvoice(level);
|
|
452
498
|
}
|
|
453
499
|
|
|
454
500
|
// Enhanced validation with feature flags
|
|
@@ -489,14 +535,72 @@ export class EInvoice implements TInvoice {
|
|
|
489
535
|
// Update validation status
|
|
490
536
|
this.validationErrors = result.errors;
|
|
491
537
|
result.valid = result.errors.length === 0 || options?.reportOnly === true;
|
|
538
|
+
|
|
539
|
+
if (cacheKey) {
|
|
540
|
+
this.validationCache.set(cacheKey, this.cloneValidationResult(result));
|
|
541
|
+
}
|
|
492
542
|
|
|
493
543
|
return result;
|
|
494
544
|
} catch (error) {
|
|
545
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
495
546
|
if (error instanceof EInvoiceError) {
|
|
496
547
|
throw error;
|
|
497
548
|
}
|
|
498
|
-
throw new EInvoiceValidationError(`Validation failed: ${
|
|
549
|
+
throw new EInvoiceValidationError(`Validation failed: ${errorMessage}`, [], { validationLevel: level });
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
private validateDecodedInvoice(level: ValidationLevel): ValidationResult {
|
|
554
|
+
const invoice = this.mapToTInvoice();
|
|
555
|
+
const errors = EN16931Validator.collectMandatoryFieldErrors(invoice).map(message =>
|
|
556
|
+
this.createValidationError(message)
|
|
557
|
+
);
|
|
558
|
+
|
|
559
|
+
return {
|
|
560
|
+
valid: errors.length === 0,
|
|
561
|
+
errors,
|
|
562
|
+
warnings: level === ValidationLevel.SYNTAX
|
|
563
|
+
? [{
|
|
564
|
+
code: 'VAL-NO-XML',
|
|
565
|
+
message: 'Syntax validation was skipped because no XML document has been loaded.'
|
|
566
|
+
}]
|
|
567
|
+
: [],
|
|
568
|
+
level: level
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
private shouldUseFastBusinessValidation(level: ValidationLevel, options?: ValidationOptions): boolean {
|
|
573
|
+
if (level !== ValidationLevel.BUSINESS) {
|
|
574
|
+
return false;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (options?.featureFlags?.length || options?.reportOnly || this.detectedFormat !== InvoiceFormat.UBL) {
|
|
578
|
+
return false;
|
|
499
579
|
}
|
|
580
|
+
|
|
581
|
+
// For tiny plain-UBL documents without parties or lines, the decoded invoice model already
|
|
582
|
+
// contains everything needed for the mandatory-field failures the XML validator would report.
|
|
583
|
+
return this.items.length === 0 && !this.from?.name && !this.to?.name;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
private getValidationCacheKey(level: ValidationLevel, options?: ValidationOptions): string | undefined {
|
|
587
|
+
if (!this.xmlString || options) {
|
|
588
|
+
return undefined;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
if (level !== ValidationLevel.SYNTAX && level !== ValidationLevel.SEMANTIC) {
|
|
592
|
+
return undefined;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
return `xml:${level}`;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
private cloneValidationResult(result: ValidationResult): ValidationResult {
|
|
599
|
+
return {
|
|
600
|
+
...result,
|
|
601
|
+
errors: result.errors.map(error => ({ ...error })),
|
|
602
|
+
warnings: result.warnings?.map(warning => ({ ...warning }))
|
|
603
|
+
};
|
|
500
604
|
}
|
|
501
605
|
|
|
502
606
|
/**
|
|
@@ -514,7 +618,8 @@ export class EInvoice implements TInvoice {
|
|
|
514
618
|
}
|
|
515
619
|
return embedResult.data! as Buffer;
|
|
516
620
|
} catch (error) {
|
|
517
|
-
|
|
621
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
622
|
+
throw new EInvoicePDFError(`Failed to embed XML in PDF: ${errorMessage}`, 'embed', { format }, error as Error);
|
|
518
623
|
}
|
|
519
624
|
}
|
|
520
625
|
|
|
@@ -546,10 +651,11 @@ export class EInvoice implements TInvoice {
|
|
|
546
651
|
await plugins.fs.writeFile(filePath, xmlString, 'utf-8');
|
|
547
652
|
}
|
|
548
653
|
} catch (error) {
|
|
654
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
549
655
|
if (error instanceof EInvoiceError) {
|
|
550
656
|
throw error;
|
|
551
657
|
}
|
|
552
|
-
throw new EInvoiceError(`Failed to save file: ${
|
|
658
|
+
throw new EInvoiceError(`Failed to save file: ${errorMessage}`, 'FILE_SAVE_ERROR', { filePath });
|
|
553
659
|
}
|
|
554
660
|
}
|
|
555
661
|
|
|
@@ -649,4 +755,20 @@ export class EInvoice implements TInvoice {
|
|
|
649
755
|
public addItem(item: Partial<TAccountingDocItem>): void {
|
|
650
756
|
this.items.push(this.createItem(item));
|
|
651
757
|
}
|
|
652
|
-
|
|
758
|
+
|
|
759
|
+
private createValidationError(message: string): ValidationError {
|
|
760
|
+
const match = message.match(/^([A-Z]{2,}(?:-[A-Z0-9]+)*-\d+):\s*(.+)$/);
|
|
761
|
+
|
|
762
|
+
if (match) {
|
|
763
|
+
return {
|
|
764
|
+
code: match[1],
|
|
765
|
+
message: match[2]
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
return {
|
|
770
|
+
code: 'VALIDATION_ERROR',
|
|
771
|
+
message
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
}
|
|
@@ -37,6 +37,13 @@ export abstract class BaseDecoder {
|
|
|
37
37
|
return this.xml;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Gets a parsed XML document when a decoder keeps one around.
|
|
42
|
+
*/
|
|
43
|
+
public getParsedDocument(): Document | undefined {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
40
47
|
/**
|
|
41
48
|
* Parses a CII date string based on format code
|
|
42
49
|
* @param dateStr Date string
|
|
@@ -3,6 +3,14 @@ import type { TInvoice, TCreditNote, TDebitNote } from '../../interfaces/common.
|
|
|
3
3
|
import { CII_NAMESPACES, CIIProfile } from './cii.types.js';
|
|
4
4
|
import { DOMParser, xpath } from '../../plugins.js';
|
|
5
5
|
|
|
6
|
+
const ciiParser = new DOMParser();
|
|
7
|
+
const ciiNamespaces = {
|
|
8
|
+
rsm: CII_NAMESPACES.RSM,
|
|
9
|
+
ram: CII_NAMESPACES.RAM,
|
|
10
|
+
udt: CII_NAMESPACES.UDT,
|
|
11
|
+
};
|
|
12
|
+
const ciiSelect = xpath.useNamespaces(ciiNamespaces);
|
|
13
|
+
|
|
6
14
|
/**
|
|
7
15
|
* Base decoder for CII-based invoice formats
|
|
8
16
|
*/
|
|
@@ -16,22 +24,22 @@ export abstract class CIIBaseDecoder extends BaseDecoder {
|
|
|
16
24
|
super(xml, skipValidation);
|
|
17
25
|
|
|
18
26
|
// Parse XML document
|
|
19
|
-
this.doc =
|
|
27
|
+
this.doc = ciiParser.parseFromString(xml, 'application/xml');
|
|
20
28
|
|
|
21
29
|
// Set up namespaces for XPath queries
|
|
22
|
-
this.namespaces =
|
|
23
|
-
rsm: CII_NAMESPACES.RSM,
|
|
24
|
-
ram: CII_NAMESPACES.RAM,
|
|
25
|
-
udt: CII_NAMESPACES.UDT
|
|
26
|
-
};
|
|
30
|
+
this.namespaces = ciiNamespaces;
|
|
27
31
|
|
|
28
32
|
// Create XPath selector with namespaces
|
|
29
|
-
this.select =
|
|
33
|
+
this.select = ciiSelect;
|
|
30
34
|
|
|
31
35
|
// Detect profile
|
|
32
36
|
this.detectProfile();
|
|
33
37
|
}
|
|
34
38
|
|
|
39
|
+
public override getParsedDocument(): Document {
|
|
40
|
+
return this.doc;
|
|
41
|
+
}
|
|
42
|
+
|
|
35
43
|
/**
|
|
36
44
|
* Decodes CII XML into a TInvoice object
|
|
37
45
|
* @returns Promise resolving to a TInvoice object
|
|
@@ -93,7 +101,8 @@ export abstract class CIIBaseDecoder extends BaseDecoder {
|
|
|
93
101
|
* @returns Text value or empty string if not found
|
|
94
102
|
*/
|
|
95
103
|
protected getText(xpathExpr: string, context?: Node): string {
|
|
96
|
-
const
|
|
104
|
+
const result = this.select(xpathExpr, context || this.doc);
|
|
105
|
+
const node = Array.isArray(result) ? result[0] : null;
|
|
97
106
|
return node ? (node.textContent || '') : '';
|
|
98
107
|
}
|
|
99
108
|
|
|
@@ -4,31 +4,35 @@ import type { ValidationResult } from '../../interfaces/common.js';
|
|
|
4
4
|
import { CII_NAMESPACES, CIIProfile } from './cii.types.js';
|
|
5
5
|
import { DOMParser, xpath } from '../../plugins.js';
|
|
6
6
|
|
|
7
|
+
const ciiValidatorParser = new DOMParser();
|
|
8
|
+
const ciiValidatorNamespaces = {
|
|
9
|
+
rsm: CII_NAMESPACES.RSM,
|
|
10
|
+
ram: CII_NAMESPACES.RAM,
|
|
11
|
+
udt: CII_NAMESPACES.UDT,
|
|
12
|
+
};
|
|
13
|
+
const ciiValidatorSelect = xpath.useNamespaces(ciiValidatorNamespaces);
|
|
14
|
+
|
|
7
15
|
/**
|
|
8
16
|
* Base validator for CII-based invoice formats
|
|
9
17
|
*/
|
|
10
18
|
export abstract class CIIBaseValidator extends BaseValidator {
|
|
11
|
-
protected doc
|
|
12
|
-
protected namespaces
|
|
13
|
-
protected select
|
|
19
|
+
protected doc!: Document;
|
|
20
|
+
protected namespaces!: Record<string, string>;
|
|
21
|
+
protected select!: xpath.XPathSelect;
|
|
14
22
|
protected profile: CIIProfile = CIIProfile.EN16931;
|
|
15
23
|
|
|
16
|
-
constructor(xml: string) {
|
|
24
|
+
constructor(xml: string, doc?: Document) {
|
|
17
25
|
super(xml);
|
|
18
26
|
|
|
19
27
|
try {
|
|
20
|
-
//
|
|
21
|
-
this.doc =
|
|
28
|
+
// Reuse an existing parsed document when available.
|
|
29
|
+
this.doc = doc ?? ciiValidatorParser.parseFromString(xml, 'application/xml');
|
|
22
30
|
|
|
23
31
|
// Set up namespaces for XPath queries
|
|
24
|
-
this.namespaces =
|
|
25
|
-
rsm: CII_NAMESPACES.RSM,
|
|
26
|
-
ram: CII_NAMESPACES.RAM,
|
|
27
|
-
udt: CII_NAMESPACES.UDT
|
|
28
|
-
};
|
|
32
|
+
this.namespaces = ciiValidatorNamespaces;
|
|
29
33
|
|
|
30
34
|
// Create XPath selector with namespaces
|
|
31
|
-
this.select =
|
|
35
|
+
this.select = ciiValidatorSelect;
|
|
32
36
|
|
|
33
37
|
// Detect profile
|
|
34
38
|
this.detectProfile();
|
|
@@ -139,7 +143,8 @@ export abstract class CIIBaseValidator extends BaseValidator {
|
|
|
139
143
|
* @returns Text value or empty string if not found
|
|
140
144
|
*/
|
|
141
145
|
protected getText(xpathExpr: string, context?: Node): string {
|
|
142
|
-
const
|
|
146
|
+
const result = this.select(xpathExpr, context || this.doc);
|
|
147
|
+
const node = Array.isArray(result) ? result[0] : null;
|
|
143
148
|
return node ? (node.textContent || '') : '';
|
|
144
149
|
}
|
|
145
150
|
|
|
@@ -6,17 +6,16 @@
|
|
|
6
6
|
import * as plugins from '../../plugins.js';
|
|
7
7
|
import type { EInvoice } from '../../einvoice.js';
|
|
8
8
|
import type { TAccountingDocItem } from '@tsclass/tsclass/dist_ts/finance/index.js';
|
|
9
|
-
import { DOMParser } from '@xmldom/xmldom';
|
|
10
9
|
|
|
11
10
|
/**
|
|
12
11
|
* Converter for XML formats to EInvoice - simplified version
|
|
13
12
|
* This is a basic converter that extracts essential fields for testing
|
|
14
13
|
*/
|
|
15
14
|
export class XMLToEInvoiceConverter {
|
|
16
|
-
private parser: DOMParser
|
|
15
|
+
private parser: InstanceType<typeof plugins.DOMParser>;
|
|
17
16
|
|
|
18
17
|
constructor() {
|
|
19
|
-
this.parser = new DOMParser();
|
|
18
|
+
this.parser = new plugins.DOMParser();
|
|
20
19
|
}
|
|
21
20
|
|
|
22
21
|
/**
|
|
@@ -116,7 +115,7 @@ export class XMLToEInvoiceConverter {
|
|
|
116
115
|
console.warn('Error parsing XML:', error);
|
|
117
116
|
}
|
|
118
117
|
|
|
119
|
-
return mockInvoice as EInvoice;
|
|
118
|
+
return mockInvoice as unknown as EInvoice;
|
|
120
119
|
}
|
|
121
120
|
|
|
122
121
|
/**
|
|
@@ -139,4 +138,4 @@ export class XMLToEInvoiceConverter {
|
|
|
139
138
|
|
|
140
139
|
return null;
|
|
141
140
|
}
|
|
142
|
-
}
|
|
141
|
+
}
|
|
@@ -16,10 +16,15 @@ export class DecoderFactory {
|
|
|
16
16
|
* Creates a decoder for the specified XML content
|
|
17
17
|
* @param xml XML content to decode
|
|
18
18
|
* @param skipValidation Whether to skip EN16931 validation
|
|
19
|
+
* @param formatHint Optional pre-detected format to avoid re-detecting large XML inputs
|
|
19
20
|
* @returns Appropriate decoder instance
|
|
20
21
|
*/
|
|
21
|
-
public static createDecoder(
|
|
22
|
-
|
|
22
|
+
public static createDecoder(
|
|
23
|
+
xml: string,
|
|
24
|
+
skipValidation: boolean = false,
|
|
25
|
+
formatHint?: InvoiceFormat,
|
|
26
|
+
): BaseDecoder {
|
|
27
|
+
const format = formatHint ?? FormatDetector.detectFormat(xml);
|
|
23
28
|
|
|
24
29
|
switch (format) {
|
|
25
30
|
case InvoiceFormat.UBL:
|
|
@@ -59,28 +59,34 @@ export class ValidatorFactory {
|
|
|
59
59
|
/**
|
|
60
60
|
* Creates a validator for the specified XML content
|
|
61
61
|
* @param xml XML content to validate
|
|
62
|
+
* @param formatHint Optional pre-detected format to avoid re-detecting large XML inputs
|
|
63
|
+
* @param parsedDocument Optional parsed XML document to avoid parsing twice
|
|
62
64
|
* @returns Appropriate validator instance
|
|
63
65
|
*/
|
|
64
|
-
public static createValidator(
|
|
66
|
+
public static createValidator(
|
|
67
|
+
xml: string,
|
|
68
|
+
formatHint?: InvoiceFormat,
|
|
69
|
+
parsedDocument?: Document,
|
|
70
|
+
): BaseValidator {
|
|
65
71
|
try {
|
|
66
|
-
const format = FormatDetector.detectFormat(xml);
|
|
72
|
+
const format = formatHint ?? FormatDetector.detectFormat(xml);
|
|
67
73
|
|
|
68
74
|
switch (format) {
|
|
69
75
|
case InvoiceFormat.UBL:
|
|
70
|
-
return new EN16931UBLValidator(xml);
|
|
76
|
+
return new EN16931UBLValidator(xml, parsedDocument);
|
|
71
77
|
|
|
72
78
|
case InvoiceFormat.XRECHNUNG:
|
|
73
|
-
return new XRechnungValidator(xml);
|
|
79
|
+
return new XRechnungValidator(xml, parsedDocument);
|
|
74
80
|
|
|
75
81
|
case InvoiceFormat.CII:
|
|
76
82
|
// For now, use Factur-X validator for generic CII
|
|
77
|
-
return new FacturXValidator(xml);
|
|
83
|
+
return new FacturXValidator(xml, parsedDocument);
|
|
78
84
|
|
|
79
85
|
case InvoiceFormat.ZUGFERD:
|
|
80
|
-
return new ZUGFeRDValidator(xml);
|
|
86
|
+
return new ZUGFeRDValidator(xml, parsedDocument);
|
|
81
87
|
|
|
82
88
|
case InvoiceFormat.FACTURX:
|
|
83
|
-
return new FacturXValidator(xml);
|
|
89
|
+
return new FacturXValidator(xml, parsedDocument);
|
|
84
90
|
|
|
85
91
|
case InvoiceFormat.FATTURAPA:
|
|
86
92
|
return new FatturaPAValidator(xml);
|
|
@@ -131,4 +137,4 @@ class GenericValidator extends BaseValidator {
|
|
|
131
137
|
protected validateBusinessRules(): boolean {
|
|
132
138
|
return false;
|
|
133
139
|
}
|
|
134
|
-
}
|
|
140
|
+
}
|
|
@@ -169,17 +169,45 @@ export class SemanticModelAdapter {
|
|
|
169
169
|
}
|
|
170
170
|
|
|
171
171
|
// Set payment options
|
|
172
|
-
if (
|
|
172
|
+
if (
|
|
173
|
+
model.paymentInstructions.paymentAccountIdentifier ||
|
|
174
|
+
model.paymentInstructions.paymentMeansText ||
|
|
175
|
+
model.paymentInstructions.paymentServiceProviderIdentifier
|
|
176
|
+
) {
|
|
173
177
|
invoice.paymentOptions = {
|
|
174
|
-
|
|
178
|
+
description: model.paymentInstructions.paymentMeansText,
|
|
179
|
+
sepaConnection: {
|
|
180
|
+
iban: model.paymentInstructions.paymentAccountIdentifier || '',
|
|
181
|
+
bic: model.paymentInstructions.paymentServiceProviderIdentifier || '',
|
|
182
|
+
institution: model.paymentInstructions.paymentServiceProviderIdentifier
|
|
183
|
+
},
|
|
184
|
+
payPal: { email: '' }
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
invoice.metadata = {
|
|
188
|
+
...invoice.metadata,
|
|
189
|
+
paymentMeansCode: model.paymentInstructions.paymentMeansTypeCode,
|
|
190
|
+
paymentAccount: {
|
|
175
191
|
iban: model.paymentInstructions.paymentAccountIdentifier,
|
|
176
|
-
|
|
192
|
+
accountName: model.paymentInstructions.paymentAccountName,
|
|
193
|
+
bankId: model.paymentInstructions.paymentServiceProviderIdentifier
|
|
177
194
|
},
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
195
|
+
extensions: {
|
|
196
|
+
...invoice.metadata?.extensions,
|
|
197
|
+
paymentMeans: {
|
|
198
|
+
paymentMeansCode: model.paymentInstructions.paymentMeansTypeCode,
|
|
199
|
+
paymentMeansText: model.paymentInstructions.paymentMeansText,
|
|
200
|
+
remittanceInformation: model.paymentInstructions.remittanceInformation
|
|
201
|
+
},
|
|
202
|
+
paymentAccount: {
|
|
203
|
+
iban: model.paymentInstructions.paymentAccountIdentifier,
|
|
204
|
+
accountName: model.paymentInstructions.paymentAccountName,
|
|
205
|
+
bankId: model.paymentInstructions.paymentServiceProviderIdentifier,
|
|
206
|
+
bic: model.paymentInstructions.paymentServiceProviderIdentifier,
|
|
207
|
+
institutionName: model.paymentInstructions.paymentServiceProviderIdentifier
|
|
208
|
+
}
|
|
181
209
|
}
|
|
182
|
-
}
|
|
210
|
+
};
|
|
183
211
|
}
|
|
184
212
|
|
|
185
213
|
// Set extensions
|
|
@@ -376,15 +404,21 @@ export class SemanticModelAdapter {
|
|
|
376
404
|
*/
|
|
377
405
|
private mapPaymentInstructions(invoice: EInvoice): PaymentInstructions {
|
|
378
406
|
const paymentMeans = invoice.metadata?.extensions?.paymentMeans;
|
|
379
|
-
const paymentAccount = invoice.metadata?.extensions?.paymentAccount;
|
|
407
|
+
const paymentAccount = invoice.metadata?.extensions?.paymentAccount || invoice.metadata?.paymentAccount;
|
|
408
|
+
const sepaConnection = invoice.paymentOptions?.sepaConnection;
|
|
380
409
|
|
|
381
410
|
return {
|
|
382
|
-
paymentMeansTypeCode: paymentMeans?.paymentMeansCode || '30',
|
|
383
|
-
paymentMeansText: paymentMeans?.paymentMeansText,
|
|
411
|
+
paymentMeansTypeCode: paymentMeans?.paymentMeansCode || invoice.metadata?.paymentMeansCode || '30',
|
|
412
|
+
paymentMeansText: paymentMeans?.paymentMeansText || invoice.paymentOptions?.description,
|
|
384
413
|
remittanceInformation: paymentMeans?.remittanceInformation,
|
|
385
|
-
paymentAccountIdentifier: paymentAccount?.iban,
|
|
414
|
+
paymentAccountIdentifier: paymentAccount?.iban || sepaConnection?.iban,
|
|
386
415
|
paymentAccountName: paymentAccount?.accountName,
|
|
387
|
-
paymentServiceProviderIdentifier:
|
|
416
|
+
paymentServiceProviderIdentifier:
|
|
417
|
+
paymentAccount?.bic ||
|
|
418
|
+
paymentAccount?.institutionName ||
|
|
419
|
+
paymentAccount?.bankId ||
|
|
420
|
+
sepaConnection?.bic ||
|
|
421
|
+
sepaConnection?.institution
|
|
388
422
|
};
|
|
389
423
|
}
|
|
390
424
|
|
|
@@ -397,10 +431,10 @@ export class SemanticModelAdapter {
|
|
|
397
431
|
taxExclusiveAmount: invoice.totalNet,
|
|
398
432
|
taxInclusiveAmount: invoice.totalGross,
|
|
399
433
|
allowanceTotalAmount: invoice.metadata?.extensions?.documentAllowances?.reduce(
|
|
400
|
-
(sum, a) => sum + a.amount, 0
|
|
434
|
+
(sum: number, a: { amount: number }) => sum + a.amount, 0
|
|
401
435
|
),
|
|
402
436
|
chargeTotalAmount: invoice.metadata?.extensions?.documentCharges?.reduce(
|
|
403
|
-
(sum, c) => sum + c.amount, 0
|
|
437
|
+
(sum: number, c: { amount: number }) => sum + c.amount, 0
|
|
404
438
|
),
|
|
405
439
|
prepaidAmount: invoice.metadata?.extensions?.prepaidAmount,
|
|
406
440
|
roundingAmount: invoice.metadata?.extensions?.roundingAmount,
|
|
@@ -597,4 +631,4 @@ export class SemanticModelAdapter {
|
|
|
597
631
|
|
|
598
632
|
return errors;
|
|
599
633
|
}
|
|
600
|
-
}
|
|
634
|
+
}
|
|
@@ -90,7 +90,8 @@ export class EN16931UBLValidator extends UBLBaseValidator {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
// BR-08: An Invoice shall contain the Seller postal address (BG-5).
|
|
93
|
-
const
|
|
93
|
+
const sellerAddressResult = this.select('//cac:AccountingSupplierParty//cac:PostalAddress', this.doc);
|
|
94
|
+
const sellerAddress = Array.isArray(sellerAddressResult) ? sellerAddressResult[0] : null;
|
|
94
95
|
if (!sellerAddress || !this.exists('.//cbc:IdentificationCode', sellerAddress)) {
|
|
95
96
|
this.addError('BR-08', 'An Invoice shall contain the Seller postal address', '//cac:AccountingSupplierParty//cac:PostalAddress');
|
|
96
97
|
valid = false;
|
|
@@ -103,7 +104,8 @@ export class EN16931UBLValidator extends UBLBaseValidator {
|
|
|
103
104
|
}
|
|
104
105
|
|
|
105
106
|
// BR-10: An Invoice shall contain the Buyer postal address (BG-8).
|
|
106
|
-
const
|
|
107
|
+
const buyerAddressResult = this.select('//cac:AccountingCustomerParty//cac:PostalAddress', this.doc);
|
|
108
|
+
const buyerAddress = Array.isArray(buyerAddressResult) ? buyerAddressResult[0] : null;
|
|
107
109
|
if (!buyerAddress || !this.exists('.//cbc:IdentificationCode', buyerAddress)) {
|
|
108
110
|
this.addError('BR-10', 'An Invoice shall contain the Buyer postal address', '//cac:AccountingCustomerParty//cac:PostalAddress');
|
|
109
111
|
valid = false;
|
|
@@ -213,4 +215,4 @@ export class EN16931UBLValidator extends UBLBaseValidator {
|
|
|
213
215
|
|
|
214
216
|
return valid;
|
|
215
217
|
}
|
|
216
|
-
}
|
|
218
|
+
}
|