Skip to content

Commit b1d364f

Browse files
authored
Merge pull request #123 from giladtaase/unifyRepos
CSV exporter
2 parents 6c8adf6 + 4a1c427 commit b1d364f

File tree

10 files changed

+294
-5
lines changed

10 files changed

+294
-5
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
"@sentry/electron": "^1.3.0",
3232
"@vue/composition-api": "^1.0.0-beta.14",
3333
"core-js": "^3.4.4",
34+
"csv-parse": "^4.14.1",
35+
"csv-stringify": "^5.5.3",
3436
"direct-vuex": "^0.12.0",
3537
"download-chromium": "^2.2.0",
3638
"electron-log": "^4.1.1",

src/components/app/Exporters.vue

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
<v-toolbar>
44
<v-toolbar-title>Exporters</v-toolbar-title>
55
</v-toolbar>
6+
<expansion-panel title="Csv (Excel)">
7+
<csv-exporter />
8+
</expansion-panel>
69
<expansion-panel title="Json">
710
<json-exporter />
811
</expansion-panel>
@@ -17,14 +20,15 @@
1720

1821
<script lang="ts">
1922
import { defineComponent } from '@vue/composition-api';
23+
import CsvExporter from './exporters/CsvExporter.vue';
2024
import JsonExporter from './exporters/JsonExporter.vue';
2125
import YnabExporter from './exporters/YnabExporter.vue';
2226
import ExpansionPanel from './exporters/ExpansionPanel.vue';
2327
import SpreadsheetExporter from './exporters/googleSheets/SpreadsheetExporter.vue';
2428
2529
export default defineComponent({
2630
components: {
27-
ExpansionPanel, JsonExporter, YnabExporter, SpreadsheetExporter
31+
ExpansionPanel, CsvExporter, JsonExporter, YnabExporter, SpreadsheetExporter
2832
},
2933
});
3034
</script>
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<template>
2+
<v-form
3+
ref="vForm"
4+
v-model="validated"
5+
>
6+
<v-checkbox
7+
v-model="exporter.active"
8+
class="ma-0"
9+
label="Active"
10+
@change="changed = true"
11+
/>
12+
<v-text-field
13+
v-model="exporter.options.filePath"
14+
label="CSV file"
15+
outlined
16+
:rules="[rules.legalPath]"
17+
@change="changed = true"
18+
/>
19+
<v-btn
20+
color="primary"
21+
:disabled="!readyToSave.value"
22+
@click="submit()"
23+
>
24+
Save
25+
</v-btn>
26+
</v-form>
27+
</template>
28+
29+
<script lang="ts">
30+
import { setupExporterConfigForm } from '@/components/app/exporters/exportersCommon';
31+
import { OutputVendorName } from '@/originalBudgetTrackingApp/commonTypes';
32+
import { legalPath } from '@/components/shared/formValidations';
33+
import { defineComponent } from '@vue/composition-api';
34+
35+
export default defineComponent({
36+
name: 'CsvExporter',
37+
38+
setup() {
39+
return {
40+
...setupExporterConfigForm(OutputVendorName.CSV),
41+
rules: {
42+
legalPath
43+
}
44+
};
45+
}
46+
});
47+
48+
</script>
49+
<style scoped/>

src/originalBudgetTrackingApp/configManager/configManager.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export interface Config {
1212
[OutputVendorName.GOOGLE_SHEETS]?: GoogleSheetsConfig;
1313
[OutputVendorName.YNAB]?: YnabConfig;
1414
[OutputVendorName.JSON]?: JsonConfig;
15+
[OutputVendorName.CSV]?: CsvConfig;
1516
};
1617
scraping: {
1718
numDaysBack: number;
@@ -30,7 +31,8 @@ export interface Config {
3031
export enum OutputVendorName {
3132
YNAB = 'ynab',
3233
GOOGLE_SHEETS = 'googleSheets',
33-
JSON = 'json'
34+
JSON = 'json',
35+
CSV = 'csv'
3436
}
3537

3638
export type OutputVendorConfigs = Exclude<Config['outputVendors'][OutputVendorName], undefined>
@@ -40,6 +42,12 @@ interface OutputVendorConfigBase {
4042
active: boolean;
4143
}
4244

45+
export interface CsvConfig extends OutputVendorConfigBase {
46+
options: {
47+
filePath: string;
48+
}
49+
}
50+
4351
export interface JsonConfig extends OutputVendorConfigBase {
4452
options: {
4553
filePath: string;

src/originalBudgetTrackingApp/configManager/defaultConfig.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,14 @@ const DEFAULT_CONFIG: Config = {
77
accountsToScrape: [],
88
},
99
outputVendors: {
10-
json: {
10+
csv: {
1111
active: true,
12+
options: {
13+
filePath: '../transaction.csv'
14+
}
15+
},
16+
json: {
17+
active: false,
1218
options: {
1319
filePath: '../transaction.json'
1420
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import {
2+
EnrichedTransaction, ExportTransactionsFunction, OutputVendor, OutputVendorName
3+
} from '@/originalBudgetTrackingApp/commonTypes';
4+
import { mergeTransactions, sortByDate } from '@/originalBudgetTrackingApp/transactions/transactions';
5+
import { TransactionInstallments } from 'israeli-bank-scrapers-core/lib/transactions';
6+
import { promises as fs } from 'fs';
7+
import stringify from 'csv-stringify/lib/sync';
8+
import parse from 'csv-parse/lib/sync';
9+
10+
export function parseTransactions(csvText: string) {
11+
return parse(csvText, {
12+
columns: true,
13+
cast: (value, context) => {
14+
return parseColumn(value, context.column);
15+
}
16+
}) as EnrichedTransaction[];
17+
}
18+
19+
function parseColumn(value:string, column) {
20+
switch (column) {
21+
case 'chargedAmount':
22+
case 'identifier':
23+
case 'originalAmount':
24+
return value === '' ? undefined : parseFloat(value);
25+
26+
case 'installments':
27+
return parseInstallments(value);
28+
29+
default:
30+
return value;
31+
}
32+
}
33+
34+
function parseInstallments(value: string) {
35+
if (value === '') {
36+
return undefined;
37+
}
38+
const separator = value.indexOf('/');
39+
if (separator !== -1) {
40+
const installments: TransactionInstallments = {
41+
number: parseInt(value.substring(0, separator - 1), 10),
42+
total: parseInt(value.substring(separator + 1), 10)
43+
};
44+
return installments;
45+
}
46+
return value;
47+
}
48+
49+
export const serializeTransactions = (transactions: EnrichedTransaction[]) => {
50+
return stringify(transactions, {
51+
header: true,
52+
columns: [
53+
{ key: 'date' },
54+
{ key: 'description' },
55+
{ key: 'chargedAmount' },
56+
{ key: 'memo' },
57+
{ key: 'category' },
58+
{ key: 'accountNumber' },
59+
{ key: 'installments' },
60+
{ key: 'originalAmount' },
61+
{ key: 'originalCurrency' },
62+
{ key: 'processedDate' },
63+
{ key: 'identifier' },
64+
{ key: 'hash' },
65+
{ key: 'status' },
66+
{ key: 'type' }
67+
],
68+
cast: {
69+
object: (value, context) => {
70+
if (context.column === 'installments' && !context.header) {
71+
return `${value.number} / ${value.total}`;
72+
}
73+
return `${value}`;
74+
}
75+
}
76+
});
77+
};
78+
79+
const exportTransactions: ExportTransactionsFunction = async ({ transactionsToCreate, outputVendorsConfig }) => {
80+
const { filePath } = outputVendorsConfig.csv!.options;
81+
const savedTransactions = await parseTransactionsFile(filePath);
82+
const mergedTransactions = mergeTransactions(savedTransactions, transactionsToCreate);
83+
const sorted = sortByDate(mergedTransactions);
84+
await writeCsvFile(filePath, serializeTransactions(sorted));
85+
};
86+
87+
const parseTransactionsFile = async (filename: string) => {
88+
try {
89+
const content = await fs.readFile(filename, { encoding: 'utf8' });
90+
return parseTransactions(content);
91+
} catch (err) {
92+
if (err.code === 'ENOENT') {
93+
return [] as EnrichedTransaction[];
94+
}
95+
throw err;
96+
}
97+
};
98+
99+
async function writeCsvFile(filePath: string, csvText: string) {
100+
await fs.writeFile(filePath, csvText);
101+
}
102+
103+
export default {
104+
name: OutputVendorName.CSV,
105+
exportTransactions
106+
} as OutputVendor;
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { googleSheetsOutputVendor } from './googleSheets/googleSheets';
1+
import csv from './csv/csv';
22
import json from './json/json';
33
import { ynabOutputVendor } from './ynab/ynab';
4+
import { googleSheetsOutputVendor } from './googleSheets/googleSheets';
45

5-
const outputVendors = [ynabOutputVendor, googleSheetsOutputVendor, json];
6+
const outputVendors = [csv, json, ynabOutputVendor, googleSheetsOutputVendor];
67

78
export default outputVendors;
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`CSV exporter Serialize and parse transactions Basic test case 1`] = `
4+
"date,description,chargedAmount,memo,category,accountNumber,installments,originalAmount,originalCurrency,processedDate,identifier,hash,status,type
5+
2018-11-07T22:00:00.000Z,Ikea,78,hello,Misc,abcd,2 / 7,-50,ILS,2018-11-10T22:00:00.000Z,3982580,111,completed,normal
6+
2018-11-07T22:00:00.000Z,Samuel,1000,amazing memo,Finance,8375982KJHDS2,,932,ILS,2018-11-10T22:00:00.000Z,,11KJFLDKJ22__3231,pending,installments
7+
"
8+
`;
9+
10+
exports[`CSV exporter Serialize and parse transactions Basic test case 2`] = `
11+
Array [
12+
Object {
13+
"accountNumber": "abcd",
14+
"category": "Misc",
15+
"chargedAmount": 78,
16+
"date": "2018-11-07T22:00:00.000Z",
17+
"description": "Ikea",
18+
"hash": "111",
19+
"identifier": 3982580,
20+
"installments": Object {
21+
"number": 2,
22+
"total": 7,
23+
},
24+
"memo": "hello",
25+
"originalAmount": -50,
26+
"originalCurrency": "ILS",
27+
"processedDate": "2018-11-10T22:00:00.000Z",
28+
"status": "completed",
29+
"type": "normal",
30+
},
31+
Object {
32+
"accountNumber": "8375982KJHDS2",
33+
"category": "Finance",
34+
"chargedAmount": 1000,
35+
"date": "2018-11-07T22:00:00.000Z",
36+
"description": "Samuel",
37+
"hash": "11KJFLDKJ22__3231",
38+
"identifier": undefined,
39+
"installments": undefined,
40+
"memo": "amazing memo",
41+
"originalAmount": 932,
42+
"originalCurrency": "ILS",
43+
"processedDate": "2018-11-10T22:00:00.000Z",
44+
"status": "pending",
45+
"type": "installments",
46+
},
47+
]
48+
`;

test/unit/specs/csv.spec.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { TransactionStatuses, TransactionTypes } from 'israeli-bank-scrapers-core/lib/transactions';
2+
import { EnrichedTransaction } from '@/originalBudgetTrackingApp/commonTypes';
3+
import { serializeTransactions, parseTransactions } from '@/originalBudgetTrackingApp/export/outputVendors/csv/csv';
4+
5+
const transactions: EnrichedTransaction[] = [
6+
{
7+
identifier: 3982580,
8+
chargedAmount: 78,
9+
date: '2018-11-07T22:00:00.000Z',
10+
description: 'Ikea',
11+
installments: { number: 2, total: 7 },
12+
memo: 'hello',
13+
originalAmount: -50,
14+
originalCurrency: 'ILS',
15+
processedDate: '2018-11-10T22:00:00.000Z',
16+
accountNumber: 'abcd',
17+
category: 'Misc',
18+
hash: '111',
19+
status: TransactionStatuses.Completed,
20+
type: TransactionTypes.Normal
21+
},
22+
{
23+
identifier: undefined,
24+
chargedAmount: 1000,
25+
date: '2018-11-07T22:00:00.000Z',
26+
description: 'Samuel',
27+
installments: undefined,
28+
memo: 'amazing memo',
29+
originalAmount: 932,
30+
originalCurrency: 'ILS',
31+
processedDate: '2018-11-10T22:00:00.000Z',
32+
accountNumber: '8375982KJHDS2',
33+
category: 'Finance',
34+
hash: '11KJFLDKJ22__3231',
35+
status: TransactionStatuses.Pending,
36+
type: TransactionTypes.Installments
37+
}
38+
];
39+
40+
describe('CSV exporter', () => {
41+
describe('Serialize and parse transactions', () => {
42+
test('Basic test case', () => {
43+
const serialized = serializeTransactions(transactions);
44+
expect(serialized).toMatchSnapshot();
45+
const parsed = parseTransactions(serialized);
46+
expect(parsed).toMatchSnapshot();
47+
});
48+
test('should convert undefined string to empty string', () => {
49+
transactions[1].memo = undefined;
50+
const serialized = serializeTransactions(transactions);
51+
const parsed = parseTransactions(serialized);
52+
expect(parsed[1].memo).toEqual('');
53+
});
54+
});
55+
});

yarn.lock

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4663,6 +4663,16 @@ cssstyle@^2.0.0, cssstyle@^2.2.0:
46634663
dependencies:
46644664
cssom "~0.3.6"
46654665

4666+
csv-parse@^4.14.1:
4667+
version "4.14.1"
4668+
resolved "https://registry.yarnpkg.com/csv-parse/-/csv-parse-4.14.1.tgz#b6b3736508fb94682fa6d450fe1755237221d291"
4669+
integrity sha512-4wmcO7QbWtDAncGFaBwlWFPhEN4Akr64IbM4zvDwEOFekI8blLc04Nw7XjQjtSNy+3AUAgBgtUa9nWo5Cq89Xg==
4670+
4671+
csv-stringify@^5.5.3:
4672+
version "5.5.3"
4673+
resolved "https://registry.yarnpkg.com/csv-stringify/-/csv-stringify-5.5.3.tgz#b7a287daee7492de3722b13dccb238f2d60db522"
4674+
integrity sha512-JKG8vIHpWPzdilp2SAmvjmAiIhD+XGKGdhZBGi8QIECgJAsFr7k5CmJIW2QkSxBBsctvmojM25s+UINzQ5NLTg==
4675+
46664676
currently-unhandled@^0.4.1:
46674677
version "0.4.1"
46684678
resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea"

0 commit comments

Comments
 (0)