lib-fints 1.4.5 → 1.4.6

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.
@@ -1,194 +1,151 @@
1
- export var TokenType535;
2
- (function (TokenType535) {
3
- TokenType535["DepotValueBlock"] = "DepotValueBlock";
4
- TokenType535["DepotValueCurrency"] = "DepotValueCurrency";
5
- TokenType535["FinBlock"] = "FinBlock";
6
- TokenType535["SecurityIdentification"] = "SecurityIdentification";
7
- TokenType535["AcquisitionPrice"] = "AcquisitionPrice";
8
- TokenType535["PriceBlock"] = "PriceBlock";
9
- TokenType535["AmountBlock"] = "AmountBlock";
10
- TokenType535["DateTimeBlock"] = "DateTimeBlock";
11
- TokenType535["DateString"] = "DateString";
12
- TokenType535["TimeString"] = "TimeString";
13
- })(TokenType535 || (TokenType535 = {}));
14
- const tokens535 = {
15
- [TokenType535.DepotValueBlock]: /:16R:ADDINFO(.*?):16S:ADDINFO/ms,
16
- [TokenType535.DepotValueCurrency]: /EUR(.*)/ms,
17
- [TokenType535.FinBlock]: /:16R:FIN(.*?):16S:FIN/gms,
18
- [TokenType535.SecurityIdentification]: /:35B:(.*?):/ms,
19
- [TokenType535.AcquisitionPrice]: /:70E::HOLD\/\/\d*STK2(\d*),(\d*)\+([A-Z]{3})/ms,
20
- [TokenType535.PriceBlock]: /:90([AB])::(.*?):/ms,
21
- [TokenType535.AmountBlock]: /:93B::(.*?):/ms,
22
- [TokenType535.DateTimeBlock]: /:98([AC])::(.*?):/ms,
23
- [TokenType535.DateString]: /(\d{4})(\d{2})(\d{2})/,
24
- [TokenType535.TimeString]: /^.{14}(\d{2})(\d{2})(\d{2})/ms,
25
- };
26
1
  export class Mt535Parser {
27
- cleanedRawData;
2
+ rawData;
28
3
  constructor(rawData) {
29
- // The divider can be either \r\n or @@
30
- const crlfCount = (rawData.match(/\r\n-/g) || []).length;
31
- const atAtCount = (rawData.match(/@@-/g) || []).length;
32
- const divider = crlfCount > atAtCount ? '\r\n' : '@@';
33
- // Remove dividers that are not followed by a colon (tag indicator)
34
- const regex = new RegExp(`${divider}([^:])`, 'gms');
35
- this.cleanedRawData = rawData.replace(regex, '$1');
4
+ this.rawData = rawData;
36
5
  }
37
6
  parse() {
38
7
  const result = {
39
8
  holdings: [],
40
9
  };
41
- // Parse total depot value
42
- result.totalValue = this.parseDepotValue();
43
- // Parse individual holdings
44
- result.holdings = this.parseHoldings();
45
- return result;
46
- }
47
- parseDepotValue() {
48
- const addInfoMatch = this.cleanedRawData.match(tokens535[TokenType535.DepotValueBlock]);
49
- if (addInfoMatch) {
50
- const eurMatch = addInfoMatch[1].match(tokens535[TokenType535.DepotValueCurrency]);
51
- if (eurMatch) {
52
- return parseFloat(eurMatch[1].replace(',', '.'));
53
- }
54
- }
55
- return undefined;
56
- }
57
- parseHoldings() {
58
- const holdings = [];
59
- const finBlocks = this.cleanedRawData.match(tokens535[TokenType535.FinBlock]);
60
- if (!finBlocks) {
61
- return holdings;
62
- }
63
- for (const block of finBlocks) {
64
- const holding = this.parseHolding(block);
65
- if (holding) {
66
- holdings.push(holding);
67
- }
68
- }
69
- return holdings;
70
- }
71
- parseHolding(block) {
72
- const holding = {};
73
- // Parse ISIN, WKN & Name from :35B:
74
- // :35B:ISIN DE0005190003/DE/519000BAY.MOTOREN WERKE AG ST
75
- this.parseSecurityIdentification(block, holding);
76
- // Parse acquisition price from :70E::HOLD//
77
- this.parseAcquisitionPrice(block, holding);
78
- // Parse current price from :90B: or :90A:
79
- this.parsePrice(block, holding);
80
- // Parse amount from :93B:
81
- this.parseAmount(block, holding);
82
- // Parse date/time from :98A: or :98C:
83
- this.parseDateTime(block, holding);
84
- // Calculate value if we have price and amount
85
- if (holding.amount !== undefined && holding.price !== undefined) {
86
- // For all currencies, value is price multiplied by amount
87
- holding.value = holding.price * holding.amount;
88
- }
89
- return holding;
90
- }
91
- parseSecurityIdentification(block, holding) {
92
- const match = block.match(tokens535[TokenType535.SecurityIdentification]);
93
- if (match) {
94
- const content = match[1];
95
- // ISIN: characters 5-16 (12 chars)
96
- const isinMatch = content.match(/^.{5}(.{12})/ms);
97
- if (isinMatch) {
98
- holding.isin = isinMatch[1];
99
- }
100
- // WKN: characters 21-26 (6 chars)
101
- const wknMatch = content.match(/^.{21}(.{6})/ms);
102
- if (wknMatch) {
103
- holding.wkn = wknMatch[1];
104
- }
105
- // Name: everything from character 27 onwards
106
- const nameMatch = content.match(/^.{27}(.*)/ms);
107
- if (nameMatch) {
108
- holding.name = nameMatch[1].trim();
109
- }
110
- }
111
- }
112
- parseAcquisitionPrice(block, holding) {
113
- const match = block.match(tokens535[TokenType535.AcquisitionPrice]);
114
- if (match) {
115
- holding.acquisitionPrice = parseFloat(`${match[1]}.${match[2]}`);
116
- if (!holding.currency) {
117
- holding.currency = match[3];
118
- }
10
+ const tokens = this.rawData.split(/^:(\d{2}[A-Z]?):/m);
11
+ const fields = [];
12
+ for (let i = 1; i < tokens.length; i += 2) {
13
+ const tag = tokens[i];
14
+ const value = tokens[i + 1]?.trim() || '';
15
+ fields.push({ tag, value });
119
16
  }
120
- }
121
- parsePrice(block, holding) {
122
- const match = block.match(tokens535[TokenType535.PriceBlock]);
123
- if (match) {
124
- const type = match[1];
125
- const content = match[2];
126
- if (type === 'B') {
127
- // Currency from characters 11-13 (3 chars)
128
- const currencyMatch = content.match(/^.{11}(.{3})/ms);
129
- if (currencyMatch) {
130
- holding.currency = currencyMatch[1];
131
- }
132
- // Price from character 14 onwards
133
- const priceMatch = content.match(/^.{14}(.*)/ms);
134
- if (priceMatch) {
135
- holding.price = parseFloat(priceMatch[1].replace(',', '.'));
136
- }
137
- }
138
- else if (type === 'A') {
139
- holding.currency = '%';
140
- // Price from character 11 onwards
141
- const priceMatch = content.match(/^.{11}(.*)/ms);
142
- if (priceMatch) {
143
- holding.price = parseFloat(priceMatch[1].replace(',', '.')) / 100;
144
- }
145
- }
146
- }
147
- }
148
- parseAmount(block, holding) {
149
- const match = block.match(tokens535[TokenType535.AmountBlock]);
150
- if (match) {
151
- // Amount from character 11 onwards
152
- const amountMatch = match[1].match(/^.{11}(.*)/ms);
153
- if (amountMatch) {
154
- holding.amount = parseFloat(amountMatch[1].replace(',', '.'));
155
- }
156
- }
157
- }
158
- parseDateTime(block, holding) {
159
- const match = block.match(tokens535[TokenType535.DateTimeBlock]);
160
- if (match) {
161
- const type = match[1];
162
- const content = match[2];
163
- // Date from characters 6-13 (8 chars: YYYYMMDD)
164
- const dateMatch = content.match(tokens535[TokenType535.DateString]);
165
- if (dateMatch) {
166
- const parsedDate = this.parseDate(dateMatch[0]);
167
- if (type === 'C') {
168
- // :98C: has a time component HHMMSS starting at character 14
169
- const timeMatch = content.match(tokens535[TokenType535.TimeString]);
170
- if (timeMatch) {
171
- parsedDate.setHours(parseInt(timeMatch[1], 10), parseInt(timeMatch[2], 10), parseInt(timeMatch[3], 10));
17
+ const sequences = [];
18
+ let currentSequence = '';
19
+ let currentHolding = null;
20
+ for (let i = 0; i < fields.length; i++) {
21
+ const field = fields[i];
22
+ switch (field.tag) {
23
+ case '16R':
24
+ currentSequence = field.value;
25
+ sequences.push(currentSequence);
26
+ if (currentSequence === 'FIN') {
27
+ currentHolding = {};
28
+ result.holdings.push(currentHolding);
172
29
  }
30
+ break;
31
+ case '16S':
32
+ sequences.pop();
33
+ currentSequence = sequences[sequences.length - 1] || '';
34
+ break;
35
+ case '19A': {
36
+ const match = field.value.match(/:HOL[DPS]\/\/([A-Z]{3})(-?\d+(,\d*)?)/);
37
+ if (match) {
38
+ if (currentSequence === 'ADDINFO') {
39
+ result.currency = match[1];
40
+ result.totalValue = parseFloat(match[2].replace(',', '.'));
41
+ }
42
+ else if (currentSequence === 'FIN' && currentHolding) {
43
+ if (currentHolding.currency !== '%') {
44
+ currentHolding.currency = match[1];
45
+ }
46
+ currentHolding.value = parseFloat(match[2].replace(',', '.'));
47
+ }
48
+ }
49
+ break;
173
50
  }
174
- else {
175
- // :98A: time defaults to 00:00:00
176
- parsedDate.setHours(0, 0, 0, 0);
177
- }
178
- holding.date = parsedDate;
51
+ case '35B':
52
+ if (currentHolding) {
53
+ let lastIndex = 0;
54
+ let match = field.value.match(/^ISIN (\w{12})/);
55
+ if (match) {
56
+ currentHolding.isin = match[1];
57
+ lastIndex = match[0].length;
58
+ }
59
+ match = field.value.match(/\/[A-Z]{2}\/(\w*?)$/m);
60
+ if (match) {
61
+ currentHolding.wkn = match[1].trim();
62
+ lastIndex = (match.index ?? 0) + match[0].length;
63
+ }
64
+ currentHolding.name = field.value
65
+ .substring(lastIndex)
66
+ .trim()
67
+ .replaceAll('\r\n', '/')
68
+ .trim();
69
+ }
70
+ break;
71
+ case '70E':
72
+ if (currentHolding) {
73
+ const match = field.value.match(/:HOLD\/\/\d(.*?)\+(.*?)\+(.*?)\+(.*?)\+(\d{4})(\d{2})(\d{2}).*?\d([\d,]+)\+([A-Z]{3})/s);
74
+ if (match) {
75
+ currentHolding.acquisitionDate = new Date(`${match[5]}-${match[6]}-${match[7]}T12:00`);
76
+ currentHolding.acquisitionPrice = parseFloat(match[8].replace(',', '.'));
77
+ }
78
+ }
79
+ break;
80
+ case '93B':
81
+ if (currentHolding) {
82
+ const match = field.value.match(/:AGGR\/\/UNIT\/([\d,]+)/);
83
+ if (match) {
84
+ currentHolding.amount = parseFloat(match[1].replace(',', '.'));
85
+ }
86
+ }
87
+ break;
88
+ case '90A':
89
+ if (currentHolding) {
90
+ const match = field.value.match(/:.*?\/\/.*?\/([\d,]+)/);
91
+ if (match) {
92
+ currentHolding.price = parseFloat(match[1].replace(',', '.')) / 100;
93
+ currentHolding.currency = '%';
94
+ }
95
+ }
96
+ break;
97
+ case '90B':
98
+ if (currentHolding) {
99
+ const match = field.value.match(/:.*?\/\/.*?\/([A-Z]{3})([\d,]+)/);
100
+ if (match) {
101
+ currentHolding.price = parseFloat(match[2].replace(',', '.'));
102
+ currentHolding.currency = match[1];
103
+ }
104
+ }
105
+ break;
106
+ case '98A':
107
+ if (currentHolding) {
108
+ const matchDate = field.value.match(/(\d{4})(\d{2})(\d{2})/);
109
+ if (matchDate) {
110
+ currentHolding.date = new Date(`${matchDate[1]}-${matchDate[2]}-${matchDate[3]}T12:00`);
111
+ }
112
+ }
113
+ break;
114
+ case '98C':
115
+ if (currentHolding) {
116
+ const matchDate = field.value.match(/(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/);
117
+ if (matchDate) {
118
+ currentHolding.date = new Date(`${matchDate[1]}-${matchDate[2]}-${matchDate[3]}T${matchDate[4]}:${matchDate[5]}:${matchDate[6]}`);
119
+ }
120
+ }
121
+ break;
122
+ case '70C':
123
+ if (currentHolding) {
124
+ const cleanBlock = field.value.replace(/^:SUBB\/\//, '');
125
+ const lines = cleanBlock.split(/\r?\n/);
126
+ let name = '';
127
+ for (const line of lines) {
128
+ const trimmed = line.trim();
129
+ if (trimmed.startsWith('1')) {
130
+ name = trimmed.substring(1).trim();
131
+ }
132
+ if (trimmed.startsWith('2')) {
133
+ name += ` /${trimmed.substring(1).trim()}`;
134
+ }
135
+ const matchLine3 = trimmed.match(/^3\s*([A-Z0-9]{3,4})\s+(?<price>[\d.]+)(?<currency>[A-Z]{3})\s+(?<timestamp>[\d\-T:.]+)/);
136
+ if (matchLine3) {
137
+ currentHolding.price = parseFloat(matchLine3.groups?.price || '0');
138
+ currentHolding.currency = matchLine3.groups?.currency || '';
139
+ currentHolding.date = new Date(matchLine3.groups?.timestamp || '');
140
+ }
141
+ }
142
+ if (!currentHolding.name && name) {
143
+ currentHolding.name = name;
144
+ }
145
+ }
146
+ break;
179
147
  }
180
148
  }
181
- }
182
- parseDate(dateString) {
183
- const match = dateString.match(tokens535[TokenType535.DateString]);
184
- if (!match) {
185
- throw new Error(`Invalid date format: ${dateString}`);
186
- }
187
- try {
188
- return new Date(parseInt(match[1], 10), parseInt(match[2], 10) - 1, parseInt(match[3], 10));
189
- }
190
- catch (error) {
191
- throw new Error(`Invalid date: ${dateString}`, { cause: error });
192
- }
149
+ return result;
193
150
  }
194
151
  }
@@ -1,155 +1,245 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
  import { Mt535Parser } from '../mt535parser.js';
3
3
  describe('Mt535Parser', () => {
4
- it('parses a MT535 input string with depot value and multiple holdings', () => {
5
- const input = ':16R:ADDINFO\r\n' +
6
- 'EUR125000,50\r\n' +
7
- ':16S:ADDINFO\r\n' +
8
- // Holding 1 (BMW)
9
- ':16R:FIN\r\n' +
10
- ':35B:ISIN DE0005190003/DE/519000BAY.MOTOREN WERKE AG ST:\r\n' +
11
- ':70E::HOLD//100STK275,30+EUR\r\n' +
12
- ':90B::PREFIX_11_CEUR100,50:\r\n' +
13
- ':93B::QTY/AVALBLX100,:\r\n' +
14
- ':98A::SETT//DTE20231101:\r\n' +
15
- ':16S:FIN\r\n' +
16
- // Holding 2 (Apple)
17
- ':16R:FIN\r\n' +
18
- ':35B:ISIN US0378331005/US/037833Apple Inc. Common Stock:\r\n' +
19
- ':70E::HOLD//50STK2150,75+USD\r\n' +
20
- ':90A::PRIC/RATE_X85,50:\r\n' +
21
- ':93B::QTY/AVALBLX50,:\r\n' +
22
- ':98C::TRADTE20231101143000:\r\n' +
23
- ':16S:FIN\r\n';
24
- const parser = new Mt535Parser(input);
4
+ it('handles empty input', () => {
5
+ const parser = new Mt535Parser('');
25
6
  const statement = parser.parse();
26
- // Test depot value
27
- expect(statement.totalValue).toBe(125000.5);
28
- expect(statement.holdings).toHaveLength(2);
29
- // Test first holding (BMW)
30
- const bmwHolding = statement.holdings[0];
31
- expect(bmwHolding.isin).toBe('DE0005190003');
32
- expect(bmwHolding.wkn).toBe('519000');
33
- expect(bmwHolding.name).toBe('BAY.MOTOREN WERKE AG ST');
34
- expect(bmwHolding.acquisitionPrice).toBe(75.3);
35
- expect(bmwHolding.price).toBe(100.5);
36
- expect(bmwHolding.currency).toBe('EUR');
37
- expect(bmwHolding.amount).toBe(100);
38
- expect(bmwHolding.value).toBe(10050);
39
- expect(bmwHolding.date).toEqual(new Date(2023, 10, 1, 0, 0, 0));
40
- // Test second holding (Apple)
41
- const appleHolding = statement.holdings[1];
42
- expect(appleHolding.isin).toBe('US0378331005');
43
- expect(appleHolding.wkn).toBe('037833');
44
- expect(appleHolding.name).toBe('Apple Inc. Common Stock');
45
- expect(appleHolding.acquisitionPrice).toBe(150.75);
46
- expect(appleHolding.price).toBe(0.855);
47
- expect(appleHolding.currency).toBe('%');
48
- expect(appleHolding.amount).toBe(50);
49
- expect(appleHolding.value).toBe(42.75);
50
- expect(appleHolding.date).toEqual(new Date(2023, 10, 1, 14, 30, 0));
7
+ expect(statement.totalValue).toBeUndefined();
8
+ expect(statement.holdings).toHaveLength(0);
51
9
  });
52
- it('parses MT535 with @@ dividers and different data points', () => {
53
- const input = ':16R:ADDINFO@@' +
54
- 'EUR50000,25@@' +
55
- ':16S:ADDINFO@@' +
56
- ':16R:FIN@@' +
57
- ':35B:ISIN DE0007164600/DE/716460SAP SE:@@' +
58
- ':70E::HOLD//25STK2120,80+EUR@@' +
59
- ':90B::UNKNOWNVALXEUR115,75:@@' +
60
- ':93B::SOMESTATUS_25,:@@' +
61
- ':98A::REGD//DTE20231215:@@' +
62
- ':16S:FIN@@';
10
+ it('parses holding with :90A:: percentage price (bonds)', () => {
11
+ const input = ':16R:FIN\r\n' +
12
+ ':35B:ISIN DE0001102580\r\n' +
13
+ '/DE/110258\r\n' +
14
+ 'BUNDESREP.DEUTSCHLAND ANL.V.2021\r\n' +
15
+ ':90A::MRKT//PRCT/98,50\r\n' +
16
+ ':93B::AGGR//UNIT/10000,\r\n' +
17
+ ':19A::HOLD//EUR9850,00\r\n' +
18
+ ':16S:FIN\r\n';
63
19
  const parser = new Mt535Parser(input);
64
20
  const statement = parser.parse();
65
- expect(statement.totalValue).toBe(50000.25);
66
21
  expect(statement.holdings).toHaveLength(1);
67
22
  const holding = statement.holdings[0];
68
- expect(holding.isin).toBe('DE0007164600');
69
- expect(holding.wkn).toBe('716460');
70
- expect(holding.name).toBe('SAP SE');
71
- expect(holding.acquisitionPrice).toBe(120.8);
72
- expect(holding.price).toBe(115.75);
73
- expect(holding.currency).toBe('EUR');
74
- expect(holding.amount).toBe(25);
75
- expect(holding.value).toBe(2893.75);
76
- expect(holding.date).toEqual(new Date(2023, 11, 15));
23
+ expect(holding.isin).toBe('DE0001102580');
24
+ expect(holding.wkn).toBe('110258');
25
+ expect(holding.currency).toBe('%');
26
+ expect(holding.price).toBe(0.985);
27
+ expect(holding.amount).toBe(10000);
28
+ expect(holding.value).toBe(9850);
77
29
  });
78
- it('handles empty input', () => {
79
- const parser = new Mt535Parser('');
30
+ it('parses holding with :90B:: absolute price', () => {
31
+ const input = ':16R:FIN\r\n' +
32
+ ':35B:ISIN DE0005190003\r\n' +
33
+ '/DE/519000\r\n' +
34
+ 'BAY.MOTOREN WERKE AG ST\r\n' +
35
+ ':90B::MRKT//ACTU/EUR89,50\r\n' +
36
+ ':93B::AGGR//UNIT/50,\r\n' +
37
+ ':19A::HOLD//EUR4475,00\r\n' +
38
+ ':16S:FIN\r\n';
39
+ const parser = new Mt535Parser(input);
80
40
  const statement = parser.parse();
81
- expect(statement.totalValue).toBeUndefined();
82
- expect(statement.holdings).toHaveLength(0);
41
+ const holding = statement.holdings[0];
42
+ expect(holding.isin).toBe('DE0005190003');
43
+ expect(holding.currency).toBe('EUR');
44
+ expect(holding.price).toBe(89.5);
45
+ expect(holding.amount).toBe(50);
46
+ expect(holding.value).toBe(4475);
83
47
  });
84
- it('handles input without depot value', () => {
48
+ it('parses 98A date-only field', () => {
85
49
  const input = ':16R:FIN\r\n' +
86
- ':35B:ISIN DE0005190003/DE/519000SOME OTHER AG ST:\r\n' +
87
- ':90B::PREFIX_11_CEUR100,50:\r\n' +
88
- ':93B::QTY/AVALBLX100,:\r\n' +
89
- ':98A::SETT//DTE20231101:\r\n' +
50
+ ':35B:ISIN DE0007164600\r\n' +
51
+ '/DE/716460\r\n' +
52
+ 'SAP SE\r\n' +
53
+ ':98A::SETT//20231215\r\n' +
90
54
  ':16S:FIN\r\n';
91
55
  const parser = new Mt535Parser(input);
92
56
  const statement = parser.parse();
93
- expect(statement.totalValue).toBeUndefined();
94
- expect(statement.holdings).toHaveLength(1);
95
- expect(statement.holdings[0].isin).toBe('DE0005190003');
96
- expect(statement.holdings[0].name).toBe('SOME OTHER AG ST');
97
- expect(statement.holdings[0].price).toBe(100.5);
98
- expect(statement.holdings[0].amount).toBe(100);
57
+ const holding = statement.holdings[0];
58
+ expect(holding.date).toEqual(new Date('2023-12-15T12:00'));
99
59
  });
100
- it('handles input without holdings', () => {
101
- const input = ':16R:ADDINFO\r\n' + 'EUR75000,00\r\n' + ':16S:ADDINFO\r\n';
60
+ it('parses 98C date-time field', () => {
61
+ const input = ':16R:FIN\r\n' +
62
+ ':35B:ISIN DE0007164600\r\n' +
63
+ '/DE/716460\r\n' +
64
+ 'SAP SE\r\n' +
65
+ ':98C::PRIC//20240115093045\r\n' +
66
+ ':16S:FIN\r\n';
102
67
  const parser = new Mt535Parser(input);
103
68
  const statement = parser.parse();
104
- expect(statement.totalValue).toBe(75000.0);
105
- expect(statement.holdings).toHaveLength(0);
69
+ const holding = statement.holdings[0];
70
+ expect(holding.date).toEqual(new Date(2024, 0, 15, 9, 30, 45));
106
71
  });
107
- it('handles holding with minimal data (only security identification)', () => {
108
- const input = ':16R:FIN\r\n' + ':35B:ISIN FR0000120271/FR/120271AIR LIQUIDE SA: \r\n' + ':16S:FIN\r\n';
72
+ it('parses holding with minimal data (only ISIN)', () => {
73
+ const input = ':16R:FIN\r\n' + ':35B:ISIN FR0000120271\r\n' + 'AIR LIQUIDE SA\r\n' + ':16S:FIN\r\n';
109
74
  const parser = new Mt535Parser(input);
110
75
  const statement = parser.parse();
111
76
  expect(statement.holdings).toHaveLength(1);
112
77
  const holding = statement.holdings[0];
113
78
  expect(holding.isin).toBe('FR0000120271');
114
- expect(holding.wkn).toBe('120271');
115
79
  expect(holding.name).toBe('AIR LIQUIDE SA');
116
80
  expect(holding.price).toBeUndefined();
117
81
  expect(holding.amount).toBeUndefined();
118
- expect(holding.value).toBeUndefined();
119
- expect(holding.date).toBeUndefined();
120
82
  });
121
- it('calculates value correctly for percentage currency when price is from :90A', () => {
122
- const input = ':16R:FIN\r\n' +
123
- ':35B:ISIN US0378331005/US/037833Apple Inc. Common Stock:\r\n' +
124
- ':90A::PRIC/RATE_X85,50:\r\n' +
125
- ':93B::QTY/AVALBLX100,:\r\n' +
126
- ':16S:FIN\r\n';
83
+ it('parses DKB statement correctly', () => {
84
+ const input = ':16R:GENL\r\n' +
85
+ ':28E:1/ONLY\r\n' +
86
+ ':20C::SEME//NONREF\r\n' +
87
+ ':23G:NEWM\r\n' +
88
+ ':98C::PREP//20260110153747\r\n' +
89
+ ':98A::STAT//20260110\r\n' +
90
+ ':22F::STTY//CUST\r\n' +
91
+ ':97A::SAFE//12030000/123456789\r\n' +
92
+ ':17B::ACTI//Y\r\n' +
93
+ ':16S:GENL\r\n' +
94
+ ':16R:FIN\r\n' +
95
+ ':35B:ISIN LU0950674332\r\n' +
96
+ '/DE/A1W3CQ\r\n' +
97
+ 'UBS MSCI WORLD SOC.RES. NAMENS-ANTE\r\n' +
98
+ 'ILE A ACC. USD O.N.\r\n' +
99
+ ':90B::MRKT//ACTU/EUR33,22\r\n' +
100
+ ':98C::PRIC//20260109210244\r\n' +
101
+ ':93B::AGGR//UNIT/652,\r\n' +
102
+ ':16R:SUBBAL\r\n' +
103
+ ':93C::TAVI//UNIT/AVAI/652,\r\n' +
104
+ ':16S:SUBBAL\r\n' +
105
+ ':19A::HOLD//EUR18669,64\r\n' +
106
+ ':70E::HOLD//1STK++++20220902\r\n' +
107
+ '221,2012438+EUR\r\n' +
108
+ ':16S:FIN\r\n' +
109
+ ':16R:ADDINFO\r\n' +
110
+ ':19A::HOLP//EUR18669,64\r\n' +
111
+ ':16S:ADDINFO\r\n';
127
112
  const parser = new Mt535Parser(input);
128
113
  const statement = parser.parse();
114
+ expect(statement.totalValue).toBe(18669.64);
115
+ expect(statement.currency).toBe('EUR');
116
+ expect(statement.holdings).toHaveLength(1);
129
117
  const holding = statement.holdings[0];
130
- expect(holding.currency).toBe('%');
131
- expect(holding.price).toBe(0.855);
132
- expect(holding.amount).toBe(100);
133
- expect(holding.value).toBe(85.5);
118
+ expect(holding.isin).toBe('LU0950674332');
119
+ expect(holding.wkn).toBe('A1W3CQ');
120
+ expect(holding.name).toBe('UBS MSCI WORLD SOC.RES. NAMENS-ANTE/ILE A ACC. USD O.N.');
121
+ expect(holding.acquisitionDate).toEqual(new Date('2022-09-02T12:00'));
122
+ expect(holding.acquisitionPrice).toBe(21.2012438);
123
+ expect(holding.amount).toBe(652);
124
+ expect(holding.price).toBe(33.22);
125
+ expect(holding.currency).toBe('EUR');
126
+ expect(holding.value).toBe(18669.64);
127
+ expect(holding.date).toEqual(new Date(2026, 0, 9, 21, 2, 44));
134
128
  });
135
- it('correctly parses date and time for :98C', () => {
136
- const input = ':16R:FIN\r\n' +
137
- ':35B:ISIN DE000BASF111/DE/BASF11BASF SE:\r\n' +
138
- ':98C::QUALIF20240115093045:\r\n' +
139
- ':16S:FIN\r\n';
129
+ it('parses Baader statement correctly', () => {
130
+ const input = ':16R:GENL\r\n' +
131
+ ':28E:1/ONLY\r\n' +
132
+ ':20C::SEME//NONREF\r\n' +
133
+ ':23G:NEWM\r\n' +
134
+ ':98A::PREP//20260108\r\n' +
135
+ ':98A::STAT//20260108\r\n' +
136
+ ':22F::STTY//CUST\r\n' +
137
+ ':97A::SAFE//70033100/12345678\r\n' +
138
+ ':17B::ACTI//Y\r\n' +
139
+ ':16S:GENL\r\n' +
140
+ ':16R:FIN\r\n' +
141
+ ':35B:ISIN IE000UQND7H4\r\n' +
142
+ 'HSBC ETF- WORLD DLA\r\n' +
143
+ 'HSBC MSCI WORLD UCITS ETF\r\n' +
144
+ ':93B::AGGR//UNIT/680\r\n' +
145
+ ':16R:SUBBAL\r\n' +
146
+ ':93C::TAVI//UNIT/AVAI/680\r\n' +
147
+ ':70C::SUBB//1 HSBC ETF- WORLD DLA\r\n' +
148
+ '2\r\n' +
149
+ '3 EDE 37.200000000EUR 2026-01-08T19:08:31.7\r\n' +
150
+ '4 25875.56EUR IE000UQND7H4, 1/SO\r\n' +
151
+ ':16S:SUBBAL\r\n' +
152
+ ':19A::HOLD//EUR25296\r\n' +
153
+ ':70E::HOLD//1STK++++20260107\r\n' +
154
+ '237,267+EUR\r\n' +
155
+ ':16S:FIN\r\n' +
156
+ ':16R:ADDINFO\r\n' +
157
+ ':19A::HOLP//EUR25296\r\n' +
158
+ ':16S:ADDINFO\r\n';
140
159
  const parser = new Mt535Parser(input);
141
160
  const statement = parser.parse();
161
+ expect(statement.totalValue).toBe(25296);
162
+ expect(statement.currency).toBe('EUR');
163
+ expect(statement.holdings).toHaveLength(1);
142
164
  const holding = statement.holdings[0];
143
- expect(holding.date).toEqual(new Date(2024, 0, 15, 9, 30, 45));
165
+ expect(holding.isin).toBe('IE000UQND7H4');
166
+ expect(holding.wkn).toBeUndefined();
167
+ expect(holding.name).toBe('HSBC ETF- WORLD DLA/HSBC MSCI WORLD UCITS ETF');
168
+ expect(holding.acquisitionDate).toEqual(new Date('2026-01-07T12:00'));
169
+ expect(holding.acquisitionPrice).toBe(37.267);
170
+ expect(holding.amount).toBe(680);
171
+ expect(holding.price).toBe(37.2);
172
+ expect(holding.currency).toBe('EUR');
173
+ expect(holding.value).toBe(25296);
174
+ expect(holding.date).toEqual(new Date(2026, 0, 8, 19, 8, 31, 700));
144
175
  });
145
- it('correctly parses date for :98A (time defaults to 00:00:00)', () => {
146
- const input = ':16R:FIN\r\n' +
147
- ':35B:ISIN DE000BAY0017/DE/BAY001BAYER AG:\r\n' +
148
- ':98A::SETT//DTE20231005:\r\n' +
149
- ':16S:FIN\r\n';
176
+ it('parses ING statement correctly', () => {
177
+ const input = ':16R:GENL\r\n' +
178
+ ':28E:1/ONLY\r\n' +
179
+ ':20C::SEME//NONREF\r\n' +
180
+ ':23G:NEWM\r\n' +
181
+ ':98C::STAT//20260125221440\r\n' +
182
+ ':22F::STTY//CUST\r\n' +
183
+ ':97A::SAFE//12345678/1234567890\r\n' +
184
+ ':17B::ACTI//Y\r\n' +
185
+ ':16S:GENL\r\n' +
186
+ ':16R:FIN\r\n' +
187
+ ':35B:ISIN IE00B5BMR087\r\n' +
188
+ '/DE/A0YEDG\r\n' +
189
+ 'ISHSVII-CORE S+P500 DLACC\r\n' +
190
+ ':90B::MRKT//ACTU/EUR627,17\r\n' +
191
+ ':94B::PRIC//LMAR/XGAT\r\n' +
192
+ ':98A::PRIC//20260123\r\n' +
193
+ ':93B::AGGR//UNIT/15,5\r\n' +
194
+ ':16R:SUBBAL\r\n' +
195
+ ':93C::TAVI//UNIT/AVAI/15,5\r\n' +
196
+ ':16S:SUBBAL\r\n' +
197
+ ':19A::HOLD//EUR9721,14\r\n' +
198
+ ':70E::HOLD//1STK\r\n' +
199
+ '2456,114666+EUR\r\n' +
200
+ ':16S:FIN\r\n' +
201
+ ':16R:FIN\r\n' +
202
+ ':35B:ISIN IE00B3WJKG14\r\n' +
203
+ '/DE/A142N1\r\n' +
204
+ 'ISHSV-S+500INF.T.SECT.DLA\r\n' +
205
+ ':90B::MRKT//ACTU/EUR34,975\r\n' +
206
+ ':94B::PRIC//LMAR/XGAT\r\n' +
207
+ ':98A::PRIC//20260123\r\n' +
208
+ ':93B::AGGR//UNIT/85,\r\n' +
209
+ ':16R:SUBBAL\r\n' +
210
+ ':93C::TAVI//UNIT/AVAI/85,\r\n' +
211
+ ':16S:SUBBAL\r\n' +
212
+ ':19A::HOLD//EUR2972,88\r\n' +
213
+ ':70E::HOLD//1STK\r\n' +
214
+ '225,208135+EUR\r\n' +
215
+ ':16S:FIN\r\n' +
216
+ ':16R:ADDINFO\r\n' +
217
+ ':19A::HOLP//EUR12694,02\r\n' +
218
+ ':16S:ADDINFO\r\n';
150
219
  const parser = new Mt535Parser(input);
151
220
  const statement = parser.parse();
152
- const holding = statement.holdings[0];
153
- expect(holding.date).toEqual(new Date(2023, 9, 5, 0, 0, 0));
221
+ expect(statement.totalValue).toBe(12694.02);
222
+ expect(statement.currency).toBe('EUR');
223
+ expect(statement.holdings).toHaveLength(2);
224
+ // First holding
225
+ const holding1 = statement.holdings[0];
226
+ expect(holding1.isin).toBe('IE00B5BMR087');
227
+ expect(holding1.wkn).toBe('A0YEDG');
228
+ expect(holding1.name).toBe('ISHSVII-CORE S+P500 DLACC');
229
+ expect(holding1.amount).toBe(15.5);
230
+ expect(holding1.price).toBe(627.17);
231
+ expect(holding1.currency).toBe('EUR');
232
+ expect(holding1.value).toBe(9721.14);
233
+ expect(holding1.date).toEqual(new Date('2026-01-23T12:00'));
234
+ // Second holding
235
+ const holding2 = statement.holdings[1];
236
+ expect(holding2.isin).toBe('IE00B3WJKG14');
237
+ expect(holding2.wkn).toBe('A142N1');
238
+ expect(holding2.name).toBe('ISHSV-S+500INF.T.SECT.DLA');
239
+ expect(holding2.amount).toBe(85);
240
+ expect(holding2.price).toBe(34.975);
241
+ expect(holding2.currency).toBe('EUR');
242
+ expect(holding2.value).toBe(2972.88);
243
+ expect(holding2.date).toEqual(new Date('2026-01-23T12:00'));
154
244
  });
155
245
  });
@@ -7,37 +7,17 @@ export interface Holding {
7
7
  isin?: string;
8
8
  wkn?: string;
9
9
  name?: string;
10
+ date?: Date;
10
11
  amount?: number;
11
12
  price?: number;
12
- currency?: string;
13
13
  value?: number;
14
+ currency?: string;
15
+ acquisitionDate?: Date;
14
16
  acquisitionPrice?: number;
15
- date?: Date;
16
- }
17
- export declare enum TokenType535 {
18
- DepotValueBlock = "DepotValueBlock",
19
- DepotValueCurrency = "DepotValueCurrency",
20
- FinBlock = "FinBlock",
21
- SecurityIdentification = "SecurityIdentification",
22
- AcquisitionPrice = "AcquisitionPrice",
23
- PriceBlock = "PriceBlock",
24
- AmountBlock = "AmountBlock",
25
- DateTimeBlock = "DateTimeBlock",
26
- DateString = "DateString",
27
- TimeString = "TimeString"
28
17
  }
29
18
  export declare class Mt535Parser {
30
- private cleanedRawData;
19
+ private rawData;
31
20
  constructor(rawData: string);
32
21
  parse(): StatementOfHoldings;
33
- private parseDepotValue;
34
- private parseHoldings;
35
- private parseHolding;
36
- private parseSecurityIdentification;
37
- private parseAcquisitionPrice;
38
- private parsePrice;
39
- private parseAmount;
40
- private parseDateTime;
41
- private parseDate;
42
22
  }
43
23
  //# sourceMappingURL=mt535parser.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"mt535parser.d.ts","sourceRoot":"","sources":["../../src/mt535parser.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,mBAAmB;IACnC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,OAAO,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,OAAO;IACvB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,IAAI,CAAC,EAAE,IAAI,CAAC;CACZ;AAED,oBAAY,YAAY;IACvB,eAAe,oBAAoB;IACnC,kBAAkB,uBAAuB;IACzC,QAAQ,aAAa;IACrB,sBAAsB,2BAA2B;IACjD,gBAAgB,qBAAqB;IACrC,UAAU,eAAe;IACzB,WAAW,gBAAgB;IAC3B,aAAa,kBAAkB;IAC/B,UAAU,eAAe;IACzB,UAAU,eAAe;CACzB;AAeD,qBAAa,WAAW;IACvB,OAAO,CAAC,cAAc,CAAS;gBAEnB,OAAO,EAAE,MAAM;IAW3B,KAAK,IAAI,mBAAmB;IAc5B,OAAO,CAAC,eAAe;IAWvB,OAAO,CAAC,aAAa;IAmBrB,OAAO,CAAC,YAAY;IA4BpB,OAAO,CAAC,2BAA2B;IAyBnC,OAAO,CAAC,qBAAqB;IAU7B,OAAO,CAAC,UAAU;IA8BlB,OAAO,CAAC,WAAW;IAWnB,OAAO,CAAC,aAAa;IA8BrB,OAAO,CAAC,SAAS;CAYjB"}
1
+ {"version":3,"file":"mt535parser.d.ts","sourceRoot":"","sources":["../../src/mt535parser.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,mBAAmB;IACnC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,OAAO,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,OAAO;IACvB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,IAAI,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC1B;AAOD,qBAAa,WAAW;IACX,OAAO,CAAC,OAAO;gBAAP,OAAO,EAAE,MAAM;IAEnC,KAAK,IAAI,mBAAmB;CA2K5B"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lib-fints",
3
- "version": "1.4.5",
3
+ "version": "1.4.6",
4
4
  "description": "Typescript/Javascript client library for Online-Banking via the FinTS 3.0 protocol with PIN/TAN",
5
5
  "engines": {
6
6
  "node": ">=18.0.0"