@fin.cx/opendata 3.5.1 → 4.0.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 (87) hide show
  1. package/{npmextra.json → .smartconfig.json} +24 -18
  2. package/dist_ts/00_commitinfo_data.js +3 -3
  3. package/dist_ts/handelsregister/classes.handelsregisterclient.d.ts +64 -0
  4. package/dist_ts/handelsregister/classes.handelsregisterclient.js +397 -0
  5. package/dist_ts/handelsregister/classes.handelsregistererror.d.ts +56 -0
  6. package/dist_ts/handelsregister/classes.handelsregistererror.js +37 -0
  7. package/dist_ts/handelsregister/classes.portalsession.d.ts +46 -0
  8. package/dist_ts/handelsregister/classes.portalsession.js +160 -0
  9. package/dist_ts/handelsregister/classes.slidingwindowgate.d.ts +32 -0
  10. package/dist_ts/handelsregister/classes.slidingwindowgate.js +55 -0
  11. package/dist_ts/handelsregister/constants.d.ts +13 -0
  12. package/dist_ts/handelsregister/constants.js +19 -0
  13. package/dist_ts/handelsregister/data.codelists.d.ts +29 -0
  14. package/dist_ts/handelsregister/data.codelists.js +616 -0
  15. package/dist_ts/handelsregister/forms.d.ts +27 -0
  16. package/dist_ts/handelsregister/forms.js +77 -0
  17. package/dist_ts/handelsregister/html.d.ts +26 -0
  18. package/dist_ts/handelsregister/html.js +82 -0
  19. package/dist_ts/handelsregister/index.d.ts +9 -0
  20. package/dist_ts/handelsregister/index.js +10 -0
  21. package/dist_ts/handelsregister/interfaces.d.ts +240 -0
  22. package/dist_ts/handelsregister/interfaces.js +2 -0
  23. package/dist_ts/handelsregister/parse.searchresults.d.ts +35 -0
  24. package/dist_ts/handelsregister/parse.searchresults.js +175 -0
  25. package/dist_ts/handelsregister/parse.structuredcontent.d.ts +6 -0
  26. package/dist_ts/handelsregister/parse.structuredcontent.js +306 -0
  27. package/dist_ts/handelsregister/plugins.d.ts +4 -0
  28. package/dist_ts/handelsregister/plugins.js +8 -0
  29. package/dist_ts/handelsregister/references.d.ts +34 -0
  30. package/dist_ts/handelsregister/references.js +76 -0
  31. package/dist_ts/index.d.ts +0 -1
  32. package/dist_ts/index.js +1 -2
  33. package/dist_ts/plugins.d.ts +2 -14
  34. package/dist_ts/plugins.js +8 -16
  35. package/dist_ts/stocks/classes.baseproviderservice.js +8 -8
  36. package/dist_ts/stocks/classes.fundamentalsservice.js +9 -9
  37. package/dist_ts/stocks/classes.stockdataservice.js +23 -23
  38. package/dist_ts/stocks/classes.stockservice.js +9 -9
  39. package/dist_ts/stocks/providers/provider.coingecko.d.ts +1 -1
  40. package/dist_ts/stocks/providers/provider.coingecko.js +45 -39
  41. package/dist_ts/stocks/providers/provider.marketstack.d.ts +3 -3
  42. package/dist_ts/stocks/providers/provider.marketstack.js +20 -16
  43. package/dist_ts/stocks/providers/provider.secedgar.d.ts +1 -1
  44. package/dist_ts/stocks/providers/provider.secedgar.js +22 -22
  45. package/dist_ts/stocks/providers/provider.yahoo.d.ts +2 -2
  46. package/dist_ts/stocks/providers/provider.yahoo.js +14 -13
  47. package/package.json +30 -31
  48. package/readme.md +327 -676
  49. package/ts/00_commitinfo_data.ts +2 -2
  50. package/ts/handelsregister/classes.handelsregisterclient.ts +474 -0
  51. package/ts/handelsregister/classes.handelsregistererror.ts +82 -0
  52. package/ts/handelsregister/classes.portalsession.ts +191 -0
  53. package/ts/handelsregister/classes.slidingwindowgate.ts +74 -0
  54. package/ts/handelsregister/constants.ts +30 -0
  55. package/ts/handelsregister/data.codelists.ts +640 -0
  56. package/ts/handelsregister/forms.ts +99 -0
  57. package/ts/handelsregister/html.ts +113 -0
  58. package/ts/handelsregister/index.ts +27 -0
  59. package/ts/handelsregister/interfaces.ts +272 -0
  60. package/ts/handelsregister/parse.searchresults.ts +241 -0
  61. package/ts/handelsregister/parse.structuredcontent.ts +338 -0
  62. package/ts/handelsregister/plugins.ts +15 -0
  63. package/ts/handelsregister/references.ts +115 -0
  64. package/ts/index.ts +0 -1
  65. package/ts/plugins.ts +9 -29
  66. package/ts/stocks/classes.fundamentalsservice.ts +1 -1
  67. package/ts/stocks/classes.stockdataservice.ts +17 -7
  68. package/ts/stocks/classes.stockservice.ts +2 -2
  69. package/ts/stocks/providers/provider.marketstack.ts +9 -7
  70. package/ts/stocks/providers/provider.secedgar.ts +2 -2
  71. package/ts/stocks/providers/provider.yahoo.ts +3 -3
  72. package/dist_ts/classes.businessrecord.d.ts +0 -38
  73. package/dist_ts/classes.businessrecord.js +0 -47
  74. package/dist_ts/classes.germanbusinessdata.d.ts +0 -9
  75. package/dist_ts/classes.germanbusinessdata.js +0 -78
  76. package/dist_ts/classes.handelsregister.d.ts +0 -132
  77. package/dist_ts/classes.handelsregister.js +0 -288
  78. package/dist_ts/classes.jsonldata.d.ts +0 -43
  79. package/dist_ts/classes.jsonldata.js +0 -65
  80. package/dist_ts/classes.main.opendata.d.ts +0 -24
  81. package/dist_ts/classes.main.opendata.js +0 -58
  82. package/readme.hints.md +0 -89
  83. package/readme.plan.md +0 -193
  84. package/ts/classes.businessrecord.ts +0 -67
  85. package/ts/classes.handelsregister.ts +0 -360
  86. package/ts/classes.jsonldata.ts +0 -113
  87. package/ts/classes.main.opendata.ts +0 -87
@@ -0,0 +1,241 @@
1
+ import { HandelsregisterError } from './classes.handelsregistererror.js';
2
+ import { registerDocuments, registerTypes, structuredContentNotice } from './constants.js';
3
+ import { registerCourts } from './data.codelists.js';
4
+ import * as forms from './forms.js';
5
+ import * as html from './html.js';
6
+ import type {
7
+ IRegisterCourt,
8
+ IRegisterFormerName,
9
+ IRegisterMatch,
10
+ IStructuredContentNotice,
11
+ TRegisterDocument,
12
+ TRegisterSheetStatus,
13
+ TRegisterType,
14
+ } from './interfaces.js';
15
+
16
+ export const resultFormId = 'ergebnissForm';
17
+ const resultTableId = 'ergebnissForm:selectedSuchErgebnisFormTable';
18
+ const resultRowsId = `${resultTableId}_data`;
19
+ const hitLimitLabelId = 'ergebnissForm:ergebnisseAnzahl_label';
20
+
21
+ /** one row of the result list with what is needed to retrieve its documents */
22
+ export interface IResultRow {
23
+ match: IRegisterMatch;
24
+ /** the extra fields a click on each document link submits with the result form */
25
+ documentSubmissions: Map<TRegisterDocument, Array<[string, string]>>;
26
+ }
27
+
28
+ export interface IParsedResultList {
29
+ rows: IResultRow[];
30
+ truncated: boolean;
31
+ portalNotice?: string;
32
+ /** the number of hits the portal reports */
33
+ hitCount?: number;
34
+ /** the result form, for retrieving documents */
35
+ form?: forms.IPortalForm;
36
+ }
37
+
38
+ /** the public shape of a parsed result list */
39
+ export interface IParsedSearchResults {
40
+ matches: IRegisterMatch[];
41
+ truncated: boolean;
42
+ portalNotice?: string;
43
+ notice: IStructuredContentNotice;
44
+ }
45
+
46
+ const unexpected = (messageArg: string): HandelsregisterError =>
47
+ new HandelsregisterError('unexpected_response', messageArg, { step: 'result_list' });
48
+
49
+ const sheetStatusOf = (textArg: string): TRegisterSheetStatus => {
50
+ switch (textArg) {
51
+ case 'aktuell':
52
+ return 'current';
53
+ case 'Geschlossenes Registerblatt':
54
+ return 'closed';
55
+ default:
56
+ return 'other';
57
+ }
58
+ };
59
+
60
+ const courtLinePattern = new RegExp(
61
+ `^Amtsgericht (.+?) (${registerTypes.join('|')}) (\\d+)(?: ([A-Za-zÄÖÜäöü]{1,5}))?(?: früher Amtsgericht (.+))?$`
62
+ );
63
+
64
+ interface ICourtLine {
65
+ court: IRegisterCourt;
66
+ type: TRegisterType;
67
+ number: string;
68
+ formerCourt?: string;
69
+ }
70
+
71
+ const readCourtLine = (textArg: string, courtsArg: readonly IRegisterCourt[]): ICourtLine => {
72
+ const match = courtLinePattern.exec(textArg);
73
+ if (!match) {
74
+ throw unexpected('A row of the result list names its register in an unknown form.');
75
+ }
76
+ const court = courtsArg.find((courtArg) => courtArg.name === match[1]);
77
+ if (!court) {
78
+ throw unexpected('A row of the result list names a register court this version does not know.');
79
+ }
80
+ const type = registerTypes.find((typeArg) => typeArg === match[2]);
81
+ if (!type) {
82
+ throw unexpected('A row of the result list names an unknown register type.');
83
+ }
84
+ const line: ICourtLine = { court, type, number: match[4] ? `${match[3]} ${match[4]}` : match[3] };
85
+ if (match[5]) {
86
+ line.formerCourt = match[5];
87
+ }
88
+ return line;
89
+ };
90
+
91
+ /** `1.) Beispiel GmbH` → `Beispiel GmbH` */
92
+ const withoutListNumber = (textArg: string): string => textArg.replace(/^\d+\.\)\s*/, '').trim();
93
+
94
+ /** the parameters `PrimeFaces.addSubmitParam('<form>', {...})` adds to a submission */
95
+ const readSubmitParameters = (onclickArg: string, formIdArg: string): Array<[string, string]> | undefined => {
96
+ const call = /PrimeFaces\.addSubmitParam\('([^']+)',\{([^}]*)\}\)/.exec(onclickArg);
97
+ if (!call || call[1] !== formIdArg) {
98
+ return undefined;
99
+ }
100
+ const parameters: Array<[string, string]> = [];
101
+ const pairPattern = /'([^']*)':'([^']*)'/g;
102
+ let pair: RegExpExecArray | null;
103
+ while ((pair = pairPattern.exec(call[2])) !== null) {
104
+ parameters.push([pair[1], pair[2]]);
105
+ }
106
+ return parameters.length > 0 ? parameters : undefined;
107
+ };
108
+
109
+ const readRow = (rowArg: html.THtmlElement, courtsArg: readonly IRegisterCourt[]): IResultRow => {
110
+ const headCell = html.findFirst(rowArg, (elementArg) => elementArg.tagName === 'td' && html.hasClass(elementArg, 'fontTableNameSize'));
111
+ const courtSpan = headCell && html.findFirst(headCell, (elementArg) => html.hasClass(elementArg, 'fontWeightBold'));
112
+ const seatCell = html.findFirst(rowArg, (elementArg) => elementArg.tagName === 'td' && html.hasClass(elementArg, 'sitzSuchErgebnisse'));
113
+ const nameCell = seatCell && html.previousElementSibling(seatCell);
114
+ const statusCell = seatCell && html.nextElementSibling(seatCell);
115
+ const documentsCell = statusCell && html.nextElementSibling(statusCell);
116
+ if (!headCell || !courtSpan || !seatCell || !nameCell || !statusCell || !documentsCell) {
117
+ throw unexpected('A row of the result list lacks a cell this version reads.');
118
+ }
119
+ const courtLine = readCourtLine(html.textOf(courtSpan), courtsArg);
120
+ const name = html.textOf(nameCell);
121
+ const sheetStatusText = html.textOf(statusCell);
122
+ if (!name || !sheetStatusText) {
123
+ throw unexpected('A row of the result list has no name or no status.');
124
+ }
125
+
126
+ const documents: TRegisterDocument[] = [];
127
+ const documentSubmissions = new Map<TRegisterDocument, Array<[string, string]>>();
128
+ for (const link of html.findAll(documentsCell, (elementArg) => elementArg.tagName === 'a')) {
129
+ const document = registerDocuments.find((documentArg) => documentArg === html.textOf(link));
130
+ if (!document) {
131
+ continue;
132
+ }
133
+ documents.push(document);
134
+ const parameters = readSubmitParameters(html.getAttribute(link, 'onclick') ?? '', resultFormId);
135
+ if (parameters) {
136
+ documentSubmissions.set(document, parameters);
137
+ }
138
+ }
139
+
140
+ const formerNames: IRegisterFormerName[] = [];
141
+ for (const nameHistoryCell of html.findAll(
142
+ rowArg,
143
+ (elementArg) => elementArg.tagName === 'td' && html.hasClass(elementArg, 'RegPortErg_HistorieZn')
144
+ )) {
145
+ const formerName = withoutListNumber(html.textOf(nameHistoryCell));
146
+ if (!formerName) {
147
+ continue;
148
+ }
149
+ const seatHistoryCell = html.nextElementSibling(nameHistoryCell);
150
+ const formerSeat =
151
+ seatHistoryCell && html.hasClass(seatHistoryCell, 'RegPortErg_SitzStatus')
152
+ ? withoutListNumber(html.textOf(seatHistoryCell))
153
+ : '';
154
+ formerNames.push(formerSeat ? { name: formerName, seat: formerSeat } : { name: formerName });
155
+ }
156
+
157
+ const match: IRegisterMatch = {
158
+ reference: { court: courtLine.court, type: courtLine.type, number: courtLine.number },
159
+ federalState: html.ownTextOf(headCell),
160
+ name,
161
+ seat: html.textOf(seatCell),
162
+ sheetStatus: sheetStatusOf(sheetStatusText),
163
+ sheetStatusText,
164
+ formerNames,
165
+ documents,
166
+ };
167
+ if (courtLine.formerCourt) {
168
+ match.formerCourt = courtLine.formerCourt;
169
+ }
170
+ return { match, documentSubmissions };
171
+ };
172
+
173
+ /**
174
+ * Reads the portal's result list. `courtsArg` are the register courts the rows are resolved
175
+ * against; by default the courts this version knows.
176
+ */
177
+ export const readResultList = (
178
+ htmlArg: string,
179
+ courtsArg: readonly IRegisterCourt[] = registerCourts
180
+ ): IParsedResultList => {
181
+ const document = html.parseHtml(htmlArg);
182
+ if (html.documentLanguage(document) !== 'de') {
183
+ throw unexpected('The result list is not in German.');
184
+ }
185
+ const rowsBody = html.findById(document, resultRowsId);
186
+ if (!rowsBody) {
187
+ throw unexpected('The answer contains no result list.');
188
+ }
189
+ const rows = html
190
+ .elementChildren(rowsBody)
191
+ .filter((elementArg) => elementArg.tagName === 'tr' && html.getAttribute(elementArg, 'data-ri') !== undefined)
192
+ .map((rowArg) => readRow(rowArg, courtsArg));
193
+ if (rows.length === 0) {
194
+ const emptyMessage = html.findFirst(rowsBody, (elementArg) => html.hasClass(elementArg, 'ui-datatable-empty-message'));
195
+ if (!emptyMessage) {
196
+ throw unexpected('The result list has neither rows nor the message that there are none.');
197
+ }
198
+ }
199
+
200
+ const table = html.findById(document, resultTableId);
201
+ const hitCounter = table && html.findFirst(table, (elementArg) => html.hasClass(elementArg, 'ui-paginator-current'));
202
+ const hitMatch = hitCounter ? /von (\d+) Treffer/.exec(html.textOf(hitCounter)) : null;
203
+ const hitCount = hitMatch ? Number(hitMatch[1]) : undefined;
204
+
205
+ const hitLimitLabel = html.findById(document, hitLimitLabelId);
206
+ const portalNotice = hitLimitLabel ? html.textOf(hitLimitLabel) : '';
207
+ const hitLimitReached = /Trefferanzahl/.test(portalNotice);
208
+
209
+ const result: IParsedResultList = {
210
+ rows,
211
+ truncated: hitLimitReached || (hitCount !== undefined && hitCount > rows.length),
212
+ };
213
+ if (portalNotice) {
214
+ result.portalNotice = portalNotice;
215
+ }
216
+ if (hitCount !== undefined) {
217
+ result.hitCount = hitCount;
218
+ }
219
+ const form = forms.readForm(document, resultFormId);
220
+ if (form) {
221
+ result.form = form;
222
+ }
223
+ return result;
224
+ };
225
+
226
+ /**
227
+ * Parses a result list page of the register portal, as saved from a browser, into matches.
228
+ * Makes no request.
229
+ */
230
+ export const parseSearchResults = (htmlArg: string): IParsedSearchResults => {
231
+ const list = readResultList(htmlArg);
232
+ const result: IParsedSearchResults = {
233
+ matches: list.rows.map((rowArg) => rowArg.match),
234
+ truncated: list.truncated,
235
+ notice: structuredContentNotice,
236
+ };
237
+ if (list.portalNotice) {
238
+ result.portalNotice = list.portalNotice;
239
+ }
240
+ return result;
241
+ };
@@ -0,0 +1,338 @@
1
+ import * as plugins from './plugins.js';
2
+ import { HandelsregisterError } from './classes.handelsregistererror.js';
3
+ import { structuredContentNotice } from './constants.js';
4
+ import { entityStatusLabels, legalFormLabels, roleLabels, stateKeyToIso } from './data.codelists.js';
5
+ import type {
6
+ IRegisterAddress,
7
+ IRegisterContentRegister,
8
+ IRegisterEntityStatus,
9
+ IRegisterLegalForm,
10
+ IRegisterRepresentative,
11
+ IRegisterSeat,
12
+ IRegisterStructuredContent,
13
+ TRegisterEntityState,
14
+ } from './interfaces.js';
15
+ import { formatRegisterNumber, parseRegisterNumber, parseRegisterReferenceText } from './references.js';
16
+
17
+ type TXmlElement = plugins.smartxmlXpath.dom.Element;
18
+
19
+ const xjustizNamespace = 'http://www.xjustiz.de';
20
+ const registerMessage = 'nachricht.reg.0400003';
21
+ /** business address types: "Dienst-/Geschäftsanschrift", then "Lage der Geschäftsräume" */
22
+ const businessAddressTypes = ['003', '021'];
23
+
24
+ const invalid = (messageArg: string, causeArg?: unknown): HandelsregisterError =>
25
+ new HandelsregisterError('invalid_structured_content', messageArg, {
26
+ step: 'structured_content',
27
+ ...(causeArg === undefined ? {} : { cause: causeArg }),
28
+ });
29
+
30
+ const childrenNamed = (elementArg: TXmlElement, nameArg: string): TXmlElement[] =>
31
+ elementArg.children.filter((childArg) => childArg.localName === nameArg);
32
+
33
+ /** the first element along a path of local names */
34
+ const at = (elementArg: TXmlElement | undefined, ...namesArg: string[]): TXmlElement | undefined => {
35
+ let current = elementArg;
36
+ for (const name of namesArg) {
37
+ if (!current) {
38
+ return undefined;
39
+ }
40
+ current = current.children.find((childArg) => childArg.localName === name);
41
+ }
42
+ return current;
43
+ };
44
+
45
+ /**
46
+ * the trimmed text of the value element along a path, or undefined when absent or empty. The
47
+ * XJustiz values read here (codes, names, dates, numbers) are text only, so a value element that
48
+ * holds elements is refused.
49
+ */
50
+ const textAt = (elementArg: TXmlElement | undefined, ...namesArg: string[]): string | undefined => {
51
+ const element = at(elementArg, ...namesArg);
52
+ if (!element) {
53
+ return undefined;
54
+ }
55
+ if (element.children.length > 0) {
56
+ throw invalid('A value in the structured content holds elements instead of text.');
57
+ }
58
+ const text = element.textContent?.replace(/\s+/g, ' ').trim();
59
+ return text ? text : undefined;
60
+ };
61
+
62
+ /** the code of a code list value along a path */
63
+ const codeAt = (elementArg: TXmlElement | undefined, ...namesArg: string[]): string | undefined =>
64
+ textAt(elementArg, ...namesArg, 'code');
65
+
66
+ interface IParticipant {
67
+ kind: 'organisation' | 'person';
68
+ element: TXmlElement;
69
+ }
70
+
71
+ interface IRole {
72
+ code?: string;
73
+ participant: IParticipant;
74
+ }
75
+
76
+ const readParticipant = (participationArg: TXmlElement): IParticipant | undefined => {
77
+ const choice = at(participationArg, 'beteiligter', 'auswahl_beteiligter');
78
+ const organisation = at(choice, 'organisation');
79
+ if (organisation) {
80
+ return { kind: 'organisation', element: organisation };
81
+ }
82
+ const person = at(choice, 'natuerlichePerson');
83
+ return person ? { kind: 'person', element: person } : undefined;
84
+ };
85
+
86
+ /** title, given names, name prefix, family name and suffix; nothing else of a person is read */
87
+ const personName = (personArg: TXmlElement): string => {
88
+ const fullName = at(personArg, 'vollerName');
89
+ const familyName = textAt(fullName, 'nachname');
90
+ if (!familyName) {
91
+ throw invalid('A person in the structured content has no family name.');
92
+ }
93
+ return [
94
+ textAt(fullName, 'titel'),
95
+ textAt(fullName, 'vorname'),
96
+ textAt(fullName, 'namensvorsatz'),
97
+ familyName,
98
+ textAt(fullName, 'namenszusatz'),
99
+ ]
100
+ .filter((partArg): partArg is string => partArg !== undefined)
101
+ .join(' ');
102
+ };
103
+
104
+ const organisationName = (organisationArg: TXmlElement): string | undefined =>
105
+ textAt(organisationArg, 'bezeichnung', 'bezeichnung.aktuell');
106
+
107
+ const participantName = (participantArg: IParticipant): string => {
108
+ if (participantArg.kind === 'person') {
109
+ return personName(participantArg.element);
110
+ }
111
+ const name = organisationName(participantArg.element);
112
+ if (!name) {
113
+ throw invalid('An organisation in the structured content has no name.');
114
+ }
115
+ return name;
116
+ };
117
+
118
+ const readLegalForm = (organisationsArg: TXmlElement[]): IRegisterLegalForm | undefined => {
119
+ for (const organisation of organisationsArg) {
120
+ const code = codeAt(organisation, 'angabenZurRechtsform', 'rechtsform');
121
+ if (code) {
122
+ const label = legalFormLabels[code];
123
+ return label ? { code, ...label } : { code };
124
+ }
125
+ }
126
+ return undefined;
127
+ };
128
+
129
+ const withCountry = <T extends { country?: string; countryKey?: string }>(targetArg: T, keyArg: string | undefined): T => {
130
+ if (keyArg) {
131
+ targetArg.countryKey = keyArg;
132
+ const iso = stateKeyToIso[keyArg];
133
+ if (iso) {
134
+ targetArg.country = iso;
135
+ }
136
+ }
137
+ return targetArg;
138
+ };
139
+
140
+ const readSeat = (organisationsArg: TXmlElement[]): IRegisterSeat | undefined => {
141
+ for (const organisation of organisationsArg) {
142
+ const city = textAt(organisation, 'sitz', 'ort');
143
+ if (city) {
144
+ return withCountry<IRegisterSeat>({ city }, codeAt(organisation, 'sitz', 'staat'));
145
+ }
146
+ }
147
+ return undefined;
148
+ };
149
+
150
+ const readAddress = (holdersArg: TXmlElement[]): IRegisterAddress | undefined => {
151
+ for (const addressType of businessAddressTypes) {
152
+ for (const holder of holdersArg) {
153
+ const address = childrenNamed(holder, 'anschrift').find(
154
+ (addressArg) => codeAt(addressArg, 'anschriftstyp') === addressType
155
+ );
156
+ if (!address) {
157
+ continue;
158
+ }
159
+ const result: IRegisterAddress = {};
160
+ const street = textAt(address, 'strasse');
161
+ const houseNumber = textAt(address, 'hausnummer');
162
+ const postalCode = textAt(address, 'postleitzahl');
163
+ const city = textAt(address, 'ort');
164
+ if (street) result.street = street;
165
+ if (houseNumber) result.houseNumber = houseNumber;
166
+ if (postalCode) result.postalCode = postalCode;
167
+ if (city) result.city = city;
168
+ return withCountry(result, codeAt(address, 'staat'));
169
+ }
170
+ }
171
+ return undefined;
172
+ };
173
+
174
+ const readRegister = (rootArg: TXmlElement, organisationsArg: TXmlElement[]): IRegisterContentRegister => {
175
+ const register: IRegisterContentRegister = {};
176
+ const instance = at(rootArg, 'grunddaten', 'verfahrensdaten', 'instanzdaten');
177
+ for (const organisation of organisationsArg) {
178
+ const entry = at(organisation, 'registereintragung');
179
+ const court = at(entry, 'auswahl_registerbehoerde', 'inlaendischesRegistergericht');
180
+ const courtCode = codeAt(court, 'gericht');
181
+ const type = codeAt(court, 'registerart');
182
+ const numberText = textAt(entry, 'registernummer');
183
+ if (courtCode && !register.courtCode) register.courtCode = courtCode;
184
+ if (type && !register.type) register.type = type;
185
+ if (numberText && !register.number) {
186
+ const parsed = parseRegisterReferenceText(numberText);
187
+ if (parsed) {
188
+ register.number = parsed.number;
189
+ if (!register.type) register.type = parsed.type;
190
+ } else {
191
+ const parts = parseRegisterNumber(numberText);
192
+ if (parts) register.number = formatRegisterNumber(parts);
193
+ }
194
+ }
195
+ }
196
+ const instanceCourt = codeAt(instance, 'auswahl_instanzbehoerde', 'gericht');
197
+ if (instanceCourt && !register.courtCode) register.courtCode = instanceCourt;
198
+ const fileNumber = at(instance, 'aktenzeichen', 'auswahl_aktenzeichen');
199
+ const structured = at(fileNumber, 'aktenzeichen.strukturiert');
200
+ if (structured) {
201
+ const type = codeAt(structured, 'register');
202
+ const running = textAt(structured, 'laufendeNummer');
203
+ if (type && !register.type) register.type = type;
204
+ if (running && !register.number) register.number = running;
205
+ }
206
+ const freeText = textAt(fileNumber, 'aktenzeichen.freitext');
207
+ if (freeText) {
208
+ const parsed = parseRegisterReferenceText(freeText);
209
+ if (parsed) {
210
+ if (!register.type) register.type = parsed.type;
211
+ if (!register.number) register.number = parsed.number;
212
+ }
213
+ }
214
+ return register;
215
+ };
216
+
217
+ const statusStates: Readonly<Record<string, TRegisterEntityState>> = {
218
+ '001': 'active',
219
+ '002': 'deleted',
220
+ '003': 'in_liquidation',
221
+ '004': 'insolvent',
222
+ '005': 'merged',
223
+ };
224
+
225
+ const readStatus = (baseDataArg: TXmlElement | undefined): IRegisterEntityStatus | undefined => {
226
+ const code = codeAt(baseDataArg, 'statusRechtstraeger');
227
+ if (!code) {
228
+ return undefined;
229
+ }
230
+ const status: IRegisterEntityStatus = { state: statusStates[code] ?? 'unknown', code };
231
+ const label = entityStatusLabels[code];
232
+ if (label) {
233
+ status.label = label;
234
+ }
235
+ return status;
236
+ };
237
+
238
+ /**
239
+ * Parses the structured register content (SI) the register portal delivers as XJustiz XML
240
+ * (`nachricht.reg.0400003`). Makes no request. Of people it reads the name and the function only.
241
+ */
242
+ export const parseStructuredContent = (xmlArg: string): IRegisterStructuredContent => {
243
+ if (/<!DOCTYPE/i.test(xmlArg)) {
244
+ throw invalid('The structured content declares a document type, which XJustiz never does.');
245
+ }
246
+ let document: plugins.smartxmlXpath.dom.Document;
247
+ try {
248
+ document = plugins.smartxmlXpath.parseXmlDocument(xmlArg);
249
+ } catch (error) {
250
+ throw invalid('The structured content is not well-formed XML.', error);
251
+ }
252
+ const root = document.documentElement;
253
+ if (!root || root.localName !== registerMessage || root.namespaceURI !== xjustizNamespace) {
254
+ throw invalid('The document is not an XJustiz register message.');
255
+ }
256
+
257
+ const roles = new Map<string, IRole>();
258
+ const proceedings = at(root, 'grunddaten', 'verfahrensdaten');
259
+ const participations = proceedings ? childrenNamed(proceedings, 'beteiligung') : [];
260
+ for (const participation of participations) {
261
+ const participant = readParticipant(participation);
262
+ if (!participant) {
263
+ continue;
264
+ }
265
+ for (const role of childrenNamed(participation, 'rolle')) {
266
+ const roleNumber = textAt(role, 'rollennummer');
267
+ if (!roleNumber) {
268
+ continue;
269
+ }
270
+ const code = codeAt(role, 'rollenbezeichnung');
271
+ roles.set(roleNumber, code ? { code, participant } : { participant });
272
+ }
273
+ }
274
+
275
+ const registerData = childrenNamed(root, 'fachdatenRegister');
276
+ if (registerData.length !== 1) {
277
+ throw invalid('The structured content does not describe exactly one entity.');
278
+ }
279
+ const entityRole = roles.get(textAt(registerData[0], 'betroffenerRechtstraeger', 'ref.rollennummer') ?? '');
280
+ if (!entityRole) {
281
+ throw invalid('The structured content does not say which participant the entity is.');
282
+ }
283
+ const baseData = at(registerData[0], 'basisdatenRegister');
284
+ const organisations = [at(baseData, 'rechtstraeger'), entityRole.participant.kind === 'organisation' ? entityRole.participant.element : undefined].filter(
285
+ (elementArg): elementArg is TXmlElement => elementArg !== undefined
286
+ );
287
+
288
+ const name =
289
+ organisations.map((organisationArg) => organisationName(organisationArg)).find((nameArg) => nameArg !== undefined) ??
290
+ (entityRole.participant.kind === 'person' ? personName(entityRole.participant.element) : undefined);
291
+ if (!name) {
292
+ throw invalid('The structured content names no entity.');
293
+ }
294
+
295
+ const representatives: IRegisterRepresentative[] = [];
296
+ const representation = at(baseData, 'vertretung');
297
+ const entitled = representation ? childrenNamed(representation, 'vertretungsberechtigte') : [];
298
+ for (const entry of entitled) {
299
+ const role = roles.get(textAt(entry, 'ref.rollennummer') ?? '');
300
+ if (!role || !role.code) {
301
+ throw invalid('The structured content refers to a representative it does not list.');
302
+ }
303
+ const representative: IRegisterRepresentative = {
304
+ name: participantName(role.participant),
305
+ kind: role.participant.kind,
306
+ functionCode: role.code,
307
+ };
308
+ const label = roleLabels[role.code];
309
+ if (label) {
310
+ representative.function = label;
311
+ }
312
+ representatives.push(representative);
313
+ }
314
+
315
+ const holders = entityRole.participant.kind === 'person' ? [...organisations, entityRole.participant.element] : organisations;
316
+ const content: IRegisterStructuredContent = {
317
+ entityKind: organisations.length > 0 ? 'organisation' : 'person',
318
+ name,
319
+ register: readRegister(root, organisations),
320
+ representationStated: entitled.length > 0,
321
+ representatives,
322
+ binding: false,
323
+ notice: structuredContentNotice,
324
+ };
325
+ const legalForm = readLegalForm(organisations);
326
+ const seat = readSeat(organisations);
327
+ const address = readAddress(holders);
328
+ const status = readStatus(baseData);
329
+ const lastEntryOn = textAt(registerData[0], 'auszug', 'letzteEintragung');
330
+ const producedAt = textAt(root, 'nachrichtenkopf', 'erstellungszeitpunkt');
331
+ if (legalForm) content.legalForm = legalForm;
332
+ if (seat) content.seat = seat;
333
+ if (address) content.address = address;
334
+ if (status) content.status = status;
335
+ if (lastEntryOn) content.lastEntryOn = lastEntryOn;
336
+ if (producedAt) content.producedAt = producedAt;
337
+ return content;
338
+ };
@@ -0,0 +1,15 @@
1
+ // This entry point stands alone: it imports nothing from the rest of the package and needs no storage.
2
+
3
+ // @push.rocks scope
4
+ import * as smartxmlXpath from '@push.rocks/smartxml/xpath';
5
+
6
+ export {
7
+ smartxmlXpath,
8
+ }
9
+
10
+ // third party scope
11
+ import * as parse5 from 'parse5';
12
+
13
+ export {
14
+ parse5,
15
+ }