@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,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
|
+
};
|