israeli-banks-actual-budget-importer 1.3.1 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,22 @@
1
+ # [1.5.0](https://github.com/tomerh2001/israeli-banks-actual-budget-importer/compare/v1.4.0...v1.5.0) (2025-05-24)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * update scraper configuration for executable path and user data directory ([7227ac4](https://github.com/tomerh2001/israeli-banks-actual-budget-importer/commit/7227ac40be5c50c2e8bf95e201af1f260478846c))
7
+
8
+
9
+ ### Features
10
+
11
+ * improved the logs ([3a5f1ac](https://github.com/tomerh2001/israeli-banks-actual-budget-importer/commit/3a5f1aca53be85413225c9e0e75df29cbd70cf91))
12
+
13
+ # [1.4.0](https://github.com/tomerh2001/israeli-banks-actual-budget-importer/compare/v1.3.1...v1.4.0) (2025-05-24)
14
+
15
+
16
+ ### Features
17
+
18
+ * improved the logs ([8ca8c61](https://github.com/tomerh2001/israeli-banks-actual-budget-importer/commit/8ca8c616a394501412262243e98e535a9bace6ad))
19
+
1
20
  ## [1.3.1](https://github.com/tomerh2001/israeli-banks-actual-budget-importer/compare/v1.3.0...v1.3.1) (2025-05-23)
2
21
 
3
22
 
package/README.md CHANGED
@@ -28,6 +28,7 @@ https://hub.docker.com/r/tomerh2001/israeli-banks-actual-budget-importer
28
28
  services:
29
29
  importer:
30
30
  image: tomerh2001/israeli-banks-actual-budget-importer:latest
31
+ restart: always
31
32
  cap_add:
32
33
  - SYS_ADMIN
33
34
  environment:
@@ -39,27 +40,6 @@ services:
39
40
  - ./chrome-data:/app/chrome-data # Optional (Used to solve 2FA issues like with hapoalim)
40
41
  ```
41
42
 
42
- ### Prerequisites
43
-
44
- - **Node.js:** Make sure you have Node.js installed (a version that supports ES modules is recommended).
45
- - **Yarn:** This project uses Yarn as its package manager. Ensure Yarn is installed.
46
- - **TypeScript:** Installed via dependencies.
47
-
48
- ### Steps
49
-
50
- 1. **Clone the Repository:**
51
-
52
- ```bash
53
- git clone https://github.com/tomerh2001/israeli-banks-actual-budget-importer.git
54
- cd israeli-banks-actual-budget-importer
55
- ```
56
-
57
- 2. **Install Dependencies:**
58
-
59
- ```bash
60
- yarn install
61
- ```
62
-
63
43
  ## Configuration
64
44
 
65
45
  The application configuration is defined using JSON and validated against a schema. The key configuration file is `config.json` and its schema is described in `config.schema.json`.
@@ -112,78 +92,6 @@ Example snippet:
112
92
  }
113
93
  ```
114
94
 
115
- ## Usage
116
-
117
- The project comes with scripts configured in the `package.json`. To start the importer:
118
-
119
- ```bash
120
- yarn start
121
- ```
122
-
123
- This command runs the script defined as:
124
-
125
- ```json
126
- "start": "tsx src/index.ts"
127
- ```
128
-
129
- It initializes the connection to the Actual API, downloads the budget, scrapes bank transactions, imports them, and if enabled, performs reconciliation. After processing all tasks in the queue, the application shuts down cleanly.
130
-
131
- ## Development
132
-
133
- ### Overview
134
-
135
- The **Israeli Banks Actual Budget Importer** is a Node.js project written in TypeScript designed to:
136
-
137
- - **Scrape transactions** using specialized bank scrapers.
138
- - **Import transactions** into the Actual budgeting system.
139
- - **Perform account reconciliation** by computing and importing balance differences.
140
- - **Facilitate scheduled imports** by managing concurrent scraping tasks with a queue.
141
-
142
- The main logic is found in `src/index.ts`, which initializes the Actual API, triggers the scraping process, imports transactions, and then gracefully shuts down.
143
-
144
-
145
- ### Project Structure
146
-
147
- - **src/**
148
- Contains the TypeScript source code.
149
- - `index.ts`: Entry point.
150
- - `utils.ts`: Utility functions for scraping and importing transactions.
151
- - `config.d.ts`, `utils.d.ts`: Type definitions.
152
-
153
- - **config.schema.json:**
154
- JSON schema to validate your configuration file.
155
-
156
- - **package.json:**
157
- Lists the dependencies, scripts, and project metadata.
158
-
159
- - **.github, .vscode:**
160
- Contain CI/CD and editor-specific settings.
161
-
162
- ### TypeScript & Linting
163
-
164
- The project uses [XO](https://github.com/xojs/xo) for linting. Ensure you follow coding styles as enforced by XO.
165
-
166
- ## Testing & Linting
167
-
168
- Run tests and linting with the following command:
169
-
170
- ```bash
171
- yarn test
172
- ```
173
-
174
- Ensure that your changes pass linting rules and tests before submitting any pull requests.
175
-
176
- ## Contributing
177
-
178
- Contributions are welcome! To contribute:
179
-
180
- 1. **Fork the Repository:** Create your own fork and clone it locally.
181
- 2. **Create a New Branch:** Use descriptive branch names (e.g., `feature/new-scraper-support`).
182
- 3. **Commit Your Changes:** Follow the commit message guidelines, especially if you are using semantic release.
183
- 4. **Submit a Pull Request:** Ensure your code passes all tests and linting checks.
184
-
185
- Please also review our [CHANGELOG.md](./CHANGELOG.md) and [SECURITY.md](./SECURITY.md) for more context on versioning and security practices.
186
-
187
95
  ## License
188
96
 
189
97
  This project is open-source. Please see the [LICENSE](./LICENSE) file for licensing details.
package/compose.yml CHANGED
@@ -5,12 +5,13 @@ services:
5
5
  cap_add:
6
6
  - SYS_ADMIN
7
7
  platform: linux/amd64
8
+ restart: always
8
9
  build:
9
10
  context: .
10
11
  dockerfile: Dockerfile
11
12
  environment:
12
13
  - TZ=Asia/Jerusalem
13
- - SCHEDULE=0 0 * * *
14
+ # - SCHEDULE=0 */6 * * *
14
15
  volumes:
15
16
  - ./config.json:/app/config.json
16
17
  - ./cache:/app/cache
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.3.1",
2
+ "version": "1.5.0",
3
3
  "name": "israeli-banks-actual-budget-importer",
4
4
  "module": "index.ts",
5
5
  "type": "module",
@@ -21,15 +21,16 @@
21
21
  "papaparse": "^5.5.2",
22
22
  "semantic-release": "^24.2.3",
23
23
  "typescript": "^5.8.3",
24
- "xo": "^0.60.0"
24
+ "xo": "^1.0.0"
25
25
  },
26
26
  "packageManager": "yarn@4.9.1",
27
27
  "dependencies": {
28
28
  "@actual-app/api": "^25.5.0",
29
29
  "cronstrue": "^2.61.0",
30
- "israeli-bank-scrapers": "^5.4.7",
30
+ "israeli-bank-scrapers": "^6.0.0",
31
31
  "lodash": "^4.17.21",
32
32
  "moment": "^2.30.1",
33
+ "mute-stdout": "^2.0.0",
33
34
  "node-cron": "^4.0.7",
34
35
  "p-queue": "^8.1.0",
35
36
  "tsx": "^4.19.4"
package/src/index.ts CHANGED
@@ -1,3 +1,4 @@
1
+ /* eslint-disable @typescript-eslint/no-unsafe-call */
1
2
  /* eslint-disable unicorn/no-process-exit */
2
3
 
3
4
  /* eslint-disable no-await-in-loop */
@@ -11,6 +12,7 @@ import Queue from 'p-queue';
11
12
  import moment from 'moment';
12
13
  import cron, {type ScheduledTask, validate} from 'node-cron';
13
14
  import cronstrue from 'cronstrue';
15
+ import stdout from 'mute-stdout';
14
16
  import config from '../config.json' assert {type: 'json'};
15
17
  import type {ConfigBank} from './config.d.ts';
16
18
  import {scrapeAndImportTransactions} from './utils.ts';
@@ -25,15 +27,20 @@ async function run() {
25
27
  intervalCap: 10,
26
28
  });
27
29
 
30
+ stdout.mute();
28
31
  await actual.init(config.actual.init);
29
32
  await actual.downloadBudget(config.actual.budget.syncId, config.actual.budget);
33
+ stdout.unmute();
30
34
 
31
35
  for (const [companyId, bank] of _.entries(config.banks) as Array<[CompanyTypes, ConfigBank]>) {
32
36
  await queue.add(async () => scrapeAndImportTransactions({companyId, bank}));
33
37
  }
34
38
 
35
39
  await queue.onIdle();
40
+
41
+ stdout.mute();
36
42
  await actual.shutdown();
43
+ stdout.unmute();
37
44
 
38
45
  console.log('Done');
39
46
  }
package/src/utils.ts CHANGED
@@ -1,5 +1,7 @@
1
- /* eslint-disable @typescript-eslint/naming-convention */
2
1
  /* eslint-disable @typescript-eslint/no-unsafe-assignment */
2
+ /* eslint-disable @typescript-eslint/naming-convention */
3
+ /* eslint-disable @typescript-eslint/no-unsafe-call */
4
+
3
5
  /* eslint-disable import/extensions */
4
6
  /* eslint-disable n/file-extension-in-import */
5
7
 
@@ -9,21 +11,26 @@ import _ from 'lodash';
9
11
  import moment from 'moment';
10
12
  import actual from '@actual-app/api';
11
13
  import {type PayeeEntity, type TransactionEntity} from '@actual-app/api/@types/loot-core/types/models';
14
+ import stdout from 'mute-stdout';
12
15
  import {type ScrapeTransactionsContext} from './utils.d';
13
16
 
14
17
  export async function scrapeAndImportTransactions({companyId, bank}: ScrapeTransactionsContext) {
18
+ function log(status: any, other?: Record<string, unknown>) {
19
+ console.debug({bank: companyId, status, ...other});
20
+ }
21
+
15
22
  try {
16
23
  const scraper = createScraper({
17
24
  companyId,
18
25
  startDate: moment().subtract(6, 'month').toDate(),
19
- // ExecutablePath: '/opt/homebrew/bin/chromium',
20
- args: ['--user-data-dir=/app/chrome-data'],
26
+ executablePath: '/opt/homebrew/bin/chromium',
27
+ args: ['--user-data-dir=./chrome-data'],
21
28
  additionalTransactionInformation: true,
22
29
  verbose: process.env?.VERBOSE === 'true',
23
30
  showBrowser: process.env?.SHOW_BROWSER === 'true',
24
31
  });
25
- scraper.onProgress((companyId, payload) => {
26
- console.debug('Progress', companyId, payload);
32
+ scraper.onProgress((_companyId, payload) => {
33
+ log(payload.type);
27
34
  });
28
35
 
29
36
  const result = await scraper.scrape(bank as ScraperCredentials);
@@ -42,11 +49,7 @@ export async function scrapeAndImportTransactions({companyId, bank}: ScrapeTrans
42
49
  }
43
50
  }
44
51
 
45
- const accounts = await actual.getAccounts() as TransactionEntity[];
46
- const account = _.find(accounts, {id: bank.actualAccountId})!;
47
52
  const accountBalance = result.accounts![0].balance!;
48
- console.log('Account', account, 'Balance', accountBalance);
49
-
50
53
  const payees: PayeeEntity[] = await actual.getPayees();
51
54
  const mappedTransactions = transactions.map(async x => ({
52
55
  date: moment(x.date).format('YYYY-MM-DD'),
@@ -57,12 +60,15 @@ export async function scrapeAndImportTransactions({companyId, bank}: ScrapeTrans
57
60
  imported_id: `${x.identifier}-${moment(x.date).format('YYYY-MM-DD HH:mm:ss')}`,
58
61
  }));
59
62
 
63
+ stdout.mute();
60
64
  const importResult = await actual.importTransactions(bank.actualAccountId, await Promise.all(mappedTransactions), {defaultCleared: true});
65
+ stdout.unmute();
66
+
61
67
  if (_.isEmpty(importResult)) {
62
68
  console.error('Errors', importResult.errors);
63
69
  throw new Error('Failed to import transactions');
64
70
  } else {
65
- console.log('Imported', importResult.added, 'transactions');
71
+ log('IMPORTED', {transactions: importResult.added.length});
66
72
  }
67
73
 
68
74
  if (!bank.reconcile) {
@@ -75,7 +81,9 @@ export async function scrapeAndImportTransactions({companyId, bank}: ScrapeTrans
75
81
  return;
76
82
  }
77
83
 
78
- console.log('Balance diff', balanceDiff);
84
+ log('RECONCILIATION', {from: currentBalance, to: accountBalance, diff: balanceDiff});
85
+
86
+ stdout.mute();
79
87
  const reconciliationResult = await actual.importTransactions(bank.actualAccountId, [{
80
88
  date: moment().format('YYYY-MM-DD'),
81
89
  amount: actual.utils.amountToInteger(balanceDiff),
@@ -84,13 +92,17 @@ export async function scrapeAndImportTransactions({companyId, bank}: ScrapeTrans
84
92
  notes: `Reconciliation from ${currentBalance.toLocaleString()} to ${accountBalance.toLocaleString()}`,
85
93
  imported_id: `reconciliation-${moment().format('YYYY-MM-DD HH:mm:ss')}`,
86
94
  }]);
95
+ stdout.unmute();
96
+
87
97
  if (_.isEmpty(reconciliationResult)) {
88
98
  console.error('Reconciliation errors', reconciliationResult.errors);
89
99
  } else {
90
- console.info('Added a reconciliation transaction from', currentBalance, 'to', accountBalance);
100
+ log('RECONCILIATION_ADDED', {transactions: reconciliationResult.added.length});
91
101
  }
92
102
  } catch (error) {
93
103
  console.error('Error', companyId, error);
104
+ } finally {
105
+ log('DONE');
94
106
  }
95
107
  }
96
108