@fin.cx/skr 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts/index.d.ts +6 -0
- package/dist_ts/index.js +7 -1
- package/dist_ts/plugins.d.ts +8 -1
- package/dist_ts/plugins.js +10 -2
- package/dist_ts/skr.api.d.ts +70 -0
- package/dist_ts/skr.api.js +354 -3
- package/dist_ts/skr.classes.journalentry.d.ts +1 -0
- package/dist_ts/skr.classes.journalentry.js +55 -15
- package/dist_ts/skr.classes.ledger.d.ts +4 -0
- package/dist_ts/skr.classes.ledger.js +72 -1
- package/dist_ts/skr.classes.reports.js +56 -22
- package/dist_ts/skr.export.accounts.d.ts +53 -0
- package/dist_ts/skr.export.accounts.js +111 -0
- package/dist_ts/skr.export.balances.d.ts +59 -0
- package/dist_ts/skr.export.balances.js +205 -0
- package/dist_ts/skr.export.d.ts +110 -0
- package/dist_ts/skr.export.js +315 -0
- package/dist_ts/skr.export.ledger.d.ts +95 -0
- package/dist_ts/skr.export.ledger.js +164 -0
- package/dist_ts/skr.export.pdf.d.ts +82 -0
- package/dist_ts/skr.export.pdf.js +548 -0
- package/dist_ts/skr.invoice.adapter.d.ts +98 -0
- package/dist_ts/skr.invoice.adapter.js +476 -0
- package/dist_ts/skr.invoice.booking.d.ts +102 -0
- package/dist_ts/skr.invoice.booking.js +556 -0
- package/dist_ts/skr.invoice.entity.d.ts +287 -0
- package/dist_ts/skr.invoice.entity.js +2 -0
- package/dist_ts/skr.invoice.mapper.d.ts +69 -0
- package/dist_ts/skr.invoice.mapper.js +401 -0
- package/dist_ts/skr.invoice.storage.d.ts +140 -0
- package/dist_ts/skr.invoice.storage.js +529 -0
- package/dist_ts/skr.security.d.ts +65 -0
- package/dist_ts/skr.security.js +319 -0
- package/dist_ts/skr.types.d.ts +1 -0
- package/package.json +18 -12
- package/readme.md +461 -132
- package/ts/index.ts +6 -0
- package/ts/plugins.ts +22 -1
- package/ts/skr.api.ts +489 -2
- package/ts/skr.classes.journalentry.ts +63 -14
- package/ts/skr.classes.ledger.ts +85 -0
- package/ts/skr.classes.reports.ts +64 -21
- package/ts/skr.export.accounts.ts +154 -0
- package/ts/skr.export.balances.ts +270 -0
- package/ts/skr.export.ledger.ts +249 -0
- package/ts/skr.export.pdf.ts +601 -0
- package/ts/skr.export.ts +443 -0
- package/ts/skr.invoice.adapter.ts +581 -0
- package/ts/skr.invoice.booking.ts +738 -0
- package/ts/skr.invoice.entity.ts +351 -0
- package/ts/skr.invoice.mapper.ts +486 -0
- package/ts/skr.invoice.storage.ts +710 -0
- package/ts/skr.security.ts +405 -0
- package/ts/skr.types.ts +1 -0
|
@@ -0,0 +1,581 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import type {
|
|
3
|
+
IInvoice,
|
|
4
|
+
IInvoiceLine,
|
|
5
|
+
IInvoiceParty,
|
|
6
|
+
IVATCategory,
|
|
7
|
+
IValidationResult,
|
|
8
|
+
TInvoiceFormat,
|
|
9
|
+
TInvoiceDirection,
|
|
10
|
+
TTaxScenario,
|
|
11
|
+
IAllowanceCharge,
|
|
12
|
+
IPaymentTerms
|
|
13
|
+
} from './skr.invoice.entity.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Adapter for @fin.cx/einvoice library
|
|
17
|
+
* Handles parsing, validation, and format conversion of e-invoices
|
|
18
|
+
*/
|
|
19
|
+
export class InvoiceAdapter {
|
|
20
|
+
private logger: plugins.smartlog.ConsoleLog;
|
|
21
|
+
|
|
22
|
+
constructor() {
|
|
23
|
+
this.logger = new plugins.smartlog.ConsoleLog();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private readonly MAX_XML_SIZE = 10 * 1024 * 1024; // 10MB max
|
|
27
|
+
private readonly MAX_PDF_SIZE = 50 * 1024 * 1024; // 50MB max
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Parse an invoice from file or buffer
|
|
31
|
+
*/
|
|
32
|
+
public async parseInvoice(
|
|
33
|
+
file: Buffer | string,
|
|
34
|
+
direction: TInvoiceDirection
|
|
35
|
+
): Promise<IInvoice> {
|
|
36
|
+
try {
|
|
37
|
+
// Validate input size
|
|
38
|
+
if (Buffer.isBuffer(file)) {
|
|
39
|
+
if (file.length > this.MAX_XML_SIZE) {
|
|
40
|
+
throw new Error(`Invoice file too large: ${file.length} bytes (max ${this.MAX_XML_SIZE} bytes)`);
|
|
41
|
+
}
|
|
42
|
+
} else if (typeof file === 'string' && file.length > this.MAX_XML_SIZE) {
|
|
43
|
+
throw new Error(`Invoice XML too large: ${file.length} characters (max ${this.MAX_XML_SIZE} characters)`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Parse the invoice using @fin.cx/einvoice
|
|
47
|
+
let einvoice;
|
|
48
|
+
if (typeof file === 'string') {
|
|
49
|
+
einvoice = await plugins.einvoice.EInvoice.fromXml(file);
|
|
50
|
+
} else {
|
|
51
|
+
// Convert buffer to string first
|
|
52
|
+
const xmlString = file.toString('utf-8');
|
|
53
|
+
einvoice = await plugins.einvoice.EInvoice.fromXml(xmlString);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Get detected format
|
|
57
|
+
const format = this.mapEInvoiceFormat(einvoice.format || 'xrechnung');
|
|
58
|
+
|
|
59
|
+
// Validate the invoice (takes ~2.2ms)
|
|
60
|
+
const validationResult = await this.validateInvoice(einvoice);
|
|
61
|
+
|
|
62
|
+
// Extract invoice data
|
|
63
|
+
const invoiceData = einvoice.toObject();
|
|
64
|
+
|
|
65
|
+
// Map to internal invoice model
|
|
66
|
+
const invoice = await this.mapToInternalModel(
|
|
67
|
+
invoiceData,
|
|
68
|
+
format,
|
|
69
|
+
direction,
|
|
70
|
+
validationResult
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// Store original XML content
|
|
74
|
+
invoice.xmlContent = einvoice.getXml();
|
|
75
|
+
|
|
76
|
+
// Calculate content hash
|
|
77
|
+
invoice.contentHash = await this.calculateContentHash(invoice.xmlContent);
|
|
78
|
+
|
|
79
|
+
// Classify tax scenario
|
|
80
|
+
invoice.taxScenario = this.classifyTaxScenario(invoice);
|
|
81
|
+
|
|
82
|
+
return invoice;
|
|
83
|
+
} catch (error) {
|
|
84
|
+
this.logger.log('error', `Failed to parse invoice: ${error}`);
|
|
85
|
+
throw new Error(`Invoice parsing failed: ${error.message}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Validate an invoice using multi-level validation
|
|
91
|
+
*/
|
|
92
|
+
private async validateInvoice(einvoice: any): Promise<IValidationResult> {
|
|
93
|
+
// Perform multi-level validation
|
|
94
|
+
const validationResult = await einvoice.validate();
|
|
95
|
+
|
|
96
|
+
// Parse validation results into our structure
|
|
97
|
+
const syntaxResult = {
|
|
98
|
+
isValid: validationResult.syntax?.valid !== false,
|
|
99
|
+
errors: validationResult.syntax?.errors || [],
|
|
100
|
+
warnings: validationResult.syntax?.warnings || []
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const semanticResult = {
|
|
104
|
+
isValid: validationResult.semantic?.valid !== false,
|
|
105
|
+
errors: validationResult.semantic?.errors || [],
|
|
106
|
+
warnings: validationResult.semantic?.warnings || []
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const businessResult = {
|
|
110
|
+
isValid: validationResult.business?.valid !== false,
|
|
111
|
+
errors: validationResult.business?.errors || [],
|
|
112
|
+
warnings: validationResult.business?.warnings || []
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const countryResult = {
|
|
116
|
+
isValid: validationResult.country?.valid !== false,
|
|
117
|
+
errors: validationResult.country?.errors || [],
|
|
118
|
+
warnings: validationResult.country?.warnings || []
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
isValid: syntaxResult.isValid && semanticResult.isValid && businessResult.isValid,
|
|
123
|
+
syntax: {
|
|
124
|
+
valid: syntaxResult.isValid,
|
|
125
|
+
errors: syntaxResult.errors || [],
|
|
126
|
+
warnings: syntaxResult.warnings || []
|
|
127
|
+
},
|
|
128
|
+
semantic: {
|
|
129
|
+
valid: semanticResult.isValid,
|
|
130
|
+
errors: semanticResult.errors || [],
|
|
131
|
+
warnings: semanticResult.warnings || []
|
|
132
|
+
},
|
|
133
|
+
businessRules: {
|
|
134
|
+
valid: businessResult.isValid,
|
|
135
|
+
errors: businessResult.errors || [],
|
|
136
|
+
warnings: businessResult.warnings || []
|
|
137
|
+
},
|
|
138
|
+
countrySpecific: {
|
|
139
|
+
valid: countryResult.isValid,
|
|
140
|
+
errors: countryResult.errors || [],
|
|
141
|
+
warnings: countryResult.warnings || []
|
|
142
|
+
},
|
|
143
|
+
validatedAt: new Date(),
|
|
144
|
+
validatorVersion: '5.1.4'
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Map EN16931 Business Terms to internal invoice model
|
|
150
|
+
*/
|
|
151
|
+
private async mapToInternalModel(
|
|
152
|
+
businessTerms: any,
|
|
153
|
+
format: TInvoiceFormat,
|
|
154
|
+
direction: TInvoiceDirection,
|
|
155
|
+
validationResult: IValidationResult
|
|
156
|
+
): Promise<IInvoice> {
|
|
157
|
+
const invoice: IInvoice = {
|
|
158
|
+
// Identity
|
|
159
|
+
id: plugins.smartunique.shortId(),
|
|
160
|
+
direction,
|
|
161
|
+
format,
|
|
162
|
+
|
|
163
|
+
// EN16931 Business Terms
|
|
164
|
+
invoiceNumber: businessTerms.BT1_InvoiceNumber,
|
|
165
|
+
issueDate: new Date(businessTerms.BT2_IssueDate),
|
|
166
|
+
invoiceTypeCode: businessTerms.BT3_InvoiceTypeCode || '380',
|
|
167
|
+
currencyCode: businessTerms.BT5_CurrencyCode || 'EUR',
|
|
168
|
+
taxCurrencyCode: businessTerms.BT6_TaxCurrencyCode,
|
|
169
|
+
taxPointDate: businessTerms.BT7_TaxPointDate ? new Date(businessTerms.BT7_TaxPointDate) : undefined,
|
|
170
|
+
paymentDueDate: businessTerms.BT9_PaymentDueDate ? new Date(businessTerms.BT9_PaymentDueDate) : undefined,
|
|
171
|
+
buyerReference: businessTerms.BT10_BuyerReference,
|
|
172
|
+
projectReference: businessTerms.BT11_ProjectReference,
|
|
173
|
+
contractReference: businessTerms.BT12_ContractReference,
|
|
174
|
+
orderReference: businessTerms.BT13_OrderReference,
|
|
175
|
+
sellerOrderReference: businessTerms.BT14_SellerOrderReference,
|
|
176
|
+
|
|
177
|
+
// Parties
|
|
178
|
+
supplier: this.mapParty(businessTerms.BG4_Seller),
|
|
179
|
+
customer: this.mapParty(businessTerms.BG7_Buyer),
|
|
180
|
+
payee: businessTerms.BG10_Payee ? this.mapParty(businessTerms.BG10_Payee) : undefined,
|
|
181
|
+
|
|
182
|
+
// Line items
|
|
183
|
+
lines: this.mapInvoiceLines(businessTerms.BG25_InvoiceLines || []),
|
|
184
|
+
|
|
185
|
+
// Allowances and charges
|
|
186
|
+
allowances: this.mapAllowancesCharges(businessTerms.BG20_DocumentAllowances || [], true),
|
|
187
|
+
charges: this.mapAllowancesCharges(businessTerms.BG21_DocumentCharges || [], false),
|
|
188
|
+
|
|
189
|
+
// Amounts
|
|
190
|
+
lineNetAmount: parseFloat(businessTerms.BT106_SumOfLineNetAmounts || 0),
|
|
191
|
+
allowanceTotalAmount: parseFloat(businessTerms.BT107_AllowanceTotalAmount || 0),
|
|
192
|
+
chargeTotalAmount: parseFloat(businessTerms.BT108_ChargeTotalAmount || 0),
|
|
193
|
+
taxExclusiveAmount: parseFloat(businessTerms.BT109_TaxExclusiveAmount || 0),
|
|
194
|
+
taxInclusiveAmount: parseFloat(businessTerms.BT112_TaxInclusiveAmount || 0),
|
|
195
|
+
prepaidAmount: parseFloat(businessTerms.BT113_PrepaidAmount || 0),
|
|
196
|
+
payableAmount: parseFloat(businessTerms.BT115_PayableAmount || 0),
|
|
197
|
+
|
|
198
|
+
// VAT breakdown
|
|
199
|
+
vatBreakdown: this.mapVATBreakdown(businessTerms.BG23_VATBreakdown || []),
|
|
200
|
+
totalVATAmount: parseFloat(businessTerms.BT110_TotalVATAmount || 0),
|
|
201
|
+
|
|
202
|
+
// Payment
|
|
203
|
+
paymentTerms: this.mapPaymentTerms(businessTerms),
|
|
204
|
+
paymentMeans: this.mapPaymentMeans(businessTerms.BG16_PaymentInstructions),
|
|
205
|
+
|
|
206
|
+
// Notes
|
|
207
|
+
invoiceNote: businessTerms.BT22_InvoiceNote,
|
|
208
|
+
|
|
209
|
+
// Processing metadata
|
|
210
|
+
status: 'validated',
|
|
211
|
+
|
|
212
|
+
// Storage (to be filled later)
|
|
213
|
+
contentHash: '',
|
|
214
|
+
|
|
215
|
+
// Validation
|
|
216
|
+
validationResult,
|
|
217
|
+
|
|
218
|
+
// Audit trail
|
|
219
|
+
createdAt: new Date(),
|
|
220
|
+
createdBy: 'system',
|
|
221
|
+
|
|
222
|
+
// Metadata
|
|
223
|
+
metadata: {
|
|
224
|
+
importedAt: new Date(),
|
|
225
|
+
parserVersion: '5.1.4',
|
|
226
|
+
originalFormat: format
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
return invoice;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Map party information
|
|
235
|
+
*/
|
|
236
|
+
private mapParty(partyData: any): IInvoiceParty {
|
|
237
|
+
if (!partyData) {
|
|
238
|
+
return {
|
|
239
|
+
id: '',
|
|
240
|
+
name: '',
|
|
241
|
+
address: { countryCode: 'DE' }
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
id: partyData.BT29_SellerID || partyData.BT46_BuyerID || plugins.smartunique.shortId(),
|
|
247
|
+
name: partyData.BT27_SellerName || partyData.BT44_BuyerName || '',
|
|
248
|
+
address: {
|
|
249
|
+
street: partyData.BT35_SellerStreet || partyData.BT50_BuyerStreet,
|
|
250
|
+
city: partyData.BT37_SellerCity || partyData.BT52_BuyerCity,
|
|
251
|
+
postalCode: partyData.BT38_SellerPostalCode || partyData.BT53_BuyerPostalCode,
|
|
252
|
+
countryCode: partyData.BT40_SellerCountryCode || partyData.BT55_BuyerCountryCode || 'DE'
|
|
253
|
+
},
|
|
254
|
+
vatId: partyData.BT31_SellerVATID || partyData.BT48_BuyerVATID,
|
|
255
|
+
taxId: partyData.BT32_SellerTaxID || partyData.BT47_BuyerTaxID,
|
|
256
|
+
email: partyData.BT34_SellerEmail || partyData.BT49_BuyerEmail,
|
|
257
|
+
phone: partyData.BT33_SellerPhone,
|
|
258
|
+
bankAccount: this.mapBankAccount(partyData)
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Map bank account information
|
|
264
|
+
*/
|
|
265
|
+
private mapBankAccount(partyData: any): IInvoiceParty['bankAccount'] | undefined {
|
|
266
|
+
if (!partyData?.BT84_PaymentAccountID) {
|
|
267
|
+
return undefined;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
iban: partyData.BT84_PaymentAccountID,
|
|
272
|
+
bic: partyData.BT86_PaymentServiceProviderID,
|
|
273
|
+
accountHolder: partyData.BT85_PaymentAccountName
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Map invoice lines
|
|
279
|
+
*/
|
|
280
|
+
private mapInvoiceLines(linesData: any[]): IInvoiceLine[] {
|
|
281
|
+
return linesData.map((line, index) => ({
|
|
282
|
+
lineNumber: index + 1,
|
|
283
|
+
description: line.BT154_ItemDescription || '',
|
|
284
|
+
quantity: parseFloat(line.BT129_Quantity || 1),
|
|
285
|
+
unitPrice: parseFloat(line.BT146_NetPrice || 0),
|
|
286
|
+
netAmount: parseFloat(line.BT131_LineNetAmount || 0),
|
|
287
|
+
vatCategory: this.mapVATCategory(line.BT151_ItemVATCategory, line.BT152_ItemVATRate),
|
|
288
|
+
vatAmount: parseFloat(line.lineVATAmount || 0),
|
|
289
|
+
grossAmount: parseFloat(line.BT131_LineNetAmount || 0) + parseFloat(line.lineVATAmount || 0),
|
|
290
|
+
productCode: line.BT155_ItemSellerID,
|
|
291
|
+
allowances: this.mapLineAllowancesCharges(line.BG27_LineAllowances || [], true),
|
|
292
|
+
charges: this.mapLineAllowancesCharges(line.BG28_LineCharges || [], false)
|
|
293
|
+
}));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Map VAT category
|
|
298
|
+
*/
|
|
299
|
+
private mapVATCategory(categoryCode: string, rate: string | number): IVATCategory {
|
|
300
|
+
const vatRate = typeof rate === 'string' ? parseFloat(rate) : rate;
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
code: categoryCode || 'S',
|
|
304
|
+
rate: vatRate || 0,
|
|
305
|
+
exemptionReason: this.getExemptionReason(categoryCode)
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Get exemption reason for VAT category
|
|
311
|
+
*/
|
|
312
|
+
private getExemptionReason(categoryCode: string): string | undefined {
|
|
313
|
+
const exemptionReasons: Record<string, string> = {
|
|
314
|
+
'E': 'Tax exempt',
|
|
315
|
+
'Z': 'Zero rated',
|
|
316
|
+
'AE': 'Reverse charge (§13b UStG)',
|
|
317
|
+
'K': 'Intra-EU supply',
|
|
318
|
+
'G': 'Export outside EU',
|
|
319
|
+
'O': 'Outside scope of tax',
|
|
320
|
+
'S': undefined // Standard rate, no exemption
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
return exemptionReasons[categoryCode];
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Map VAT breakdown
|
|
328
|
+
*/
|
|
329
|
+
private mapVATBreakdown(vatBreakdown: any[]): IInvoice['vatBreakdown'] {
|
|
330
|
+
return vatBreakdown.map(vat => ({
|
|
331
|
+
vatCategory: this.mapVATCategory(vat.BT118_VATCategory, vat.BT119_VATRate),
|
|
332
|
+
taxableAmount: parseFloat(vat.BT116_TaxableAmount || 0),
|
|
333
|
+
taxAmount: parseFloat(vat.BT117_TaxAmount || 0)
|
|
334
|
+
}));
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Map allowances and charges
|
|
339
|
+
*/
|
|
340
|
+
private mapAllowancesCharges(data: any[], isAllowance: boolean): IAllowanceCharge[] {
|
|
341
|
+
return data.map(item => ({
|
|
342
|
+
reason: item.BT97_AllowanceReason || item.BT104_ChargeReason || '',
|
|
343
|
+
amount: parseFloat(item.BT92_AllowanceAmount || item.BT99_ChargeAmount || 0),
|
|
344
|
+
percentage: item.BT94_AllowancePercentage || item.BT101_ChargePercentage,
|
|
345
|
+
vatCategory: item.BT95_AllowanceVATCategory || item.BT102_ChargeVATCategory
|
|
346
|
+
? this.mapVATCategory(
|
|
347
|
+
item.BT95_AllowanceVATCategory || item.BT102_ChargeVATCategory,
|
|
348
|
+
item.BT96_AllowanceVATRate || item.BT103_ChargeVATRate
|
|
349
|
+
)
|
|
350
|
+
: undefined,
|
|
351
|
+
vatAmount: parseFloat(item.allowanceVATAmount || item.chargeVATAmount || 0)
|
|
352
|
+
}));
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Map line-level allowances and charges
|
|
357
|
+
*/
|
|
358
|
+
private mapLineAllowancesCharges(data: any[], isAllowance: boolean): IAllowanceCharge[] {
|
|
359
|
+
return data.map(item => ({
|
|
360
|
+
reason: item.BT140_LineAllowanceReason || item.BT145_LineChargeReason || '',
|
|
361
|
+
amount: parseFloat(item.BT136_LineAllowanceAmount || item.BT141_LineChargeAmount || 0),
|
|
362
|
+
percentage: item.BT138_LineAllowancePercentage || item.BT143_LineChargePercentage
|
|
363
|
+
}));
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Map payment terms
|
|
368
|
+
*/
|
|
369
|
+
private mapPaymentTerms(businessTerms: any): IPaymentTerms | undefined {
|
|
370
|
+
if (!businessTerms.BT9_PaymentDueDate && !businessTerms.BT20_PaymentTerms) {
|
|
371
|
+
return undefined;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const paymentTerms: IPaymentTerms = {
|
|
375
|
+
dueDate: businessTerms.BT9_PaymentDueDate
|
|
376
|
+
? new Date(businessTerms.BT9_PaymentDueDate)
|
|
377
|
+
: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // Default 30 days
|
|
378
|
+
paymentTermsNote: businessTerms.BT20_PaymentTerms
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
// Parse skonto from payment terms note if present
|
|
382
|
+
if (businessTerms.BT20_PaymentTerms) {
|
|
383
|
+
paymentTerms.skonto = this.parseSkontoTerms(businessTerms.BT20_PaymentTerms);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return paymentTerms;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Parse skonto terms from payment terms text
|
|
391
|
+
*/
|
|
392
|
+
private parseSkontoTerms(paymentTermsText: string): IPaymentTerms['skonto'] {
|
|
393
|
+
const skontoTerms: IPaymentTerms['skonto'] = [];
|
|
394
|
+
|
|
395
|
+
// Common German skonto patterns:
|
|
396
|
+
// "2% Skonto bei Zahlung innerhalb von 10 Tagen"
|
|
397
|
+
// "3% bei Zahlung bis 8 Tage, 2% bis 14 Tage"
|
|
398
|
+
const skontoPattern = /(\d+(?:\.\d+)?)\s*%.*?(\d+)\s*(?:Tag|Day)/gi;
|
|
399
|
+
let match;
|
|
400
|
+
|
|
401
|
+
while ((match = skontoPattern.exec(paymentTermsText)) !== null) {
|
|
402
|
+
skontoTerms.push({
|
|
403
|
+
percentage: parseFloat(match[1]),
|
|
404
|
+
days: parseInt(match[2]),
|
|
405
|
+
baseAmount: 0 // To be calculated based on invoice amount
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return skontoTerms.length > 0 ? skontoTerms : undefined;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Map payment means
|
|
414
|
+
*/
|
|
415
|
+
private mapPaymentMeans(paymentInstructions: any): IInvoice['paymentMeans'] | undefined {
|
|
416
|
+
if (!paymentInstructions) {
|
|
417
|
+
return undefined;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return {
|
|
421
|
+
code: paymentInstructions.BT81_PaymentMeansCode || '30', // 30 = Bank transfer
|
|
422
|
+
account: paymentInstructions.BT84_PaymentAccountID
|
|
423
|
+
? {
|
|
424
|
+
iban: paymentInstructions.BT84_PaymentAccountID,
|
|
425
|
+
bic: paymentInstructions.BT86_PaymentServiceProviderID,
|
|
426
|
+
accountHolder: paymentInstructions.BT85_PaymentAccountName
|
|
427
|
+
}
|
|
428
|
+
: undefined
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Classify tax scenario based on invoice data
|
|
434
|
+
*/
|
|
435
|
+
private classifyTaxScenario(invoice: IInvoice): TTaxScenario {
|
|
436
|
+
const supplierCountry = invoice.supplier.address.countryCode;
|
|
437
|
+
const customerCountry = invoice.customer.address.countryCode;
|
|
438
|
+
const hasVAT = invoice.totalVATAmount > 0;
|
|
439
|
+
const vatCategories = invoice.vatBreakdown.map(vb => vb.vatCategory.code);
|
|
440
|
+
|
|
441
|
+
// Reverse charge
|
|
442
|
+
if (vatCategories.includes('AE')) {
|
|
443
|
+
return 'reverse_charge';
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Small business exemption
|
|
447
|
+
if (vatCategories.includes('E') && invoice.invoiceNote?.includes('§19')) {
|
|
448
|
+
return 'small_business';
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Export outside EU
|
|
452
|
+
if (vatCategories.includes('G') || (!this.isEUCountry(customerCountry) && supplierCountry === 'DE')) {
|
|
453
|
+
return 'export';
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Intra-EU transactions
|
|
457
|
+
if (supplierCountry !== customerCountry && this.isEUCountry(supplierCountry) && this.isEUCountry(customerCountry)) {
|
|
458
|
+
if (invoice.direction === 'outbound') {
|
|
459
|
+
return 'intra_eu_supply';
|
|
460
|
+
} else {
|
|
461
|
+
return 'intra_eu_acquisition';
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Domestic exempt
|
|
466
|
+
if (!hasVAT && supplierCountry === 'DE' && customerCountry === 'DE') {
|
|
467
|
+
return 'domestic_exempt';
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Default: Domestic taxed
|
|
471
|
+
return 'domestic_taxed';
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Check if country is in EU
|
|
476
|
+
*/
|
|
477
|
+
private isEUCountry(countryCode: string): boolean {
|
|
478
|
+
const euCountries = [
|
|
479
|
+
'AT', 'BE', 'BG', 'HR', 'CY', 'CZ', 'DK', 'EE', 'FI', 'FR',
|
|
480
|
+
'DE', 'GR', 'HU', 'IE', 'IT', 'LV', 'LT', 'LU', 'MT', 'NL',
|
|
481
|
+
'PL', 'PT', 'RO', 'SK', 'SI', 'ES', 'SE'
|
|
482
|
+
];
|
|
483
|
+
return euCountries.includes(countryCode);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Map e-invoice format from library format
|
|
488
|
+
*/
|
|
489
|
+
private mapEInvoiceFormat(format: string): TInvoiceFormat {
|
|
490
|
+
const formatMap: Record<string, TInvoiceFormat> = {
|
|
491
|
+
'xrechnung': 'xrechnung',
|
|
492
|
+
'zugferd': 'zugferd',
|
|
493
|
+
'factur-x': 'facturx',
|
|
494
|
+
'facturx': 'facturx',
|
|
495
|
+
'peppol': 'peppol',
|
|
496
|
+
'ubl': 'ubl'
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
return formatMap[format.toLowerCase()] || 'xrechnung';
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Calculate content hash for the invoice
|
|
504
|
+
*/
|
|
505
|
+
private async calculateContentHash(xmlContent: string): Promise<string> {
|
|
506
|
+
const hash = await plugins.smarthash.sha256FromString(xmlContent);
|
|
507
|
+
return hash;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Convert invoice to different format
|
|
512
|
+
*/
|
|
513
|
+
public async convertFormat(
|
|
514
|
+
invoice: IInvoice,
|
|
515
|
+
targetFormat: TInvoiceFormat
|
|
516
|
+
): Promise<string> {
|
|
517
|
+
try {
|
|
518
|
+
// Load from existing XML
|
|
519
|
+
const einvoice = await plugins.einvoice.EInvoice.fromXml(invoice.xmlContent!);
|
|
520
|
+
|
|
521
|
+
// Convert to target format (takes ~0.6ms)
|
|
522
|
+
const convertedXml = await einvoice.exportXml(targetFormat as any);
|
|
523
|
+
|
|
524
|
+
return convertedXml;
|
|
525
|
+
} catch (error) {
|
|
526
|
+
this.logger.log('error', `Failed to convert invoice format: ${error}`);
|
|
527
|
+
throw new Error(`Format conversion failed: ${error.message}`);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Generate invoice from internal data
|
|
533
|
+
*/
|
|
534
|
+
public async generateInvoice(
|
|
535
|
+
invoiceData: Partial<IInvoice>,
|
|
536
|
+
format: TInvoiceFormat
|
|
537
|
+
): Promise<{ xml: string; pdf?: Buffer }> {
|
|
538
|
+
try {
|
|
539
|
+
// Create a new invoice instance
|
|
540
|
+
const einvoice = new plugins.einvoice.EInvoice();
|
|
541
|
+
|
|
542
|
+
// Set invoice data
|
|
543
|
+
const businessTerms = this.mapToBusinessTerms(invoiceData);
|
|
544
|
+
Object.assign(einvoice, businessTerms);
|
|
545
|
+
|
|
546
|
+
// Generate XML in requested format
|
|
547
|
+
const xml = await einvoice.exportXml(format as any);
|
|
548
|
+
|
|
549
|
+
// Generate PDF if ZUGFeRD or Factur-X
|
|
550
|
+
let pdf: Buffer | undefined;
|
|
551
|
+
if (format === 'zugferd' || format === 'facturx') {
|
|
552
|
+
// Access the pdf property if it exists
|
|
553
|
+
if (einvoice.pdf && einvoice.pdf.buffer) {
|
|
554
|
+
pdf = Buffer.from(einvoice.pdf.buffer);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
return { xml, pdf };
|
|
559
|
+
} catch (error) {
|
|
560
|
+
this.logger.log('error', `Failed to generate invoice: ${error}`);
|
|
561
|
+
throw new Error(`Invoice generation failed: ${error.message}`);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Map internal invoice to EN16931 Business Terms
|
|
567
|
+
*/
|
|
568
|
+
private mapToBusinessTerms(invoice: Partial<IInvoice>): any {
|
|
569
|
+
return {
|
|
570
|
+
BT1_InvoiceNumber: invoice.invoiceNumber,
|
|
571
|
+
BT2_IssueDate: invoice.issueDate?.toISOString(),
|
|
572
|
+
BT3_InvoiceTypeCode: invoice.invoiceTypeCode || '380',
|
|
573
|
+
BT5_CurrencyCode: invoice.currencyCode || 'EUR',
|
|
574
|
+
BT7_TaxPointDate: invoice.taxPointDate?.toISOString(),
|
|
575
|
+
BT9_PaymentDueDate: invoice.paymentDueDate?.toISOString(),
|
|
576
|
+
|
|
577
|
+
// Map other Business Terms...
|
|
578
|
+
// This would be a comprehensive mapping in production
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
}
|