forked from leolabs/you-need-a-parser
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add Trade Republic parser (#322)
- Loading branch information
1 parent
d5c9e1f
commit d9182ca
Showing
5 changed files
with
239 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
{ | ||
"name": "@envelope-zero/ynap-parsers", | ||
"version": "1.16.7", | ||
"version": "1.17.0", | ||
"description": "Parsers from various formats to YNAB CSV", | ||
"main": "index.js", | ||
"author": "Envelope Zero Team <[email protected]> (https://envelope-zero.org)", | ||
|
69 changes: 69 additions & 0 deletions
69
packages/ynap-parsers/src/de/trade-republic/test-data/transactions.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
{ | ||
"items": [ | ||
{ | ||
"id": "3af95aed-76a9-4142-9c9b-d4fb2598e013", | ||
"timestamp": "2024-02-04T15:56:33.375+0000", | ||
"title": "Zinsen", | ||
"icon": "logos/timeline_interest_new/v2", | ||
"badge": null, | ||
"subtitle": null, | ||
"amount": { "currency": "EUR", "value": 10, "fractionDigits": 2 }, | ||
"subAmount": null, | ||
"status": "EXECUTED", | ||
"action": { | ||
"type": "timelineDetail", | ||
"payload": "3af95aed-76a9-4142-9c9b-d4fb2598e013" | ||
}, | ||
"eventType": "INTEREST_PAYOUT_CREATED" | ||
}, | ||
{ | ||
"id": "e2f5c297-79ab-4118-8a79-8ce7d53a4b41", | ||
"timestamp": "2023-06-11T15:44:14.017+0000", | ||
"title": "Einzahlung", | ||
"icon": "logos/timeline_plus_circle/v2", | ||
"badge": null, | ||
"subtitle": null, | ||
"amount": { "currency": "EUR", "value": 50.0, "fractionDigits": 2 }, | ||
"subAmount": null, | ||
"status": "EXECUTED", | ||
"action": { | ||
"type": "timelineDetail", | ||
"payload": "e2f5c297-79ab-4118-8a79-8ce7d53a4b41" | ||
}, | ||
"eventType": "PAYMENT_INBOUND_SEPA_DIRECT_DEBIT" | ||
}, | ||
{ | ||
"id": "5b17cc36-67ec-4e39-9c24-a26f653634d5", | ||
"timestamp": "2017-10-17T08:11:08.217+0000", | ||
"title": "Some Asset", | ||
"icon": "logos/IDENTIFIER/v2", | ||
"badge": null, | ||
"subtitle": "Sparplan ausgef\u00fchrt", | ||
"amount": { "currency": "EUR", "value": -70.0, "fractionDigits": 2 }, | ||
"subAmount": null, | ||
"status": "EXECUTED", | ||
"action": { | ||
"type": "timelineDetail", | ||
"payload": "5b17cc36-67ec-4e39-9c24-a26f653634d5" | ||
}, | ||
"eventType": "SAVINGS_PLAN_EXECUTED" | ||
}, | ||
{ | ||
"id": "7f28cf4a-435a-4fd4-9346-5215a196a9e5", | ||
"timestamp": "2012-10-11T02:28:16.441+0000", | ||
"title": "Some Asset", | ||
"icon": "logos/IDENTIFIER/v2", | ||
"badge": null, | ||
"subtitle": "Vorabpauschale", | ||
"amount": { "currency": "EUR", "value": -17.03, "fractionDigits": 2 }, | ||
"subAmount": null, | ||
"status": "EXECUTED", | ||
"action": { | ||
"type": "timelineDetail", | ||
"payload": "7f28cf4a-435a-4fd4-9346-5215a196a9e5" | ||
}, | ||
"eventType": "PRE_DETERMINED_TAX_BASE" | ||
} | ||
], | ||
"cursors": { "after": "143dde86-1996-4017-ae80-bbdfdf6d79e8", "before": null } | ||
} |
86 changes: 86 additions & 0 deletions
86
packages/ynap-parsers/src/de/trade-republic/trade-republic.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
import { generateYnabDate, tradeRepublic } from './trade-republic' | ||
import { YnabFile } from '../..' | ||
import { encode } from 'iconv-lite' | ||
|
||
const content = encode( | ||
`{ | ||
"items": [ | ||
{ | ||
"id": "3af95aed-76a9-4142-9c9b-d4fb2598e013", | ||
"timestamp": "2024-02-04T15:56:33.375+0000", | ||
"title": "Zinsen", | ||
"icon": "logos/timeline_interest_new/v2", | ||
"badge": null, | ||
"subtitle": null, | ||
"amount": { "currency": "EUR", "value": 10, "fractionDigits": 2 }, | ||
"subAmount": null, | ||
"status": "EXECUTED", | ||
"action": { | ||
"type": "timelineDetail", | ||
"payload": "3af95aed-76a9-4142-9c9b-d4fb2598e013" | ||
}, | ||
"eventType": "INTEREST_PAYOUT_CREATED" | ||
} | ||
], | ||
"cursors": { "after": "143dde86-1996-4017-ae80-bbdfdf6d79e8", "before": null } | ||
}`, | ||
'utf-8' | ||
) | ||
|
||
const ynabResult: YnabFile[] = [ | ||
{ | ||
data: [ | ||
{ | ||
Date: '02/04/2024', | ||
Payee: 'Zinsen', | ||
Inflow: 10, | ||
}, | ||
], | ||
}, | ||
] | ||
|
||
describe('trade-republic Parser Module', () => { | ||
describe('Matcher', () => { | ||
it('should match trade-republic files by file name', async () => { | ||
const fileName = 'transactions.json' | ||
const result = !!fileName.match(tradeRepublic.filenamePattern) | ||
expect(result).toBe(true) | ||
}) | ||
|
||
it('should not match other files by file name', async () => { | ||
const invalidFile = new File([], 'test.json') | ||
const result = await tradeRepublic.match(invalidFile) | ||
expect(result).toBe(false) | ||
}) | ||
|
||
it('should match trade-republic files by fields', async () => { | ||
const file = new File([content], 'test.json') | ||
const result = await tradeRepublic.match(file) | ||
expect(result).toBe(true) | ||
}) | ||
|
||
it('should not match empty files', async () => { | ||
const file = new File([], 'test.json') | ||
const result = await tradeRepublic.match(file) | ||
expect(result).toBe(false) | ||
}) | ||
}) | ||
|
||
describe('Parser', () => { | ||
it('should parse data correctly', async () => { | ||
const file = new File([content], 'test.json') | ||
const result = await tradeRepublic.parse(file) | ||
expect(result).toEqual(ynabResult) | ||
}) | ||
}) | ||
|
||
describe('Date Converter', () => { | ||
it('should format an input date correctly', () => { | ||
expect(generateYnabDate('2024-02-01')).toEqual('02/01/2024') | ||
}) | ||
|
||
it('should throw an error when the input date is incorrect', () => { | ||
expect(() => generateYnabDate('2024-01')).toThrow('not a valid date') | ||
}) | ||
}) | ||
}) |
78 changes: 78 additions & 0 deletions
78
packages/ynap-parsers/src/de/trade-republic/trade-republic.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
import 'mdn-polyfills/String.prototype.startsWith' | ||
import { ParserFunction, MatcherFunction, ParserModule } from '../..' | ||
import { readEncodedFile } from '../../util/read-encoded-file' | ||
|
||
export interface TradeRepublicEntry { | ||
timestamp: string | ||
title: string | ||
amount: { | ||
value: number | ||
} | ||
eventType: string | ||
} | ||
|
||
export const generateYnabDate = (input: string) => { | ||
const match = input.match(/(\d{4})\-(\d{2})\-(\d{2})/) | ||
|
||
if (!match) { | ||
throw new Error( | ||
'The input is not a valid date. Expected format: YYYY-MM-DD' | ||
) | ||
} | ||
|
||
const [, year, month, day] = match | ||
return [month.padStart(2, '0'), day.padStart(2, '0'), year].join('/') | ||
} | ||
|
||
export const tradeRepublicParser: ParserFunction = async (file: File) => { | ||
const fileString = await readEncodedFile(file) | ||
const data = await JSON.parse(fileString)['items'] | ||
|
||
return [ | ||
{ | ||
data: (data as TradeRepublicEntry[]) | ||
.filter( | ||
// German "Vorabpauschale" is deducted from the asset value directly | ||
r => r.eventType != 'PRE_DETERMINED_TAX_BASE' && r.amount.value != 0 | ||
) | ||
.map(r => ({ | ||
Date: generateYnabDate(r.timestamp), | ||
Outflow: r.amount.value < 0 ? -r.amount.value : undefined, | ||
Inflow: r.amount.value > 0 ? r.amount.value : undefined, | ||
|
||
// Savings plans have the target asset as title, but we want it as a "Portfolio" account | ||
Payee: | ||
r.eventType === 'SAVINGS_PLAN_EXECUTED' | ||
? 'Trade Republic Portfolio' | ||
: r.title, | ||
Memo: r.eventType === 'SAVINGS_PLAN_EXECUTED' ? r.title : undefined, | ||
})), | ||
}, | ||
] | ||
} | ||
|
||
export const tradeRepublicMatcher: MatcherFunction = async (file: File) => { | ||
const rawFileString = await readEncodedFile(file) | ||
|
||
try { | ||
const data = await JSON.parse(rawFileString) | ||
const first = data.items[0] | ||
if (generateYnabDate(first.timestamp) && first.eventType) { | ||
return true | ||
} | ||
|
||
return false | ||
} catch { | ||
return false | ||
} | ||
} | ||
|
||
export const tradeRepublic: ParserModule = { | ||
name: 'trade-republic', | ||
country: 'de', | ||
fileExtension: 'json', | ||
filenamePattern: /^transactions.json$/, | ||
link: 'https://traderepublic.com', | ||
match: tradeRepublicMatcher, | ||
parse: tradeRepublicParser, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters