@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.
Files changed (137) hide show
  1. package/.dockerignore +12 -0
  2. package/README.md +19 -0
  3. package/app.js +11 -0
  4. package/babel.config.json +3 -0
  5. package/bin/@actual-app/sync-server +55 -0
  6. package/docker/alpine.Dockerfile +62 -0
  7. package/docker/ubuntu.Dockerfile +63 -0
  8. package/docker-compose.yml +29 -0
  9. package/jest.config.json +19 -0
  10. package/jest.global-setup.js +101 -0
  11. package/jest.global-teardown.js +6 -0
  12. package/migrations/1694360000000-create-folders.js +25 -0
  13. package/migrations/1694360479680-create-account-db.js +30 -0
  14. package/migrations/1694362247011-create-secret-table.js +16 -0
  15. package/migrations/1702667624000-rename-nordigen-secrets.js +19 -0
  16. package/migrations/1718889148000-openid.js +41 -0
  17. package/migrations/1719409568000-multiuser.js +116 -0
  18. package/package.json +64 -0
  19. package/src/account-db.js +239 -0
  20. package/src/accounts/openid.js +361 -0
  21. package/src/accounts/password.js +149 -0
  22. package/src/app-account.js +155 -0
  23. package/src/app-admin.js +410 -0
  24. package/src/app-admin.test.js +381 -0
  25. package/src/app-gocardless/README.md +198 -0
  26. package/src/app-gocardless/app-gocardless.js +274 -0
  27. package/src/app-gocardless/bank-factory.js +91 -0
  28. package/src/app-gocardless/banks/abanca_caglesmm.js +22 -0
  29. package/src/app-gocardless/banks/abnamro_abnanl2a.js +57 -0
  30. package/src/app-gocardless/banks/american_express_aesudef1.js +40 -0
  31. package/src/app-gocardless/banks/bancsabadell_bsabesbbb.js +31 -0
  32. package/src/app-gocardless/banks/bank.interface.ts +51 -0
  33. package/src/app-gocardless/banks/bank_of_ireland_b365_bofiie2d.js +39 -0
  34. package/src/app-gocardless/banks/bankinter_bkbkesmm.js +24 -0
  35. package/src/app-gocardless/banks/belfius_gkccbebb.js +17 -0
  36. package/src/app-gocardless/banks/berliner_sparkasse_beladebexxx.js +61 -0
  37. package/src/app-gocardless/banks/bnp_be_gebabebb.js +73 -0
  38. package/src/app-gocardless/banks/cbc_cregbebb.js +34 -0
  39. package/src/app-gocardless/banks/commerzbank_cobadeff.js +51 -0
  40. package/src/app-gocardless/banks/danskebank_dabno22.js +39 -0
  41. package/src/app-gocardless/banks/direkt_heladef1822.js +18 -0
  42. package/src/app-gocardless/banks/easybank_bawaatww.js +50 -0
  43. package/src/app-gocardless/banks/entercard_swednokk.js +40 -0
  44. package/src/app-gocardless/banks/fortuneo_ftnofrp1xxx.js +46 -0
  45. package/src/app-gocardless/banks/hype_hyeeit22.js +74 -0
  46. package/src/app-gocardless/banks/ing_ingbrobu.js +70 -0
  47. package/src/app-gocardless/banks/ing_ingddeff.js +47 -0
  48. package/src/app-gocardless/banks/ing_pl_ingbplpw.js +46 -0
  49. package/src/app-gocardless/banks/integration-bank.js +115 -0
  50. package/src/app-gocardless/banks/isybank_itbbitmm.js +18 -0
  51. package/src/app-gocardless/banks/kbc_kredbebb.js +33 -0
  52. package/src/app-gocardless/banks/lhv-lhvbee22.js +36 -0
  53. package/src/app-gocardless/banks/mbank_retail_brexplpw.js +56 -0
  54. package/src/app-gocardless/banks/nationwide_naiagb21.js +46 -0
  55. package/src/app-gocardless/banks/nbg_ethngraaxxx.js +51 -0
  56. package/src/app-gocardless/banks/norwegian_xx_norwnok1.js +74 -0
  57. package/src/app-gocardless/banks/revolut_revolt21.js +37 -0
  58. package/src/app-gocardless/banks/sandboxfinance_sfin0000.js +28 -0
  59. package/src/app-gocardless/banks/seb_kort_bank_ab.js +58 -0
  60. package/src/app-gocardless/banks/seb_privat.js +29 -0
  61. package/src/app-gocardless/banks/sparnord_spnodk22.js +24 -0
  62. package/src/app-gocardless/banks/spk_karlsruhe_karsde66.js +61 -0
  63. package/src/app-gocardless/banks/spk_marburg_biedenkopf_heladef1mar.js +30 -0
  64. package/src/app-gocardless/banks/spk_worms_alzey_ried_malade51wor.js +19 -0
  65. package/src/app-gocardless/banks/ssk_dusseldorf_dussdeddxxx.js +50 -0
  66. package/src/app-gocardless/banks/swedbank_habalv22.js +47 -0
  67. package/src/app-gocardless/banks/tests/abanca_caglesmm.spec.js +21 -0
  68. package/src/app-gocardless/banks/tests/abnamro_abnanl2a.spec.js +61 -0
  69. package/src/app-gocardless/banks/tests/bancsabadell_bsabesbbb.spec.js +53 -0
  70. package/src/app-gocardless/banks/tests/belfius_gkccbebb.spec.js +22 -0
  71. package/src/app-gocardless/banks/tests/cbc_cregbebb.spec.js +34 -0
  72. package/src/app-gocardless/banks/tests/commerzbank_cobadeff.spec.js +110 -0
  73. package/src/app-gocardless/banks/tests/easybank_bawaatww.spec.js +54 -0
  74. package/src/app-gocardless/banks/tests/fortuneo_ftnofrp1xxx.spec.js +206 -0
  75. package/src/app-gocardless/banks/tests/ing_ingddeff.spec.js +302 -0
  76. package/src/app-gocardless/banks/tests/ing_pl_ingbplpw.spec.js +202 -0
  77. package/src/app-gocardless/banks/tests/integration_bank.spec.js +158 -0
  78. package/src/app-gocardless/banks/tests/kbc_kredbebb.spec.js +38 -0
  79. package/src/app-gocardless/banks/tests/lhv-lhvbee22.spec.js +68 -0
  80. package/src/app-gocardless/banks/tests/mbank_retail_brexplpw.spec.js +171 -0
  81. package/src/app-gocardless/banks/tests/nationwide_naiagb21.spec.js +105 -0
  82. package/src/app-gocardless/banks/tests/nbg_ethngraaxxx.spec.js +48 -0
  83. package/src/app-gocardless/banks/tests/revolut_revolt21.spec.js +42 -0
  84. package/src/app-gocardless/banks/tests/sandboxfinance_sfin0000.spec.js +133 -0
  85. package/src/app-gocardless/banks/tests/spk_marburg_biedenkopf_heladef1mar.spec.js +256 -0
  86. package/src/app-gocardless/banks/tests/ssk_dusseldorf_dussdeddxxx.spec.js +102 -0
  87. package/src/app-gocardless/banks/tests/swedbank_habalv22.spec.js +57 -0
  88. package/src/app-gocardless/banks/tests/virgin_nrnbgb22.spec.js +54 -0
  89. package/src/app-gocardless/banks/util/extract-payeeName-from-remittanceInfo.js +36 -0
  90. package/src/app-gocardless/banks/virgin_nrnbgb22.js +39 -0
  91. package/src/app-gocardless/errors.js +84 -0
  92. package/src/app-gocardless/gocardless-node.types.ts +497 -0
  93. package/src/app-gocardless/gocardless.types.ts +93 -0
  94. package/src/app-gocardless/link.html +18 -0
  95. package/src/app-gocardless/services/gocardless-service.js +620 -0
  96. package/src/app-gocardless/services/tests/fixtures.js +181 -0
  97. package/src/app-gocardless/services/tests/gocardless-service.spec.js +537 -0
  98. package/src/app-gocardless/tests/bank-factory.spec.js +20 -0
  99. package/src/app-gocardless/tests/utils.spec.js +162 -0
  100. package/src/app-gocardless/util/handle-error.js +16 -0
  101. package/src/app-gocardless/utils.js +45 -0
  102. package/src/app-openid.js +108 -0
  103. package/src/app-pluggyai/app-pluggyai.js +215 -0
  104. package/src/app-pluggyai/pluggyai-service.js +120 -0
  105. package/src/app-secrets.js +61 -0
  106. package/src/app-simplefin/app-simplefin.js +418 -0
  107. package/src/app-sync/errors.js +13 -0
  108. package/src/app-sync/services/files-service.js +243 -0
  109. package/src/app-sync/tests/services/files-service.test.js +250 -0
  110. package/src/app-sync/validation.js +77 -0
  111. package/src/app-sync.js +391 -0
  112. package/src/app-sync.test.js +877 -0
  113. package/src/app.js +145 -0
  114. package/src/config-types.ts +44 -0
  115. package/src/db.js +58 -0
  116. package/src/load-config.js +307 -0
  117. package/src/migrations.js +36 -0
  118. package/src/run-migrations.js +8 -0
  119. package/src/scripts/disable-openid.js +44 -0
  120. package/src/scripts/enable-openid.js +53 -0
  121. package/src/scripts/health-check.js +23 -0
  122. package/src/scripts/reset-password.js +51 -0
  123. package/src/secrets.test.js +83 -0
  124. package/src/services/secrets-service.js +94 -0
  125. package/src/services/user-service.js +272 -0
  126. package/src/sql/messages.sql +9 -0
  127. package/src/sync-simple.js +95 -0
  128. package/src/util/hash.js +5 -0
  129. package/src/util/middlewares.js +62 -0
  130. package/src/util/paths.js +13 -0
  131. package/src/util/payee-name.js +45 -0
  132. package/src/util/prompt.js +88 -0
  133. package/src/util/title/index.js +59 -0
  134. package/src/util/title/lower-case.js +93 -0
  135. package/src/util/title/specials.js +21 -0
  136. package/src/util/validate-user.js +68 -0
  137. 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
+ };