@fin.cx/einvoice 5.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts/00_commitinfo_data.d.ts +8 -0
- package/dist_ts/00_commitinfo_data.js +9 -0
- package/dist_ts/classes.decoder.d.ts +40 -0
- package/dist_ts/classes.decoder.js +320 -0
- package/dist_ts/classes.encoder.d.ts +51 -0
- package/dist_ts/classes.encoder.js +293 -0
- package/dist_ts/classes.xinvoice.d.ts +136 -0
- package/dist_ts/classes.xinvoice.js +352 -0
- package/dist_ts/einvoice.d.ts +217 -0
- package/dist_ts/einvoice.js +505 -0
- package/dist_ts/errors.d.ts +121 -0
- package/dist_ts/errors.js +241 -0
- package/dist_ts/formats/base/base.decoder.d.ts +32 -0
- package/dist_ts/formats/base/base.decoder.js +52 -0
- package/dist_ts/formats/base/base.encoder.d.ts +13 -0
- package/dist_ts/formats/base/base.encoder.js +7 -0
- package/dist_ts/formats/base/base.validator.d.ts +44 -0
- package/dist_ts/formats/base/base.validator.js +39 -0
- package/dist_ts/formats/base.decoder.d.ts +19 -0
- package/dist_ts/formats/base.decoder.js +130 -0
- package/dist_ts/formats/base.validator.d.ts +44 -0
- package/dist_ts/formats/base.validator.js +39 -0
- package/dist_ts/formats/cii/cii.decoder.d.ts +61 -0
- package/dist_ts/formats/cii/cii.decoder.js +111 -0
- package/dist_ts/formats/cii/cii.encoder.d.ts +43 -0
- package/dist_ts/formats/cii/cii.encoder.js +48 -0
- package/dist_ts/formats/cii/cii.types.d.ts +31 -0
- package/dist_ts/formats/cii/cii.types.js +40 -0
- package/dist_ts/formats/cii/cii.validator.d.ts +56 -0
- package/dist_ts/formats/cii/cii.validator.js +146 -0
- package/dist_ts/formats/cii/facturx/facturx.decoder.d.ts +43 -0
- package/dist_ts/formats/cii/facturx/facturx.decoder.js +207 -0
- package/dist_ts/formats/cii/facturx/facturx.encoder.d.ts +82 -0
- package/dist_ts/formats/cii/facturx/facturx.encoder.js +400 -0
- package/dist_ts/formats/cii/facturx/facturx.types.d.ts +10 -0
- package/dist_ts/formats/cii/facturx/facturx.types.js +15 -0
- package/dist_ts/formats/cii/facturx/facturx.validator.d.ts +32 -0
- package/dist_ts/formats/cii/facturx/facturx.validator.js +138 -0
- package/dist_ts/formats/cii/zugferd/zugferd.decoder.d.ts +43 -0
- package/dist_ts/formats/cii/zugferd/zugferd.decoder.js +218 -0
- package/dist_ts/formats/cii/zugferd/zugferd.encoder.d.ts +97 -0
- package/dist_ts/formats/cii/zugferd/zugferd.encoder.js +550 -0
- package/dist_ts/formats/cii/zugferd/zugferd.types.d.ts +10 -0
- package/dist_ts/formats/cii/zugferd/zugferd.types.js +15 -0
- package/dist_ts/formats/cii/zugferd/zugferd.v1.decoder.d.ts +49 -0
- package/dist_ts/formats/cii/zugferd/zugferd.v1.decoder.js +229 -0
- package/dist_ts/formats/cii/zugferd/zugferd.validator.d.ts +16 -0
- package/dist_ts/formats/cii/zugferd/zugferd.validator.js +29 -0
- package/dist_ts/formats/decoder.factory.d.ts +15 -0
- package/dist_ts/formats/decoder.factory.js +46 -0
- package/dist_ts/formats/factories/decoder.factory.d.ts +13 -0
- package/dist_ts/formats/factories/decoder.factory.js +56 -0
- package/dist_ts/formats/factories/encoder.factory.d.ts +14 -0
- package/dist_ts/formats/factories/encoder.factory.js +40 -0
- package/dist_ts/formats/factories/validator.factory.d.ts +12 -0
- package/dist_ts/formats/factories/validator.factory.js +110 -0
- package/dist_ts/formats/factorx.decoder.d.ts +18 -0
- package/dist_ts/formats/factorx.decoder.js +178 -0
- package/dist_ts/formats/factorx.encoder.d.ts +51 -0
- package/dist_ts/formats/factorx.encoder.js +293 -0
- package/dist_ts/formats/facturx.decoder.d.ts +18 -0
- package/dist_ts/formats/facturx.decoder.js +210 -0
- package/dist_ts/formats/facturx.encoder.d.ts +51 -0
- package/dist_ts/formats/facturx.encoder.js +293 -0
- package/dist_ts/formats/facturx.validator.d.ts +67 -0
- package/dist_ts/formats/facturx.validator.js +266 -0
- package/dist_ts/formats/pdf/extractors/associated.extractor.d.ts +14 -0
- package/dist_ts/formats/pdf/extractors/associated.extractor.js +68 -0
- package/dist_ts/formats/pdf/extractors/base.extractor.d.ts +92 -0
- package/dist_ts/formats/pdf/extractors/base.extractor.js +319 -0
- package/dist_ts/formats/pdf/extractors/index.d.ts +4 -0
- package/dist_ts/formats/pdf/extractors/index.js +5 -0
- package/dist_ts/formats/pdf/extractors/standard.extractor.d.ts +13 -0
- package/dist_ts/formats/pdf/extractors/standard.extractor.js +74 -0
- package/dist_ts/formats/pdf/extractors/text.extractor.d.ts +45 -0
- package/dist_ts/formats/pdf/extractors/text.extractor.js +151 -0
- package/dist_ts/formats/pdf/pdf.embedder.d.ts +69 -0
- package/dist_ts/formats/pdf/pdf.embedder.js +183 -0
- package/dist_ts/formats/pdf/pdf.extractor.d.ts +49 -0
- package/dist_ts/formats/pdf/pdf.extractor.js +98 -0
- package/dist_ts/formats/pdf/robust-pdf.extractor.d.ts +40 -0
- package/dist_ts/formats/pdf/robust-pdf.extractor.js +324 -0
- package/dist_ts/formats/ubl/en16931.ubl.validator.d.ts +18 -0
- package/dist_ts/formats/ubl/en16931.ubl.validator.js +169 -0
- package/dist_ts/formats/ubl/generic/ubl.encoder.d.ts +151 -0
- package/dist_ts/formats/ubl/generic/ubl.encoder.js +892 -0
- package/dist_ts/formats/ubl/ubl.decoder.d.ts +61 -0
- package/dist_ts/formats/ubl/ubl.decoder.js +95 -0
- package/dist_ts/formats/ubl/ubl.encoder.d.ts +38 -0
- package/dist_ts/formats/ubl/ubl.encoder.js +46 -0
- package/dist_ts/formats/ubl/ubl.types.d.ts +16 -0
- package/dist_ts/formats/ubl/ubl.types.js +21 -0
- package/dist_ts/formats/ubl/ubl.validator.d.ts +50 -0
- package/dist_ts/formats/ubl/ubl.validator.js +112 -0
- package/dist_ts/formats/ubl/xrechnung/xrechnung.decoder.d.ts +34 -0
- package/dist_ts/formats/ubl/xrechnung/xrechnung.decoder.js +424 -0
- package/dist_ts/formats/ubl/xrechnung/xrechnung.encoder.d.ts +75 -0
- package/dist_ts/formats/ubl/xrechnung/xrechnung.encoder.js +555 -0
- package/dist_ts/formats/ubl/xrechnung.validator.d.ts +15 -0
- package/dist_ts/formats/ubl/xrechnung.validator.js +107 -0
- package/dist_ts/formats/ubl.validator.d.ts +82 -0
- package/dist_ts/formats/ubl.validator.js +306 -0
- package/dist_ts/formats/utils/format.detector.d.ts +62 -0
- package/dist_ts/formats/utils/format.detector.js +249 -0
- package/dist_ts/formats/validation/en16931.validator.d.ts +23 -0
- package/dist_ts/formats/validation/en16931.validator.js +119 -0
- package/dist_ts/formats/validator.factory.d.ts +18 -0
- package/dist_ts/formats/validator.factory.js +78 -0
- package/dist_ts/formats/xinvoice.decoder.d.ts +28 -0
- package/dist_ts/formats/xinvoice.decoder.js +332 -0
- package/dist_ts/formats/xinvoice.encoder.d.ts +19 -0
- package/dist_ts/formats/xinvoice.encoder.js +282 -0
- package/dist_ts/index.d.ts +51 -0
- package/dist_ts/index.js +90 -0
- package/dist_ts/interfaces/common.d.ts +79 -0
- package/dist_ts/interfaces/common.js +24 -0
- package/dist_ts/interfaces.d.ts +87 -0
- package/dist_ts/interfaces.js +23 -0
- package/dist_ts/plugins.d.ts +14 -0
- package/dist_ts/plugins.js +31 -0
- package/npmextra.json +35 -0
- package/package.json +71 -0
- package/readme.hints.md +1107 -0
- package/readme.howtofixtests.md +38 -0
- package/readme.literature.md +1 -0
- package/readme.md +998 -0
- package/readme.plan.md +481 -0
- package/ts/00_commitinfo_data.ts +8 -0
- package/ts/einvoice.ts +603 -0
- package/ts/errors.ts +341 -0
- package/ts/formats/base/base.decoder.ts +68 -0
- package/ts/formats/base/base.encoder.ts +14 -0
- package/ts/formats/base/base.validator.ts +64 -0
- package/ts/formats/cii/cii.decoder.ts +139 -0
- package/ts/formats/cii/cii.encoder.ts +64 -0
- package/ts/formats/cii/cii.types.ts +44 -0
- package/ts/formats/cii/cii.validator.ts +171 -0
- package/ts/formats/cii/facturx/facturx.decoder.ts +243 -0
- package/ts/formats/cii/facturx/facturx.encoder.ts +483 -0
- package/ts/formats/cii/facturx/facturx.types.ts +18 -0
- package/ts/formats/cii/facturx/facturx.validator.ts +180 -0
- package/ts/formats/cii/zugferd/zugferd.decoder.ts +256 -0
- package/ts/formats/cii/zugferd/zugferd.encoder.ts +661 -0
- package/ts/formats/cii/zugferd/zugferd.types.ts +18 -0
- package/ts/formats/cii/zugferd/zugferd.v1.decoder.ts +267 -0
- package/ts/formats/cii/zugferd/zugferd.validator.ts +31 -0
- package/ts/formats/factories/decoder.factory.ts +62 -0
- package/ts/formats/factories/encoder.factory.ts +47 -0
- package/ts/formats/factories/validator.factory.ts +134 -0
- package/ts/formats/pdf/extractors/associated.extractor.ts +78 -0
- package/ts/formats/pdf/extractors/base.extractor.ts +355 -0
- package/ts/formats/pdf/extractors/index.ts +4 -0
- package/ts/formats/pdf/extractors/standard.extractor.ts +86 -0
- package/ts/formats/pdf/extractors/text.extractor.ts +162 -0
- package/ts/formats/pdf/pdf.embedder.ts +242 -0
- package/ts/formats/pdf/pdf.extractor.ts +141 -0
- package/ts/formats/ubl/en16931.ubl.validator.ts +216 -0
- package/ts/formats/ubl/generic/ubl.encoder.ts +1041 -0
- package/ts/formats/ubl/ubl.decoder.ts +121 -0
- package/ts/formats/ubl/ubl.encoder.ts +63 -0
- package/ts/formats/ubl/ubl.types.ts +22 -0
- package/ts/formats/ubl/ubl.validator.ts +133 -0
- package/ts/formats/ubl/xrechnung/xrechnung.decoder.ts +471 -0
- package/ts/formats/ubl/xrechnung/xrechnung.encoder.ts +619 -0
- package/ts/formats/ubl/xrechnung.validator.ts +185 -0
- package/ts/formats/utils/format.detector.ts +306 -0
- package/ts/formats/validation/en16931.validator.ts +135 -0
- package/ts/index.ts +164 -0
- package/ts/interfaces/common.ts +90 -0
- package/ts/interfaces.ts +98 -0
- package/ts/plugins.ts +61 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { PDFDocument, AFRelationship } from '../../plugins.js';
|
|
2
|
+
import type { IPdf } from '../../interfaces/common.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Error types for PDF embedding operations
|
|
6
|
+
*/
|
|
7
|
+
export enum PDFEmbedError {
|
|
8
|
+
LOAD_ERROR = 'PDF loading failed',
|
|
9
|
+
EMBED_ERROR = 'XML embedding failed',
|
|
10
|
+
SAVE_ERROR = 'PDF saving failed',
|
|
11
|
+
INVALID_INPUT = 'Invalid input parameters'
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Result of a PDF embedding operation
|
|
16
|
+
*/
|
|
17
|
+
export interface PDFEmbedResult {
|
|
18
|
+
success: boolean;
|
|
19
|
+
data?: Uint8Array;
|
|
20
|
+
pdf?: IPdf;
|
|
21
|
+
error?: {
|
|
22
|
+
type: PDFEmbedError;
|
|
23
|
+
message: string;
|
|
24
|
+
originalError?: Error;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Class for embedding XML into PDF files
|
|
30
|
+
* Provides robust error handling and support for different PDF formats
|
|
31
|
+
*/
|
|
32
|
+
export class PDFEmbedder {
|
|
33
|
+
/**
|
|
34
|
+
* Embeds XML into a PDF
|
|
35
|
+
* @param pdfBuffer PDF buffer
|
|
36
|
+
* @param xmlContent XML content to embed
|
|
37
|
+
* @param filename Filename for the embedded XML
|
|
38
|
+
* @param description Description for the embedded XML
|
|
39
|
+
* @returns Result with either modified PDF buffer or error information
|
|
40
|
+
*/
|
|
41
|
+
public async embedXml(
|
|
42
|
+
pdfBuffer: Uint8Array | Buffer,
|
|
43
|
+
xmlContent: string,
|
|
44
|
+
filename: string = 'invoice.xml',
|
|
45
|
+
description: string = 'XML Invoice'
|
|
46
|
+
): Promise<PDFEmbedResult> {
|
|
47
|
+
try {
|
|
48
|
+
// Validate inputs
|
|
49
|
+
if (!pdfBuffer || pdfBuffer.length === 0) {
|
|
50
|
+
return this.createErrorResult(PDFEmbedError.INVALID_INPUT, 'PDF buffer is empty or undefined');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!xmlContent) {
|
|
54
|
+
return this.createErrorResult(PDFEmbedError.INVALID_INPUT, 'XML content is empty or undefined');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Ensure buffer is Uint8Array
|
|
58
|
+
const pdfBufferArray = Buffer.isBuffer(pdfBuffer) ? new Uint8Array(pdfBuffer) : pdfBuffer;
|
|
59
|
+
|
|
60
|
+
// Load the PDF
|
|
61
|
+
let pdfDoc: PDFDocument;
|
|
62
|
+
try {
|
|
63
|
+
pdfDoc = await PDFDocument.load(pdfBufferArray, {
|
|
64
|
+
ignoreEncryption: true, // Try to load encrypted PDFs
|
|
65
|
+
updateMetadata: false // Don't automatically update metadata
|
|
66
|
+
});
|
|
67
|
+
} catch (error) {
|
|
68
|
+
return this.createErrorResult(
|
|
69
|
+
PDFEmbedError.LOAD_ERROR,
|
|
70
|
+
`Failed to load PDF: ${error instanceof Error ? error.message : String(error)}`,
|
|
71
|
+
error instanceof Error ? error : undefined
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Normalize filename (lowercase with XML extension)
|
|
76
|
+
filename = this.normalizeFilename(filename);
|
|
77
|
+
|
|
78
|
+
// Convert the XML string to a Uint8Array
|
|
79
|
+
const xmlBuffer = new TextEncoder().encode(xmlContent);
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
// Use pdf-lib's .attach() to embed the XML
|
|
83
|
+
pdfDoc.attach(xmlBuffer, filename, {
|
|
84
|
+
mimeType: 'text/xml',
|
|
85
|
+
description: description,
|
|
86
|
+
creationDate: new Date(),
|
|
87
|
+
modificationDate: new Date(),
|
|
88
|
+
afRelationship: AFRelationship.Alternative,
|
|
89
|
+
});
|
|
90
|
+
} catch (error) {
|
|
91
|
+
return this.createErrorResult(
|
|
92
|
+
PDFEmbedError.EMBED_ERROR,
|
|
93
|
+
`Failed to embed XML: ${error instanceof Error ? error.message : String(error)}`,
|
|
94
|
+
error instanceof Error ? error : undefined
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Save the modified PDF
|
|
99
|
+
let modifiedPdfBytes: Uint8Array;
|
|
100
|
+
try {
|
|
101
|
+
modifiedPdfBytes = await pdfDoc.save({
|
|
102
|
+
addDefaultPage: false, // Don't add a page if the document is empty
|
|
103
|
+
useObjectStreams: false, // Better compatibility with older PDF readers
|
|
104
|
+
updateFieldAppearances: false // Don't update form fields
|
|
105
|
+
});
|
|
106
|
+
} catch (error) {
|
|
107
|
+
return this.createErrorResult(
|
|
108
|
+
PDFEmbedError.SAVE_ERROR,
|
|
109
|
+
`Failed to save modified PDF: ${error instanceof Error ? error.message : String(error)}`,
|
|
110
|
+
error instanceof Error ? error : undefined
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
success: true,
|
|
116
|
+
data: modifiedPdfBytes
|
|
117
|
+
};
|
|
118
|
+
} catch (error) {
|
|
119
|
+
// Catch any uncaught errors
|
|
120
|
+
return this.createErrorResult(
|
|
121
|
+
PDFEmbedError.EMBED_ERROR,
|
|
122
|
+
`Unexpected error during XML embedding: ${error instanceof Error ? error.message : String(error)}`,
|
|
123
|
+
error instanceof Error ? error : undefined
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Creates an IPdf object with embedded XML
|
|
130
|
+
* @param pdfBuffer PDF buffer
|
|
131
|
+
* @param xmlContent XML content to embed
|
|
132
|
+
* @param filename Filename for the embedded XML
|
|
133
|
+
* @param description Description for the embedded XML
|
|
134
|
+
* @param pdfName Name for the PDF
|
|
135
|
+
* @param pdfId ID for the PDF
|
|
136
|
+
* @returns Result with either IPdf object or error information
|
|
137
|
+
*/
|
|
138
|
+
public async createPdfWithXml(
|
|
139
|
+
pdfBuffer: Uint8Array | Buffer,
|
|
140
|
+
xmlContent: string,
|
|
141
|
+
filename: string = 'invoice.xml',
|
|
142
|
+
description: string = 'XML Invoice',
|
|
143
|
+
pdfName: string = 'invoice.pdf',
|
|
144
|
+
pdfId: string = `invoice-${Date.now()}`
|
|
145
|
+
): Promise<PDFEmbedResult> {
|
|
146
|
+
// Embed XML into PDF
|
|
147
|
+
const embedResult = await this.embedXml(pdfBuffer, xmlContent, filename, description);
|
|
148
|
+
|
|
149
|
+
// If embedding failed, return the error
|
|
150
|
+
if (!embedResult.success || !embedResult.data) {
|
|
151
|
+
return embedResult;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Create IPdf object
|
|
155
|
+
const pdfObject: IPdf = {
|
|
156
|
+
name: pdfName,
|
|
157
|
+
id: pdfId,
|
|
158
|
+
metadata: {
|
|
159
|
+
textExtraction: '',
|
|
160
|
+
format: this.detectPdfFormat(xmlContent),
|
|
161
|
+
embeddedXml: {
|
|
162
|
+
filename: filename,
|
|
163
|
+
description: description
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
buffer: embedResult.data
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
success: true,
|
|
171
|
+
pdf: pdfObject
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Ensures the filename is normalized according to PDF/A requirements
|
|
177
|
+
* @param filename Filename to normalize
|
|
178
|
+
* @returns Normalized filename
|
|
179
|
+
*/
|
|
180
|
+
private normalizeFilename(filename: string): string {
|
|
181
|
+
// Convert to lowercase
|
|
182
|
+
let normalized = filename.toLowerCase();
|
|
183
|
+
|
|
184
|
+
// Ensure it has .xml extension
|
|
185
|
+
if (!normalized.endsWith('.xml')) {
|
|
186
|
+
normalized = normalized.replace(/\.[^/.]+$/, '') + '.xml';
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Replace invalid characters
|
|
190
|
+
normalized = normalized.replace(/[^a-z0-9_.-]/g, '_');
|
|
191
|
+
|
|
192
|
+
return normalized;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Tries to detect the format of the XML content
|
|
197
|
+
* @param xmlContent XML content
|
|
198
|
+
* @returns Format string or undefined
|
|
199
|
+
*/
|
|
200
|
+
private detectPdfFormat(xmlContent: string): string | undefined {
|
|
201
|
+
if (xmlContent.includes('factur-x.eu') || xmlContent.includes('factur-x.xml')) {
|
|
202
|
+
return 'factur-x';
|
|
203
|
+
} else if (xmlContent.includes('zugferd') || xmlContent.includes('ZUGFeRD')) {
|
|
204
|
+
return 'zugferd';
|
|
205
|
+
} else if (xmlContent.includes('xrechnung')) {
|
|
206
|
+
return 'xrechnung';
|
|
207
|
+
} else if (xmlContent.includes('<Invoice') || xmlContent.includes('<CreditNote')) {
|
|
208
|
+
return 'ubl';
|
|
209
|
+
} else if (xmlContent.includes('FatturaElettronica')) {
|
|
210
|
+
return 'fatturapa';
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Creates an error result object
|
|
218
|
+
* @param type Error type
|
|
219
|
+
* @param message Error message
|
|
220
|
+
* @param originalError Original error object
|
|
221
|
+
* @returns Error result
|
|
222
|
+
*/
|
|
223
|
+
private createErrorResult(
|
|
224
|
+
type: PDFEmbedError,
|
|
225
|
+
message: string,
|
|
226
|
+
originalError?: Error
|
|
227
|
+
): PDFEmbedResult {
|
|
228
|
+
console.error(`PDF Embedder Error (${type}): ${message}`);
|
|
229
|
+
if (originalError) {
|
|
230
|
+
console.error(originalError);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
success: false,
|
|
235
|
+
error: {
|
|
236
|
+
type,
|
|
237
|
+
message,
|
|
238
|
+
originalError
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BaseXMLExtractor,
|
|
3
|
+
StandardXMLExtractor,
|
|
4
|
+
AssociatedFilesExtractor,
|
|
5
|
+
TextXMLExtractor
|
|
6
|
+
} from './extractors/index.js';
|
|
7
|
+
import { FormatDetector } from '../utils/format.detector.js';
|
|
8
|
+
import { InvoiceFormat } from '../../interfaces/common.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Error types for PDF extraction operations
|
|
12
|
+
*/
|
|
13
|
+
export enum PDFExtractError {
|
|
14
|
+
EXTRACT_ERROR = 'XML extraction failed',
|
|
15
|
+
INVALID_INPUT = 'Invalid input parameters',
|
|
16
|
+
NO_XML_FOUND = 'No XML found in PDF'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Result of a PDF extraction operation
|
|
21
|
+
*/
|
|
22
|
+
export interface PDFExtractResult {
|
|
23
|
+
success: boolean;
|
|
24
|
+
xml?: string;
|
|
25
|
+
format?: InvoiceFormat;
|
|
26
|
+
extractorUsed?: string;
|
|
27
|
+
error?: {
|
|
28
|
+
type: PDFExtractError;
|
|
29
|
+
message: string;
|
|
30
|
+
originalError?: Error;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Main PDF extractor class that orchestrates the extraction process
|
|
36
|
+
* Uses multiple specialized extractors in sequence to maximize success rate
|
|
37
|
+
*/
|
|
38
|
+
export class PDFExtractor {
|
|
39
|
+
private extractors: BaseXMLExtractor[] = [];
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Constructor initializes the chain of extractors
|
|
43
|
+
*/
|
|
44
|
+
constructor() {
|
|
45
|
+
// Add extractors in order of preference/likelihood of success
|
|
46
|
+
this.extractors.push(
|
|
47
|
+
new StandardXMLExtractor(), // Standard PDF/A-3 embedded files
|
|
48
|
+
new AssociatedFilesExtractor(), // Associated files (ZUGFeRD v1, some Factur-X)
|
|
49
|
+
new TextXMLExtractor() // Text-based extraction (fallback)
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Extract XML from a PDF buffer
|
|
55
|
+
* Tries multiple extraction methods in sequence
|
|
56
|
+
* @param pdfBuffer PDF buffer
|
|
57
|
+
* @returns Result with either the extracted XML or error information
|
|
58
|
+
*/
|
|
59
|
+
public async extractXml(pdfBuffer: Uint8Array | Buffer): Promise<PDFExtractResult> {
|
|
60
|
+
try {
|
|
61
|
+
console.log('Starting XML extraction from PDF...');
|
|
62
|
+
|
|
63
|
+
// Validate input
|
|
64
|
+
if (!pdfBuffer || pdfBuffer.length === 0) {
|
|
65
|
+
return this.createErrorResult(PDFExtractError.INVALID_INPUT, 'PDF buffer is empty or undefined');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Ensure buffer is Uint8Array
|
|
69
|
+
const pdfBufferArray = Buffer.isBuffer(pdfBuffer) ? new Uint8Array(pdfBuffer) : pdfBuffer;
|
|
70
|
+
|
|
71
|
+
// Try each extractor in sequence
|
|
72
|
+
for (const extractor of this.extractors) {
|
|
73
|
+
const extractorName = extractor.constructor.name;
|
|
74
|
+
console.log(`Trying extraction with ${extractorName}...`);
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const xml = await extractor.extractXml(pdfBufferArray);
|
|
78
|
+
|
|
79
|
+
if (xml) {
|
|
80
|
+
console.log(`Successfully extracted XML using ${extractorName}`);
|
|
81
|
+
|
|
82
|
+
// Detect format of the extracted XML
|
|
83
|
+
const format = FormatDetector.detectFormat(xml);
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
success: true,
|
|
87
|
+
xml,
|
|
88
|
+
format,
|
|
89
|
+
extractorUsed: extractorName
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
console.log(`Extraction with ${extractorName} failed, trying next method...`);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
// Log error but continue with next extractor
|
|
96
|
+
console.warn(`Error using ${extractorName}: ${error instanceof Error ? error.message : String(error)}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// If all extractors fail, return a no XML found error
|
|
101
|
+
return this.createErrorResult(
|
|
102
|
+
PDFExtractError.NO_XML_FOUND,
|
|
103
|
+
'All extraction methods failed, no valid XML found in PDF'
|
|
104
|
+
);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
// Handle any unexpected errors
|
|
107
|
+
return this.createErrorResult(
|
|
108
|
+
PDFExtractError.EXTRACT_ERROR,
|
|
109
|
+
`Unexpected error during XML extraction: ${error instanceof Error ? error.message : String(error)}`,
|
|
110
|
+
error instanceof Error ? error : undefined
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Create a PDF extract result with error information
|
|
117
|
+
* @param type Error type
|
|
118
|
+
* @param message Error message
|
|
119
|
+
* @param originalError Original error object
|
|
120
|
+
* @returns Error result
|
|
121
|
+
*/
|
|
122
|
+
private createErrorResult(
|
|
123
|
+
type: PDFExtractError,
|
|
124
|
+
message: string,
|
|
125
|
+
originalError?: Error
|
|
126
|
+
): PDFExtractResult {
|
|
127
|
+
console.error(`PDF Extractor Error (${type}): ${message}`);
|
|
128
|
+
if (originalError) {
|
|
129
|
+
console.error(originalError);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
success: false,
|
|
134
|
+
error: {
|
|
135
|
+
type,
|
|
136
|
+
message,
|
|
137
|
+
originalError
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { UBLBaseValidator } from './ubl.validator.js';
|
|
2
|
+
import { ValidationLevel } from '../../interfaces/common.js';
|
|
3
|
+
import { xpath } from '../../plugins.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* EN16931-compliant UBL validator that implements all business rules
|
|
7
|
+
*/
|
|
8
|
+
export class EN16931UBLValidator extends UBLBaseValidator {
|
|
9
|
+
/**
|
|
10
|
+
* Validates the structure of the UBL document
|
|
11
|
+
*/
|
|
12
|
+
protected validateStructure(): boolean {
|
|
13
|
+
let valid = true;
|
|
14
|
+
|
|
15
|
+
// Check for required elements
|
|
16
|
+
const requiredElements = [
|
|
17
|
+
{ path: '//cbc:ID', error: 'Required element cbc:ID is missing' },
|
|
18
|
+
{ path: '//cbc:IssueDate', error: 'Required element cbc:IssueDate is missing' },
|
|
19
|
+
{ path: '//cbc:CustomizationID', error: 'Required element cbc:CustomizationID is missing' }
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
for (const element of requiredElements) {
|
|
23
|
+
if (!this.exists(element.path)) {
|
|
24
|
+
this.addError('STRUCT-REQUIRED', element.error, element.path);
|
|
25
|
+
valid = false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Check for at least one invoice line or credit note line
|
|
30
|
+
const invoiceLines = this.select('//cac:InvoiceLine', this.doc) as Node[];
|
|
31
|
+
const creditNoteLines = this.select('//cac:CreditNoteLine', this.doc) as Node[];
|
|
32
|
+
|
|
33
|
+
if (invoiceLines.length === 0 && creditNoteLines.length === 0) {
|
|
34
|
+
this.addError('STRUCT-LINE', 'At least one invoice line or credit note line is required', '/');
|
|
35
|
+
valid = false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return valid;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Validates EN16931 business rules
|
|
43
|
+
*/
|
|
44
|
+
protected validateBusinessRules(): boolean {
|
|
45
|
+
let valid = true;
|
|
46
|
+
|
|
47
|
+
// BR-01: An Invoice shall have a Specification identifier (BT-24).
|
|
48
|
+
if (!this.exists('//cbc:CustomizationID')) {
|
|
49
|
+
this.addError('BR-01', 'An Invoice shall have a Specification identifier', '//cbc:CustomizationID');
|
|
50
|
+
valid = false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// BR-02: An Invoice shall have an Invoice number (BT-1).
|
|
54
|
+
if (!this.exists('//cbc:ID')) {
|
|
55
|
+
this.addError('BR-02', 'An Invoice shall have an Invoice number', '//cbc:ID');
|
|
56
|
+
valid = false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// BR-03: An Invoice shall have an Invoice issue date (BT-2).
|
|
60
|
+
if (!this.exists('//cbc:IssueDate')) {
|
|
61
|
+
this.addError('BR-03', 'An Invoice shall have an Invoice issue date', '//cbc:IssueDate');
|
|
62
|
+
valid = false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// BR-04: An Invoice shall have an Invoice type code (BT-3).
|
|
66
|
+
const isInvoice = this.doc.documentElement.localName === 'Invoice';
|
|
67
|
+
if (isInvoice && !this.exists('//cbc:InvoiceTypeCode')) {
|
|
68
|
+
this.addError('BR-04', 'An Invoice shall have an Invoice type code', '//cbc:InvoiceTypeCode');
|
|
69
|
+
valid = false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// BR-05: An Invoice shall have an Invoice currency code (BT-5).
|
|
73
|
+
if (!this.exists('//cbc:DocumentCurrencyCode')) {
|
|
74
|
+
this.addError('BR-05', 'An Invoice shall have an Invoice currency code', '//cbc:DocumentCurrencyCode');
|
|
75
|
+
valid = false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// BR-06: An Invoice shall contain the Seller name (BT-27).
|
|
79
|
+
if (!this.exists('//cac:AccountingSupplierParty//cbc:RegistrationName') &&
|
|
80
|
+
!this.exists('//cac:AccountingSupplierParty//cbc:Name')) {
|
|
81
|
+
this.addError('BR-06', 'An Invoice shall contain the Seller name', '//cac:AccountingSupplierParty');
|
|
82
|
+
valid = false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// BR-07: An Invoice shall contain the Buyer name (BT-44).
|
|
86
|
+
if (!this.exists('//cac:AccountingCustomerParty//cbc:RegistrationName') &&
|
|
87
|
+
!this.exists('//cac:AccountingCustomerParty//cbc:Name')) {
|
|
88
|
+
this.addError('BR-07', 'An Invoice shall contain the Buyer name', '//cac:AccountingCustomerParty');
|
|
89
|
+
valid = false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// BR-08: An Invoice shall contain the Seller postal address (BG-5).
|
|
93
|
+
const sellerAddress = this.select('//cac:AccountingSupplierParty//cac:PostalAddress', this.doc)[0];
|
|
94
|
+
if (!sellerAddress || !this.exists('.//cbc:IdentificationCode', sellerAddress)) {
|
|
95
|
+
this.addError('BR-08', 'An Invoice shall contain the Seller postal address', '//cac:AccountingSupplierParty//cac:PostalAddress');
|
|
96
|
+
valid = false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// BR-09: The Seller postal address (BG-5) shall contain a Seller country code (BT-40).
|
|
100
|
+
if (sellerAddress && !this.exists('.//cac:Country/cbc:IdentificationCode', sellerAddress)) {
|
|
101
|
+
this.addError('BR-09', 'The Seller postal address shall contain a Seller country code', '//cac:AccountingSupplierParty//cac:PostalAddress//cac:Country');
|
|
102
|
+
valid = false;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// BR-10: An Invoice shall contain the Buyer postal address (BG-8).
|
|
106
|
+
const buyerAddress = this.select('//cac:AccountingCustomerParty//cac:PostalAddress', this.doc)[0];
|
|
107
|
+
if (!buyerAddress || !this.exists('.//cbc:IdentificationCode', buyerAddress)) {
|
|
108
|
+
this.addError('BR-10', 'An Invoice shall contain the Buyer postal address', '//cac:AccountingCustomerParty//cac:PostalAddress');
|
|
109
|
+
valid = false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// BR-11: The Buyer postal address (BG-8) shall contain a Buyer country code (BT-55).
|
|
113
|
+
if (buyerAddress && !this.exists('.//cac:Country/cbc:IdentificationCode', buyerAddress)) {
|
|
114
|
+
this.addError('BR-11', 'The Buyer postal address shall contain a Buyer country code', '//cac:AccountingCustomerParty//cac:PostalAddress//cac:Country');
|
|
115
|
+
valid = false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// BR-12: An Invoice shall have the Sum of Invoice line net amount (BT-106).
|
|
119
|
+
if (!this.exists('//cac:LegalMonetaryTotal/cbc:LineExtensionAmount')) {
|
|
120
|
+
this.addError('BR-12', 'An Invoice shall have the Sum of Invoice line net amount', '//cac:LegalMonetaryTotal/cbc:LineExtensionAmount');
|
|
121
|
+
valid = false;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// BR-13: An Invoice shall have the Invoice total amount without VAT (BT-109).
|
|
125
|
+
if (!this.exists('//cac:LegalMonetaryTotal/cbc:TaxExclusiveAmount')) {
|
|
126
|
+
this.addError('BR-13', 'An Invoice shall have the Invoice total amount without VAT', '//cac:LegalMonetaryTotal/cbc:TaxExclusiveAmount');
|
|
127
|
+
valid = false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// BR-14: An Invoice shall have the Invoice total amount with VAT (BT-112).
|
|
131
|
+
if (!this.exists('//cac:LegalMonetaryTotal/cbc:TaxInclusiveAmount')) {
|
|
132
|
+
this.addError('BR-14', 'An Invoice shall have the Invoice total amount with VAT', '//cac:LegalMonetaryTotal/cbc:TaxInclusiveAmount');
|
|
133
|
+
valid = false;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// BR-15: An Invoice shall have the Amount due for payment (BT-115).
|
|
137
|
+
if (!this.exists('//cac:LegalMonetaryTotal/cbc:PayableAmount')) {
|
|
138
|
+
this.addError('BR-15', 'An Invoice shall have the Amount due for payment', '//cac:LegalMonetaryTotal/cbc:PayableAmount');
|
|
139
|
+
valid = false;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// BR-16: An Invoice shall have at least one Invoice line (BG-25).
|
|
143
|
+
const lines = this.select('//cac:InvoiceLine | //cac:CreditNoteLine', this.doc) as Node[];
|
|
144
|
+
if (lines.length === 0) {
|
|
145
|
+
this.addError('BR-16', 'An Invoice shall have at least one Invoice line', '//cac:InvoiceLine');
|
|
146
|
+
valid = false;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Validate calculation rules if we have the necessary data
|
|
150
|
+
if (this.exists('//cac:LegalMonetaryTotal/cbc:LineExtensionAmount')) {
|
|
151
|
+
valid = this.validateCalculationRules() && valid;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return valid;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Validates calculation rules (BR-CO-*)
|
|
159
|
+
*/
|
|
160
|
+
private validateCalculationRules(): boolean {
|
|
161
|
+
let valid = true;
|
|
162
|
+
|
|
163
|
+
// BR-CO-10: Sum of Invoice line net amount = Σ Invoice line net amount.
|
|
164
|
+
const lineExtensionAmount = this.getNumber('//cac:LegalMonetaryTotal/cbc:LineExtensionAmount');
|
|
165
|
+
const lines = this.select('//cac:InvoiceLine | //cac:CreditNoteLine', this.doc) as Node[];
|
|
166
|
+
|
|
167
|
+
let calculatedSum = 0;
|
|
168
|
+
for (const line of lines) {
|
|
169
|
+
const lineAmount = this.getNumber('.//cbc:LineExtensionAmount', line);
|
|
170
|
+
calculatedSum += lineAmount;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Allow for small rounding differences (0.01)
|
|
174
|
+
if (Math.abs(lineExtensionAmount - calculatedSum) > 0.01) {
|
|
175
|
+
this.addError(
|
|
176
|
+
'BR-CO-10',
|
|
177
|
+
`Sum of Invoice line net amount (${lineExtensionAmount}) must equal sum of line amounts (${calculatedSum})`,
|
|
178
|
+
'//cac:LegalMonetaryTotal/cbc:LineExtensionAmount'
|
|
179
|
+
);
|
|
180
|
+
valid = false;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// BR-CO-13: Invoice total amount without VAT = Σ Invoice line net amount - Sum of allowances on document level + Sum of charges on document level.
|
|
184
|
+
const taxExclusiveAmount = this.getNumber('//cac:LegalMonetaryTotal/cbc:TaxExclusiveAmount');
|
|
185
|
+
const allowanceTotal = this.getNumber('//cac:LegalMonetaryTotal/cbc:AllowanceTotalAmount') || 0;
|
|
186
|
+
const chargeTotal = this.getNumber('//cac:LegalMonetaryTotal/cbc:ChargeTotalAmount') || 0;
|
|
187
|
+
|
|
188
|
+
const calculatedTaxExclusive = lineExtensionAmount - allowanceTotal + chargeTotal;
|
|
189
|
+
|
|
190
|
+
if (Math.abs(taxExclusiveAmount - calculatedTaxExclusive) > 0.01) {
|
|
191
|
+
this.addError(
|
|
192
|
+
'BR-CO-13',
|
|
193
|
+
`Invoice total amount without VAT (${taxExclusiveAmount}) must equal calculated amount (${calculatedTaxExclusive})`,
|
|
194
|
+
'//cac:LegalMonetaryTotal/cbc:TaxExclusiveAmount'
|
|
195
|
+
);
|
|
196
|
+
valid = false;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// BR-CO-15: Invoice total amount with VAT = Invoice total amount without VAT + Invoice total VAT amount.
|
|
200
|
+
const taxInclusiveAmount = this.getNumber('//cac:LegalMonetaryTotal/cbc:TaxInclusiveAmount');
|
|
201
|
+
const totalTaxAmount = this.getNumber('//cac:TaxTotal/cbc:TaxAmount') || 0;
|
|
202
|
+
|
|
203
|
+
const calculatedTaxInclusive = taxExclusiveAmount + totalTaxAmount;
|
|
204
|
+
|
|
205
|
+
if (Math.abs(taxInclusiveAmount - calculatedTaxInclusive) > 0.01) {
|
|
206
|
+
this.addError(
|
|
207
|
+
'BR-CO-15',
|
|
208
|
+
`Invoice total amount with VAT (${taxInclusiveAmount}) must equal calculated amount (${calculatedTaxInclusive})`,
|
|
209
|
+
'//cac:LegalMonetaryTotal/cbc:TaxInclusiveAmount'
|
|
210
|
+
);
|
|
211
|
+
valid = false;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return valid;
|
|
215
|
+
}
|
|
216
|
+
}
|