@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.
- package/.smartconfig.json +6 -2
- package/dist_ts/00_commitinfo_data.js +3 -3
- package/dist_ts/financialstatements/amounts.d.ts +58 -0
- package/dist_ts/financialstatements/amounts.js +130 -0
- package/dist_ts/financialstatements/classes.financialstatementerror.d.ts +24 -0
- package/dist_ts/financialstatements/classes.financialstatementerror.js +10 -0
- package/dist_ts/financialstatements/constants.d.ts +16 -0
- package/dist_ts/financialstatements/constants.js +20 -0
- package/dist_ts/financialstatements/dates.d.ts +16 -0
- package/dist_ts/financialstatements/dates.js +72 -0
- package/dist_ts/financialstatements/html.d.ts +21 -0
- package/dist_ts/financialstatements/html.js +92 -0
- package/dist_ts/financialstatements/index.d.ts +6 -0
- package/dist_ts/financialstatements/index.js +11 -0
- package/dist_ts/financialstatements/interfaces.d.ts +258 -0
- package/dist_ts/financialstatements/interfaces.js +2 -0
- package/dist_ts/financialstatements/layout.d.ts +20 -0
- package/dist_ts/financialstatements/layout.html.d.ts +8 -0
- package/dist_ts/financialstatements/layout.html.js +125 -0
- package/dist_ts/financialstatements/layout.js +2 -0
- package/dist_ts/financialstatements/layout.pdf.d.ts +6 -0
- package/dist_ts/financialstatements/layout.pdf.js +229 -0
- package/dist_ts/financialstatements/mediatypes.d.ts +3 -0
- package/dist_ts/financialstatements/mediatypes.js +11 -0
- package/dist_ts/financialstatements/outline.d.ts +22 -0
- package/dist_ts/financialstatements/outline.js +26 -0
- package/dist_ts/financialstatements/parse.esef.d.ts +13 -0
- package/dist_ts/financialstatements/parse.esef.js +349 -0
- package/dist_ts/financialstatements/parse.hgb.d.ts +9 -0
- package/dist_ts/financialstatements/parse.hgb.js +1014 -0
- package/dist_ts/financialstatements/parse.statement.d.ts +8 -0
- package/dist_ts/financialstatements/parse.statement.js +25 -0
- package/dist_ts/financialstatements/plugins.d.ts +10 -0
- package/dist_ts/financialstatements/plugins.js +15 -0
- package/dist_ts/financialstatements/read.d.ts +10 -0
- package/dist_ts/financialstatements/read.js +113 -0
- package/package.json +14 -4
- package/readme.md +82 -4
- package/ts/00_commitinfo_data.ts +2 -2
- package/ts/financialstatements/amounts.ts +181 -0
- package/ts/financialstatements/classes.financialstatementerror.ts +35 -0
- package/ts/financialstatements/constants.ts +24 -0
- package/ts/financialstatements/dates.ts +89 -0
- package/ts/financialstatements/html.ts +107 -0
- package/ts/financialstatements/index.ts +17 -0
- package/ts/financialstatements/interfaces.ts +268 -0
- package/ts/financialstatements/layout.html.ts +130 -0
- package/ts/financialstatements/layout.pdf.ts +268 -0
- package/ts/financialstatements/layout.ts +16 -0
- package/ts/financialstatements/mediatypes.ts +14 -0
- package/ts/financialstatements/outline.ts +44 -0
- package/ts/financialstatements/parse.esef.ts +394 -0
- package/ts/financialstatements/parse.hgb.ts +1235 -0
- package/ts/financialstatements/parse.statement.ts +26 -0
- package/ts/financialstatements/plugins.ts +29 -0
- package/ts/financialstatements/read.ts +135 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { IHgbStatement } from './interfaces.js';
|
|
2
|
+
/**
|
|
3
|
+
* Reads a statement under the HGB from the HTML or the PDF a register shows: the balance sheet
|
|
4
|
+
* (§ 266 HGB) and, where one is published, the income statement (§ 275 HGB). Everything it returns
|
|
5
|
+
* was printed and adds up; where the current year's balance sheet cannot be read with certainty it
|
|
6
|
+
* throws `unparseable` rather than guess. A PDF is read from its text layer only.
|
|
7
|
+
*/
|
|
8
|
+
export declare const parseStatementDocument: (bytesArg: Uint8Array, mediaTypeArg: string) => Promise<IHgbStatement>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { FinancialStatementError } from './classes.financialstatementerror.js';
|
|
2
|
+
import { maxStatementBytes } from './constants.js';
|
|
3
|
+
import { decodeHtml } from './html.js';
|
|
4
|
+
import { htmlLayout } from './layout.html.js';
|
|
5
|
+
import { pdfLayout } from './layout.pdf.js';
|
|
6
|
+
import { statementMediaTypeOf } from './mediatypes.js';
|
|
7
|
+
import { readHgbStatement } from './parse.hgb.js';
|
|
8
|
+
/**
|
|
9
|
+
* Reads a statement under the HGB from the HTML or the PDF a register shows: the balance sheet
|
|
10
|
+
* (§ 266 HGB) and, where one is published, the income statement (§ 275 HGB). Everything it returns
|
|
11
|
+
* was printed and adds up; where the current year's balance sheet cannot be read with certainty it
|
|
12
|
+
* throws `unparseable` rather than guess. A PDF is read from its text layer only.
|
|
13
|
+
*/
|
|
14
|
+
export const parseStatementDocument = async (bytesArg, mediaTypeArg) => {
|
|
15
|
+
const mediaType = statementMediaTypeOf(mediaTypeArg);
|
|
16
|
+
if (mediaType !== 'text/html' && mediaType !== 'application/xhtml+xml' && mediaType !== 'application/pdf') {
|
|
17
|
+
throw new FinancialStatementError('unsupported_media_type', `A statement in "${mediaTypeArg}" is not read here; HTML and PDF are.`);
|
|
18
|
+
}
|
|
19
|
+
if (bytesArg.byteLength > maxStatementBytes) {
|
|
20
|
+
throw new FinancialStatementError('too_large', `The document has ${bytesArg.byteLength} bytes, more than the ${maxStatementBytes} read here.`);
|
|
21
|
+
}
|
|
22
|
+
const blocks = mediaType === 'application/pdf' ? await pdfLayout(bytesArg) : htmlLayout(decodeHtml(bytesArg));
|
|
23
|
+
return readHgbStatement(blocks);
|
|
24
|
+
};
|
|
25
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicGFyc2Uuc3RhdGVtZW50LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vdHMvZmluYW5jaWFsc3RhdGVtZW50cy9wYXJzZS5zdGF0ZW1lbnQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLHVCQUF1QixFQUFFLE1BQU0sc0NBQXNDLENBQUM7QUFDL0UsT0FBTyxFQUFFLGlCQUFpQixFQUFFLE1BQU0sZ0JBQWdCLENBQUM7QUFDbkQsT0FBTyxFQUFFLFVBQVUsRUFBRSxNQUFNLFdBQVcsQ0FBQztBQUN2QyxPQUFPLEVBQUUsVUFBVSxFQUFFLE1BQU0sa0JBQWtCLENBQUM7QUFDOUMsT0FBTyxFQUFFLFNBQVMsRUFBRSxNQUFNLGlCQUFpQixDQUFDO0FBQzVDLE9BQU8sRUFBRSxvQkFBb0IsRUFBRSxNQUFNLGlCQUFpQixDQUFDO0FBQ3ZELE9BQU8sRUFBRSxnQkFBZ0IsRUFBRSxNQUFNLGdCQUFnQixDQUFDO0FBR2xEOzs7OztHQUtHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sc0JBQXNCLEdBQUcsS0FBSyxFQUFFLFFBQW9CLEVBQUUsWUFBb0IsRUFBMEIsRUFBRTtJQUNqSCxNQUFNLFNBQVMsR0FBRyxvQkFBb0IsQ0FBQyxZQUFZLENBQUMsQ0FBQztJQUNyRCxJQUFJLFNBQVMsS0FBSyxXQUFXLElBQUksU0FBUyxLQUFLLHVCQUF1QixJQUFJLFNBQVMsS0FBSyxpQkFBaUIsRUFBRSxDQUFDO1FBQzFHLE1BQU0sSUFBSSx1QkFBdUIsQ0FBQyx3QkFBd0IsRUFBRSxtQkFBbUIsWUFBWSx1Q0FBdUMsQ0FBQyxDQUFDO0lBQ3RJLENBQUM7SUFDRCxJQUFJLFFBQVEsQ0FBQyxVQUFVLEdBQUcsaUJBQWlCLEVBQUUsQ0FBQztRQUM1QyxNQUFNLElBQUksdUJBQXVCLENBQUMsV0FBVyxFQUFFLG9CQUFvQixRQUFRLENBQUMsVUFBVSx5QkFBeUIsaUJBQWlCLGFBQWEsQ0FBQyxDQUFDO0lBQ2pKLENBQUM7SUFDRCxNQUFNLE1BQU0sR0FBRyxTQUFTLEtBQUssaUJBQWlCLENBQUMsQ0FBQyxDQUFDLE1BQU0sU0FBUyxDQUFDLFFBQVEsQ0FBQyxDQUFDLENBQUMsQ0FBQyxVQUFVLENBQUMsVUFBVSxDQUFDLFFBQVEsQ0FBQyxDQUFDLENBQUM7SUFDOUcsT0FBTyxnQkFBZ0IsQ0FBQyxNQUFNLENBQUMsQ0FBQztBQUNsQyxDQUFDLENBQUMifQ==
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import * as path from 'node:path';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
export { path, createRequire, };
|
|
4
|
+
import * as smartarchiveZip from '@push.rocks/smartarchive/web/zip';
|
|
5
|
+
import * as smartxmlXpath from '@push.rocks/smartxml/xpath';
|
|
6
|
+
export { smartarchiveZip, smartxmlXpath, };
|
|
7
|
+
import * as parse5 from 'parse5';
|
|
8
|
+
export { parse5, };
|
|
9
|
+
/** PDF.js for Node.js (its legacy build), loaded when the first PDF is read */
|
|
10
|
+
export declare const loadPdfjs: () => Promise<typeof import("pdfjs-dist/legacy/build/pdf.mjs")>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// This entry point stands alone: it imports nothing from the rest of the package and needs no storage.
|
|
2
|
+
// node native scope
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
export { path, createRequire, };
|
|
6
|
+
// @push.rocks scope
|
|
7
|
+
import * as smartarchiveZip from '@push.rocks/smartarchive/web/zip';
|
|
8
|
+
import * as smartxmlXpath from '@push.rocks/smartxml/xpath';
|
|
9
|
+
export { smartarchiveZip, smartxmlXpath, };
|
|
10
|
+
// third party scope
|
|
11
|
+
import * as parse5 from 'parse5';
|
|
12
|
+
export { parse5, };
|
|
13
|
+
/** PDF.js for Node.js (its legacy build), loaded when the first PDF is read */
|
|
14
|
+
export const loadPdfjs = () => import('pdfjs-dist/legacy/build/pdf.mjs');
|
|
15
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicGx1Z2lucy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3RzL2ZpbmFuY2lhbHN0YXRlbWVudHMvcGx1Z2lucy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSx1R0FBdUc7QUFFdkcsb0JBQW9CO0FBQ3BCLE9BQU8sS0FBSyxJQUFJLE1BQU0sV0FBVyxDQUFDO0FBQ2xDLE9BQU8sRUFBRSxhQUFhLEVBQUUsTUFBTSxhQUFhLENBQUM7QUFFNUMsT0FBTyxFQUNMLElBQUksRUFDSixhQUFhLEdBQ2QsQ0FBQTtBQUVELG9CQUFvQjtBQUNwQixPQUFPLEtBQUssZUFBZSxNQUFNLGtDQUFrQyxDQUFDO0FBQ3BFLE9BQU8sS0FBSyxhQUFhLE1BQU0sNEJBQTRCLENBQUM7QUFFNUQsT0FBTyxFQUNMLGVBQWUsRUFDZixhQUFhLEdBQ2QsQ0FBQTtBQUVELG9CQUFvQjtBQUNwQixPQUFPLEtBQUssTUFBTSxNQUFNLFFBQVEsQ0FBQztBQUVqQyxPQUFPLEVBQ0wsTUFBTSxHQUNQLENBQUE7QUFFRCwrRUFBK0U7QUFDL0UsTUFBTSxDQUFDLE1BQU0sU0FBUyxHQUFHLEdBQUcsRUFBRSxDQUFDLE1BQU0sQ0FBQyxpQ0FBaUMsQ0FBQyxDQUFDIn0=
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { IFinancialStatement, IReadFinancialStatementOptions } from './interfaces.js';
|
|
2
|
+
/**
|
|
3
|
+
* Keeps a published statement as a caller stores it: the filing as the caller states it, the
|
|
4
|
+
* document byte for byte with its SHA-256, and what could be read from it: an ESEF report by
|
|
5
|
+
* `parseEsefPackage` or `parseEsefReport`, any other HTML or PDF by `parseStatementDocument`. A
|
|
6
|
+
* document that cannot be read with certainty is kept with `parsed` null and `parseFailure` saying
|
|
7
|
+
* why. Throws `invalid_input`, `unsupported_media_type` and, for a document larger than
|
|
8
|
+
* `maxStatementBytes`, `too_large`, as nothing would be kept.
|
|
9
|
+
*/
|
|
10
|
+
export declare const readFinancialStatement: (optionsArg: IReadFinancialStatementOptions) => Promise<IFinancialStatement>;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { FinancialStatementError } from './classes.financialstatementerror.js';
|
|
2
|
+
import { maxStatementBytes, unternehmensregisterNotice } from './constants.js';
|
|
3
|
+
import { statementMediaTypeOf } from './mediatypes.js';
|
|
4
|
+
import { parseEsefPackage, parseEsefReport } from './parse.esef.js';
|
|
5
|
+
import { parseStatementDocument } from './parse.statement.js';
|
|
6
|
+
const filingSources = ['unternehmensregister', 'bundesanzeiger', 'issuer', 'unknown'];
|
|
7
|
+
const filingKinds = ['annual_accounts', 'consolidated_accounts', 'deposited_balance_sheet', 'annual_financial_report'];
|
|
8
|
+
const sizeClasses = ['micro', 'small', 'medium', 'large'];
|
|
9
|
+
const parseFailureCodes = new Set(['invalid_package', 'unreadable', 'no_text', 'unparseable', 'too_large']);
|
|
10
|
+
/** the inline XBRL namespace, which marks an HTML document as an ESEF report */
|
|
11
|
+
const inlineXbrlNamespace = 'http://www.xbrl.org/2013/inlineXBRL';
|
|
12
|
+
const invalid = (messageArg) => {
|
|
13
|
+
throw new FinancialStatementError('invalid_input', messageArg);
|
|
14
|
+
};
|
|
15
|
+
const isIsoDate = (valueArg) => {
|
|
16
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(valueArg)) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
const date = new Date(`${valueArg}T00:00:00Z`);
|
|
20
|
+
return !Number.isNaN(date.getTime()) && date.toISOString().slice(0, 10) === valueArg;
|
|
21
|
+
};
|
|
22
|
+
const filingOf = (inputArg) => {
|
|
23
|
+
if (!filingSources.includes(inputArg?.source)) {
|
|
24
|
+
invalid(`The filing's source "${inputArg?.source}" is not one of ${filingSources.join(', ')}.`);
|
|
25
|
+
}
|
|
26
|
+
if (!filingKinds.includes(inputArg.kind)) {
|
|
27
|
+
invalid(`The filing's kind "${inputArg.kind}" is not one of ${filingKinds.join(', ')}.`);
|
|
28
|
+
}
|
|
29
|
+
if (inputArg.sizeClass != null && !sizeClasses.includes(inputArg.sizeClass)) {
|
|
30
|
+
invalid(`The filing's size class "${inputArg.sizeClass}" is not one of ${sizeClasses.join(', ')}.`);
|
|
31
|
+
}
|
|
32
|
+
for (const field of ['fiscalYearStart', 'fiscalYearEnd', 'publishedOn']) {
|
|
33
|
+
const value = inputArg[field];
|
|
34
|
+
if (value != null && (typeof value !== 'string' || !isIsoDate(value))) {
|
|
35
|
+
invalid(`The filing's ${field} "${value}" is not an ISO date (YYYY-MM-DD).`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (inputArg.source === 'unternehmensregister' && inputArg.publishedOn == null) {
|
|
39
|
+
invalid('A filing from the Unternehmensregister needs publishedOn, the day it was posted or deposited, for its origin note.');
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
source: inputArg.source,
|
|
43
|
+
kind: inputArg.kind,
|
|
44
|
+
companyName: inputArg.companyName ?? null,
|
|
45
|
+
registerReference: inputArg.registerReference ?? null,
|
|
46
|
+
lei: inputArg.lei ?? null,
|
|
47
|
+
fiscalYearStart: inputArg.fiscalYearStart ?? null,
|
|
48
|
+
fiscalYearEnd: inputArg.fiscalYearEnd ?? null,
|
|
49
|
+
publishedOn: inputArg.publishedOn ?? null,
|
|
50
|
+
title: inputArg.title ?? null,
|
|
51
|
+
sizeClass: inputArg.sizeClass ?? null,
|
|
52
|
+
documentId: inputArg.documentId ?? null,
|
|
53
|
+
sourceUrl: inputArg.sourceUrl ?? null,
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
const sha256Of = async (bytesArg) => {
|
|
57
|
+
const digest = new Uint8Array(await globalThis.crypto.subtle.digest('SHA-256', bytesArg));
|
|
58
|
+
return [...digest].map((byteArg) => byteArg.toString(16).padStart(2, '0')).join('');
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Keeps a published statement as a caller stores it: the filing as the caller states it, the
|
|
62
|
+
* document byte for byte with its SHA-256, and what could be read from it: an ESEF report by
|
|
63
|
+
* `parseEsefPackage` or `parseEsefReport`, any other HTML or PDF by `parseStatementDocument`. A
|
|
64
|
+
* document that cannot be read with certainty is kept with `parsed` null and `parseFailure` saying
|
|
65
|
+
* why. Throws `invalid_input`, `unsupported_media_type` and, for a document larger than
|
|
66
|
+
* `maxStatementBytes`, `too_large`, as nothing would be kept.
|
|
67
|
+
*/
|
|
68
|
+
export const readFinancialStatement = async (optionsArg) => {
|
|
69
|
+
if (!(optionsArg?.bytes instanceof Uint8Array)) {
|
|
70
|
+
invalid('bytes must be a Uint8Array.');
|
|
71
|
+
}
|
|
72
|
+
const filing = filingOf(optionsArg.filing);
|
|
73
|
+
if (optionsArg.bytes.byteLength > maxStatementBytes) {
|
|
74
|
+
throw new FinancialStatementError('too_large', `The document has ${optionsArg.bytes.byteLength} bytes, more than the ${maxStatementBytes} read here.`);
|
|
75
|
+
}
|
|
76
|
+
const mediaType = statementMediaTypeOf(String(optionsArg.mediaType));
|
|
77
|
+
if (!mediaType) {
|
|
78
|
+
throw new FinancialStatementError('unsupported_media_type', `A statement in "${optionsArg.mediaType}" is not read here; HTML, PDF, XHTML and ZIP packages are.`);
|
|
79
|
+
}
|
|
80
|
+
const bytes = new Uint8Array(optionsArg.bytes);
|
|
81
|
+
const sha256 = await sha256Of(bytes);
|
|
82
|
+
let parsed = null;
|
|
83
|
+
let parseFailure = null;
|
|
84
|
+
try {
|
|
85
|
+
if (mediaType === 'application/zip') {
|
|
86
|
+
parsed = await parseEsefPackage(bytes);
|
|
87
|
+
}
|
|
88
|
+
else if (mediaType === 'application/xhtml+xml' ||
|
|
89
|
+
(mediaType === 'text/html' && new TextDecoder('windows-1252').decode(bytes.subarray(0, 65536)).includes(inlineXbrlNamespace))) {
|
|
90
|
+
parsed = parseEsefReport(bytes);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
parsed = await parseStatementDocument(bytes, mediaType);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
if (!(error instanceof FinancialStatementError) || !parseFailureCodes.has(error.code)) {
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
parseFailure = { code: error.code, message: error.message };
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
filing,
|
|
104
|
+
raw: { bytes, mediaType, sha256 },
|
|
105
|
+
parsed,
|
|
106
|
+
parseFailure,
|
|
107
|
+
obtainedAt: (optionsArg.now ?? Date.now)(),
|
|
108
|
+
notice: filing.source === 'unternehmensregister'
|
|
109
|
+
? unternehmensregisterNotice(filing.publishedOn, filing.kind === 'deposited_balance_sheet')
|
|
110
|
+
: null,
|
|
111
|
+
};
|
|
112
|
+
};
|
|
113
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicmVhZC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3RzL2ZpbmFuY2lhbHN0YXRlbWVudHMvcmVhZC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsdUJBQXVCLEVBQUUsTUFBTSxzQ0FBc0MsQ0FBQztBQUMvRSxPQUFPLEVBQUUsaUJBQWlCLEVBQUUsMEJBQTBCLEVBQUUsTUFBTSxnQkFBZ0IsQ0FBQztBQUMvRSxPQUFPLEVBQUUsb0JBQW9CLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQztBQUN2RCxPQUFPLEVBQUUsZ0JBQWdCLEVBQUUsZUFBZSxFQUFFLE1BQU0saUJBQWlCLENBQUM7QUFDcEUsT0FBTyxFQUFFLHNCQUFzQixFQUFFLE1BQU0sc0JBQXNCLENBQUM7QUFZOUQsTUFBTSxhQUFhLEdBQW9CLENBQUMsc0JBQXNCLEVBQUUsZ0JBQWdCLEVBQUUsUUFBUSxFQUFFLFNBQVMsQ0FBQyxDQUFDO0FBQ3ZHLE1BQU0sV0FBVyxHQUFrQixDQUFDLGlCQUFpQixFQUFFLHVCQUF1QixFQUFFLHlCQUF5QixFQUFFLHlCQUF5QixDQUFDLENBQUM7QUFDdEksTUFBTSxXQUFXLEdBQWlCLENBQUMsT0FBTyxFQUFFLE9BQU8sRUFBRSxRQUFRLEVBQUUsT0FBTyxDQUFDLENBQUM7QUFDeEUsTUFBTSxpQkFBaUIsR0FBRyxJQUFJLEdBQUcsQ0FBUyxDQUFDLGlCQUFpQixFQUFFLFlBQVksRUFBRSxTQUFTLEVBQUUsYUFBYSxFQUFFLFdBQVcsQ0FBQyxDQUFDLENBQUM7QUFFcEgsZ0ZBQWdGO0FBQ2hGLE1BQU0sbUJBQW1CLEdBQUcscUNBQXFDLENBQUM7QUFFbEUsTUFBTSxPQUFPLEdBQUcsQ0FBQyxVQUFrQixFQUFTLEVBQUU7SUFDNUMsTUFBTSxJQUFJLHVCQUF1QixDQUFDLGVBQWUsRUFBRSxVQUFVLENBQUMsQ0FBQztBQUNqRSxDQUFDLENBQUM7QUFFRixNQUFNLFNBQVMsR0FBRyxDQUFDLFFBQWdCLEVBQVcsRUFBRTtJ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
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fin.cx/opendata",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.8.0",
|
|
4
4
|
"private": false,
|
|
5
|
-
"description": "Open business and financial data for TypeScript: register lookups on the German register portal (handelsregister.de) without storage, the statutory public holidays of the German federal states, the base rate of § 247 BGB, the German consumer price index with the arithmetic of index clauses, and stock and crypto prices and fundamentals from several providers.",
|
|
5
|
+
"description": "Open business and financial data for TypeScript: register lookups on the German register portal (handelsregister.de) without storage, the statutory public holidays of the German federal states, the base rate of § 247 BGB, the German consumer price index with the arithmetic of index clauses, published financial statements read from their HTML, PDF or ESEF documents, and stock and crypto prices and fundamentals from several providers.",
|
|
6
6
|
"main": "dist_ts/index.js",
|
|
7
7
|
"typings": "dist_ts/index.d.ts",
|
|
8
8
|
"exports": {
|
|
@@ -30,6 +30,10 @@
|
|
|
30
30
|
"types": "./dist_ts/cpi/index.d.ts",
|
|
31
31
|
"import": "./dist_ts/cpi/index.js"
|
|
32
32
|
},
|
|
33
|
+
"./financialstatements": {
|
|
34
|
+
"types": "./dist_ts/financialstatements/index.d.ts",
|
|
35
|
+
"import": "./dist_ts/financialstatements/index.js"
|
|
36
|
+
},
|
|
33
37
|
"./package.json": "./package.json"
|
|
34
38
|
},
|
|
35
39
|
"type": "module",
|
|
@@ -43,11 +47,13 @@
|
|
|
43
47
|
"@types/node": "26.6.4"
|
|
44
48
|
},
|
|
45
49
|
"dependencies": {
|
|
50
|
+
"@push.rocks/smartarchive": "^5.5.2",
|
|
46
51
|
"@push.rocks/smartpath": "^6.0.0",
|
|
47
52
|
"@push.rocks/smartpromise": "^4.3.0",
|
|
48
53
|
"@push.rocks/smartrequest": "^5.0.1",
|
|
49
54
|
"@push.rocks/smartxml": "^2.1.0",
|
|
50
|
-
"parse5": "^8.0.1"
|
|
55
|
+
"parse5": "^8.0.1",
|
|
56
|
+
"pdfjs-dist": "^6.3.289"
|
|
51
57
|
},
|
|
52
58
|
"repository": {
|
|
53
59
|
"type": "git",
|
|
@@ -93,7 +99,11 @@
|
|
|
93
99
|
"base rate",
|
|
94
100
|
"consumer price index",
|
|
95
101
|
"Verbraucherpreisindex",
|
|
96
|
-
"index clause"
|
|
102
|
+
"index clause",
|
|
103
|
+
"Jahresabschluss",
|
|
104
|
+
"financial statements",
|
|
105
|
+
"HGB",
|
|
106
|
+
"ESEF"
|
|
97
107
|
],
|
|
98
108
|
"scripts": {
|
|
99
109
|
"test": "(tstest test/ --verbose)",
|
package/readme.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# @fin.cx/opendata
|
|
2
2
|
|
|
3
|
-
`@fin.cx/opendata` is a TypeScript toolbox for open business and market data: stock and crypto prices, SEC fundamentals, lookups in the German registers through the register portal of the federal states, the statutory public holidays of the German federal states, the base rate of § 247 BGB,
|
|
3
|
+
`@fin.cx/opendata` is a TypeScript toolbox for open business and market data: stock and crypto prices, SEC fundamentals, lookups in the German registers through the register portal of the federal states, the statutory public holidays of the German federal states, the base rate of § 247 BGB, the German consumer price index for index clauses, and the figures of published financial statements. It is built for programmers who want scriptable APIs and real workflows instead of a thin wrapper around one remote endpoint.
|
|
4
4
|
|
|
5
|
-
It gives you
|
|
5
|
+
It gives you eight practical lanes in one package:
|
|
6
6
|
|
|
7
7
|
- `StockPriceService` for stock and crypto prices with caching, retries, failover, historical ranges, and intraday support
|
|
8
8
|
- `FundamentalsService` and `StockDataService` for SEC fundamentals and combined price + fundamentals payloads
|
|
@@ -11,6 +11,7 @@ It gives you seven practical lanes in one package:
|
|
|
11
11
|
- the base rate, from `@fin.cx/opendata/baserate`, for the Basiszinssatz of § 247 BGB per half-year as the Deutsche Bundesbank publishes it, for default interest under § 288 BGB
|
|
12
12
|
- VAT ID confirmation, from `@fin.cx/opendata/vatid`, over the BZSt's eVatR REST API (the confirmation of § 18e UStG, simple or qualified) and the European Commission's VIES REST API, without any storage
|
|
13
13
|
- the consumer price index, from `@fin.cx/opendata/cpi`, for the Verbraucherpreisindex für Deutschland of the Statistisches Bundesamt as the Deutsche Bundesbank republishes it, with the arithmetic of index clauses (Wertsicherungsklauseln, § 557b BGB), without any storage
|
|
14
|
+
- published financial statements, from `@fin.cx/opendata/financialstatements`, for the balance sheet and the income statement under the HGB from the HTML or the PDF the Unternehmensregister shows, and the facts of an ESEF report, read from a document the caller obtained, without any request or storage
|
|
14
15
|
|
|
15
16
|
## Issue Reporting and Security
|
|
16
17
|
|
|
@@ -179,6 +180,69 @@ The figures are the Statistisches Bundesamt's, under the Datenlizenz Deutschland
|
|
|
179
180
|
|
|
180
181
|
A change in percent, a threshold or a scaled amount is not a published figure. Mark it as such, for example "eigene Berechnung", next to the source note.
|
|
181
182
|
|
|
183
|
+
### Published Financial Statements
|
|
184
|
+
|
|
185
|
+
Everything here comes from `@fin.cx/opendata/financialstatements`. That entry point imports nothing else of the package, makes no request and keeps nothing. It reads a statement from the bytes the caller obtained, such as a statement saved from the Unternehmensregister, and returns what the caller stores: the filing, the document byte for byte, and what could be read from it. Its types hold no storage of their own, so any store can keep them.
|
|
186
|
+
|
|
187
|
+
- `readFinancialStatement({ filing, bytes, mediaType, now? })`
|
|
188
|
+
A statement as a caller keeps it (`IFinancialStatement`): the filing as the caller states it (`filing`, an `IFiling`), the document with its media type and SHA-256 (`raw`), the statement read from it (`parsed`), why none was (`parseFailure`), when it was obtained (`obtainedAt`) and the publisher's origin note (`notice`). An ESEF report, on its own or in its package, goes to the ESEF reader, and so does HTML that holds inline XBRL; any other HTML or PDF goes to `parseStatementDocument`.
|
|
189
|
+
- `parseStatementDocument(bytes, mediaType)`
|
|
190
|
+
A statement under the HGB from the HTML or the PDF a register shows (`IHgbStatement`): the balance sheet (§ 266 HGB) and, where one is published, the income statement (§ 275 HGB, by the total cost or the cost of sales method), each printed line with its outline code, label, kind and the amounts of both years, the unit (EUR, TEUR or Mio. EUR), the periods, and the key figures: balance sheet total, equity, net income and revenue. A PDF is read from its text layer.
|
|
191
|
+
- `parseEsefPackage(zipBytes)`, `parseEsefReport(xhtmlBytes, reportPath?)`
|
|
192
|
+
A listed issuer's annual financial report in the European Single Electronic Format (`IEsefStatement`): the LEI, every numeric fact its inline XBRL tags (concept, period, dimensions, unit, and the value with scale and sign applied), and the key figures of its consolidated statements from the IFRS concepts `Assets`, `Equity`, `ProfitLoss` and `Revenue`. A package must hold exactly one report under `reports/`.
|
|
193
|
+
- `unternehmensregisterNotice(publishedOn, deposited?)`, `unternehmensregisterOrigin`
|
|
194
|
+
The Unternehmensregister's origin note, "Auszug aus dem Unternehmensregister", with the day a document was posted or deposited.
|
|
195
|
+
- `maxStatementBytes` (100 MiB), `maxPdfPages` (500), `maxEsefFacts` (100,000), `maxEsefPackageEntries` (10,000)
|
|
196
|
+
- `FinancialStatementError` with the codes `invalid_input`, `unsupported_media_type`, `too_large`, `invalid_package`, `unreadable`, `no_text` and `unparseable`
|
|
197
|
+
|
|
198
|
+
Amounts are decimal strings in the currency itself, the unit's factor applied (`1.234` printed in TEUR is `'1234000'`), never floats.
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
import { FinancialStatementError, readFinancialStatement } from '@fin.cx/opendata/financialstatements';
|
|
202
|
+
|
|
203
|
+
const statement = await readFinancialStatement({
|
|
204
|
+
filing: {
|
|
205
|
+
source: 'unternehmensregister',
|
|
206
|
+
kind: 'annual_accounts',
|
|
207
|
+
companyName: 'Muster Werkzeugbau GmbH',
|
|
208
|
+
fiscalYearStart: '2024-01-01',
|
|
209
|
+
fiscalYearEnd: '2024-12-31',
|
|
210
|
+
publishedOn: '2025-11-03',
|
|
211
|
+
},
|
|
212
|
+
bytes, // the PDF as saved from the register, a Uint8Array
|
|
213
|
+
mediaType: 'application/pdf',
|
|
214
|
+
});
|
|
215
|
+
// statement.raw: { bytes, mediaType: 'application/pdf', sha256: '...' }
|
|
216
|
+
// statement.notice: 'Auszug aus dem Unternehmensregister, eingestellt am 03.11.2025'
|
|
217
|
+
|
|
218
|
+
if (statement.parsed?.standard === 'hgb') {
|
|
219
|
+
statement.parsed.keyFigures.current;
|
|
220
|
+
// { totalAssets: '500000.00', equity: '300000.00', netIncome: '40000.00', revenue: '1000000.00' }
|
|
221
|
+
} else if (statement.parseFailure) {
|
|
222
|
+
statement.parseFailure; // { code: 'unparseable', message: 'No balance sheet was found.' }
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
await readFinancialStatement({ filing: statement.filing, bytes: scan, mediaType: 'image/png' });
|
|
227
|
+
} catch (error) {
|
|
228
|
+
if (error instanceof FinancialStatementError) {
|
|
229
|
+
console.log(error.code); // 'unsupported_media_type': nothing is kept
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
#### Read, never guessed
|
|
235
|
+
|
|
236
|
+
`parsed` is null rather than guessed. Everything `parseStatementDocument` returns was printed and adds up: each position with parts equals their sum, each side of the balance sheet its total, both totals each other, and the lines of the income statement its net income, whether it prints its expenses with a minus or as amounts to subtract. A balance sheet whose current year does not add up, whose columns do not state their year or unit, that holds text where an amount belongs, or that is in a currency other than the euro ends in `unparseable`; `readFinancialStatement` then keeps the document with `parsed` null and the reason in `parseFailure`. A prior year that does not add up, and an income statement that does not, are left out with a note in `notes`. A PDF without a text layer, such as a scan, ends in `no_text`: nothing is recognised from images.
|
|
237
|
+
|
|
238
|
+
The ESEF reader takes values as the report tags them. It reads the formats `num-dot-decimal`, `num-comma-decimal`, `fixed-zero` and `zerodash` (and the older `numdotdecimal` and `numcommadecimal`); a fact in another format is left out and counted in `notes`. A key figure tagged twice with values that differ at the coarser of their `decimals` is left out with a note.
|
|
239
|
+
|
|
240
|
+
#### Where the documents come from
|
|
241
|
+
|
|
242
|
+
The entry point makes no request; the caller obtains each document and passes its bytes in. The statements of financial years beginning after 31 December 2021 are published in the Unternehmensregister, earlier ones in the Bundesanzeiger. Both show a statement only after a security check (a captcha); the Unternehmensregister shows it as HTML and usually also as PDF. A balance sheet a micro company deposited (§ 326 Abs. 2 HGB) is retrieved only for a fee and after registration. Listed issuers publish their ESEF reports on their own sites as well.
|
|
243
|
+
|
|
244
|
+
The Unternehmensregister's terms of use allow copying the statements it makes accessible by print or as a file (§ 3 Abs. 3) and mark them with the origin note "Auszug aus dem Unternehmensregister" and the day the document was posted, or for a deposited balance sheet the day it was deposited (§ 3 Abs. 5). A filing from the Unternehmensregister therefore needs `publishedOn`, and `notice` gives the note, so that a kept copy keeps it.
|
|
245
|
+
|
|
182
246
|
## Before You Start
|
|
183
247
|
|
|
184
248
|
- No stock or fundamentals providers are pre-registered. You must register them before calling the services.
|
|
@@ -613,9 +677,18 @@ Four holidays apply only in part of a state:
|
|
|
613
677
|
- `bundesbankCpiBaseUrl`, `bundesbankVpiMonthlyKey`, `bundesbankVpiAnnualKey`, `bundesbankVpiFirstMonth`, `bundesbankVpiFirstYear`, `destatisLicence`, `defaultCpiTimeoutMs`
|
|
614
678
|
- `CpiError` with the codes `invalid_input`, `before_first_period`, `not_published`, `base_mismatch`, `series_withdrawn`, `busy`, `unavailable`, `unreachable`, `timeout`, `aborted` and `unexpected_answer`
|
|
615
679
|
|
|
680
|
+
### Financial statements (`@fin.cx/opendata/financialstatements`)
|
|
681
|
+
|
|
682
|
+
- `readFinancialStatement({ filing, bytes, mediaType, now? })`
|
|
683
|
+
- `parseStatementDocument(bytes, mediaType)`
|
|
684
|
+
- `parseEsefPackage(zipBytes)`, `parseEsefReport(xhtmlBytes, reportPath?)`
|
|
685
|
+
- `unternehmensregisterNotice(publishedOn, deposited?)`, `unternehmensregisterOrigin`, `maxStatementBytes`, `maxPdfPages`, `maxEsefFacts`, `maxEsefPackageEntries`
|
|
686
|
+
- the types `IFiling`, `IFinancialStatement`, `IHgbStatement`, `IEsefStatement`, `IKeyFigures`, `IStatementPosition` and their parts
|
|
687
|
+
- `FinancialStatementError` with the codes `invalid_input`, `unsupported_media_type`, `too_large`, `invalid_package`, `unreadable`, `no_text` and `unparseable`
|
|
688
|
+
|
|
616
689
|
## Data Sources
|
|
617
690
|
|
|
618
|
-
The package ships no market or register data. Prices and fundamentals come at run time from the providers you register (Marketstack, CoinGecko, SEC EDGAR), and register entries come at run time from the register portal of the federal states (handelsregister.de), one lookup at a time.
|
|
691
|
+
The package ships no market or register data. Prices and fundamentals come at run time from the providers you register (Marketstack, CoinGecko, SEC EDGAR), and register entries come at run time from the register portal of the federal states (handelsregister.de), one lookup at a time. Financial statements come from the caller: `@fin.cx/opendata/financialstatements` reads the documents it is given and fetches none.
|
|
619
692
|
|
|
620
693
|
The package carries two kinds of data: the holiday rules of `@fin.cx/opendata/holidays` (`ts/holidays/data.holidays.ts`), described at the end of this section, and the code tables that `@fin.cx/opendata/handelsregister` uses to decode the structured register content (`ts/handelsregister/data.codelists.ts`). The code tables are official code lists, published on XRepository (https://www.xrepository.de), and were taken on 2026-09-23:
|
|
621
694
|
|
|
@@ -674,6 +747,10 @@ The consumer price index of `@fin.cx/opendata/cpi` is read at run time, one requ
|
|
|
674
747
|
|
|
675
748
|
- Not verified: limits on the number of requests to the Bundesbank's API (none is documented) and terms of use specific to it. The Bundesbank's terms for reusing statistics exclude data of third parties, so the figures are used under the Statistisches Bundesamt's licence. Whether the Bundesbank keeps the series of an old base for a while after a change of base year is not known; today the key of 2015=100 answers 404.
|
|
676
749
|
|
|
750
|
+
The reader of `@fin.cx/opendata/financialstatements` follows the outlines of the balance sheet (§ 266 HGB) and the income statement (§ 275 HGB), the terms of use of the Unternehmensregister (`https://www.unternehmensregister.de/i18n-doc/D061_UReg_nutz_0118_de.pdf`, § 3) and its help page on searching, which names the security check, both read on 2026-10-10. The ESEF reader was also run on two reports issuers published, which are not part of the repository.
|
|
751
|
+
|
|
752
|
+
- Not verified: the markup the Unternehmensregister and the Bundesanzeiger show a statement in today, and the PDF they make of it. No live statement was opened, to stay clear of the security check; the HTML and PDF readers are tested against documents written in the shape of published statements, not against saved ones. Whether readers can obtain a statement as filed in XML or XBRL is not known either; the help pages name HTML and PDF only.
|
|
753
|
+
|
|
677
754
|
## Behavior And Caveats
|
|
678
755
|
|
|
679
756
|
- Historical market data is cached aggressively because it is treated as immutable.
|
|
@@ -681,6 +758,7 @@ The consumer price index of `@fin.cx/opendata/cpi` is read at run time, one requ
|
|
|
681
758
|
- `StockDataService` is convenience-first. `StockPriceService` is the better low-level choice if you care about request-shape-specific cache behavior.
|
|
682
759
|
- The register lookup reads the portal's German pages and asks for German explicitly. It depends on the portal's current page structure: when that changes, calls end in `unexpected_response` rather than in guessed data.
|
|
683
760
|
- The structured register content is a non-binding service of the portal; the current printout is binding. Results carry `binding: false` and the portal's note.
|
|
761
|
+
- The statement reader depends on how a statement is laid out: when a layout is not one it can read with certainty, the document is kept with `parsed` null rather than read in part or guessed. It reads the balance sheet and the income statement under the HGB and the numeric facts of ESEF reports; the notes and the management report are not read.
|
|
684
762
|
- There is no hosted backend in this package. Everything is meant to run from your own TypeScript/Node process.
|
|
685
763
|
|
|
686
764
|
## Testing
|
|
@@ -689,7 +767,7 @@ The consumer price index of `@fin.cx/opendata/cpi` is read at run time, one requ
|
|
|
689
767
|
pnpm test
|
|
690
768
|
```
|
|
691
769
|
|
|
692
|
-
Some market data tests hit live remote services, so they may depend on network access or optional API credentials. The register lookup is tested against minimal hand-written pages and structured content with synthetic values only; the tests never contact the register portal. The VAT ID clients are tested against the answers the two services gave on 2026-09-27 to their documented test values (`test/fixtures/vatid/`); the tests never contact them. The consumer price index client is tested against the answers the Bundesbank's API gave on 2026-10-05 and two answers written by hand from them (`test/fixtures/cpi/`); the tests never contact it.
|
|
770
|
+
Some market data tests hit live remote services, so they may depend on network access or optional API credentials. The register lookup is tested against minimal hand-written pages and structured content with synthetic values only; the tests never contact the register portal. The VAT ID clients are tested against the answers the two services gave on 2026-09-27 to their documented test values (`test/fixtures/vatid/`); the tests never contact them. The consumer price index client is tested against the answers the Bundesbank's API gave on 2026-10-05 and two answers written by hand from them (`test/fixtures/cpi/`); the tests never contact it. The financial statement reader is tested against HTML statements written by hand in the shape of the register's (`test/fixtures/financialstatements/`), PDFs the tests write themselves, and ESEF reports and packages the tests write, all with synthetic values; the tests contact no register.
|
|
693
771
|
|
|
694
772
|
## License and Legal Information
|
|
695
773
|
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@fin.cx/opendata',
|
|
6
|
-
version: '4.
|
|
7
|
-
description: 'Open business and financial data for TypeScript: register lookups on the German register portal (handelsregister.de) without storage, the statutory public holidays of the German federal states, the base rate of § 247 BGB, the German consumer price index with the arithmetic of index clauses, and stock and crypto prices and fundamentals from several providers.'
|
|
6
|
+
version: '4.8.0',
|
|
7
|
+
description: 'Open business and financial data for TypeScript: register lookups on the German register portal (handelsregister.de) without storage, the statutory public holidays of the German federal states, the base rate of § 247 BGB, the German consumer price index with the arithmetic of index clauses, published financial statements read from their HTML, PDF or ESEF documents, and stock and crypto prices and fundamentals from several providers.'
|
|
8
8
|
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import type { IStatementUnit } from './interfaces.js';
|
|
2
|
+
|
|
3
|
+
/** millionths of a euro per euro: amounts are kept as whole millionths, so no sum loses a digit */
|
|
4
|
+
const microsPerEuro = 1_000_000n;
|
|
5
|
+
|
|
6
|
+
/** a step at or below one cent: the amount was printed to the cent and is exact */
|
|
7
|
+
const exactStep = 10_000n;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* An amount as this entry point computes with it: millionths of a euro, and the rounding it may
|
|
11
|
+
* carry. `slack` is twice the largest rounding error, also in millionths: zero for an amount printed
|
|
12
|
+
* to the cent, the step of the last printed digit for one printed rounded, and the sum of its terms'
|
|
13
|
+
* slack for a sum.
|
|
14
|
+
*/
|
|
15
|
+
export interface IAmount {
|
|
16
|
+
micros: bigint;
|
|
17
|
+
slack: bigint;
|
|
18
|
+
/** the digits after the decimal point the amount is written with */
|
|
19
|
+
fractionDigits: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** a number as printed, before its unit is applied */
|
|
23
|
+
export interface IPrintedAmount {
|
|
24
|
+
/** the printed digits as one integer: `1.234,5` is 12345 */
|
|
25
|
+
digits: bigint;
|
|
26
|
+
/** the digits after the decimal comma */
|
|
27
|
+
decimals: number;
|
|
28
|
+
/** the unit printed with the number, where one is */
|
|
29
|
+
unit: IStatementUnit | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const unitEur: IStatementUnit = { label: 'EUR', factor: '1' };
|
|
33
|
+
export const unitTeur: IStatementUnit = { label: 'TEUR', factor: '1000' };
|
|
34
|
+
export const unitMioEur: IStatementUnit = { label: 'Mio. EUR', factor: '1000000' };
|
|
35
|
+
|
|
36
|
+
/** the euro after a prefix such as `Mio.` or `T` */
|
|
37
|
+
const euroSuffix = String.raw`(?:€|EUR\b|Euro\b|EURO\b)`;
|
|
38
|
+
|
|
39
|
+
/** the units the statements print, millions and thousands before plain euros */
|
|
40
|
+
const unitPatterns: Array<{ unit: IStatementUnit; pattern: string }> = [
|
|
41
|
+
{ unit: unitMioEur, pattern: String.raw`\b(?:Mio\.?|Millionen)\s*${euroSuffix}` },
|
|
42
|
+
{ unit: unitTeur, pattern: String.raw`\b(?:T|Tsd\.?\s*|Tausend\s+)${euroSuffix}` },
|
|
43
|
+
{ unit: unitEur, pattern: String.raw`(?:€|\b${euroSuffix})` },
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
const unitAlternation = unitPatterns.map((entryArg) => `(${entryArg.pattern})`).join('|');
|
|
47
|
+
|
|
48
|
+
/** the units a text names, in the order it names them */
|
|
49
|
+
export const unitsIn = (textArg: string): IStatementUnit[] => {
|
|
50
|
+
const result: IStatementUnit[] = [];
|
|
51
|
+
for (const match of textArg.matchAll(new RegExp(unitAlternation, 'g'))) {
|
|
52
|
+
const index = match.slice(1).findIndex((groupArg) => groupArg !== undefined);
|
|
53
|
+
result.push(unitPatterns[index].unit);
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/** a text with the units it names blanked out */
|
|
59
|
+
export const withoutUnits = (textArg: string): string => textArg.replace(new RegExp(unitAlternation, 'g'), ' ');
|
|
60
|
+
|
|
61
|
+
/** the currencies other than the euro a statement may name */
|
|
62
|
+
const otherCurrencySource = String.raw`\b(?:USD|US-Dollar|CHF|GBP|JPY|CNY|DKK|SEK|NOK|PLN|CZK|HUF)\b|\$|£|¥`;
|
|
63
|
+
|
|
64
|
+
/** whether a text names a currency other than the euro */
|
|
65
|
+
export const namesOtherCurrency = (textArg: string): boolean => new RegExp(otherCurrencySource).test(textArg);
|
|
66
|
+
|
|
67
|
+
/** a text with the currencies other than the euro it names blanked out */
|
|
68
|
+
export const withoutOtherCurrencies = (textArg: string): string => textArg.replace(new RegExp(otherCurrencySource, 'g'), ' ');
|
|
69
|
+
|
|
70
|
+
/** the unit alone, `TEUR` or `(in TEUR)`, as a line of its own */
|
|
71
|
+
const unitOnlyPattern = new RegExp(String.raw`^\(?\s*(?:in\s+)?(?:${unitAlternation})\s*\)?$`);
|
|
72
|
+
|
|
73
|
+
/** the explicit phrases a statement states its unit with: `Angaben in TEUR`, `(Beträge in Euro)` */
|
|
74
|
+
const unitPhrasePattern = new RegExp(
|
|
75
|
+
String.raw`(?:\(\s*in\s+|\b(?:Angaben|Beträge|Betrag|Werte|Zahlen|Alle Beträge|Zahlenangaben)\s+in\s+)(?:${unitAlternation})`
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
/** the unit a line of context states, `Angaben in TEUR` or a line that is only a unit; null if none */
|
|
79
|
+
export const unitStatedBy = (textArg: string): IStatementUnit | null => {
|
|
80
|
+
const text = normalizeSpaces(textArg);
|
|
81
|
+
if (text.length > 200) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
const phrase = unitPhrasePattern.exec(text);
|
|
85
|
+
if (phrase) {
|
|
86
|
+
return unitsIn(phrase[0])[0] ?? null;
|
|
87
|
+
}
|
|
88
|
+
return unitOnlyPattern.test(text) ? unitsIn(text)[0] ?? null : null;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/** collapses the spaces statements print, no-break and narrow ones among them */
|
|
92
|
+
export const normalizeSpaces = (textArg: string): string => textArg.replace(/\s+/g, ' ').trim();
|
|
93
|
+
|
|
94
|
+
const dashOnlyPattern = /^[-–—−]{1,3}$/;
|
|
95
|
+
const numberPattern = /^([-–—−])?\s*(\d{1,3}(?:[. ]\d{3})+|\d+)(?:,(\d{1,6}))?$/;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Reads an amount as German statements print it: `1.234.567,89`, `-1 234`, `– 12,5`, a dash alone
|
|
99
|
+
* for zero, with or without a unit before or after it (`EUR 1.234,56`, `1.234 T€`). Returns null for
|
|
100
|
+
* anything else, a year or a note number among them for the caller to tell apart.
|
|
101
|
+
*/
|
|
102
|
+
export const parsePrintedAmount = (textArg: string): IPrintedAmount | null => {
|
|
103
|
+
let text = normalizeSpaces(textArg);
|
|
104
|
+
if (text === '') {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
let unit: IStatementUnit | null = null;
|
|
108
|
+
const leading = new RegExp(String.raw`^(?:${unitAlternation})\s*`).exec(text);
|
|
109
|
+
if (leading) {
|
|
110
|
+
unit = unitsIn(leading[0])[0];
|
|
111
|
+
text = text.slice(leading[0].length);
|
|
112
|
+
} else {
|
|
113
|
+
const trailing = new RegExp(String.raw`\s*(?:${unitAlternation})$`).exec(text);
|
|
114
|
+
if (trailing && trailing.index > 0) {
|
|
115
|
+
unit = unitsIn(trailing[0])[0];
|
|
116
|
+
text = text.slice(0, trailing.index);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (dashOnlyPattern.test(text)) {
|
|
120
|
+
return { digits: 0n, decimals: 0, unit };
|
|
121
|
+
}
|
|
122
|
+
const match = numberPattern.exec(text);
|
|
123
|
+
if (!match) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
const decimals = match[3]?.length ?? 0;
|
|
127
|
+
const magnitude = BigInt(match[2].replace(/[. ]/g, '') + (match[3] ?? ''));
|
|
128
|
+
return { digits: match[1] ? -magnitude : magnitude, decimals, unit };
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/** whether a text reads as an amount rather than as a year such as `2024` or `2024 EUR` */
|
|
132
|
+
export const readsAsAmount = (textArg: string): boolean =>
|
|
133
|
+
parsePrintedAmount(textArg) !== null && !/^(?:19|20)\d{2}$/.test(normalizeSpaces(withoutUnits(textArg)));
|
|
134
|
+
|
|
135
|
+
/** an amount in a unit: `1.234` in TEUR is 1,234,000 euros, rounded to the thousand */
|
|
136
|
+
export const toAmount = (printedArg: IPrintedAmount, unitArg: IStatementUnit): IAmount => {
|
|
137
|
+
const step = BigInt(unitArg.factor) * 10n ** BigInt(6 - printedArg.decimals);
|
|
138
|
+
return {
|
|
139
|
+
micros: printedArg.digits * step,
|
|
140
|
+
slack: step <= exactStep ? 0n : step,
|
|
141
|
+
fractionDigits: fractionDigitsOf(step),
|
|
142
|
+
};
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const fractionDigitsOf = (stepArg: bigint): number => {
|
|
146
|
+
let digits = 6;
|
|
147
|
+
for (let step = stepArg; digits > 0 && step % 10n === 0n; step /= 10n) {
|
|
148
|
+
digits--;
|
|
149
|
+
}
|
|
150
|
+
return digits;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
export const zeroAmount: IAmount = { micros: 0n, slack: 0n, fractionDigits: 0 };
|
|
154
|
+
|
|
155
|
+
export const addAmounts = (leftArg: IAmount, rightArg: IAmount): IAmount => ({
|
|
156
|
+
micros: leftArg.micros + rightArg.micros,
|
|
157
|
+
slack: leftArg.slack + rightArg.slack,
|
|
158
|
+
fractionDigits: Math.max(leftArg.fractionDigits, rightArg.fractionDigits),
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
export const negateAmount = (amountArg: IAmount): IAmount => ({ ...amountArg, micros: -amountArg.micros });
|
|
162
|
+
|
|
163
|
+
export const sumAmounts = (amountsArg: IAmount[]): IAmount => amountsArg.reduce(addAmounts, zeroAmount);
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Whether two amounts can be the same figure: equal, or for figures printed rounded, no further
|
|
167
|
+
* apart than both their roundings together allow.
|
|
168
|
+
*/
|
|
169
|
+
export const amountsAgree = (leftArg: IAmount, rightArg: IAmount): boolean => {
|
|
170
|
+
const difference = leftArg.micros - rightArg.micros;
|
|
171
|
+
return (difference < 0n ? -difference : difference) * 2n <= leftArg.slack + rightArg.slack;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
/** an amount as a decimal string in euros, written with its own digits: `1234000`, `-12.50` */
|
|
175
|
+
export const formatAmount = (amountArg: IAmount): string => {
|
|
176
|
+
const negative = amountArg.micros < 0n;
|
|
177
|
+
const magnitude = negative ? -amountArg.micros : amountArg.micros;
|
|
178
|
+
const whole = (magnitude / microsPerEuro).toString();
|
|
179
|
+
const fraction = (magnitude % microsPerEuro).toString().padStart(6, '0').slice(0, amountArg.fractionDigits);
|
|
180
|
+
return `${negative ? '-' : ''}${whole}${fraction === '' ? '' : `.${fraction}`}`;
|
|
181
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Why a document was not read:
|
|
3
|
+
* - `invalid_input`: arguments this entry point does not take: no bytes, a date that is not one, a
|
|
4
|
+
* statement from the Unternehmensregister without the day it was posted;
|
|
5
|
+
* - `unsupported_media_type`: a media type other than HTML, PDF, XHTML or a ZIP package;
|
|
6
|
+
* - `too_large`: a document, or the report in a package, larger than `maxStatementBytes`, a package
|
|
7
|
+
* with more than `maxEsefPackageEntries` entries, a PDF with more than `maxPdfPages` pages, or a
|
|
8
|
+
* report with more than `maxEsefFacts` facts;
|
|
9
|
+
* - `invalid_package`: a package that is not a readable ZIP, is encrypted, or does not hold exactly
|
|
10
|
+
* one report under `reports/`;
|
|
11
|
+
* - `unreadable`: a document that does not open as what its media type says: a damaged or
|
|
12
|
+
* password-protected PDF, HTML in an unknown encoding, XHTML that is not well-formed;
|
|
13
|
+
* - `no_text`: a PDF without a text layer, such as a scan;
|
|
14
|
+
* - `unparseable`: a readable document in which no statement was found with certainty: no balance
|
|
15
|
+
* sheet, columns that do not say their year or unit, or current-year figures that do not add up.
|
|
16
|
+
*/
|
|
17
|
+
export type TFinancialStatementErrorCode =
|
|
18
|
+
| 'invalid_input'
|
|
19
|
+
| 'unsupported_media_type'
|
|
20
|
+
| 'too_large'
|
|
21
|
+
| 'invalid_package'
|
|
22
|
+
| 'unreadable'
|
|
23
|
+
| 'no_text'
|
|
24
|
+
| 'unparseable';
|
|
25
|
+
|
|
26
|
+
/** The typed error of `@fin.cx/opendata/financialstatements`. */
|
|
27
|
+
export class FinancialStatementError extends Error {
|
|
28
|
+
public readonly code: TFinancialStatementErrorCode;
|
|
29
|
+
|
|
30
|
+
constructor(code: TFinancialStatementErrorCode, message: string, options?: { cause?: unknown }) {
|
|
31
|
+
super(message, options);
|
|
32
|
+
this.name = 'FinancialStatementError';
|
|
33
|
+
this.code = code;
|
|
34
|
+
}
|
|
35
|
+
}
|