From cf3000be2fa7bafa613e759b05acef5fef741405 Mon Sep 17 00:00:00 2001 From: morre Date: Wed, 8 Nov 2023 21:05:26 +0100 Subject: [PATCH] chore(lint): update prettier configuration (#215) --- .prettierrc | 6 +- .../ynap-bank2ynab-converter/package.json | 2 +- .../ynap-bank2ynab-converter/src/index.ts | 120 +++++++------- .../src/parserconfig.ts | 20 +-- packages/ynap-parsers/jest.config.js | 2 +- packages/ynap-parsers/package.json | 2 +- .../src/at/ing/ing-austria.spec.ts | 68 ++++---- .../ynap-parsers/src/at/ing/ing-austria.ts | 76 ++++----- .../src/bank2ynab/bank2ynab.spec.ts | 48 +++--- .../ynap-parsers/src/bank2ynab/bank2ynab.ts | 141 ++++++++-------- .../ynap-parsers/src/bank2ynab/banks.json | 70 +++++++- .../src/bank2ynab/parse-date.spec.ts | 36 ++--- .../ynap-parsers/src/bank2ynab/parse-date.ts | 22 +-- .../src/de/1822direkt/1822direkt.spec.ts | 62 +++---- .../src/de/1822direkt/1822direkt.ts | 130 +++++++-------- .../src/de/comdirect/comdirect.spec.ts | 112 ++++++------- .../src/de/comdirect/comdirect.ts | 102 ++++++------ .../src/de/ing-diba/ing-diba.spec.ts | 68 ++++---- .../ynap-parsers/src/de/ing-diba/ing-diba.ts | 78 ++++----- .../src/de/kontist/kontist.spec.ts | 48 +++--- .../ynap-parsers/src/de/kontist/kontist.ts | 68 ++++---- packages/ynap-parsers/src/de/n26/n26.spec.ts | 88 +++++----- packages/ynap-parsers/src/de/n26/n26.ts | 64 ++++---- .../src/de/outbank/outbank.spec.ts | 66 ++++---- .../ynap-parsers/src/de/outbank/outbank.ts | 120 +++++++------- .../src/de/volksbank-eg/volksbank-eg.spec.ts | 68 ++++---- .../src/de/volksbank-eg/volksbank-eg.ts | 106 ++++++------ .../src/gr/piraeus/piraeus.spec.ts | 62 +++---- .../ynap-parsers/src/gr/piraeus/piraeus.ts | 56 +++---- packages/ynap-parsers/src/index.spec.ts | 46 ++++-- packages/ynap-parsers/src/index.ts | 152 +++++++++--------- .../src/international/mt940/mt940.spec.ts | 48 +++--- .../src/international/mt940/mt940.ts | 56 +++---- .../src/international/revolut/revolut.spec.ts | 64 ++++---- .../src/international/revolut/revolut.ts | 56 +++---- .../mx/bbva-bancomer/bbva-bancomer.spec.ts | 76 ++++----- .../src/mx/bbva-bancomer/bbva-bancomer.ts | 65 ++++---- .../pl/bank-pocztowy/bank-pocztowy.spec.ts | 66 ++++---- .../src/pl/bank-pocztowy/bank-pocztowy.ts | 73 ++++----- .../ynap-parsers/src/pl/mbank/mbank.spec.ts | 92 +++++------ packages/ynap-parsers/src/pl/mbank/mbank.ts | 81 +++++----- .../src/se/seb-privat/seb.spec.ts | 62 +++---- .../ynap-parsers/src/se/seb-privat/seb.ts | 58 +++---- .../2018/sparbanken-tanum.spec.ts | 52 +++--- .../sparbanken-tanum/2018/sparbanken-tanum.ts | 47 +++--- .../2019/sparbanken-tanum.spec.ts | 54 +++---- .../sparbanken-tanum/2019/sparbanken-tanum.ts | 92 ++++++----- .../ynap-parsers/src/uk/aqua/aqua.spec.ts | 54 ++++--- packages/ynap-parsers/src/uk/aqua/aqua.ts | 50 +++--- .../ynap-parsers/src/uk/marcus/marcus.spec.ts | 64 ++++---- packages/ynap-parsers/src/uk/marcus/marcus.ts | 73 ++++----- packages/ynap-parsers/src/util/jschardet.d.ts | 8 +- packages/ynap-parsers/src/util/papaparse.ts | 8 +- .../src/util/read-encoded-file.ts | 35 ++-- .../ynap-parsers/src/util/read-to-buffer.ts | 18 +-- packages/ynap-web-app/gatsby-browser.js | 8 +- packages/ynap-web-app/gatsby-config.js | 4 +- packages/ynap-web-app/package.json | 2 +- .../src/components/github-badge.tsx | 10 +- .../ynap-web-app/src/components/meta-tags.tsx | 38 +++-- packages/ynap-web-app/src/pages/404.tsx | 6 +- packages/ynap-web-app/src/pages/index.tsx | 136 ++++++++-------- .../src/pages/supported-formats.tsx | 34 ++-- packages/ynap-web-app/src/util/countries.ts | 4 +- 64 files changed, 1928 insertions(+), 1775 deletions(-) diff --git a/.prettierrc b/.prettierrc index dda5c4ef6..3d1f5e5cb 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,6 +1,6 @@ { - "printWidth": 85, - "semi": true, + "semi": false, "singleQuote": true, - "trailingComma": "all" + "trailingComma": "es5", + "arrowParens": "avoid" } diff --git a/packages/ynap-bank2ynab-converter/package.json b/packages/ynap-bank2ynab-converter/package.json index beb3d3bbd..89f0e9d74 100644 --- a/packages/ynap-bank2ynab-converter/package.json +++ b/packages/ynap-bank2ynab-converter/package.json @@ -1,6 +1,6 @@ { "name": "@envelope-zero/ynap-bank2ynab-converter", - "version": "1.14.41", + "version": "1.14.42", "license": "MIT", "author": { "email": "admin+github@leolabs.org", diff --git a/packages/ynap-bank2ynab-converter/src/index.ts b/packages/ynap-bank2ynab-converter/src/index.ts index 7e6924d06..027503d98 100644 --- a/packages/ynap-bank2ynab-converter/src/index.ts +++ b/packages/ynap-bank2ynab-converter/src/index.ts @@ -1,119 +1,119 @@ #! /usr/bin/env node -import fetch from 'node-fetch'; -import fs from 'fs'; -import { Command } from 'commander'; +import fetch from 'node-fetch' +import fs from 'fs' +import { Command } from 'commander' -const program = new Command(); +const program = new Command() program .description('Fetch and parse the current bank2ynab config file to JSON') .option( '-e, --exclude ', 'Exclude banks by their name (comma-separated)', - (v: string) => v.split(',').map((i) => i.trim()), + (v: string) => v.split(',').map(i => i.trim()) ) .option( '-b, --branch ', 'Set the branch that the config should be fetched from', - 'master', + 'master' ) .option('-o, --output ', 'Set the output file path', 'bank2ynab.json') - .parse(process.argv); + .parse(process.argv) -import { ParserConfig } from './parserconfig'; +import { ParserConfig } from './parserconfig' const CONFIG_URL = `https://raw.githubusercontent.com/bank2ynab/bank2ynab/${ program.opts()['branch'] -}/bank2ynab.conf`; +}/bank2ynab.conf` const CONFIG_LINK = `https://github.com/bank2ynab/bank2ynab/blob/${ program.opts()['branch'] -}/bank2ynab.conf`; +}/bank2ynab.conf` -const SECTION = new RegExp(/^\s*\[([^\]]+)]/); -const KEY = new RegExp(/\s*(.*?)\s*[=:]\s*(.*)/); -const COMMENT = new RegExp(/^\s*[;#]/); +const SECTION = new RegExp(/^\s*\[([^\]]+)]/) +const KEY = new RegExp(/\s*(.*?)\s*[=:]\s*(.*)/) +const COMMENT = new RegExp(/^\s*[;#]/) -const ignorelist = program.opts()['exclude'] || []; +const ignorelist = program.opts()['exclude'] || [] interface Sections { - [k: string]: ConfigFields; + [k: string]: ConfigFields } interface ConfigFields { - Line: string; - 'Source Filename Pattern'?: string; - 'Source Filename Extension'?: string; - 'Header Rows'?: string; - 'Footer Rows'?: string; - 'Input Columns'?: string; - 'Date Format'?: string; - 'Inflow or Outflow Indicator'?: string; - 'Source CSV Delimiter'?: string; - Plugin?: string; - [k: string]: string; + Line: string + 'Source Filename Pattern'?: string + 'Source Filename Extension'?: string + 'Header Rows'?: string + 'Footer Rows'?: string + 'Input Columns'?: string + 'Date Format'?: string + 'Inflow or Outflow Indicator'?: string + 'Source CSV Delimiter'?: string + Plugin?: string + [k: string]: string } export const parseConfig = (config: string) => { - const lines = config.split('\n'); + const lines = config.split('\n') - const sections: Sections = {}; + const sections: Sections = {} - let currentSection = null; + let currentSection = null for (let i = 0; i < lines.length; i++) { - const line = lines[i]; + const line = lines[i] if (line.match(COMMENT)) { - continue; + continue } - const sectionMatch = line.match(SECTION); + const sectionMatch = line.match(SECTION) if (sectionMatch) { - currentSection = sectionMatch[1]; - sections[currentSection] = { Line: String(i + 1) }; - continue; + currentSection = sectionMatch[1] + sections[currentSection] = { Line: String(i + 1) } + continue } - const keyMatch = line.match(KEY); + const keyMatch = line.match(KEY) if (currentSection && keyMatch && keyMatch[1] && keyMatch[2]) { - const key = keyMatch[1].trim(); - const value = keyMatch[2].trim(); + const key = keyMatch[1].trim() + const value = keyMatch[2].trim() if (Object.keys(sections).includes(currentSection)) { - sections[currentSection][key] = value; + sections[currentSection][key] = value } } } - return sections; -}; + return sections +} const script = async () => { - const resp = await fetch(CONFIG_URL); + const resp = await fetch(CONFIG_URL) if (!resp.ok) { - throw new Error(`Fetch failed: ${resp.status}\n\n${resp.body}`); + throw new Error(`Fetch failed: ${resp.status}\n\n${resp.body}`) } - const configData = await resp.textConverted(); + const configData = await resp.textConverted() - const config = parseConfig(configData); + const config = parseConfig(configData) - console.log('Excluding', ignorelist.length, 'items from ignorelist.'); + console.log('Excluding', ignorelist.length, 'items from ignorelist.') const filteredConfig: ParserConfig[] = Object.keys(config) - .map((c) => ({ ...config[c], Name: c })) + .map(c => ({ ...config[c], Name: c })) .filter( - (c) => + c => c.Name !== 'DEFAULT' && !ignorelist.includes(c.Name) && !c.Plugin && c['Source Filename Pattern'] !== 'unknown!' && - c['Input Columns'], + c['Input Columns'] ) .map( - (c) => + c => ({ name: c.Name.split(' ').slice(1).join(' '), country: c.Name.split(' ')[0].toLowerCase(), @@ -128,25 +128,25 @@ const script = async () => { dateFormat: c['Date Format'], inflowOutflowFlag: c['Inflow or Outflow Indicator'] ?.split(',') - .map((s) => s.trim()), + .map(s => s.trim()), headerRows: Number(c['Header Rows'] || '1'), footerRows: Number(c['Footer Rows'] || '0'), - }) as ParserConfig, - ); + }) as ParserConfig + ) console.log( 'Parsed', filteredConfig.length, 'bank configs. Filtered from', Object.keys(config).length, - 'configs.', - ); + 'configs.' + ) fs.writeFileSync( program.opts()['output'], - JSON.stringify(filteredConfig, null, 2), - ); - console.log('Saved configs to', program.opts()['output'].output); -}; + JSON.stringify(filteredConfig, null, 2) + ) + console.log('Saved configs to', program.opts()['output'].output) +} -script(); +script() diff --git a/packages/ynap-bank2ynab-converter/src/parserconfig.ts b/packages/ynap-bank2ynab-converter/src/parserconfig.ts index 8c7500d6a..ca380967e 100644 --- a/packages/ynap-bank2ynab-converter/src/parserconfig.ts +++ b/packages/ynap-bank2ynab-converter/src/parserconfig.ts @@ -1,12 +1,12 @@ export interface ParserConfig { - filenamePattern: string; - filenameExtension: string; - headerRows: number; - footerRows: number; - inputColumns: string[]; - inflowOutflowFlag?: [string, string, string]; - dateFormat?: string; - name: string; - link: string; - country: string; + filenamePattern: string + filenameExtension: string + headerRows: number + footerRows: number + inputColumns: string[] + inflowOutflowFlag?: [string, string, string] + dateFormat?: string + name: string + link: string + country: string } diff --git a/packages/ynap-parsers/jest.config.js b/packages/ynap-parsers/jest.config.js index c127305f2..4fcc455f2 100644 --- a/packages/ynap-parsers/jest.config.js +++ b/packages/ynap-parsers/jest.config.js @@ -6,4 +6,4 @@ module.exports = { testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$', moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], testEnvironment: 'jsdom', -}; +} diff --git a/packages/ynap-parsers/package.json b/packages/ynap-parsers/package.json index c72ef5d19..07d8d1c04 100644 --- a/packages/ynap-parsers/package.json +++ b/packages/ynap-parsers/package.json @@ -1,6 +1,6 @@ { "name": "@envelope-zero/ynap-parsers", - "version": "1.15.34", + "version": "1.15.35", "description": "Parsers from various formats to YNAB CSV", "main": "index.js", "author": "Envelope Zero Team (https://envelope-zero.org)", diff --git a/packages/ynap-parsers/src/at/ing/ing-austria.spec.ts b/packages/ynap-parsers/src/at/ing/ing-austria.spec.ts index 8b53340cb..849a0c30c 100644 --- a/packages/ynap-parsers/src/at/ing/ing-austria.spec.ts +++ b/packages/ynap-parsers/src/at/ing/ing-austria.spec.ts @@ -1,13 +1,13 @@ -import { generateYnabDate, ingAustria } from './ing-austria'; -import { YnabFile } from '../..'; -import { encode } from 'iconv-lite'; +import { generateYnabDate, ingAustria } from './ing-austria' +import { YnabFile } from '../..' +import { encode } from 'iconv-lite' const content = encode( `IBAN;Text;Valutadatum;Währung;Soll;Haben AT483200000012345864;Outflow from Max Mustermann;01.12.2019;EUR;100,01;0,00 AT483200000012345864;Inflow from John Doe;02.12.2019;EUR;0,00;200,01`, - 'ISO-8859-1', -); + 'ISO-8859-1' +) const ynabResult: YnabFile[] = [ { @@ -28,50 +28,50 @@ const ynabResult: YnabFile[] = [ }, ], }, -]; +] describe('ING Austria Parser Module', () => { describe('Matcher', () => { it('should match ING Austria files by file name', async () => { - const fileName = 'ING_Umsaetze.csv'; - const result = !!fileName.match(ingAustria.filenamePattern); - expect(result).toBe(true); - }); + const fileName = 'ING_Umsaetze.csv' + const result = !!fileName.match(ingAustria.filenamePattern) + expect(result).toBe(true) + }) it('should not match other files by file name', async () => { - const invalidFile = new File([], 'test.csv'); - const result = await ingAustria.match(invalidFile); - expect(result).toBe(false); - }); + const invalidFile = new File([], 'test.csv') + const result = await ingAustria.match(invalidFile) + expect(result).toBe(false) + }) it('should match ING Austria files by fields', async () => { - const file = new File([content], 'test.csv'); - const result = await ingAustria.match(file); - expect(result).toBe(true); - }); + const file = new File([content], 'test.csv') + const result = await ingAustria.match(file) + expect(result).toBe(true) + }) it('should not match empty files', async () => { - const file = new File([], 'test.csv'); - const result = await ingAustria.match(file); - expect(result).toBe(false); - }); - }); + const file = new File([], 'test.csv') + const result = await ingAustria.match(file) + expect(result).toBe(false) + }) + }) describe('Parser', () => { it('should parse data correctly', async () => { - const file = new File([content], 'test.csv'); - const result = await ingAustria.parse(file); - expect(result).toEqual(ynabResult); - }); - }); + const file = new File([content], 'test.csv') + const result = await ingAustria.parse(file) + expect(result).toEqual(ynabResult) + }) + }) describe('Date Converter', () => { it('should format an input date correctly', () => { - expect(generateYnabDate('03.05.2018')).toEqual('05/03/2018'); - }); + expect(generateYnabDate('03.05.2018')).toEqual('05/03/2018') + }) it('should throw an error when the input date is incorrect', () => { - expect(() => generateYnabDate('1.1.1')).toThrow('not a valid date'); - }); - }); -}); + expect(() => generateYnabDate('1.1.1')).toThrow('not a valid date') + }) + }) +}) diff --git a/packages/ynap-parsers/src/at/ing/ing-austria.ts b/packages/ynap-parsers/src/at/ing/ing-austria.ts index 1c7bae04c..17fc14c59 100644 --- a/packages/ynap-parsers/src/at/ing/ing-austria.ts +++ b/packages/ynap-parsers/src/at/ing/ing-austria.ts @@ -1,48 +1,52 @@ -import 'mdn-polyfills/String.prototype.startsWith'; -import { ParserFunction, MatcherFunction, ParserModule } from '../..'; -import { parse } from '../../util/papaparse'; -import { readEncodedFile } from '../../util/read-encoded-file'; +import 'mdn-polyfills/String.prototype.startsWith' +import { ParserFunction, MatcherFunction, ParserModule } from '../..' +import { parse } from '../../util/papaparse' +import { readEncodedFile } from '../../util/read-encoded-file' export interface IngAustriaRow { - IBAN: string; - Text: string; - Valutadatum: string; - Währung: string; - Soll: string; - Haben: string; + IBAN: string + Text: string + Valutadatum: string + Währung: string + Soll: string + Haben: string } export const generateYnabDate = (input: string) => { - const match = input.match(/(\d{2})\.(\d{2})\.(\d{4})/); + const match = input.match(/(\d{2})\.(\d{2})\.(\d{4})/) if (!match) { - throw new Error('The input is not a valid date. Expected format: YYYY.MM.DD'); + throw new Error( + 'The input is not a valid date. Expected format: YYYY.MM.DD' + ) } - const [, day, month, year] = match; - return [month.padStart(2, '0'), day.padStart(2, '0'), year].join('/'); -}; + const [, day, month, year] = match + return [month.padStart(2, '0'), day.padStart(2, '0'), year].join('/') +} -export const parseNumber = (input: string) => Number(input.replace(',', '.')); +export const parseNumber = (input: string) => Number(input.replace(',', '.')) export const ingAustriaParser: ParserFunction = async (file: File) => { - const fileString = await readEncodedFile(file); - const { data } = await parse(fileString, { header: true, delimiter: ';' }); + const fileString = await readEncodedFile(file) + const { data } = await parse(fileString, { header: true, delimiter: ';' }) return [ { data: (data as IngAustriaRow[]) - .filter((r) => r.Valutadatum && (r.Soll || r.Haben)) - .map((r) => ({ + .filter(r => r.Valutadatum && (r.Soll || r.Haben)) + .map(r => ({ Date: generateYnabDate(r.Valutadatum), Payee: r.Text, Memo: undefined, - Outflow: r.Soll != '0,00' ? parseNumber(r.Soll).toFixed(2) : undefined, - Inflow: r.Haben != '0,00' ? parseNumber(r.Haben).toFixed(2) : undefined, + Outflow: + r.Soll != '0,00' ? parseNumber(r.Soll).toFixed(2) : undefined, + Inflow: + r.Haben != '0,00' ? parseNumber(r.Haben).toFixed(2) : undefined, })), }, - ]; -}; + ] +} export const ingAustriaMatcher: MatcherFunction = async (file: File) => { const requiredKeys: (keyof IngAustriaRow)[] = [ @@ -52,36 +56,36 @@ export const ingAustriaMatcher: MatcherFunction = async (file: File) => { 'Währung', 'Soll', 'Haben', - ]; + ] - const rawFileString = await readEncodedFile(file); + const rawFileString = await readEncodedFile(file) if (rawFileString.startsWith('IBAN;Text;Valutadatum;Währung;Soll;Haben')) { - return true; + return true } try { const { data } = await parse(rawFileString, { header: true, delimiter: ';', - }); + }) if (data.length === 0) { - return false; + return false } - const keys = Object.keys(data[0]); - const missingKeys = requiredKeys.filter((k) => !keys.includes(k)); + const keys = Object.keys(data[0]) + const missingKeys = requiredKeys.filter(k => !keys.includes(k)) if (missingKeys.length === 0) { - return true; + return true } } catch (e) { - return false; + return false } - return false; -}; + return false +} export const ingAustria: ParserModule = { name: 'ING Austria', @@ -91,4 +95,4 @@ export const ingAustria: ParserModule = { link: 'https://www.ing.at/', match: ingAustriaMatcher, parse: ingAustriaParser, -}; +} diff --git a/packages/ynap-parsers/src/bank2ynab/bank2ynab.spec.ts b/packages/ynap-parsers/src/bank2ynab/bank2ynab.spec.ts index 2e18e9fcb..3a8e44695 100644 --- a/packages/ynap-parsers/src/bank2ynab/bank2ynab.spec.ts +++ b/packages/ynap-parsers/src/bank2ynab/bank2ynab.spec.ts @@ -1,36 +1,36 @@ -import { calculateInflow, calculateOutflow, parseNumber } from './bank2ynab'; +import { calculateInflow, calculateOutflow, parseNumber } from './bank2ynab' describe('bank2ynab Parser Module', () => { describe('Number Parser', () => { it('should parse numbers in different formats correctly', () => { - expect(parseNumber('10,00')).toBe(10); - expect(parseNumber('12.50 ')).toBe(12.5); - }); + expect(parseNumber('10,00')).toBe(10) + expect(parseNumber('12.50 ')).toBe(12.5) + }) it('should return NaN when a number is invalid', () => { - expect(parseNumber('test')).toBeNaN(); - }); - }); + expect(parseNumber('test')).toBeNaN() + }) + }) describe('Inflow Parser', () => { it('should parse inflow correctly', () => { - expect(calculateInflow(undefined, undefined)).toBeUndefined(); - expect(calculateInflow(10, undefined)).toBe(10); - expect(calculateInflow(0, undefined)).toBe(0); - expect(calculateInflow(-10, undefined)).toBeUndefined(); - expect(() => calculateInflow(10, 20)).toThrow(); - expect(calculateInflow(undefined, -20)).toBe(20); - }); - }); + expect(calculateInflow(undefined, undefined)).toBeUndefined() + expect(calculateInflow(10, undefined)).toBe(10) + expect(calculateInflow(0, undefined)).toBe(0) + expect(calculateInflow(-10, undefined)).toBeUndefined() + expect(() => calculateInflow(10, 20)).toThrow() + expect(calculateInflow(undefined, -20)).toBe(20) + }) + }) describe('Outflow Parser', () => { it('should parse outflow correctly', () => { - expect(calculateOutflow(undefined, undefined)).toBeUndefined(); - expect(calculateOutflow(undefined, 10)).toBe(10); - expect(calculateOutflow(undefined, 0)).toBe(0); - expect(calculateOutflow(undefined, -10)).toBeUndefined(); - expect(() => calculateOutflow(10, 20)).toThrow(); - expect(calculateOutflow(-20, undefined)).toBe(20); - }); - }); -}); + expect(calculateOutflow(undefined, undefined)).toBeUndefined() + expect(calculateOutflow(undefined, 10)).toBe(10) + expect(calculateOutflow(undefined, 0)).toBe(0) + expect(calculateOutflow(undefined, -10)).toBeUndefined() + expect(() => calculateOutflow(10, 20)).toThrow() + expect(calculateOutflow(-20, undefined)).toBe(20) + }) + }) +}) diff --git a/packages/ynap-parsers/src/bank2ynab/bank2ynab.ts b/packages/ynap-parsers/src/bank2ynab/bank2ynab.ts index 6348f1054..8871f56e3 100644 --- a/packages/ynap-parsers/src/bank2ynab/bank2ynab.ts +++ b/packages/ynap-parsers/src/bank2ynab/bank2ynab.ts @@ -1,66 +1,66 @@ -import 'mdn-polyfills/String.prototype.startsWith'; -import { ParserFunction, MatcherFunction, ParserModule, YnabRow } from '..'; -import { parse as parseCsv } from '../util/papaparse'; -import { readEncodedFile } from '../util/read-encoded-file'; -import { parseDate, ynabDate } from './parse-date'; -import { ParserConfig } from '@envelope-zero/ynap-bank2ynab-converter/parserconfig'; +import 'mdn-polyfills/String.prototype.startsWith' +import { ParserFunction, MatcherFunction, ParserModule, YnabRow } from '..' +import { parse as parseCsv } from '../util/papaparse' +import { readEncodedFile } from '../util/read-encoded-file' +import { parseDate, ynabDate } from './parse-date' +import { ParserConfig } from '@envelope-zero/ynap-bank2ynab-converter/parserconfig' -import banks from './banks.json'; +import banks from './banks.json' export const parseNumber = (input?: string) => { if (typeof input === 'undefined') { - return undefined; + return undefined } // Return 0 for an empty string since this is an empty field if (!input.trim()) { - return 0; + return 0 } // Remove all superfluous characters - let cleaned = input.replace(/[^\d.,-]/g, ''); + let cleaned = input.replace(/[^\d.,-]/g, '') if (cleaned == '') { - return NaN; + return NaN } // If the cleaned string contains both "," and ".", it has // one or more thousands separators AND a decimal separator. // Determine the decimal separator and remove the thousands separator - if ([',', '.'].every((term) => cleaned.includes(term))) { - const comma = cleaned.indexOf(','); - const dot = cleaned.indexOf('.'); + if ([',', '.'].every(term => cleaned.includes(term))) { + const comma = cleaned.indexOf(',') + const dot = cleaned.indexOf('.') if (dot > comma) { // If "." is used as decimal separator, remove the thousands separators - cleaned = cleaned.replace(',', ''); + cleaned = cleaned.replace(',', '') } else { // If "," is used as decimal separator, remove the "." as thousands separator - cleaned = cleaned.replace('.', ''); + cleaned = cleaned.replace('.', '') } } // Replace "," as decimal separator with "." - cleaned = cleaned.replace(',', '.'); + cleaned = cleaned.replace(',', '.') // Try to cast the string to a number try { - return Number(cleaned); + return Number(cleaned) } catch (e) { - return undefined; + return undefined } -}; +} export const calculateInflow = (inflow?: number, outflow?: number) => { if (typeof inflow === 'undefined' && typeof outflow === 'undefined') { - return undefined; + return undefined } if (typeof inflow !== 'undefined' && typeof outflow === 'undefined') { - return inflow < 0 ? undefined : inflow; + return inflow < 0 ? undefined : inflow } if (typeof inflow === 'undefined' && typeof outflow !== 'undefined') { - return outflow < 0 ? -outflow : undefined; + return outflow < 0 ? -outflow : undefined } if ( @@ -68,21 +68,21 @@ export const calculateInflow = (inflow?: number, outflow?: number) => { typeof outflow !== 'undefined' && !(outflow === 0 || inflow === 0) ) { - throw new Error("Inflow and outflow can't be set simultaneously"); + throw new Error("Inflow and outflow can't be set simultaneously") } -}; +} export const calculateOutflow = (inflow?: number, outflow?: number) => { if (typeof outflow === 'undefined' && typeof inflow === 'undefined') { - return undefined; + return undefined } if (typeof outflow !== 'undefined' && typeof inflow === 'undefined') { - return outflow < 0 ? undefined : outflow; + return outflow < 0 ? undefined : outflow } if (typeof outflow === 'undefined' && typeof inflow !== 'undefined') { - return inflow < 0 ? -inflow : undefined; + return inflow < 0 ? -inflow : undefined } if ( @@ -90,88 +90,89 @@ export const calculateOutflow = (inflow?: number, outflow?: number) => { typeof inflow !== 'undefined' && !(outflow === 0 || inflow === 0) ) { - throw new Error("Inflow and outflow can't be set simultaneously"); + throw new Error("Inflow and outflow can't be set simultaneously") } -}; +} export const generateParser = (config: ParserConfig) => { const columns = config.inputColumns.reduce( (acc, cur, index) => { if (cur === 'skip') { - return acc; + return acc } return { ...acc, [cur]: index, - }; + } }, - {} as { [k in keyof (YnabRow & { CDFlag?: string })]: number }, - ); + {} as { [k in keyof (YnabRow & { CDFlag?: string })]: number } + ) - const hasCol = (name: keyof typeof columns) => Object.keys(columns).includes(name); + const hasCol = (name: keyof typeof columns) => + Object.keys(columns).includes(name) const match: MatcherFunction = async (file: File) => { - const content = await readEncodedFile(file); - const { data } = await parseCsv(content.trim()); + const content = await readEncodedFile(file) + const { data } = await parseCsv(content.trim()) - const match = file.name.match(new RegExp(`^${config.filenamePattern}`)); + const match = file.name.match(new RegExp(`^${config.filenamePattern}`)) if (!match) { - return false; + return false } // Check that enough columns exist // Skip header rows in length check - const headerRow = config.headerRows > 0 ? config.headerRows - 1 : 0; + const headerRow = config.headerRows > 0 ? config.headerRows - 1 : 0 if ( data.length <= headerRow || data[headerRow].length < config.inputColumns.length ) { - return false; + return false } // Get all rows after the header row, filter empty lines, then use the first row of that - const row = data.slice(config.headerRows).filter((d) => d.length > 1)[0]; + const row = data.slice(config.headerRows).filter(d => d.length > 1)[0] // Check that the date column is set correctly try { if (!parseDate(row[columns.Date], config.dateFormat)) { - return false; + return false } } catch (e) { - return false; + return false } // Check that the payee column is not a date try { if (columns.Payee && parseDate(row[columns.Payee], config.dateFormat)) { - return false; + return false } } catch (e) {} // Check that the inflow column is set correctly, if it exists if (columns.Inflow && isNaN(parseNumber(row[columns.Inflow]))) { - return false; + return false } // Check that the outflow column is set correctly, if it exists if (columns.Outflow && isNaN(parseNumber(row[columns.Outflow]))) { - return false; + return false } - return true; - }; + return true + } const parse: ParserFunction = async (file: File) => { - const content = await readEncodedFile(file); - const { data } = await parseCsv(content.trim()); + const content = await readEncodedFile(file) + const { data } = await parseCsv(content.trim()) const ynabData = data .slice(config.headerRows, data.length - config.footerRows) - .filter((d) => d.length > 1) + .filter(d => d.length > 1) .map( - (d) => + d => ({ Category: hasCol('Category') ? d[columns.Category] : undefined, Payee: hasCol('Payee') ? d[columns.Payee] : undefined, @@ -191,23 +192,31 @@ export const generateParser = (config: ParserConfig) => { } : { Inflow: calculateInflow( - hasCol('Inflow') ? parseNumber(d[columns.Inflow]) : undefined, - hasCol('Outflow') ? parseNumber(d[columns.Outflow]) : undefined, + hasCol('Inflow') + ? parseNumber(d[columns.Inflow]) + : undefined, + hasCol('Outflow') + ? parseNumber(d[columns.Outflow]) + : undefined ), Outflow: calculateOutflow( - hasCol('Inflow') ? parseNumber(d[columns.Inflow]) : undefined, - hasCol('Outflow') ? parseNumber(d[columns.Outflow]) : undefined, + hasCol('Inflow') + ? parseNumber(d[columns.Inflow]) + : undefined, + hasCol('Outflow') + ? parseNumber(d[columns.Outflow]) + : undefined ), }), - }) as YnabRow, - ); + }) as YnabRow + ) return [ { data: ynabData, }, - ]; - }; + ] + } return { name: config.name, @@ -217,10 +226,10 @@ export const generateParser = (config: ParserConfig) => { fileExtension: config.filenameExtension || 'csv', match, parse, - } as ParserModule; -}; + } as ParserModule +} -const ignorelist = ['de N26', 'de ING-DiBa', 'ie N26']; +const ignorelist = ['de N26', 'de ING-DiBa', 'ie N26'] export const bank2ynab = banks - .filter((b) => !ignorelist.includes(`${b.country} ${b.name}`)) - .map((bank) => generateParser(bank as ParserConfig)); + .filter(b => !ignorelist.includes(`${b.country} ${b.name}`)) + .map(bank => generateParser(bank as ParserConfig)) diff --git a/packages/ynap-parsers/src/bank2ynab/banks.json b/packages/ynap-parsers/src/bank2ynab/banks.json index e28b22ba2..08265593e 100644 --- a/packages/ynap-parsers/src/bank2ynab/banks.json +++ b/packages/ynap-parsers/src/bank2ynab/banks.json @@ -332,7 +332,15 @@ "country": "ch", "filenamePattern": "^Erweiterte Suche - Kontoauszug, Buchungen \\d{14}\\.csv", "filenameExtension": "csv", - "inputColumns": ["Date", "Payee", "skip", "skip", "skip", "Outflow", "Inflow"], + "inputColumns": [ + "Date", + "Payee", + "skip", + "skip", + "skip", + "Outflow", + "Inflow" + ], "link": "https://github.com/bank2ynab/bank2ynab/blob/develop/bank2ynab.conf#L248", "dateFormat": "%d.%m.%Y", "headerRows": 1, @@ -343,7 +351,15 @@ "country": "ch", "filenamePattern": "^Finanzassistent-Chronik\\.csv", "filenameExtension": "csv", - "inputColumns": ["Date", "Category", "skip", "Inflow", "Memo", "Payee", "skip"], + "inputColumns": [ + "Date", + "Category", + "skip", + "Inflow", + "Memo", + "Payee", + "skip" + ], "link": "https://github.com/bank2ynab/bank2ynab/blob/develop/bank2ynab.conf#L257", "dateFormat": "%Y-%m-%d", "headerRows": 1, @@ -1100,7 +1116,15 @@ "country": "it", "filenamePattern": "listamovimenticsv[0-9]{2}_[0-9]{2}_20[0-9]{2}_[0-9]{2}_[0-9]{2}_[0-9]{2}\\.csv", "filenameExtension": "csv", - "inputColumns": ["Date", "skip", "Outflow", "Inflow", "skip", "Memo", "skip"], + "inputColumns": [ + "Date", + "skip", + "Outflow", + "Inflow", + "skip", + "Memo", + "skip" + ], "link": "https://github.com/bank2ynab/bank2ynab/blob/develop/bank2ynab.conf#L575", "dateFormat": "%d/%m/%Y", "headerRows": 1, @@ -1418,7 +1442,15 @@ "country": "no", "filenamePattern": "OversiktKonti-\\d{2}.\\d{2}.\\d{4}-\\d{2}.\\d{2}.\\d{4}\\.csv", "filenameExtension": "csv", - "inputColumns": ["Date", "Payee", "skip", "Inflow", "Outflow", "skip", "skip"], + "inputColumns": [ + "Date", + "Payee", + "skip", + "Inflow", + "Outflow", + "skip", + "skip" + ], "link": "https://github.com/bank2ynab/bank2ynab/blob/develop/bank2ynab.conf#L717", "dateFormat": "%d.%m.%Y", "headerRows": 1, @@ -1654,7 +1686,15 @@ "country": "sg", "filenamePattern": "[0-9a-z]{32}.[A-Z][0-9]{15}\\.csv", "filenameExtension": "csv", - "inputColumns": ["Date", "skip", "Outflow", "Inflow", "Payee", "Memo", "skip"], + "inputColumns": [ + "Date", + "skip", + "Outflow", + "Inflow", + "Payee", + "Memo", + "skip" + ], "link": "https://github.com/bank2ynab/bank2ynab/blob/develop/bank2ynab.conf#L882", "dateFormat": "%d %b %Y", "headerRows": 0, @@ -1893,7 +1933,15 @@ "country": "us", "filenamePattern": "Source Filename Pattern = XXXXXX[0-9]{6}_(Checking)_Transactions_[0-9]{8}-[0-9]{6}\\.csv", "filenameExtension": "csv", - "inputColumns": ["Date", "skip", "skip", "Payee", "Outflow", "Inflow", "skip"], + "inputColumns": [ + "Date", + "skip", + "skip", + "Payee", + "Outflow", + "Inflow", + "skip" + ], "link": "https://github.com/bank2ynab/bank2ynab/blob/develop/bank2ynab.conf#L1008", "dateFormat": "%m/%d/%Y", "headerRows": 3, @@ -1904,7 +1952,15 @@ "country": "us", "filenamePattern": "Source Filename Pattern = XXXXXX[0-9]{6}_(Savings)_Transactions_[0-9]{8}-[0-9]{6}\\.csv", "filenameExtension": "csv", - "inputColumns": ["Date", "skip", "skip", "Payee", "Outflow", "Inflow", "skip"], + "inputColumns": [ + "Date", + "skip", + "skip", + "Payee", + "Outflow", + "Inflow", + "skip" + ], "link": "https://github.com/bank2ynab/bank2ynab/blob/develop/bank2ynab.conf#L1016", "dateFormat": "%m/%d/%Y", "headerRows": 3, diff --git a/packages/ynap-parsers/src/bank2ynab/parse-date.spec.ts b/packages/ynap-parsers/src/bank2ynab/parse-date.spec.ts index 92c901c8b..1543b0a0e 100644 --- a/packages/ynap-parsers/src/bank2ynab/parse-date.spec.ts +++ b/packages/ynap-parsers/src/bank2ynab/parse-date.spec.ts @@ -1,28 +1,28 @@ -import { parseDate, ynabDate } from './parse-date'; -import { format } from 'date-fns'; +import { parseDate, ynabDate } from './parse-date' +import { format } from 'date-fns' describe('bank2ynab Date Parser', () => { it('should parse dates correctly', () => { - const result1 = parseDate('20.12.2018', '%d.%m.%Y'); - expect(format(result1, 'MM/dd/yyyy')).toBe('12/20/2018'); + const result1 = parseDate('20.12.2018', '%d.%m.%Y') + expect(format(result1, 'MM/dd/yyyy')).toBe('12/20/2018') - const result2 = parseDate('20/12/2018', '%d/%m/%Y'); - expect(format(result2, 'MM/dd/yyyy')).toBe('12/20/2018'); + const result2 = parseDate('20/12/2018', '%d/%m/%Y') + expect(format(result2, 'MM/dd/yyyy')).toBe('12/20/2018') - const result3 = parseDate('10 Feb 18', '%d %b %y'); - expect(format(result3, 'MM/dd/yyyy')).toBe('02/10/2018'); + const result3 = parseDate('10 Feb 18', '%d %b %y') + expect(format(result3, 'MM/dd/yyyy')).toBe('02/10/2018') - const result4 = parseDate('10 Feb 18', '%d %b %y'); - expect(format(result4, 'MM/dd/yyyy')).toBe('02/10/2018'); + const result4 = parseDate('10 Feb 18', '%d %b %y') + expect(format(result4, 'MM/dd/yyyy')).toBe('02/10/2018') - const result5 = parseDate('2019-03-04'); - expect(format(result5, 'MM/dd/yyyy')).toBe('03/04/2019'); - }); -}); + const result5 = parseDate('2019-03-04') + expect(format(result5, 'MM/dd/yyyy')).toBe('03/04/2019') + }) +}) describe('bank2ynab YNAB Date Formatter', () => { it('should format dates correctly according to the YNAB format', () => { - const result1 = ynabDate(new Date('2018-03-18')); - expect(result1).toBe('03/18/2018'); - }); -}); + const result1 = ynabDate(new Date('2018-03-18')) + expect(result1).toBe('03/18/2018') + }) +}) diff --git a/packages/ynap-parsers/src/bank2ynab/parse-date.ts b/packages/ynap-parsers/src/bank2ynab/parse-date.ts index 4f9445197..36163e795 100644 --- a/packages/ynap-parsers/src/bank2ynab/parse-date.ts +++ b/packages/ynap-parsers/src/bank2ynab/parse-date.ts @@ -1,4 +1,4 @@ -import { parse, format } from 'date-fns'; +import { parse, format } from 'date-fns' // See https://github.com/bank2ynab/bank2ynab/wiki/DateFormatting#dates-in-data-rows export const placeholders: { [k: string]: string } = { @@ -10,27 +10,27 @@ export const placeholders: { [k: string]: string } = { '%H': 'HH', // 2-digit hour (24h) '%M': 'mm', // 2-digit minutes '%S': 'ss', // 2-digit seconds -}; +} const ensureValidity = (date: Date, input: string) => { if (isNaN(date.getTime())) { - throw new Error(`${input} is not a valid date.`); + throw new Error(`${input} is not a valid date.`) } - return date; -}; + return date +} export const parseDate = (input: string, format?: string) => { if (!format) { - return ensureValidity(new Date(Date.parse(input)), input); + return ensureValidity(new Date(Date.parse(input)), input) } const convertedFormat = Object.keys(placeholders).reduce( (acc, cur) => acc.replace(cur, placeholders[cur]), - format, - ); + format + ) - return ensureValidity(parse(input, convertedFormat, new Date()), input); -}; + return ensureValidity(parse(input, convertedFormat, new Date()), input) +} -export const ynabDate = (input: number | Date) => format(input, 'MM/dd/yyyy'); +export const ynabDate = (input: number | Date) => format(input, 'MM/dd/yyyy') diff --git a/packages/ynap-parsers/src/de/1822direkt/1822direkt.spec.ts b/packages/ynap-parsers/src/de/1822direkt/1822direkt.spec.ts index cf6a95b37..d4db3fa9f 100644 --- a/packages/ynap-parsers/src/de/1822direkt/1822direkt.spec.ts +++ b/packages/ynap-parsers/src/de/1822direkt/1822direkt.spec.ts @@ -1,11 +1,11 @@ -import { generateYnabDate, _1822direkt } from './1822direkt'; -import fs from 'fs'; -import path from 'path'; -import { YnabFile } from '../..'; +import { generateYnabDate, _1822direkt } from './1822direkt' +import fs from 'fs' +import path from 'path' +import { YnabFile } from '../..' const content = fs.readFileSync( - path.join(__dirname, 'test-data', 'umsaetze-12345678-25.03.2020_11_45.csv'), -); + path.join(__dirname, 'test-data', 'umsaetze-12345678-25.03.2020_11_45.csv') +) const ynabResult: YnabFile[] = [ { @@ -34,44 +34,44 @@ const ynabResult: YnabFile[] = [ }, ], }, -]; +] describe('1822direkt Parser Module', () => { describe('Matcher', () => { it('should match 1822direkt files by file name', async () => { - const fileName = 'umsaetze-12345678-25.03.2020_11_45.csv'; - const result = !!fileName.match(_1822direkt.filenamePattern); - expect(result).toBe(true); - }); + const fileName = 'umsaetze-12345678-25.03.2020_11_45.csv' + const result = !!fileName.match(_1822direkt.filenamePattern) + expect(result).toBe(true) + }) it('should not match other files by file name', async () => { - const invalidFile = new File([], 'test.csv'); - const result = await _1822direkt.match(invalidFile); - expect(result).toBe(false); - }); + const invalidFile = new File([], 'test.csv') + const result = await _1822direkt.match(invalidFile) + expect(result).toBe(false) + }) it('should match 1822direkt files by fields', async () => { - const file = new File([content], 'test.csv'); - const result = await _1822direkt.match(file); - expect(result).toBe(true); - }); - }); + const file = new File([content], 'test.csv') + const result = await _1822direkt.match(file) + expect(result).toBe(true) + }) + }) describe('Parser', () => { it('should parse data correctly', async () => { - const file = new File([content], 'test.csv'); - const result = await _1822direkt.parse(file); - expect(result).toEqual(ynabResult); - }); - }); + const file = new File([content], 'test.csv') + const result = await _1822direkt.parse(file) + expect(result).toEqual(ynabResult) + }) + }) describe('Date Converter', () => { it('should format an input date correctly', () => { - expect(generateYnabDate('03.05.2018')).toEqual('05/03/2018'); - }); + expect(generateYnabDate('03.05.2018')).toEqual('05/03/2018') + }) it('should throw an error when the input date is incorrect', () => { - expect(() => generateYnabDate('1.1.1')).toThrow('not a valid date'); - }); - }); -}); + expect(() => generateYnabDate('1.1.1')).toThrow('not a valid date') + }) + }) +}) diff --git a/packages/ynap-parsers/src/de/1822direkt/1822direkt.ts b/packages/ynap-parsers/src/de/1822direkt/1822direkt.ts index eadb81801..404c12e31 100644 --- a/packages/ynap-parsers/src/de/1822direkt/1822direkt.ts +++ b/packages/ynap-parsers/src/de/1822direkt/1822direkt.ts @@ -1,56 +1,58 @@ -import 'mdn-polyfills/String.prototype.startsWith'; -import { ParserFunction, MatcherFunction, ParserModule } from '../..'; -import { parse } from '../../util/papaparse'; -import { readEncodedFile } from '../../util/read-encoded-file'; +import 'mdn-polyfills/String.prototype.startsWith' +import { ParserFunction, MatcherFunction, ParserModule } from '../..' +import { parse } from '../../util/papaparse' +import { readEncodedFile } from '../../util/read-encoded-file' export interface Row { - Kontonummer: string; - 'Datum/Zeit': string; - Buchungstag: string; - Wertstellung: string; - 'Soll/Haben': string; - Buchungsschlüssel: string; - Buchungsart: string; - 'Empfänger/Auftraggeber Name': string; - 'Empfänger/Auftraggeber IBAN': string; - 'Empfänger/Auftraggeber BIC': string; - 'Glaeubiger-ID': string; - Mandatsreferenz: string; - Mandatsdatum: string; - 'Vwz.0': string; - 'Vwz.1': string; - 'Vwz.2': string; - 'Vwz.3': string; - 'Vwz.4': string; - 'Vwz.5': string; - 'Vwz.6': string; - 'Vwz.7': string; - 'Vwz.8': string; - 'Vwz.9': string; - 'Vwz.10': string; - 'Vwz.11': string; - 'Vwz.12': string; - 'Vwz.13': string; - 'Vwz.14': string; - 'Vwz.15': string; - 'Vwz.16': string; - 'Vwz.17': string; - 'End-to-End-Identifikation': string; + Kontonummer: string + 'Datum/Zeit': string + Buchungstag: string + Wertstellung: string + 'Soll/Haben': string + Buchungsschlüssel: string + Buchungsart: string + 'Empfänger/Auftraggeber Name': string + 'Empfänger/Auftraggeber IBAN': string + 'Empfänger/Auftraggeber BIC': string + 'Glaeubiger-ID': string + Mandatsreferenz: string + Mandatsdatum: string + 'Vwz.0': string + 'Vwz.1': string + 'Vwz.2': string + 'Vwz.3': string + 'Vwz.4': string + 'Vwz.5': string + 'Vwz.6': string + 'Vwz.7': string + 'Vwz.8': string + 'Vwz.9': string + 'Vwz.10': string + 'Vwz.11': string + 'Vwz.12': string + 'Vwz.13': string + 'Vwz.14': string + 'Vwz.15': string + 'Vwz.16': string + 'Vwz.17': string + 'End-to-End-Identifikation': string } export const generateYnabDate = (input: string) => { - const match = input.match(/(\d{2})\.(\d{2})\.(\d{4})/); + const match = input.match(/(\d{2})\.(\d{2})\.(\d{4})/) if (!match) { - throw new Error('The input is not a valid date. Expected format: YYYY-MM-DD'); + throw new Error( + 'The input is not a valid date. Expected format: YYYY-MM-DD' + ) } - const [, day, month, year] = match; - return [month.padStart(2, '0'), day.padStart(2, '0'), year].join('/'); -}; + const [, day, month, year] = match + return [month.padStart(2, '0'), day.padStart(2, '0'), year].join('/') +} export const parseNumber = (input: string) => - Number(input.replace(/\./g, '').replace(',', '.')); + Number(input.replace(/\./g, '').replace(',', '.')) export const getMergedMemo = (r: Row) => [ @@ -76,20 +78,20 @@ export const getMergedMemo = (r: Row) => .filter(Boolean) // When the string is 35 characters long, it's likely to overflow into the next // field, so we don't add a space. Otherwise, we add a space for separation. - .map((s) => (s.length >= 35 ? s : s + ' ')) + .map(s => (s.length >= 35 ? s : s + ' ')) .join('') - .trim(); + .trim() export const _1822direktParser: ParserFunction = async (file: File) => { - const fileString = await readEncodedFile(file, 'win1252'); - const { data }: { data: Row[] } = await parse(fileString, { header: true }); + const fileString = await readEncodedFile(file, 'win1252') + const { data }: { data: Row[] } = await parse(fileString, { header: true }) return [ { accountName: String(data[0]?.Kontonummer), data: (data as Row[]) - .filter((r) => r.Buchungstag && r['Soll/Haben']) - .map((r) => ({ + .filter(r => r.Buchungstag && r['Soll/Haben']) + .map(r => ({ Date: generateYnabDate(r.Buchungstag), Payee: r['Empfänger/Auftraggeber Name'], Memo: getMergedMemo(r), @@ -103,8 +105,8 @@ export const _1822direktParser: ParserFunction = async (file: File) => { : undefined, })), }, - ]; -}; + ] +} export const _1822direktMatcher: MatcherFunction = async (file: File) => { const requiredKeys: (keyof Row)[] = [ @@ -112,41 +114,41 @@ export const _1822direktMatcher: MatcherFunction = async (file: File) => { 'Soll/Haben', 'Vwz.0', 'Empfänger/Auftraggeber Name', - ]; + ] - const rawFileString = await readEncodedFile(file, 'win1252'); + const rawFileString = await readEncodedFile(file, 'win1252') if ( rawFileString.startsWith( - 'Kontonummer;Datum/Zeit;Buchungstag;Wertstellung;Soll/Haben;Buchungsschlüssel;Buchungsart;Empfänger/Auftraggeber Name;Empfänger/Auftraggeber IBAN;Empfänger/Auftraggeber BIC;Glaeubiger-ID;', + 'Kontonummer;Datum/Zeit;Buchungstag;Wertstellung;Soll/Haben;Buchungsschlüssel;Buchungsart;Empfänger/Auftraggeber Name;Empfänger/Auftraggeber IBAN;Empfänger/Auftraggeber BIC;Glaeubiger-ID;' ) ) { - return true; + return true } if (rawFileString.length === 0) { - return false; + return false } try { - const { data } = await parse(rawFileString, { header: true }); + const { data } = await parse(rawFileString, { header: true }) if (data.length === 0) { - return false; + return false } - const keys = Object.keys(data[0]); - const missingKeys = requiredKeys.filter((k) => !keys.includes(k)); + const keys = Object.keys(data[0]) + const missingKeys = requiredKeys.filter(k => !keys.includes(k)) if (missingKeys.length === 0) { - return true; + return true } } catch (e) { - return false; + return false } - return false; -}; + return false +} export const _1822direkt: ParserModule = { name: '1822direkt', @@ -156,4 +158,4 @@ export const _1822direkt: ParserModule = { link: 'https://www.1822direkt.de/', match: _1822direktMatcher, parse: _1822direktParser, -}; +} diff --git a/packages/ynap-parsers/src/de/comdirect/comdirect.spec.ts b/packages/ynap-parsers/src/de/comdirect/comdirect.spec.ts index 415744893..4194ba1fb 100644 --- a/packages/ynap-parsers/src/de/comdirect/comdirect.spec.ts +++ b/packages/ynap-parsers/src/de/comdirect/comdirect.spec.ts @@ -3,9 +3,9 @@ import { comdirect, extractField, trimMetaData, -} from './comdirect'; -import { YnabFile } from '../..'; -import { encode, decode } from 'iconv-lite'; +} from './comdirect' +import { YnabFile } from '../..' +import { encode, decode } from 'iconv-lite' const content = encode( `; @@ -18,14 +18,14 @@ const content = encode( "01.04.2019";"01.04.2019";"DTA-glt. Buchung";" Zahlungspflichtiger: John DoeKto/IBAN: DE84100110012626835902 BLZ/BIC: NTSBDEB1XXX Buchungstext: Sparplan 1 Ref. H9219087I4644658/2 ";"180,00"; "09.05.2023";"09.05.2023";"Lastschrift / Belastung";"Auftraggeber: Some Supermarket Buchungstext: Some Supermarket KARTE 0000 12345678912 05052023 KDN-REF 000000000000 Ref. IY22312953149668/154221";"-7.191,19"; "Alter Kontostand";"16,89 EUR";`, - 'win1252', -); + 'win1252' +) const trimmedContent = `"Buchungstag";"Wertstellung (Valuta)";"Vorgang";"Buchungstext";"Umsatz in EUR"; "offen";"--";"Kartenverfügung";"Kto/IBAN: 000000000000 Buchungstext: NETFLIX.COM Berlin DE 2021-01-01T00:00:00 ";"-15,99"; "01.04.2019";"03.04.2019";"Wertpapiere";" Buchungstext: ISHSII-MSCI EUR.SRI EOACC WPKNR: A1H7ZS ISIN: IE00B52VJ196 Ref. 25F1909221559359/2 ";"-119,98"; "01.04.2019";"01.04.2019";"DTA-glt. Buchung";" Zahlungspflichtiger: John DoeKto/IBAN: DE84100110012626835902 BLZ/BIC: NTSBDEB1XXX Buchungstext: Sparplan 1 Ref. H9219087I4644658/2 ";"180,00"; -"09.05.2023";"09.05.2023";"Lastschrift / Belastung";"Auftraggeber: Some Supermarket Buchungstext: Some Supermarket KARTE 0000 12345678912 05052023 KDN-REF 000000000000 Ref. IY22312953149668/154221";"-7.191,19";`; +"09.05.2023";"09.05.2023";"Lastschrift / Belastung";"Auftraggeber: Some Supermarket Buchungstext: Some Supermarket KARTE 0000 12345678912 05052023 KDN-REF 000000000000 Ref. IY22312953149668/154221";"-7.191,19";` const ynabResult: YnabFile[] = [ { @@ -49,87 +49,89 @@ const ynabResult: YnabFile[] = [ }, ], }, -]; +] describe('comdirect Parser Module', () => { describe('Matcher', () => { it('should match comdirect files by file name', async () => { - const fileName = 'umsaetze_1182395441_20190403-2324.csv'; - const result = !!fileName.match(comdirect.filenamePattern); - expect(result).toBe(true); - }); + const fileName = 'umsaetze_1182395441_20190403-2324.csv' + const result = !!fileName.match(comdirect.filenamePattern) + expect(result).toBe(true) + }) it('should not match other files by file name', async () => { - const invalidFile = new File([], 'test.csv'); - const result = await comdirect.match(invalidFile); - expect(result).toBe(false); - }); + const invalidFile = new File([], 'test.csv') + const result = await comdirect.match(invalidFile) + expect(result).toBe(false) + }) it('should match comdirect files by fields', async () => { - const file = new File([content], 'test.csv'); - const result = await comdirect.match(file); - expect(result).toBe(true); - }); + const file = new File([content], 'test.csv') + const result = await comdirect.match(file) + expect(result).toBe(true) + }) it('should not match empty files', async () => { - const file = new File([], 'test.csv'); - const result = await comdirect.match(file); - expect(result).toBe(false); - }); - }); + const file = new File([], 'test.csv') + const result = await comdirect.match(file) + expect(result).toBe(false) + }) + }) describe('Parser', () => { it('should parse data correctly', async () => { - const file = new File([content], 'test.csv'); - const result = await comdirect.parse(file); - expect(result).toEqual(ynabResult); - }); - }); + const file = new File([content], 'test.csv') + const result = await comdirect.parse(file) + expect(result).toEqual(ynabResult) + }) + }) describe('Date Converter', () => { it('should format an input date correctly', () => { - expect(generateYnabDate('03.05.2018')).toEqual('05/03/2018'); - }); + expect(generateYnabDate('03.05.2018')).toEqual('05/03/2018') + }) it('should throw an error when the input date is incorrect', () => { - expect(() => generateYnabDate('1.1.1')).toThrow('not a valid date'); - }); - }); + expect(() => generateYnabDate('1.1.1')).toThrow('not a valid date') + }) + }) describe('Field Extractor', () => { const postingText1 = - ' Buchungstext: AMUNDI ETF MSCI WLD X EMU WPKNR: A0RPV6 ISIN: FR0010756114 Ref. 07F1909220100960/2 '; + ' Buchungstext: AMUNDI ETF MSCI WLD X EMU WPKNR: A0RPV6 ISIN: FR0010756114 Ref. 07F1909220100960/2 ' const postingText2 = - ' Zahlungspflichtiger: John DoeKto/IBAN: DE27100777770209299700 BLZ/BIC: NTSBDEB1XXX Buchungstext: Sparplan 1 Ref. H9219087I4642658/2 '; + ' Zahlungspflichtiger: John DoeKto/IBAN: DE27100777770209299700 BLZ/BIC: NTSBDEB1XXX Buchungstext: Sparplan 1 Ref. H9219087I4642658/2 ' it('should extract a given field from a posting text', () => { expect(extractField(postingText1, 'Buchungstext')).toEqual( - 'AMUNDI ETF MSCI WLD X EMU WPKNR: A0RPV6 ISIN: FR0010756114', - ); - expect(extractField(postingText1, 'Ref.')).toEqual('07F1909220100960/2'); - expect(extractField(postingText2, 'Zahlungspflichtiger')).toEqual('John Doe'); - expect(extractField(postingText2, 'Buchungstext')).toEqual('Sparplan 1'); + 'AMUNDI ETF MSCI WLD X EMU WPKNR: A0RPV6 ISIN: FR0010756114' + ) + expect(extractField(postingText1, 'Ref.')).toEqual('07F1909220100960/2') + expect(extractField(postingText2, 'Zahlungspflichtiger')).toEqual( + 'John Doe' + ) + expect(extractField(postingText2, 'Buchungstext')).toEqual('Sparplan 1') expect(extractField(postingText2, 'Kto/IBAN')).toEqual( - 'DE27100777770209299700', - ); - }); + 'DE27100777770209299700' + ) + }) it("should return undefined when a field doesn't exist", () => { - expect(extractField(postingText1, 'Zahlungspflichtiger')).toBeUndefined(); - expect(extractField(postingText1, 'Auftraggeber')).toBeUndefined(); - }); - }); + expect(extractField(postingText1, 'Zahlungspflichtiger')).toBeUndefined() + expect(extractField(postingText1, 'Auftraggeber')).toBeUndefined() + }) + }) describe('Metadata Trimmer', () => { it('should trim all metadata from a valid input string', () => { - expect(trimMetaData(decode(content, 'win1252'))).toEqual(trimmedContent); - }); + expect(trimMetaData(decode(content, 'win1252'))).toEqual(trimmedContent) + }) it('should throw an error when the input string is invalid', () => { expect(() => trimMetaData('invalid string')).toThrow( - 'file format is incorrect', - ); - }); - }); -}); + 'file format is incorrect' + ) + }) + }) +}) diff --git a/packages/ynap-parsers/src/de/comdirect/comdirect.ts b/packages/ynap-parsers/src/de/comdirect/comdirect.ts index a5a4e5cb2..58bf0f5dd 100644 --- a/packages/ynap-parsers/src/de/comdirect/comdirect.ts +++ b/packages/ynap-parsers/src/de/comdirect/comdirect.ts @@ -1,46 +1,48 @@ -import 'mdn-polyfills/String.prototype.startsWith'; -import { ParserFunction, MatcherFunction, ParserModule } from '../..'; -import { parse } from '../../util/papaparse'; -import { readEncodedFile } from '../../util/read-encoded-file'; +import 'mdn-polyfills/String.prototype.startsWith' +import { ParserFunction, MatcherFunction, ParserModule } from '../..' +import { parse } from '../../util/papaparse' +import { readEncodedFile } from '../../util/read-encoded-file' export interface ComdirectRow { - Buchungstag: string; - 'Wertstellung (Valuta)': string; - Vorgang: string; - Buchungstext: string; - 'Umsatz in EUR': string; + Buchungstag: string + 'Wertstellung (Valuta)': string + Vorgang: string + Buchungstext: string + 'Umsatz in EUR': string } export const generateYnabDate = (input: string) => { - const match = input.match(/(\d{2})\.(\d{2})\.(\d{4})/); + const match = input.match(/(\d{2})\.(\d{2})\.(\d{4})/) if (!match) { - throw new Error('The input is not a valid date. Expected format: YYYY-MM-DD'); + throw new Error( + 'The input is not a valid date. Expected format: YYYY-MM-DD' + ) } - const [, day, month, year] = match; - return [month.padStart(2, '0'), day.padStart(2, '0'), year].join('/'); -}; + const [, day, month, year] = match + return [month.padStart(2, '0'), day.padStart(2, '0'), year].join('/') +} // Replace '.' with '' to remove German format thousands separator, then // replace ',' with '.' to convert from German decimal separator to TS-style '.' export const parseNumber = (input: string) => - Number(input.replace('.', '').replace(',', '.')); + Number(input.replace('.', '').replace(',', '.')) export const trimMetaData = (input: string) => { - const beginning = input.indexOf('"Buchungstag"'); - const end = input.lastIndexOf('\n"Alter Kontostand"'); + const beginning = input.indexOf('"Buchungstag"') + const end = input.lastIndexOf('\n"Alter Kontostand"') if (beginning === -1 || end === -1) { throw new Error( - 'Metadata could not be trimmed because the file format is incorrect.', - ); + 'Metadata could not be trimmed because the file format is incorrect.' + ) } return input .substr(beginning, input.length - beginning - (input.length - end)) - .trim(); -}; + .trim() +} const postingTextFields = { Buchungstext: 'Buchungstext', @@ -50,47 +52,47 @@ const postingTextFields = { 'Kto/IBAN': 'Kto/IBAN', 'BLZ/BIC': 'BLZ/BIC', 'Ref.': 'Ref.', -}; +} export const extractField = ( postingText: string, - field: keyof typeof postingTextFields, + field: keyof typeof postingTextFields ) => { // First, split the input by field name // so we can remove everything before that. - const split1 = postingText.split(field); + const split1 = postingText.split(field) if (split1.length < 2) { // Field doesn't exist - return undefined; + return undefined } // Next, split the new string again by any possible // key so we can remove everything after that. const nextField = new RegExp( `(${Object.keys(postingTextFields) - .map((k) => k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')) + .map(k => k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')) .join('|')})`, - 'i', - ); - const rawContent = split1[1].split(nextField)[0]; + 'i' + ) + const rawContent = split1[1].split(nextField)[0] // Last, trim the content to remove any white spaces // or other residue from the previous operations. - return rawContent.replace(/(^[:.\s]+|\s+$)/g, ''); -}; + return rawContent.replace(/(^[:.\s]+|\s+$)/g, '') +} export const comdirectParser: ParserFunction = async (file: File) => { - const fileString = trimMetaData(await readEncodedFile(file)); - const { data } = await parse(fileString, { header: true }); + const fileString = trimMetaData(await readEncodedFile(file)) + const { data } = await parse(fileString, { header: true }) return [ { data: (data as ComdirectRow[]) .filter( - (r) => r.Buchungstag && r.Buchungstag != 'offen' && r['Umsatz in EUR'], + r => r.Buchungstag && r.Buchungstag != 'offen' && r['Umsatz in EUR'] ) - .map((r) => ({ + .map(r => ({ Date: generateYnabDate(r.Buchungstag), Payee: extractField(r.Buchungstext, 'Empfänger') || @@ -107,8 +109,8 @@ export const comdirectParser: ParserFunction = async (file: File) => { : undefined, })), }, - ]; -}; + ] +} export const comdirectMatcher: MatcherFunction = async (file: File) => { const requiredKeys: (keyof ComdirectRow)[] = [ @@ -117,37 +119,37 @@ export const comdirectMatcher: MatcherFunction = async (file: File) => { 'Buchungstext', 'Umsatz in EUR', 'Vorgang', - ]; + ] - const rawFileString = await readEncodedFile(file); + const rawFileString = await readEncodedFile(file) if (rawFileString.startsWith(';\n"Umsätze Verrechnungskonto')) { - return true; + return true } if (rawFileString.length === 0) { - return false; + return false } try { - const { data } = await parse(trimMetaData(rawFileString), { header: true }); + const { data } = await parse(trimMetaData(rawFileString), { header: true }) if (data.length === 0) { - return false; + return false } - const keys = Object.keys(data[0]); - const missingKeys = requiredKeys.filter((k) => !keys.includes(k)); + const keys = Object.keys(data[0]) + const missingKeys = requiredKeys.filter(k => !keys.includes(k)) if (missingKeys.length === 0) { - return true; + return true } } catch (e) { - return false; + return false } - return false; -}; + return false +} export const comdirect: ParserModule = { name: 'comdirect', @@ -157,4 +159,4 @@ export const comdirect: ParserModule = { link: 'https://www.comdirect.de', match: comdirectMatcher, parse: comdirectParser, -}; +} diff --git a/packages/ynap-parsers/src/de/ing-diba/ing-diba.spec.ts b/packages/ynap-parsers/src/de/ing-diba/ing-diba.spec.ts index ae3e2f174..2246313d3 100644 --- a/packages/ynap-parsers/src/de/ing-diba/ing-diba.spec.ts +++ b/packages/ynap-parsers/src/de/ing-diba/ing-diba.spec.ts @@ -1,6 +1,6 @@ -import { generateYnabDate, ingDiBa } from './ing-diba'; -import { YnabRow, YnabFile } from '../..'; -import { encode } from 'iconv-lite'; +import { generateYnabDate, ingDiBa } from './ing-diba' +import { YnabRow, YnabFile } from '../..' +import { encode } from 'iconv-lite' const content = encode( `Umsatzanzeige;Datei erstellt am: 03.04.2019 22:16 @@ -21,8 +21,8 @@ Buchung;Valuta;Auftraggeber/Empfänger;Buchungstext;Verwendungszweck;Betrag;Wäh 03.04.2019;03.04.2019;Income;Gehalt/Rente;MAERZ 2019;700,00;EUR 03.04.2020;03.04.2020;Income;Gehalt/Rente;MAERZ 2020;1.700,00;EUR 03.04.2020;03.04.2020;Thousand Euros gone;Lastschrift;Thank you;-1.000,00;EUR`, - 'win1252', -); + 'win1252' +) const ynabResult: YnabFile[] = [ { @@ -57,50 +57,50 @@ const ynabResult: YnabFile[] = [ }, ], }, -]; +] describe('ING-DiBa Parser Module', () => { describe('Matcher', () => { it('should match ING-DiBa files by file name', async () => { - const fileName = 'Umsatzanzeige_DE27100777770209299700_20190403.csv'; - const result = !!fileName.match(ingDiBa.filenamePattern); - expect(result).toBe(true); - }); + const fileName = 'Umsatzanzeige_DE27100777770209299700_20190403.csv' + const result = !!fileName.match(ingDiBa.filenamePattern) + expect(result).toBe(true) + }) it('should not match other files by file name', async () => { - const invalidFile = new File([], 'test.csv'); - const result = await ingDiBa.match(invalidFile); - expect(result).toBe(false); - }); + const invalidFile = new File([], 'test.csv') + const result = await ingDiBa.match(invalidFile) + expect(result).toBe(false) + }) it('should match ING-DiBa files by fields', async () => { - const file = new File([content], 'test.csv'); - const result = await ingDiBa.match(file); - expect(result).toBe(true); - }); + const file = new File([content], 'test.csv') + const result = await ingDiBa.match(file) + expect(result).toBe(true) + }) it('should not match empty files', async () => { - const file = new File([], 'test.csv'); - const result = await ingDiBa.match(file); - expect(result).toBe(false); - }); - }); + const file = new File([], 'test.csv') + const result = await ingDiBa.match(file) + expect(result).toBe(false) + }) + }) describe('Parser', () => { it('should parse data correctly', async () => { - const file = new File([content], 'test.csv'); - const result = await ingDiBa.parse(file); - expect(result).toEqual(ynabResult); - }); - }); + const file = new File([content], 'test.csv') + const result = await ingDiBa.parse(file) + expect(result).toEqual(ynabResult) + }) + }) describe('Date Converter', () => { it('should format an input date correctly', () => { - expect(generateYnabDate('03.05.2018')).toEqual('05/03/2018'); - }); + expect(generateYnabDate('03.05.2018')).toEqual('05/03/2018') + }) it('should throw an error when the input date is incorrect', () => { - expect(() => generateYnabDate('1.1.1')).toThrow('not a valid date'); - }); - }); -}); + expect(() => generateYnabDate('1.1.1')).toThrow('not a valid date') + }) + }) +}) diff --git a/packages/ynap-parsers/src/de/ing-diba/ing-diba.ts b/packages/ynap-parsers/src/de/ing-diba/ing-diba.ts index 7f6b8a80e..d49334d06 100644 --- a/packages/ynap-parsers/src/de/ing-diba/ing-diba.ts +++ b/packages/ynap-parsers/src/de/ing-diba/ing-diba.ts @@ -1,44 +1,46 @@ -import 'mdn-polyfills/String.prototype.startsWith'; -import { ParserFunction, MatcherFunction, ParserModule } from '../..'; -import { parse } from '../../util/papaparse'; -import { readEncodedFile } from '../../util/read-encoded-file'; +import 'mdn-polyfills/String.prototype.startsWith' +import { ParserFunction, MatcherFunction, ParserModule } from '../..' +import { parse } from '../../util/papaparse' +import { readEncodedFile } from '../../util/read-encoded-file' export interface IngDiBaRow { - Buchung: string; - Valuta: string; - 'Auftraggeber/Empfänger': string; - Buchungstext: string; - Verwendungszweck: string; - Betrag: string; - Währung: string; + Buchung: string + Valuta: string + 'Auftraggeber/Empfänger': string + Buchungstext: string + Verwendungszweck: string + Betrag: string + Währung: string } export const generateYnabDate = (input: string) => { - const match = input.match(/(\d{2})\.(\d{2})\.(\d{4})/); + const match = input.match(/(\d{2})\.(\d{2})\.(\d{4})/) if (!match) { - throw new Error('The input is not a valid date. Expected format: YYYY-MM-DD'); + throw new Error( + 'The input is not a valid date. Expected format: YYYY-MM-DD' + ) } - const [, day, month, year] = match; - return [month.padStart(2, '0'), day.padStart(2, '0'), year].join('/'); -}; + const [, day, month, year] = match + return [month.padStart(2, '0'), day.padStart(2, '0'), year].join('/') +} export const parseNumber = (input: string) => - Number(input.replace('.', '').replace(',', '.')); + Number(input.replace('.', '').replace(',', '.')) export const trimMetaData = (input: string) => - input.substr(input.indexOf('Buchung;')); + input.substr(input.indexOf('Buchung;')) export const ingDiBaParser: ParserFunction = async (file: File) => { - const fileString = trimMetaData(await readEncodedFile(file)); - const { data } = await parse(fileString, { header: true, delimiter: ';' }); + const fileString = trimMetaData(await readEncodedFile(file)) + const { data } = await parse(fileString, { header: true, delimiter: ';' }) return [ { data: (data as IngDiBaRow[]) - .filter((r) => r.Buchung && r.Betrag) - .map((r) => ({ + .filter(r => r.Buchung && r.Betrag) + .map(r => ({ Date: generateYnabDate(r.Buchung), Payee: r['Auftraggeber/Empfänger'], Memo: r.Verwendungszweck, @@ -47,11 +49,13 @@ export const ingDiBaParser: ParserFunction = async (file: File) => { ? (-parseNumber(r.Betrag)).toFixed(2) : undefined, Inflow: - parseNumber(r.Betrag) > 0 ? parseNumber(r.Betrag).toFixed(2) : undefined, + parseNumber(r.Betrag) > 0 + ? parseNumber(r.Betrag).toFixed(2) + : undefined, })), }, - ]; -}; + ] +} export const ingDiBaMatcher: MatcherFunction = async (file: File) => { const requiredKeys: (keyof IngDiBaRow)[] = [ @@ -60,36 +64,36 @@ export const ingDiBaMatcher: MatcherFunction = async (file: File) => { 'Auftraggeber/Empfänger', 'Buchungstext', 'Verwendungszweck', - ]; + ] - const rawFileString = await readEncodedFile(file); + const rawFileString = await readEncodedFile(file) if (rawFileString.startsWith('Umsatzanzeige;Datei erstellt am:')) { - return true; + return true } try { const { data } = await parse(trimMetaData(rawFileString), { header: true, delimiter: ';', - }); + }) if (data.length === 0) { - return false; + return false } - const keys = Object.keys(data[0]); - const missingKeys = requiredKeys.filter((k) => !keys.includes(k)); + const keys = Object.keys(data[0]) + const missingKeys = requiredKeys.filter(k => !keys.includes(k)) if (missingKeys.length === 0) { - return true; + return true } } catch (e) { - return false; + return false } - return false; -}; + return false +} export const ingDiBa: ParserModule = { name: 'ING-DiBa', @@ -99,4 +103,4 @@ export const ingDiBa: ParserModule = { link: 'https://www.ing-diba.de', match: ingDiBaMatcher, parse: ingDiBaParser, -}; +} diff --git a/packages/ynap-parsers/src/de/kontist/kontist.spec.ts b/packages/ynap-parsers/src/de/kontist/kontist.spec.ts index a925c62f0..fbe4e8bdb 100644 --- a/packages/ynap-parsers/src/de/kontist/kontist.spec.ts +++ b/packages/ynap-parsers/src/de/kontist/kontist.spec.ts @@ -1,9 +1,9 @@ -import { generateYnabDate, kontist } from './kontist'; -import { YnabRow, YnabFile } from '../..'; +import { generateYnabDate, kontist } from './kontist' +import { YnabRow, YnabFile } from '../..' const content = `booking_date,valuta_date,amount,name,purpose,end_to_end_id,booking_status 2018-07-19,2018-07-19,-18000,John Doe,Invoice 2018006,E-e5a5084f898385ba8bc869c9627d8d2,processed -2018-07-19,2018-07-19,55800,Company,LB-5-2018.2018-07-15.CRIS.10001,NOTPROVIDED,processed`; +2018-07-19,2018-07-19,55800,Company,LB-5-2018.2018-07-15.CRIS.10001,NOTPROVIDED,processed` const ynabResult: YnabFile[] = [ { @@ -24,38 +24,38 @@ const ynabResult: YnabFile[] = [ }, ], }, -]; +] describe('Kontist Parser Module', () => { describe('Matcher', () => { it('should match Kontist files by fields', async () => { - const file = new File([content], 'test.csv'); - const result = await kontist.match(file); - expect(result).toBe(true); - }); + const file = new File([content], 'test.csv') + const result = await kontist.match(file) + expect(result).toBe(true) + }) it('should not match empty files', async () => { - const file = new File([], 'test.csv'); - const result = await kontist.match(file); - expect(result).toBe(false); - }); - }); + const file = new File([], 'test.csv') + const result = await kontist.match(file) + expect(result).toBe(false) + }) + }) describe('Parser', () => { it('should parse data correctly', async () => { - const file = new File([content], 'test.csv'); - const result = await kontist.parse(file); - expect(result).toEqual(ynabResult); - }); - }); + const file = new File([content], 'test.csv') + const result = await kontist.parse(file) + expect(result).toEqual(ynabResult) + }) + }) describe('Date Converter', () => { it('should convert dates correctly', () => { - expect(generateYnabDate('2018-09-01')).toEqual('09/01/2018'); - }); + expect(generateYnabDate('2018-09-01')).toEqual('09/01/2018') + }) it('should throw an error when the input date is incorrect', () => { - expect(() => generateYnabDate('1.1.1')).toThrow('not a valid date'); - }); - }); -}); + expect(() => generateYnabDate('1.1.1')).toThrow('not a valid date') + }) + }) +}) diff --git a/packages/ynap-parsers/src/de/kontist/kontist.ts b/packages/ynap-parsers/src/de/kontist/kontist.ts index 85986030a..123a04fdc 100644 --- a/packages/ynap-parsers/src/de/kontist/kontist.ts +++ b/packages/ynap-parsers/src/de/kontist/kontist.ts @@ -1,47 +1,53 @@ -import 'mdn-polyfills/String.prototype.startsWith'; -import { ParserFunction, MatcherFunction, ParserModule } from '../..'; -import { parse } from '../../util/papaparse'; +import 'mdn-polyfills/String.prototype.startsWith' +import { ParserFunction, MatcherFunction, ParserModule } from '../..' +import { parse } from '../../util/papaparse' export interface KontistRow { - booking_date: string; - valuta_date: string; - amount: string; - name: string; - purpose: string; - end_to_end_id: string; - booking_status: string; + booking_date: string + valuta_date: string + amount: string + name: string + purpose: string + end_to_end_id: string + booking_status: string } export const generateYnabDate = (input: string) => { - const match = input.match(/(\d{4})-(\d{2})-(\d{2})/); + 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'); + 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('/'); -}; + const [, year, month, day] = match + return [month.padStart(2, '0'), day.padStart(2, '0'), year].join('/') +} export const kontistParser: ParserFunction = async (file: File) => { - const { data } = await parse(file, { header: true }); + const { data } = await parse(file, { header: true }) return [ { data: (data as KontistRow[]) - .filter((r) => r.booking_date && r.amount) - .map((r) => ({ + .filter(r => r.booking_date && r.amount) + .map(r => ({ Date: generateYnabDate(r.booking_date), Payee: r.name, Memo: r.purpose, Outflow: - Number(r.amount) < 0 ? (-Number(r.amount) / 100).toFixed(2) : undefined, + Number(r.amount) < 0 + ? (-Number(r.amount) / 100).toFixed(2) + : undefined, Inflow: - Number(r.amount) > 0 ? (Number(r.amount) / 100).toFixed(2) : undefined, + Number(r.amount) > 0 + ? (Number(r.amount) / 100).toFixed(2) + : undefined, })), }, - ]; -}; + ] +} export const kontistMatcher: MatcherFunction = async (file: File) => { const requiredKeys: (keyof KontistRow)[] = [ @@ -51,23 +57,23 @@ export const kontistMatcher: MatcherFunction = async (file: File) => { 'purpose', 'end_to_end_id', 'booking_status', - ]; + ] - const { data } = await parse(file, { header: true }); + const { data } = await parse(file, { header: true }) if (data.length === 0) { - return false; + return false } - const keys = Object.keys(data[0]); - const missingKeys = requiredKeys.filter((k) => !keys.includes(k)); + const keys = Object.keys(data[0]) + const missingKeys = requiredKeys.filter(k => !keys.includes(k)) if (missingKeys.length === 0) { - return true; + return true } - return false; -}; + return false +} export const kontist: ParserModule = { name: 'Kontist', @@ -77,4 +83,4 @@ export const kontist: ParserModule = { link: 'https://intercom.help/kontist/konto/konto-export-von-kontoauszugen', match: kontistMatcher, parse: kontistParser, -}; +} diff --git a/packages/ynap-parsers/src/de/n26/n26.spec.ts b/packages/ynap-parsers/src/de/n26/n26.spec.ts index 0b05cabaf..9b1551068 100644 --- a/packages/ynap-parsers/src/de/n26/n26.spec.ts +++ b/packages/ynap-parsers/src/de/n26/n26.spec.ts @@ -1,6 +1,6 @@ -import { generateYnabDate, n26 } from './n26'; -import { YnabFile } from '../..'; -import { unparse } from 'papaparse'; +import { generateYnabDate, n26 } from './n26' +import { YnabFile } from '../..' +import { unparse } from 'papaparse' const content2021 = unparse([ { @@ -27,7 +27,7 @@ const content2021 = unparse([ 'Type Foreign Currency': 'EUR', 'Exchange Rate': '1.0', }, -]); +]) const ynabResult2021: YnabFile[] = [ { @@ -50,7 +50,7 @@ const ynabResult2021: YnabFile[] = [ }, ], }, -]; +] const content2022 = unparse([ { @@ -73,7 +73,7 @@ const content2022 = unparse([ 'Type Foreign Currency': 'EUR', 'Exchange Rate': '1.0', }, -]); +]) const ynabResult2022: YnabFile[] = [ { @@ -96,64 +96,64 @@ const ynabResult2022: YnabFile[] = [ }, ], }, -]; +] describe('N26 Parser Module', () => { describe('Matcher', () => { it('should match N26 files by file name', async () => { - const fileName = 'n26-csv-transactions.csv'; - const result = !!fileName.match(n26.filenamePattern); - expect(result).toBe(true); - }); + const fileName = 'n26-csv-transactions.csv' + const result = !!fileName.match(n26.filenamePattern) + expect(result).toBe(true) + }) it('should not match other files by file name', async () => { - const invalidFile = new File([], 'invalid.csv'); - const result = await n26.match(invalidFile); - expect(result).toBe(false); - }); + const invalidFile = new File([], 'invalid.csv') + const result = await n26.match(invalidFile) + expect(result).toBe(false) + }) it('should match N26 files until 2021 by fields', async () => { - const file = new File([content2021], 'test.csv'); - const result = await n26.match(file); - expect(result).toBe(true); - }); + const file = new File([content2021], 'test.csv') + const result = await n26.match(file) + expect(result).toBe(true) + }) it('should match N26 files since 2022 by fields', async () => { - const file = new File([content2022], 'test.csv'); - const result = await n26.match(file); - expect(result).toBe(true); - }); + const file = new File([content2022], 'test.csv') + const result = await n26.match(file) + expect(result).toBe(true) + }) it('should not match empty files', async () => { - const file = new File([], 'test.csv'); - const result = await n26.match(file); - expect(result).toBe(false); - }); - }); + const file = new File([], 'test.csv') + const result = await n26.match(file) + expect(result).toBe(false) + }) + }) describe('Parser', () => { it('should parse data until 2021 correctly', async () => { - const file = new File([content2021], 'test.csv'); - const result = await n26.parse(file); - expect(result).toEqual(ynabResult2021); - }); - }); + const file = new File([content2021], 'test.csv') + const result = await n26.parse(file) + expect(result).toEqual(ynabResult2021) + }) + }) describe('Parser', () => { it('should parse data since 2022 correctly', async () => { - const file = new File([content2022], 'test.csv'); - const result = await n26.parse(file); - expect(result).toEqual(ynabResult2022); - }); - }); + const file = new File([content2022], 'test.csv') + const result = await n26.parse(file) + expect(result).toEqual(ynabResult2022) + }) + }) describe('Date Converter', () => { it('should throw an error when the input date is incorrect', () => { - expect(() => generateYnabDate('1.1.1')).toThrow('not a valid date'); - }); + expect(() => generateYnabDate('1.1.1')).toThrow('not a valid date') + }) it('should convert dates correctly', () => { - expect(generateYnabDate('2018-09-01')).toEqual('09/01/2018'); - }); - }); -}); + expect(generateYnabDate('2018-09-01')).toEqual('09/01/2018') + }) + }) +}) diff --git a/packages/ynap-parsers/src/de/n26/n26.ts b/packages/ynap-parsers/src/de/n26/n26.ts index 0ee0eaeea..f1b641391 100644 --- a/packages/ynap-parsers/src/de/n26/n26.ts +++ b/packages/ynap-parsers/src/de/n26/n26.ts @@ -1,38 +1,40 @@ -import 'mdn-polyfills/String.prototype.startsWith'; -import { ParserFunction, MatcherFunction, ParserModule } from '../..'; -import { parse } from '../../util/papaparse'; +import 'mdn-polyfills/String.prototype.startsWith' +import { ParserFunction, MatcherFunction, ParserModule } from '../..' +import { parse } from '../../util/papaparse' export interface N26Row { - Date: string; - Payee: string; - 'Transaction type': string; - 'Payment reference': string; - Category: string; - 'Amount (EUR)': string; - 'Amount (Foreign Currency)': string; - 'Type Foreign Currency': string; - 'Exchange Rate': string; + Date: string + Payee: string + 'Transaction type': string + 'Payment reference': string + Category: string + 'Amount (EUR)': string + 'Amount (Foreign Currency)': string + 'Type Foreign Currency': string + 'Exchange Rate': string } export const generateYnabDate = (input: string) => { - const match = input.match(/(\d{4})-(\d{2})-(\d{2})/); + 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'); + 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('/'); -}; + const [, year, month, day] = match + return [month.padStart(2, '0'), day.padStart(2, '0'), year].join('/') +} export const n26Parser: ParserFunction = async (file: File) => { - const { data } = await parse(file, { header: true }); + const { data } = await parse(file, { header: true }) return [ { data: (data as N26Row[]) - .filter((r) => r.Date && r['Amount (EUR)']) - .map((r) => ({ + .filter(r => r.Date && r['Amount (EUR)']) + .map(r => ({ Date: generateYnabDate(r.Date), Payee: r.Payee, Category: r.Category, @@ -47,8 +49,8 @@ export const n26Parser: ParserFunction = async (file: File) => { : undefined, })), }, - ]; -}; + ] +} export const n26Matcher: MatcherFunction = async (file: File) => { const requiredKeys: (keyof N26Row)[] = [ @@ -57,23 +59,23 @@ export const n26Matcher: MatcherFunction = async (file: File) => { 'Transaction type', 'Payment reference', 'Amount (EUR)', - ]; + ] - const { data } = await parse(file, { header: true }); + const { data } = await parse(file, { header: true }) if (data.length === 0) { - return false; + return false } - const keys = Object.keys(data[0]); - const missingKeys = requiredKeys.filter((k) => !keys.includes(k)); + const keys = Object.keys(data[0]) + const missingKeys = requiredKeys.filter(k => !keys.includes(k)) if (missingKeys.length === 0) { - return true; + return true } - return false; -}; + return false +} export const n26: ParserModule = { name: 'N26', @@ -83,4 +85,4 @@ export const n26: ParserModule = { link: 'https://support.n26.com/en-eu/fixing-an-issue/payments-and-transfers/how-to-export-a-list-of-my-transactions', match: n26Matcher, parse: n26Parser, -}; +} diff --git a/packages/ynap-parsers/src/de/outbank/outbank.spec.ts b/packages/ynap-parsers/src/de/outbank/outbank.spec.ts index ab9940fc5..f8a48491f 100644 --- a/packages/ynap-parsers/src/de/outbank/outbank.spec.ts +++ b/packages/ynap-parsers/src/de/outbank/outbank.spec.ts @@ -1,11 +1,11 @@ -import { generateYnabDate, outbank } from './outbank'; -import { YnabFile } from '../..'; -import fs from 'fs'; -import path from 'path'; +import { generateYnabDate, outbank } from './outbank' +import { YnabFile } from '../..' +import fs from 'fs' +import path from 'path' const content = fs.readFileSync( - path.join(__dirname, 'test-data/Outbank_Export_20190403.csv'), -); + path.join(__dirname, 'test-data/Outbank_Export_20190403.csv') +) const ynabResult: YnabFile[] = [ { @@ -29,46 +29,46 @@ const ynabResult: YnabFile[] = [ }, ], }, -]; +] describe('Outbank Parser Module', () => { describe('Matcher', () => { it('should match Outbank files by file name', async () => { - const fileName = 'Outbank_Export_20190403.csv'; - const result = !!fileName.match(outbank.filenamePattern); - expect(result).toBe(true); - }); + const fileName = 'Outbank_Export_20190403.csv' + const result = !!fileName.match(outbank.filenamePattern) + expect(result).toBe(true) + }) it('should not match other files by file name', async () => { - const invalidFile = new File([], 'test.csv'); - const result = await outbank.match(invalidFile); - expect(result).toBe(false); - }); + const invalidFile = new File([], 'test.csv') + const result = await outbank.match(invalidFile) + expect(result).toBe(false) + }) it('should match Outbank files by fields', async () => { - const file = new File([content], 'test.csv'); - const result = await outbank.match(file); - expect(result).toBe(true); - }); + const file = new File([content], 'test.csv') + const result = await outbank.match(file) + expect(result).toBe(true) + }) it('should not match empty files', async () => { - const file = new File([], 'test.csv'); - const result = await outbank.match(file); - expect(result).toBe(false); - }); - }); + const file = new File([], 'test.csv') + const result = await outbank.match(file) + expect(result).toBe(false) + }) + }) describe('Parser', () => { it('should parse data correctly', async () => { - const file = new File([content], 'test.csv'); - const result = await outbank.parse(file); - expect(result).toEqual(ynabResult); - }); - }); + const file = new File([content], 'test.csv') + const result = await outbank.parse(file) + expect(result).toEqual(ynabResult) + }) + }) describe('Date Converter', () => { it('should throw an error when the input date is incorrect', () => { - expect(() => generateYnabDate('1.1.1')).toThrow('not a valid date'); - }); - }); -}); + expect(() => generateYnabDate('1.1.1')).toThrow('not a valid date') + }) + }) +}) diff --git a/packages/ynap-parsers/src/de/outbank/outbank.ts b/packages/ynap-parsers/src/de/outbank/outbank.ts index 91e4d5740..4b53c0239 100644 --- a/packages/ynap-parsers/src/de/outbank/outbank.ts +++ b/packages/ynap-parsers/src/de/outbank/outbank.ts @@ -1,57 +1,57 @@ -import 'mdn-polyfills/String.prototype.startsWith'; -import { ParserFunction, MatcherFunction, ParserModule, YnabRow } from '../..'; -import { parse } from '../../util/papaparse'; -import iban from 'iban'; -import slugify from 'slugify'; +import 'mdn-polyfills/String.prototype.startsWith' +import { ParserFunction, MatcherFunction, ParserModule, YnabRow } from '../..' +import { parse } from '../../util/papaparse' +import iban from 'iban' +import slugify from 'slugify' export interface OutbankRow { - '#': string; - Account?: string; - Date?: string; - 'Value Date'?: string; - Amount?: string; - Currency?: string; - Name?: string; - Number?: string; - Bank?: string; - Reason?: string; - Category?: string; - Subcategory?: string; - Tags?: string; - Note?: string; - 'Bank name'?: string; - 'Ultimate Receiver Name'?: string; - 'Original Amount'?: string; - 'Compensation Amount'?: string; - 'Exchange Rate'?: string; - 'Posting Key'?: string; - 'Posting Text'?: string; - 'Purpose Code'?: string; - 'SEPA Reference'?: string; - 'Client Reference'?: string; - 'Mandate Identification'?: string; - 'Originator Identifier'?: string; + '#': string + Account?: string + Date?: string + 'Value Date'?: string + Amount?: string + Currency?: string + Name?: string + Number?: string + Bank?: string + Reason?: string + Category?: string + Subcategory?: string + Tags?: string + Note?: string + 'Bank name'?: string + 'Ultimate Receiver Name'?: string + 'Original Amount'?: string + 'Compensation Amount'?: string + 'Exchange Rate'?: string + 'Posting Key'?: string + 'Posting Text'?: string + 'Purpose Code'?: string + 'SEPA Reference'?: string + 'Client Reference'?: string + 'Mandate Identification'?: string + 'Originator Identifier'?: string } export const generateYnabDate = (input: string) => { - const match = input.match(/(\d{1,2})\/(\d{1,2})\/(\d{1,2})/); + const match = input.match(/(\d{1,2})\/(\d{1,2})\/(\d{1,2})/) if (!match) { - throw new Error('The input is not a valid date. Expected format: M/D/Y'); + throw new Error('The input is not a valid date. Expected format: M/D/Y') } - const [, month, day, year] = match; - return [month.padStart(2, '0'), day.padStart(2, '0'), `20${year}`].join('/'); -}; + const [, month, day, year] = match + return [month.padStart(2, '0'), day.padStart(2, '0'), `20${year}`].join('/') +} -export const parseNumber = (input: string) => Number(input.replace(',', '.')); +export const parseNumber = (input: string) => Number(input.replace(',', '.')) export const outbankParser: ParserFunction = async (file: File) => { - const { data } = await parse(file, { header: true }); - const banks = (await import('./blz.json')).default; + const { data } = await parse(file, { header: true }) + const banks = (await import('./blz.json')).default const groupedData = (data as OutbankRow[]) - .filter((r) => r.Date && r.Amount) + .filter(r => r.Date && r.Amount) .reduce( (acc, cur) => { const data = { @@ -67,31 +67,31 @@ export const outbankParser: ParserFunction = async (file: File) => { parseNumber(cur.Amount!) > 0 ? parseNumber(cur.Amount!).toFixed(2) : undefined, - }; + } - const key = cur.Account || 'no-account'; + const key = cur.Account || 'no-account' if (Object.keys(acc).includes(key)) { - acc[key].push(data); + acc[key].push(data) } else { - acc[key] = [data]; + acc[key] = [data] } - return acc; + return acc }, - {} as { [k: string]: YnabRow[] }, - ); + {} as { [k: string]: YnabRow[] } + ) const getBankSlug = (key: string) => banks[key.substr(4, 8)] ? slugify(banks[key.substr(4, 8)]).toLowerCase() - : 'unknown'; + : 'unknown' - return Object.keys(groupedData).map((key) => ({ + return Object.keys(groupedData).map(key => ({ accountName: iban.isValid(key) ? getBankSlug(key) : key, data: groupedData[key], - })); -}; + })) +} export const outbankMatcher: MatcherFunction = async (file: File) => { const requiredKeys = [ @@ -106,23 +106,23 @@ export const outbankMatcher: MatcherFunction = async (file: File) => { 'Bank', 'Reason', 'Category', - ]; + ] - const { data } = await parse(file, { header: true }); + const { data } = await parse(file, { header: true }) if (data.length === 0) { - return false; + return false } - const keys = Object.keys(data[0]); - const missingKeys = requiredKeys.filter((k) => !keys.includes(k)); + const keys = Object.keys(data[0]) + const missingKeys = requiredKeys.filter(k => !keys.includes(k)) if (missingKeys.length === 0) { - return true; + return true } - return false; -}; + return false +} export const outbank: ParserModule = { name: 'Outbank', @@ -132,4 +132,4 @@ export const outbank: ParserModule = { link: 'https://help.outbankapp.com/en/kb/articles/wie-kann-ich-ums-tze-als-csv-datei-exportieren', match: outbankMatcher, parse: outbankParser, -}; +} diff --git a/packages/ynap-parsers/src/de/volksbank-eg/volksbank-eg.spec.ts b/packages/ynap-parsers/src/de/volksbank-eg/volksbank-eg.spec.ts index bb6edf158..d39aeeb0f 100644 --- a/packages/ynap-parsers/src/de/volksbank-eg/volksbank-eg.spec.ts +++ b/packages/ynap-parsers/src/de/volksbank-eg/volksbank-eg.spec.ts @@ -1,6 +1,6 @@ -import { generateYnabDate, volksbankEG } from './volksbank-eg'; -import { YnabFile } from '../..'; -import { encode } from 'iconv-lite'; +import { generateYnabDate, volksbankEG } from './volksbank-eg' +import { YnabFile } from '../..' +import { encode } from 'iconv-lite' const content = encode( `Volksbank eG;;;;;;;;;;;; @@ -31,8 +31,8 @@ Vielen Dank fuer Ihren Einkauf";;EUR;258,17;S 28.03.2019;;;;;;;;;Anfangssaldo;EUR;22.257,11;H 04.04.2019;;;;;;;;;Endsaldo;EUR;21.488,94;H `, - 'win1252', -); + 'win1252' +) const ynabResult: YnabFile[] = [ { @@ -60,50 +60,50 @@ const ynabResult: YnabFile[] = [ }, ], }, -]; +] describe('Volksbank Parser Module', () => { describe('Matcher', () => { it('should match Volksbank files by file name', async () => { - const fileName = 'Umsaetze_DE84000099000008800049_2019.04.04.csv'; - const result = !!fileName.match(volksbankEG.filenamePattern); - expect(result).toBe(true); - }); + const fileName = 'Umsaetze_DE84000099000008800049_2019.04.04.csv' + const result = !!fileName.match(volksbankEG.filenamePattern) + expect(result).toBe(true) + }) it('should not match other files by file name', async () => { - const invalidFile = new File([], 'test.csv'); - const result = await volksbankEG.match(invalidFile); - expect(result).toBe(false); - }); + const invalidFile = new File([], 'test.csv') + const result = await volksbankEG.match(invalidFile) + expect(result).toBe(false) + }) it('should match Volksbank files by fields', async () => { - const file = new File([content], 'test.csv'); - const result = await volksbankEG.match(file); - expect(result).toBe(true); - }); + const file = new File([content], 'test.csv') + const result = await volksbankEG.match(file) + expect(result).toBe(true) + }) it('should not match empty files', async () => { - const file = new File([], 'test.csv'); - const result = await volksbankEG.match(file); - expect(result).toBe(false); - }); - }); + const file = new File([], 'test.csv') + const result = await volksbankEG.match(file) + expect(result).toBe(false) + }) + }) describe('Parser', () => { it('should parse data correctly', async () => { - const file = new File([content], 'test.csv'); - const result = await volksbankEG.parse(file); - expect(result).toEqual(ynabResult); - }); - }); + const file = new File([content], 'test.csv') + const result = await volksbankEG.parse(file) + expect(result).toEqual(ynabResult) + }) + }) describe('Date Converter', () => { it('should format an input date correctly', () => { - expect(generateYnabDate('03.05.2018')).toEqual('05/03/2018'); - }); + expect(generateYnabDate('03.05.2018')).toEqual('05/03/2018') + }) it('should throw an error when the input date is incorrect', () => { - expect(() => generateYnabDate('1.1.1')).toThrow('not a valid date'); - }); - }); -}); + expect(() => generateYnabDate('1.1.1')).toThrow('not a valid date') + }) + }) +}) diff --git a/packages/ynap-parsers/src/de/volksbank-eg/volksbank-eg.ts b/packages/ynap-parsers/src/de/volksbank-eg/volksbank-eg.ts index 517c77d35..f9864565a 100644 --- a/packages/ynap-parsers/src/de/volksbank-eg/volksbank-eg.ts +++ b/packages/ynap-parsers/src/de/volksbank-eg/volksbank-eg.ts @@ -1,70 +1,72 @@ -import 'mdn-polyfills/String.prototype.startsWith'; -import { ParserFunction, MatcherFunction, ParserModule } from '../..'; -import { parse } from '../../util/papaparse'; -import { readEncodedFile } from '../../util/read-encoded-file'; +import 'mdn-polyfills/String.prototype.startsWith' +import { ParserFunction, MatcherFunction, ParserModule } from '../..' +import { parse } from '../../util/papaparse' +import { readEncodedFile } from '../../util/read-encoded-file' export interface VolksbankRow { - Buchungstag: string; - Valuta: string; - 'Auftraggeber/Zahlungsempfänger': string; - 'Empfänger/Zahlungspflichtiger': string; - 'Konto-Nr.': string; - IBAN: string; - BLZ: string; - BIC: string; - 'Vorgang/Verwendungszweck': string; - Kundenreferenz: string; - Währung: string; - Umsatz: string; + Buchungstag: string + Valuta: string + 'Auftraggeber/Zahlungsempfänger': string + 'Empfänger/Zahlungspflichtiger': string + 'Konto-Nr.': string + IBAN: string + BLZ: string + BIC: string + 'Vorgang/Verwendungszweck': string + Kundenreferenz: string + Währung: string + Umsatz: string } export const generateYnabDate = (input: string) => { - const match = input.match(/(\d{2})\.(\d{2})\.(\d{4})/); + const match = input.match(/(\d{2})\.(\d{2})\.(\d{4})/) if (!match) { - throw new Error('The input is not a valid date. Expected format: YYYY-MM-DD'); + throw new Error( + 'The input is not a valid date. Expected format: YYYY-MM-DD' + ) } - const [, day, month, year] = match; - return [month.padStart(2, '0'), day.padStart(2, '0'), year].join('/'); -}; + const [, day, month, year] = match + return [month.padStart(2, '0'), day.padStart(2, '0'), year].join('/') +} -export const parseNumber = (input: string) => Number(input.replace(',', '.')); +export const parseNumber = (input: string) => Number(input.replace(',', '.')) export const trimMetaData = (input: string) => { - const beginning = input.indexOf('Buchungstag;Valuta;'); - const end = input.lastIndexOf('\n;;;;'); + const beginning = input.indexOf('Buchungstag;Valuta;') + const end = input.lastIndexOf('\n;;;;') if (beginning === -1 || end === -1) { throw new Error( - 'Metadata could not be trimmed because the file format is incorrect.', - ); + 'Metadata could not be trimmed because the file format is incorrect.' + ) } const res = input .substr(beginning, input.length - beginning - (input.length - end)) - .trim(); + .trim() - return res; -}; + return res +} export const sanitizeMemo = (input: string) => { return input .split('\n') .slice(1) - .filter((r) => !r.startsWith('Verwendete TAN:')) - .join(' '); -}; + .filter(r => !r.startsWith('Verwendete TAN:')) + .join(' ') +} export const volksbankParser: ParserFunction = async (file: File) => { - const fileString = trimMetaData(await readEncodedFile(file)); - const { data } = await parse(fileString, { header: true }); + const fileString = trimMetaData(await readEncodedFile(file)) + const { data } = await parse(fileString, { header: true }) return [ { data: (data as VolksbankRow[]) - .filter((r) => r.Valuta && r.Umsatz) - .map((r) => ({ + .filter(r => r.Valuta && r.Umsatz) + .map(r => ({ Date: generateYnabDate(r.Valuta), Payee: r['Empfänger/Zahlungspflichtiger'], Memo: sanitizeMemo(r['Vorgang/Verwendungszweck']), @@ -73,11 +75,13 @@ export const volksbankParser: ParserFunction = async (file: File) => { ? (-parseNumber(r.Umsatz)).toFixed(2) : undefined, Inflow: - parseNumber(r.Umsatz) > 0 ? parseNumber(r.Umsatz).toFixed(2) : undefined, + parseNumber(r.Umsatz) > 0 + ? parseNumber(r.Umsatz).toFixed(2) + : undefined, })), }, - ]; -}; + ] +} export const volksbankMatcher: MatcherFunction = async (file: File) => { const requiredKeys: (keyof VolksbankRow)[] = [ @@ -93,33 +97,33 @@ export const volksbankMatcher: MatcherFunction = async (file: File) => { 'Valuta', 'Vorgang/Verwendungszweck', 'Währung', - ]; + ] - const rawFileString = await readEncodedFile(file); + const rawFileString = await readEncodedFile(file) if (rawFileString.startsWith('Volksbank eG;;;;;;;;;;;;')) { - return true; + return true } try { - const { data } = await parse(trimMetaData(rawFileString), { header: true }); + const { data } = await parse(trimMetaData(rawFileString), { header: true }) if (data.length === 0) { - return false; + return false } - const keys = Object.keys(data[0]); - const missingKeys = requiredKeys.filter((k) => !keys.includes(k)); + const keys = Object.keys(data[0]) + const missingKeys = requiredKeys.filter(k => !keys.includes(k)) if (missingKeys.length === 0) { - return true; + return true } } catch (e) { - return false; + return false } - return false; -}; + return false +} export const volksbankEG: ParserModule = { name: 'Volksbank', @@ -129,4 +133,4 @@ export const volksbankEG: ParserModule = { link: 'https://www.volksbank-eg.de/privatkunden.html', match: volksbankMatcher, parse: volksbankParser, -}; +} diff --git a/packages/ynap-parsers/src/gr/piraeus/piraeus.spec.ts b/packages/ynap-parsers/src/gr/piraeus/piraeus.spec.ts index 5ddf6271b..f21b45dbf 100644 --- a/packages/ynap-parsers/src/gr/piraeus/piraeus.spec.ts +++ b/packages/ynap-parsers/src/gr/piraeus/piraeus.spec.ts @@ -1,11 +1,11 @@ -import { generateYnabDate, piraeus } from './piraeus'; -import { YnabFile } from '../..'; -import fs from 'fs'; -import path from 'path'; +import { generateYnabDate, piraeus } from './piraeus' +import { YnabFile } from '../..' +import fs from 'fs' +import path from 'path' const content = fs.readFileSync( - path.join(__dirname, 'test-data/Account.Transactions_20200725.xlsx'), -); + path.join(__dirname, 'test-data/Account.Transactions_20200725.xlsx') +) const ynabResult: YnabFile[] = [ { @@ -138,44 +138,44 @@ const ynabResult: YnabFile[] = [ }, ], }, -]; +] describe('Piraeus Bank Parser Module', () => { describe('Matcher', () => { it('should match Piraeus Bank files by file name', async () => { - const fileName = 'Account Transactions_20190601.xlsx'; - const result = !!fileName.match(piraeus.filenamePattern); - expect(result).toBe(true); - }); + const fileName = 'Account Transactions_20190601.xlsx' + const result = !!fileName.match(piraeus.filenamePattern) + expect(result).toBe(true) + }) it('should not match other files by file name', async () => { - const invalidFile = new File([], 'test.xlsx'); - const result = await piraeus.match(invalidFile); - expect(result).toBe(false); - }); + const invalidFile = new File([], 'test.xlsx') + const result = await piraeus.match(invalidFile) + expect(result).toBe(false) + }) it('should match Piraeus Bank files by fields', async () => { - const file = new File([content], 'test.xlsx'); - const result = await piraeus.match(file); - expect(result).toBe(true); - }); - }); + const file = new File([content], 'test.xlsx') + const result = await piraeus.match(file) + expect(result).toBe(true) + }) + }) describe('Parser', () => { it('should parse data correctly', async () => { - const file = new File([content], 'test.xlsx'); - const result = await piraeus.parse(file); - expect(result).toEqual(ynabResult); - }); - }); + const file = new File([content], 'test.xlsx') + const result = await piraeus.parse(file) + expect(result).toEqual(ynabResult) + }) + }) describe('Date Converter', () => { it('should format an input date correctly', () => { - expect(generateYnabDate('03/05/2018')).toEqual('05/03/2018'); - }); + expect(generateYnabDate('03/05/2018')).toEqual('05/03/2018') + }) it('should throw an error when the input date is incorrect', () => { - expect(() => generateYnabDate('1.1.1')).toThrow('not a valid date'); - }); - }); -}); + expect(() => generateYnabDate('1.1.1')).toThrow('not a valid date') + }) + }) +}) diff --git a/packages/ynap-parsers/src/gr/piraeus/piraeus.ts b/packages/ynap-parsers/src/gr/piraeus/piraeus.ts index 0a986dfb0..0c2ae02c7 100644 --- a/packages/ynap-parsers/src/gr/piraeus/piraeus.ts +++ b/packages/ynap-parsers/src/gr/piraeus/piraeus.ts @@ -1,38 +1,38 @@ -import 'mdn-polyfills/String.prototype.startsWith'; -import { ParserFunction, MatcherFunction, ParserModule, YnabRow } from '../..'; -import { CellObject } from 'xlsx/types'; -import { readToBuffer } from '../../util/read-to-buffer'; +import 'mdn-polyfills/String.prototype.startsWith' +import { ParserFunction, MatcherFunction, ParserModule, YnabRow } from '../..' +import { CellObject } from 'xlsx/types' +import { readToBuffer } from '../../util/read-to-buffer' export const generateYnabDate = (input: string) => { - const match = input.match(/(\d{2})\/(\d{2})\/(\d{4})/); + const match = input.match(/(\d{2})\/(\d{2})\/(\d{4})/) if (!match) { throw new Error( - 'The input is not a valid date. Expected format: DD/MM/YYYY, got ' + input, - ); + 'The input is not a valid date. Expected format: DD/MM/YYYY, got ' + input + ) } - const [, day, month, year] = match; - return [month.padStart(2, '0'), day.padStart(2, '0'), year].join('/'); -}; + const [, day, month, year] = match + return [month.padStart(2, '0'), day.padStart(2, '0'), year].join('/') +} -export const parseNumber = (input: string) => Number(input.replace(',', '.')); +export const parseNumber = (input: string) => Number(input.replace(',', '.')) -export const cleanString = (input: string) => input.replace(/\s+/g, ' ').trim(); +export const cleanString = (input: string) => input.replace(/\s+/g, ' ').trim() export const piraeusParser: ParserFunction = async (file: File) => { - const xlsx = await import('xlsx'); + const xlsx = await import('xlsx') const workbook = xlsx.read(await readToBuffer(file), { type: 'buffer', - }); + }) - const sheet = workbook.Sheets[workbook.SheetNames[0]]; - const rows: YnabRow[] = []; + const sheet = workbook.Sheets[workbook.SheetNames[0]] + const rows: YnabRow[] = [] for (let rowNum = 5; true; rowNum++) { - let category: CellObject | undefined = sheet[`A${rowNum}`]; + let category: CellObject | undefined = sheet[`A${rowNum}`] if (!category || category.t === 'e' || String(category.v).trim() === '') { - break; + break } rows.push({ @@ -41,29 +41,29 @@ export const piraeusParser: ParserFunction = async (file: File) => { Memo: cleanString(String(sheet[`B${rowNum}`].v).split('\r')[0]), Inflow: sheet[`E${rowNum}`].v > 0 ? sheet[`E${rowNum}`].v : undefined, Outflow: sheet[`E${rowNum}`].v < 0 ? -sheet[`E${rowNum}`].v : undefined, - }); + }) } return [ { data: rows, }, - ]; -}; + ] +} export const piraeusMatcher: MatcherFunction = async (file: File) => { try { - const xlsx = await import('xlsx'); + const xlsx = await import('xlsx') const workbook = xlsx.read(await readToBuffer(file), { type: 'buffer', - }); + }) - const cell: CellObject = workbook.Sheets[workbook.SheetNames[0]]['A1']; - return cell.v === 'Piraeus Bank'; + const cell: CellObject = workbook.Sheets[workbook.SheetNames[0]]['A1'] + return cell.v === 'Piraeus Bank' } catch (e) { - return false; + return false } -}; +} export const piraeus: ParserModule = { name: 'Piraeus Bank', @@ -73,4 +73,4 @@ export const piraeus: ParserModule = { link: 'https://www.piraeusbank.gr', match: piraeusMatcher, parse: piraeusParser, -}; +} diff --git a/packages/ynap-parsers/src/index.spec.ts b/packages/ynap-parsers/src/index.spec.ts index 06173c6c9..69effec1e 100644 --- a/packages/ynap-parsers/src/index.spec.ts +++ b/packages/ynap-parsers/src/index.spec.ts @@ -1,35 +1,47 @@ -import { matchFile, parseFile } from '.'; -import { glob } from 'glob'; -import fs from 'fs'; -import path from 'path'; +import { matchFile, parseFile } from '.' +import { glob } from 'glob' +import fs from 'fs' +import path from 'path' describe('Main Module', () => { it('should match only one parser for each test file', async () => { - const files = await glob('**/test-data/*', { absolute: true, cwd: __dirname }); + const files = await glob('**/test-data/*', { + absolute: true, + cwd: __dirname, + }) for (const fileName of files) { - const file = new File([fs.readFileSync(fileName)], path.basename(fileName)); - const matches = await matchFile(file); + const file = new File( + [fs.readFileSync(fileName)], + path.basename(fileName) + ) + const matches = await matchFile(file) if (matches.length === 0) { - console.error('No parsers for', fileName); + console.error('No parsers for', fileName) } if (matches.length > 1) { console.warn( 'Multiple parsers for', fileName + ':', - matches.map((m) => m), - ); + matches.map(m => m) + ) } - expect(matches.length).toBeGreaterThanOrEqual(1); + expect(matches.length).toBeGreaterThanOrEqual(1) } - }); + }) it('should parse all test files', async () => { - const files = await glob('**/test-data/*', { absolute: true, cwd: __dirname }); + const files = await glob('**/test-data/*', { + absolute: true, + cwd: __dirname, + }) for (const fileName of files) { - const file = new File([fs.readFileSync(fileName)], path.basename(fileName)); - const parsed = await parseFile(file); + const file = new File( + [fs.readFileSync(fileName)], + path.basename(fileName) + ) + const parsed = await parseFile(file) } - }); -}); + }) +}) diff --git a/packages/ynap-parsers/src/index.ts b/packages/ynap-parsers/src/index.ts index aecef01eb..61b71a686 100644 --- a/packages/ynap-parsers/src/index.ts +++ b/packages/ynap-parsers/src/index.ts @@ -1,59 +1,59 @@ -import { unparse } from 'papaparse'; -import 'mdn-polyfills/String.prototype.padStart'; - -import uniq from 'lodash/uniq'; -import last from 'lodash/last'; - -import { outbank } from './de/outbank/outbank'; -import { _1822direkt } from './de/1822direkt/1822direkt'; -import { n26 } from './de/n26/n26'; -import { revolut } from './international/revolut/revolut'; -import { ingDiBa } from './de/ing-diba/ing-diba'; -import { comdirect } from './de/comdirect/comdirect'; -import { kontist } from './de/kontist/kontist'; -import { volksbankEG } from './de/volksbank-eg/volksbank-eg'; - -import { ingAustria } from './at/ing/ing-austria'; -import { bancomer } from './mx/bbva-bancomer/bbva-bancomer'; -import { piraeus } from './gr/piraeus/piraeus'; -import { marcus } from './uk/marcus/marcus'; -import { aqua } from './uk/aqua/aqua'; - -import { bank2ynab } from './bank2ynab/bank2ynab'; -import { sparbankenTanum as sparbankenTanum2018 } from './se/sparbanken-tanum/2018/sparbanken-tanum'; -import { sparbankenTanum as sparbankenTanum2019 } from './se/sparbanken-tanum/2019/sparbanken-tanum'; -import { mt940 } from './international/mt940/mt940'; - -import { mbank } from './pl/mbank/mbank'; -import { bankPocztowy } from './pl/bank-pocztowy/bank-pocztowy'; -import { seb } from './se/seb-privat/seb'; +import { unparse } from 'papaparse' +import 'mdn-polyfills/String.prototype.padStart' + +import uniq from 'lodash/uniq' +import last from 'lodash/last' + +import { outbank } from './de/outbank/outbank' +import { _1822direkt } from './de/1822direkt/1822direkt' +import { n26 } from './de/n26/n26' +import { revolut } from './international/revolut/revolut' +import { ingDiBa } from './de/ing-diba/ing-diba' +import { comdirect } from './de/comdirect/comdirect' +import { kontist } from './de/kontist/kontist' +import { volksbankEG } from './de/volksbank-eg/volksbank-eg' + +import { ingAustria } from './at/ing/ing-austria' +import { bancomer } from './mx/bbva-bancomer/bbva-bancomer' +import { piraeus } from './gr/piraeus/piraeus' +import { marcus } from './uk/marcus/marcus' +import { aqua } from './uk/aqua/aqua' + +import { bank2ynab } from './bank2ynab/bank2ynab' +import { sparbankenTanum as sparbankenTanum2018 } from './se/sparbanken-tanum/2018/sparbanken-tanum' +import { sparbankenTanum as sparbankenTanum2019 } from './se/sparbanken-tanum/2019/sparbanken-tanum' +import { mt940 } from './international/mt940/mt940' + +import { mbank } from './pl/mbank/mbank' +import { bankPocztowy } from './pl/bank-pocztowy/bank-pocztowy' +import { seb } from './se/seb-privat/seb' export interface YnabRow { - Date?: string; - Payee?: string; - Category?: string; - Memo?: string; - Outflow?: number | string; - Inflow?: number | string; + Date?: string + Payee?: string + Category?: string + Memo?: string + Outflow?: number | string + Inflow?: number | string } export interface YnabFile { - accountName?: string; - data: YnabRow[]; + accountName?: string + data: YnabRow[] } export interface ParserModule { - name: string; - country: string; - link: string; - fileExtension: string; - filenamePattern: RegExp; - match: MatcherFunction; - parse: ParserFunction; + name: string + country: string + link: string + fileExtension: string + filenamePattern: RegExp + match: MatcherFunction + parse: ParserFunction } -export type MatcherFunction = (file: File) => Promise; -export type ParserFunction = (file: File) => Promise; +export type MatcherFunction = (file: File) => Promise +export type ParserFunction = (file: File) => Promise export const parsers: ParserModule[] = [ // AT @@ -91,34 +91,36 @@ export const parsers: ParserModule[] = [ revolut, mt940, ...bank2ynab, -]; +] export const countries = uniq( - parsers.filter((p) => p.country.length === 2).map((p) => p.country), -); + parsers.filter(p => p.country.length === 2).map(p => p.country) +) export const matchFile = async (file: File): Promise => { if (file.name.match(/^(.+)-ynap\.csv$/)) { - throw new Error('This file has already been converted by YNAP.'); + throw new Error('This file has already been converted by YNAP.') } - const filenameMatches = parsers.filter((p) => file.name.match(p.filenamePattern)); + const filenameMatches = parsers.filter(p => + file.name.match(p.filenamePattern) + ) // If parser modules match the file by its filename, try those first if (filenameMatches.length > 0) { const parsers = ( await Promise.all( - filenameMatches.map(async (p) => ({ + filenameMatches.map(async p => ({ parser: p, matched: await p.match(file), - })), + })) ) ) - .filter((r) => r.matched) - .map((p) => p.parser); + .filter(r => r.matched) + .map(p => p.parser) if (parsers.length > 0) { - return parsers; + return parsers } } @@ -127,48 +129,48 @@ export const matchFile = async (file: File): Promise => { await Promise.all( parsers .filter( - (p) => + p => p.fileExtension.toLowerCase() === - last(file.name.split('.')).toLowerCase(), + last(file.name.split('.')).toLowerCase() ) - .map(async (p) => ({ + .map(async p => ({ parser: p, matched: await p.match(file), - })), + })) ) ) - .filter((r) => r.matched) - .map((p) => p.parser); + .filter(r => r.matched) + .map(p => p.parser) - return results; -}; + return results +} export const parseFile = async (file: File, parserOverride?: ParserModule) => { - let parser: ParserModule | null = null; + let parser: ParserModule | null = null if (parserOverride) { - parser = parserOverride; + parser = parserOverride } else { - const matches = await matchFile(file); + const matches = await matchFile(file) console.log( 'The file', file.name, 'was matched by', - matches.map((m) => m.name).join(', '), - ); - parser = matches.length > 0 ? matches[0] : null; + matches.map(m => m.name).join(', ') + ) + parser = matches.length > 0 ? matches[0] : null } if (!parser) { - throw new Error(`No parser is available for this file.`); + throw new Error(`No parser is available for this file.`) } - const ynabData = await parser.parse(file); + const ynabData = await parser.parse(file) - return ynabData.map((f) => ({ + return ynabData.map(f => ({ ...f, data: unparse(f.data), rawData: f.data, matchedParser: parser, - })); -}; + })) +} diff --git a/packages/ynap-parsers/src/international/mt940/mt940.spec.ts b/packages/ynap-parsers/src/international/mt940/mt940.spec.ts index cd79f3a8d..40a5119a1 100644 --- a/packages/ynap-parsers/src/international/mt940/mt940.spec.ts +++ b/packages/ynap-parsers/src/international/mt940/mt940.spec.ts @@ -1,9 +1,11 @@ -import { mt940matcher, mt940parser, generateYnabDate } from './mt940'; -import fs from 'fs'; -import path from 'path'; -import { YnabFile } from '../..'; +import { mt940matcher, mt940parser, generateYnabDate } from './mt940' +import fs from 'fs' +import path from 'path' +import { YnabFile } from '../..' -const content = fs.readFileSync(path.join(__dirname, 'test-data/mt940-bunq.sta')); +const content = fs.readFileSync( + path.join(__dirname, 'test-data/mt940-bunq.sta') +) const output: YnabFile[] = [ { @@ -29,35 +31,35 @@ const output: YnabFile[] = [ }, ], }, -]; +] describe('MT940 Parser Module', () => { describe('Matcher', () => { it('should correctly match MT940 files', async () => { - const result = await mt940matcher(new File([content], '')); - expect(result).toBeTruthy(); - }); + const result = await mt940matcher(new File([content], '')) + expect(result).toBeTruthy() + }) it('should fail to match invalid files', async () => { - const result = await mt940matcher(new File(['test'], '')); - expect(result).toBeFalsy(); - }); - }); + const result = await mt940matcher(new File(['test'], '')) + expect(result).toBeFalsy() + }) + }) describe('Parser', () => { it('should correctly parse MT940 files', async () => { - const result = await mt940parser(new File([content], 'test.sta')); - expect(result).toEqual(output); - }); - }); + const result = await mt940parser(new File([content], 'test.sta')) + expect(result).toEqual(output) + }) + }) describe('Date Converter', () => { it('should convert dates correctly', () => { - expect(generateYnabDate('2018-09-01')).toEqual('09/01/2018'); - }); + expect(generateYnabDate('2018-09-01')).toEqual('09/01/2018') + }) it('should throw an error when the input date is incorrect', () => { - expect(() => generateYnabDate('1.1.1')).toThrow('not a valid date'); - }); - }); -}); + expect(() => generateYnabDate('1.1.1')).toThrow('not a valid date') + }) + }) +}) diff --git a/packages/ynap-parsers/src/international/mt940/mt940.ts b/packages/ynap-parsers/src/international/mt940/mt940.ts index 71d4370a7..dfaa33378 100644 --- a/packages/ynap-parsers/src/international/mt940/mt940.ts +++ b/packages/ynap-parsers/src/international/mt940/mt940.ts @@ -1,52 +1,54 @@ -import 'mdn-polyfills/String.prototype.startsWith'; -import { ParserFunction, MatcherFunction, ParserModule, YnabRow } from '../..'; -import { readToBuffer } from '../../util/read-to-buffer'; -import { YnabFile } from '../..'; +import 'mdn-polyfills/String.prototype.startsWith' +import { ParserFunction, MatcherFunction, ParserModule, YnabRow } from '../..' +import { readToBuffer } from '../../util/read-to-buffer' +import { YnabFile } from '../..' export const generateYnabDate = (input: string) => { - const match = input.match(/(\d{4})-(\d{2})-(\d{2})/); + 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'); + 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('/'); -}; + const [, year, month, day] = match + return [month.padStart(2, '0'), day.padStart(2, '0'), year].join('/') +} -export const mt940matcher: MatcherFunction = async (file) => { - const mt = await import('mt940-js'); - const buffer = await readToBuffer(file); +export const mt940matcher: MatcherFunction = async file => { + const mt = await import('mt940-js') + const buffer = await readToBuffer(file) try { - const res = await mt.read(buffer); - return res.length > 0; + const res = await mt.read(buffer) + return res.length > 0 } catch {} - return false; -}; + return false +} -export const mt940parser: ParserFunction = async (file) => { - const mt = await import('mt940-js'); - const buffer = await readToBuffer(file); - const statements = await mt.read(buffer); +export const mt940parser: ParserFunction = async file => { + const mt = await import('mt940-js') + const buffer = await readToBuffer(file) + const statements = await mt.read(buffer) return statements.map( - (s) => + s => ({ accountName: [s.referenceNumber, s.accountId].filter(Boolean).join(' '), data: s.transactions.map( - (t) => + t => ({ Inflow: t.isCredit ? t.amount : undefined, Outflow: t.isCredit ? undefined : t.amount, Date: generateYnabDate(t.entryDate), Memo: t.description, - }) as YnabRow, + }) as YnabRow ), - }) as YnabFile, - ); -}; + }) as YnabFile + ) +} export const mt940: ParserModule = { name: 'MT940 (standard)', @@ -56,4 +58,4 @@ export const mt940: ParserModule = { filenamePattern: /(.*)\.sta$/, match: mt940matcher, parse: mt940parser, -}; +} diff --git a/packages/ynap-parsers/src/international/revolut/revolut.spec.ts b/packages/ynap-parsers/src/international/revolut/revolut.spec.ts index 6080ca457..cfc2d419c 100644 --- a/packages/ynap-parsers/src/international/revolut/revolut.spec.ts +++ b/packages/ynap-parsers/src/international/revolut/revolut.spec.ts @@ -1,10 +1,10 @@ -import { generateYnabDate, revolut } from './revolut'; -import { YnabFile } from '../..'; +import { generateYnabDate, revolut } from './revolut' +import { YnabFile } from '../..' const content = `Completed Date ; Reference ; Paid Out (RON) ; Paid In (RON) ; Exchange Out; Exchange In ; Balance (RON); Exchange Rate ; Category 24 Mar 2020 ; Premium plan fee ; 29.99 ; ; ; ; 13.09 ; ; Services 1 Apr 2019 ; To Piglet ; ; 1.42 ; ; ; 43.08 ; ; Transfers -`; +` const ynabResult: YnabFile[] = [ { @@ -27,50 +27,50 @@ const ynabResult: YnabFile[] = [ }, ], }, -]; +] describe('Revolut Parser Module', () => { describe('Matcher', () => { it('should match Revolut files by file name', async () => { - const fileName = 'Revolut-RON-Statement-May 2018 to Apr 2019.csv'; - const result = !!fileName.match(revolut.filenamePattern); - expect(result).toBe(true); - }); + const fileName = 'Revolut-RON-Statement-May 2018 to Apr 2019.csv' + const result = !!fileName.match(revolut.filenamePattern) + expect(result).toBe(true) + }) it('should not match other files by file name', async () => { - const invalidFile = new File([], 'invalid.csv'); - const result = await revolut.match(invalidFile); - expect(result).toBe(false); - }); + const invalidFile = new File([], 'invalid.csv') + const result = await revolut.match(invalidFile) + expect(result).toBe(false) + }) it('should match Revolut files by fields', async () => { - const file = new File([content], 'test.csv'); - const result = await revolut.match(file); - expect(result).toBe(true); - }); + const file = new File([content], 'test.csv') + const result = await revolut.match(file) + expect(result).toBe(true) + }) it('should not match empty files', async () => { - const file = new File([], 'test.csv'); - const result = await revolut.match(file); - expect(result).toBe(false); - }); - }); + const file = new File([], 'test.csv') + const result = await revolut.match(file) + expect(result).toBe(false) + }) + }) describe('Parser', () => { it('should parse data correctly', async () => { - const file = new File([content], 'test.csv'); - const result = await revolut.parse(file); - expect(result).toEqual(ynabResult); - }); - }); + const file = new File([content], 'test.csv') + const result = await revolut.parse(file) + expect(result).toEqual(ynabResult) + }) + }) describe('Date Converter', () => { it('should throw an error when the input date is incorrect', () => { - expect(() => generateYnabDate('1.1.1')).toThrow('Invalid time value'); - }); + expect(() => generateYnabDate('1.1.1')).toThrow('Invalid time value') + }) it('should convert dates correctly', () => { - expect(generateYnabDate('12 Feb 2019')).toEqual('2019-02-12'); - }); - }); -}); + expect(generateYnabDate('12 Feb 2019')).toEqual('2019-02-12') + }) + }) +}) diff --git a/packages/ynap-parsers/src/international/revolut/revolut.ts b/packages/ynap-parsers/src/international/revolut/revolut.ts index 2c98c3765..24169b0ad 100644 --- a/packages/ynap-parsers/src/international/revolut/revolut.ts +++ b/packages/ynap-parsers/src/international/revolut/revolut.ts @@ -1,42 +1,44 @@ -import 'mdn-polyfills/String.prototype.startsWith'; -import { parse, format } from 'date-fns'; +import 'mdn-polyfills/String.prototype.startsWith' +import { parse, format } from 'date-fns' -import { ParserFunction, MatcherFunction, ParserModule } from '../..'; -import { parse as parseCsv } from '../../util/papaparse'; +import { ParserFunction, MatcherFunction, ParserModule } from '../..' +import { parse as parseCsv } from '../../util/papaparse' -const COMPLETED_DATE = 0; -const REFERENCE = 1; -const PAID_OUT = 2; -const PAID_IN = 3; -const CATEGORY = 8; +const COMPLETED_DATE = 0 +const REFERENCE = 1 +const PAID_OUT = 2 +const PAID_IN = 3 +const CATEGORY = 8 export const generateYnabDate = (input: string) => { - let match = parse(input, 'dd MMM', Date.now()); + let match = parse(input, 'dd MMM', Date.now()) if (!isValidDate(match)) { - match = parse(input, 'dd MMM yyyy', Date.now()); + match = parse(input, 'dd MMM yyyy', Date.now()) } - return format(match, 'yyyy-MM-dd'); -}; + return format(match, 'yyyy-MM-dd') +} export const revolutParser: ParserFunction = async (file: File) => { - const { data } = await parseCsv(file, { header: false }); + const { data } = await parseCsv(file, { header: false }) return [ { data: (data as string[][]) .slice(1) - .filter((r) => r[0]) - .map((r) => ({ + .filter(r => r[0]) + .map(r => ({ Date: generateYnabDate(r[COMPLETED_DATE]), Payee: r[REFERENCE].trim(), Category: r[CATEGORY].trim(), Memo: r[REFERENCE].trim(), - Outflow: r[PAID_OUT].trim() ? Number(r[PAID_OUT]).toFixed(2) : undefined, + Outflow: r[PAID_OUT].trim() + ? Number(r[PAID_OUT]).toFixed(2) + : undefined, Inflow: r[PAID_IN].trim() ? Number(r[PAID_IN]).toFixed(2) : undefined, })), }, - ]; -}; + ] +} export const revolutMatcher: MatcherFunction = async (file: File) => { const requiredKeys: string[] = [ @@ -44,17 +46,17 @@ export const revolutMatcher: MatcherFunction = async (file: File) => { 'Reference', 'Exchange Rate', 'Category', - ]; + ] - const { data } = await parseCsv(file, { preview: 1 }); + const { data } = await parseCsv(file, { preview: 1 }) if (data.length === 0) { - return false; + return false } - const csvColumnNames = data[0].map((r) => r.trim()); - return requiredKeys.every((key) => csvColumnNames.includes(key)); -}; + const csvColumnNames = data[0].map(r => r.trim()) + return requiredKeys.every(key => csvColumnNames.includes(key)) +} export const revolut: ParserModule = { name: 'Revolut', @@ -64,6 +66,6 @@ export const revolut: ParserModule = { link: 'https://blog.revolut.com/new-feature-exportable-statements/', match: revolutMatcher, parse: revolutParser, -}; +} -const isValidDate = (date: Date) => !isNaN(date.getTime()); +const isValidDate = (date: Date) => !isNaN(date.getTime()) diff --git a/packages/ynap-parsers/src/mx/bbva-bancomer/bbva-bancomer.spec.ts b/packages/ynap-parsers/src/mx/bbva-bancomer/bbva-bancomer.spec.ts index a39fe8a25..177903513 100644 --- a/packages/ynap-parsers/src/mx/bbva-bancomer/bbva-bancomer.spec.ts +++ b/packages/ynap-parsers/src/mx/bbva-bancomer/bbva-bancomer.spec.ts @@ -1,6 +1,6 @@ -import { generateYnabDate, bancomer, trimMetaData } from './bbva-bancomer'; -import { YnabFile } from '../..'; -import { encode, decode } from 'iconv-lite'; +import { generateYnabDate, bancomer, trimMetaData } from './bbva-bancomer' +import { YnabFile } from '../..' +import { encode, decode } from 'iconv-lite' const content = encode( `Card number: ·9999 @@ -14,12 +14,12 @@ DATE DESCRIPTION OUTFLOW INFLOW BALANCE "BBVA Bancomer, S.A., Institución de Banca Múltiple, Grupo Financiero BBVA Bancomer." `, - 'utf16le', -); + 'utf16le' +) const trimmedContent = `13/04/2019 RESTAURANT "1,064.80" "11,509.22" 12/4/2019 GROCERIES 471.66 "10,444.42" -9/4/2019 AMAZON "1,022.00" "9,651.92"`; +9/4/2019 AMAZON "1,022.00" "9,651.92"` const ynabResult: YnabFile[] = [ { @@ -44,56 +44,56 @@ const ynabResult: YnabFile[] = [ }, ], }, -]; +] describe('BBVA Bancomer Parser Module', () => { describe('Matcher', () => { it('should match bancomer files by file name', async () => { - const fileName = 'descarga.csv'; - const result = !!fileName.match(bancomer.filenamePattern); - expect(result).toBe(true); - }); + const fileName = 'descarga.csv' + const result = !!fileName.match(bancomer.filenamePattern) + expect(result).toBe(true) + }) it('should not match other files by file name', async () => { - const invalidFile = new File([], 'test.csv'); - const result = await bancomer.match(invalidFile); - expect(result).toBe(false); - }); + const invalidFile = new File([], 'test.csv') + const result = await bancomer.match(invalidFile) + expect(result).toBe(false) + }) it('should match bancomer files by fields', async () => { - const file = new File([content], 'test.csv'); - const result = await bancomer.match(file); - expect(result).toBe(true); - }); + const file = new File([content], 'test.csv') + const result = await bancomer.match(file) + expect(result).toBe(true) + }) it('should not match empty files', async () => { - const file = new File([], 'test.csv'); - const result = await bancomer.match(file); - expect(result).toBe(false); - }); - }); + const file = new File([], 'test.csv') + const result = await bancomer.match(file) + expect(result).toBe(false) + }) + }) describe('Parser', () => { it('should parse data correctly', async () => { - const file = new File([content], 'test.csv'); - const result = await bancomer.parse(file); - expect(result).toEqual(ynabResult); - }); - }); + const file = new File([content], 'test.csv') + const result = await bancomer.parse(file) + expect(result).toEqual(ynabResult) + }) + }) describe('Date Converter', () => { it('should format an input date correctly', () => { - expect(generateYnabDate('03/05/2018')).toEqual('05/03/2018'); - }); + expect(generateYnabDate('03/05/2018')).toEqual('05/03/2018') + }) it('should throw an error when the input date is incorrect', () => { - expect(() => generateYnabDate('1.1.1')).toThrow('not a valid date'); - }); - }); + expect(() => generateYnabDate('1.1.1')).toThrow('not a valid date') + }) + }) describe('Metadata Trimmer', () => { it('should trim all metadata from a valid input string', () => { - expect(trimMetaData(decode(content, 'utf16le'))).toEqual(trimmedContent); - }); - }); -}); + expect(trimMetaData(decode(content, 'utf16le'))).toEqual(trimmedContent) + }) + }) +}) diff --git a/packages/ynap-parsers/src/mx/bbva-bancomer/bbva-bancomer.ts b/packages/ynap-parsers/src/mx/bbva-bancomer/bbva-bancomer.ts index 23e591501..38cb2444f 100644 --- a/packages/ynap-parsers/src/mx/bbva-bancomer/bbva-bancomer.ts +++ b/packages/ynap-parsers/src/mx/bbva-bancomer/bbva-bancomer.ts @@ -1,7 +1,7 @@ -import 'mdn-polyfills/String.prototype.startsWith'; -import { ParserFunction, MatcherFunction, ParserModule } from '../..'; -import { parse } from '../../util/papaparse'; -import { readEncodedFile } from '../../util/read-encoded-file'; +import 'mdn-polyfills/String.prototype.startsWith' +import { ParserFunction, MatcherFunction, ParserModule } from '../..' +import { parse } from '../../util/papaparse' +import { readEncodedFile } from '../../util/read-encoded-file' // Bancomer Row: // DATE, DESCRIPTION, OUTFLOW, INFLOW, BALANCE @@ -12,77 +12,82 @@ import { readEncodedFile } from '../../util/read-encoded-file'; */ export const generateYnabDate = (input: string) => { - const match = input.match(/(\d{1,2})\/(\d{1,2})\/(\d{4})/); + const match = input.match(/(\d{1,2})\/(\d{1,2})\/(\d{4})/) if (!match) { - throw new Error('The input is not a valid date. Expected format: DD/MM/YYYY'); + throw new Error( + 'The input is not a valid date. Expected format: DD/MM/YYYY' + ) } - const [, day, month, year] = match; - return [month.padStart(2, '0'), day.padStart(2, '0'), year].join('/'); -}; + const [, day, month, year] = match + return [month.padStart(2, '0'), day.padStart(2, '0'), year].join('/') +} -export const parseNumber = (input: string) => Number(input.replace(',', '')); +export const parseNumber = (input: string) => Number(input.replace(',', '')) export const trimMetaData = (input: string) => { - const lines = input.split('\n'); + const lines = input.split('\n') return lines .splice(3) .filter( - (l) => - l && l.trim() !== '' && !l.startsWith(' ') && !l.match(/^"BBVA (.+)"/), + l => + l && + l.trim() !== '' && + !l.startsWith(' ') && + !l.match(/^"BBVA (.+)"/) ) - .join('\n'); -}; + .join('\n') +} export const bancomerParser: ParserFunction = async (file: File) => { - const fileString = trimMetaData(await readEncodedFile(file, 'utf16le')); - const { data } = await parse(fileString, { delimiter: '\t' }); + const fileString = trimMetaData(await readEncodedFile(file, 'utf16le')) + const { data } = await parse(fileString, { delimiter: '\t' }) return [ { data: (data as string[][]) - .filter((r) => r[0] && r[0].trim()) - .map((r) => ({ + .filter(r => r[0] && r[0].trim()) + .map(r => ({ Date: generateYnabDate(r[0]), Memo: r[1], Outflow: r[2] ? parseNumber(r[2]) : undefined, Inflow: r[3] ? parseNumber(r[3]) : undefined, })), }, - ]; -}; + ] +} export const bancomerMatcher: MatcherFunction = async (file: File) => { - const rawFileString = await readEncodedFile(file, 'utf16le'); + const rawFileString = await readEncodedFile(file, 'utf16le') if (rawFileString.length === 0) { - return false; + return false } if ( rawFileString.startsWith('Card number: ') || rawFileString.startsWith('Número de Tarjeta: ') ) { - return true; + return true } // This might happen when the file encoding is wrong. if (rawFileString.indexOf('\n') === -1) { - return false; + return false } - const headerRow = rawFileString.split('\n')[2].trim(); + const headerRow = rawFileString.split('\n')[2].trim() if ( headerRow === 'DATE\tDESCRIPTION\tOUTFLOW\tINFLOW\tBALANCE' || headerRow === 'FECHA\tDESCRIPCIÓN\tCARGO\tABONO\tSALDO' ) { - return true; + return true } - return false; -}; + return false +} export const bancomer: ParserModule = { name: 'bancomer', @@ -92,4 +97,4 @@ export const bancomer: ParserModule = { link: 'https://www.bancomer.com', match: bancomerMatcher, parse: bancomerParser, -}; +} diff --git a/packages/ynap-parsers/src/pl/bank-pocztowy/bank-pocztowy.spec.ts b/packages/ynap-parsers/src/pl/bank-pocztowy/bank-pocztowy.spec.ts index c34f62bea..8a0aa196c 100644 --- a/packages/ynap-parsers/src/pl/bank-pocztowy/bank-pocztowy.spec.ts +++ b/packages/ynap-parsers/src/pl/bank-pocztowy/bank-pocztowy.spec.ts @@ -1,9 +1,9 @@ -import fs from 'fs'; -import path from 'path'; -import iconv from 'iconv-lite'; +import fs from 'fs' +import path from 'path' +import iconv from 'iconv-lite' -import { YnabFile } from '../..'; -import { bankPocztowy, FILE_ENCODING } from './bank-pocztowy'; +import { YnabFile } from '../..' +import { bankPocztowy, FILE_ENCODING } from './bank-pocztowy' const ynabResult: YnabFile[] = [ { @@ -31,47 +31,47 @@ const ynabResult: YnabFile[] = [ }, ], }, -]; +] describe('Bank Pocztowy Parser Module', () => { describe('Matcher', () => { it('should match bank pocztowy files by file name', async () => { - const fileName = '1571076127593.csv'; - const result = !!fileName.match(bankPocztowy.filenamePattern); - expect(result).toBe(true); - }); + const fileName = '1571076127593.csv' + const result = !!fileName.match(bankPocztowy.filenamePattern) + expect(result).toBe(true) + }) it('should not match other files by file name', async () => { - const invalidFile = new File([], 'test.csv'); - const result = await bankPocztowy.match(invalidFile); - expect(result).toBe(false); - }); + const invalidFile = new File([], 'test.csv') + const result = await bankPocztowy.match(invalidFile) + expect(result).toBe(false) + }) it('should match bankPocztowy files by fields', async () => { const content = fs.readFileSync( - path.resolve(__dirname, 'test-data', '1571076127593.csv'), - ); - const file = new File([iconv.decode(content, FILE_ENCODING)], 'test.csv'); - const result = await bankPocztowy.match(file); - expect(result).toBe(true); - }); + path.resolve(__dirname, 'test-data', '1571076127593.csv') + ) + const file = new File([iconv.decode(content, FILE_ENCODING)], 'test.csv') + const result = await bankPocztowy.match(file) + expect(result).toBe(true) + }) it('should not match empty files', async () => { - const file = new File([], 'test.csv'); - const result = await bankPocztowy.match(file); - expect(result).toBe(false); - }); - }); + const file = new File([], 'test.csv') + const result = await bankPocztowy.match(file) + expect(result).toBe(false) + }) + }) describe('Parser', () => { it('should parse data correctly', async () => { const content = fs.readFileSync( - path.resolve(__dirname, 'test-data', '1571076127593.csv'), - ); - const file = new File([content], '1571076127593.csv'); - const result = await bankPocztowy.parse(file); + path.resolve(__dirname, 'test-data', '1571076127593.csv') + ) + const file = new File([content], '1571076127593.csv') + const result = await bankPocztowy.parse(file) - expect(result).toEqual(ynabResult); - }); - }); -}); + expect(result).toEqual(ynabResult) + }) + }) +}) diff --git a/packages/ynap-parsers/src/pl/bank-pocztowy/bank-pocztowy.ts b/packages/ynap-parsers/src/pl/bank-pocztowy/bank-pocztowy.ts index ffd5f7a2c..a6d47505b 100644 --- a/packages/ynap-parsers/src/pl/bank-pocztowy/bank-pocztowy.ts +++ b/packages/ynap-parsers/src/pl/bank-pocztowy/bank-pocztowy.ts @@ -1,6 +1,6 @@ -import { ParserFunction, MatcherFunction, ParserModule } from '../..'; -import { readEncodedFile } from '../../util/read-encoded-file'; -import { parse } from '../../util/papaparse'; +import { ParserFunction, MatcherFunction, ParserModule } from '../..' +import { readEncodedFile } from '../../util/read-encoded-file' +import { parse } from '../../util/papaparse' type bankPocztowyRow = [ string, @@ -12,44 +12,45 @@ type bankPocztowyRow = [ string, string, string, -]; +] -export const FILE_ENCODING = 'windows1250'; -const DATE_REGEXP = /\d{4}\s\d{2}\s\d{2}/; -const ACCOUNT_NUMBER_REGEXP = /\d{2}1320\d{20}/; -const AMOUNT_CLEANUP_REGEXP = /[-PLN\s]/g; -const PAYEE_REGEXP = /\d{26},(\d{26}|(\d{1,}-){1,}\d),(.+?),-?\d{1,}\.\d{2},PLN/g; -const NEW_LINE_REGEXP = /\|/g; +export const FILE_ENCODING = 'windows1250' +const DATE_REGEXP = /\d{4}\s\d{2}\s\d{2}/ +const ACCOUNT_NUMBER_REGEXP = /\d{2}1320\d{20}/ +const AMOUNT_CLEANUP_REGEXP = /[-PLN\s]/g +const PAYEE_REGEXP = + /\d{26},(\d{26}|(\d{1,}-){1,}\d),(.+?),-?\d{1,}\.\d{2},PLN/g +const NEW_LINE_REGEXP = /\|/g const PARSER_SETTINGS = { header: false, delimiter: ',', quoteChar: '"', -}; +} const fixInput = (input: string) => input .trim() .split(/\n/) - .map((line) => + .map(line => line.replace(PAYEE_REGEXP, (match, a, b, payee) => - match.replace(payee, payee.replace(/,/g, '__')), - ), + match.replace(payee, payee.replace(/,/g, '__')) + ) ) - .join('\n'); + .join('\n') const bankPocztowyMatch: MatcherFunction = async (file: File) => { - const fileString = await readEncodedFile(file, FILE_ENCODING); + const fileString = await readEncodedFile(file, FILE_ENCODING) try { - const { data } = await parse(fixInput(fileString), PARSER_SETTINGS); - const [row] = data; + const { data } = await parse(fixInput(fileString), PARSER_SETTINGS) + const [row] = data if (data.length === 0) { - return false; + return false } if (row.length !== 9) { - return false; + return false } if ( @@ -57,30 +58,30 @@ const bankPocztowyMatch: MatcherFunction = async (file: File) => { row[1].match(DATE_REGEXP) && row[2].match(ACCOUNT_NUMBER_REGEXP) ) { - return true; + return true } } catch (e) { - return false; + return false } - return false; -}; + return false +} const bankPocztowyParser: ParserFunction = async (file: File) => { - const fileString = await readEncodedFile(file, FILE_ENCODING); - const { data } = await parse(fixInput(fileString), PARSER_SETTINGS); - const result = data as bankPocztowyRow[]; + const fileString = await readEncodedFile(file, FILE_ENCODING) + const { data } = await parse(fixInput(fileString), PARSER_SETTINGS) + const result = data as bankPocztowyRow[] return [ { data: result - .filter((item) => item.length === 9) - .map((item) => { - const [YYYY, MM, DD] = item[0].split(' '); - const isOutflow = item[5].startsWith('-'); + .filter(item => item.length === 9) + .map(item => { + const [YYYY, MM, DD] = item[0].split(' ') + const isOutflow = item[5].startsWith('-') const amount = item[5] .replace(AMOUNT_CLEANUP_REGEXP, '') - .replace(',', '.'); + .replace(',', '.') return { Memo: item[7].replace(NEW_LINE_REGEXP, ''), @@ -88,11 +89,11 @@ const bankPocztowyParser: ParserFunction = async (file: File) => { Payee: undefined, Outflow: isOutflow ? amount : undefined, Inflow: !isOutflow ? amount : undefined, - }; + } }), }, - ]; -}; + ] +} export const bankPocztowy: ParserModule = { name: 'Bank Pocztowy', @@ -102,4 +103,4 @@ export const bankPocztowy: ParserModule = { link: 'https://www.pocztowy.pl', match: bankPocztowyMatch, parse: bankPocztowyParser, -}; +} diff --git a/packages/ynap-parsers/src/pl/mbank/mbank.spec.ts b/packages/ynap-parsers/src/pl/mbank/mbank.spec.ts index 59651e3e1..f0faf4ca0 100644 --- a/packages/ynap-parsers/src/pl/mbank/mbank.spec.ts +++ b/packages/ynap-parsers/src/pl/mbank/mbank.spec.ts @@ -1,9 +1,9 @@ -import fs from 'fs'; -import path from 'path'; -import iconv from 'iconv-lite'; +import fs from 'fs' +import path from 'path' +import iconv from 'iconv-lite' -import { YnabFile } from '../..'; -import { mbank } from './mbank'; +import { YnabFile } from '../..' +import { mbank } from './mbank' const ynabResult: YnabFile[] = [ { @@ -24,41 +24,41 @@ const ynabResult: YnabFile[] = [ }, ], }, -]; +] describe('mBank Parser Module', () => { describe('Matcher', () => { it('should match mBank files by file name', async () => { - const fileName = 'operations_190710_191010_201910100004038185.csv'; - const result = !!fileName.match(mbank.filenamePattern); - expect(result).toBe(true); - }); + const fileName = 'operations_190710_191010_201910100004038185.csv' + const result = !!fileName.match(mbank.filenamePattern) + expect(result).toBe(true) + }) it('should not match other files by file name', async () => { - const invalidFile = new File([], 'test.csv'); - const result = await mbank.match(invalidFile); - expect(result).toBe(false); - }); + const invalidFile = new File([], 'test.csv') + const result = await mbank.match(invalidFile) + expect(result).toBe(false) + }) it('should match mBank files by fields', async () => { const content = fs.readFileSync( path.resolve( __dirname, 'test-data', - 'operations_190710_191010_201910100004038185.csv', - ), - ); - const file = new File([iconv.decode(content, 'msee')], 'test.csv'); - const result = await mbank.match(file); - expect(result).toBe(true); - }); + 'operations_190710_191010_201910100004038185.csv' + ) + ) + const file = new File([iconv.decode(content, 'msee')], 'test.csv') + const result = await mbank.match(file) + expect(result).toBe(true) + }) it('should not match empty files', async () => { - const file = new File([], 'test.csv'); - const result = await mbank.match(file); - expect(result).toBe(false); - }); - }); + const file = new File([], 'test.csv') + const result = await mbank.match(file) + expect(result).toBe(false) + }) + }) describe('Parser', () => { it('should parse data correctly', async () => { @@ -66,14 +66,14 @@ describe('mBank Parser Module', () => { path.resolve( __dirname, 'test-data', - 'operations_190710_191010_201910100004038185.csv', - ), - ); - const file = new File([content], 'test.csv'); - const result = await mbank.parse(file); + 'operations_190710_191010_201910100004038185.csv' + ) + ) + const file = new File([content], 'test.csv') + const result = await mbank.parse(file) - expect(result).toEqual(ynabResult); - }); + expect(result).toEqual(ynabResult) + }) it('should properly escape quote characters', async () => { const content = [ @@ -81,13 +81,13 @@ describe('mBank Parser Module', () => { '2019-09-25;INCOME;eKonto 0000 ... 1111;Wpływy - inne;2 000,00 PLN;2 867,35 PLN', '2019-09-22;"QUOTED" Name.;eKonto 0000 ... 1111;Wydatki - inne;-18,39 PLN;4 684,40 PLN', '2019-09-25;INCOME;eKonto 0000 ... 1111;Wpływy - inne;2 000,00 PLN;2 867,35 PLN', - ].join('\r\n'); - const file = new File([iconv.encode(content, 'msee')], 'test.csv'); - const result = await mbank.parse(file); + ].join('\r\n') + const file = new File([iconv.encode(content, 'msee')], 'test.csv') + const result = await mbank.parse(file) - expect(result[0].data).toHaveLength(3); - expect(result[0].data[1].Memo).toBe('"QUOTED" Name.'); - }); + expect(result[0].data).toHaveLength(3) + expect(result[0].data[1].Memo).toBe('"QUOTED" Name.') + }) it('should parse uncleared transactions', async () => { const content = [ @@ -97,11 +97,11 @@ describe('mBank Parser Module', () => { ' transakcja nierozliczona ', ' ;eKonto 0000 ... 1111;Wydatki - inne;-18,39 PLN;4 684,40 PLN', '2019-09-25;INCOME;eKonto 0000 ... 1111;Wpływy - inne;2 000,00 PLN;2 867,35 PLN', - ].join('\r\n'); - const file = new File([iconv.encode(content, 'msee')], 'test.csv'); - const result = await mbank.parse(file); + ].join('\r\n') + const file = new File([iconv.encode(content, 'msee')], 'test.csv') + const result = await mbank.parse(file) - expect(result[0].data).toHaveLength(3); - }); - }); -}); + expect(result[0].data).toHaveLength(3) + }) + }) +}) diff --git a/packages/ynap-parsers/src/pl/mbank/mbank.ts b/packages/ynap-parsers/src/pl/mbank/mbank.ts index 44aa3a6d7..f33e4ed26 100644 --- a/packages/ynap-parsers/src/pl/mbank/mbank.ts +++ b/packages/ynap-parsers/src/pl/mbank/mbank.ts @@ -1,76 +1,77 @@ -import { ParserFunction, MatcherFunction, ParserModule } from '../..'; -import { readEncodedFile } from '../../util/read-encoded-file'; -import { parse } from '../../util/papaparse'; +import { ParserFunction, MatcherFunction, ParserModule } from '../..' +import { readEncodedFile } from '../../util/read-encoded-file' +import { parse } from '../../util/papaparse' interface mBankRow { - '#Data operacji': string; - '#Opis operacji': string; - '#Rachunek': string; - '#Kategoria': string; - '#Kwota': string; - '#Saldo po operacji': string; + '#Data operacji': string + '#Opis operacji': string + '#Rachunek': string + '#Kategoria': string + '#Kwota': string + '#Saldo po operacji': string } -const FILE_ENCODING = 'msee'; -const AMOUNT_CLEANUP_REGEXP = /[-PLN\s]/g; -const SHEET_CLEANUP_REGEXP = /\s{3,}/g; -const REQUIRED_FIELDS = ['#Data operacji', '#Kwota', '#Opis operacji']; +const FILE_ENCODING = 'msee' +const AMOUNT_CLEANUP_REGEXP = /[-PLN\s]/g +const SHEET_CLEANUP_REGEXP = /\s{3,}/g +const REQUIRED_FIELDS = ['#Data operacji', '#Kwota', '#Opis operacji'] const PARSER_SETTINGS = { header: true, delimiter: ';', quoteChar: "'", -}; +} -const cleanup = (input: string) => input.replace(SHEET_CLEANUP_REGEXP, '').trim(); +const cleanup = (input: string) => + input.replace(SHEET_CLEANUP_REGEXP, '').trim() const trimMetaData = (input: string) => - input.substr(input.indexOf('#Data operacji;')); + input.substr(input.indexOf('#Data operacji;')) export const mbankMatch: MatcherFunction = async (file: File) => { - const fileString = await readEncodedFile(file, FILE_ENCODING); + const fileString = await readEncodedFile(file, FILE_ENCODING) if (fileString.startsWith('mBank S.A.')) { - return true; + return true } try { - const { data } = await parse(trimMetaData(fileString), PARSER_SETTINGS); + const { data } = await parse(trimMetaData(fileString), PARSER_SETTINGS) if (data.length === 0) { - return false; + return false } - const keys = Object.keys(data[0]); - const missingKeys = REQUIRED_FIELDS.filter((k) => !keys.includes(k)); + const keys = Object.keys(data[0]) + const missingKeys = REQUIRED_FIELDS.filter(k => !keys.includes(k)) if (missingKeys.length === 0) { - return true; + return true } } catch (e) { - return false; + return false } - return false; -}; + return false +} const mbankParser: ParserFunction = async (file: File) => { const fileString = cleanup( - trimMetaData(await readEncodedFile(file, FILE_ENCODING)), - ); - const { data } = await parse(fileString, PARSER_SETTINGS); - const result = data as mBankRow[]; + trimMetaData(await readEncodedFile(file, FILE_ENCODING)) + ) + const { data } = await parse(fileString, PARSER_SETTINGS) + const result = data as mBankRow[] return [ { data: result - .filter((item) => - REQUIRED_FIELDS.every((key) => typeof item[key] !== 'undefined'), + .filter(item => + REQUIRED_FIELDS.every(key => typeof item[key] !== 'undefined') ) - .map((item) => { - const [YYYY, MM, DD] = item['#Data operacji'].split('-'); - const isOutflow = item['#Kwota'].startsWith('-'); + .map(item => { + const [YYYY, MM, DD] = item['#Data operacji'].split('-') + const isOutflow = item['#Kwota'].startsWith('-') const amount = item['#Kwota'] .replace(AMOUNT_CLEANUP_REGEXP, '') - .replace(',', '.'); + .replace(',', '.') return { Memo: item['#Opis operacji'], @@ -78,11 +79,11 @@ const mbankParser: ParserFunction = async (file: File) => { Payee: undefined, Outflow: isOutflow ? amount : undefined, Inflow: !isOutflow ? amount : undefined, - }; + } }), }, - ]; -}; + ] +} export const mbank: ParserModule = { name: 'mBank', @@ -92,4 +93,4 @@ export const mbank: ParserModule = { link: 'https://www.mbank.pl/', match: mbankMatch, parse: mbankParser, -}; +} diff --git a/packages/ynap-parsers/src/se/seb-privat/seb.spec.ts b/packages/ynap-parsers/src/se/seb-privat/seb.spec.ts index 04b1365ad..bffe49277 100644 --- a/packages/ynap-parsers/src/se/seb-privat/seb.spec.ts +++ b/packages/ynap-parsers/src/se/seb-privat/seb.spec.ts @@ -1,9 +1,11 @@ -import { generateYnabDate, seb } from './seb'; -import { YnabFile } from '../..'; -import fs from 'fs'; -import path from 'path'; +import { generateYnabDate, seb } from './seb' +import { YnabFile } from '../..' +import fs from 'fs' +import path from 'path' -const content = fs.readFileSync(path.join(__dirname, 'test-data/kontoutdrag.xlsx')); +const content = fs.readFileSync( + path.join(__dirname, 'test-data/kontoutdrag.xlsx') +) const ynabResult: YnabFile[] = [ { @@ -24,44 +26,44 @@ const ynabResult: YnabFile[] = [ }, ], }, -]; +] describe('SEB Bank Parser Module', () => { describe('Matcher', () => { it('should match SEB Bank files by file name', async () => { - const fileName = 'kontoutdrag.xlsx'; - const result = !!fileName.match(seb.filenamePattern); - expect(result).toBe(true); - }); + const fileName = 'kontoutdrag.xlsx' + const result = !!fileName.match(seb.filenamePattern) + expect(result).toBe(true) + }) it('should not match other files by file name', async () => { - const invalidFile = new File([], 'test.xlsx'); - const result = await seb.match(invalidFile); - expect(result).toBe(false); - }); + const invalidFile = new File([], 'test.xlsx') + const result = await seb.match(invalidFile) + expect(result).toBe(false) + }) it('should match SEB Bank files by fields', async () => { - const file = new File([content], 'test.xlsx'); - const result = await seb.match(file); - expect(result).toBe(true); - }); - }); + const file = new File([content], 'test.xlsx') + const result = await seb.match(file) + expect(result).toBe(true) + }) + }) describe('Parser', () => { it('should parse data correctly', async () => { - const file = new File([content], 'test.xlsx'); - const result = await seb.parse(file); - expect(result).toEqual(ynabResult); - }); - }); + const file = new File([content], 'test.xlsx') + const result = await seb.parse(file) + expect(result).toEqual(ynabResult) + }) + }) describe('Date Converter', () => { it('should format an input date correctly', () => { - expect(generateYnabDate('2020-30-03')).toEqual('30/03/2020'); - }); + expect(generateYnabDate('2020-30-03')).toEqual('30/03/2020') + }) it('should throw an error when the input date is incorrect', () => { - expect(() => generateYnabDate('1.1.1')).toThrow('not a valid date'); - }); - }); -}); + expect(() => generateYnabDate('1.1.1')).toThrow('not a valid date') + }) + }) +}) diff --git a/packages/ynap-parsers/src/se/seb-privat/seb.ts b/packages/ynap-parsers/src/se/seb-privat/seb.ts index 93d4cf5ea..bbbe3deb2 100644 --- a/packages/ynap-parsers/src/se/seb-privat/seb.ts +++ b/packages/ynap-parsers/src/se/seb-privat/seb.ts @@ -1,37 +1,37 @@ -import 'mdn-polyfills/String.prototype.startsWith'; -import { ParserFunction, MatcherFunction, ParserModule, YnabRow } from '../..'; -import { CellObject } from 'xlsx/types'; -import { readToBuffer } from '../../util/read-to-buffer'; +import 'mdn-polyfills/String.prototype.startsWith' +import { ParserFunction, MatcherFunction, ParserModule, YnabRow } from '../..' +import { CellObject } from 'xlsx/types' +import { readToBuffer } from '../../util/read-to-buffer' export const generateYnabDate = (input: string) => { - const match = input.match(/(\d{4})-(\d{2})-(\d{2})/); + 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, got ' + input, - ); + 'The input is not a valid date. Expected format: YYYY-MM-DD, got ' + input + ) } - const [, year, month, day] = match; - return [month.padStart(2, '0'), day.padStart(2, '0'), year].join('/'); -}; + const [, year, month, day] = match + return [month.padStart(2, '0'), day.padStart(2, '0'), year].join('/') +} -export const parseNumber = (input: string) => Number(input.replace(',', '')); // , is for thousands separator +export const parseNumber = (input: string) => Number(input.replace(',', '')) // , is for thousands separator export const sebPrivatParser: ParserFunction = async (file: File) => { - const xlsx = await import('xlsx'); + const xlsx = await import('xlsx') const workbook = xlsx.read(await readToBuffer(file), { type: 'buffer', - }); + }) - const sheet = workbook.Sheets[workbook.SheetNames[0]]; - const rows: YnabRow[] = []; + const sheet = workbook.Sheets[workbook.SheetNames[0]] + const rows: YnabRow[] = [] - let rowNum = 9; + let rowNum = 9 while (true) { - let dateCol: CellObject | undefined = sheet[`A${rowNum}`]; + let dateCol: CellObject | undefined = sheet[`A${rowNum}`] if (!dateCol || dateCol.t === 'e' || String(dateCol.v).trim() === '') { - break; + break } rows.push({ @@ -40,31 +40,31 @@ export const sebPrivatParser: ParserFunction = async (file: File) => { Memo: String(sheet[`D${rowNum}`].v).split('\r')[0].trim(), Inflow: sheet[`E${rowNum}`].v > 0 ? sheet[`E${rowNum}`].v : undefined, Outflow: sheet[`E${rowNum}`].v < 0 ? -sheet[`E${rowNum}`].v : undefined, - }); + }) - rowNum++; + rowNum++ } return [ { data: rows, }, - ]; -}; + ] +} export const sebMatcher: MatcherFunction = async (file: File) => { try { - const xlsx = await import('xlsx'); + const xlsx = await import('xlsx') const workbook = xlsx.read(await readToBuffer(file), { type: 'buffer', - }); + }) - const cell: CellObject = workbook.Sheets[workbook.SheetNames[0]]['A1']; - return cell.v === 'Export från internetbanken för privatpersoner'; + const cell: CellObject = workbook.Sheets[workbook.SheetNames[0]]['A1'] + return cell.v === 'Export från internetbanken för privatpersoner' } catch (e) { - return false; + return false } -}; +} export const seb: ParserModule = { name: 'SEB Bank', @@ -74,4 +74,4 @@ export const seb: ParserModule = { link: 'https://www.seb.se', match: sebMatcher, parse: sebPrivatParser, -}; +} diff --git a/packages/ynap-parsers/src/se/sparbanken-tanum/2018/sparbanken-tanum.spec.ts b/packages/ynap-parsers/src/se/sparbanken-tanum/2018/sparbanken-tanum.spec.ts index 85c8f11b2..2392e12d3 100644 --- a/packages/ynap-parsers/src/se/sparbanken-tanum/2018/sparbanken-tanum.spec.ts +++ b/packages/ynap-parsers/src/se/sparbanken-tanum/2018/sparbanken-tanum.spec.ts @@ -1,9 +1,9 @@ -import fs from 'fs'; -import path from 'path'; -import { generateYnabDate, sparbankenTanum } from './sparbanken-tanum'; -import { YnabRow, YnabFile } from '../../..'; +import fs from 'fs' +import path from 'path' +import { generateYnabDate, sparbankenTanum } from './sparbanken-tanum' +import { YnabRow, YnabFile } from '../../..' -const content = fs.readFileSync(path.join(__dirname, 'test-data', 'export.csv')); +const content = fs.readFileSync(path.join(__dirname, 'test-data', 'export.csv')) const ynabResult: YnabFile[] = [ { @@ -22,38 +22,38 @@ const ynabResult: YnabFile[] = [ }, ], }, -]; +] describe('Sparbanken Tanum Parser Module', () => { describe('Matcher', () => { it('should match Sparbanken Tanum files by fields', async () => { - const file = new File([content], 'test.csv'); - const result = await sparbankenTanum.match(file); - expect(result).toBe(true); - }); + const file = new File([content], 'test.csv') + const result = await sparbankenTanum.match(file) + expect(result).toBe(true) + }) it('should not match empty files', async () => { - const file = new File([], 'test.csv'); - const result = await sparbankenTanum.match(file); - expect(result).toBe(false); - }); - }); + const file = new File([], 'test.csv') + const result = await sparbankenTanum.match(file) + expect(result).toBe(false) + }) + }) describe('Parser', () => { it('should parse data correctly', async () => { - const file = new File([content], 'test.csv'); - const result = await sparbankenTanum.parse(file); - expect(result).toEqual(ynabResult); - }); - }); + const file = new File([content], 'test.csv') + const result = await sparbankenTanum.parse(file) + expect(result).toEqual(ynabResult) + }) + }) describe('Date Converter', () => { it('should convert dates correctly', () => { - expect(generateYnabDate('2018-09-01')).toEqual('09/01/2018'); - }); + expect(generateYnabDate('2018-09-01')).toEqual('09/01/2018') + }) it('should throw an error when the input date is incorrect', () => { - expect(() => generateYnabDate('1.1.1')).toThrow('not a valid date'); - }); - }); -}); + expect(() => generateYnabDate('1.1.1')).toThrow('not a valid date') + }) + }) +}) diff --git a/packages/ynap-parsers/src/se/sparbanken-tanum/2018/sparbanken-tanum.ts b/packages/ynap-parsers/src/se/sparbanken-tanum/2018/sparbanken-tanum.ts index 6dbe910ec..edf48eb23 100644 --- a/packages/ynap-parsers/src/se/sparbanken-tanum/2018/sparbanken-tanum.ts +++ b/packages/ynap-parsers/src/se/sparbanken-tanum/2018/sparbanken-tanum.ts @@ -1,6 +1,6 @@ -import 'mdn-polyfills/String.prototype.startsWith'; -import { ParserFunction, MatcherFunction, ParserModule } from '../../..'; -import { parse } from '../../../util/papaparse'; +import 'mdn-polyfills/String.prototype.startsWith' +import { ParserFunction, MatcherFunction, ParserModule } from '../../..' +import { parse } from '../../../util/papaparse' /* * Row format: @@ -8,56 +8,59 @@ import { parse } from '../../../util/papaparse'; */ export const generateYnabDate = (input: string) => { - const match = input.match(/(\d{4})-(\d{2})-(\d{2})/); + 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'); + 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('/'); -}; + const [, year, month, day] = match + return [month.padStart(2, '0'), day.padStart(2, '0'), year].join('/') +} export const toNumber = (input: string) => - Number(input.replace(',', '.').replace(' ', '')); + Number(input.replace(',', '.').replace(' ', '')) export const sparbankenTanumParser: ParserFunction = async (file: File) => { - const { data } = await parse(file, { delimiter: ';' }); + const { data } = await parse(file, { delimiter: ';' }) return [ { data: data - .filter((r) => r[0] && r[2] !== '-') - .map((r) => ({ + .filter(r => r[0] && r[2] !== '-') + .map(r => ({ Date: generateYnabDate(r[1]), Payee: String(r[0]).trim(), - Outflow: toNumber(r[3]) < 0 ? (-toNumber(r[3])).toFixed(2) : undefined, + Outflow: + toNumber(r[3]) < 0 ? (-toNumber(r[3])).toFixed(2) : undefined, Inflow: toNumber(r[3]) > 0 ? toNumber(r[3]).toFixed(2) : undefined, })), }, - ]; -}; + ] +} export const sparbankenTanumMatcher: MatcherFunction = async (file: File) => { - const { data } = await parse(file, { delimiter: ';' }); + const { data } = await parse(file, { delimiter: ';' }) // Check if the file contains any data if (data.length === 0) { - return false; + return false } // Check if the second field is a date if (!String(data[0][1]).match(/\d{4}-\d{2}-\d{2}/)) { - return false; + return false } // Check if the fourth field is a valid number if (!String(data[0][3]).match(/-?[\d ]+,\d{2}/)) { - return false; + return false } - return true; -}; + return true +} export const sparbankenTanum: ParserModule = { name: 'Sparbanken Tanum', @@ -67,4 +70,4 @@ export const sparbankenTanum: ParserModule = { link: 'https://www.sparbankentanum.se/', match: sparbankenTanumMatcher, parse: sparbankenTanumParser, -}; +} diff --git a/packages/ynap-parsers/src/se/sparbanken-tanum/2019/sparbanken-tanum.spec.ts b/packages/ynap-parsers/src/se/sparbanken-tanum/2019/sparbanken-tanum.spec.ts index 360c671b4..6b8954237 100644 --- a/packages/ynap-parsers/src/se/sparbanken-tanum/2019/sparbanken-tanum.spec.ts +++ b/packages/ynap-parsers/src/se/sparbanken-tanum/2019/sparbanken-tanum.spec.ts @@ -1,7 +1,7 @@ -import fs from 'fs'; -import path from 'path'; -import { generateYnabDate, sparbankenTanum } from './sparbanken-tanum'; -import { YnabFile } from '../../..'; +import fs from 'fs' +import path from 'path' +import { generateYnabDate, sparbankenTanum } from './sparbanken-tanum' +import { YnabFile } from '../../..' const data: YnabFile[] = [ { @@ -177,42 +177,42 @@ const data: YnabFile[] = [ }, ], }, -]; +] const content = fs.readFileSync( - path.join(__dirname, 'test-data', 'Transaktioner_2019-10-12_14-57-29.csv'), -); + path.join(__dirname, 'test-data', 'Transaktioner_2019-10-12_14-57-29.csv') +) describe('Sparbanken Tanum Parser Module (2019)', () => { describe('Matcher', () => { it('should match Sparbanken Tanum files by fields', async () => { - const file = new File([content], 'test.csv'); - const result = await sparbankenTanum.match(file); - expect(result).toBe(true); - }); + const file = new File([content], 'test.csv') + const result = await sparbankenTanum.match(file) + expect(result).toBe(true) + }) it('should not match empty files', async () => { - const file = new File([], 'test.csv'); - const result = await sparbankenTanum.match(file); - expect(result).toBe(false); - }); - }); + const file = new File([], 'test.csv') + const result = await sparbankenTanum.match(file) + expect(result).toBe(false) + }) + }) describe('Parser', () => { it('should parse data correctly', async () => { - const file = new File([content], 'test.csv'); - const result = await sparbankenTanum.parse(file); - expect(result).toEqual(data); - }); - }); + const file = new File([content], 'test.csv') + const result = await sparbankenTanum.parse(file) + expect(result).toEqual(data) + }) + }) describe('Date Converter', () => { it('should convert dates correctly', () => { - expect(generateYnabDate('2018-09-01')).toEqual('09/01/2018'); - }); + expect(generateYnabDate('2018-09-01')).toEqual('09/01/2018') + }) it('should throw an error when the input date is incorrect', () => { - expect(() => generateYnabDate('1.1.1')).toThrow('not a valid date'); - }); - }); -}); + expect(() => generateYnabDate('1.1.1')).toThrow('not a valid date') + }) + }) +}) diff --git a/packages/ynap-parsers/src/se/sparbanken-tanum/2019/sparbanken-tanum.ts b/packages/ynap-parsers/src/se/sparbanken-tanum/2019/sparbanken-tanum.ts index b377eab6d..4996a891e 100644 --- a/packages/ynap-parsers/src/se/sparbanken-tanum/2019/sparbanken-tanum.ts +++ b/packages/ynap-parsers/src/se/sparbanken-tanum/2019/sparbanken-tanum.ts @@ -1,95 +1,103 @@ -import 'mdn-polyfills/String.prototype.startsWith'; -import { ParserFunction, MatcherFunction, ParserModule, YnabRow } from '../../..'; -import { parse } from '../../../util/papaparse'; -import { readEncodedFile } from '../../../util/read-encoded-file'; +import 'mdn-polyfills/String.prototype.startsWith' +import { + ParserFunction, + MatcherFunction, + ParserModule, + YnabRow, +} from '../../..' +import { parse } from '../../../util/papaparse' +import { readEncodedFile } from '../../../util/read-encoded-file' export interface Row { - Radnummer: string; - Clearingnummer: string; - Kontonummer: string; - Produkt: string; - Valuta: string; - Bokföringsdag: string; - Transaktionsdag: string; - Valutadag: string; - Referens: string; - Beskrivning: string; - Belopp: string; - 'Bokfört saldo': string; + Radnummer: string + Clearingnummer: string + Kontonummer: string + Produkt: string + Valuta: string + Bokföringsdag: string + Transaktionsdag: string + Valutadag: string + Referens: string + Beskrivning: string + Belopp: string + 'Bokfört saldo': string } export const generateYnabDate = (input: string) => { - const match = input.match(/(\d{4})-(\d{2})-(\d{2})/); + 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'); + 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('/'); -}; + const [, year, month, day] = match + return [month.padStart(2, '0'), day.padStart(2, '0'), year].join('/') +} -export const trimMeta = (input: string) => input.substr(input.indexOf('Radnummer')); +export const trimMeta = (input: string) => + input.substr(input.indexOf('Radnummer')) export const sparbankenTanumParser: ParserFunction = async (file: File) => { const { data } = await parse(trimMeta(await readEncodedFile(file)), { header: true, - }); + }) const groupedData = (data as Row[]) - .filter((r) => r.Radnummer && r.Belopp) + .filter(r => r.Radnummer && r.Belopp) .reduce( (acc, cur) => { - const amount = Number(cur.Belopp); + const amount = Number(cur.Belopp) const data = { Date: generateYnabDate(cur.Transaktionsdag), Payee: cur.Referens, Category: cur.Beskrivning, Outflow: amount < 0 ? Math.abs(amount).toFixed(2) : undefined, Inflow: amount > 0 ? amount.toFixed(2) : undefined, - }; + } - const key = [cur.Produkt, cur.Kontonummer].filter(Boolean).join('-'); + const key = [cur.Produkt, cur.Kontonummer].filter(Boolean).join('-') if (Object.keys(acc).includes(key)) { - acc[key].push(data); + acc[key].push(data) } else { - acc[key] = [data]; + acc[key] = [data] } - return acc; + return acc }, - {} as Record, - ); + {} as Record + ) - return Object.keys(groupedData).map((key) => ({ + return Object.keys(groupedData).map(key => ({ accountName: key, data: groupedData[key], - })); -}; + })) +} export const sparbankenTanumMatcher: MatcherFunction = async (file: File) => { const { data } = await parse(trimMeta(await readEncodedFile(file)), { header: true, - }); + }) // Check if the file contains any data if (data.length === 0) { - return false; + return false } // Check if the first date field is a date if (!String((data[0] as Row).Bokföringsdag).match(/\d{4}-\d{2}-\d{2}/)) { - return false; + return false } // Check if the fourth field is a valid number if (!String((data[0] as Row).Belopp).match(/-?\d+\.\d{2}/)) { - return false; + return false } - return true; -}; + return true +} export const sparbankenTanum: ParserModule = { name: 'Sparbanken Tanum (2019)', @@ -99,4 +107,4 @@ export const sparbankenTanum: ParserModule = { link: 'https://www.sparbankentanum.se/', match: sparbankenTanumMatcher, parse: sparbankenTanumParser, -}; +} diff --git a/packages/ynap-parsers/src/uk/aqua/aqua.spec.ts b/packages/ynap-parsers/src/uk/aqua/aqua.spec.ts index 484b18823..35b715bc0 100644 --- a/packages/ynap-parsers/src/uk/aqua/aqua.spec.ts +++ b/packages/ynap-parsers/src/uk/aqua/aqua.spec.ts @@ -1,9 +1,11 @@ -import { generateYnabDate, aqua } from './aqua'; -import { YnabFile } from '../..'; -import fs from 'fs'; -import path from 'path'; +import { generateYnabDate, aqua } from './aqua' +import { YnabFile } from '../..' +import fs from 'fs' +import path from 'path' -const content = fs.readFileSync(path.join(__dirname, 'test-data/transactions.csv')); +const content = fs.readFileSync( + path.join(__dirname, 'test-data/transactions.csv') +) const ynabResult: YnabFile[] = [ { @@ -20,38 +22,38 @@ const ynabResult: YnabFile[] = [ }, ], }, -]; +] describe('Aqua Parser Module', () => { describe('Matcher', () => { it('should match Aqua files by fields', async () => { - const file = new File([content], 'test.csv'); - const result = await aqua.match(file); - expect(result).toBe(true); - }); + const file = new File([content], 'test.csv') + const result = await aqua.match(file) + expect(result).toBe(true) + }) it('should not match empty files', async () => { - const file = new File([], 'test.csv'); - const result = await aqua.match(file); - expect(result).toBe(false); - }); - }); + const file = new File([], 'test.csv') + const result = await aqua.match(file) + expect(result).toBe(false) + }) + }) describe('Parser', () => { it('should parse data correctly', async () => { - const file = new File([content], 'test.csv'); - const result = await aqua.parse(file); - expect(result).toEqual(ynabResult); - }); - }); + const file = new File([content], 'test.csv') + const result = await aqua.parse(file) + expect(result).toEqual(ynabResult) + }) + }) describe('Date Converter', () => { it('should convert dates correctly', () => { - expect(generateYnabDate('01/09/2018')).toEqual('09/01/2018'); - }); + expect(generateYnabDate('01/09/2018')).toEqual('09/01/2018') + }) it('should throw an error when the input date is incorrect', () => { - expect(() => generateYnabDate('1.1.1')).toThrow('not a valid date'); - }); - }); -}); + expect(() => generateYnabDate('1.1.1')).toThrow('not a valid date') + }) + }) +}) diff --git a/packages/ynap-parsers/src/uk/aqua/aqua.ts b/packages/ynap-parsers/src/uk/aqua/aqua.ts index f1580cfaa..575c87521 100644 --- a/packages/ynap-parsers/src/uk/aqua/aqua.ts +++ b/packages/ynap-parsers/src/uk/aqua/aqua.ts @@ -1,61 +1,61 @@ -import { ParserFunction, MatcherFunction, ParserModule, YnabRow } from '../..'; -import { parse } from '../../util/papaparse'; +import { ParserFunction, MatcherFunction, ParserModule, YnabRow } from '../..' +import { parse } from '../../util/papaparse' export const generateYnabDate = (input: string) => { - const match = input.match(/(\d{2})\/(\d{2})\/(\d{4})/); + const match = input.match(/(\d{2})\/(\d{2})\/(\d{4})/) if (!match) { throw new Error( - 'The input is not a valid date. Expected format: DD/MM/YYYY, got ' + input, - ); + 'The input is not a valid date. Expected format: DD/MM/YYYY, got ' + input + ) } - const [, day, month, year] = match; - return [month, day, year].join('/'); -}; + const [, day, month, year] = match + return [month, day, year].join('/') +} export const aquaParser: ParserFunction = async (file: File) => { - const { data } = await parse(file, { header: false }); + const { data } = await parse(file, { header: false }) const rows = (data as string[][]) .slice(1) - .filter((r) => r.length >= 3) - .filter((r) => r[0] !== 'Pending') + .filter(r => r.length >= 3) + .filter(r => r[0] !== 'Pending') .map( - (cur) => + cur => ({ Date: generateYnabDate(cur[0]), Memo: cur[1].trim().replace(/\s\s+/g, ' '), Inflow: Number(cur[2]) < 0 ? (-Number(cur[2])).toFixed(2) : undefined, Outflow: Number(cur[2]) > 0 ? Number(cur[2]).toFixed(2) : undefined, - }) as YnabRow, - ); + }) as YnabRow + ) return [ { data: rows, }, - ]; -}; + ] +} export const aquaMatcher: MatcherFunction = async (file: File) => { - const { data } = await parse(file, { header: false }); + const { data } = await parse(file, { header: false }) - const requiredKeys = ['Date', 'Description']; + const requiredKeys = ['Date', 'Description'] if (data.length === 0) { - return false; + return false } - const keys = data[0]; - const missingKeys = requiredKeys.filter((k) => !keys.includes(k)); + const keys = data[0] + const missingKeys = requiredKeys.filter(k => !keys.includes(k)) if (missingKeys.length === 0) { - return true; + return true } - return false; -}; + return false +} export const aqua: ParserModule = { name: 'Aqua', @@ -65,4 +65,4 @@ export const aqua: ParserModule = { link: 'https://www.aquacard.co.uk/', match: aquaMatcher, parse: aquaParser, -}; +} diff --git a/packages/ynap-parsers/src/uk/marcus/marcus.spec.ts b/packages/ynap-parsers/src/uk/marcus/marcus.spec.ts index 380d4ecb7..68477a37e 100644 --- a/packages/ynap-parsers/src/uk/marcus/marcus.spec.ts +++ b/packages/ynap-parsers/src/uk/marcus/marcus.spec.ts @@ -1,14 +1,14 @@ -import { generateYnabDate, marcus } from './marcus'; -import { YnabFile } from '../..'; -import fs from 'fs'; -import path from 'path'; +import { generateYnabDate, marcus } from './marcus' +import { YnabFile } from '../..' +import fs from 'fs' +import path from 'path' const content = fs.readFileSync( path.join( __dirname, - 'test-data/Transactions [Account Number] 2019-06-12 13_40.csv', - ), -); + 'test-data/Transactions [Account Number] 2019-06-12 13_40.csv' + ) +) const ynabResult: YnabFile[] = [ { @@ -45,44 +45,44 @@ const ynabResult: YnabFile[] = [ }, ], }, -]; +] describe('Marcus Parser Module', () => { describe('Matcher', () => { it('should match Marcus files by name', async () => { - const fileName = 'Transactions [Account Number] 2019-06-12 13_40.csv'; - const result = !!fileName.match(marcus.filenamePattern); - expect(result).toBe(true); - }); + const fileName = 'Transactions [Account Number] 2019-06-12 13_40.csv' + const result = !!fileName.match(marcus.filenamePattern) + expect(result).toBe(true) + }) it('should match Marcus files by fields', async () => { - const file = new File([content], 'test.csv'); - const result = await marcus.match(file); - expect(result).toBe(true); - }); + const file = new File([content], 'test.csv') + const result = await marcus.match(file) + expect(result).toBe(true) + }) it('should not match empty files', async () => { - const file = new File([], 'test.csv'); - const result = await marcus.match(file); - expect(result).toBe(false); - }); - }); + const file = new File([], 'test.csv') + const result = await marcus.match(file) + expect(result).toBe(false) + }) + }) describe('Parser', () => { it('should parse data correctly', async () => { - const file = new File([content], 'test.csv'); - const result = await marcus.parse(file); - expect(result).toEqual(ynabResult); - }); - }); + const file = new File([content], 'test.csv') + const result = await marcus.parse(file) + expect(result).toEqual(ynabResult) + }) + }) describe('Date Converter', () => { it('should convert dates correctly', () => { - expect(generateYnabDate('20180901')).toEqual('09/01/2018'); - }); + expect(generateYnabDate('20180901')).toEqual('09/01/2018') + }) it('should throw an error when the input date is incorrect', () => { - expect(() => generateYnabDate('1.1.1')).toThrow('not a valid date'); - }); - }); -}); + expect(() => generateYnabDate('1.1.1')).toThrow('not a valid date') + }) + }) +}) diff --git a/packages/ynap-parsers/src/uk/marcus/marcus.ts b/packages/ynap-parsers/src/uk/marcus/marcus.ts index 6a391f0bb..0fdf2099a 100644 --- a/packages/ynap-parsers/src/uk/marcus/marcus.ts +++ b/packages/ynap-parsers/src/uk/marcus/marcus.ts @@ -1,32 +1,32 @@ -import 'mdn-polyfills/String.prototype.startsWith'; -import { ParserFunction, MatcherFunction, ParserModule, YnabRow } from '../..'; -import { parse } from '../../util/papaparse'; +import 'mdn-polyfills/String.prototype.startsWith' +import { ParserFunction, MatcherFunction, ParserModule, YnabRow } from '../..' +import { parse } from '../../util/papaparse' export interface MarcusRow { - TransactionDate: string; - Description: string; - Value: string; - AccountBalance: string; - AccountName: string; - AccountNumber: string; + TransactionDate: string + Description: string + Value: string + AccountBalance: string + AccountName: string + AccountNumber: string } export const generateYnabDate = (input: string) => { - const match = input.match(/(\d{4})(\d{2})(\d{2})/); + const match = input.match(/(\d{4})(\d{2})(\d{2})/) if (!match) { - throw new Error('The input is not a valid date. Expected format: YYYYMMDD'); + throw new Error('The input is not a valid date. Expected format: YYYYMMDD') } - const [, year, month, day] = match; - return [month.padStart(2, '0'), day.padStart(2, '0'), year].join('/'); -}; + const [, year, month, day] = match + return [month.padStart(2, '0'), day.padStart(2, '0'), year].join('/') +} export const marcusParser: ParserFunction = async (file: File) => { - const { data } = await parse(file, { header: true }); + const { data } = await parse(file, { header: true }) const groupedData = (data as MarcusRow[]) - .filter((r) => r.TransactionDate && r.Value) + .filter(r => r.TransactionDate && r.Value) .reduce( (acc, cur) => { const row = { @@ -34,27 +34,28 @@ export const marcusParser: ParserFunction = async (file: File) => { Memo: cur.Description, Outflow: Number(cur.Value) < 0 ? (-Number(cur.Value)).toFixed(2) : undefined, - Inflow: Number(cur.Value) > 0 ? Number(cur.Value).toFixed(2) : undefined, - }; + Inflow: + Number(cur.Value) > 0 ? Number(cur.Value).toFixed(2) : undefined, + } - const key = cur.AccountName || 'no-account'; + const key = cur.AccountName || 'no-account' if (Object.keys(acc).includes(key)) { - acc[key].push(row); + acc[key].push(row) } else { - acc[key] = [row]; + acc[key] = [row] } - return acc; + return acc }, - {} as { [k: string]: YnabRow[] }, - ); + {} as { [k: string]: YnabRow[] } + ) - return Object.keys(groupedData).map((key) => ({ + return Object.keys(groupedData).map(key => ({ accountName: key, data: groupedData[key], - })); -}; + })) +} export const marcusMatcher: MatcherFunction = async (file: File) => { const requiredKeys: (keyof MarcusRow)[] = [ @@ -64,23 +65,23 @@ export const marcusMatcher: MatcherFunction = async (file: File) => { 'AccountBalance', 'AccountName', 'AccountNumber', - ]; + ] - const { data } = await parse(file, { header: true }); + const { data } = await parse(file, { header: true }) if (data.length === 0) { - return false; + return false } - const keys = Object.keys(data[0]); - const missingKeys = requiredKeys.filter((k) => !keys.includes(k)); + const keys = Object.keys(data[0]) + const missingKeys = requiredKeys.filter(k => !keys.includes(k)) if (missingKeys.length === 0) { - return true; + return true } - return false; -}; + return false +} export const marcus: ParserModule = { name: 'Marcus', @@ -91,4 +92,4 @@ export const marcus: ParserModule = { link: 'https://www.marcus.co.uk/uk/en', match: marcusMatcher, parse: marcusParser, -}; +} diff --git a/packages/ynap-parsers/src/util/jschardet.d.ts b/packages/ynap-parsers/src/util/jschardet.d.ts index e106de87c..e3f89eaca 100644 --- a/packages/ynap-parsers/src/util/jschardet.d.ts +++ b/packages/ynap-parsers/src/util/jschardet.d.ts @@ -1,12 +1,12 @@ declare module 'jschardet' { interface Options { - minimumThreshold: number; + minimumThreshold: number } interface Result { - encoding: string | null; - confidence: number; + encoding: string | null + confidence: number } - export const detect: (str: string, options?: Options) => Result; + export const detect: (str: string, options?: Options) => Result } diff --git a/packages/ynap-parsers/src/util/papaparse.ts b/packages/ynap-parsers/src/util/papaparse.ts index 6f040c4d8..7a397e205 100644 --- a/packages/ynap-parsers/src/util/papaparse.ts +++ b/packages/ynap-parsers/src/util/papaparse.ts @@ -1,13 +1,13 @@ -import { parse as papaParse, ParseConfig, ParseResult } from 'papaparse'; +import { parse as papaParse, ParseConfig, ParseResult } from 'papaparse' export const parse = ( file: File | string, - config?: ParseConfig, + config?: ParseConfig ): Promise> => new Promise((complete, error) => { papaParse(file as any, { ...config, complete, error, - }); - }); + }) + }) diff --git a/packages/ynap-parsers/src/util/read-encoded-file.ts b/packages/ynap-parsers/src/util/read-encoded-file.ts index e78b99220..739821d4e 100644 --- a/packages/ynap-parsers/src/util/read-encoded-file.ts +++ b/packages/ynap-parsers/src/util/read-encoded-file.ts @@ -1,29 +1,32 @@ -import chardet from 'jschardet'; -import { decode } from 'iconv-lite'; -import { Buffer } from 'buffer'; +import chardet from 'jschardet' +import { decode } from 'iconv-lite' +import { Buffer } from 'buffer' -export const readEncodedFile = (file: File, charset?: string): Promise => { +export const readEncodedFile = ( + file: File, + charset?: string +): Promise => { return new Promise((res, rej) => { - const reader = new FileReader(); + const reader = new FileReader() reader.addEventListener('load', () => { if (reader.result === null) { - rej('Result is null.'); + rej('Result is null.') } - const result = reader.result! as string; + const result = reader.result! as string if (result.length === 0) { - return res(''); + return res('') } if (!charset) { - const detectedCharset = chardet.detect(result); - charset = detectedCharset ? detectedCharset.encoding : 'utf-8'; + const detectedCharset = chardet.detect(result) + charset = detectedCharset ? detectedCharset.encoding : 'utf-8' } - const decoded = decode(Buffer.from(result, 'binary'), charset); - return res(decoded); - }); - reader.readAsBinaryString(file); - }); -}; + const decoded = decode(Buffer.from(result, 'binary'), charset) + return res(decoded) + }) + reader.readAsBinaryString(file) + }) +} diff --git a/packages/ynap-parsers/src/util/read-to-buffer.ts b/packages/ynap-parsers/src/util/read-to-buffer.ts index ac5f5d41b..87b8a09e3 100644 --- a/packages/ynap-parsers/src/util/read-to-buffer.ts +++ b/packages/ynap-parsers/src/util/read-to-buffer.ts @@ -1,19 +1,19 @@ export const readToBuffer = (file: File): Promise => { return new Promise((res, rej) => { - const reader = new FileReader(); + const reader = new FileReader() reader.addEventListener('load', () => { if (reader.result === null) { - rej('Result is null.'); + rej('Result is null.') } - const result = reader.result! as string; + const result = reader.result! as string if (result.length === 0) { - return res(Buffer.from('')); + return res(Buffer.from('')) } - return res(Buffer.from(result, 'binary')); - }); - reader.readAsBinaryString(file); - }); -}; + return res(Buffer.from(result, 'binary')) + }) + reader.readAsBinaryString(file) + }) +} diff --git a/packages/ynap-web-app/gatsby-browser.js b/packages/ynap-web-app/gatsby-browser.js index cf4ee8755..10e17e11b 100644 --- a/packages/ynap-web-app/gatsby-browser.js +++ b/packages/ynap-web-app/gatsby-browser.js @@ -1,8 +1,8 @@ -const toastify = require('react-toastify'); +const toastify = require('react-toastify') export const onServiceWorkerUpdateReady = () => { toastify.toast.info( `YNAP has been updated. Please click this message or reload the page to load the latest version.`, - { autoClose: false, onClose: () => window.location.reload() }, - ); -}; + { autoClose: false, onClose: () => window.location.reload() } + ) +} diff --git a/packages/ynap-web-app/gatsby-config.js b/packages/ynap-web-app/gatsby-config.js index 639a48aed..b6e00a8bf 100644 --- a/packages/ynap-web-app/gatsby-config.js +++ b/packages/ynap-web-app/gatsby-config.js @@ -1,4 +1,4 @@ -const dateFns = require('date-fns'); +const dateFns = require('date-fns') module.exports = { siteMetadata: { @@ -13,4 +13,4 @@ module.exports = { `gatsby-plugin-netlify`, 'gatsby-plugin-offline', ], -}; +} diff --git a/packages/ynap-web-app/package.json b/packages/ynap-web-app/package.json index c5c95fd30..e5121fb54 100644 --- a/packages/ynap-web-app/package.json +++ b/packages/ynap-web-app/package.json @@ -1,6 +1,6 @@ { "name": "ynap-web-app", - "version": "1.15.101", + "version": "1.15.102", "license": "MIT", "private": true, "dependencies": { diff --git a/packages/ynap-web-app/src/components/github-badge.tsx b/packages/ynap-web-app/src/components/github-badge.tsx index 34fefa963..ae0b8f63c 100644 --- a/packages/ynap-web-app/src/components/github-badge.tsx +++ b/packages/ynap-web-app/src/components/github-badge.tsx @@ -1,5 +1,5 @@ -import React from 'react'; -import styled, { keyframes } from 'styled-components'; +import React from 'react' +import styled, { keyframes } from 'styled-components' const wave = keyframes` 0%, @@ -14,7 +14,7 @@ const wave = keyframes` 80% { transform: rotate(10deg); } -`; +` const Badge = styled.svg` fill: #1a2a40; @@ -31,7 +31,7 @@ const Badge = styled.svg` &:hover .octo-arm { animation: ${wave} 560ms ease-in-out; } -`; +` export const GitHubBadge: React.FC = () => ( ( /> -); +) diff --git a/packages/ynap-web-app/src/components/meta-tags.tsx b/packages/ynap-web-app/src/components/meta-tags.tsx index a0258adde..823fe8d2b 100644 --- a/packages/ynap-web-app/src/components/meta-tags.tsx +++ b/packages/ynap-web-app/src/components/meta-tags.tsx @@ -1,21 +1,21 @@ -import Helmet from 'react-helmet'; -import React from 'react'; +import Helmet from 'react-helmet' +import React from 'react' interface MetaTagsProps { - title?: string; - description?: string; + title?: string + description?: string } const MetaTags: React.FC = ({ title, description }) => { const pageTitle = title ? `${title} – You Need A Parser` - : 'You Need A Parser – Convert bank statements for use with YNAB'; + : 'You Need A Parser – Convert bank statements for use with YNAB' const pageDescription = description || 'YNAP converts CSV files from a variety of sources into a ' + 'format that can easily be imported into You Need A Budget. ' + - 'Your files will never leave your browser.'; + 'Your files will never leave your browser.' return ( @@ -25,9 +25,23 @@ const MetaTags: React.FC = ({ title, description }) => { content="width=device-width, initial-scale=1, shrink-to-fit=no" /> - - - + + + @@ -47,7 +61,7 @@ const MetaTags: React.FC = ({ title, description }) => { - ); -}; + ) +} -export default MetaTags; +export default MetaTags diff --git a/packages/ynap-web-app/src/pages/404.tsx b/packages/ynap-web-app/src/pages/404.tsx index 23cac3c42..ead137dc3 100644 --- a/packages/ynap-web-app/src/pages/404.tsx +++ b/packages/ynap-web-app/src/pages/404.tsx @@ -1,5 +1,5 @@ -import React from 'react'; +import React from 'react' -const NotFoundPage = () =>

Not found.

; +const NotFoundPage = () =>

Not found.

-export default NotFoundPage; +export default NotFoundPage diff --git a/packages/ynap-web-app/src/pages/index.tsx b/packages/ynap-web-app/src/pages/index.tsx index 9c474ebbb..dc944cc9c 100644 --- a/packages/ynap-web-app/src/pages/index.tsx +++ b/packages/ynap-web-app/src/pages/index.tsx @@ -1,14 +1,14 @@ -import React, { useEffect, useState } from 'react'; -import { Link, graphql } from 'gatsby'; -import { saveAs } from 'file-saver'; -import styled, { keyframes, css } from 'styled-components'; -import { ToastContainer, toast } from 'react-toastify'; -import 'react-toastify/dist/ReactToastify.css'; - -import '../styles/index.css'; -import { parseFile, parsers, countries } from '@envelope-zero/ynap-parsers'; -import MetaTags from '../components/meta-tags'; -import { GitHubBadge } from '../components/github-badge'; +import React, { useEffect, useState } from 'react' +import { Link, graphql } from 'gatsby' +import { saveAs } from 'file-saver' +import styled, { keyframes, css } from 'styled-components' +import { ToastContainer, toast } from 'react-toastify' +import 'react-toastify/dist/ReactToastify.css' + +import '../styles/index.css' +import { parseFile, parsers, countries } from '@envelope-zero/ynap-parsers' +import MetaTags from '../components/meta-tags' +import { GitHubBadge } from '../components/github-badge' const pulse = keyframes` 0% { @@ -20,7 +20,7 @@ const pulse = keyframes` 100% { box-shadow: 0 0 0 0 rgba(62, 189, 147, 0); } -`; +` const Container = styled.div<{ uploadHover?: boolean }>` min-height: 100vh; @@ -41,7 +41,7 @@ const Container = styled.div<{ uploadHover?: boolean }>` margin-bottom: 4rem; } - ${(p) => + ${p => p.uploadHover && css` background-color: hsl(218, 40, 90); @@ -55,7 +55,7 @@ const Container = styled.div<{ uploadHover?: boolean }>` border-color: #3ebd93; } `} -`; +` const DropArea = styled.div` display: flex; @@ -71,7 +71,7 @@ const DropArea = styled.div` border-radius: 2rem; transition: border-color 0.2s; -`; +` const UploadIcon = styled.svg` display: block; @@ -83,7 +83,7 @@ const UploadIcon = styled.svg` .arrow-up { transition: transform 0.2s; } -`; +` const Footer = styled.footer` p { @@ -95,93 +95,93 @@ const Footer = styled.footer` opacity: 0.8; } } -`; +` const App: React.FC<{ version: string; commit: string; timestamp: string }> = ({ version, commit, timestamp, }) => { - const [uploadHover, setUploadHover] = useState(false); + const [uploadHover, setUploadHover] = useState(false) useEffect(() => { const enter = (e: DragEvent) => { - setUploadHover(true); - e.preventDefault(); - e.stopPropagation(); - }; + setUploadHover(true) + e.preventDefault() + e.stopPropagation() + } const leave = (e: DragEvent) => { - setUploadHover(false); - e.preventDefault(); - e.stopPropagation(); - }; + setUploadHover(false) + e.preventDefault() + e.stopPropagation() + } const drop = async (e: DragEvent) => { - setUploadHover(false); - e.preventDefault(); - e.stopPropagation(); + setUploadHover(false) + e.preventDefault() + e.stopPropagation() - const files = Array.from(e.dataTransfer!.files); - let errors: number = 0; - let resultCount: number = 0; + const files = Array.from(e.dataTransfer!.files) + let errors: number = 0 + let resultCount: number = 0 for (const file of files) { try { - const result = await parseFile(file); + const result = await parseFile(file) for (const parsedFile of result) { const blob = new Blob([parsedFile.data], { type: 'text/csv;charset=utf-8', - }); + }) const fileName = [ parsedFile.matchedParser.name, parsedFile.accountName, 'ynap', ] - .filter((e) => e) - .join('-'); - saveAs(blob, `${fileName}.csv`); - resultCount++; + .filter(e => e) + .join('-') + saveAs(blob, `${fileName}.csv`) + resultCount++ } } catch (e) { - errors++; + errors++ toast( <> The file {file.name} errored: {e.message} , - { type: 'error' }, - ); - throw e; + { type: 'error' } + ) + throw e } } - const successCount = files.length - errors; + const successCount = files.length - errors if (files.length - errors > 0) { toast( <> Converted {successCount} input{' '} {successCount === 1 ? 'file' : 'files'} into{' '} - {resultCount} {resultCount === 1 ? 'file' : 'files'} for - YNAB + {resultCount}{' '} + {resultCount === 1 ? 'file' : 'files'} for YNAB , - { type: 'success' }, - ); + { type: 'success' } + ) } - }; + } - window.document.body.addEventListener('dragenter', enter); - window.document.body.addEventListener('dragover', enter); - window.document.body.addEventListener('dragleave', leave); - window.document.body.addEventListener('drop', drop); + window.document.body.addEventListener('dragenter', enter) + window.document.body.addEventListener('dragover', enter) + window.document.body.addEventListener('dragleave', leave) + window.document.body.addEventListener('drop', drop) return () => { - window.document.body.removeEventListener('dragenter', enter); - window.document.body.removeEventListener('dragover', enter); - window.document.body.removeEventListener('dragleave', leave); - window.document.body.removeEventListener('drop', drop); - }; - }, []); + window.document.body.removeEventListener('dragenter', enter) + window.document.body.removeEventListener('dragover', enter) + window.document.body.removeEventListener('dragleave', leave) + window.document.body.removeEventListener('drop', drop) + } + }, []) return ( <> @@ -189,13 +189,13 @@ const App: React.FC<{ version: string; commit: string; timestamp: string }> = ({

You Need A Parser

- YNAP converts CSV files from a variety of sources into a format that can - easily be imported into{' '} + YNAP converts CSV files from a variety of sources into a format that + can easily be imported into{' '} You Need A Budget - . Just drag the files you want to convert into this window. Your files will - never leave your browser. + . Just drag the files you want to convert into this window. Your files + will never leave your browser.

= ({ YNAP supports {parsers.length} different formats for banks of{' '} {countries.length} countries, including{' '} {parsers - .map((p) => ( + .map(p => ( <> {p.name} @@ -263,8 +263,8 @@ const App: React.FC<{ version: string; commit: string; timestamp: string }> = ({
- ); -}; + ) +} const Index = ({ data }) => ( <> @@ -276,7 +276,7 @@ const Index = ({ data }) => ( timestamp={data.site.siteMetadata.timestamp} /> -); +) export const query = graphql` { @@ -288,6 +288,6 @@ export const query = graphql` } } } -`; +` -export default Index; +export default Index diff --git a/packages/ynap-web-app/src/pages/supported-formats.tsx b/packages/ynap-web-app/src/pages/supported-formats.tsx index c671a6595..25d1a0d81 100644 --- a/packages/ynap-web-app/src/pages/supported-formats.tsx +++ b/packages/ynap-web-app/src/pages/supported-formats.tsx @@ -1,18 +1,18 @@ -import React, { Fragment } from 'react'; -import styled from 'styled-components'; +import React, { Fragment } from 'react' +import styled from 'styled-components' -import '../styles/index.css'; +import '../styles/index.css' -import { parsers, countries } from '@envelope-zero/ynap-parsers'; -import countryNames from '../util/countries'; -import MetaTags from '../components/meta-tags'; -import { Link } from 'gatsby'; +import { parsers, countries } from '@envelope-zero/ynap-parsers' +import countryNames from '../util/countries' +import MetaTags from '../components/meta-tags' +import { Link } from 'gatsby' const Container = styled.div` padding: 4rem 2rem; max-width: 40rem; margin: auto; -`; +` const ParserPill = styled.a` display: inline-block; @@ -23,17 +23,19 @@ const ParserPill = styled.a` text-decoration: none; background: #3ebd93; color: #fff !important; -`; +` const SupportedFormats = () => ( <> p.name) + .map(p => p.name) .join(', ')}, and more.`} /> @@ -44,13 +46,13 @@ const SupportedFormats = () => (

Supported Formats

- {['international', ...countries].map((c) => ( + {['international', ...countries].map(c => (

{countryNames[c] || c}

{parsers - .filter((p) => p.country === c) - .map((p) => ( + .filter(p => p.country === c) + .map(p => ( {p.name} @@ -60,6 +62,6 @@ const SupportedFormats = () => ( ))} -); +) -export default SupportedFormats; +export default SupportedFormats diff --git a/packages/ynap-web-app/src/util/countries.ts b/packages/ynap-web-app/src/util/countries.ts index 41d09cf32..11f40efa1 100644 --- a/packages/ynap-web-app/src/util/countries.ts +++ b/packages/ynap-web-app/src/util/countries.ts @@ -1,6 +1,6 @@ -import countries from './countries-map.json'; +import countries from './countries-map.json' export default { ...countries, international: 'International', -}; +}