@fin.cx/opendata 4.7.0 → 4.8.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.
Files changed (56) hide show
  1. package/.smartconfig.json +6 -2
  2. package/dist_ts/00_commitinfo_data.js +3 -3
  3. package/dist_ts/financialstatements/amounts.d.ts +58 -0
  4. package/dist_ts/financialstatements/amounts.js +130 -0
  5. package/dist_ts/financialstatements/classes.financialstatementerror.d.ts +24 -0
  6. package/dist_ts/financialstatements/classes.financialstatementerror.js +10 -0
  7. package/dist_ts/financialstatements/constants.d.ts +16 -0
  8. package/dist_ts/financialstatements/constants.js +20 -0
  9. package/dist_ts/financialstatements/dates.d.ts +16 -0
  10. package/dist_ts/financialstatements/dates.js +72 -0
  11. package/dist_ts/financialstatements/html.d.ts +21 -0
  12. package/dist_ts/financialstatements/html.js +92 -0
  13. package/dist_ts/financialstatements/index.d.ts +6 -0
  14. package/dist_ts/financialstatements/index.js +11 -0
  15. package/dist_ts/financialstatements/interfaces.d.ts +258 -0
  16. package/dist_ts/financialstatements/interfaces.js +2 -0
  17. package/dist_ts/financialstatements/layout.d.ts +20 -0
  18. package/dist_ts/financialstatements/layout.html.d.ts +8 -0
  19. package/dist_ts/financialstatements/layout.html.js +125 -0
  20. package/dist_ts/financialstatements/layout.js +2 -0
  21. package/dist_ts/financialstatements/layout.pdf.d.ts +6 -0
  22. package/dist_ts/financialstatements/layout.pdf.js +229 -0
  23. package/dist_ts/financialstatements/mediatypes.d.ts +3 -0
  24. package/dist_ts/financialstatements/mediatypes.js +11 -0
  25. package/dist_ts/financialstatements/outline.d.ts +22 -0
  26. package/dist_ts/financialstatements/outline.js +26 -0
  27. package/dist_ts/financialstatements/parse.esef.d.ts +13 -0
  28. package/dist_ts/financialstatements/parse.esef.js +349 -0
  29. package/dist_ts/financialstatements/parse.hgb.d.ts +9 -0
  30. package/dist_ts/financialstatements/parse.hgb.js +1014 -0
  31. package/dist_ts/financialstatements/parse.statement.d.ts +8 -0
  32. package/dist_ts/financialstatements/parse.statement.js +25 -0
  33. package/dist_ts/financialstatements/plugins.d.ts +10 -0
  34. package/dist_ts/financialstatements/plugins.js +15 -0
  35. package/dist_ts/financialstatements/read.d.ts +10 -0
  36. package/dist_ts/financialstatements/read.js +113 -0
  37. package/package.json +14 -4
  38. package/readme.md +82 -4
  39. package/ts/00_commitinfo_data.ts +2 -2
  40. package/ts/financialstatements/amounts.ts +181 -0
  41. package/ts/financialstatements/classes.financialstatementerror.ts +35 -0
  42. package/ts/financialstatements/constants.ts +24 -0
  43. package/ts/financialstatements/dates.ts +89 -0
  44. package/ts/financialstatements/html.ts +107 -0
  45. package/ts/financialstatements/index.ts +17 -0
  46. package/ts/financialstatements/interfaces.ts +268 -0
  47. package/ts/financialstatements/layout.html.ts +130 -0
  48. package/ts/financialstatements/layout.pdf.ts +268 -0
  49. package/ts/financialstatements/layout.ts +16 -0
  50. package/ts/financialstatements/mediatypes.ts +14 -0
  51. package/ts/financialstatements/outline.ts +44 -0
  52. package/ts/financialstatements/parse.esef.ts +394 -0
  53. package/ts/financialstatements/parse.hgb.ts +1235 -0
  54. package/ts/financialstatements/parse.statement.ts +26 -0
  55. package/ts/financialstatements/plugins.ts +29 -0
  56. package/ts/financialstatements/read.ts +135 -0
@@ -0,0 +1,394 @@
1
+ import * as plugins from './plugins.js';
2
+ import { FinancialStatementError } from './classes.financialstatementerror.js';
3
+ import { maxEsefFacts, maxEsefPackageEntries, maxStatementBytes } from './constants.js';
4
+ import type { IEsefFact, IEsefPeriod, IEsefStatement, IKeyFigures, IStatementPeriod, TIsoDate } from './interfaces.js';
5
+
6
+ type TXmlDocument = plugins.smartxmlXpath.dom.Document;
7
+ type TXmlElement = plugins.smartxmlXpath.dom.Element;
8
+
9
+ const ixNamespace = 'http://www.xbrl.org/2013/inlineXBRL';
10
+ const xbrliNamespace = 'http://www.xbrl.org/2003/instance';
11
+ const xbrldiNamespace = 'http://xbrl.org/2006/xbrldi';
12
+ const xsiNamespace = 'http://www.w3.org/2001/XMLSchema-instance';
13
+ const xmlNamespace = 'http://www.w3.org/XML/1998/namespace';
14
+ const leiScheme = 'http://standards.iso.org/iso/17442';
15
+ const transformationNamespacePrefix = 'http://www.xbrl.org/inlineXBRL/transformation/';
16
+ const ifrsNamespacePattern = /^https?:\/\/xbrl\.ifrs\.org\/taxonomy\/[^/]+\/ifrs-full$/;
17
+
18
+ /** a report in an ESEF package: `<package root>/reports/<report>.xhtml` */
19
+ const reportPathPattern = /^[^/]+\/reports\/[^/]+\.x?html?$/i;
20
+
21
+ /** the formats of the Inline XBRL transformation registry read here, by local name */
22
+ const formatReaders: Record<string, (textArg: string) => string | null> = {
23
+ 'num-dot-decimal': (textArg) => decimalIn(textArg, '.'),
24
+ numdotdecimal: (textArg) => decimalIn(textArg, '.'),
25
+ 'num-comma-decimal': (textArg) => decimalIn(textArg, ','),
26
+ numcommadecimal: (textArg) => decimalIn(textArg, ','),
27
+ 'fixed-zero': () => '0',
28
+ zerodash: () => '0',
29
+ };
30
+
31
+ /** a number written with a decimal separator and any group separators, as a plain decimal string */
32
+ const decimalIn = (textArg: string, separatorArg: '.' | ','): string | null => {
33
+ const text = textArg.replace(/[\s']/g, '').replace(separatorArg === '.' ? /,/g : /\./g, '');
34
+ const [whole, fraction, ...rest] = text.split(separatorArg);
35
+ if (rest.length > 0 || !/^\d+$/.test(whole) || (fraction !== undefined && !/^\d+$/.test(fraction))) {
36
+ return null;
37
+ }
38
+ return fraction === undefined ? whole : `${whole}.${fraction}`;
39
+ };
40
+
41
+ /** a decimal string with its decimal point moved `scaleArg` places to the right */
42
+ const scaleDecimal = (valueArg: string, scaleArg: number): string => {
43
+ const [whole, fraction = ''] = valueArg.split('.');
44
+ let digits = whole + fraction;
45
+ let point = whole.length + scaleArg;
46
+ if (point > digits.length) {
47
+ digits = digits.padEnd(point, '0');
48
+ }
49
+ if (point < 0) {
50
+ digits = '0'.repeat(-point) + digits;
51
+ point = 0;
52
+ }
53
+ const integer = digits.slice(0, point).replace(/^0+(?=\d)/, '') || '0';
54
+ const decimals = digits.slice(point).replace(/0+$/, '');
55
+ return decimals === '' ? integer : `${integer}.${decimals}`;
56
+ };
57
+
58
+ /** a decimal string rounded to `decimalsArg` places, half away from zero, in units of that place */
59
+ const roundedTo = (valueArg: string, decimalsArg: number): bigint => {
60
+ const negative = valueArg.startsWith('-');
61
+ const [whole, fraction = ''] = scaleDecimal(negative ? valueArg.slice(1) : valueArg, decimalsArg).split('.');
62
+ const magnitude = BigInt(whole) + (fraction !== '' && fraction[0] >= '5' ? 1n : 0n);
63
+ return negative ? -magnitude : magnitude;
64
+ };
65
+
66
+ /** whether two facts for one concept and context state the same figure within their precision */
67
+ const factsAgree = (leftArg: IEsefFact, rightArg: IEsefFact): boolean => {
68
+ if (leftArg.value === rightArg.value) {
69
+ return true;
70
+ }
71
+ if (leftArg.value === null || rightArg.value === null) {
72
+ return false;
73
+ }
74
+ const precisions = [leftArg.decimals, rightArg.decimals]
75
+ .filter((decimalsArg) => decimalsArg !== null && decimalsArg !== 'INF')
76
+ .map(Number);
77
+ if (precisions.length === 0 || precisions.some((precisionArg) => !Number.isInteger(precisionArg))) {
78
+ return false;
79
+ }
80
+ const decimals = Math.min(...precisions);
81
+ return roundedTo(leftArg.value, decimals) === roundedTo(rightArg.value, decimals);
82
+ };
83
+
84
+ const childrenNamed = (elementArg: TXmlElement, namespaceArg: string, localNameArg: string): TXmlElement[] =>
85
+ elementArg.getElementsByTagNameNS(namespaceArg, localNameArg);
86
+
87
+ const trimmedText = (elementArg: TXmlElement | undefined): string | null => {
88
+ const text = elementArg?.textContent?.trim();
89
+ return text ? text : null;
90
+ };
91
+
92
+ interface IContext {
93
+ period: IEsefPeriod;
94
+ dimensions: Record<string, string>;
95
+ lei: string | null;
96
+ }
97
+
98
+ const readContexts = (documentArg: TXmlDocument): Map<string, IContext> => {
99
+ const contexts = new Map<string, IContext>();
100
+ for (const context of documentArg.getElementsByTagNameNS(xbrliNamespace, 'context')) {
101
+ const id = context.getAttribute('id');
102
+ const period = childrenNamed(context, xbrliNamespace, 'period')[0];
103
+ if (!id || !period) {
104
+ continue;
105
+ }
106
+ const dateOf = (nameArg: string): TIsoDate | null => trimmedText(childrenNamed(period, xbrliNamespace, nameArg)[0])?.slice(0, 10) ?? null;
107
+ const instant = dateOf('instant');
108
+ const end = instant ?? dateOf('endDate');
109
+ if (!end) {
110
+ continue;
111
+ }
112
+ const dimensions: Record<string, string> = {};
113
+ for (const member of [
114
+ ...childrenNamed(context, xbrldiNamespace, 'explicitMember'),
115
+ ...childrenNamed(context, xbrldiNamespace, 'typedMember'),
116
+ ]) {
117
+ const dimension = member.getAttribute('dimension');
118
+ if (dimension) {
119
+ dimensions[dimension] = trimmedText(member) ?? '';
120
+ }
121
+ }
122
+ const identifier = childrenNamed(context, xbrliNamespace, 'identifier')[0];
123
+ contexts.set(id, {
124
+ period: { start: instant ? null : dateOf('startDate'), end },
125
+ dimensions,
126
+ lei: identifier?.getAttribute('scheme') === leiScheme ? trimmedText(identifier) : null,
127
+ });
128
+ }
129
+ return contexts;
130
+ };
131
+
132
+ /** a measure without its prefix: `iso4217:EUR` is `EUR` */
133
+ const measureName = (elementArg: TXmlElement): string => (trimmedText(elementArg) ?? '').replace(/^[^:]*:/, '');
134
+
135
+ const readUnits = (documentArg: TXmlDocument): Map<string, string> => {
136
+ const units = new Map<string, string>();
137
+ for (const unit of documentArg.getElementsByTagNameNS(xbrliNamespace, 'unit')) {
138
+ const id = unit.getAttribute('id');
139
+ if (!id) {
140
+ continue;
141
+ }
142
+ const numerator = childrenNamed(unit, xbrliNamespace, 'unitNumerator')[0];
143
+ const denominator = childrenNamed(unit, xbrliNamespace, 'unitDenominator')[0];
144
+ const measures = (elementArg: TXmlElement) => childrenNamed(elementArg, xbrliNamespace, 'measure').map(measureName).join('*');
145
+ units.set(id, numerator && denominator ? `${measures(numerator)}/${measures(denominator)}` : measures(unit));
146
+ }
147
+ return units;
148
+ };
149
+
150
+ /** counts what was left out, so that a note names each reason once */
151
+ class Skips {
152
+ private counts = new Map<string, number>();
153
+
154
+ public add(reasonArg: string) {
155
+ this.counts.set(reasonArg, (this.counts.get(reasonArg) ?? 0) + 1);
156
+ }
157
+
158
+ public notes(): string[] {
159
+ return [...this.counts].map(([reason, count]) => `${count} ${count === 1 ? 'fact was' : 'facts were'} left out: ${reason}.`);
160
+ }
161
+ }
162
+
163
+ const readFacts = (documentArg: TXmlDocument, contextsArg: Map<string, IContext>, unitsArg: Map<string, string>, skipsArg: Skips): IEsefFact[] => {
164
+ const elements = documentArg.getElementsByTagNameNS(ixNamespace, 'nonFraction');
165
+ if (elements.length > maxEsefFacts) {
166
+ throw new FinancialStatementError('too_large', `The report tags ${elements.length} numeric facts, more than the ${maxEsefFacts} read here.`);
167
+ }
168
+ const facts: IEsefFact[] = [];
169
+ for (const element of elements) {
170
+ const name = element.getAttribute('name') ?? '';
171
+ const [prefix, localName] = name.includes(':') ? name.split(':', 2) : [null, name];
172
+ const namespace = localName ? element.lookupNamespaceURI(prefix) : null;
173
+ if (!namespace) {
174
+ skipsArg.add(`a concept name whose prefix the report does not declare`);
175
+ continue;
176
+ }
177
+ const contextId = element.getAttribute('contextRef') ?? '';
178
+ const context = contextsArg.get(contextId);
179
+ if (!context) {
180
+ skipsArg.add('a context the report does not define');
181
+ continue;
182
+ }
183
+ const unitRef = element.getAttribute('unitRef');
184
+ const unit = unitRef ? (unitsArg.get(unitRef) ?? null) : null;
185
+ if (unitRef && unit === null) {
186
+ skipsArg.add('a unit the report does not define');
187
+ continue;
188
+ }
189
+ let value: string | null = null;
190
+ if (element.getAttributeNS(xsiNamespace, 'nil') !== 'true') {
191
+ const text = element.textContent ?? '';
192
+ const format = element.getAttribute('format');
193
+ if (format) {
194
+ const [formatPrefix, formatName] = format.includes(':') ? format.split(':', 2) : [null, format];
195
+ const reader = formatReaders[formatName];
196
+ if (!(element.lookupNamespaceURI(formatPrefix) ?? '').startsWith(transformationNamespacePrefix) || !reader) {
197
+ skipsArg.add(`the format "${format}", which is not read here`);
198
+ continue;
199
+ }
200
+ value = reader(text);
201
+ } else {
202
+ value = decimalIn(text, '.');
203
+ }
204
+ const scale = Number(element.getAttribute('scale') ?? '0');
205
+ if (value === null || !Number.isInteger(scale) || Math.abs(scale) > 30) {
206
+ skipsArg.add('a value that does not read as a number in its format');
207
+ continue;
208
+ }
209
+ value = scaleDecimal(value, scale);
210
+ if (element.getAttribute('sign') === '-' && value !== '0') {
211
+ value = `-${value}`;
212
+ }
213
+ }
214
+ facts.push({
215
+ concept: name,
216
+ namespace,
217
+ contextId,
218
+ period: context.period,
219
+ dimensions: context.dimensions,
220
+ unit,
221
+ value,
222
+ decimals: element.getAttribute('decimals'),
223
+ id: element.getAttribute('id'),
224
+ });
225
+ }
226
+ return facts;
227
+ };
228
+
229
+ const keyConcepts = { totalAssets: 'Assets', equity: 'Equity', netIncome: 'ProfitLoss', revenue: 'Revenue' } as const;
230
+ type TKeyFigure = keyof typeof keyConcepts;
231
+
232
+ /**
233
+ * Reads an ESEF report, the XHTML of its inline XBRL, on its own: its numeric facts and the key
234
+ * figures of its consolidated statements. Throws `unreadable` where it is not well-formed XML and
235
+ * `too_large` where it tags more than `maxEsefFacts` facts.
236
+ */
237
+ export const parseEsefReport = (bytesArg: Uint8Array, reportPathArg: string | null = null): IEsefStatement => {
238
+ const start = bytesArg[0] === 0xef && bytesArg[1] === 0xbb && bytesArg[2] === 0xbf ? 3 : 0;
239
+ const declared = /^<\?xml[^>]*\bencoding\s*=\s*["']([\w.-]+)["']/i.exec(
240
+ new TextDecoder('windows-1252').decode(bytesArg.subarray(start, start + 200))
241
+ )?.[1];
242
+ if (declared && !/^utf-?8$/i.test(declared)) {
243
+ throw new FinancialStatementError('unreadable', `The report declares the encoding "${declared}", which is not read here.`);
244
+ }
245
+ let document: TXmlDocument;
246
+ try {
247
+ const text = new TextDecoder('utf-8', { fatal: true }).decode(bytesArg.subarray(start));
248
+ document = plugins.smartxmlXpath.parseXmlDocument(text);
249
+ } catch (error) {
250
+ throw new FinancialStatementError('unreadable', 'The report is not well-formed XML in UTF-8.', { cause: error });
251
+ }
252
+ const skips = new Skips();
253
+ const contexts = readContexts(document);
254
+ const facts = readFacts(document, contexts, readUnits(document), skips);
255
+ const notes = skips.notes();
256
+
257
+ const leis = new Set([...contexts.values()].map((contextArg) => contextArg.lei).filter((leiArg) => leiArg !== null));
258
+ if (leis.size > 1) {
259
+ notes.push(`The report's contexts identify more than one issuer (${[...leis].join(', ')}).`);
260
+ }
261
+
262
+ const keyFacts = facts.filter(
263
+ (factArg) =>
264
+ ifrsNamespacePattern.test(factArg.namespace) &&
265
+ Object.keys(factArg.dimensions).length === 0 &&
266
+ factArg.value !== null &&
267
+ factArg.unit !== null &&
268
+ /^[A-Z]{3}$/.test(factArg.unit)
269
+ );
270
+ const conceptOf = (factArg: IEsefFact) => factArg.concept.slice(factArg.concept.indexOf(':') + 1);
271
+ const currencies = new Set(keyFacts.map((factArg) => factArg.unit as string));
272
+ let currency: string | null = currencies.size === 1 ? [...currencies][0] : null;
273
+ if (currencies.size > 1) {
274
+ notes.push(`The key figures are tagged in more than one currency (${[...currencies].join(', ')}), so none was read.`);
275
+ }
276
+
277
+ const durations = keyFacts.filter(
278
+ (factArg) => factArg.period.start !== null && (conceptOf(factArg) === 'ProfitLoss' || conceptOf(factArg) === 'Revenue')
279
+ );
280
+ const instants = keyFacts.filter((factArg) => factArg.period.start === null && conceptOf(factArg) === 'Assets');
281
+ const basis = durations.length > 0 ? durations : instants;
282
+ const ends = [...new Set(basis.map((factArg) => factArg.period.end))].sort().reverse();
283
+ const periodEndingOn = (endArg: TIsoDate | undefined): IEsefPeriod | null => {
284
+ if (!endArg) {
285
+ return null;
286
+ }
287
+ const starts = basis
288
+ .filter((factArg) => factArg.period.end === endArg && factArg.period.start !== null)
289
+ .map((factArg) => factArg.period.start as TIsoDate)
290
+ .sort();
291
+ return { start: starts[0] ?? null, end: endArg };
292
+ };
293
+ const current = currency ? periodEndingOn(ends[0]) : null;
294
+ const prior = currency ? periodEndingOn(ends[1]) : null;
295
+ if (currency && !current) {
296
+ notes.push('The report tags no key figures of its consolidated statements (IFRS) without dimensions.');
297
+ currency = null;
298
+ }
299
+
300
+ const keyFiguresOf = (periodArg: IEsefPeriod): IKeyFigures => {
301
+ const figure = (keyArg: TKeyFigure): string | null => {
302
+ const instant = keyArg === 'totalAssets' || keyArg === 'equity';
303
+ const matching = keyFacts.filter(
304
+ (factArg) =>
305
+ conceptOf(factArg) === keyConcepts[keyArg] &&
306
+ factArg.period.end === periodArg.end &&
307
+ (instant ? factArg.period.start === null : factArg.period.start !== null && factArg.period.start === periodArg.start)
308
+ );
309
+ if (matching.length === 0) {
310
+ return null;
311
+ }
312
+ if (!matching.every((factArg) => factsAgree(factArg, matching[0]))) {
313
+ notes.push(`The report tags ifrs-full:${keyConcepts[keyArg]} for ${periodArg.end} with values that differ, so it was left out.`);
314
+ return null;
315
+ }
316
+ const precision = (factArg: IEsefFact) =>
317
+ factArg.decimals === 'INF' ? Number.POSITIVE_INFINITY : Number(factArg.decimals ?? Number.NEGATIVE_INFINITY);
318
+ return [...matching].sort((leftArg, rightArg) => precision(rightArg) - precision(leftArg))[0].value;
319
+ };
320
+ return {
321
+ totalAssets: figure('totalAssets'),
322
+ equity: figure('equity'),
323
+ netIncome: figure('netIncome'),
324
+ revenue: figure('revenue'),
325
+ };
326
+ };
327
+ const statementPeriod = (periodArg: IEsefPeriod | null): IStatementPeriod => ({
328
+ heading: null,
329
+ start: periodArg?.start ?? null,
330
+ end: periodArg?.end ?? null,
331
+ });
332
+
333
+ const root = document.documentElement;
334
+ return {
335
+ standard: 'esef',
336
+ lei: leis.size === 1 ? [...leis][0] : null,
337
+ reportPath: reportPathArg,
338
+ language: root?.getAttributeNS(xmlNamespace, 'lang') ?? root?.getAttribute('lang') ?? null,
339
+ currency,
340
+ periods: { current: statementPeriod(current), prior: prior ? statementPeriod(prior) : null },
341
+ keyFigures: {
342
+ current: current ? keyFiguresOf(current) : { totalAssets: null, equity: null, netIncome: null, revenue: null },
343
+ prior: prior ? keyFiguresOf(prior) : null,
344
+ },
345
+ facts,
346
+ notes,
347
+ };
348
+ };
349
+
350
+ const tooLargeZipCodes = new Set<string>([
351
+ plugins.smartarchiveZip.ZIP_ERROR_CODES.ARCHIVE_SIZE_LIMIT,
352
+ plugins.smartarchiveZip.ZIP_ERROR_CODES.ENTRY_COUNT_LIMIT,
353
+ plugins.smartarchiveZip.ZIP_ERROR_CODES.COMPRESSED_SIZE_LIMIT,
354
+ plugins.smartarchiveZip.ZIP_ERROR_CODES.UNCOMPRESSED_SIZE_LIMIT,
355
+ plugins.smartarchiveZip.ZIP_ERROR_CODES.TOTAL_UNCOMPRESSED_SIZE_LIMIT,
356
+ ]);
357
+
358
+ /**
359
+ * Reads an ESEF report package (`.zip` or `.xbri`): the one report under `reports/`, by
360
+ * `parseEsefReport`. Throws `invalid_package` where the package is not a readable ZIP, is encrypted
361
+ * or does not hold exactly one report, and `too_large` where it or its report exceeds the limits.
362
+ */
363
+ export const parseEsefPackage = async (zipBytesArg: Uint8Array): Promise<IEsefStatement> => {
364
+ let entries: Array<{ path: string; content: Uint8Array }>;
365
+ try {
366
+ entries = await new plugins.smartarchiveZip.ZipTools().extractSelectedZipEntries(zipBytesArg, {
367
+ filter: (entryArg) => entryArg.isFile && reportPathPattern.test(entryArg.path),
368
+ maxArchiveBytes: maxStatementBytes,
369
+ maxEntries: maxEsefPackageEntries,
370
+ maxSelectedEntries: 1,
371
+ maxCompressedEntryBytes: maxStatementBytes,
372
+ maxUncompressedEntryBytes: maxStatementBytes,
373
+ maxTotalUncompressedBytes: maxStatementBytes,
374
+ });
375
+ } catch (error) {
376
+ if (!(error instanceof plugins.smartarchiveZip.ZipError) || error.code === plugins.smartarchiveZip.ZIP_ERROR_CODES.INVALID_LIMIT) {
377
+ throw error;
378
+ }
379
+ if (tooLargeZipCodes.has(error.code)) {
380
+ throw new FinancialStatementError('too_large', `The package exceeds the limits read here: ${error.message}`, { cause: error });
381
+ }
382
+ const reason =
383
+ error.code === plugins.smartarchiveZip.ZIP_ERROR_CODES.SELECTED_ENTRY_COUNT_LIMIT
384
+ ? 'holds more than one report under reports/'
385
+ : error.code === plugins.smartarchiveZip.ZIP_ERROR_CODES.ENCRYPTED
386
+ ? 'is encrypted'
387
+ : `is not a readable ZIP (${error.message})`;
388
+ throw new FinancialStatementError('invalid_package', `The package ${reason}.`, { cause: error });
389
+ }
390
+ if (entries.length === 0) {
391
+ throw new FinancialStatementError('invalid_package', 'The package holds no report under reports/.');
392
+ }
393
+ return parseEsefReport(entries[0].content, entries[0].path);
394
+ };