@actual-app/sync-server 25.5.0-alpha.2 → 25.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/README.md CHANGED
@@ -28,12 +28,11 @@ actual-server [options]
28
28
 
29
29
  **Available options**
30
30
 
31
- | Command | Description |
32
- | ------------- |-------------|
33
- |`-h` or `--help` |Print this list and exit. |
34
- |`-v` or `--version` |Print this version and exit. |
35
- |`--config` |Path to the config file. |
36
-
31
+ | Command | Description |
32
+ | ------------------- | ---------------------------- |
33
+ | `-h` or `--help` | Print this list and exit. |
34
+ | `-v` or `--version` | Print this version and exit. |
35
+ | `--config` | Path to the config file. |
37
36
 
38
37
  **Examples**
39
38
 
@@ -43,7 +42,7 @@ Run with default configuration
43
42
  actual-server
44
43
  ```
45
44
 
46
- Runs with custom configuration
45
+ Run with custom configuration
47
46
 
48
47
  ```bash
49
48
  actual-server --config ./config.json
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import { existsSync } from 'node:fs';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { dirname, resolve } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
3
5
  import { parseArgs } from 'node:util';
4
6
 
5
- import packageJson from '../package.json' with { type: 'json' };
6
-
7
7
  const args = process.argv;
8
8
 
9
9
  const options = {
@@ -51,12 +51,38 @@ if (values.help) {
51
51
  }
52
52
 
53
53
  if (values.version) {
54
- console.log('v' + packageJson.version);
54
+ const __dirname = dirname(fileURLToPath(import.meta.url));
55
+ const packageJsonPath = resolve(__dirname, '../package.json');
56
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
55
57
 
58
+ console.log('v' + packageJson.version);
56
59
  process.exit();
57
60
  }
58
61
 
59
- // Read the config argument if specified
62
+ const setupDataDir = (dataDir = undefined) => {
63
+ if (process.env.ACTUAL_DATA_DIR) {
64
+ return; // Env variables must not be overwritten
65
+ }
66
+
67
+ if (dataDir) {
68
+ process.env.ACTUAL_DATA_DIR = dataDir; // Use the dir specified
69
+ } else {
70
+ // Setup defaults
71
+ if (existsSync('./data')) {
72
+ // The default data directory exists - use it
73
+ console.info('Found existing data directory');
74
+ process.env.ACTUAL_DATA_DIR = './data';
75
+ } else {
76
+ console.info(
77
+ 'Using default data directory. You can specify a custom config with --config',
78
+ );
79
+ process.env.ACTUAL_DATA_DIR = './';
80
+ }
81
+
82
+ console.info(`Data directory: ${process.env.ACTUAL_DATA_DIR}`);
83
+ }
84
+ };
85
+
60
86
  if (values.config) {
61
87
  const configExists = existsSync(values.config);
62
88
 
@@ -66,19 +92,25 @@ if (values.config) {
66
92
  );
67
93
 
68
94
  process.exit();
69
- } else if (values.config) {
95
+ } else {
70
96
  console.log(`Loading config from ${values.config}`);
97
+ const configJson = JSON.parse(readFileSync(values.config, 'utf-8'));
71
98
  process.env.ACTUAL_CONFIG_PATH = values.config;
99
+ setupDataDir(configJson.dataDir);
72
100
  }
73
101
  } else {
74
- // No config specified, use reasonable defaults
75
- console.info(
76
- 'Using default config. You can specify a custom config with --config',
77
- );
78
- process.env.ACTUAL_DATA_DIR = './';
79
- console.info(
80
- 'user-files and server-files will be created in the current directory',
81
- );
102
+ // If no config is specified, check for a default config in the current directory
103
+ const defaultConfigJsonFile = './config.json';
104
+ const configExists = existsSync(defaultConfigJsonFile);
105
+
106
+ if (configExists) {
107
+ console.info('Found config.json in the current directory');
108
+ const configJson = JSON.parse(readFileSync(defaultConfigJsonFile, 'utf-8'));
109
+ process.env.ACTUAL_CONFIG_PATH = defaultConfigJsonFile;
110
+ setupDataDir(configJson.dataDir);
111
+ } else {
112
+ setupDataDir(); // No default config exists - setup data dir with defaults
113
+ }
82
114
  }
83
115
 
84
116
  // start the sync server
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@actual-app/sync-server",
3
- "version": "25.5.0-alpha.2",
3
+ "version": "25.5.0",
4
4
  "license": "MIT",
5
5
  "description": "actual syncing server",
6
6
  "bin": {
@@ -20,7 +20,7 @@
20
20
  "start": "node app",
21
21
  "start-monitor": "nodemon app",
22
22
  "build": "tsc",
23
- "test": "NODE_ENV=test NODE_OPTIONS='--experimental-vm-modules --trace-warnings' jest --coverage",
23
+ "test": "NODE_ENV=test NODE_OPTIONS='--experimental-vm-modules --trace-warnings' vitest",
24
24
  "db:migrate": "NODE_ENV=development node src/run-migrations.js up",
25
25
  "db:downgrade": "NODE_ENV=development node src/run-migrations.js down",
26
26
  "db:test-migrate": "NODE_ENV=test node src/run-migrations.js up",
@@ -31,7 +31,7 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "@actual-app/crdt": "2.1.0",
34
- "@actual-app/web": "25.3.1",
34
+ "@actual-app/web": "25.5.0",
35
35
  "bcrypt": "^5.1.1",
36
36
  "better-sqlite3": "^11.9.1",
37
37
  "body-parser": "^1.20.3",
@@ -62,14 +62,14 @@
62
62
  "@types/cors": "^2.8.17",
63
63
  "@types/express": "^5.0.0",
64
64
  "@types/express-actuator": "^1.8.3",
65
- "@types/jest": "^29.5.14",
66
65
  "@types/node": "^22.14.0",
67
66
  "@types/supertest": "^2.0.16",
68
67
  "@types/uuid": "^9.0.8",
69
- "http-proxy-middleware": "^3.0.3",
70
- "jest": "^29.7.0",
68
+ "@vitest/coverage-v8": "3.1.1",
69
+ "http-proxy-middleware": "^3.0.5",
71
70
  "nodemon": "^3.1.9",
72
71
  "supertest": "^6.3.4",
73
- "typescript": "^5.8.2"
72
+ "typescript": "^5.8.2",
73
+ "vitest": "^3.0.2"
74
74
  }
75
75
  }
@@ -1,7 +1,11 @@
1
1
  import { generators, Issuer } from 'openid-client';
2
2
  import { v4 as uuidv4 } from 'uuid';
3
3
 
4
- import { clearExpiredSessions, getAccountDb } from '../account-db.js';
4
+ import {
5
+ clearExpiredSessions,
6
+ getAccountDb,
7
+ listLoginMethods,
8
+ } from '../account-db.js';
5
9
  import { config } from '../load-config.js';
6
10
  import {
7
11
  getUserByUsername,
@@ -99,10 +103,13 @@ export async function loginWithOpenIdSetup(
99
103
  [''],
100
104
  );
101
105
  if (countUsersWithUserName === 0) {
102
- const valid = checkPassword(firstTimeLoginPassword);
106
+ const methods = listLoginMethods();
107
+ if (methods.some(authMethod => authMethod.method === 'password')) {
108
+ const valid = checkPassword(firstTimeLoginPassword);
103
109
 
104
- if (!valid) {
105
- return { error: 'invalid-password' };
110
+ if (!valid) {
111
+ return { error: 'invalid-password' };
112
+ }
106
113
  }
107
114
  }
108
115
 
@@ -84,7 +84,7 @@ app.post('/login', async (req, res) => {
84
84
  break;
85
85
  }
86
86
  case 'openid': {
87
- if (!isValidRedirectUrl(req.body.return_url)) {
87
+ if (!isValidRedirectUrl(req.body.returnUrl)) {
88
88
  res
89
89
  .status(400)
90
90
  .send({ status: 'error', reason: 'Invalid redirect URL' });
@@ -92,14 +92,14 @@ app.post('/login', async (req, res) => {
92
92
  }
93
93
 
94
94
  const { error, url } = await loginWithOpenIdSetup(
95
- req.body.return_url,
95
+ req.body.returnUrl,
96
96
  req.body.password,
97
97
  );
98
98
  if (error) {
99
99
  res.status(400).send({ status: 'error', reason: error });
100
100
  return;
101
101
  }
102
- res.send({ status: 'ok', data: { redirect_url: url } });
102
+ res.send({ status: 'ok', data: { returnUrl: url } });
103
103
  return;
104
104
  }
105
105
 
@@ -1,198 +1,198 @@
1
- # Integration new bank
2
-
3
- If the default bank integration does not work for you, you can integrate a new bank by following these steps.
4
-
5
- 1. Find in [this google doc](https://docs.google.com/spreadsheets/d/1ogpzydzotOltbssrc3IQ8rhBLlIZbQgm5QCiiNJrkyA/edit#gid=489769432) what is the identifier of the bank which you want to integrate.
6
-
7
- 2. Launch frontend and backend server.
8
-
9
- 3. In the frontend, create a new linked account selecting the institution which you are interested in.
10
-
11
- This will trigger the process of fetching the data from the bank and will log the data in the backend. Use this data to fill the logic of the bank class.
12
-
13
- 4. Create new a bank class based on an existing example in `app-gocardless/banks`.
14
-
15
- Name of the file and class should follow the existing patterns and be created based on the ID of the integrated institution, found in step 1.
16
-
17
- 5. Fill the logic of `normalizeAccount`, `normalizeTransaction`, `sortTransactions`, and `calculateStartingBalance` functions.
18
- You do not need to fill every function, only those which are necessary for the integration to work.
19
-
20
- You should do it based on the data which you found in the logs.
21
-
22
- Example logs which help you to fill:
23
-
24
- - `normalizeAccount` function:
25
-
26
- ```log
27
- Available account properties for new institution integration {
28
- account: '{
29
- "iban": "PL00000000000000000987654321",
30
- "currency": "PLN",
31
- "ownerName": "John Example",
32
- "displayName": "Product name",
33
- "product": "Daily account",
34
- "usage": "PRIV",
35
- "ownerAddressUnstructured": [
36
- "POL",
37
- "UL. Example 1",
38
- "00-000 Warsaw"
39
- ],
40
- "id": "XXXXXXXX-XXXX-XXXXX-XXXXXX-XXXXXXXXX",
41
- "created": "2023-01-18T12:15:16.502446Z",
42
- "last_accessed": null,
43
- "institution_id": "MBANK_RETAIL_BREXPLPW",
44
- "status": "READY",
45
- "owner_name": "",
46
- "institution": {
47
- "id": "MBANK_RETAIL_BREXPLPW",
48
- "name": "mBank Retail",
49
- "bic": "BREXPLPW",
50
- "transaction_total_days": "90",
51
- "countries": [
52
- "PL"
53
- ],
54
- "logo": "https://cdn.nordigen.com/ais/MBANK_RETAIL_BREXCZPP.png",
55
- "supported_payments": {},
56
- "supported_features": [
57
- "access_scopes",
58
- "business_accounts",
59
- "card_accounts",
60
- "corporate_accounts",
61
- "pending_transactions",
62
- "private_accounts"
63
- ]
64
- }
65
- }'
66
- }
67
- ```
68
-
69
- - `sortTransactions` function:
70
-
71
- ```log
72
- Available (first 10) transactions properties for new integration of institution in sortTransactions function
73
- {
74
- top10SortedTransactions: '[
75
- {
76
- "transactionId": "20220101001",
77
- "bookingDate": "2022-01-01",
78
- "valueDate": "2022-01-01",
79
- "transactionAmount": {
80
- "amount": "5.01",
81
- "currency": "EUR"
82
- },
83
- "creditorName": "JOHN EXAMPLE",
84
- "creditorAccount": {
85
- "iban": "PL00000000000000000987654321"
86
- },
87
- "debtorName": "CHRIS EXAMPLE",
88
- "debtorAccount": {
89
- "iban": "PL12345000000000000987654321"
90
- },
91
- "remittanceInformationUnstructured": "TEST BANK TRANSFER",
92
- "remittanceInformationUnstructuredArray": [
93
- "TEST BANK TRANSFER"
94
- ],
95
- "balanceAfterTransaction": {
96
- "balanceAmount": {
97
- "amount": "448.52",
98
- "currency": "EUR"
99
- },
100
- "balanceType": "interimBooked"
101
- },
102
- "internalTransactionId": "casfib7720c2a02c0331cw2"
103
- }
104
- ]'
105
- }
106
- ```
107
-
108
- - `calculateStartingBalance` function:
109
-
110
- ```log
111
- Available (first 10) transactions properties for new integration of institution in calculateStartingBalance function {
112
- balances: '[
113
- {
114
- "balanceAmount": {
115
- "amount": "448.52",
116
- "currency": "EUR"
117
- },
118
- "balanceType": "forwardAvailable"
119
- },
120
- {
121
- "balanceAmount": {
122
- "amount": "448.52",
123
- "currency": "EUR"
124
- },
125
- "balanceType": "interimBooked"
126
- }
127
- ]',
128
- top10SortedTransactions: '[
129
- {
130
- "transactionId": "20220101001",
131
- "bookingDate": "2022-01-01",
132
- "valueDate": "2022-01-01",
133
- "transactionAmount": {
134
- "amount": "5.01",
135
- "currency": "EUR"
136
- },
137
- "creditorName": "JOHN EXAMPLE",
138
- "creditorAccount": {
139
- "iban": "PL00000000000000000987654321"
140
- },
141
- "debtorName": "CHRIS EXAMPLE",
142
- "debtorAccount": {
143
- "iban": "PL12345000000000000987654321"
144
- },
145
- "remittanceInformationUnstructured": "TEST BANK TRANSFER",
146
- "remittanceInformationUnstructuredArray": [
147
- "TEST BANK TRANSFER"
148
- ],
149
- "balanceAfterTransaction": {
150
- "balanceAmount": {
151
- "amount": "448.52",
152
- "currency": "EUR"
153
- },
154
- "balanceType": "interimBooked"
155
- },
156
- "internalTransactionId": "casfib7720c2a02c0331cw2"
157
- }
158
- ]'
159
- }
160
- ```
161
-
162
- 6. Add new bank integration to `BankFactory` class in file `actual-server/app-gocardless/bank-factory.js`
163
-
164
- 7. Remember to add tests for new bank integration in
165
-
166
- ## normalizeTransaction
167
-
168
- This is the most commonly used override as it allows you to change the data that is returned to the client.
169
-
170
- Please follow the following patterns when implementing a custom normalizeTransaction method:
171
-
172
- 1. If you need to edit the values of transaction fields (excluding the transaction amount) do not mutate the original transaction object. Instead, create a shallow copy and make your changes there.
173
- 2. End the function by returning the result of calling the fallback normalizeTransaction method from integration-bank.js
174
-
175
- E.g.
176
-
177
- ```js
178
- import Fallback from './integration-bank.js';
179
-
180
- export default {
181
- ...
182
-
183
- normalizeTransaction(transaction, booked) {
184
- // create a shallow copy of the transaction object
185
- const editedTrans = { ...transaction };
186
-
187
- // make any changes required to the copy
188
- editedTrans.remittanceInformationUnstructured = transaction.remittanceInformationStructured;
189
-
190
- // call the fallback method, passing in your edited transaction as the 3rd parameter
191
- // this will calculate the date, payee name and notes fields based on your changes
192
- // but leave the original fields available for mapping in the UI
193
- return Fallback.normalizeTransaction(transaction, booked, editedTrans);
194
- }
195
-
196
- ...
197
- }
198
- ```
1
+ # Integration new bank
2
+
3
+ If the default bank integration does not work for you, you can integrate a new bank by following these steps.
4
+
5
+ 1. Find in [this google doc](https://docs.google.com/spreadsheets/d/1ogpzydzotOltbssrc3IQ8rhBLlIZbQgm5QCiiNJrkyA/edit#gid=489769432) what is the identifier of the bank which you want to integrate.
6
+
7
+ 2. Launch frontend and backend server.
8
+
9
+ 3. In the frontend, create a new linked account selecting the institution which you are interested in.
10
+
11
+ This will trigger the process of fetching the data from the bank and will log the data in the backend. Use this data to fill the logic of the bank class.
12
+
13
+ 4. Create new a bank class based on an existing example in `app-gocardless/banks`.
14
+
15
+ Name of the file and class should follow the existing patterns and be created based on the ID of the integrated institution, found in step 1.
16
+
17
+ 5. Fill the logic of `normalizeAccount`, `normalizeTransaction`, `sortTransactions`, and `calculateStartingBalance` functions.
18
+ You do not need to fill every function, only those which are necessary for the integration to work.
19
+
20
+ You should do it based on the data which you found in the logs.
21
+
22
+ Example logs which help you to fill:
23
+
24
+ - `normalizeAccount` function:
25
+
26
+ ```log
27
+ Available account properties for new institution integration {
28
+ account: '{
29
+ "iban": "PL00000000000000000987654321",
30
+ "currency": "PLN",
31
+ "ownerName": "John Example",
32
+ "displayName": "Product name",
33
+ "product": "Daily account",
34
+ "usage": "PRIV",
35
+ "ownerAddressUnstructured": [
36
+ "POL",
37
+ "UL. Example 1",
38
+ "00-000 Warsaw"
39
+ ],
40
+ "id": "XXXXXXXX-XXXX-XXXXX-XXXXXX-XXXXXXXXX",
41
+ "created": "2023-01-18T12:15:16.502446Z",
42
+ "last_accessed": null,
43
+ "institution_id": "MBANK_RETAIL_BREXPLPW",
44
+ "status": "READY",
45
+ "owner_name": "",
46
+ "institution": {
47
+ "id": "MBANK_RETAIL_BREXPLPW",
48
+ "name": "mBank Retail",
49
+ "bic": "BREXPLPW",
50
+ "transaction_total_days": "90",
51
+ "countries": [
52
+ "PL"
53
+ ],
54
+ "logo": "https://cdn.nordigen.com/ais/MBANK_RETAIL_BREXCZPP.png",
55
+ "supported_payments": {},
56
+ "supported_features": [
57
+ "access_scopes",
58
+ "business_accounts",
59
+ "card_accounts",
60
+ "corporate_accounts",
61
+ "pending_transactions",
62
+ "private_accounts"
63
+ ]
64
+ }
65
+ }'
66
+ }
67
+ ```
68
+
69
+ - `sortTransactions` function:
70
+
71
+ ```log
72
+ Available (first 10) transactions properties for new integration of institution in sortTransactions function
73
+ {
74
+ top10SortedTransactions: '[
75
+ {
76
+ "transactionId": "20220101001",
77
+ "bookingDate": "2022-01-01",
78
+ "valueDate": "2022-01-01",
79
+ "transactionAmount": {
80
+ "amount": "5.01",
81
+ "currency": "EUR"
82
+ },
83
+ "creditorName": "JOHN EXAMPLE",
84
+ "creditorAccount": {
85
+ "iban": "PL00000000000000000987654321"
86
+ },
87
+ "debtorName": "CHRIS EXAMPLE",
88
+ "debtorAccount": {
89
+ "iban": "PL12345000000000000987654321"
90
+ },
91
+ "remittanceInformationUnstructured": "TEST BANK TRANSFER",
92
+ "remittanceInformationUnstructuredArray": [
93
+ "TEST BANK TRANSFER"
94
+ ],
95
+ "balanceAfterTransaction": {
96
+ "balanceAmount": {
97
+ "amount": "448.52",
98
+ "currency": "EUR"
99
+ },
100
+ "balanceType": "interimBooked"
101
+ },
102
+ "internalTransactionId": "casfib7720c2a02c0331cw2"
103
+ }
104
+ ]'
105
+ }
106
+ ```
107
+
108
+ - `calculateStartingBalance` function:
109
+
110
+ ```log
111
+ Available (first 10) transactions properties for new integration of institution in calculateStartingBalance function {
112
+ balances: '[
113
+ {
114
+ "balanceAmount": {
115
+ "amount": "448.52",
116
+ "currency": "EUR"
117
+ },
118
+ "balanceType": "forwardAvailable"
119
+ },
120
+ {
121
+ "balanceAmount": {
122
+ "amount": "448.52",
123
+ "currency": "EUR"
124
+ },
125
+ "balanceType": "interimBooked"
126
+ }
127
+ ]',
128
+ top10SortedTransactions: '[
129
+ {
130
+ "transactionId": "20220101001",
131
+ "bookingDate": "2022-01-01",
132
+ "valueDate": "2022-01-01",
133
+ "transactionAmount": {
134
+ "amount": "5.01",
135
+ "currency": "EUR"
136
+ },
137
+ "creditorName": "JOHN EXAMPLE",
138
+ "creditorAccount": {
139
+ "iban": "PL00000000000000000987654321"
140
+ },
141
+ "debtorName": "CHRIS EXAMPLE",
142
+ "debtorAccount": {
143
+ "iban": "PL12345000000000000987654321"
144
+ },
145
+ "remittanceInformationUnstructured": "TEST BANK TRANSFER",
146
+ "remittanceInformationUnstructuredArray": [
147
+ "TEST BANK TRANSFER"
148
+ ],
149
+ "balanceAfterTransaction": {
150
+ "balanceAmount": {
151
+ "amount": "448.52",
152
+ "currency": "EUR"
153
+ },
154
+ "balanceType": "interimBooked"
155
+ },
156
+ "internalTransactionId": "casfib7720c2a02c0331cw2"
157
+ }
158
+ ]'
159
+ }
160
+ ```
161
+
162
+ 6. Add new bank integration to `BankFactory` class in file `actual-server/app-gocardless/bank-factory.js`
163
+
164
+ 7. Remember to add tests for new bank integration in
165
+
166
+ ## normalizeTransaction
167
+
168
+ This is the most commonly used override as it allows you to change the data that is returned to the client.
169
+
170
+ Please follow the following patterns when implementing a custom normalizeTransaction method:
171
+
172
+ 1. If you need to edit the values of transaction fields (excluding the transaction amount) do not mutate the original transaction object. Instead, create a shallow copy and make your changes there.
173
+ 2. End the function by returning the result of calling the fallback normalizeTransaction method from integration-bank.js
174
+
175
+ E.g.
176
+
177
+ ```js
178
+ import Fallback from './integration-bank.js';
179
+
180
+ export default {
181
+ ...
182
+
183
+ normalizeTransaction(transaction, booked) {
184
+ // create a shallow copy of the transaction object
185
+ const editedTrans = { ...transaction };
186
+
187
+ // make any changes required to the copy
188
+ editedTrans.remittanceInformationUnstructured = transaction.remittanceInformationStructured;
189
+
190
+ // call the fallback method, passing in your edited transaction as the 3rd parameter
191
+ // this will calculate the date, payee name and notes fields based on your changes
192
+ // but leave the original fields available for mapping in the UI
193
+ return Fallback.normalizeTransaction(transaction, booked, editedTrans);
194
+ }
195
+
196
+ ...
197
+ }
198
+ ```
@@ -1,4 +1,5 @@
1
1
  import Fallback from './integration-bank.js';
2
+ import { escapeRegExp } from './util/escape-regexp.js';
2
3
 
3
4
  /** @type {import('./bank.interface.js').IBank} */
4
5
  export default {
@@ -38,7 +39,9 @@ export default {
38
39
  // Clean up remittanceInformation, deduplicate payee (removing slashes ...
39
40
  // ... that are added to the remittanceInformation field), and ...
40
41
  // ... remove clutter like "End-to-End-Ref.: NOTPROVIDED"
41
- const payee = transaction.creditorName || transaction.debtorName || '';
42
+ const payee = escapeRegExp(
43
+ transaction.creditorName || transaction.debtorName || '',
44
+ );
42
45
  editedTrans.remittanceInformationUnstructured =
43
46
  editedTrans.remittanceInformationUnstructured
44
47
  .replace(/\s*(,)?\s+/g, '$1 ')
@@ -10,6 +10,7 @@ export default {
10
10
  'SEB_KORT_AB_NO_SKHSFI21',
11
11
  'SEB_KORT_AB_SE_SKHSFI21',
12
12
  'SEB_CARD_ESSESESS',
13
+ 'NORDIC_CHOICE_CLUB_NO_SKHSFI21',
13
14
  ],
14
15
 
15
16
  /**
@@ -106,5 +106,28 @@ describe('CommerzbankCobadeff', () => {
106
106
  'CREDITOR00BIC CREDITOR000IBAN DESCRIPTION, Dauerauftrag',
107
107
  );
108
108
  });
109
+
110
+ it('correctly uses regex on payee with special characters', () => {
111
+ const transaction = {
112
+ endToEndId: '1234567890',
113
+ mandateId: '1234567890',
114
+ bookingDate: '2025-04-18',
115
+ valueDate: '2025-04-18',
116
+ transactionAmount: {
117
+ amount: '-1',
118
+ currency: 'EUR',
119
+ },
120
+ creditorName: 'Netto Marken-Discount Halle (Saale',
121
+ remittanceInformationUnstructured: 'Example',
122
+ remittanceInformationUnstructuredArray: ['Example'],
123
+ remittanceInformationStructured: 'Example',
124
+ // internalTransactionId: '3815213adb654baeadfb231c853',
125
+ };
126
+ const normalizedTransaction = CommerzbankCobadeff.normalizeTransaction(
127
+ transaction,
128
+ false,
129
+ );
130
+ expect(normalizedTransaction.notes).toEqual('Example');
131
+ });
109
132
  });
110
133
  });
@@ -1,5 +1,3 @@
1
- import { jest } from '@jest/globals';
2
-
3
1
  import {
4
2
  mockExtendAccountsAboutInstitutions,
5
3
  mockInstitution,
@@ -10,7 +8,7 @@ describe('IntegrationBank', () => {
10
8
  let consoleSpy;
11
9
 
12
10
  beforeEach(() => {
13
- consoleSpy = jest.spyOn(console, 'debug');
11
+ consoleSpy = vi.spyOn(console, 'debug');
14
12
  });
15
13
 
16
14
  describe('normalizeAccount', () => {
@@ -201,9 +201,8 @@ describe('SpkMarburgBiedenkopfHeladef1mar', () => {
201
201
  normalizeTransactions[a] = normalizeTransactions[b];
202
202
  normalizeTransactions[b] = swap;
203
203
  };
204
- swap(1, 4);
205
- swap(3, 6);
206
- swap(0, 7);
204
+ swap(1, 3);
205
+ swap(0, 2);
207
206
  const sortedTransactions =
208
207
  SpkMarburgBiedenkopfHeladef1mar.sortTransactions(normalizeTransactions);
209
208
  expect(sortedTransactions).toEqual(originalOrder);
@@ -1,12 +1,10 @@
1
- import { jest } from '@jest/globals';
2
-
3
1
  import SskDusseldorfDussdeddxxx from '../ssk_dusseldorf_dussdeddxxx.js';
4
2
 
5
3
  describe('ssk_dusseldorf_dussdeddxxx', () => {
6
4
  let consoleSpy;
7
5
 
8
6
  beforeEach(() => {
9
- consoleSpy = jest.spyOn(console, 'debug');
7
+ consoleSpy = vi.spyOn(console, 'debug');
10
8
  });
11
9
 
12
10
  afterEach(() => {
@@ -0,0 +1,4 @@
1
+ // Escape special characters in the string to create a valid regular expression
2
+ export function escapeRegExp(string) {
3
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
4
+ }
@@ -1,18 +1,18 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="utf-8" />
5
- <title>Actual</title>
6
- </head>
7
- <body>
8
- <script>
9
- window.close();
10
- </script>
11
-
12
- <p>Please wait...</p>
13
- <p>
14
- The window should close automatically. If nothing happened you can close
15
- this window or tab.
16
- </p>
17
- </body>
18
- </html>
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>Actual</title>
6
+ </head>
7
+ <body>
8
+ <script>
9
+ window.close();
10
+ </script>
11
+
12
+ <p>Please wait...</p>
13
+ <p>
14
+ The window should close automatically. If nothing happened you can close
15
+ this window or tab.
16
+ </p>
17
+ </body>
18
+ </html>
@@ -1,5 +1,3 @@
1
- import { jest } from '@jest/globals';
2
-
3
1
  import {
4
2
  AccessDeniedError,
5
3
  AccountNotLinkedToRequisition,
@@ -51,29 +49,29 @@ describe('goCardlessService', () => {
51
49
  let setTokenSpy;
52
50
 
53
51
  beforeEach(() => {
54
- getInstitutionsSpy = jest.spyOn(client, 'getInstitutions');
55
- getInstitutionSpy = jest.spyOn(client, 'getInstitutionById');
56
- getRequisitionsSpy = jest.spyOn(client, 'getRequisitionById');
57
- deleteRequisitionsSpy = jest.spyOn(client, 'deleteRequisition');
58
- createRequisitionSpy = jest.spyOn(client, 'initSession');
59
- getBalancesSpy = jest.spyOn(client, 'getBalances');
60
- getTransactionsSpy = jest.spyOn(client, 'getTransactions');
61
- getDetailsSpy = jest.spyOn(client, 'getDetails');
62
- getMetadataSpy = jest.spyOn(client, 'getMetadata');
63
- setTokenSpy = jest.spyOn(goCardlessService, 'setToken');
52
+ getInstitutionsSpy = vi.spyOn(client, 'getInstitutions');
53
+ getInstitutionSpy = vi.spyOn(client, 'getInstitutionById');
54
+ getRequisitionsSpy = vi.spyOn(client, 'getRequisitionById');
55
+ deleteRequisitionsSpy = vi.spyOn(client, 'deleteRequisition');
56
+ createRequisitionSpy = vi.spyOn(client, 'initSession');
57
+ getBalancesSpy = vi.spyOn(client, 'getBalances');
58
+ getTransactionsSpy = vi.spyOn(client, 'getTransactions');
59
+ getDetailsSpy = vi.spyOn(client, 'getDetails');
60
+ getMetadataSpy = vi.spyOn(client, 'getMetadata');
61
+ setTokenSpy = vi.spyOn(goCardlessService, 'setToken');
64
62
  });
65
63
 
66
64
  afterEach(() => {
67
- jest.resetAllMocks();
65
+ vi.resetAllMocks();
68
66
  });
69
67
 
70
68
  describe('#getLinkedRequisition', () => {
71
69
  it('returns requisition', async () => {
72
70
  setTokenSpy.mockResolvedValue();
73
71
 
74
- jest
75
- .spyOn(goCardlessService, 'getRequisition')
76
- .mockResolvedValue(mockRequisition);
72
+ vi.spyOn(goCardlessService, 'getRequisition').mockResolvedValue(
73
+ mockRequisition,
74
+ );
77
75
 
78
76
  expect(
79
77
  await goCardlessService.getLinkedRequisition(requisitionId),
@@ -83,9 +81,10 @@ describe('goCardlessService', () => {
83
81
  it('throws RequisitionNotLinked error if requisition status is different than LN', async () => {
84
82
  setTokenSpy.mockResolvedValue();
85
83
 
86
- jest
87
- .spyOn(goCardlessService, 'getRequisition')
88
- .mockResolvedValue({ ...mockRequisition, status: 'ER' });
84
+ vi.spyOn(goCardlessService, 'getRequisition').mockResolvedValue({
85
+ ...mockRequisition,
86
+ status: 'ER',
87
+ });
89
88
 
90
89
  await expect(() =>
91
90
  goCardlessService.getLinkedRequisition(requisitionId),
@@ -95,30 +94,31 @@ describe('goCardlessService', () => {
95
94
 
96
95
  describe('#getRequisitionWithAccounts', () => {
97
96
  it('returns combined data', async () => {
98
- jest
99
- .spyOn(goCardlessService, 'getRequisition')
100
- .mockResolvedValue(mockRequisitionWithExampleAccounts);
101
- jest
102
- .spyOn(goCardlessService, 'getDetailedAccount')
103
- .mockResolvedValueOnce(mockDetailedAccountExample1);
104
- jest
105
- .spyOn(goCardlessService, 'getDetailedAccount')
106
- .mockResolvedValueOnce(mockDetailedAccountExample2);
107
- jest
108
- .spyOn(goCardlessService, 'getInstitution')
109
- .mockResolvedValue(mockInstitution);
110
- jest
111
- .spyOn(goCardlessService, 'extendAccountsAboutInstitutions')
112
- .mockResolvedValue([
113
- {
114
- ...mockExtendAccountsAboutInstitutions[0],
115
- institution_id: 'NEWONE',
116
- },
117
- {
118
- ...mockExtendAccountsAboutInstitutions[1],
119
- institution_id: 'NEWONE',
120
- },
121
- ]);
97
+ vi.spyOn(goCardlessService, 'getRequisition').mockResolvedValue(
98
+ mockRequisitionWithExampleAccounts,
99
+ );
100
+ vi.spyOn(goCardlessService, 'getDetailedAccount').mockResolvedValueOnce(
101
+ mockDetailedAccountExample1,
102
+ );
103
+ vi.spyOn(goCardlessService, 'getDetailedAccount').mockResolvedValueOnce(
104
+ mockDetailedAccountExample2,
105
+ );
106
+ vi.spyOn(goCardlessService, 'getInstitution').mockResolvedValue(
107
+ mockInstitution,
108
+ );
109
+ vi.spyOn(
110
+ goCardlessService,
111
+ 'extendAccountsAboutInstitutions',
112
+ ).mockResolvedValue([
113
+ {
114
+ ...mockExtendAccountsAboutInstitutions[0],
115
+ institution_id: 'NEWONE',
116
+ },
117
+ {
118
+ ...mockExtendAccountsAboutInstitutions[1],
119
+ institution_id: 'NEWONE',
120
+ },
121
+ ]);
122
122
 
123
123
  const response = await goCardlessService.getRequisitionWithAccounts(
124
124
  mockRequisitionWithExampleAccounts.id,
@@ -146,18 +146,18 @@ describe('goCardlessService', () => {
146
146
  describe('#getTransactionsWithBalance', () => {
147
147
  const requisitionId = mockRequisition.id;
148
148
  it('returns transaction with starting balance', async () => {
149
- jest
150
- .spyOn(goCardlessService, 'getLinkedRequisition')
151
- .mockResolvedValue(mockRequisition);
152
- jest
153
- .spyOn(goCardlessService, 'getAccountMetadata')
154
- .mockResolvedValue(mockAccountMetaData);
155
- jest
156
- .spyOn(goCardlessService, 'getTransactions')
157
- .mockResolvedValue(mockTransactions);
158
- jest
159
- .spyOn(goCardlessService, 'getBalances')
160
- .mockResolvedValue(mockedBalances);
149
+ vi.spyOn(goCardlessService, 'getLinkedRequisition').mockResolvedValue(
150
+ mockRequisition,
151
+ );
152
+ vi.spyOn(goCardlessService, 'getAccountMetadata').mockResolvedValue(
153
+ mockAccountMetaData,
154
+ );
155
+ vi.spyOn(goCardlessService, 'getTransactions').mockResolvedValue(
156
+ mockTransactions,
157
+ );
158
+ vi.spyOn(goCardlessService, 'getBalances').mockResolvedValue(
159
+ mockedBalances,
160
+ );
161
161
 
162
162
  expect(
163
163
  await goCardlessService.getTransactionsWithBalance(
@@ -216,9 +216,9 @@ describe('goCardlessService', () => {
216
216
  });
217
217
 
218
218
  it('throws AccountNotLinkedToRequisition error if requisition accounts not includes requested account', async () => {
219
- jest
220
- .spyOn(goCardlessService, 'getLinkedRequisition')
221
- .mockResolvedValue(mockRequisition);
219
+ vi.spyOn(goCardlessService, 'getLinkedRequisition').mockResolvedValue(
220
+ mockRequisition,
221
+ );
222
222
 
223
223
  await expect(() =>
224
224
  goCardlessService.getTransactionsWithBalance({
@@ -352,67 +352,54 @@ async function getAccounts(
352
352
  noTransactions = false,
353
353
  ) {
354
354
  const sfin = parseAccessKey(accessKey);
355
- const options = {
356
- headers: {
357
- Authorization: `Basic ${Buffer.from(
358
- `${sfin.username}:${sfin.password}`,
359
- ).toString('base64')}`,
360
- },
355
+
356
+ const headers = {
357
+ Authorization: `Basic ${Buffer.from(
358
+ `${sfin.username}:${sfin.password}`,
359
+ ).toString('base64')}`,
361
360
  };
362
- const params = [];
361
+
362
+ const params = new URLSearchParams();
363
363
  if (!noTransactions) {
364
364
  if (startDate) {
365
- params.push(`start-date=${normalizeDate(startDate)}`);
365
+ params.append('start-date', normalizeDate(startDate));
366
366
  }
367
367
  if (endDate) {
368
- params.push(`end-date=${normalizeDate(endDate)}`);
368
+ params.append('end-date', normalizeDate(endDate));
369
369
  }
370
-
371
- params.push(`pending=1`);
370
+ params.append('pending', '1');
372
371
  } else {
373
- params.push(`balances-only=1`);
372
+ params.append('balances-only', '1');
374
373
  }
375
374
 
376
375
  if (accounts) {
377
- accounts.forEach(id => {
378
- params.push(`account=${encodeURIComponent(id)}`);
379
- });
376
+ for (const id of accounts) {
377
+ params.append('account', id);
378
+ }
380
379
  }
381
380
 
382
- let queryString = '';
383
- if (params.length > 0) {
384
- queryString += '?' + params.join('&');
385
- }
386
- return new Promise((resolve, reject) => {
387
- const req = https.request(
388
- new URL(`${sfin.baseUrl}/accounts${queryString}`),
389
- options,
390
- res => {
391
- let data = '';
392
- res.on('data', d => {
393
- data += d;
394
- });
395
- res.on('end', () => {
396
- if (res.statusCode === 403) {
397
- reject(new Error('Forbidden'));
398
- } else {
399
- try {
400
- const results = JSON.parse(data);
401
- results.sferrors = results.errors;
402
- results.hasError = false;
403
- results.errors = {};
404
- resolve(results);
405
- } catch (e) {
406
- console.log(`Error parsing JSON response: ${data}`);
407
- reject(e);
408
- }
409
- }
410
- });
411
- },
412
- );
413
- req.on('error', e => {
414
- reject(e);
415
- });
416
- req.end();
381
+ const url = new URL(`${sfin.baseUrl}/accounts`);
382
+ url.search = params.toString();
383
+
384
+ const response = await fetch(url.toString(), {
385
+ method: 'GET',
386
+ headers,
387
+ redirect: 'follow',
417
388
  });
389
+
390
+ if (response.status === 403) {
391
+ throw new Error('Forbidden');
392
+ }
393
+
394
+ const text = await response.text();
395
+ try {
396
+ const results = JSON.parse(text);
397
+ results.sferrors = results.errors;
398
+ results.hasError = false;
399
+ results.errors = {};
400
+ return results;
401
+ } catch (e) {
402
+ console.log(`Error parsing JSON response: ${text}`);
403
+ throw e;
404
+ }
418
405
  }
@@ -33,20 +33,17 @@ describe('FilesService', () => {
33
33
  accountDb.mutate('DELETE FROM files');
34
34
  };
35
35
 
36
- beforeAll(done => {
36
+ beforeAll(() => {
37
37
  accountDb = getAccountDb();
38
38
  filesService = new FilesService(accountDb);
39
- done();
40
39
  });
41
40
 
42
- beforeEach(done => {
41
+ beforeEach(() => {
43
42
  insertToyExampleData();
44
- done();
45
43
  });
46
44
 
47
- afterEach(done => {
45
+ afterEach(() => {
48
46
  clearDatabase();
49
- done();
50
47
  });
51
48
 
52
49
  test('get should return a file', () => {
package/src/app.js CHANGED
@@ -108,10 +108,14 @@ function parseHTTPSConfig(value) {
108
108
  }
109
109
 
110
110
  export async function run() {
111
- if (config.openId) {
111
+ const openIdConfig = config?.getProperties()?.openId;
112
+ if (
113
+ openIdConfig?.discoveryURL ||
114
+ openIdConfig?.issuer?.authorization_endpoint
115
+ ) {
112
116
  console.log('OpenID configuration found. Preparing server to use it');
113
117
  try {
114
- const { error } = await bootstrap({ openId: config.openId }, true);
118
+ const { error } = await bootstrap({ openId: openIdConfig }, true);
115
119
  if (error) {
116
120
  console.log(error);
117
121
  } else {
@@ -1,9 +1,9 @@
1
-
2
- CREATE TABLE messages_binary
3
- (timestamp TEXT PRIMARY KEY,
4
- is_encrypted BOOLEAN,
5
- content bytea);
6
-
7
- CREATE TABLE messages_merkles
8
- (id INTEGER PRIMARY KEY,
9
- merkle TEXT);
1
+
2
+ CREATE TABLE messages_binary
3
+ (timestamp TEXT PRIMARY KEY,
4
+ is_encrypted BOOLEAN,
5
+ content bytea);
6
+
7
+ CREATE TABLE messages_merkles
8
+ (id INTEGER PRIMARY KEY,
9
+ merkle TEXT);