@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,620 @@
1
+ import jwt from 'jws';
2
+ import * as nordigenNode from 'nordigen-node';
3
+ import { v4 as uuidv4 } from 'uuid';
4
+
5
+ import { SecretName, secretsService } from '../../services/secrets-service.js';
6
+ import { BankFactory, BANKS_WITH_LIMITED_HISTORY } from '../bank-factory.js';
7
+ import {
8
+ AccessDeniedError,
9
+ AccountNotLinkedToRequisition,
10
+ GenericGoCardlessError,
11
+ InvalidInputDataError,
12
+ InvalidGoCardlessTokenError,
13
+ NotFoundError,
14
+ RateLimitError,
15
+ ResourceSuspended,
16
+ RequisitionNotLinked,
17
+ ServiceError,
18
+ UnknownError,
19
+ } from '../errors.js';
20
+
21
+ const GoCardlessClient = nordigenNode.default;
22
+
23
+ const clients = new Map();
24
+
25
+ const getGocardlessClient = () => {
26
+ const secrets = {
27
+ secretId: secretsService.get(SecretName.gocardless_secretId),
28
+ secretKey: secretsService.get(SecretName.gocardless_secretKey),
29
+ };
30
+
31
+ const hash = JSON.stringify(secrets);
32
+
33
+ if (!clients.has(hash)) {
34
+ clients.set(hash, new GoCardlessClient(secrets));
35
+ }
36
+
37
+ return clients.get(hash);
38
+ };
39
+
40
+ export const handleGoCardlessError = error => {
41
+ const status = error?.response?.status;
42
+
43
+ switch (status) {
44
+ case 400:
45
+ throw new InvalidInputDataError(error);
46
+ case 401:
47
+ throw new InvalidGoCardlessTokenError(error);
48
+ case 403:
49
+ throw new AccessDeniedError(error);
50
+ case 404:
51
+ throw new NotFoundError(error);
52
+ case 409:
53
+ throw new ResourceSuspended(error);
54
+ case 429:
55
+ throw new RateLimitError(error);
56
+ case 500:
57
+ throw new UnknownError(error);
58
+ case 503:
59
+ throw new ServiceError(error);
60
+ default:
61
+ throw new GenericGoCardlessError(error);
62
+ }
63
+ };
64
+
65
+ export const goCardlessService = {
66
+ /**
67
+ * Check if the GoCardless service is configured to be used.
68
+ * @returns {boolean}
69
+ */
70
+ isConfigured: () => {
71
+ return !!(
72
+ getGocardlessClient().secretId && getGocardlessClient().secretKey
73
+ );
74
+ },
75
+
76
+ /**
77
+ *
78
+ * @returns {Promise<void>}
79
+ */
80
+ setToken: async () => {
81
+ const isExpiredJwtToken = token => {
82
+ const decodedToken = jwt.decode(token);
83
+ if (!decodedToken) {
84
+ return true;
85
+ }
86
+ const payload = decodedToken.payload;
87
+ const clockTimestamp = Math.floor(Date.now() / 1000);
88
+ return clockTimestamp >= payload.exp;
89
+ };
90
+
91
+ if (isExpiredJwtToken(getGocardlessClient().token)) {
92
+ // Generate new access token. Token is valid for 24 hours
93
+ // Note: access_token is automatically injected to other requests after you successfully obtain it
94
+ try {
95
+ await client.generateToken();
96
+ } catch (error) {
97
+ handleGoCardlessError(error);
98
+ }
99
+ }
100
+ },
101
+
102
+ /**
103
+ *
104
+ * @param requisitionId
105
+ * @throws {RequisitionNotLinked} Will throw an error if requisition is not in Linked
106
+ * @throws {InvalidInputDataError}
107
+ * @throws {InvalidGoCardlessTokenError}
108
+ * @throws {AccessDeniedError}
109
+ * @throws {NotFoundError}
110
+ * @throws {ResourceSuspended}
111
+ * @throws {RateLimitError}
112
+ * @throws {UnknownError}
113
+ * @throws {ServiceError}
114
+ * @returns {Promise<import('../gocardless-node.types.js').Requisition>}
115
+ */
116
+ getLinkedRequisition: async requisitionId => {
117
+ const requisition = await goCardlessService.getRequisition(requisitionId);
118
+
119
+ const { status } = requisition;
120
+
121
+ // Continue only if status of requisition is "LN" what does
122
+ // mean that account has been successfully linked to requisition
123
+ if (status !== 'LN') {
124
+ throw new RequisitionNotLinked({ requisitionStatus: status });
125
+ }
126
+
127
+ return requisition;
128
+ },
129
+
130
+ /**
131
+ * Returns requisition and all linked accounts in their Bank format.
132
+ * Each account object is extended about details of the institution
133
+ * @param requisitionId
134
+ * @throws {RequisitionNotLinked} Will throw an error if requisition is not in Linked
135
+ * @throws {InvalidInputDataError}
136
+ * @throws {InvalidGoCardlessTokenError}
137
+ * @throws {AccessDeniedError}
138
+ * @throws {NotFoundError}
139
+ * @throws {ResourceSuspended}
140
+ * @throws {RateLimitError}
141
+ * @throws {UnknownError}
142
+ * @throws {ServiceError}
143
+ * @returns {Promise<{requisition: import('../gocardless-node.types.js').Requisition, accounts: Array<import('../gocardless.types.js').NormalizedAccountDetails>}>}
144
+ */
145
+ getRequisitionWithAccounts: async requisitionId => {
146
+ const requisition =
147
+ await goCardlessService.getLinkedRequisition(requisitionId);
148
+
149
+ const institutionIdSet = new Set();
150
+ const detailedAccounts = await Promise.all(
151
+ requisition.accounts.map(async accountId => {
152
+ const account = await goCardlessService.getDetailedAccount(accountId);
153
+ institutionIdSet.add(account.institution_id);
154
+ return account;
155
+ }),
156
+ );
157
+
158
+ const institutions = await Promise.all(
159
+ Array.from(institutionIdSet).map(async institutionId => {
160
+ return await goCardlessService.getInstitution(institutionId);
161
+ }),
162
+ );
163
+
164
+ const extendedAccounts =
165
+ await goCardlessService.extendAccountsAboutInstitutions({
166
+ accounts: detailedAccounts,
167
+ institutions,
168
+ });
169
+
170
+ const normalizedAccounts = extendedAccounts.map(account => {
171
+ const bankAccount = BankFactory(account.institution_id);
172
+ return bankAccount.normalizeAccount(account);
173
+ });
174
+
175
+ return { requisition, accounts: normalizedAccounts };
176
+ },
177
+
178
+ /**
179
+ *
180
+ * @param requisitionId
181
+ * @param accountId
182
+ * @param startDate
183
+ * @param endDate
184
+ * @throws {AccountNotLinkedToRequisition} Will throw an error if requisition not includes provided account id
185
+ * @throws {RequisitionNotLinked} Will throw an error if requisition is not in Linked
186
+ * @throws {InvalidInputDataError}
187
+ * @throws {InvalidGoCardlessTokenError}
188
+ * @throws {AccessDeniedError}
189
+ * @throws {NotFoundError}
190
+ * @throws {ResourceSuspended}
191
+ * @throws {RateLimitError}
192
+ * @throws {UnknownError}
193
+ * @throws {ServiceError}
194
+ * @returns {Promise<{balances: Array<import('../gocardless-node.types.js').Balance>, institutionId: string, transactions: {booked: Array<import('../gocardless-node.types.js').Transaction>, pending: Array<import('../gocardless-node.types.js').Transaction>, all: Array<import('../gocardless.types.js').TransactionWithBookedStatus>}, startingBalance: number}>}
195
+ */
196
+ getTransactionsWithBalance: async (
197
+ requisitionId,
198
+ accountId,
199
+ startDate,
200
+ endDate,
201
+ ) => {
202
+ const { institution_id, accounts: accountIds } =
203
+ await goCardlessService.getLinkedRequisition(requisitionId);
204
+
205
+ if (!accountIds.includes(accountId)) {
206
+ throw new AccountNotLinkedToRequisition(accountId, requisitionId);
207
+ }
208
+
209
+ const [normalizedTransactions, accountBalance] = await Promise.all([
210
+ goCardlessService.getNormalizedTransactions(
211
+ requisitionId,
212
+ accountId,
213
+ startDate,
214
+ endDate,
215
+ ),
216
+ goCardlessService.getBalances(accountId),
217
+ ]);
218
+
219
+ const transactions = normalizedTransactions.transactions;
220
+
221
+ const bank = BankFactory(institution_id);
222
+
223
+ const startingBalance = bank.calculateStartingBalance(
224
+ transactions.booked,
225
+ accountBalance.balances,
226
+ );
227
+
228
+ return {
229
+ balances: accountBalance.balances,
230
+ institutionId: institution_id,
231
+ startingBalance,
232
+ transactions,
233
+ };
234
+ },
235
+
236
+ /**
237
+ *
238
+ * @param requisitionId
239
+ * @param accountId
240
+ * @param startDate
241
+ * @param endDate
242
+ * @throws {AccountNotLinkedToRequisition} Will throw an error if requisition not includes provided account id
243
+ * @throws {RequisitionNotLinked} Will throw an error if requisition is not in Linked
244
+ * @throws {InvalidInputDataError}
245
+ * @throws {InvalidGoCardlessTokenError}
246
+ * @throws {AccessDeniedError}
247
+ * @throws {NotFoundError}
248
+ * @throws {ResourceSuspended}
249
+ * @throws {RateLimitError}
250
+ * @throws {UnknownError}
251
+ * @throws {ServiceError}
252
+ * @returns {Promise<{institutionId: string, transactions: {booked: Array<import('../gocardless-node.types.js').Transaction>, pending: Array<import('../gocardless-node.types.js').Transaction>, all: Array<import('../gocardless.types.js').TransactionWithBookedStatus>}}>}
253
+ */
254
+ getNormalizedTransactions: async (
255
+ requisitionId,
256
+ accountId,
257
+ startDate,
258
+ endDate,
259
+ ) => {
260
+ const { institution_id, accounts: accountIds } =
261
+ await goCardlessService.getLinkedRequisition(requisitionId);
262
+
263
+ if (!accountIds.includes(accountId)) {
264
+ throw new AccountNotLinkedToRequisition(accountId, requisitionId);
265
+ }
266
+
267
+ const transactions = await goCardlessService.getTransactions({
268
+ institutionId: institution_id,
269
+ accountId,
270
+ startDate,
271
+ endDate,
272
+ });
273
+
274
+ const bank = BankFactory(institution_id);
275
+ const sortedBookedTransactions = bank.sortTransactions(
276
+ transactions.transactions?.booked,
277
+ );
278
+ const sortedPendingTransactions = bank.sortTransactions(
279
+ transactions.transactions?.pending,
280
+ );
281
+ const allTransactions = sortedBookedTransactions.map(t => {
282
+ return { ...t, booked: true };
283
+ });
284
+ sortedPendingTransactions.forEach(t =>
285
+ allTransactions.push({ ...t, booked: false }),
286
+ );
287
+ const sortedAllTransactions = bank.sortTransactions(allTransactions);
288
+
289
+ return {
290
+ institutionId: institution_id,
291
+ transactions: {
292
+ booked: sortedBookedTransactions,
293
+ pending: sortedPendingTransactions,
294
+ all: sortedAllTransactions,
295
+ },
296
+ };
297
+ },
298
+
299
+ /**
300
+ *
301
+ * @param {import('../gocardless.types.js').CreateRequisitionParams} params
302
+ * @throws {InvalidInputDataError}
303
+ * @throws {InvalidGoCardlessTokenError}
304
+ * @throws {AccessDeniedError}
305
+ * @throws {NotFoundError}
306
+ * @throws {ResourceSuspended}
307
+ * @throws {RateLimitError}
308
+ * @throws {UnknownError}
309
+ * @throws {ServiceError}
310
+ * @returns {Promise<{requisitionId, link}>}
311
+ */
312
+ createRequisition: async ({ institutionId, host }) => {
313
+ await goCardlessService.setToken();
314
+
315
+ const institution = await goCardlessService.getInstitution(institutionId);
316
+
317
+ let response;
318
+ try {
319
+ response = await client.initSession({
320
+ redirectUrl: host + '/gocardless/link',
321
+ institutionId,
322
+ referenceId: uuidv4(),
323
+ accessValidForDays: institution.max_access_valid_for_days,
324
+ maxHistoricalDays: BANKS_WITH_LIMITED_HISTORY.includes(institutionId)
325
+ ? Number(institution.transaction_total_days) >= 90
326
+ ? '89'
327
+ : institution.transaction_total_days
328
+ : institution.transaction_total_days,
329
+ userLanguage: 'en',
330
+ ssn: null,
331
+ redirectImmediate: false,
332
+ accountSelection: false,
333
+ });
334
+ } catch (error) {
335
+ handleGoCardlessError(error);
336
+ }
337
+
338
+ const { link, id: requisitionId } = response;
339
+
340
+ return {
341
+ link,
342
+ requisitionId,
343
+ };
344
+ },
345
+
346
+ /**
347
+ * Deletes requisition by provided ID
348
+ * @param requisitionId
349
+ * @throws {InvalidInputDataError}
350
+ * @throws {InvalidGoCardlessTokenError}
351
+ * @throws {AccessDeniedError}
352
+ * @throws {NotFoundError}
353
+ * @throws {ResourceSuspended}
354
+ * @throws {RateLimitError}
355
+ * @throws {UnknownError}
356
+ * @throws {ServiceError}
357
+ * @returns {Promise<{summary: string, detail: string}>}
358
+ */
359
+ deleteRequisition: async requisitionId => {
360
+ await goCardlessService.getRequisition(requisitionId);
361
+
362
+ let response;
363
+ try {
364
+ response = client.deleteRequisition(requisitionId);
365
+ } catch (error) {
366
+ handleGoCardlessError(error);
367
+ }
368
+
369
+ return response;
370
+ },
371
+
372
+ /**
373
+ * Retrieve a requisition by ID
374
+ * https://nordigen.com/en/docs/account-information/integration/parameters-and-responses/#/requisitions/requisition%20by%20id
375
+ * @param { string } requisitionId
376
+ * @throws {InvalidInputDataError}
377
+ * @throws {InvalidGoCardlessTokenError}
378
+ * @throws {AccessDeniedError}
379
+ * @throws {NotFoundError}
380
+ * @throws {ResourceSuspended}
381
+ * @throws {RateLimitError}
382
+ * @throws {UnknownError}
383
+ * @throws {ServiceError}
384
+ * @returns { Promise<import('../gocardless-node.types.js').Requisition> }
385
+ */
386
+ getRequisition: async requisitionId => {
387
+ await goCardlessService.setToken();
388
+
389
+ let response;
390
+ try {
391
+ response = client.getRequisitionById(requisitionId);
392
+ } catch (error) {
393
+ handleGoCardlessError(error);
394
+ }
395
+
396
+ return response;
397
+ },
398
+
399
+ /**
400
+ * Retrieve an detailed account by account id
401
+ * @param accountId
402
+ * @returns {Promise<import('../gocardless.types.js').DetailedAccount>}
403
+ */
404
+ getDetailedAccount: async accountId => {
405
+ let detailedAccount, metadataAccount;
406
+ try {
407
+ [detailedAccount, metadataAccount] = await Promise.all([
408
+ client.getDetails(accountId),
409
+ client.getMetadata(accountId),
410
+ ]);
411
+ } catch (error) {
412
+ handleGoCardlessError(error);
413
+ }
414
+
415
+ return {
416
+ ...detailedAccount.account,
417
+ ...metadataAccount,
418
+ };
419
+ },
420
+
421
+ /**
422
+ * Retrieve account metadata by account id
423
+ *
424
+ * Unlike getDetailedAccount, this method is not affected by institution rate-limits.
425
+ *
426
+ * @param accountId
427
+ * @returns {Promise<import('../gocardless-node.types.js').GoCardlessAccountMetadata>}
428
+ */
429
+ getAccountMetadata: async accountId => {
430
+ let response;
431
+ try {
432
+ response = await client.getMetadata(accountId);
433
+ } catch (error) {
434
+ handleGoCardlessError(error);
435
+ }
436
+
437
+ return response;
438
+ },
439
+
440
+ /**
441
+ * Retrieve details about all Institutions in a specific country
442
+ * @param country
443
+ * @throws {InvalidInputDataError}
444
+ * @throws {InvalidGoCardlessTokenError}
445
+ * @throws {AccessDeniedError}
446
+ * @throws {NotFoundError}
447
+ * @throws {ResourceSuspended}
448
+ * @throws {RateLimitError}
449
+ * @throws {UnknownError}
450
+ * @throws {ServiceError}
451
+ * @returns {Promise<Array<import('../gocardless-node.types.js').Institution>>}
452
+ */
453
+ getInstitutions: async country => {
454
+ let response;
455
+ try {
456
+ response = await client.getInstitutions(country);
457
+ } catch (error) {
458
+ handleGoCardlessError(error);
459
+ }
460
+
461
+ return response;
462
+ },
463
+
464
+ /**
465
+ * Retrieve details about a specific Institution
466
+ * @param institutionId
467
+ * @throws {InvalidInputDataError}
468
+ * @throws {InvalidGoCardlessTokenError}
469
+ * @throws {AccessDeniedError}
470
+ * @throws {NotFoundError}
471
+ * @throws {ResourceSuspended}
472
+ * @throws {RateLimitError}
473
+ * @throws {UnknownError}
474
+ * @throws {ServiceError}
475
+ * @returns {Promise<import('../gocardless-node.types.js').Institution>}
476
+ */
477
+ getInstitution: async institutionId => {
478
+ let response;
479
+ try {
480
+ response = await client.getInstitutionById(institutionId);
481
+ } catch (error) {
482
+ handleGoCardlessError(error);
483
+ }
484
+
485
+ return response;
486
+ },
487
+
488
+ /**
489
+ * Extends provided accounts about details of their institution
490
+ * @param {{accounts: Array<import('../gocardless.types.js').DetailedAccount>, institutions: Array<import('../gocardless-node.types.js').Institution>}} params
491
+ * @returns {Promise<Array<import('../gocardless.types.js').DetailedAccount&{institution: import('../gocardless-node.types.js').Institution}>>}
492
+ */
493
+ extendAccountsAboutInstitutions: async ({ accounts, institutions }) => {
494
+ const institutionsById = institutions.reduce((acc, institution) => {
495
+ acc[institution.id] = institution;
496
+ return acc;
497
+ }, {});
498
+
499
+ return accounts.map(account => {
500
+ const institution = institutionsById[account.institution_id] || null;
501
+ return {
502
+ ...account,
503
+ institution,
504
+ };
505
+ });
506
+ },
507
+
508
+ /**
509
+ * Returns account transaction in provided dates
510
+ * @param {import('../gocardless.types.js').GetTransactionsParams} params
511
+ * @throws {InvalidInputDataError}
512
+ * @throws {InvalidGoCardlessTokenError}
513
+ * @throws {AccessDeniedError}
514
+ * @throws {NotFoundError}
515
+ * @throws {ResourceSuspended}
516
+ * @throws {RateLimitError}
517
+ * @throws {UnknownError}
518
+ * @throws {ServiceError}
519
+ * @returns {Promise<import('../gocardless.types.js').GetTransactionsResponse>}
520
+ */
521
+ getTransactions: async ({ institutionId, accountId, startDate, endDate }) => {
522
+ let response;
523
+ try {
524
+ response = await client.getTransactions({
525
+ accountId,
526
+ dateFrom: startDate,
527
+ dateTo: endDate,
528
+ });
529
+ } catch (error) {
530
+ handleGoCardlessError(error);
531
+ }
532
+
533
+ const bank = BankFactory(institutionId);
534
+ response.transactions.booked = response.transactions.booked
535
+ .map(transaction => bank.normalizeTransaction(transaction, true))
536
+ .filter(transaction => transaction);
537
+ response.transactions.pending = response.transactions.pending
538
+ .map(transaction => bank.normalizeTransaction(transaction, false))
539
+ .filter(transaction => transaction);
540
+
541
+ return response;
542
+ },
543
+
544
+ /**
545
+ * Returns account available balances
546
+ * @param accountId
547
+ * @throws {InvalidInputDataError}
548
+ * @throws {InvalidGoCardlessTokenError}
549
+ * @throws {AccessDeniedError}
550
+ * @throws {NotFoundError}
551
+ * @throws {ResourceSuspended}
552
+ * @throws {RateLimitError}
553
+ * @throws {UnknownError}
554
+ * @throws {ServiceError}
555
+ * @returns {Promise<import('../gocardless.types.js').GetBalances>}
556
+ */
557
+ getBalances: async accountId => {
558
+ let response;
559
+ try {
560
+ response = await client.getBalances(accountId);
561
+ } catch (error) {
562
+ handleGoCardlessError(error);
563
+ }
564
+
565
+ return response;
566
+ },
567
+ };
568
+
569
+ /**
570
+ * All executions of goCardlessClient should be here for testing purposes,
571
+ * as the nordigen-node library is not written in a way that is conducive to testing.
572
+ * In that way we can mock the `client` const instead of nordigen library
573
+ */
574
+ export const client = {
575
+ getBalances: async accountId =>
576
+ await getGocardlessClient().account(accountId).getBalances(),
577
+ getTransactions: async ({ accountId, dateFrom, dateTo }) =>
578
+ await getGocardlessClient().account(accountId).getTransactions({
579
+ dateFrom,
580
+ dateTo,
581
+ country: undefined,
582
+ }),
583
+ getInstitutions: async country =>
584
+ await getGocardlessClient().institution.getInstitutions({ country }),
585
+ getInstitutionById: async institutionId =>
586
+ await getGocardlessClient().institution.getInstitutionById(institutionId),
587
+ getDetails: async accountId =>
588
+ await getGocardlessClient().account(accountId).getDetails(),
589
+ getMetadata: async accountId =>
590
+ await getGocardlessClient().account(accountId).getMetadata(),
591
+ getRequisitionById: async requisitionId =>
592
+ await getGocardlessClient().requisition.getRequisitionById(requisitionId),
593
+ deleteRequisition: async requisitionId =>
594
+ await getGocardlessClient().requisition.deleteRequisition(requisitionId),
595
+ initSession: async ({
596
+ redirectUrl,
597
+ institutionId,
598
+ referenceId,
599
+ accessValidForDays,
600
+ maxHistoricalDays,
601
+ userLanguage,
602
+ ssn,
603
+ redirectImmediate,
604
+ accountSelection,
605
+ }) =>
606
+ await getGocardlessClient().initSession({
607
+ redirectUrl,
608
+ institutionId,
609
+ referenceId,
610
+ accessValidForDays,
611
+ maxHistoricalDays,
612
+ userLanguage,
613
+ ssn,
614
+ redirectImmediate,
615
+ accountSelection,
616
+ }),
617
+ generateToken: async () => await getGocardlessClient().generateToken(),
618
+ exchangeToken: async ({ refreshToken }) =>
619
+ await getGocardlessClient().exchangeToken({ refreshToken }),
620
+ };