diff --git a/src/mt535parser.ts b/src/mt535parser.ts index fdfcb60..ee608ee 100644 --- a/src/mt535parser.ts +++ b/src/mt535parser.ts @@ -8,242 +8,192 @@ export interface Holding { isin?: string; wkn?: string; name?: string; + date?: Date; amount?: number; price?: number; - currency?: string; value?: number; + currency?: string; + acquisitionDate?: Date; acquisitionPrice?: number; - date?: Date; } -export enum TokenType535 { - DepotValueBlock = 'DepotValueBlock', - DepotValueCurrency = 'DepotValueCurrency', - FinBlock = 'FinBlock', - SecurityIdentification = 'SecurityIdentification', - AcquisitionPrice = 'AcquisitionPrice', - PriceBlock = 'PriceBlock', - AmountBlock = 'AmountBlock', - DateTimeBlock = 'DateTimeBlock', - DateString = 'DateString', - TimeString = 'TimeString', -} - -const tokens535: { [key in TokenType535]: RegExp } = { - [TokenType535.DepotValueBlock]: /:16R:ADDINFO(.*?):16S:ADDINFO/ms, - [TokenType535.DepotValueCurrency]: /EUR(.*)/ms, - [TokenType535.FinBlock]: /:16R:FIN(.*?):16S:FIN/gms, - [TokenType535.SecurityIdentification]: /:35B:(.*?):/ms, - [TokenType535.AcquisitionPrice]: /:70E::HOLD\/\/\d*STK2(\d*),(\d*)\+([A-Z]{3})/ms, - [TokenType535.PriceBlock]: /:90([AB])::(.*?):/ms, - [TokenType535.AmountBlock]: /:93B::(.*?):/ms, - [TokenType535.DateTimeBlock]: /:98([AC])::(.*?):/ms, - [TokenType535.DateString]: /(\d{4})(\d{2})(\d{2})/, - [TokenType535.TimeString]: /^.{14}(\d{2})(\d{2})(\d{2})/ms, +type Field = { + tag: string; + value: string; }; export class Mt535Parser { - private cleanedRawData: string; - - constructor(rawData: string) { - // The divider can be either \r\n or @@ - const crlfCount = (rawData.match(/\r\n-/g) || []).length; - const atAtCount = (rawData.match(/@@-/g) || []).length; - const divider = crlfCount > atAtCount ? '\r\n' : '@@'; - - // Remove dividers that are not followed by a colon (tag indicator) - const regex = new RegExp(`${divider}([^:])`, 'gms'); - this.cleanedRawData = rawData.replace(regex, '$1'); - } + constructor(private rawData: string) {} parse(): StatementOfHoldings { const result: StatementOfHoldings = { holdings: [], }; - // Parse total depot value - result.totalValue = this.parseDepotValue(); - - // Parse individual holdings - result.holdings = this.parseHoldings(); - - return result; - } - - private parseDepotValue(): number | undefined { - const addInfoMatch = this.cleanedRawData.match(tokens535[TokenType535.DepotValueBlock]); - if (addInfoMatch) { - const eurMatch = addInfoMatch[1].match(tokens535[TokenType535.DepotValueCurrency]); - if (eurMatch) { - return parseFloat(eurMatch[1].replace(',', '.')); - } - } - return undefined; - } - - private parseHoldings(): Holding[] { - const holdings: Holding[] = []; + const tokens = this.rawData.split(/^:(\d{2}[A-Z]?):/m); + const fields: Field[] = []; - const finBlocks = this.cleanedRawData.match(tokens535[TokenType535.FinBlock]); - - if (!finBlocks) { - return holdings; - } - - for (const block of finBlocks) { - const holding = this.parseHolding(block); - if (holding) { - holdings.push(holding); - } - } - - return holdings; - } - - private parseHolding(block: string): Holding { - const holding: Holding = {}; - - // Parse ISIN, WKN & Name from :35B: - // :35B:ISIN DE0005190003/DE/519000BAY.MOTOREN WERKE AG ST - this.parseSecurityIdentification(block, holding); - - // Parse acquisition price from :70E::HOLD// - this.parseAcquisitionPrice(block, holding); - - // Parse current price from :90B: or :90A: - this.parsePrice(block, holding); - - // Parse amount from :93B: - this.parseAmount(block, holding); - - // Parse date/time from :98A: or :98C: - this.parseDateTime(block, holding); - - // Calculate value if we have price and amount - if (holding.amount !== undefined && holding.price !== undefined) { - // For all currencies, value is price multiplied by amount - holding.value = holding.price * holding.amount; + for (let i = 1; i < tokens.length; i += 2) { + const tag = tokens[i]; + const value = tokens[i + 1]?.trim() || ''; + fields.push({ tag, value }); } - return holding; - } - - private parseSecurityIdentification(block: string, holding: Holding): void { - const match = block.match(tokens535[TokenType535.SecurityIdentification]); - if (match) { - const content = match[1]; + const sequences = []; + let currentSequence: string = ''; + let currentHolding: Holding | null = null; - // ISIN: characters 5-16 (12 chars) - const isinMatch = content.match(/^.{5}(.{12})/ms); - if (isinMatch) { - holding.isin = isinMatch[1]; - } + for (let i = 0; i < fields.length; i++) { + const field = fields[i]; - // WKN: characters 21-26 (6 chars) - const wknMatch = content.match(/^.{21}(.{6})/ms); - if (wknMatch) { - holding.wkn = wknMatch[1]; - } + switch (field.tag) { + case '16R': + currentSequence = field.value; + sequences.push(currentSequence); - // Name: everything from character 27 onwards - const nameMatch = content.match(/^.{27}(.*)/ms); - if (nameMatch) { - holding.name = nameMatch[1].trim(); - } - } - } - - private parseAcquisitionPrice(block: string, holding: Holding): void { - const match = block.match(tokens535[TokenType535.AcquisitionPrice]); - if (match) { - holding.acquisitionPrice = parseFloat(`${match[1]}.${match[2]}`); - if (!holding.currency) { - holding.currency = match[3]; - } - } - } - - private parsePrice(block: string, holding: Holding): void { - const match = block.match(tokens535[TokenType535.PriceBlock]); - if (match) { - const type = match[1]; - const content = match[2]; - - if (type === 'B') { - // Currency from characters 11-13 (3 chars) - const currencyMatch = content.match(/^.{11}(.{3})/ms); - if (currencyMatch) { - holding.currency = currencyMatch[1]; - } - - // Price from character 14 onwards - const priceMatch = content.match(/^.{14}(.*)/ms); - if (priceMatch) { - holding.price = parseFloat(priceMatch[1].replace(',', '.')); - } - } else if (type === 'A') { - holding.currency = '%'; - - // Price from character 11 onwards - const priceMatch = content.match(/^.{11}(.*)/ms); - if (priceMatch) { - holding.price = parseFloat(priceMatch[1].replace(',', '.')) / 100; + if (currentSequence === 'FIN') { + currentHolding = {}; + result.holdings.push(currentHolding); + } + break; + case '16S': + sequences.pop(); + currentSequence = sequences[sequences.length - 1] || ''; + break; + case '19A': { + const match = field.value.match(/:HOL[DPS]\/\/([A-Z]{3})(-?\d+(,\d*)?)/); + if (match) { + if (currentSequence === 'ADDINFO') { + result.currency = match[1]; + result.totalValue = parseFloat(match[2].replace(',', '.')); + } else if (currentSequence === 'FIN' && currentHolding) { + if (currentHolding.currency !== '%') { + currentHolding.currency = match[1]; + } + currentHolding.value = parseFloat(match[2].replace(',', '.')); + } + } + break; } - } - } - } - - private parseAmount(block: string, holding: Holding): void { - const match = block.match(tokens535[TokenType535.AmountBlock]); - if (match) { - // Amount from character 11 onwards - const amountMatch = match[1].match(/^.{11}(.*)/ms); - if (amountMatch) { - holding.amount = parseFloat(amountMatch[1].replace(',', '.')); - } - } - } - - private parseDateTime(block: string, holding: Holding): void { - const match = block.match(tokens535[TokenType535.DateTimeBlock]); - if (match) { - const type = match[1]; - const content = match[2]; - - // Date from characters 6-13 (8 chars: YYYYMMDD) - const dateMatch = content.match(tokens535[TokenType535.DateString]); - if (dateMatch) { - const parsedDate = this.parseDate(dateMatch[0]); - - if (type === 'C') { - // :98C: has a time component HHMMSS starting at character 14 - const timeMatch = content.match(tokens535[TokenType535.TimeString]); - if (timeMatch) { - parsedDate.setHours( - parseInt(timeMatch[1], 10), - parseInt(timeMatch[2], 10), - parseInt(timeMatch[3], 10), + case '35B': + if (currentHolding) { + let lastIndex = 0; + + let match = field.value.match(/^ISIN (\w{12})/); + if (match) { + currentHolding.isin = match[1]; + lastIndex = match[0].length; + } + + match = field.value.match(/\/[A-Z]{2}\/(\w*?)$/m); + + if (match) { + currentHolding.wkn = match[1].trim(); + lastIndex = (match.index ?? 0) + match[0].length; + } + + currentHolding.name = field.value + .substring(lastIndex) + .trim() + .replaceAll('\r\n', '/') + .trim(); + } + break; + case '70E': + if (currentHolding) { + const match = field.value.match( + /:HOLD\/\/\d(.*?)\+(.*?)\+(.*?)\+(.*?)\+(\d{4})(\d{2})(\d{2}).*?\d([\d,]+)\+([A-Z]{3})/s, ); + if (match) { + currentHolding.acquisitionDate = new Date( + `${match[5]}-${match[6]}-${match[7]}T12:00`, + ); + currentHolding.acquisitionPrice = parseFloat(match[8].replace(',', '.')); + } } - } else { - // :98A: time defaults to 00:00:00 - parsedDate.setHours(0, 0, 0, 0); - } - holding.date = parsedDate; + break; + case '93B': + if (currentHolding) { + const match = field.value.match(/:AGGR\/\/UNIT\/([\d,]+)/); + if (match) { + currentHolding.amount = parseFloat(match[1].replace(',', '.')); + } + } + break; + case '90A': + if (currentHolding) { + const match = field.value.match(/:.*?\/\/.*?\/([\d,]+)/); + if (match) { + currentHolding.price = parseFloat(match[1].replace(',', '.')) / 100; + currentHolding.currency = '%'; + } + } + break; + case '90B': + if (currentHolding) { + const match = field.value.match(/:.*?\/\/.*?\/([A-Z]{3})([\d,]+)/); + if (match) { + currentHolding.price = parseFloat(match[2].replace(',', '.')); + currentHolding.currency = match[1]; + } + } + break; + case '98A': + if (currentHolding) { + const matchDate = field.value.match(/(\d{4})(\d{2})(\d{2})/); + if (matchDate) { + currentHolding.date = new Date( + `${matchDate[1]}-${matchDate[2]}-${matchDate[3]}T12:00`, + ); + } + } + break; + case '98C': + if (currentHolding) { + const matchDate = field.value.match(/(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/); + if (matchDate) { + currentHolding.date = new Date( + `${matchDate[1]}-${matchDate[2]}-${matchDate[3]}T${matchDate[4]}:${matchDate[5]}:${matchDate[6]}`, + ); + } + } + break; + case '70C': + if (currentHolding) { + const cleanBlock = field.value.replace(/^:SUBB\/\//, ''); + const lines = cleanBlock.split(/\r?\n/); + let name = ''; + + for (const line of lines) { + const trimmed = line.trim(); + + if (trimmed.startsWith('1')) { + name = trimmed.substring(1).trim(); + } + + if (trimmed.startsWith('2')) { + name += ` /${trimmed.substring(1).trim()}`; + } + + const matchLine3 = trimmed.match( + /^3\s*([A-Z0-9]{3,4})\s+(?[\d.]+)(?[A-Z]{3})\s+(?[\d\-T:.]+)/, + ); + + if (matchLine3) { + currentHolding.price = parseFloat(matchLine3.groups?.price || '0'); + currentHolding.currency = matchLine3.groups?.currency || ''; + currentHolding.date = new Date(matchLine3.groups?.timestamp || ''); + } + } + + if (!currentHolding.name && name) { + currentHolding.name = name; + } + } + break; } } - } - - private parseDate(dateString: string): Date { - const match = dateString.match(tokens535[TokenType535.DateString]); - if (!match) { - throw new Error(`Invalid date format: ${dateString}`); - } - try { - return new Date(parseInt(match[1], 10), parseInt(match[2], 10) - 1, parseInt(match[3], 10)); - } catch (error) { - throw new Error(`Invalid date: ${dateString}`, { cause: error }); - } + return result; } } diff --git a/src/tests/mt535parser.test.ts b/src/tests/mt535parser.test.ts index 21a10df..f427d0e 100644 --- a/src/tests/mt535parser.test.ts +++ b/src/tests/mt535parser.test.ts @@ -2,129 +2,94 @@ import { describe, expect, it } from 'vitest'; import { Mt535Parser } from '../mt535parser.js'; describe('Mt535Parser', () => { - it('parses a MT535 input string with depot value and multiple holdings', () => { + it('handles empty input', () => { + const parser = new Mt535Parser(''); + const statement = parser.parse(); + expect(statement.totalValue).toBeUndefined(); + expect(statement.holdings).toHaveLength(0); + }); + + it('parses holding with :90A:: percentage price (bonds)', () => { const input = - ':16R:ADDINFO\r\n' + - 'EUR125000,50\r\n' + - ':16S:ADDINFO\r\n' + - // Holding 1 (BMW) - ':16R:FIN\r\n' + - ':35B:ISIN DE0005190003/DE/519000BAY.MOTOREN WERKE AG ST:\r\n' + - ':70E::HOLD//100STK275,30+EUR\r\n' + - ':90B::PREFIX_11_CEUR100,50:\r\n' + - ':93B::QTY/AVALBLX100,:\r\n' + - ':98A::SETT//DTE20231101:\r\n' + - ':16S:FIN\r\n' + - // Holding 2 (Apple) ':16R:FIN\r\n' + - ':35B:ISIN US0378331005/US/037833Apple Inc. Common Stock:\r\n' + - ':70E::HOLD//50STK2150,75+USD\r\n' + - ':90A::PRIC/RATE_X85,50:\r\n' + - ':93B::QTY/AVALBLX50,:\r\n' + - ':98C::TRADTE20231101143000:\r\n' + + ':35B:ISIN DE0001102580\r\n' + + '/DE/110258\r\n' + + 'BUNDESREP.DEUTSCHLAND ANL.V.2021\r\n' + + ':90A::MRKT//PRCT/98,50\r\n' + + ':93B::AGGR//UNIT/10000,\r\n' + + ':19A::HOLD//EUR9850,00\r\n' + ':16S:FIN\r\n'; const parser = new Mt535Parser(input); const statement = parser.parse(); - // Test depot value - expect(statement.totalValue).toBe(125000.5); - expect(statement.holdings).toHaveLength(2); - - // Test first holding (BMW) - const bmwHolding = statement.holdings[0]; - expect(bmwHolding.isin).toBe('DE0005190003'); - expect(bmwHolding.wkn).toBe('519000'); - expect(bmwHolding.name).toBe('BAY.MOTOREN WERKE AG ST'); - expect(bmwHolding.acquisitionPrice).toBe(75.3); - expect(bmwHolding.price).toBe(100.5); - expect(bmwHolding.currency).toBe('EUR'); - expect(bmwHolding.amount).toBe(100); - expect(bmwHolding.value).toBe(10050); - expect(bmwHolding.date).toEqual(new Date(2023, 10, 1, 0, 0, 0)); - - // Test second holding (Apple) - const appleHolding = statement.holdings[1]; - expect(appleHolding.isin).toBe('US0378331005'); - expect(appleHolding.wkn).toBe('037833'); - expect(appleHolding.name).toBe('Apple Inc. Common Stock'); - expect(appleHolding.acquisitionPrice).toBe(150.75); - expect(appleHolding.price).toBe(0.855); - expect(appleHolding.currency).toBe('%'); - expect(appleHolding.amount).toBe(50); - expect(appleHolding.value).toBe(42.75); - expect(appleHolding.date).toEqual(new Date(2023, 10, 1, 14, 30, 0)); + expect(statement.holdings).toHaveLength(1); + const holding = statement.holdings[0]; + expect(holding.isin).toBe('DE0001102580'); + expect(holding.wkn).toBe('110258'); + expect(holding.currency).toBe('%'); + expect(holding.price).toBe(0.985); + expect(holding.amount).toBe(10000); + expect(holding.value).toBe(9850); }); - it('parses MT535 with @@ dividers and different data points', () => { + it('parses holding with :90B:: absolute price', () => { const input = - ':16R:ADDINFO@@' + - 'EUR50000,25@@' + - ':16S:ADDINFO@@' + - ':16R:FIN@@' + - ':35B:ISIN DE0007164600/DE/716460SAP SE:@@' + - ':70E::HOLD//25STK2120,80+EUR@@' + - ':90B::UNKNOWNVALXEUR115,75:@@' + - ':93B::SOMESTATUS_25,:@@' + - ':98A::REGD//DTE20231215:@@' + - ':16S:FIN@@'; + ':16R:FIN\r\n' + + ':35B:ISIN DE0005190003\r\n' + + '/DE/519000\r\n' + + 'BAY.MOTOREN WERKE AG ST\r\n' + + ':90B::MRKT//ACTU/EUR89,50\r\n' + + ':93B::AGGR//UNIT/50,\r\n' + + ':19A::HOLD//EUR4475,00\r\n' + + ':16S:FIN\r\n'; const parser = new Mt535Parser(input); const statement = parser.parse(); - expect(statement.totalValue).toBe(50000.25); - expect(statement.holdings).toHaveLength(1); - const holding = statement.holdings[0]; - expect(holding.isin).toBe('DE0007164600'); - expect(holding.wkn).toBe('716460'); - expect(holding.name).toBe('SAP SE'); - expect(holding.acquisitionPrice).toBe(120.8); - expect(holding.price).toBe(115.75); + expect(holding.isin).toBe('DE0005190003'); expect(holding.currency).toBe('EUR'); - expect(holding.amount).toBe(25); - expect(holding.value).toBe(2893.75); - expect(holding.date).toEqual(new Date(2023, 11, 15)); - }); - - it('handles empty input', () => { - const parser = new Mt535Parser(''); - const statement = parser.parse(); - expect(statement.totalValue).toBeUndefined(); - expect(statement.holdings).toHaveLength(0); + expect(holding.price).toBe(89.5); + expect(holding.amount).toBe(50); + expect(holding.value).toBe(4475); }); - it('handles input without depot value', () => { + it('parses 98A date-only field', () => { const input = ':16R:FIN\r\n' + - ':35B:ISIN DE0005190003/DE/519000SOME OTHER AG ST:\r\n' + - ':90B::PREFIX_11_CEUR100,50:\r\n' + - ':93B::QTY/AVALBLX100,:\r\n' + - ':98A::SETT//DTE20231101:\r\n' + + ':35B:ISIN DE0007164600\r\n' + + '/DE/716460\r\n' + + 'SAP SE\r\n' + + ':98A::SETT//20231215\r\n' + ':16S:FIN\r\n'; const parser = new Mt535Parser(input); const statement = parser.parse(); - expect(statement.totalValue).toBeUndefined(); - expect(statement.holdings).toHaveLength(1); - expect(statement.holdings[0].isin).toBe('DE0005190003'); - expect(statement.holdings[0].name).toBe('SOME OTHER AG ST'); - expect(statement.holdings[0].price).toBe(100.5); - expect(statement.holdings[0].amount).toBe(100); + const holding = statement.holdings[0]; + expect(holding.date).toEqual(new Date('2023-12-15T12:00')); }); - it('handles input without holdings', () => { - const input = ':16R:ADDINFO\r\n' + 'EUR75000,00\r\n' + ':16S:ADDINFO\r\n'; + it('parses 98C date-time field', () => { + const input = + ':16R:FIN\r\n' + + ':35B:ISIN DE0007164600\r\n' + + '/DE/716460\r\n' + + 'SAP SE\r\n' + + ':98C::PRIC//20240115093045\r\n' + + ':16S:FIN\r\n'; + const parser = new Mt535Parser(input); const statement = parser.parse(); - expect(statement.totalValue).toBe(75000.0); - expect(statement.holdings).toHaveLength(0); + + const holding = statement.holdings[0]; + expect(holding.date).toEqual(new Date(2024, 0, 15, 9, 30, 45)); }); - it('handles holding with minimal data (only security identification)', () => { + it('parses holding with minimal data (only ISIN)', () => { const input = - ':16R:FIN\r\n' + ':35B:ISIN FR0000120271/FR/120271AIR LIQUIDE SA: \r\n' + ':16S:FIN\r\n'; + ':16R:FIN\r\n' + ':35B:ISIN FR0000120271\r\n' + 'AIR LIQUIDE SA\r\n' + ':16S:FIN\r\n'; const parser = new Mt535Parser(input); const statement = parser.parse(); @@ -132,53 +97,109 @@ describe('Mt535Parser', () => { expect(statement.holdings).toHaveLength(1); const holding = statement.holdings[0]; expect(holding.isin).toBe('FR0000120271'); - expect(holding.wkn).toBe('120271'); expect(holding.name).toBe('AIR LIQUIDE SA'); expect(holding.price).toBeUndefined(); expect(holding.amount).toBeUndefined(); - expect(holding.value).toBeUndefined(); - expect(holding.date).toBeUndefined(); }); - it('calculates value correctly for percentage currency when price is from :90A', () => { + it('parses DKB statement correctly', () => { const input = + ':16R:GENL\r\n' + + ':28E:1/ONLY\r\n' + + ':20C::SEME//NONREF\r\n' + + ':23G:NEWM\r\n' + + ':98C::PREP//20260110153747\r\n' + + ':98A::STAT//20260110\r\n' + + ':22F::STTY//CUST\r\n' + + ':97A::SAFE//12030000/123456789\r\n' + + ':17B::ACTI//Y\r\n' + + ':16S:GENL\r\n' + ':16R:FIN\r\n' + - ':35B:ISIN US0378331005/US/037833Apple Inc. Common Stock:\r\n' + - ':90A::PRIC/RATE_X85,50:\r\n' + - ':93B::QTY/AVALBLX100,:\r\n' + - ':16S:FIN\r\n'; + ':35B:ISIN LU0950674332\r\n' + + '/DE/A1W3CQ\r\n' + + 'UBS MSCI WORLD SOC.RES. NAMENS-ANTE\r\n' + + 'ILE A ACC. USD O.N.\r\n' + + ':90B::MRKT//ACTU/EUR33,22\r\n' + + ':98C::PRIC//20260109210244\r\n' + + ':93B::AGGR//UNIT/652,\r\n' + + ':16R:SUBBAL\r\n' + + ':93C::TAVI//UNIT/AVAI/652,\r\n' + + ':16S:SUBBAL\r\n' + + ':19A::HOLD//EUR18669,64\r\n' + + ':70E::HOLD//1STK++++20220902\r\n' + + '221,2012438+EUR\r\n' + + ':16S:FIN\r\n' + + ':16R:ADDINFO\r\n' + + ':19A::HOLP//EUR18669,64\r\n' + + ':16S:ADDINFO\r\n'; const parser = new Mt535Parser(input); const statement = parser.parse(); + expect(statement.totalValue).toBe(18669.64); + expect(statement.currency).toBe('EUR'); + expect(statement.holdings).toHaveLength(1); const holding = statement.holdings[0]; - expect(holding.currency).toBe('%'); - expect(holding.price).toBe(0.855); - expect(holding.amount).toBe(100); - expect(holding.value).toBe(85.5); + expect(holding.isin).toBe('LU0950674332'); + expect(holding.wkn).toBe('A1W3CQ'); + expect(holding.name).toBe('UBS MSCI WORLD SOC.RES. NAMENS-ANTE/ILE A ACC. USD O.N.'); + expect(holding.acquisitionDate).toEqual(new Date('2022-09-02T12:00')); + expect(holding.acquisitionPrice).toBe(21.2012438); + expect(holding.amount).toBe(652); + expect(holding.price).toBe(33.22); + expect(holding.currency).toBe('EUR'); + expect(holding.value).toBe(18669.64); + expect(holding.date).toEqual(new Date(2026, 0, 9, 21, 2, 44)); }); - it('correctly parses date and time for :98C', () => { + it('parses Baader statement correctly', () => { const input = + ':16R:GENL\r\n' + + ':28E:1/ONLY\r\n' + + ':20C::SEME//NONREF\r\n' + + ':23G:NEWM\r\n' + + ':98A::PREP//20260108\r\n' + + ':98A::STAT//20260108\r\n' + + ':22F::STTY//CUST\r\n' + + ':97A::SAFE//70033100/12345678\r\n' + + ':17B::ACTI//Y\r\n' + + ':16S:GENL\r\n' + ':16R:FIN\r\n' + - ':35B:ISIN DE000BASF111/DE/BASF11BASF SE:\r\n' + - ':98C::QUALIF20240115093045:\r\n' + - ':16S:FIN\r\n'; - const parser = new Mt535Parser(input); - const statement = parser.parse(); - const holding = statement.holdings[0]; - expect(holding.date).toEqual(new Date(2024, 0, 15, 9, 30, 45)); - }); + ':35B:ISIN IE000UQND7H4\r\n' + + 'HSBC ETF- WORLD DLA\r\n' + + 'HSBC MSCI WORLD UCITS ETF\r\n' + + ':93B::AGGR//UNIT/680\r\n' + + ':16R:SUBBAL\r\n' + + ':93C::TAVI//UNIT/AVAI/680\r\n' + + ':70C::SUBB//1 HSBC ETF- WORLD DLA\r\n' + + '2\r\n' + + '3 EDE 37.200000000EUR 2026-01-08T19:08:31.7\r\n' + + '4 25875.56EUR IE000UQND7H4, 1/SO\r\n' + + ':16S:SUBBAL\r\n' + + ':19A::HOLD//EUR25296\r\n' + + ':70E::HOLD//1STK++++20260107\r\n' + + '237,267+EUR\r\n' + + ':16S:FIN\r\n' + + ':16R:ADDINFO\r\n' + + ':19A::HOLP//EUR25296\r\n' + + ':16S:ADDINFO\r\n'; - it('correctly parses date for :98A (time defaults to 00:00:00)', () => { - const input = - ':16R:FIN\r\n' + - ':35B:ISIN DE000BAY0017/DE/BAY001BAYER AG:\r\n' + - ':98A::SETT//DTE20231005:\r\n' + - ':16S:FIN\r\n'; const parser = new Mt535Parser(input); const statement = parser.parse(); + + expect(statement.totalValue).toBe(25296); + expect(statement.currency).toBe('EUR'); + expect(statement.holdings).toHaveLength(1); const holding = statement.holdings[0]; - expect(holding.date).toEqual(new Date(2023, 9, 5, 0, 0, 0)); + expect(holding.isin).toBe('IE000UQND7H4'); + expect(holding.wkn).toBeUndefined(); + expect(holding.name).toBe('HSBC ETF- WORLD DLA/HSBC MSCI WORLD UCITS ETF'); + expect(holding.acquisitionDate).toEqual(new Date('2026-01-07T12:00')); + expect(holding.acquisitionPrice).toBe(37.267); + expect(holding.amount).toBe(680); + expect(holding.price).toBe(37.2); + expect(holding.currency).toBe('EUR'); + expect(holding.value).toBe(25296); + expect(holding.date).toEqual(new Date(2026, 0, 8, 19, 8, 31, 700)); }); }); diff --git a/vitest.config.js b/vitest.config.js index b4b65ab..ffc4452 100644 --- a/vitest.config.js +++ b/vitest.config.js @@ -2,6 +2,6 @@ import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { - // ... Specify options here. + exclude: ['**/node_modules/**', '**/dist/**'], }, });