@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.
Files changed (171) hide show
  1. package/dist_ts/00_commitinfo_data.d.ts +8 -0
  2. package/dist_ts/00_commitinfo_data.js +9 -0
  3. package/dist_ts/classes.decoder.d.ts +40 -0
  4. package/dist_ts/classes.decoder.js +320 -0
  5. package/dist_ts/classes.encoder.d.ts +51 -0
  6. package/dist_ts/classes.encoder.js +293 -0
  7. package/dist_ts/classes.xinvoice.d.ts +136 -0
  8. package/dist_ts/classes.xinvoice.js +352 -0
  9. package/dist_ts/einvoice.d.ts +217 -0
  10. package/dist_ts/einvoice.js +505 -0
  11. package/dist_ts/errors.d.ts +121 -0
  12. package/dist_ts/errors.js +241 -0
  13. package/dist_ts/formats/base/base.decoder.d.ts +32 -0
  14. package/dist_ts/formats/base/base.decoder.js +52 -0
  15. package/dist_ts/formats/base/base.encoder.d.ts +13 -0
  16. package/dist_ts/formats/base/base.encoder.js +7 -0
  17. package/dist_ts/formats/base/base.validator.d.ts +44 -0
  18. package/dist_ts/formats/base/base.validator.js +39 -0
  19. package/dist_ts/formats/base.decoder.d.ts +19 -0
  20. package/dist_ts/formats/base.decoder.js +130 -0
  21. package/dist_ts/formats/base.validator.d.ts +44 -0
  22. package/dist_ts/formats/base.validator.js +39 -0
  23. package/dist_ts/formats/cii/cii.decoder.d.ts +61 -0
  24. package/dist_ts/formats/cii/cii.decoder.js +111 -0
  25. package/dist_ts/formats/cii/cii.encoder.d.ts +43 -0
  26. package/dist_ts/formats/cii/cii.encoder.js +48 -0
  27. package/dist_ts/formats/cii/cii.types.d.ts +31 -0
  28. package/dist_ts/formats/cii/cii.types.js +40 -0
  29. package/dist_ts/formats/cii/cii.validator.d.ts +56 -0
  30. package/dist_ts/formats/cii/cii.validator.js +146 -0
  31. package/dist_ts/formats/cii/facturx/facturx.decoder.d.ts +43 -0
  32. package/dist_ts/formats/cii/facturx/facturx.decoder.js +207 -0
  33. package/dist_ts/formats/cii/facturx/facturx.encoder.d.ts +82 -0
  34. package/dist_ts/formats/cii/facturx/facturx.encoder.js +400 -0
  35. package/dist_ts/formats/cii/facturx/facturx.types.d.ts +10 -0
  36. package/dist_ts/formats/cii/facturx/facturx.types.js +15 -0
  37. package/dist_ts/formats/cii/facturx/facturx.validator.d.ts +32 -0
  38. package/dist_ts/formats/cii/facturx/facturx.validator.js +138 -0
  39. package/dist_ts/formats/cii/zugferd/zugferd.decoder.d.ts +43 -0
  40. package/dist_ts/formats/cii/zugferd/zugferd.decoder.js +218 -0
  41. package/dist_ts/formats/cii/zugferd/zugferd.encoder.d.ts +97 -0
  42. package/dist_ts/formats/cii/zugferd/zugferd.encoder.js +550 -0
  43. package/dist_ts/formats/cii/zugferd/zugferd.types.d.ts +10 -0
  44. package/dist_ts/formats/cii/zugferd/zugferd.types.js +15 -0
  45. package/dist_ts/formats/cii/zugferd/zugferd.v1.decoder.d.ts +49 -0
  46. package/dist_ts/formats/cii/zugferd/zugferd.v1.decoder.js +229 -0
  47. package/dist_ts/formats/cii/zugferd/zugferd.validator.d.ts +16 -0
  48. package/dist_ts/formats/cii/zugferd/zugferd.validator.js +29 -0
  49. package/dist_ts/formats/decoder.factory.d.ts +15 -0
  50. package/dist_ts/formats/decoder.factory.js +46 -0
  51. package/dist_ts/formats/factories/decoder.factory.d.ts +13 -0
  52. package/dist_ts/formats/factories/decoder.factory.js +56 -0
  53. package/dist_ts/formats/factories/encoder.factory.d.ts +14 -0
  54. package/dist_ts/formats/factories/encoder.factory.js +40 -0
  55. package/dist_ts/formats/factories/validator.factory.d.ts +12 -0
  56. package/dist_ts/formats/factories/validator.factory.js +110 -0
  57. package/dist_ts/formats/factorx.decoder.d.ts +18 -0
  58. package/dist_ts/formats/factorx.decoder.js +178 -0
  59. package/dist_ts/formats/factorx.encoder.d.ts +51 -0
  60. package/dist_ts/formats/factorx.encoder.js +293 -0
  61. package/dist_ts/formats/facturx.decoder.d.ts +18 -0
  62. package/dist_ts/formats/facturx.decoder.js +210 -0
  63. package/dist_ts/formats/facturx.encoder.d.ts +51 -0
  64. package/dist_ts/formats/facturx.encoder.js +293 -0
  65. package/dist_ts/formats/facturx.validator.d.ts +67 -0
  66. package/dist_ts/formats/facturx.validator.js +266 -0
  67. package/dist_ts/formats/pdf/extractors/associated.extractor.d.ts +14 -0
  68. package/dist_ts/formats/pdf/extractors/associated.extractor.js +68 -0
  69. package/dist_ts/formats/pdf/extractors/base.extractor.d.ts +92 -0
  70. package/dist_ts/formats/pdf/extractors/base.extractor.js +319 -0
  71. package/dist_ts/formats/pdf/extractors/index.d.ts +4 -0
  72. package/dist_ts/formats/pdf/extractors/index.js +5 -0
  73. package/dist_ts/formats/pdf/extractors/standard.extractor.d.ts +13 -0
  74. package/dist_ts/formats/pdf/extractors/standard.extractor.js +74 -0
  75. package/dist_ts/formats/pdf/extractors/text.extractor.d.ts +45 -0
  76. package/dist_ts/formats/pdf/extractors/text.extractor.js +151 -0
  77. package/dist_ts/formats/pdf/pdf.embedder.d.ts +69 -0
  78. package/dist_ts/formats/pdf/pdf.embedder.js +183 -0
  79. package/dist_ts/formats/pdf/pdf.extractor.d.ts +49 -0
  80. package/dist_ts/formats/pdf/pdf.extractor.js +98 -0
  81. package/dist_ts/formats/pdf/robust-pdf.extractor.d.ts +40 -0
  82. package/dist_ts/formats/pdf/robust-pdf.extractor.js +324 -0
  83. package/dist_ts/formats/ubl/en16931.ubl.validator.d.ts +18 -0
  84. package/dist_ts/formats/ubl/en16931.ubl.validator.js +169 -0
  85. package/dist_ts/formats/ubl/generic/ubl.encoder.d.ts +151 -0
  86. package/dist_ts/formats/ubl/generic/ubl.encoder.js +892 -0
  87. package/dist_ts/formats/ubl/ubl.decoder.d.ts +61 -0
  88. package/dist_ts/formats/ubl/ubl.decoder.js +95 -0
  89. package/dist_ts/formats/ubl/ubl.encoder.d.ts +38 -0
  90. package/dist_ts/formats/ubl/ubl.encoder.js +46 -0
  91. package/dist_ts/formats/ubl/ubl.types.d.ts +16 -0
  92. package/dist_ts/formats/ubl/ubl.types.js +21 -0
  93. package/dist_ts/formats/ubl/ubl.validator.d.ts +50 -0
  94. package/dist_ts/formats/ubl/ubl.validator.js +112 -0
  95. package/dist_ts/formats/ubl/xrechnung/xrechnung.decoder.d.ts +34 -0
  96. package/dist_ts/formats/ubl/xrechnung/xrechnung.decoder.js +424 -0
  97. package/dist_ts/formats/ubl/xrechnung/xrechnung.encoder.d.ts +75 -0
  98. package/dist_ts/formats/ubl/xrechnung/xrechnung.encoder.js +555 -0
  99. package/dist_ts/formats/ubl/xrechnung.validator.d.ts +15 -0
  100. package/dist_ts/formats/ubl/xrechnung.validator.js +107 -0
  101. package/dist_ts/formats/ubl.validator.d.ts +82 -0
  102. package/dist_ts/formats/ubl.validator.js +306 -0
  103. package/dist_ts/formats/utils/format.detector.d.ts +62 -0
  104. package/dist_ts/formats/utils/format.detector.js +249 -0
  105. package/dist_ts/formats/validation/en16931.validator.d.ts +23 -0
  106. package/dist_ts/formats/validation/en16931.validator.js +119 -0
  107. package/dist_ts/formats/validator.factory.d.ts +18 -0
  108. package/dist_ts/formats/validator.factory.js +78 -0
  109. package/dist_ts/formats/xinvoice.decoder.d.ts +28 -0
  110. package/dist_ts/formats/xinvoice.decoder.js +332 -0
  111. package/dist_ts/formats/xinvoice.encoder.d.ts +19 -0
  112. package/dist_ts/formats/xinvoice.encoder.js +282 -0
  113. package/dist_ts/index.d.ts +51 -0
  114. package/dist_ts/index.js +90 -0
  115. package/dist_ts/interfaces/common.d.ts +79 -0
  116. package/dist_ts/interfaces/common.js +24 -0
  117. package/dist_ts/interfaces.d.ts +87 -0
  118. package/dist_ts/interfaces.js +23 -0
  119. package/dist_ts/plugins.d.ts +14 -0
  120. package/dist_ts/plugins.js +31 -0
  121. package/npmextra.json +35 -0
  122. package/package.json +71 -0
  123. package/readme.hints.md +1107 -0
  124. package/readme.howtofixtests.md +38 -0
  125. package/readme.literature.md +1 -0
  126. package/readme.md +998 -0
  127. package/readme.plan.md +481 -0
  128. package/ts/00_commitinfo_data.ts +8 -0
  129. package/ts/einvoice.ts +603 -0
  130. package/ts/errors.ts +341 -0
  131. package/ts/formats/base/base.decoder.ts +68 -0
  132. package/ts/formats/base/base.encoder.ts +14 -0
  133. package/ts/formats/base/base.validator.ts +64 -0
  134. package/ts/formats/cii/cii.decoder.ts +139 -0
  135. package/ts/formats/cii/cii.encoder.ts +64 -0
  136. package/ts/formats/cii/cii.types.ts +44 -0
  137. package/ts/formats/cii/cii.validator.ts +171 -0
  138. package/ts/formats/cii/facturx/facturx.decoder.ts +243 -0
  139. package/ts/formats/cii/facturx/facturx.encoder.ts +483 -0
  140. package/ts/formats/cii/facturx/facturx.types.ts +18 -0
  141. package/ts/formats/cii/facturx/facturx.validator.ts +180 -0
  142. package/ts/formats/cii/zugferd/zugferd.decoder.ts +256 -0
  143. package/ts/formats/cii/zugferd/zugferd.encoder.ts +661 -0
  144. package/ts/formats/cii/zugferd/zugferd.types.ts +18 -0
  145. package/ts/formats/cii/zugferd/zugferd.v1.decoder.ts +267 -0
  146. package/ts/formats/cii/zugferd/zugferd.validator.ts +31 -0
  147. package/ts/formats/factories/decoder.factory.ts +62 -0
  148. package/ts/formats/factories/encoder.factory.ts +47 -0
  149. package/ts/formats/factories/validator.factory.ts +134 -0
  150. package/ts/formats/pdf/extractors/associated.extractor.ts +78 -0
  151. package/ts/formats/pdf/extractors/base.extractor.ts +355 -0
  152. package/ts/formats/pdf/extractors/index.ts +4 -0
  153. package/ts/formats/pdf/extractors/standard.extractor.ts +86 -0
  154. package/ts/formats/pdf/extractors/text.extractor.ts +162 -0
  155. package/ts/formats/pdf/pdf.embedder.ts +242 -0
  156. package/ts/formats/pdf/pdf.extractor.ts +141 -0
  157. package/ts/formats/ubl/en16931.ubl.validator.ts +216 -0
  158. package/ts/formats/ubl/generic/ubl.encoder.ts +1041 -0
  159. package/ts/formats/ubl/ubl.decoder.ts +121 -0
  160. package/ts/formats/ubl/ubl.encoder.ts +63 -0
  161. package/ts/formats/ubl/ubl.types.ts +22 -0
  162. package/ts/formats/ubl/ubl.validator.ts +133 -0
  163. package/ts/formats/ubl/xrechnung/xrechnung.decoder.ts +471 -0
  164. package/ts/formats/ubl/xrechnung/xrechnung.encoder.ts +619 -0
  165. package/ts/formats/ubl/xrechnung.validator.ts +185 -0
  166. package/ts/formats/utils/format.detector.ts +306 -0
  167. package/ts/formats/validation/en16931.validator.ts +135 -0
  168. package/ts/index.ts +164 -0
  169. package/ts/interfaces/common.ts +90 -0
  170. package/ts/interfaces.ts +98 -0
  171. package/ts/plugins.ts +61 -0
@@ -0,0 +1,185 @@
1
+ import { EN16931UBLValidator } from './en16931.ubl.validator.js';
2
+
3
+ /**
4
+ * XRechnung-specific validator that extends EN16931 validation
5
+ * Implements additional German CIUS (Core Invoice Usage Specification) rules
6
+ */
7
+ export class XRechnungValidator extends EN16931UBLValidator {
8
+ /**
9
+ * Validates XRechnung-specific structure requirements
10
+ */
11
+ protected validateStructure(): boolean {
12
+ // First validate EN16931 structure
13
+ let valid = super.validateStructure();
14
+
15
+ // XRechnung-specific: Check for proper customization ID
16
+ const customizationID = this.getText('//cbc:CustomizationID');
17
+ if (!customizationID || !customizationID.includes('xrechnung')) {
18
+ this.addError(
19
+ 'XRECH-STRUCT-1',
20
+ 'XRechnung customization ID is missing or invalid',
21
+ '//cbc:CustomizationID'
22
+ );
23
+ valid = false;
24
+ }
25
+
26
+ return valid;
27
+ }
28
+
29
+ /**
30
+ * Validates XRechnung-specific business rules
31
+ */
32
+ protected validateBusinessRules(): boolean {
33
+ // First validate EN16931 business rules
34
+ let valid = super.validateBusinessRules();
35
+
36
+ // BR-DE-1: Payment terms (BT-20) or Payment due date (BT-9) shall be provided.
37
+ if (!this.exists('//cbc:PaymentDueDate') && !this.exists('//cac:PaymentTerms/cbc:Note')) {
38
+ this.addError(
39
+ 'BR-DE-1',
40
+ 'Payment terms or Payment due date shall be provided',
41
+ '//cac:PaymentTerms'
42
+ );
43
+ valid = false;
44
+ }
45
+
46
+ // BR-DE-2: The element "Buyer reference" (BT-10) shall be provided.
47
+ if (!this.exists('//cbc:BuyerReference')) {
48
+ this.addError(
49
+ 'BR-DE-2',
50
+ 'Buyer reference is required in XRechnung',
51
+ '//cbc:BuyerReference'
52
+ );
53
+ valid = false;
54
+ }
55
+
56
+ // BR-DE-5: In Germany, the element "Seller VAT identifier" (BT-31) shall be provided.
57
+ const sellerCountry = this.getText('//cac:AccountingSupplierParty//cac:PostalAddress//cac:Country/cbc:IdentificationCode');
58
+ if (sellerCountry === 'DE' && !this.exists('//cac:AccountingSupplierParty//cac:PartyTaxScheme[cac:TaxScheme/cbc:ID="VAT"]/cbc:CompanyID')) {
59
+ this.addError(
60
+ 'BR-DE-5',
61
+ 'Seller VAT identifier is required for German sellers',
62
+ '//cac:AccountingSupplierParty//cac:PartyTaxScheme'
63
+ );
64
+ valid = false;
65
+ }
66
+
67
+ // BR-DE-6: In Germany, the element "Buyer VAT identifier" (BT-48) shall be provided.
68
+ const buyerCountry = this.getText('//cac:AccountingCustomerParty//cac:PostalAddress//cac:Country/cbc:IdentificationCode');
69
+ if (buyerCountry === 'DE' && !this.exists('//cac:AccountingCustomerParty//cac:PartyTaxScheme[cac:TaxScheme/cbc:ID="VAT"]/cbc:CompanyID')) {
70
+ this.addError(
71
+ 'BR-DE-6',
72
+ 'Buyer VAT identifier is required for German buyers',
73
+ '//cac:AccountingCustomerParty//cac:PartyTaxScheme'
74
+ );
75
+ valid = false;
76
+ }
77
+
78
+ // BR-DE-7: The element "Seller city" (BT-37) shall be provided.
79
+ if (!this.exists('//cac:AccountingSupplierParty//cac:PostalAddress/cbc:CityName')) {
80
+ this.addError(
81
+ 'BR-DE-7',
82
+ 'Seller city is required',
83
+ '//cac:AccountingSupplierParty//cac:PostalAddress'
84
+ );
85
+ valid = false;
86
+ }
87
+
88
+ // BR-DE-8: The element "Seller post code" (BT-38) shall be provided.
89
+ if (!this.exists('//cac:AccountingSupplierParty//cac:PostalAddress/cbc:PostalZone')) {
90
+ this.addError(
91
+ 'BR-DE-8',
92
+ 'Seller post code is required',
93
+ '//cac:AccountingSupplierParty//cac:PostalAddress'
94
+ );
95
+ valid = false;
96
+ }
97
+
98
+ // BR-DE-9: The element "Buyer city" (BT-52) shall be provided.
99
+ if (!this.exists('//cac:AccountingCustomerParty//cac:PostalAddress/cbc:CityName')) {
100
+ this.addError(
101
+ 'BR-DE-9',
102
+ 'Buyer city is required',
103
+ '//cac:AccountingCustomerParty//cac:PostalAddress'
104
+ );
105
+ valid = false;
106
+ }
107
+
108
+ // BR-DE-10: The element "Buyer post code" (BT-53) shall be provided.
109
+ if (!this.exists('//cac:AccountingCustomerParty//cac:PostalAddress/cbc:PostalZone')) {
110
+ this.addError(
111
+ 'BR-DE-10',
112
+ 'Buyer post code is required',
113
+ '//cac:AccountingCustomerParty//cac:PostalAddress'
114
+ );
115
+ valid = false;
116
+ }
117
+
118
+ // BR-DE-11: The element "Seller contact telephone number" (BT-42) shall be provided.
119
+ if (!this.exists('//cac:AccountingSupplierParty//cac:Contact/cbc:Telephone')) {
120
+ this.addError(
121
+ 'BR-DE-11',
122
+ 'Seller contact telephone number is required',
123
+ '//cac:AccountingSupplierParty//cac:Contact'
124
+ );
125
+ valid = false;
126
+ }
127
+
128
+ // BR-DE-12: The element "Seller contact email address" (BT-43) shall be provided.
129
+ if (!this.exists('//cac:AccountingSupplierParty//cac:Contact/cbc:ElectronicMail')) {
130
+ this.addError(
131
+ 'BR-DE-12',
132
+ 'Seller contact email address is required',
133
+ '//cac:AccountingSupplierParty//cac:Contact'
134
+ );
135
+ valid = false;
136
+ }
137
+
138
+ // BR-DE-13: The element "Buyer electronic address" (BT-49) shall be provided.
139
+ if (!this.exists('//cac:AccountingCustomerParty//cac:Party/cbc:EndpointID')) {
140
+ this.addError(
141
+ 'BR-DE-13',
142
+ 'Buyer electronic address (EndpointID) is required',
143
+ '//cac:AccountingCustomerParty//cac:Party'
144
+ );
145
+ valid = false;
146
+ }
147
+
148
+ // BR-DE-14: The element "Payment means type code" (BT-81) shall be provided.
149
+ if (!this.exists('//cac:PaymentMeans/cbc:PaymentMeansCode')) {
150
+ this.addError(
151
+ 'BR-DE-14',
152
+ 'Payment means type code is required',
153
+ '//cac:PaymentMeans'
154
+ );
155
+ valid = false;
156
+ }
157
+
158
+ // BR-DE-15: The element "Invoice line identifier" (BT-126) shall be provided.
159
+ const invoiceLines = this.select('//cac:InvoiceLine | //cac:CreditNoteLine', this.doc) as Node[];
160
+ for (let i = 0; i < invoiceLines.length; i++) {
161
+ const line = invoiceLines[i];
162
+ if (!this.exists('./cbc:ID', line)) {
163
+ this.addError(
164
+ 'BR-DE-15',
165
+ `Invoice line ${i + 1} is missing identifier`,
166
+ `//cac:InvoiceLine[${i + 1}]`
167
+ );
168
+ valid = false;
169
+ }
170
+ }
171
+
172
+ // German VAT ID format validation
173
+ const supplierVatId = this.getText('//cac:AccountingSupplierParty//cbc:CompanyID[../cac:TaxScheme/cbc:ID="VAT"]');
174
+ if (supplierVatId && supplierVatId.startsWith('DE') && !/^DE[0-9]{9}$/.test(supplierVatId)) {
175
+ this.addError(
176
+ 'BR-DE-VAT',
177
+ 'German VAT ID format is invalid (must be DE followed by 9 digits)',
178
+ '//cac:AccountingSupplierParty//cbc:CompanyID'
179
+ );
180
+ valid = false;
181
+ }
182
+
183
+ return valid;
184
+ }
185
+ }
@@ -0,0 +1,306 @@
1
+ import { InvoiceFormat } from '../../interfaces/common.js';
2
+ import { DOMParser, xpath } from '../../plugins.js';
3
+ import { CII_PROFILE_IDS, ZUGFERD_V1_NAMESPACES } from '../cii/cii.types.js';
4
+
5
+ /**
6
+ * Utility class for detecting invoice formats
7
+ */
8
+ export class FormatDetector {
9
+ /**
10
+ * Detects the format of an XML document
11
+ * @param xml XML content to analyze
12
+ * @returns Detected invoice format
13
+ */
14
+ public static detectFormat(xml: string): InvoiceFormat {
15
+ try {
16
+ // Quick check for empty or invalid XML
17
+ if (!xml || typeof xml !== 'string' || xml.trim().length === 0) {
18
+ return InvoiceFormat.UNKNOWN;
19
+ }
20
+
21
+ // Quick string-based pre-checks for performance
22
+ const quickCheck = FormatDetector.quickFormatCheck(xml);
23
+ if (quickCheck !== InvoiceFormat.UNKNOWN) {
24
+ return quickCheck;
25
+ }
26
+
27
+ // More thorough parsing-based checks
28
+ const doc = new DOMParser().parseFromString(xml, 'application/xml');
29
+ const root = doc.documentElement;
30
+
31
+ if (!root) {
32
+ return InvoiceFormat.UNKNOWN;
33
+ }
34
+
35
+ // UBL detection (Invoice or CreditNote root element)
36
+ if (FormatDetector.isUBLFormat(root)) {
37
+ // Check for XRechnung customization
38
+ if (FormatDetector.isXRechnungFormat(doc)) {
39
+ return InvoiceFormat.XRECHNUNG;
40
+ }
41
+ return InvoiceFormat.UBL;
42
+ }
43
+
44
+ // Factur-X/ZUGFeRD detection (CrossIndustryInvoice root element)
45
+ if (FormatDetector.isCIIFormat(root)) {
46
+ return FormatDetector.detectCIIFormat(doc, xml);
47
+ }
48
+
49
+ // ZUGFeRD v1 detection (CrossIndustryDocument root element)
50
+ if (FormatDetector.isZUGFeRDV1Format(root)) {
51
+ return InvoiceFormat.ZUGFERD;
52
+ }
53
+
54
+ // FatturaPA detection
55
+ if (FormatDetector.isFatturaPAFormat(root)) {
56
+ return InvoiceFormat.FATTURAPA;
57
+ }
58
+
59
+ return InvoiceFormat.UNKNOWN;
60
+ } catch (error) {
61
+ console.error('Error detecting format:', error);
62
+ return InvoiceFormat.UNKNOWN;
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Performs a quick format check based on string content
68
+ * This is faster than full XML parsing for obvious cases
69
+ * @param xml XML string
70
+ * @returns Detected format or UNKNOWN if more analysis is needed
71
+ */
72
+ private static quickFormatCheck(xml: string): InvoiceFormat {
73
+ const lowerXml = xml.toLowerCase();
74
+
75
+ // Check for obvious Factur-X indicators
76
+ if (
77
+ lowerXml.includes('factur-x.eu') ||
78
+ lowerXml.includes('factur-x.xml') ||
79
+ lowerXml.includes('factur-x:') ||
80
+ lowerXml.includes('urn:cen.eu:en16931:2017') && lowerXml.includes('factur-x')
81
+ ) {
82
+ return InvoiceFormat.FACTURX;
83
+ }
84
+
85
+ // Check for obvious ZUGFeRD indicators
86
+ if (
87
+ lowerXml.includes('zugferd:') ||
88
+ lowerXml.includes('zugferd-invoice.xml') ||
89
+ lowerXml.includes('urn:ferd:') ||
90
+ lowerXml.includes('urn:zugferd')
91
+ ) {
92
+ return InvoiceFormat.ZUGFERD;
93
+ }
94
+
95
+ // Check for obvious XRechnung indicators
96
+ if (
97
+ lowerXml.includes('xrechnung') ||
98
+ lowerXml.includes('urn:xoev-de:kosit:standard:xrechnung')
99
+ ) {
100
+ return InvoiceFormat.XRECHNUNG;
101
+ }
102
+
103
+ // Check for obvious FatturaPA indicators
104
+ if (
105
+ lowerXml.includes('fatturapa') ||
106
+ lowerXml.includes('fattura elettronica') ||
107
+ lowerXml.includes('fatturaelettronica')
108
+ ) {
109
+ return InvoiceFormat.FATTURAPA;
110
+ }
111
+
112
+ // Need more analysis
113
+ return InvoiceFormat.UNKNOWN;
114
+ }
115
+
116
+ /**
117
+ * Checks if the document is a UBL format
118
+ * @param root Root element
119
+ * @returns True if it's a UBL format
120
+ */
121
+ private static isUBLFormat(root: Element): boolean {
122
+ return (
123
+ root.nodeName === 'Invoice' ||
124
+ root.nodeName === 'CreditNote' ||
125
+ root.nodeName === 'ubl:Invoice' ||
126
+ root.nodeName === 'ubl:CreditNote' ||
127
+ root.nodeName.endsWith(':Invoice') ||
128
+ root.nodeName.endsWith(':CreditNote')
129
+ );
130
+ }
131
+
132
+ /**
133
+ * Checks if the document is an XRechnung format
134
+ * @param doc XML document
135
+ * @returns True if it's an XRechnung format
136
+ */
137
+ private static isXRechnungFormat(doc: Document): boolean {
138
+ try {
139
+ // Set up namespaces for XPath queries
140
+ const namespaces = {
141
+ 'cbc': 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2',
142
+ 'ubl': 'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2'
143
+ };
144
+
145
+ // Create XPath selector with namespaces
146
+ const select = xpath.useNamespaces(namespaces);
147
+
148
+ // Use getElementsByTagName directly for more reliable results
149
+ const customizationNodes = doc.getElementsByTagName('cbc:CustomizationID');
150
+
151
+ // Check if any CustomizationID node contains "xrechnung"
152
+ for (let i = 0; i < customizationNodes.length; i++) {
153
+ const node = customizationNodes[i];
154
+ if (node.textContent && node.textContent.includes('xrechnung')) {
155
+ return true;
156
+ }
157
+ }
158
+
159
+ return false;
160
+ } catch (error) {
161
+ console.warn('Error checking for XRechnung format:', error);
162
+ // If direct DOM access fails, try a string-based approach
163
+ const xmlStr = new XMLSerializer().serializeToString(doc);
164
+ return xmlStr.includes('xrechnung') || xmlStr.includes('XRechnung');
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Checks if the document is a CII format (Factur-X/ZUGFeRD v2+)
170
+ * @param root Root element
171
+ * @returns True if it's a CII format
172
+ */
173
+ private static isCIIFormat(root: Element): boolean {
174
+ return (
175
+ root.nodeName === 'rsm:CrossIndustryInvoice' ||
176
+ root.nodeName === 'CrossIndustryInvoice' ||
177
+ root.nodeName.endsWith(':CrossIndustryInvoice')
178
+ );
179
+ }
180
+
181
+ /**
182
+ * Checks if the document is a ZUGFeRD v1 format
183
+ * @param root Root element
184
+ * @returns True if it's a ZUGFeRD v1 format
185
+ */
186
+ private static isZUGFeRDV1Format(root: Element): boolean {
187
+ return (
188
+ root.nodeName === 'rsm:CrossIndustryDocument' ||
189
+ root.nodeName === 'CrossIndustryDocument' ||
190
+ root.nodeName === 'ram:CrossIndustryDocument' ||
191
+ root.nodeName.endsWith(':CrossIndustryDocument')
192
+ );
193
+ }
194
+
195
+ /**
196
+ * Checks if the document is a FatturaPA format
197
+ * @param root Root element
198
+ * @returns True if it's a FatturaPA format
199
+ */
200
+ private static isFatturaPAFormat(root: Element): boolean {
201
+ return (
202
+ root.nodeName === 'FatturaElettronica' ||
203
+ (root.getAttribute('xmlns') && root.getAttribute('xmlns')!.includes('fatturapa.gov.it'))
204
+ );
205
+ }
206
+
207
+ /**
208
+ * Detects the specific CII format (Factur-X vs ZUGFeRD)
209
+ * @param doc XML document
210
+ * @param xml Original XML string for fallback checks
211
+ * @returns Detected format
212
+ */
213
+ private static detectCIIFormat(doc: Document, xml: string): InvoiceFormat {
214
+ try {
215
+ // Use direct DOM traversal instead of XPath for more reliable behavior
216
+ const contextNodes = doc.getElementsByTagNameNS(
217
+ 'urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100',
218
+ 'ExchangedDocumentContext'
219
+ );
220
+
221
+ if (contextNodes.length === 0) {
222
+ // Try without namespace
223
+ const noNsContextNodes = doc.getElementsByTagName('ExchangedDocumentContext');
224
+ if (noNsContextNodes.length === 0) {
225
+ // Fallback to string-based detection
226
+ return FormatDetector.detectCIIFormatFromString(xml);
227
+ }
228
+ }
229
+
230
+ // Loop through all potential context nodes
231
+ const allContextNodes = [...Array.from(contextNodes), ...Array.from(doc.getElementsByTagName('ExchangedDocumentContext'))];
232
+
233
+ for (const contextNode of allContextNodes) {
234
+ // Find guideline parameter
235
+ const guidelineNodes = contextNode.getElementsByTagName('ram:GuidelineSpecifiedDocumentContextParameter');
236
+
237
+ if (guidelineNodes.length === 0) {
238
+ continue;
239
+ }
240
+
241
+ for (const guidelineNode of Array.from(guidelineNodes)) {
242
+ // Find ID element
243
+ const idNodes = guidelineNode.getElementsByTagName('ram:ID');
244
+
245
+ if (idNodes.length === 0) {
246
+ continue;
247
+ }
248
+
249
+ for (const idNode of Array.from(idNodes)) {
250
+ const profileText = idNode.textContent || '';
251
+
252
+ // Check for ZUGFeRD profiles (v1 and v2)
253
+ if (
254
+ profileText.includes('zugferd') ||
255
+ profileText.includes('urn:ferd:') ||
256
+ profileText === CII_PROFILE_IDS.ZUGFERD_BASIC ||
257
+ profileText === CII_PROFILE_IDS.ZUGFERD_COMFORT ||
258
+ profileText === CII_PROFILE_IDS.ZUGFERD_EXTENDED ||
259
+ profileText === CII_PROFILE_IDS.ZUGFERD_V1_BASIC ||
260
+ profileText === CII_PROFILE_IDS.ZUGFERD_V1_COMFORT ||
261
+ profileText === CII_PROFILE_IDS.ZUGFERD_V1_EXTENDED
262
+ ) {
263
+ return InvoiceFormat.ZUGFERD;
264
+ }
265
+
266
+ // Check for Factur-X profiles
267
+ if (
268
+ profileText.includes('factur-x') ||
269
+ profileText === CII_PROFILE_IDS.FACTURX_MINIMUM ||
270
+ profileText === CII_PROFILE_IDS.FACTURX_BASIC ||
271
+ profileText === CII_PROFILE_IDS.FACTURX_EN16931
272
+ ) {
273
+ return InvoiceFormat.FACTURX;
274
+ }
275
+ }
276
+ }
277
+ }
278
+
279
+ // If we reach here, fall back to string checking
280
+ return FormatDetector.detectCIIFormatFromString(xml);
281
+ } catch (error) {
282
+ console.warn('Error detecting CII format, falling back to generic CII:', error);
283
+ return FormatDetector.detectCIIFormatFromString(xml);
284
+ }
285
+ }
286
+
287
+ /**
288
+ * Fallback method to detect CII format from string content
289
+ * @param xml XML string
290
+ * @returns Detected format
291
+ */
292
+ private static detectCIIFormatFromString(xml: string): InvoiceFormat {
293
+ // Check for Factur-X indicators
294
+ if (xml.includes('factur-x') || xml.includes('Factur-X')) {
295
+ return InvoiceFormat.FACTURX;
296
+ }
297
+
298
+ // Check for ZUGFeRD indicators
299
+ if (xml.includes('zugferd') || xml.includes('ZUGFeRD')) {
300
+ return InvoiceFormat.ZUGFERD;
301
+ }
302
+
303
+ // Generic CII if we can't determine more specifically
304
+ return InvoiceFormat.CII;
305
+ }
306
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * EN16931 mandatory field validator
3
+ * Validates that invoices contain the minimum required fields per the EN16931 standard
4
+ */
5
+ export class EN16931Validator {
6
+ /**
7
+ * Valid ISO 4217 currency codes (common subset)
8
+ */
9
+ private static readonly VALID_CURRENCIES = [
10
+ 'AED', 'AFN', 'ALL', 'AMD', 'ANG', 'AOA', 'ARS', 'AUD', 'AWG', 'AZN',
11
+ 'BAM', 'BBD', 'BDT', 'BGN', 'BHD', 'BIF', 'BMD', 'BND', 'BOB', 'BRL',
12
+ 'BSD', 'BTN', 'BWP', 'BYN', 'BZD', 'CAD', 'CDF', 'CHF', 'CLP', 'CNY',
13
+ 'COP', 'CRC', 'CUC', 'CUP', 'CVE', 'CZK', 'DJF', 'DKK', 'DOP', 'DZD',
14
+ 'EGP', 'ERN', 'ETB', 'EUR', 'FJD', 'FKP', 'GBP', 'GEL', 'GGP', 'GHS',
15
+ 'GIP', 'GMD', 'GNF', 'GTQ', 'GYD', 'HKD', 'HNL', 'HRK', 'HTG', 'HUF',
16
+ 'IDR', 'ILS', 'IMP', 'INR', 'IQD', 'IRR', 'ISK', 'JEP', 'JMD', 'JOD',
17
+ 'JPY', 'KES', 'KGS', 'KHR', 'KMF', 'KPW', 'KRW', 'KWD', 'KYD', 'KZT',
18
+ 'LAK', 'LBP', 'LKR', 'LRD', 'LSL', 'LYD', 'MAD', 'MDL', 'MGA', 'MKD',
19
+ 'MMK', 'MNT', 'MOP', 'MRU', 'MUR', 'MVR', 'MWK', 'MXN', 'MYR', 'MZN',
20
+ 'NAD', 'NGN', 'NIO', 'NOK', 'NPR', 'NZD', 'OMR', 'PAB', 'PEN', 'PGK',
21
+ 'PHP', 'PKR', 'PLN', 'PYG', 'QAR', 'RON', 'RSD', 'RUB', 'RWF', 'SAR',
22
+ 'SBD', 'SCR', 'SDG', 'SEK', 'SGD', 'SHP', 'SLL', 'SOS', 'SPL', 'SRD',
23
+ 'STN', 'SYP', 'SZL', 'THB', 'TJS', 'TMT', 'TND', 'TOP', 'TRY', 'TTD',
24
+ 'TVD', 'TWD', 'TZS', 'UAH', 'UGX', 'USD', 'UYU', 'UZS', 'VEF', 'VND',
25
+ 'VUV', 'WST', 'XAF', 'XCD', 'XDR', 'XOF', 'XPF', 'YER', 'ZAR', 'ZMW',
26
+ 'ZWD'
27
+ ];
28
+
29
+ /**
30
+ * Validates that an invoice object contains all mandatory EN16931 fields
31
+ * @param invoice The invoice object to validate
32
+ * @throws Error if mandatory fields are missing
33
+ */
34
+ public static validateMandatoryFields(invoice: any): void {
35
+ const errors: string[] = [];
36
+
37
+ // BR-01: Invoice number is mandatory
38
+ if (!invoice.id && !invoice.accountingDocId && !invoice.invoiceId) {
39
+ errors.push('BR-01: Invoice number is mandatory');
40
+ }
41
+
42
+ // BR-02: Invoice issue date is mandatory
43
+ if (!invoice.date && !invoice.issueDate) {
44
+ errors.push('BR-02: Invoice issue date is mandatory');
45
+ }
46
+
47
+ // BR-06: Seller name is mandatory
48
+ if (!invoice.from?.name) {
49
+ errors.push('BR-06: Seller name is mandatory');
50
+ }
51
+
52
+ // BR-07: Buyer name is mandatory
53
+ if (!invoice.to?.name) {
54
+ errors.push('BR-07: Buyer name is mandatory');
55
+ }
56
+
57
+ // BR-16: Invoice line is mandatory (at least one)
58
+ if (!invoice.items || invoice.items.length === 0) {
59
+ errors.push('BR-16: At least one invoice line is mandatory');
60
+ }
61
+
62
+ // BR-03: Invoice type code is mandatory
63
+ if (!invoice.accountingDocType) {
64
+ errors.push('BR-03: Invoice type code is mandatory');
65
+ }
66
+
67
+ // BR-05: Invoice currency code is mandatory
68
+ if (!invoice.currency) {
69
+ errors.push('BR-05: Invoice currency code is mandatory');
70
+ } else {
71
+ // Validate currency format
72
+ if (!this.VALID_CURRENCIES.includes(invoice.currency.toUpperCase())) {
73
+ errors.push(`Invalid currency code: ${invoice.currency}. Must be a valid ISO 4217 currency code`);
74
+ }
75
+ }
76
+
77
+ // BR-08: Seller postal address is mandatory
78
+ if (!invoice.from?.address?.city || !invoice.from?.address?.postalCode || (!invoice.from?.address?.countryCode && !invoice.from?.address?.country)) {
79
+ errors.push('BR-08: Seller postal address (city, postal code, country) is mandatory');
80
+ }
81
+
82
+ // BR-10: Buyer postal address is mandatory
83
+ if (!invoice.to?.address?.city || !invoice.to?.address?.postalCode || (!invoice.to?.address?.countryCode && !invoice.to?.address?.country)) {
84
+ errors.push('BR-10: Buyer postal address (city, postal code, country) is mandatory');
85
+ }
86
+
87
+ if (errors.length > 0) {
88
+ throw new Error(`EN16931 validation failed:\n${errors.join('\n')}`);
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Validates mandatory fields for a parsed XML structure
94
+ * @param doc The parsed XML document
95
+ * @param format The invoice format
96
+ * @throws Error if mandatory fields are missing in XML
97
+ */
98
+ public static validateXmlMandatoryFields(doc: Document, format: string): void {
99
+ const errors: string[] = [];
100
+
101
+ if (format === 'ubl' || format === 'xrechnung') {
102
+ // Check for UBL mandatory fields
103
+ const id = doc.getElementsByTagName('cbc:ID')[0]?.textContent;
104
+ if (!id) {
105
+ errors.push('BR-01: Invoice ID is missing in XML');
106
+ }
107
+
108
+ const issueDate = doc.getElementsByTagName('cbc:IssueDate')[0]?.textContent;
109
+ if (!issueDate) {
110
+ errors.push('BR-02: Invoice issue date is missing in XML');
111
+ }
112
+
113
+ const sellerName = doc.querySelector('AccountingSupplierParty PartyName Name')?.textContent ||
114
+ doc.querySelector('AccountingSupplierParty PartyLegalEntity RegistrationName')?.textContent;
115
+ if (!sellerName) {
116
+ errors.push('BR-06: Seller name is missing in XML');
117
+ }
118
+
119
+ const buyerName = doc.querySelector('AccountingCustomerParty PartyName Name')?.textContent ||
120
+ doc.querySelector('AccountingCustomerParty PartyLegalEntity RegistrationName')?.textContent;
121
+ if (!buyerName) {
122
+ errors.push('BR-07: Buyer name is missing in XML');
123
+ }
124
+
125
+ const invoiceLines = doc.getElementsByTagName('InvoiceLine');
126
+ if (invoiceLines.length === 0) {
127
+ errors.push('BR-16: No invoice lines found in XML');
128
+ }
129
+ }
130
+
131
+ if (errors.length > 0) {
132
+ throw new Error(`EN16931 XML validation failed:\n${errors.join('\n')}`);
133
+ }
134
+ }
135
+ }