@fin.cx/einvoice 5.0.3 → 5.1.3
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.js +1 -1
- package/dist_ts/einvoice.d.ts +4 -8
- package/dist_ts/einvoice.js +46 -7
- package/dist_ts/formats/converters/xml-to-einvoice.converter.d.ts +21 -0
- package/dist_ts/formats/converters/xml-to-einvoice.converter.js +127 -0
- package/dist_ts/formats/semantic/bt-bg.model.d.ts +445 -0
- package/dist_ts/formats/semantic/bt-bg.model.js +12 -0
- package/dist_ts/formats/semantic/semantic.adapter.d.ts +87 -0
- package/dist_ts/formats/semantic/semantic.adapter.js +538 -0
- package/dist_ts/formats/semantic/semantic.validator.d.ts +48 -0
- package/dist_ts/formats/semantic/semantic.validator.js +596 -0
- package/dist_ts/formats/utils/currency.calculator.decimal.d.ts +98 -0
- package/dist_ts/formats/utils/currency.calculator.decimal.js +212 -0
- package/dist_ts/formats/utils/currency.utils.d.ts +99 -0
- package/dist_ts/formats/utils/currency.utils.js +250 -0
- package/dist_ts/formats/utils/decimal.d.ts +151 -0
- package/dist_ts/formats/utils/decimal.js +435 -0
- package/dist_ts/formats/validation/codelist.validator.d.ts +49 -0
- package/dist_ts/formats/validation/codelist.validator.js +188 -0
- package/dist_ts/formats/validation/conformance.harness.d.ts +99 -0
- package/dist_ts/formats/validation/conformance.harness.js +471 -0
- package/dist_ts/formats/validation/en16931.business-rules.validator.d.ts +58 -0
- package/dist_ts/formats/validation/en16931.business-rules.validator.js +459 -0
- package/dist_ts/formats/validation/facturx.validator.d.ts +75 -0
- package/dist_ts/formats/validation/facturx.validator.js +522 -0
- package/dist_ts/formats/validation/integrated.validator.d.ts +82 -0
- package/dist_ts/formats/validation/integrated.validator.js +311 -0
- package/dist_ts/formats/validation/peppol.validator.d.ts +77 -0
- package/dist_ts/formats/validation/peppol.validator.js +529 -0
- package/dist_ts/formats/validation/schematron.downloader.d.ts +71 -0
- package/dist_ts/formats/validation/schematron.downloader.js +254 -0
- package/dist_ts/formats/validation/schematron.integration.d.ts +60 -0
- package/dist_ts/formats/validation/schematron.integration.js +230 -0
- package/dist_ts/formats/validation/schematron.validator.d.ts +79 -0
- package/dist_ts/formats/validation/schematron.validator.js +288 -0
- package/dist_ts/formats/validation/schematron.worker.d.ts +50 -0
- package/dist_ts/formats/validation/schematron.worker.js +185 -0
- package/dist_ts/formats/validation/validation.types.d.ts +163 -0
- package/dist_ts/formats/validation/validation.types.js +181 -0
- package/dist_ts/formats/validation/vat-categories.validator.d.ts +87 -0
- package/dist_ts/formats/validation/vat-categories.validator.js +497 -0
- package/dist_ts/formats/validation/xrechnung.validator.d.ts +92 -0
- package/dist_ts/formats/validation/xrechnung.validator.js +417 -0
- package/dist_ts/interfaces/common.d.ts +1 -0
- package/dist_ts/interfaces/en16931-metadata.d.ts +83 -0
- package/dist_ts/interfaces/en16931-metadata.js +6 -0
- package/dist_ts/plugins.d.ts +3 -1
- package/dist_ts/plugins.js +7 -2
- package/dist_ts_install/download-schematron.d.ts +6 -0
- package/dist_ts_install/download-schematron.js +62 -0
- package/dist_ts_install/download-test-samples.d.ts +19 -0
- package/dist_ts_install/download-test-samples.js +169 -0
- package/dist_ts_install/download-xrechnung-rules.d.ts +7 -0
- package/dist_ts_install/download-xrechnung-rules.js +146 -0
- package/dist_ts_install/index.d.ts +8 -0
- package/dist_ts_install/index.js +246 -0
- package/npmextra.json +1 -1
- package/package.json +12 -6
- package/readme.md +255 -810
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/einvoice.ts +63 -14
- package/ts/formats/converters/xml-to-einvoice.converter.ts +142 -0
- package/ts/formats/semantic/bt-bg.model.ts +524 -0
- package/ts/formats/semantic/semantic.adapter.ts +600 -0
- package/ts/formats/semantic/semantic.validator.ts +654 -0
- package/ts/formats/utils/currency.calculator.decimal.ts +323 -0
- package/ts/formats/utils/currency.utils.ts +299 -0
- package/ts/formats/utils/decimal.ts +509 -0
- package/ts/formats/validation/codelist.validator.ts +317 -0
- package/ts/formats/validation/conformance.harness.ts +591 -0
- package/ts/formats/validation/en16931.business-rules.validator.ts +694 -0
- package/ts/formats/validation/facturx.validator.ts +579 -0
- package/ts/formats/validation/integrated.validator.ts +405 -0
- package/ts/formats/validation/peppol.validator.ts +589 -0
- package/ts/formats/validation/schematron.downloader.ts +304 -0
- package/ts/formats/validation/schematron.integration.ts +285 -0
- package/ts/formats/validation/schematron.validator.ts +348 -0
- package/ts/formats/validation/schematron.worker.ts +221 -0
- package/ts/formats/validation/validation.types.ts +274 -0
- package/ts/formats/validation/vat-categories.validator.ts +845 -0
- package/ts/formats/validation/xrechnung.validator.ts +494 -0
- package/ts/interfaces/common.ts +1 -0
- package/ts/interfaces/en16931-metadata.ts +104 -0
- package/ts/plugins.ts +8 -0
- package/ts/tspublish.json +3 -0
- package/dist_ts/classes.decoder.d.ts +0 -40
- package/dist_ts/classes.decoder.js +0 -320
- package/dist_ts/classes.encoder.d.ts +0 -51
- package/dist_ts/classes.encoder.js +0 -293
- package/dist_ts/classes.xinvoice.d.ts +0 -136
- package/dist_ts/classes.xinvoice.js +0 -352
- package/dist_ts/formats/base.decoder.d.ts +0 -19
- package/dist_ts/formats/base.decoder.js +0 -130
- package/dist_ts/formats/base.validator.d.ts +0 -44
- package/dist_ts/formats/base.validator.js +0 -39
- package/dist_ts/formats/decoder.factory.d.ts +0 -15
- package/dist_ts/formats/decoder.factory.js +0 -46
- package/dist_ts/formats/factorx.decoder.d.ts +0 -18
- package/dist_ts/formats/factorx.decoder.js +0 -178
- package/dist_ts/formats/factorx.encoder.d.ts +0 -51
- package/dist_ts/formats/factorx.encoder.js +0 -293
- package/dist_ts/formats/facturx.decoder.d.ts +0 -18
- package/dist_ts/formats/facturx.decoder.js +0 -210
- package/dist_ts/formats/facturx.encoder.d.ts +0 -51
- package/dist_ts/formats/facturx.encoder.js +0 -293
- package/dist_ts/formats/facturx.validator.d.ts +0 -67
- package/dist_ts/formats/facturx.validator.js +0 -266
- package/dist_ts/formats/pdf/robust-pdf.extractor.d.ts +0 -40
- package/dist_ts/formats/pdf/robust-pdf.extractor.js +0 -324
- package/dist_ts/formats/ubl.validator.d.ts +0 -82
- package/dist_ts/formats/ubl.validator.js +0 -306
- package/dist_ts/formats/validator.factory.d.ts +0 -18
- package/dist_ts/formats/validator.factory.js +0 -78
- package/dist_ts/formats/xinvoice.decoder.d.ts +0 -28
- package/dist_ts/formats/xinvoice.decoder.js +0 -332
- package/dist_ts/formats/xinvoice.encoder.d.ts +0 -19
- package/dist_ts/formats/xinvoice.encoder.js +0 -282
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter for converting between EInvoice and EN16931 Semantic Model
|
|
3
|
+
* Provides bidirectional conversion capabilities
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { EInvoice } from '../../einvoice.js';
|
|
7
|
+
import type {
|
|
8
|
+
EN16931SemanticModel,
|
|
9
|
+
Seller,
|
|
10
|
+
Buyer,
|
|
11
|
+
PostalAddress,
|
|
12
|
+
Contact,
|
|
13
|
+
InvoiceLine,
|
|
14
|
+
VATBreakdown,
|
|
15
|
+
DocumentTotals,
|
|
16
|
+
PaymentInstructions,
|
|
17
|
+
Allowance,
|
|
18
|
+
Charge,
|
|
19
|
+
Period,
|
|
20
|
+
DeliveryInformation,
|
|
21
|
+
PriceDetails,
|
|
22
|
+
VATInformation,
|
|
23
|
+
ItemInformation
|
|
24
|
+
} from './bt-bg.model.js';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Adapter for converting between EInvoice and EN16931 Semantic Model
|
|
28
|
+
*/
|
|
29
|
+
export class SemanticModelAdapter {
|
|
30
|
+
/**
|
|
31
|
+
* Convert EInvoice to EN16931 Semantic Model
|
|
32
|
+
*/
|
|
33
|
+
public toSemanticModel(invoice: EInvoice): EN16931SemanticModel {
|
|
34
|
+
return {
|
|
35
|
+
// Core document information
|
|
36
|
+
documentInformation: {
|
|
37
|
+
invoiceNumber: invoice.accountingDocId,
|
|
38
|
+
issueDate: invoice.issueDate,
|
|
39
|
+
typeCode: this.mapInvoiceType(invoice.accountingDocType),
|
|
40
|
+
currencyCode: invoice.currency,
|
|
41
|
+
notes: invoice.notes ? this.mapNotes(invoice.notes) : undefined
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
// Process metadata
|
|
45
|
+
processControl: invoice.metadata?.profileId ? {
|
|
46
|
+
businessProcessType: invoice.metadata?.extensions?.businessProcessId,
|
|
47
|
+
specificationIdentifier: invoice.metadata.profileId
|
|
48
|
+
} : undefined,
|
|
49
|
+
|
|
50
|
+
// References
|
|
51
|
+
references: {
|
|
52
|
+
buyerReference: invoice.metadata?.buyerReference,
|
|
53
|
+
projectReference: invoice.metadata?.extensions?.projectReference,
|
|
54
|
+
contractReference: invoice.metadata?.extensions?.contractReference,
|
|
55
|
+
purchaseOrderReference: invoice.metadata?.extensions?.purchaseOrderReference,
|
|
56
|
+
salesOrderReference: invoice.metadata?.extensions?.salesOrderReference,
|
|
57
|
+
precedingInvoices: invoice.metadata?.extensions?.precedingInvoices
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
// Seller
|
|
61
|
+
seller: {
|
|
62
|
+
...this.mapSeller(invoice.from),
|
|
63
|
+
postalAddress: this.mapAddress(invoice.from),
|
|
64
|
+
contact: this.mapContact(invoice.from)
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
// Buyer
|
|
68
|
+
buyer: {
|
|
69
|
+
...this.mapBuyer(invoice.to),
|
|
70
|
+
postalAddress: this.mapAddress(invoice.to),
|
|
71
|
+
contact: this.mapContact(invoice.to)
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
// Payee (if different from seller)
|
|
75
|
+
payee: invoice.metadata?.extensions?.payee,
|
|
76
|
+
|
|
77
|
+
// Tax representative
|
|
78
|
+
taxRepresentative: invoice.metadata?.extensions?.taxRepresentative,
|
|
79
|
+
|
|
80
|
+
// Delivery
|
|
81
|
+
delivery: this.mapDelivery(invoice),
|
|
82
|
+
|
|
83
|
+
// Invoice period
|
|
84
|
+
invoicingPeriod: invoice.metadata?.extensions?.invoicingPeriod ? {
|
|
85
|
+
startDate: invoice.metadata.extensions.invoicingPeriod.startDate,
|
|
86
|
+
endDate: invoice.metadata.extensions.invoicingPeriod.endDate,
|
|
87
|
+
descriptionCode: invoice.metadata.extensions.invoicingPeriod.descriptionCode
|
|
88
|
+
} : undefined,
|
|
89
|
+
|
|
90
|
+
// Payment instructions
|
|
91
|
+
paymentInstructions: this.mapPaymentInstructions(invoice),
|
|
92
|
+
|
|
93
|
+
// Payment card info
|
|
94
|
+
paymentCardInfo: invoice.metadata?.extensions?.paymentCard,
|
|
95
|
+
|
|
96
|
+
// Direct debit
|
|
97
|
+
directDebit: invoice.metadata?.extensions?.directDebit,
|
|
98
|
+
|
|
99
|
+
// Payment terms
|
|
100
|
+
paymentTerms: invoice.dueInDays !== undefined ? {
|
|
101
|
+
note: `Payment due in ${invoice.dueInDays} days`
|
|
102
|
+
} : undefined,
|
|
103
|
+
|
|
104
|
+
// Document level allowances and charges
|
|
105
|
+
documentLevelAllowances: invoice.metadata?.extensions?.documentAllowances,
|
|
106
|
+
documentLevelCharges: invoice.metadata?.extensions?.documentCharges,
|
|
107
|
+
|
|
108
|
+
// Document totals
|
|
109
|
+
documentTotals: this.mapDocumentTotals(invoice),
|
|
110
|
+
|
|
111
|
+
// VAT breakdown
|
|
112
|
+
vatBreakdown: this.mapVATBreakdown(invoice),
|
|
113
|
+
|
|
114
|
+
// Additional documents
|
|
115
|
+
additionalDocuments: invoice.metadata?.extensions?.supportingDocuments,
|
|
116
|
+
|
|
117
|
+
// Invoice lines
|
|
118
|
+
invoiceLines: this.mapInvoiceLines(invoice.items || [])
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Convert EN16931 Semantic Model to EInvoice
|
|
124
|
+
*/
|
|
125
|
+
public fromSemanticModel(model: EN16931SemanticModel): EInvoice {
|
|
126
|
+
const invoice = new EInvoice();
|
|
127
|
+
invoice.accountingDocId = model.documentInformation.invoiceNumber;
|
|
128
|
+
invoice.issueDate = model.documentInformation.issueDate;
|
|
129
|
+
invoice.accountingDocType = this.reverseMapInvoiceType(model.documentInformation.typeCode) as 'invoice';
|
|
130
|
+
invoice.currency = model.documentInformation.currencyCode as any;
|
|
131
|
+
invoice.from = this.reverseMapSeller(model.seller);
|
|
132
|
+
invoice.to = this.reverseMapBuyer(model.buyer);
|
|
133
|
+
invoice.items = this.reverseMapInvoiceLines(model.invoiceLines);
|
|
134
|
+
|
|
135
|
+
// Set metadata
|
|
136
|
+
if (model.processControl) {
|
|
137
|
+
invoice.metadata = {
|
|
138
|
+
...invoice.metadata,
|
|
139
|
+
profileId: model.processControl.specificationIdentifier,
|
|
140
|
+
extensions: {
|
|
141
|
+
...invoice.metadata?.extensions,
|
|
142
|
+
businessProcessId: model.processControl.businessProcessType
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Set references
|
|
148
|
+
if (model.references) {
|
|
149
|
+
invoice.metadata = {
|
|
150
|
+
...invoice.metadata,
|
|
151
|
+
buyerReference: model.references.buyerReference,
|
|
152
|
+
extensions: {
|
|
153
|
+
...invoice.metadata?.extensions,
|
|
154
|
+
contractReference: model.references.contractReference,
|
|
155
|
+
purchaseOrderReference: model.references.purchaseOrderReference,
|
|
156
|
+
salesOrderReference: model.references.salesOrderReference,
|
|
157
|
+
precedingInvoices: model.references.precedingInvoices,
|
|
158
|
+
projectReference: model.references.projectReference
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Set payment terms
|
|
164
|
+
if (model.paymentTerms?.note) {
|
|
165
|
+
const daysMatch = model.paymentTerms.note.match(/(\d+) days/);
|
|
166
|
+
if (daysMatch) {
|
|
167
|
+
invoice.dueInDays = parseInt(daysMatch[1], 10);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Set payment options
|
|
172
|
+
if (model.paymentInstructions.paymentAccountIdentifier) {
|
|
173
|
+
invoice.paymentOptions = {
|
|
174
|
+
sepa: {
|
|
175
|
+
iban: model.paymentInstructions.paymentAccountIdentifier,
|
|
176
|
+
bic: model.paymentInstructions.paymentServiceProviderIdentifier
|
|
177
|
+
},
|
|
178
|
+
bankInfo: {
|
|
179
|
+
accountHolder: model.paymentInstructions.paymentAccountName || '',
|
|
180
|
+
institutionName: model.paymentInstructions.paymentServiceProviderIdentifier || ''
|
|
181
|
+
}
|
|
182
|
+
} as any;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Set extensions
|
|
186
|
+
if (model.payee || model.taxRepresentative || model.documentLevelAllowances) {
|
|
187
|
+
invoice.metadata = {
|
|
188
|
+
...invoice.metadata,
|
|
189
|
+
extensions: {
|
|
190
|
+
...invoice.metadata?.extensions,
|
|
191
|
+
payee: model.payee,
|
|
192
|
+
taxRepresentative: model.taxRepresentative,
|
|
193
|
+
documentAllowances: model.documentLevelAllowances,
|
|
194
|
+
documentCharges: model.documentLevelCharges,
|
|
195
|
+
supportingDocuments: model.additionalDocuments,
|
|
196
|
+
paymentCard: model.paymentCardInfo,
|
|
197
|
+
directDebit: model.directDebit,
|
|
198
|
+
taxDetails: model.vatBreakdown
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return invoice;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Map invoice type code
|
|
208
|
+
*/
|
|
209
|
+
private mapInvoiceType(type: string): string {
|
|
210
|
+
const typeMap: Record<string, string> = {
|
|
211
|
+
'invoice': '380',
|
|
212
|
+
'creditNote': '381',
|
|
213
|
+
'debitNote': '383',
|
|
214
|
+
'correctedInvoice': '384',
|
|
215
|
+
'prepaymentInvoice': '386',
|
|
216
|
+
'selfBilledInvoice': '389',
|
|
217
|
+
'invoice_380': '380',
|
|
218
|
+
'credit_note_381': '381'
|
|
219
|
+
};
|
|
220
|
+
return typeMap[type] || '380';
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Reverse map invoice type code
|
|
225
|
+
*/
|
|
226
|
+
private reverseMapInvoiceType(code: string): string {
|
|
227
|
+
const typeMap: Record<string, string> = {
|
|
228
|
+
'380': 'invoice',
|
|
229
|
+
'381': 'creditNote',
|
|
230
|
+
'383': 'debitNote',
|
|
231
|
+
'384': 'correctedInvoice',
|
|
232
|
+
'386': 'prepaymentInvoice',
|
|
233
|
+
'389': 'selfBilledInvoice'
|
|
234
|
+
};
|
|
235
|
+
return typeMap[code] || 'invoice';
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Map notes
|
|
240
|
+
*/
|
|
241
|
+
private mapNotes(notes: string | string[]): Array<{ noteContent: string }> {
|
|
242
|
+
const notesArray = Array.isArray(notes) ? notes : [notes];
|
|
243
|
+
return notesArray.map(note => ({ noteContent: note }));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Map seller information
|
|
248
|
+
*/
|
|
249
|
+
private mapSeller(from: EInvoice['from']): Seller {
|
|
250
|
+
const contact = from as any;
|
|
251
|
+
if (contact.type === 'company') {
|
|
252
|
+
return {
|
|
253
|
+
name: contact.name || '',
|
|
254
|
+
tradingName: contact.tradingName,
|
|
255
|
+
identifier: contact.registrationDetails?.registrationId,
|
|
256
|
+
legalRegistrationIdentifier: contact.registrationDetails?.registrationId,
|
|
257
|
+
vatIdentifier: contact.registrationDetails?.vatId || contact.vatNumber,
|
|
258
|
+
taxRegistrationIdentifier: contact.taxId,
|
|
259
|
+
additionalLegalInfo: contact.description,
|
|
260
|
+
electronicAddress: contact.email || contact.contact?.email
|
|
261
|
+
};
|
|
262
|
+
} else {
|
|
263
|
+
return {
|
|
264
|
+
name: contact.name || `${contact.firstName || ''} ${contact.lastName || ''}`.trim(),
|
|
265
|
+
identifier: contact.registrationDetails?.registrationId,
|
|
266
|
+
vatIdentifier: contact.registrationDetails?.vatId || contact.vatNumber,
|
|
267
|
+
electronicAddress: contact.email
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Map buyer information
|
|
274
|
+
*/
|
|
275
|
+
private mapBuyer(to: EInvoice['to']): Buyer {
|
|
276
|
+
const contact = to as any;
|
|
277
|
+
if (contact.type === 'company') {
|
|
278
|
+
return {
|
|
279
|
+
name: contact.name || '',
|
|
280
|
+
tradingName: contact.tradingName,
|
|
281
|
+
identifier: contact.registrationDetails?.registrationId,
|
|
282
|
+
legalRegistrationIdentifier: contact.registrationDetails?.registrationId,
|
|
283
|
+
vatIdentifier: contact.registrationDetails?.vatId || contact.vatNumber,
|
|
284
|
+
electronicAddress: contact.email || contact.contact?.email
|
|
285
|
+
};
|
|
286
|
+
} else {
|
|
287
|
+
return {
|
|
288
|
+
name: contact.name || `${contact.firstName || ''} ${contact.lastName || ''}`.trim(),
|
|
289
|
+
identifier: contact.registrationDetails?.registrationId,
|
|
290
|
+
vatIdentifier: contact.registrationDetails?.vatId || contact.vatNumber,
|
|
291
|
+
electronicAddress: contact.email
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Map address
|
|
298
|
+
*/
|
|
299
|
+
private mapAddress(party: EInvoice['from'] | EInvoice['to']): PostalAddress {
|
|
300
|
+
const contact = party as any;
|
|
301
|
+
const address: PostalAddress = {
|
|
302
|
+
countryCode: contact.address?.country || contact.country || ''
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
if (contact.address) {
|
|
306
|
+
if (typeof contact.address === 'string') {
|
|
307
|
+
const addressParts = contact.address.split(',').map((s: string) => s.trim());
|
|
308
|
+
address.addressLine1 = addressParts[0];
|
|
309
|
+
if (addressParts.length > 1) address.addressLine2 = addressParts[1];
|
|
310
|
+
} else if (typeof contact.address === 'object') {
|
|
311
|
+
address.addressLine1 = [contact.address.streetName, contact.address.houseNumber].filter(Boolean).join(' ');
|
|
312
|
+
address.city = contact.address.city;
|
|
313
|
+
address.postCode = contact.address.postalCode;
|
|
314
|
+
address.countryCode = contact.address.country || address.countryCode;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Support both nested and flat structures
|
|
319
|
+
if (!address.city) address.city = contact.city;
|
|
320
|
+
if (!address.postCode) address.postCode = contact.postalCode;
|
|
321
|
+
|
|
322
|
+
return address;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Map contact information
|
|
327
|
+
*/
|
|
328
|
+
private mapContact(party: EInvoice['from'] | EInvoice['to']): Contact | undefined {
|
|
329
|
+
const contact = party as any;
|
|
330
|
+
if (contact.type === 'company' && contact.contact) {
|
|
331
|
+
return {
|
|
332
|
+
contactPoint: contact.contact.name,
|
|
333
|
+
telephoneNumber: contact.contact.phone,
|
|
334
|
+
emailAddress: contact.contact.email
|
|
335
|
+
};
|
|
336
|
+
} else if (contact.type === 'person') {
|
|
337
|
+
return {
|
|
338
|
+
contactPoint: contact.name || `${contact.firstName || ''} ${contact.lastName || ''}`.trim(),
|
|
339
|
+
telephoneNumber: contact.phone,
|
|
340
|
+
emailAddress: contact.email
|
|
341
|
+
};
|
|
342
|
+
} else if (contact.email || contact.phone) {
|
|
343
|
+
// Fallback for any contact with email or phone
|
|
344
|
+
return {
|
|
345
|
+
contactPoint: contact.name,
|
|
346
|
+
telephoneNumber: contact.phone,
|
|
347
|
+
emailAddress: contact.email
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
return undefined;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Map delivery information
|
|
355
|
+
*/
|
|
356
|
+
private mapDelivery(invoice: EInvoice): DeliveryInformation | undefined {
|
|
357
|
+
const delivery = invoice.metadata?.extensions?.delivery;
|
|
358
|
+
if (!delivery) return undefined;
|
|
359
|
+
|
|
360
|
+
return {
|
|
361
|
+
name: delivery.name,
|
|
362
|
+
locationIdentifier: delivery.locationId,
|
|
363
|
+
actualDeliveryDate: delivery.actualDate,
|
|
364
|
+
deliveryAddress: delivery.address ? {
|
|
365
|
+
addressLine1: delivery.address.line1,
|
|
366
|
+
addressLine2: delivery.address.line2,
|
|
367
|
+
city: delivery.address.city,
|
|
368
|
+
postCode: delivery.address.postCode,
|
|
369
|
+
countryCode: delivery.address.countryCode
|
|
370
|
+
} : undefined
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Map payment instructions
|
|
376
|
+
*/
|
|
377
|
+
private mapPaymentInstructions(invoice: EInvoice): PaymentInstructions {
|
|
378
|
+
const paymentMeans = invoice.metadata?.extensions?.paymentMeans;
|
|
379
|
+
const paymentAccount = invoice.metadata?.extensions?.paymentAccount;
|
|
380
|
+
|
|
381
|
+
return {
|
|
382
|
+
paymentMeansTypeCode: paymentMeans?.paymentMeansCode || '30', // Default to credit transfer
|
|
383
|
+
paymentMeansText: paymentMeans?.paymentMeansText,
|
|
384
|
+
remittanceInformation: paymentMeans?.remittanceInformation,
|
|
385
|
+
paymentAccountIdentifier: paymentAccount?.iban,
|
|
386
|
+
paymentAccountName: paymentAccount?.accountName,
|
|
387
|
+
paymentServiceProviderIdentifier: paymentAccount?.bic || paymentAccount?.institutionName
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Map document totals
|
|
393
|
+
*/
|
|
394
|
+
private mapDocumentTotals(invoice: EInvoice): DocumentTotals {
|
|
395
|
+
return {
|
|
396
|
+
lineExtensionAmount: invoice.totalNet,
|
|
397
|
+
taxExclusiveAmount: invoice.totalNet,
|
|
398
|
+
taxInclusiveAmount: invoice.totalGross,
|
|
399
|
+
allowanceTotalAmount: invoice.metadata?.extensions?.documentAllowances?.reduce(
|
|
400
|
+
(sum, a) => sum + a.amount, 0
|
|
401
|
+
),
|
|
402
|
+
chargeTotalAmount: invoice.metadata?.extensions?.documentCharges?.reduce(
|
|
403
|
+
(sum, c) => sum + c.amount, 0
|
|
404
|
+
),
|
|
405
|
+
prepaidAmount: invoice.metadata?.extensions?.prepaidAmount,
|
|
406
|
+
roundingAmount: invoice.metadata?.extensions?.roundingAmount,
|
|
407
|
+
payableAmount: invoice.totalGross
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Map VAT breakdown
|
|
413
|
+
*/
|
|
414
|
+
private mapVATBreakdown(invoice: EInvoice): VATBreakdown[] | undefined {
|
|
415
|
+
const taxDetails = invoice.metadata?.extensions?.taxDetails;
|
|
416
|
+
if (!taxDetails) {
|
|
417
|
+
// Create default VAT breakdown from invoice totals
|
|
418
|
+
if (invoice.totalVat > 0) {
|
|
419
|
+
return [{
|
|
420
|
+
vatCategoryTaxableAmount: invoice.totalNet,
|
|
421
|
+
vatCategoryTaxAmount: invoice.totalVat,
|
|
422
|
+
vatCategoryCode: 'S', // Standard rate
|
|
423
|
+
vatCategoryRate: (invoice.totalVat / invoice.totalNet) * 100
|
|
424
|
+
}];
|
|
425
|
+
}
|
|
426
|
+
return undefined;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return taxDetails as VATBreakdown[];
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Map invoice lines
|
|
434
|
+
*/
|
|
435
|
+
private mapInvoiceLines(items: EInvoice['items']): InvoiceLine[] {
|
|
436
|
+
if (!items) return [];
|
|
437
|
+
|
|
438
|
+
return items.map((item, index) => ({
|
|
439
|
+
identifier: (index + 1).toString(),
|
|
440
|
+
note: (item as any).description || (item as any).text || '',
|
|
441
|
+
invoicedQuantity: item.unitQuantity,
|
|
442
|
+
invoicedQuantityUnitOfMeasureCode: item.unitType || 'C62',
|
|
443
|
+
lineExtensionAmount: item.unitNetPrice * item.unitQuantity,
|
|
444
|
+
purchaseOrderLineReference: (item as any).purchaseOrderLineRef,
|
|
445
|
+
buyerAccountingReference: (item as any).buyerAccountingRef,
|
|
446
|
+
period: (item as any).period,
|
|
447
|
+
allowances: (item as any).allowances,
|
|
448
|
+
charges: (item as any).charges,
|
|
449
|
+
priceDetails: {
|
|
450
|
+
itemNetPrice: item.unitNetPrice,
|
|
451
|
+
itemPriceDiscount: (item as any).priceDiscount,
|
|
452
|
+
itemGrossPrice: (item as any).grossPrice,
|
|
453
|
+
itemPriceBaseQuantity: (item as any).priceBaseQuantity || 1
|
|
454
|
+
},
|
|
455
|
+
vatInformation: {
|
|
456
|
+
categoryCode: this.mapVATCategory(item.vatPercentage),
|
|
457
|
+
rate: item.vatPercentage
|
|
458
|
+
},
|
|
459
|
+
itemInformation: {
|
|
460
|
+
name: item.name,
|
|
461
|
+
description: (item as any).description || (item as any).text || '',
|
|
462
|
+
sellersIdentifier: item.articleNumber,
|
|
463
|
+
buyersIdentifier: (item as any).buyersItemId,
|
|
464
|
+
standardIdentifier: (item as any).gtin || (item as any).ean,
|
|
465
|
+
classificationIdentifier: (item as any).unspsc,
|
|
466
|
+
originCountryCode: (item as any).originCountry,
|
|
467
|
+
attributes: (item as any).attributes
|
|
468
|
+
}
|
|
469
|
+
}));
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Map VAT category from percentage
|
|
474
|
+
*/
|
|
475
|
+
private mapVATCategory(percentage?: number): string {
|
|
476
|
+
if (percentage === undefined || percentage === null) return 'S';
|
|
477
|
+
if (percentage === 0) return 'Z';
|
|
478
|
+
if (percentage > 0) return 'S';
|
|
479
|
+
return 'E'; // Exempt
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Reverse map seller
|
|
484
|
+
*/
|
|
485
|
+
private reverseMapSeller(seller: Seller & { postalAddress: PostalAddress }): EInvoice['from'] {
|
|
486
|
+
const isCompany = seller.legalRegistrationIdentifier || seller.tradingName;
|
|
487
|
+
|
|
488
|
+
return {
|
|
489
|
+
type: isCompany ? 'company' : 'person',
|
|
490
|
+
name: seller.name,
|
|
491
|
+
description: seller.additionalLegalInfo || '',
|
|
492
|
+
address: {
|
|
493
|
+
streetName: seller.postalAddress.addressLine1 || '',
|
|
494
|
+
houseNumber: '',
|
|
495
|
+
city: seller.postalAddress.city || '',
|
|
496
|
+
postalCode: seller.postalAddress.postCode || '',
|
|
497
|
+
country: seller.postalAddress.countryCode || ''
|
|
498
|
+
},
|
|
499
|
+
registrationDetails: {
|
|
500
|
+
vatId: seller.vatIdentifier || '',
|
|
501
|
+
registrationId: seller.identifier || seller.legalRegistrationIdentifier || '',
|
|
502
|
+
registrationName: seller.name
|
|
503
|
+
},
|
|
504
|
+
status: 'active',
|
|
505
|
+
foundedDate: {
|
|
506
|
+
year: new Date().getFullYear(),
|
|
507
|
+
month: new Date().getMonth() + 1,
|
|
508
|
+
day: new Date().getDate()
|
|
509
|
+
}
|
|
510
|
+
} as any;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Reverse map buyer
|
|
515
|
+
*/
|
|
516
|
+
private reverseMapBuyer(buyer: Buyer & { postalAddress: PostalAddress }): EInvoice['to'] {
|
|
517
|
+
const isCompany = buyer.legalRegistrationIdentifier || buyer.tradingName;
|
|
518
|
+
|
|
519
|
+
return {
|
|
520
|
+
type: isCompany ? 'company' : 'person',
|
|
521
|
+
name: buyer.name,
|
|
522
|
+
description: '',
|
|
523
|
+
address: {
|
|
524
|
+
streetName: buyer.postalAddress.addressLine1 || '',
|
|
525
|
+
houseNumber: '',
|
|
526
|
+
city: buyer.postalAddress.city || '',
|
|
527
|
+
postalCode: buyer.postalAddress.postCode || '',
|
|
528
|
+
country: buyer.postalAddress.countryCode || ''
|
|
529
|
+
},
|
|
530
|
+
registrationDetails: {
|
|
531
|
+
vatId: buyer.vatIdentifier || '',
|
|
532
|
+
registrationId: buyer.identifier || buyer.legalRegistrationIdentifier || '',
|
|
533
|
+
registrationName: buyer.name
|
|
534
|
+
},
|
|
535
|
+
status: 'active',
|
|
536
|
+
foundedDate: {
|
|
537
|
+
year: new Date().getFullYear(),
|
|
538
|
+
month: new Date().getMonth() + 1,
|
|
539
|
+
day: new Date().getDate()
|
|
540
|
+
}
|
|
541
|
+
} as any;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Reverse map invoice lines
|
|
546
|
+
*/
|
|
547
|
+
private reverseMapInvoiceLines(lines: InvoiceLine[]): EInvoice['items'] {
|
|
548
|
+
return lines.map((line, index) => ({
|
|
549
|
+
position: index + 1,
|
|
550
|
+
name: line.itemInformation.name,
|
|
551
|
+
description: line.itemInformation.description || '',
|
|
552
|
+
unitQuantity: line.invoicedQuantity,
|
|
553
|
+
unitType: line.invoicedQuantityUnitOfMeasureCode,
|
|
554
|
+
unitNetPrice: line.priceDetails.itemNetPrice,
|
|
555
|
+
vatPercentage: line.vatInformation.rate || 0,
|
|
556
|
+
articleNumber: line.itemInformation.sellersIdentifier || ''
|
|
557
|
+
}));
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Validate semantic model completeness
|
|
562
|
+
*/
|
|
563
|
+
public validateSemanticModel(model: EN16931SemanticModel): string[] {
|
|
564
|
+
const errors: string[] = [];
|
|
565
|
+
|
|
566
|
+
// Check mandatory fields
|
|
567
|
+
if (!model.documentInformation.invoiceNumber) {
|
|
568
|
+
errors.push('BT-1: Invoice number is mandatory');
|
|
569
|
+
}
|
|
570
|
+
if (!model.documentInformation.issueDate) {
|
|
571
|
+
errors.push('BT-2: Invoice issue date is mandatory');
|
|
572
|
+
}
|
|
573
|
+
if (!model.documentInformation.typeCode) {
|
|
574
|
+
errors.push('BT-3: Invoice type code is mandatory');
|
|
575
|
+
}
|
|
576
|
+
if (!model.documentInformation.currencyCode) {
|
|
577
|
+
errors.push('BT-5: Invoice currency code is mandatory');
|
|
578
|
+
}
|
|
579
|
+
if (!model.seller?.name) {
|
|
580
|
+
errors.push('BT-27: Seller name is mandatory');
|
|
581
|
+
}
|
|
582
|
+
if (!model.seller?.postalAddress?.countryCode) {
|
|
583
|
+
errors.push('BT-40: Seller country code is mandatory');
|
|
584
|
+
}
|
|
585
|
+
if (!model.buyer?.name) {
|
|
586
|
+
errors.push('BT-44: Buyer name is mandatory');
|
|
587
|
+
}
|
|
588
|
+
if (!model.buyer?.postalAddress?.countryCode) {
|
|
589
|
+
errors.push('BT-55: Buyer country code is mandatory');
|
|
590
|
+
}
|
|
591
|
+
if (!model.documentTotals) {
|
|
592
|
+
errors.push('BG-22: Document totals are mandatory');
|
|
593
|
+
}
|
|
594
|
+
if (!model.invoiceLines || model.invoiceLines.length === 0) {
|
|
595
|
+
errors.push('BG-25: At least one invoice line is mandatory');
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
return errors;
|
|
599
|
+
}
|
|
600
|
+
}
|