@actual-app/sync-server 25.4.0-alpha.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/.dockerignore +12 -0
- package/README.md +19 -0
- package/app.js +11 -0
- package/babel.config.json +3 -0
- package/bin/@actual-app/sync-server +55 -0
- package/docker/alpine.Dockerfile +62 -0
- package/docker/ubuntu.Dockerfile +63 -0
- package/docker-compose.yml +29 -0
- package/jest.config.json +19 -0
- package/jest.global-setup.js +101 -0
- package/jest.global-teardown.js +6 -0
- package/migrations/1694360000000-create-folders.js +25 -0
- package/migrations/1694360479680-create-account-db.js +30 -0
- package/migrations/1694362247011-create-secret-table.js +16 -0
- package/migrations/1702667624000-rename-nordigen-secrets.js +19 -0
- package/migrations/1718889148000-openid.js +41 -0
- package/migrations/1719409568000-multiuser.js +116 -0
- package/package.json +64 -0
- package/src/account-db.js +239 -0
- package/src/accounts/openid.js +361 -0
- package/src/accounts/password.js +149 -0
- package/src/app-account.js +155 -0
- package/src/app-admin.js +410 -0
- package/src/app-admin.test.js +381 -0
- package/src/app-gocardless/README.md +198 -0
- package/src/app-gocardless/app-gocardless.js +274 -0
- package/src/app-gocardless/bank-factory.js +91 -0
- package/src/app-gocardless/banks/abanca_caglesmm.js +22 -0
- package/src/app-gocardless/banks/abnamro_abnanl2a.js +57 -0
- package/src/app-gocardless/banks/american_express_aesudef1.js +40 -0
- package/src/app-gocardless/banks/bancsabadell_bsabesbbb.js +31 -0
- package/src/app-gocardless/banks/bank.interface.ts +51 -0
- package/src/app-gocardless/banks/bank_of_ireland_b365_bofiie2d.js +39 -0
- package/src/app-gocardless/banks/bankinter_bkbkesmm.js +24 -0
- package/src/app-gocardless/banks/belfius_gkccbebb.js +17 -0
- package/src/app-gocardless/banks/berliner_sparkasse_beladebexxx.js +61 -0
- package/src/app-gocardless/banks/bnp_be_gebabebb.js +73 -0
- package/src/app-gocardless/banks/cbc_cregbebb.js +34 -0
- package/src/app-gocardless/banks/commerzbank_cobadeff.js +51 -0
- package/src/app-gocardless/banks/danskebank_dabno22.js +39 -0
- package/src/app-gocardless/banks/direkt_heladef1822.js +18 -0
- package/src/app-gocardless/banks/easybank_bawaatww.js +50 -0
- package/src/app-gocardless/banks/entercard_swednokk.js +40 -0
- package/src/app-gocardless/banks/fortuneo_ftnofrp1xxx.js +46 -0
- package/src/app-gocardless/banks/hype_hyeeit22.js +74 -0
- package/src/app-gocardless/banks/ing_ingbrobu.js +70 -0
- package/src/app-gocardless/banks/ing_ingddeff.js +47 -0
- package/src/app-gocardless/banks/ing_pl_ingbplpw.js +46 -0
- package/src/app-gocardless/banks/integration-bank.js +115 -0
- package/src/app-gocardless/banks/isybank_itbbitmm.js +18 -0
- package/src/app-gocardless/banks/kbc_kredbebb.js +33 -0
- package/src/app-gocardless/banks/lhv-lhvbee22.js +36 -0
- package/src/app-gocardless/banks/mbank_retail_brexplpw.js +56 -0
- package/src/app-gocardless/banks/nationwide_naiagb21.js +46 -0
- package/src/app-gocardless/banks/nbg_ethngraaxxx.js +51 -0
- package/src/app-gocardless/banks/norwegian_xx_norwnok1.js +74 -0
- package/src/app-gocardless/banks/revolut_revolt21.js +37 -0
- package/src/app-gocardless/banks/sandboxfinance_sfin0000.js +28 -0
- package/src/app-gocardless/banks/seb_kort_bank_ab.js +58 -0
- package/src/app-gocardless/banks/seb_privat.js +29 -0
- package/src/app-gocardless/banks/sparnord_spnodk22.js +24 -0
- package/src/app-gocardless/banks/spk_karlsruhe_karsde66.js +61 -0
- package/src/app-gocardless/banks/spk_marburg_biedenkopf_heladef1mar.js +30 -0
- package/src/app-gocardless/banks/spk_worms_alzey_ried_malade51wor.js +19 -0
- package/src/app-gocardless/banks/ssk_dusseldorf_dussdeddxxx.js +50 -0
- package/src/app-gocardless/banks/swedbank_habalv22.js +47 -0
- package/src/app-gocardless/banks/tests/abanca_caglesmm.spec.js +21 -0
- package/src/app-gocardless/banks/tests/abnamro_abnanl2a.spec.js +61 -0
- package/src/app-gocardless/banks/tests/bancsabadell_bsabesbbb.spec.js +53 -0
- package/src/app-gocardless/banks/tests/belfius_gkccbebb.spec.js +22 -0
- package/src/app-gocardless/banks/tests/cbc_cregbebb.spec.js +34 -0
- package/src/app-gocardless/banks/tests/commerzbank_cobadeff.spec.js +110 -0
- package/src/app-gocardless/banks/tests/easybank_bawaatww.spec.js +54 -0
- package/src/app-gocardless/banks/tests/fortuneo_ftnofrp1xxx.spec.js +206 -0
- package/src/app-gocardless/banks/tests/ing_ingddeff.spec.js +302 -0
- package/src/app-gocardless/banks/tests/ing_pl_ingbplpw.spec.js +202 -0
- package/src/app-gocardless/banks/tests/integration_bank.spec.js +158 -0
- package/src/app-gocardless/banks/tests/kbc_kredbebb.spec.js +38 -0
- package/src/app-gocardless/banks/tests/lhv-lhvbee22.spec.js +68 -0
- package/src/app-gocardless/banks/tests/mbank_retail_brexplpw.spec.js +171 -0
- package/src/app-gocardless/banks/tests/nationwide_naiagb21.spec.js +105 -0
- package/src/app-gocardless/banks/tests/nbg_ethngraaxxx.spec.js +48 -0
- package/src/app-gocardless/banks/tests/revolut_revolt21.spec.js +42 -0
- package/src/app-gocardless/banks/tests/sandboxfinance_sfin0000.spec.js +133 -0
- package/src/app-gocardless/banks/tests/spk_marburg_biedenkopf_heladef1mar.spec.js +256 -0
- package/src/app-gocardless/banks/tests/ssk_dusseldorf_dussdeddxxx.spec.js +102 -0
- package/src/app-gocardless/banks/tests/swedbank_habalv22.spec.js +57 -0
- package/src/app-gocardless/banks/tests/virgin_nrnbgb22.spec.js +54 -0
- package/src/app-gocardless/banks/util/extract-payeeName-from-remittanceInfo.js +36 -0
- package/src/app-gocardless/banks/virgin_nrnbgb22.js +39 -0
- package/src/app-gocardless/errors.js +84 -0
- package/src/app-gocardless/gocardless-node.types.ts +497 -0
- package/src/app-gocardless/gocardless.types.ts +93 -0
- package/src/app-gocardless/link.html +18 -0
- package/src/app-gocardless/services/gocardless-service.js +620 -0
- package/src/app-gocardless/services/tests/fixtures.js +181 -0
- package/src/app-gocardless/services/tests/gocardless-service.spec.js +537 -0
- package/src/app-gocardless/tests/bank-factory.spec.js +20 -0
- package/src/app-gocardless/tests/utils.spec.js +162 -0
- package/src/app-gocardless/util/handle-error.js +16 -0
- package/src/app-gocardless/utils.js +45 -0
- package/src/app-openid.js +108 -0
- package/src/app-pluggyai/app-pluggyai.js +215 -0
- package/src/app-pluggyai/pluggyai-service.js +120 -0
- package/src/app-secrets.js +61 -0
- package/src/app-simplefin/app-simplefin.js +418 -0
- package/src/app-sync/errors.js +13 -0
- package/src/app-sync/services/files-service.js +243 -0
- package/src/app-sync/tests/services/files-service.test.js +250 -0
- package/src/app-sync/validation.js +77 -0
- package/src/app-sync.js +391 -0
- package/src/app-sync.test.js +877 -0
- package/src/app.js +145 -0
- package/src/config-types.ts +44 -0
- package/src/db.js +58 -0
- package/src/load-config.js +307 -0
- package/src/migrations.js +36 -0
- package/src/run-migrations.js +8 -0
- package/src/scripts/disable-openid.js +44 -0
- package/src/scripts/enable-openid.js +53 -0
- package/src/scripts/health-check.js +23 -0
- package/src/scripts/reset-password.js +51 -0
- package/src/secrets.test.js +83 -0
- package/src/services/secrets-service.js +94 -0
- package/src/services/user-service.js +272 -0
- package/src/sql/messages.sql +9 -0
- package/src/sync-simple.js +95 -0
- package/src/util/hash.js +5 -0
- package/src/util/middlewares.js +62 -0
- package/src/util/paths.js +13 -0
- package/src/util/payee-name.js +45 -0
- package/src/util/prompt.js +88 -0
- package/src/util/title/index.js +59 -0
- package/src/util/title/lower-case.js +93 -0
- package/src/util/title/specials.js +21 -0
- package/src/util/validate-user.js +68 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { inspect } from 'util';
|
|
3
|
+
|
|
4
|
+
import { isAxiosError } from 'axios';
|
|
5
|
+
import express from 'express';
|
|
6
|
+
|
|
7
|
+
import { sha256String } from '../util/hash.js';
|
|
8
|
+
import {
|
|
9
|
+
requestLoggerMiddleware,
|
|
10
|
+
validateSessionMiddleware,
|
|
11
|
+
} from '../util/middlewares.js';
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
AccountNotLinkedToRequisition,
|
|
15
|
+
GenericGoCardlessError,
|
|
16
|
+
RateLimitError,
|
|
17
|
+
RequisitionNotLinked,
|
|
18
|
+
} from './errors.js';
|
|
19
|
+
import { goCardlessService } from './services/gocardless-service.js';
|
|
20
|
+
import { handleError } from './util/handle-error.js';
|
|
21
|
+
|
|
22
|
+
const app = express();
|
|
23
|
+
app.use(requestLoggerMiddleware);
|
|
24
|
+
|
|
25
|
+
app.get('/link', function (req, res) {
|
|
26
|
+
res.sendFile('link.html', { root: path.resolve('./src/app-gocardless') });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export { app as handlers };
|
|
30
|
+
app.use(express.json());
|
|
31
|
+
app.use(validateSessionMiddleware);
|
|
32
|
+
|
|
33
|
+
app.post('/status', async (req, res) => {
|
|
34
|
+
res.send({
|
|
35
|
+
status: 'ok',
|
|
36
|
+
data: {
|
|
37
|
+
configured: goCardlessService.isConfigured(),
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
app.post(
|
|
43
|
+
'/create-web-token',
|
|
44
|
+
handleError(async (req, res) => {
|
|
45
|
+
const { institutionId } = req.body;
|
|
46
|
+
const { origin } = req.headers;
|
|
47
|
+
|
|
48
|
+
const { link, requisitionId } = await goCardlessService.createRequisition({
|
|
49
|
+
institutionId,
|
|
50
|
+
host: origin,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
res.send({
|
|
54
|
+
status: 'ok',
|
|
55
|
+
data: {
|
|
56
|
+
link,
|
|
57
|
+
requisitionId,
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
}),
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
app.post(
|
|
64
|
+
'/get-accounts',
|
|
65
|
+
handleError(async (req, res) => {
|
|
66
|
+
const { requisitionId } = req.body;
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const { requisition, accounts } =
|
|
70
|
+
await goCardlessService.getRequisitionWithAccounts(requisitionId);
|
|
71
|
+
|
|
72
|
+
res.send({
|
|
73
|
+
status: 'ok',
|
|
74
|
+
data: {
|
|
75
|
+
...requisition,
|
|
76
|
+
accounts: await Promise.all(
|
|
77
|
+
accounts.map(async account =>
|
|
78
|
+
account?.iban
|
|
79
|
+
? { ...account, iban: await sha256String(account.iban) }
|
|
80
|
+
: account,
|
|
81
|
+
),
|
|
82
|
+
),
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (error instanceof RequisitionNotLinked) {
|
|
87
|
+
res.send({
|
|
88
|
+
status: 'ok',
|
|
89
|
+
requisitionStatus: error.details.requisitionStatus,
|
|
90
|
+
});
|
|
91
|
+
} else {
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}),
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
app.post(
|
|
99
|
+
'/get-banks',
|
|
100
|
+
handleError(async (req, res) => {
|
|
101
|
+
const { country, showDemo = false } = req.body;
|
|
102
|
+
|
|
103
|
+
await goCardlessService.setToken();
|
|
104
|
+
const data = await goCardlessService.getInstitutions(country);
|
|
105
|
+
|
|
106
|
+
res.send({
|
|
107
|
+
status: 'ok',
|
|
108
|
+
data: showDemo
|
|
109
|
+
? [
|
|
110
|
+
{
|
|
111
|
+
id: 'SANDBOXFINANCE_SFIN0000',
|
|
112
|
+
name: 'DEMO bank (used for testing bank-sync)',
|
|
113
|
+
},
|
|
114
|
+
...data,
|
|
115
|
+
]
|
|
116
|
+
: data,
|
|
117
|
+
});
|
|
118
|
+
}),
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
app.post(
|
|
122
|
+
'/remove-account',
|
|
123
|
+
handleError(async (req, res) => {
|
|
124
|
+
const { requisitionId } = req.body;
|
|
125
|
+
|
|
126
|
+
const data = await goCardlessService.deleteRequisition(requisitionId);
|
|
127
|
+
if (data.summary === 'Requisition deleted') {
|
|
128
|
+
res.send({
|
|
129
|
+
status: 'ok',
|
|
130
|
+
data,
|
|
131
|
+
});
|
|
132
|
+
} else {
|
|
133
|
+
res.send({
|
|
134
|
+
status: 'error',
|
|
135
|
+
data: {
|
|
136
|
+
data,
|
|
137
|
+
reason: 'Can not delete requisition',
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}),
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
app.post(
|
|
145
|
+
'/transactions',
|
|
146
|
+
handleError(async (req, res) => {
|
|
147
|
+
const {
|
|
148
|
+
requisitionId,
|
|
149
|
+
startDate,
|
|
150
|
+
endDate,
|
|
151
|
+
accountId,
|
|
152
|
+
includeBalance = true,
|
|
153
|
+
} = req.body;
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
if (includeBalance) {
|
|
157
|
+
const {
|
|
158
|
+
balances,
|
|
159
|
+
institutionId,
|
|
160
|
+
startingBalance,
|
|
161
|
+
transactions: { booked, pending, all },
|
|
162
|
+
} = await goCardlessService.getTransactionsWithBalance(
|
|
163
|
+
requisitionId,
|
|
164
|
+
accountId,
|
|
165
|
+
startDate,
|
|
166
|
+
endDate,
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
res.send({
|
|
170
|
+
status: 'ok',
|
|
171
|
+
data: {
|
|
172
|
+
balances,
|
|
173
|
+
institutionId,
|
|
174
|
+
startingBalance,
|
|
175
|
+
transactions: {
|
|
176
|
+
booked,
|
|
177
|
+
pending,
|
|
178
|
+
all,
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
} else {
|
|
183
|
+
const {
|
|
184
|
+
institutionId,
|
|
185
|
+
transactions: { booked, pending, all },
|
|
186
|
+
} = await goCardlessService.getNormalizedTransactions(
|
|
187
|
+
requisitionId,
|
|
188
|
+
accountId,
|
|
189
|
+
startDate,
|
|
190
|
+
endDate,
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
res.send({
|
|
194
|
+
status: 'ok',
|
|
195
|
+
data: {
|
|
196
|
+
institutionId,
|
|
197
|
+
transactions: {
|
|
198
|
+
booked,
|
|
199
|
+
pending,
|
|
200
|
+
all,
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
} catch (error) {
|
|
206
|
+
const headers = error.details?.response?.headers ?? {};
|
|
207
|
+
|
|
208
|
+
const rateLimitHeaders = Object.fromEntries(
|
|
209
|
+
Object.entries(headers).filter(([key]) =>
|
|
210
|
+
key.startsWith('http_x_ratelimit'),
|
|
211
|
+
),
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
const sendErrorResponse = data =>
|
|
215
|
+
res.send({
|
|
216
|
+
status: 'ok',
|
|
217
|
+
data: { ...data, details: error.details, rateLimitHeaders },
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
switch (true) {
|
|
221
|
+
case error instanceof RequisitionNotLinked:
|
|
222
|
+
sendErrorResponse({
|
|
223
|
+
error_type: 'ITEM_ERROR',
|
|
224
|
+
error_code: 'ITEM_LOGIN_REQUIRED',
|
|
225
|
+
status: 'expired',
|
|
226
|
+
reason:
|
|
227
|
+
'Access to account has expired as set in End User Agreement',
|
|
228
|
+
});
|
|
229
|
+
break;
|
|
230
|
+
case error instanceof AccountNotLinkedToRequisition:
|
|
231
|
+
sendErrorResponse({
|
|
232
|
+
error_type: 'INVALID_INPUT',
|
|
233
|
+
error_code: 'INVALID_ACCESS_TOKEN',
|
|
234
|
+
status: 'rejected',
|
|
235
|
+
reason: 'Account not linked with this requisition',
|
|
236
|
+
});
|
|
237
|
+
break;
|
|
238
|
+
case error instanceof RateLimitError:
|
|
239
|
+
sendErrorResponse({
|
|
240
|
+
error_type: 'RATE_LIMIT_EXCEEDED',
|
|
241
|
+
error_code: 'NORDIGEN_ERROR',
|
|
242
|
+
status: 'rejected',
|
|
243
|
+
reason: 'Rate limit exceeded',
|
|
244
|
+
});
|
|
245
|
+
break;
|
|
246
|
+
case error instanceof GenericGoCardlessError:
|
|
247
|
+
console.log('Something went wrong', inspect(error, { depth: null }));
|
|
248
|
+
sendErrorResponse({
|
|
249
|
+
error_type: 'SYNC_ERROR',
|
|
250
|
+
error_code: 'NORDIGEN_ERROR',
|
|
251
|
+
});
|
|
252
|
+
break;
|
|
253
|
+
case isAxiosError(error):
|
|
254
|
+
console.log(
|
|
255
|
+
'Something went wrong',
|
|
256
|
+
inspect(error.response?.data || error, { depth: null }),
|
|
257
|
+
);
|
|
258
|
+
sendErrorResponse({
|
|
259
|
+
error_type: 'SYNC_ERROR',
|
|
260
|
+
error_code: 'NORDIGEN_ERROR',
|
|
261
|
+
});
|
|
262
|
+
break;
|
|
263
|
+
default:
|
|
264
|
+
console.log('Something went wrong', inspect(error, { depth: null }));
|
|
265
|
+
sendErrorResponse({
|
|
266
|
+
error_type: 'UNKNOWN',
|
|
267
|
+
error_code: 'UNKNOWN',
|
|
268
|
+
reason: 'Something went wrong',
|
|
269
|
+
});
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}),
|
|
274
|
+
);
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
4
|
+
|
|
5
|
+
import IntegrationBank from './banks/integration-bank.js';
|
|
6
|
+
|
|
7
|
+
const dirname = path.resolve(fileURLToPath(import.meta.url), '..');
|
|
8
|
+
const banksDir = path.resolve(dirname, 'banks');
|
|
9
|
+
|
|
10
|
+
async function loadBanks() {
|
|
11
|
+
const bankHandlers = fs
|
|
12
|
+
.readdirSync(banksDir)
|
|
13
|
+
.filter(filename => filename.includes('_') && filename.endsWith('.js'));
|
|
14
|
+
|
|
15
|
+
const imports = await Promise.all(
|
|
16
|
+
bankHandlers.map(file => {
|
|
17
|
+
const fileUrlToBank = pathToFileURL(path.resolve(banksDir, file)); // pathToFileURL for ESM compatibility
|
|
18
|
+
return import(fileUrlToBank.toString()).then(handler => handler.default);
|
|
19
|
+
}),
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
return imports;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const banks = await loadBanks();
|
|
26
|
+
|
|
27
|
+
export function BankFactory(institutionId) {
|
|
28
|
+
return (
|
|
29
|
+
banks.find(b => b.institutionIds.includes(institutionId)) || IntegrationBank
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const BANKS_WITH_LIMITED_HISTORY = [
|
|
34
|
+
'AIRBANK_AIRACZPP',
|
|
35
|
+
'BANCA_AIDEXA_AIDXITMM',
|
|
36
|
+
'BANCA_PATRIMONI_SENVITT1',
|
|
37
|
+
'BANCA_SELLA_SELBIT2B',
|
|
38
|
+
'BANK_MILLENNIUM_BIGBPLPW',
|
|
39
|
+
'BANKINTER_BKBKESMM',
|
|
40
|
+
'BBVA_BBVAESMM',
|
|
41
|
+
'BNP_PL_PPABPLPK',
|
|
42
|
+
'BRED_BREDFRPPXXX',
|
|
43
|
+
'CAIXA_GERAL_DEPOSITOS_CGDIPTPL',
|
|
44
|
+
'CAIXABANK_CAIXESBB',
|
|
45
|
+
'CARTALIS_CIMTITR1',
|
|
46
|
+
'CESKA_SPORITELNA_LONG_GIBACZPX',
|
|
47
|
+
'COOP_EKRDEE22',
|
|
48
|
+
'DKB_BYLADEM1',
|
|
49
|
+
'DOTS_HYEEIT22',
|
|
50
|
+
'FINECO_FEBIITM2XXX',
|
|
51
|
+
'FINECO_UK_FEBIITM2XXX',
|
|
52
|
+
'FORTUNEO_FTNOFRP1XXX',
|
|
53
|
+
'HYPE_BUSINESS_HYEEIT22',
|
|
54
|
+
'HYPE_HYEEIT22',
|
|
55
|
+
'ILLIMITY_ITTPIT2M',
|
|
56
|
+
'INDUSTRA_MULTLV2X',
|
|
57
|
+
'INDUSTRIEL_CMCIFRPAXXX',
|
|
58
|
+
'JEKYLL_JEYKLL002',
|
|
59
|
+
'KBC_KREDBEBB',
|
|
60
|
+
'LABORALKUTXA_CLPEES2M',
|
|
61
|
+
'LHV_LHVBEE22',
|
|
62
|
+
'LUMINOR_AGBLLT2X',
|
|
63
|
+
'LUMINOR_NDEAEE2X',
|
|
64
|
+
'LUMINOR_NDEALT2X',
|
|
65
|
+
'LUMINOR_NDEALV2X',
|
|
66
|
+
'LUMINOR_RIKOEE22',
|
|
67
|
+
'LUMINOR_RIKOLV2X',
|
|
68
|
+
'MBANK_RETAIL_BREXPLPW',
|
|
69
|
+
'MEDICINOSBANK_MDBALT22XXX',
|
|
70
|
+
'NORDEA_NDEADKKK',
|
|
71
|
+
'N26_NTSBDEB1',
|
|
72
|
+
'OPYN_BITAITRRB2B',
|
|
73
|
+
'PAYTIPPER_PAYTITM1',
|
|
74
|
+
'QONTO_QNTOFRP1',
|
|
75
|
+
'REVOLUT_REVOLT21',
|
|
76
|
+
'SANTANDER_BSCHESMM',
|
|
77
|
+
'SANTANDER_DE_SCFBDE33',
|
|
78
|
+
'SEB_CBVILT2X',
|
|
79
|
+
'SEB_EEUHEE2X',
|
|
80
|
+
'SEB_UNLALV2X',
|
|
81
|
+
'SELLA_PERSONAL_CREDIT_SELBIT22',
|
|
82
|
+
'BANCOACTIVOBANK_ACTVPTPL',
|
|
83
|
+
'SMARTIKA_SELBIT22',
|
|
84
|
+
'SWEDBANK_HABAEE2X',
|
|
85
|
+
'SWEDBANK_HABALT22',
|
|
86
|
+
'SWEDBANK_HABALV22',
|
|
87
|
+
'SWEDBANK_SWEDSESS',
|
|
88
|
+
'TIM_HYEEIT22',
|
|
89
|
+
'TOT_SELBIT2B',
|
|
90
|
+
'VUB_BANKA_SUBASKBX',
|
|
91
|
+
];
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import Fallback from './integration-bank.js';
|
|
2
|
+
|
|
3
|
+
/** @type {import('./bank.interface.js').IBank} */
|
|
4
|
+
export default {
|
|
5
|
+
...Fallback,
|
|
6
|
+
|
|
7
|
+
institutionIds: [
|
|
8
|
+
'ABANCA_CAGLESMM',
|
|
9
|
+
'ABANCA_CAGLPTPL',
|
|
10
|
+
'ABANCA_CORP_CAGLPTPL',
|
|
11
|
+
],
|
|
12
|
+
|
|
13
|
+
// Abanca transactions doesn't get the creditorName/debtorName properly
|
|
14
|
+
normalizeTransaction(transaction, booked) {
|
|
15
|
+
const editedTrans = { ...transaction };
|
|
16
|
+
|
|
17
|
+
editedTrans.creditorName = transaction.remittanceInformationStructured;
|
|
18
|
+
editedTrans.debtorName = transaction.remittanceInformationStructured;
|
|
19
|
+
|
|
20
|
+
return Fallback.normalizeTransaction(transaction, booked, editedTrans);
|
|
21
|
+
},
|
|
22
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { amountToInteger } from '../utils.js';
|
|
2
|
+
|
|
3
|
+
import Fallback from './integration-bank.js';
|
|
4
|
+
|
|
5
|
+
/** @type {import('./bank.interface.js').IBank} */
|
|
6
|
+
export default {
|
|
7
|
+
...Fallback,
|
|
8
|
+
|
|
9
|
+
institutionIds: ['ABNAMRO_ABNANL2A'],
|
|
10
|
+
|
|
11
|
+
normalizeTransaction(transaction, booked) {
|
|
12
|
+
const editedTrans = { ...transaction };
|
|
13
|
+
|
|
14
|
+
// There is no remittanceInformationUnstructured, so we'll make it
|
|
15
|
+
editedTrans.remittanceInformationUnstructured =
|
|
16
|
+
transaction.remittanceInformationUnstructuredArray.join(', ');
|
|
17
|
+
|
|
18
|
+
// Remove clutter to extract the payee from remittanceInformationUnstructured ...
|
|
19
|
+
// ... when not otherwise provided.
|
|
20
|
+
const payeeName = transaction.remittanceInformationUnstructuredArray
|
|
21
|
+
.map(el => el.match(/^(?:.*\*)?(.+),PAS\d+$/))
|
|
22
|
+
.find(match => match)?.[1];
|
|
23
|
+
|
|
24
|
+
editedTrans.debtorName = transaction.debtorName || payeeName;
|
|
25
|
+
editedTrans.creditorName = transaction.creditorName || payeeName;
|
|
26
|
+
|
|
27
|
+
editedTrans.date = transaction.valueDateTime.slice(0, 10);
|
|
28
|
+
|
|
29
|
+
return Fallback.normalizeTransaction(transaction, booked, editedTrans);
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
sortTransactions(transactions = []) {
|
|
33
|
+
return transactions.sort(
|
|
34
|
+
(a, b) => +new Date(b.valueDateTime) - +new Date(a.valueDateTime),
|
|
35
|
+
);
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
calculateStartingBalance(sortedTransactions = [], balances = []) {
|
|
39
|
+
if (sortedTransactions.length) {
|
|
40
|
+
const oldestTransaction =
|
|
41
|
+
sortedTransactions[sortedTransactions.length - 1];
|
|
42
|
+
const oldestKnownBalance = amountToInteger(
|
|
43
|
+
oldestTransaction.balanceAfterTransaction.balanceAmount.amount,
|
|
44
|
+
);
|
|
45
|
+
const oldestTransactionAmount = amountToInteger(
|
|
46
|
+
oldestTransaction.transactionAmount.amount,
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
return oldestKnownBalance - oldestTransactionAmount;
|
|
50
|
+
} else {
|
|
51
|
+
return amountToInteger(
|
|
52
|
+
balances.find(balance => 'interimBooked' === balance.balanceType)
|
|
53
|
+
.balanceAmount.amount,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { amountToInteger } from '../utils.js';
|
|
2
|
+
|
|
3
|
+
import Fallback from './integration-bank.js';
|
|
4
|
+
|
|
5
|
+
/** @type {import('./bank.interface.js').IBank} */
|
|
6
|
+
export default {
|
|
7
|
+
...Fallback,
|
|
8
|
+
|
|
9
|
+
institutionIds: ['AMERICAN_EXPRESS_AESUDEF1'],
|
|
10
|
+
|
|
11
|
+
normalizeAccount(account) {
|
|
12
|
+
return {
|
|
13
|
+
...Fallback.normalizeAccount(account),
|
|
14
|
+
// The `iban` field for these American Express cards is actually a masked
|
|
15
|
+
// version of the PAN. No IBAN is provided.
|
|
16
|
+
mask: account.iban.slice(-5),
|
|
17
|
+
iban: null,
|
|
18
|
+
name: [account.details, `(${account.iban.slice(-5)})`].join(' '),
|
|
19
|
+
official_name: account.details,
|
|
20
|
+
};
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* For AMERICAN_EXPRESS_AESUDEF1 we don't know what balance was
|
|
25
|
+
* after each transaction so we have to calculate it by getting
|
|
26
|
+
* current balance from the account and subtract all the transactions
|
|
27
|
+
*
|
|
28
|
+
* As a current balance we use the non-standard `information` balance type
|
|
29
|
+
* which is the only one provided for American Express.
|
|
30
|
+
*/
|
|
31
|
+
calculateStartingBalance(sortedTransactions = [], balances = []) {
|
|
32
|
+
const currentBalance = balances.find(
|
|
33
|
+
balance => 'information' === balance.balanceType.toString(),
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
return sortedTransactions.reduce((total, trans) => {
|
|
37
|
+
return total - amountToInteger(trans.transactionAmount.amount);
|
|
38
|
+
}, amountToInteger(currentBalance.balanceAmount.amount));
|
|
39
|
+
},
|
|
40
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import Fallback from './integration-bank.js';
|
|
2
|
+
|
|
3
|
+
/** @type {import('./bank.interface.js').IBank} */
|
|
4
|
+
export default {
|
|
5
|
+
...Fallback,
|
|
6
|
+
|
|
7
|
+
institutionIds: ['BANCSABADELL_BSABESBB'],
|
|
8
|
+
|
|
9
|
+
// Sabadell transactions don't get the creditorName/debtorName properly
|
|
10
|
+
normalizeTransaction(transaction, booked) {
|
|
11
|
+
const editedTrans = { ...transaction };
|
|
12
|
+
|
|
13
|
+
const amount = transaction.transactionAmount.amount;
|
|
14
|
+
|
|
15
|
+
// The amount is negative for outgoing transactions, positive for incoming transactions.
|
|
16
|
+
const isCreditorPayee = Number.parseFloat(amount) < 0;
|
|
17
|
+
|
|
18
|
+
const payeeName = transaction.remittanceInformationUnstructuredArray
|
|
19
|
+
.join(' ')
|
|
20
|
+
.trim();
|
|
21
|
+
|
|
22
|
+
// The payee name is the creditor name for outgoing transactions and the debtor name for incoming transactions.
|
|
23
|
+
const creditorName = isCreditorPayee ? payeeName : null;
|
|
24
|
+
const debtorName = isCreditorPayee ? null : payeeName;
|
|
25
|
+
|
|
26
|
+
editedTrans.creditorName = creditorName;
|
|
27
|
+
editedTrans.debtorName = debtorName;
|
|
28
|
+
|
|
29
|
+
return Fallback.normalizeTransaction(transaction, booked, editedTrans);
|
|
30
|
+
},
|
|
31
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Transaction, Balance } from '../gocardless-node.types.js';
|
|
2
|
+
import {
|
|
3
|
+
DetailedAccountWithInstitution,
|
|
4
|
+
NormalizedAccountDetails,
|
|
5
|
+
} from '../gocardless.types.js';
|
|
6
|
+
|
|
7
|
+
type TransactionExtended = Transaction & {
|
|
8
|
+
date?: string;
|
|
9
|
+
payeeName?: string;
|
|
10
|
+
notes?: string;
|
|
11
|
+
remittanceInformationUnstructuredArrayString?: string;
|
|
12
|
+
remittanceInformationStructuredArrayString?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export interface IBank {
|
|
16
|
+
institutionIds: string[];
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Returns normalized object with required data for the frontend
|
|
20
|
+
*/
|
|
21
|
+
normalizeAccount: (
|
|
22
|
+
account: DetailedAccountWithInstitution,
|
|
23
|
+
) => NormalizedAccountDetails;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Returns a normalized transaction object
|
|
27
|
+
*
|
|
28
|
+
* The GoCardless integrations with different banks are very inconsistent in
|
|
29
|
+
* what each of the different date fields actually mean, so this function is
|
|
30
|
+
* expected to set a `date` field which corresponds to the expected
|
|
31
|
+
* transaction date.
|
|
32
|
+
*/
|
|
33
|
+
normalizeTransaction: (
|
|
34
|
+
transaction: TransactionExtended,
|
|
35
|
+
booked: boolean,
|
|
36
|
+
editedTransaction?: TransactionExtended,
|
|
37
|
+
) => TransactionExtended | null;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Function sorts an array of transactions from newest to oldest
|
|
41
|
+
*/
|
|
42
|
+
sortTransactions: <T extends Transaction>(transactions: T[]) => T[];
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Calculates account balance before which was before transactions provided in sortedTransactions param
|
|
46
|
+
*/
|
|
47
|
+
calculateStartingBalance: (
|
|
48
|
+
sortedTransactions: Transaction[],
|
|
49
|
+
balances: Balance[],
|
|
50
|
+
) => number;
|
|
51
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import Fallback from './integration-bank.js';
|
|
2
|
+
|
|
3
|
+
/** @type {import('./bank.interface.js').IBank} */
|
|
4
|
+
export default {
|
|
5
|
+
...Fallback,
|
|
6
|
+
|
|
7
|
+
institutionIds: ['BANK_OF_IRELAND_B365_BOFIIE2D'],
|
|
8
|
+
|
|
9
|
+
normalizeTransaction(transaction, booked) {
|
|
10
|
+
const editedTrans = { ...transaction };
|
|
11
|
+
|
|
12
|
+
editedTrans.remittanceInformationUnstructured = fixupPayee(
|
|
13
|
+
transaction.remittanceInformationUnstructured,
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
return Fallback.normalizeTransaction(transaction, booked, editedTrans);
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function fixupPayee(/** @type {string} */ payee) {
|
|
21
|
+
let fixedPayee = payee;
|
|
22
|
+
|
|
23
|
+
// remove all duplicate whitespace
|
|
24
|
+
fixedPayee = fixedPayee.replace(/\s+/g, ' ').trim();
|
|
25
|
+
|
|
26
|
+
// remove date prefix
|
|
27
|
+
fixedPayee = fixedPayee.replace(/^(POS)?(C)?[0-9]{1,2}\w{3}/, '').trim();
|
|
28
|
+
|
|
29
|
+
// remove direct debit postfix
|
|
30
|
+
fixedPayee = fixedPayee.replace(/sepa dd$/i, '').trim();
|
|
31
|
+
|
|
32
|
+
// remove bank transfer prefix
|
|
33
|
+
fixedPayee = fixedPayee.replace(/^365 online/i, '').trim();
|
|
34
|
+
|
|
35
|
+
// remove curve card prefix
|
|
36
|
+
fixedPayee = fixedPayee.replace(/^CRV\*/, '').trim();
|
|
37
|
+
|
|
38
|
+
return fixedPayee;
|
|
39
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import Fallback from './integration-bank.js';
|
|
2
|
+
|
|
3
|
+
/** @type {import('./bank.interface.js').IBank} */
|
|
4
|
+
export default {
|
|
5
|
+
...Fallback,
|
|
6
|
+
|
|
7
|
+
institutionIds: ['BANKINTER_BKBKESMM'],
|
|
8
|
+
|
|
9
|
+
normalizeTransaction(transaction, booked) {
|
|
10
|
+
const editedTrans = { ...transaction };
|
|
11
|
+
|
|
12
|
+
editedTrans.remittanceInformationUnstructured =
|
|
13
|
+
transaction.remittanceInformationUnstructured
|
|
14
|
+
.replaceAll(/\/Txt\/(\w\|)?/gi, '')
|
|
15
|
+
.replaceAll(';', ' ');
|
|
16
|
+
|
|
17
|
+
editedTrans.debtorName = transaction.debtorName?.replaceAll(';', ' ');
|
|
18
|
+
editedTrans.creditorName =
|
|
19
|
+
transaction.creditorName?.replaceAll(';', ' ') ??
|
|
20
|
+
editedTrans.remittanceInformationUnstructured;
|
|
21
|
+
|
|
22
|
+
return Fallback.normalizeTransaction(transaction, booked, editedTrans);
|
|
23
|
+
},
|
|
24
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import Fallback from './integration-bank.js';
|
|
2
|
+
|
|
3
|
+
/** @type {import('./bank.interface.js').IBank} */
|
|
4
|
+
export default {
|
|
5
|
+
...Fallback,
|
|
6
|
+
|
|
7
|
+
institutionIds: ['BELFIUS_GKCCBEBB'],
|
|
8
|
+
|
|
9
|
+
// The problem is that we have transaction with duplicated transaction ids.
|
|
10
|
+
// This is not expected and the nordigen api has a work-around for some backs
|
|
11
|
+
// They will set an internalTransactionId which is unique
|
|
12
|
+
normalizeTransaction(transaction, booked) {
|
|
13
|
+
transaction.transactionId = transaction.internalTransactionId;
|
|
14
|
+
|
|
15
|
+
return Fallback.normalizeTransaction(transaction, booked);
|
|
16
|
+
},
|
|
17
|
+
};
|