@fin.cx/einvoice 5.1.4 → 5.2.1
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/.smartconfig.json +59 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/einvoice.d.ts +14 -5
- package/dist_ts/einvoice.js +173 -66
- 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/cii/facturx/facturx.encoder.js +25 -16
- 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/associated.extractor.js +3 -2
- package/dist_ts/formats/pdf/extractors/base.extractor.d.ts +1 -1
- package/dist_ts/formats/pdf/extractors/base.extractor.js +50 -52
- package/dist_ts/formats/pdf/extractors/standard.extractor.js +3 -2
- package/dist_ts/formats/pdf/extractors/text.extractor.js +19 -22
- package/dist_ts/formats/pdf/pdf.embedder.js +3 -2
- package/dist_ts/formats/pdf/pdf.extractor.d.ts +2 -1
- package/dist_ts/formats/pdf/pdf.extractor.js +20 -10
- 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.d.ts +0 -1
- package/dist_ts/formats/validation/schematron.downloader.js +12 -7
- package/dist_ts/formats/validation/schematron.integration.js +16 -6
- package/dist_ts/formats/validation/schematron.validator.d.ts +9 -11
- package/dist_ts/formats/validation/schematron.validator.js +70 -72
- package/dist_ts/formats/validation/schematron.worker.d.ts +7 -6
- package/dist_ts/formats/validation/schematron.worker.js +48 -42
- 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 -6
- package/dist_ts/plugins.js +3 -12
- package/dist_ts/vendor/pako.d.ts +5 -0
- package/dist_ts/vendor/pako.js +3 -0
- package/dist_ts/vendor/pdflib.d.ts +2 -0
- package/dist_ts/vendor/pdflib.js +9 -0
- package/dist_ts/vendor/saxonjs.d.ts +23 -0
- package/dist_ts/vendor/saxonjs.js +9 -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 +21 -15
- package/readme.hints.md +24 -8
- package/readme.md +195 -328
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/einvoice.ts +156 -33
- 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/cii/facturx/facturx.encoder.ts +25 -15
- 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/pdf/extractors/associated.extractor.ts +2 -1
- package/ts/formats/pdf/extractors/base.extractor.ts +3 -2
- package/ts/formats/pdf/extractors/standard.extractor.ts +2 -1
- package/ts/formats/pdf/pdf.embedder.ts +6 -3
- package/ts/formats/pdf/pdf.extractor.ts +24 -16
- 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 -8
- package/ts/formats/validation/schematron.integration.ts +11 -5
- package/ts/formats/validation/schematron.validator.ts +79 -79
- package/ts/formats/validation/schematron.worker.ts +59 -41
- package/ts/index.ts +2 -1
- package/ts/interfaces/common.ts +1 -3
- package/ts/plugins.ts +2 -29
- package/ts/readme.md +76 -0
- package/ts/vendor/modules.d.ts +32 -0
- package/ts/vendor/pako.ts +8 -0
- package/ts/vendor/pdflib.ts +12 -0
- package/ts/vendor/saxonjs.ts +36 -0
- package/ts/vendor/xmldom.ts +27 -0
- package/npmextra.json +0 -35
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';
|
|
@@ -20,14 +20,11 @@ import { DecoderFactory } from './formats/factories/decoder.factory.js';
|
|
|
20
20
|
import { EncoderFactory } from './formats/factories/encoder.factory.js';
|
|
21
21
|
import { ValidatorFactory } from './formats/factories/validator.factory.js';
|
|
22
22
|
|
|
23
|
-
// Import PDF utilities
|
|
24
|
-
import { PDFEmbedder } from './formats/pdf/pdf.embedder.js';
|
|
25
|
-
import { PDFExtractor } from './formats/pdf/pdf.extractor.js';
|
|
26
|
-
|
|
27
23
|
// Import format detector
|
|
28
24
|
import { FormatDetector } from './formats/utils/format.detector.js';
|
|
29
25
|
|
|
30
26
|
// Import enhanced validators
|
|
27
|
+
import { EN16931Validator } from './formats/validation/en16931.validator.js';
|
|
31
28
|
import { EN16931BusinessRulesValidator } from './formats/validation/en16931.business-rules.validator.js';
|
|
32
29
|
import { CodeListValidator } from './formats/validation/codelist.validator.js';
|
|
33
30
|
import type { ValidationOptions } from './formats/validation/validation.types.js';
|
|
@@ -41,6 +38,9 @@ import type { IEInvoiceMetadata } from './interfaces/en16931-metadata.js';
|
|
|
41
38
|
* Extends the TInvoice interface for seamless integration with existing systems
|
|
42
39
|
*/
|
|
43
40
|
export class EInvoice implements TInvoice {
|
|
41
|
+
private static sharedPdfEmbedder?: import('./formats/pdf/pdf.embedder.js').PDFEmbedder;
|
|
42
|
+
private static sharedPdfExtractor?: import('./formats/pdf/pdf.extractor.js').PDFExtractor;
|
|
43
|
+
|
|
44
44
|
/**
|
|
45
45
|
* Creates an EInvoice instance from XML string
|
|
46
46
|
* @param xmlString XML string to parse
|
|
@@ -75,7 +75,8 @@ export class EInvoice implements TInvoice {
|
|
|
75
75
|
await invoice.fromPdfFile(pdfBuffer);
|
|
76
76
|
} else {
|
|
77
77
|
// If given a buffer, extract XML and parse it
|
|
78
|
-
const
|
|
78
|
+
const pdfExtractor = await invoice.getPdfExtractor();
|
|
79
|
+
const extractResult = await pdfExtractor.extractXml(pdfBuffer);
|
|
79
80
|
if (!extractResult.success || !extractResult.xml) {
|
|
80
81
|
throw new EInvoicePDFError('No invoice XML found in PDF', 'extract');
|
|
81
82
|
}
|
|
@@ -109,8 +110,8 @@ export class EInvoice implements TInvoice {
|
|
|
109
110
|
public incidenceId: string = '';
|
|
110
111
|
public language: string = 'en';
|
|
111
112
|
public objectActions: any[] = [];
|
|
112
|
-
public pdf
|
|
113
|
-
public pdfAttachments
|
|
113
|
+
public pdf?: IPdf;
|
|
114
|
+
public pdfAttachments?: IPdf[];
|
|
114
115
|
public accentColor: string | null = null;
|
|
115
116
|
public logoUrl: string | null = null;
|
|
116
117
|
|
|
@@ -149,7 +150,13 @@ export class EInvoice implements TInvoice {
|
|
|
149
150
|
this.accountingDocType === 'creditnote' ? 'creditnote' : 'debitnote';
|
|
150
151
|
}
|
|
151
152
|
public set invoiceType(value: 'invoice' | 'creditnote' | 'debitnote') {
|
|
152
|
-
|
|
153
|
+
if (value !== 'invoice') {
|
|
154
|
+
throw new EInvoiceFormatError(
|
|
155
|
+
`Unsupported invoice type: ${value}`,
|
|
156
|
+
{ unsupportedFeatures: [`invoiceType=${value}`] }
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
this.accountingDocType = 'invoice';
|
|
153
160
|
}
|
|
154
161
|
|
|
155
162
|
// Computed properties for convenience
|
|
@@ -181,15 +188,30 @@ export class EInvoice implements TInvoice {
|
|
|
181
188
|
|
|
182
189
|
private xmlString: string = '';
|
|
183
190
|
private detectedFormat: InvoiceFormat = InvoiceFormat.UNKNOWN;
|
|
191
|
+
private parsedXmlDocument?: Document;
|
|
184
192
|
private validationErrors: ValidationError[] = [];
|
|
193
|
+
private validationCache = new Map<string, ValidationResult>();
|
|
185
194
|
private options: EInvoiceOptions = {
|
|
186
195
|
validateOnLoad: false,
|
|
187
196
|
validationLevel: ValidationLevel.SYNTAX
|
|
188
197
|
};
|
|
189
198
|
|
|
190
|
-
// PDF utilities
|
|
191
|
-
private
|
|
192
|
-
|
|
199
|
+
// PDF utilities are created lazily because most invoice workflows never touch PDF I/O.
|
|
200
|
+
private async getPdfEmbedder(): Promise<import('./formats/pdf/pdf.embedder.js').PDFEmbedder> {
|
|
201
|
+
if (!EInvoice.sharedPdfEmbedder) {
|
|
202
|
+
const { PDFEmbedder } = await import('./formats/pdf/pdf.embedder.js');
|
|
203
|
+
EInvoice.sharedPdfEmbedder = new PDFEmbedder();
|
|
204
|
+
}
|
|
205
|
+
return EInvoice.sharedPdfEmbedder;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private async getPdfExtractor(): Promise<import('./formats/pdf/pdf.extractor.js').PDFExtractor> {
|
|
209
|
+
if (!EInvoice.sharedPdfExtractor) {
|
|
210
|
+
const { PDFExtractor } = await import('./formats/pdf/pdf.extractor.js');
|
|
211
|
+
EInvoice.sharedPdfExtractor = new PDFExtractor();
|
|
212
|
+
}
|
|
213
|
+
return EInvoice.sharedPdfExtractor;
|
|
214
|
+
}
|
|
193
215
|
|
|
194
216
|
/**
|
|
195
217
|
* Creates a new EInvoice instance
|
|
@@ -260,6 +282,7 @@ export class EInvoice implements TInvoice {
|
|
|
260
282
|
*/
|
|
261
283
|
public async fromXmlString(xmlString: string): Promise<EInvoice> {
|
|
262
284
|
try {
|
|
285
|
+
this.validationCache.clear();
|
|
263
286
|
this.xmlString = xmlString;
|
|
264
287
|
|
|
265
288
|
// Detect format
|
|
@@ -269,8 +292,13 @@ export class EInvoice implements TInvoice {
|
|
|
269
292
|
}
|
|
270
293
|
|
|
271
294
|
// Get appropriate decoder
|
|
272
|
-
const decoder = DecoderFactory.createDecoder(
|
|
295
|
+
const decoder = DecoderFactory.createDecoder(
|
|
296
|
+
xmlString,
|
|
297
|
+
!this.options.validateOnLoad,
|
|
298
|
+
this.detectedFormat,
|
|
299
|
+
);
|
|
273
300
|
const invoice = await decoder.decode();
|
|
301
|
+
this.parsedXmlDocument = decoder.getParsedDocument();
|
|
274
302
|
|
|
275
303
|
// Map the decoded invoice to our properties
|
|
276
304
|
this.mapFromTInvoice(invoice);
|
|
@@ -282,10 +310,11 @@ export class EInvoice implements TInvoice {
|
|
|
282
310
|
|
|
283
311
|
return this;
|
|
284
312
|
} catch (error) {
|
|
313
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
285
314
|
if (error instanceof EInvoiceError) {
|
|
286
315
|
throw error;
|
|
287
316
|
}
|
|
288
|
-
throw new EInvoiceParsingError(`Failed to parse XML: ${
|
|
317
|
+
throw new EInvoiceParsingError(`Failed to parse XML: ${errorMessage}`, {}, error as Error);
|
|
289
318
|
}
|
|
290
319
|
}
|
|
291
320
|
|
|
@@ -307,7 +336,8 @@ export class EInvoice implements TInvoice {
|
|
|
307
336
|
const xmlString = fileBuffer.toString('utf-8');
|
|
308
337
|
return this.fromXmlString(xmlString);
|
|
309
338
|
} catch (error) {
|
|
310
|
-
|
|
339
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
340
|
+
throw new EInvoiceError(`Failed to load file: ${errorMessage}`, 'FILE_LOAD_ERROR', { filePath });
|
|
311
341
|
}
|
|
312
342
|
}
|
|
313
343
|
|
|
@@ -319,7 +349,8 @@ export class EInvoice implements TInvoice {
|
|
|
319
349
|
public async fromPdfFile(filePath: string): Promise<EInvoice> {
|
|
320
350
|
try {
|
|
321
351
|
const pdfBuffer = await plugins.fs.readFile(filePath);
|
|
322
|
-
const
|
|
352
|
+
const pdfExtractor = await this.getPdfExtractor();
|
|
353
|
+
const extractResult = await pdfExtractor.extractXml(pdfBuffer);
|
|
323
354
|
const extractedXml = extractResult.success ? extractResult.xml : null;
|
|
324
355
|
|
|
325
356
|
if (!extractedXml) {
|
|
@@ -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,16 +535,74 @@ 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 });
|
|
499
550
|
}
|
|
500
551
|
}
|
|
501
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;
|
|
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
|
+
};
|
|
604
|
+
}
|
|
605
|
+
|
|
502
606
|
/**
|
|
503
607
|
* Embeds the invoice XML into a PDF
|
|
504
608
|
* @param pdfBuffer The PDF buffer to embed into
|
|
@@ -508,13 +612,15 @@ export class EInvoice implements TInvoice {
|
|
|
508
612
|
public async embedInPdf(pdfBuffer: Buffer, format: ExportFormat = 'facturx'): Promise<Buffer> {
|
|
509
613
|
try {
|
|
510
614
|
const xmlString = await this.toXmlString(format);
|
|
511
|
-
const
|
|
615
|
+
const pdfEmbedder = await this.getPdfEmbedder();
|
|
616
|
+
const embedResult = await pdfEmbedder.embedXml(pdfBuffer, xmlString, 'invoice.xml', `${format} Invoice`);
|
|
512
617
|
if (!embedResult.success) {
|
|
513
618
|
throw new EInvoicePDFError('Failed to embed XML in PDF', 'embed', { format });
|
|
514
619
|
}
|
|
515
620
|
return embedResult.data! as Buffer;
|
|
516
621
|
} catch (error) {
|
|
517
|
-
|
|
622
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
623
|
+
throw new EInvoicePDFError(`Failed to embed XML in PDF: ${errorMessage}`, 'embed', { format }, error as Error);
|
|
518
624
|
}
|
|
519
625
|
}
|
|
520
626
|
|
|
@@ -546,10 +652,11 @@ export class EInvoice implements TInvoice {
|
|
|
546
652
|
await plugins.fs.writeFile(filePath, xmlString, 'utf-8');
|
|
547
653
|
}
|
|
548
654
|
} catch (error) {
|
|
655
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
549
656
|
if (error instanceof EInvoiceError) {
|
|
550
657
|
throw error;
|
|
551
658
|
}
|
|
552
|
-
throw new EInvoiceError(`Failed to save file: ${
|
|
659
|
+
throw new EInvoiceError(`Failed to save file: ${errorMessage}`, 'FILE_SAVE_ERROR', { filePath });
|
|
553
660
|
}
|
|
554
661
|
}
|
|
555
662
|
|
|
@@ -649,4 +756,20 @@ export class EInvoice implements TInvoice {
|
|
|
649
756
|
public addItem(item: Partial<TAccountingDocItem>): void {
|
|
650
757
|
this.items.push(this.createItem(item));
|
|
651
758
|
}
|
|
652
|
-
|
|
759
|
+
|
|
760
|
+
private createValidationError(message: string): ValidationError {
|
|
761
|
+
const match = message.match(/^([A-Z]{2,}(?:-[A-Z0-9]+)*-\d+):\s*(.+)$/);
|
|
762
|
+
|
|
763
|
+
if (match) {
|
|
764
|
+
return {
|
|
765
|
+
code: match[1],
|
|
766
|
+
message: match[2]
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
return {
|
|
771
|
+
code: 'VALIDATION_ERROR',
|
|
772
|
+
message
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
}
|
|
@@ -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
|
|
|
@@ -2,6 +2,9 @@ import { CIIBaseEncoder } from '../cii.encoder.js';
|
|
|
2
2
|
import type { TInvoice, TCreditNote, TDebitNote } from '../../../interfaces/common.js';
|
|
3
3
|
import { FACTURX_PROFILE_IDS } from './facturx.types.js';
|
|
4
4
|
import { DOMParser, XMLSerializer } from '../../../plugins.js';
|
|
5
|
+
import { Decimal } from '../../utils/decimal.js';
|
|
6
|
+
import { DecimalCurrencyCalculator } from '../../utils/currency.calculator.decimal.js';
|
|
7
|
+
import { getCurrencyMinorUnits } from '../../utils/currency.utils.js';
|
|
5
8
|
|
|
6
9
|
/**
|
|
7
10
|
* Encoder for Factur-X invoice format
|
|
@@ -329,42 +332,46 @@ export class FacturXEncoder extends CIIBaseEncoder {
|
|
|
329
332
|
// Add totals
|
|
330
333
|
const monetarySummationElement = doc.createElement('ram:SpecifiedTradeSettlementHeaderMonetarySummation');
|
|
331
334
|
|
|
332
|
-
// Calculate totals
|
|
333
|
-
|
|
334
|
-
|
|
335
|
+
// Calculate totals with decimal arithmetic to avoid floating point drift.
|
|
336
|
+
const calculator = new DecimalCurrencyCalculator(invoice.currency);
|
|
337
|
+
const minorUnits = getCurrencyMinorUnits(invoice.currency);
|
|
338
|
+
let totalNetAmount = Decimal.ZERO;
|
|
339
|
+
let totalTaxAmount = Decimal.ZERO;
|
|
335
340
|
|
|
336
341
|
// Calculate from items
|
|
337
342
|
if (invoice.items) {
|
|
338
343
|
for (const item of invoice.items) {
|
|
339
|
-
const itemNetAmount = item.
|
|
340
|
-
const itemTaxAmount = itemNetAmount
|
|
344
|
+
const itemNetAmount = calculator.calculateLineNet(item.unitQuantity, item.unitNetPrice);
|
|
345
|
+
const itemTaxAmount = calculator.calculateVAT(itemNetAmount, item.vatPercentage);
|
|
341
346
|
|
|
342
|
-
totalNetAmount
|
|
343
|
-
totalTaxAmount
|
|
347
|
+
totalNetAmount = totalNetAmount.add(itemNetAmount);
|
|
348
|
+
totalTaxAmount = totalTaxAmount.add(itemTaxAmount);
|
|
344
349
|
}
|
|
345
350
|
}
|
|
346
351
|
|
|
347
|
-
|
|
352
|
+
totalNetAmount = calculator.round(totalNetAmount);
|
|
353
|
+
totalTaxAmount = calculator.round(totalTaxAmount);
|
|
354
|
+
const totalGrossAmount = calculator.calculateGrossAmount(totalNetAmount, totalTaxAmount);
|
|
348
355
|
|
|
349
356
|
// Add line total amount
|
|
350
357
|
const lineTotalElement = doc.createElement('ram:LineTotalAmount');
|
|
351
|
-
lineTotalElement.textContent = totalNetAmount.toFixed(
|
|
358
|
+
lineTotalElement.textContent = totalNetAmount.toFixed(minorUnits);
|
|
352
359
|
monetarySummationElement.appendChild(lineTotalElement);
|
|
353
360
|
|
|
354
361
|
// Add tax total amount
|
|
355
362
|
const taxTotalElement = doc.createElement('ram:TaxTotalAmount');
|
|
356
|
-
taxTotalElement.textContent = totalTaxAmount.toFixed(
|
|
363
|
+
taxTotalElement.textContent = totalTaxAmount.toFixed(minorUnits);
|
|
357
364
|
taxTotalElement.setAttribute('currencyID', invoice.currency);
|
|
358
365
|
monetarySummationElement.appendChild(taxTotalElement);
|
|
359
366
|
|
|
360
367
|
// Add grand total amount
|
|
361
368
|
const grandTotalElement = doc.createElement('ram:GrandTotalAmount');
|
|
362
|
-
grandTotalElement.textContent = totalGrossAmount.toFixed(
|
|
369
|
+
grandTotalElement.textContent = totalGrossAmount.toFixed(minorUnits);
|
|
363
370
|
monetarySummationElement.appendChild(grandTotalElement);
|
|
364
371
|
|
|
365
372
|
// Add due payable amount
|
|
366
373
|
const duePayableElement = doc.createElement('ram:DuePayableAmount');
|
|
367
|
-
duePayableElement.textContent = totalGrossAmount.toFixed(
|
|
374
|
+
duePayableElement.textContent = totalGrossAmount.toFixed(minorUnits);
|
|
368
375
|
monetarySummationElement.appendChild(duePayableElement);
|
|
369
376
|
|
|
370
377
|
settlementElement.appendChild(monetarySummationElement);
|
|
@@ -377,6 +384,9 @@ export class FacturXEncoder extends CIIBaseEncoder {
|
|
|
377
384
|
* @param invoice Invoice data
|
|
378
385
|
*/
|
|
379
386
|
private addLineItems(doc: Document, transactionElement: Element, invoice: TInvoice): void {
|
|
387
|
+
const calculator = new DecimalCurrencyCalculator(invoice.currency);
|
|
388
|
+
const minorUnits = getCurrencyMinorUnits(invoice.currency);
|
|
389
|
+
|
|
380
390
|
// Add each line item
|
|
381
391
|
if (invoice.items) {
|
|
382
392
|
for (const item of invoice.items) {
|
|
@@ -450,12 +460,12 @@ export class FacturXEncoder extends CIIBaseEncoder {
|
|
|
450
460
|
// Add monetary summation
|
|
451
461
|
const monetarySummationElement = doc.createElement('ram:SpecifiedLineTradeSettlementMonetarySummation');
|
|
452
462
|
|
|
453
|
-
// Calculate item total
|
|
454
|
-
const itemNetAmount = item.
|
|
463
|
+
// Calculate item total with decimal arithmetic and currency rounding.
|
|
464
|
+
const itemNetAmount = calculator.calculateLineNet(item.unitQuantity, item.unitNetPrice);
|
|
455
465
|
|
|
456
466
|
// Add line total amount
|
|
457
467
|
const lineTotalElement = doc.createElement('ram:LineTotalAmount');
|
|
458
|
-
lineTotalElement.textContent = itemNetAmount.toFixed(
|
|
468
|
+
lineTotalElement.textContent = itemNetAmount.toFixed(minorUnits);
|
|
459
469
|
monetarySummationElement.appendChild(lineTotalElement);
|
|
460
470
|
|
|
461
471
|
settlementElement.appendChild(monetarySummationElement);
|
|
@@ -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
|
+
}
|