Skip to content

Commit

Permalink
feat: add Trade Republic parser (#322)
Browse files Browse the repository at this point in the history
  • Loading branch information
morremeyer authored Feb 2, 2024
1 parent d5c9e1f commit d9182ca
Show file tree
Hide file tree
Showing 5 changed files with 239 additions and 7 deletions.
2 changes: 1 addition & 1 deletion packages/ynap-parsers/package.json
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)",
Expand Down
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 packages/ynap-parsers/src/de/trade-republic/trade-republic.spec.ts
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 packages/ynap-parsers/src/de/trade-republic/trade-republic.ts
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,
}
11 changes: 5 additions & 6 deletions packages/ynap-parsers/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,17 @@ import { kontist } from './de/kontist/kontist'
import { n26 } from './de/n26/n26'
import { outbank } from './de/outbank/outbank'
import { volksbankEG } from './de/volksbank-eg/volksbank-eg'
import { revolut } from './international/revolut/revolut'

import { tradeRepublic } from './de/trade-republic/trade-republic'
import { ingAustria } from './at/ing/ing-austria'
import { piraeus } from './gr/piraeus/piraeus'
import { bancomer } from './mx/bbva-bancomer/bbva-bancomer'
import { aqua } from './uk/aqua/aqua'
import { marcus } from './uk/marcus/marcus'

import { bank2ynab } from './bank2ynab/bank2ynab'
import { mt940 } from './international/mt940/mt940'
import { sparbankenTanum as sparbankenTanum2018 } from './se/sparbanken-tanum/2018/sparbanken-tanum'
import { sparbankenTanum as sparbankenTanum2019 } from './se/sparbanken-tanum/2019/sparbanken-tanum'

import { bank2ynab } from './bank2ynab/bank2ynab'
import { mt940 } from './international/mt940/mt940'
import { revolut } from './international/revolut/revolut'
import { dkb } from './de/dkb/dkb'
import { bankPocztowy } from './pl/bank-pocztowy/bank-pocztowy'
import { mbank } from './pl/mbank/mbank'
Expand Down Expand Up @@ -69,6 +67,7 @@ export const parsers: ParserModule[] = [
volksbankEG,
_1822direkt,
dkb,
tradeRepublic,

// GR
piraeus,
Expand Down

0 comments on commit d9182ca

Please sign in to comment.