@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,381 @@
1
+ import request from 'supertest';
2
+ import { v4 as uuidv4 } from 'uuid';
3
+
4
+ import { getAccountDb } from './account-db.js';
5
+ import { handlers as app } from './app-admin.js';
6
+
7
+ const ADMIN_ROLE = 'ADMIN';
8
+ const BASIC_ROLE = 'BASIC';
9
+
10
+ // Create user helper function
11
+ const createUser = (userId, userName, role, owner = 0, enabled = 1) => {
12
+ getAccountDb().mutate(
13
+ 'INSERT INTO users (id, user_name, display_name, enabled, owner, role) VALUES (?, ?, ?, ?, ?, ?)',
14
+ [userId, userName, `${userName} display`, enabled, owner, role],
15
+ );
16
+ };
17
+
18
+ const deleteUser = userId => {
19
+ getAccountDb().mutate('DELETE FROM user_access WHERE user_id = ?', [userId]);
20
+ getAccountDb().mutate('DELETE FROM users WHERE id = ?', [userId]);
21
+ };
22
+
23
+ const createSession = (userId, sessionToken) => {
24
+ getAccountDb().mutate(
25
+ 'INSERT INTO sessions (token, user_id, expires_at) VALUES (?, ?, ?)',
26
+ [sessionToken, userId, Date.now() + 1000 * 60 * 60], // Expire in 1 hour
27
+ );
28
+ };
29
+
30
+ const generateSessionToken = () => `token-${uuidv4()}`;
31
+
32
+ describe('/admin', () => {
33
+ describe('/owner-created', () => {
34
+ it('should return 200 and true if an owner user is created', async () => {
35
+ const sessionToken = generateSessionToken();
36
+ const adminId = uuidv4();
37
+ createUser(adminId, 'admin', ADMIN_ROLE, 1);
38
+ createSession(adminId, sessionToken);
39
+
40
+ const res = await request(app)
41
+ .get('/owner-created')
42
+ .set('x-actual-token', sessionToken);
43
+
44
+ expect(res.statusCode).toEqual(200);
45
+ expect(res.body).toBe(true);
46
+ });
47
+ });
48
+
49
+ describe('/users', () => {
50
+ describe('GET /users', () => {
51
+ let sessionUserId, testUserId, sessionToken;
52
+
53
+ beforeEach(() => {
54
+ sessionUserId = uuidv4();
55
+ testUserId = uuidv4();
56
+ sessionToken = generateSessionToken();
57
+
58
+ createUser(sessionUserId, 'sessionUser', ADMIN_ROLE);
59
+ createSession(sessionUserId, sessionToken);
60
+ createUser(testUserId, 'testUser', ADMIN_ROLE);
61
+ });
62
+
63
+ afterEach(() => {
64
+ deleteUser(sessionUserId);
65
+ deleteUser(testUserId);
66
+ });
67
+
68
+ it('should return 200 and a list of users', async () => {
69
+ const res = await request(app)
70
+ .get('/users')
71
+ .set('x-actual-token', sessionToken);
72
+
73
+ expect(res.statusCode).toEqual(200);
74
+ expect(res.body.length).toBeGreaterThan(0);
75
+ });
76
+ });
77
+
78
+ describe('POST /users', () => {
79
+ let sessionUserId, sessionToken;
80
+ let createdUserId;
81
+ let duplicateUserId;
82
+
83
+ beforeEach(() => {
84
+ sessionUserId = uuidv4();
85
+ sessionToken = generateSessionToken();
86
+ createUser(sessionUserId, 'sessionUser', ADMIN_ROLE);
87
+ createSession(sessionUserId, sessionToken);
88
+ });
89
+
90
+ afterEach(() => {
91
+ deleteUser(sessionUserId);
92
+ if (createdUserId) {
93
+ deleteUser(createdUserId);
94
+ createdUserId = null;
95
+ }
96
+
97
+ if (duplicateUserId) {
98
+ deleteUser(duplicateUserId);
99
+ duplicateUserId = null;
100
+ }
101
+ });
102
+
103
+ it('should return 200 and create a new user', async () => {
104
+ const newUser = {
105
+ userName: 'user1',
106
+ displayName: 'User One',
107
+ enabled: 1,
108
+ owner: 0,
109
+ role: BASIC_ROLE,
110
+ };
111
+
112
+ const res = await request(app)
113
+ .post('/users')
114
+ .send(newUser)
115
+ .set('x-actual-token', sessionToken);
116
+
117
+ expect(res.statusCode).toEqual(200);
118
+ expect(res.body.status).toBe('ok');
119
+ expect(res.body.data).toHaveProperty('id');
120
+
121
+ createdUserId = res.body.data.id;
122
+ });
123
+
124
+ it('should return 400 if the user already exists', async () => {
125
+ const newUser = {
126
+ userName: 'user1',
127
+ displayName: 'User One',
128
+ enabled: 1,
129
+ owner: 0,
130
+ role: BASIC_ROLE,
131
+ };
132
+
133
+ let res = await request(app)
134
+ .post('/users')
135
+ .send(newUser)
136
+ .set('x-actual-token', sessionToken);
137
+
138
+ duplicateUserId = res.body.data.id;
139
+
140
+ res = await request(app)
141
+ .post('/users')
142
+ .send(newUser)
143
+ .set('x-actual-token', sessionToken);
144
+
145
+ expect(res.statusCode).toEqual(400);
146
+ expect(res.body.status).toBe('error');
147
+ expect(res.body.reason).toBe('user-already-exists');
148
+ });
149
+ });
150
+
151
+ describe('PATCH /users', () => {
152
+ let sessionUserId, testUserId, sessionToken;
153
+
154
+ beforeEach(() => {
155
+ sessionUserId = uuidv4();
156
+ testUserId = uuidv4();
157
+ sessionToken = generateSessionToken();
158
+
159
+ createUser(sessionUserId, 'sessionUser', ADMIN_ROLE);
160
+ createSession(sessionUserId, sessionToken);
161
+ createUser(testUserId, 'testUser', ADMIN_ROLE);
162
+ });
163
+
164
+ afterEach(() => {
165
+ deleteUser(sessionUserId);
166
+ deleteUser(testUserId);
167
+ });
168
+
169
+ it('should return 200 and update an existing user', async () => {
170
+ const userToUpdate = {
171
+ id: testUserId,
172
+ userName: 'updatedUser',
173
+ displayName: 'Updated User',
174
+ enabled: true,
175
+ role: BASIC_ROLE,
176
+ };
177
+
178
+ const res = await request(app)
179
+ .patch('/users')
180
+ .send(userToUpdate)
181
+ .set('x-actual-token', sessionToken);
182
+
183
+ expect(res.statusCode).toEqual(200);
184
+ expect(res.body.status).toBe('ok');
185
+ expect(res.body.data.id).toBe(testUserId);
186
+ });
187
+
188
+ it('should return 400 if the user does not exist', async () => {
189
+ const userToUpdate = {
190
+ id: 'non-existing-id',
191
+ userName: 'nonexistinguser',
192
+ displayName: 'Non-existing User',
193
+ enabled: true,
194
+ role: BASIC_ROLE,
195
+ };
196
+
197
+ const res = await request(app)
198
+ .patch('/users')
199
+ .send(userToUpdate)
200
+ .set('x-actual-token', sessionToken);
201
+
202
+ expect(res.statusCode).toEqual(400);
203
+ expect(res.body.status).toBe('error');
204
+ expect(res.body.reason).toBe('cannot-find-user-to-update');
205
+ });
206
+ });
207
+
208
+ describe('POST /users/delete-all', () => {
209
+ let sessionUserId, testUserId, sessionToken;
210
+
211
+ beforeEach(() => {
212
+ sessionUserId = uuidv4();
213
+ testUserId = uuidv4();
214
+ sessionToken = generateSessionToken();
215
+
216
+ createUser(sessionUserId, 'sessionUser', ADMIN_ROLE);
217
+ createSession(sessionUserId, sessionToken);
218
+ createUser(testUserId, 'testUser', ADMIN_ROLE);
219
+ });
220
+
221
+ afterEach(() => {
222
+ deleteUser(sessionUserId);
223
+ deleteUser(testUserId);
224
+ });
225
+
226
+ it('should return 200 and delete all specified users', async () => {
227
+ const userToDelete = {
228
+ ids: [testUserId],
229
+ };
230
+
231
+ const res = await request(app)
232
+ .delete('/users')
233
+ .send(userToDelete)
234
+ .set('x-actual-token', sessionToken);
235
+
236
+ expect(res.statusCode).toEqual(200);
237
+ expect(res.body.status).toBe('ok');
238
+ expect(res.body.data.someDeletionsFailed).toBe(false);
239
+ });
240
+
241
+ it('should return 400 if not all users are deleted', async () => {
242
+ const userToDelete = {
243
+ ids: ['non-existing-id'],
244
+ };
245
+
246
+ const res = await request(app)
247
+ .delete('/users')
248
+ .send(userToDelete)
249
+ .set('x-actual-token', sessionToken);
250
+
251
+ expect(res.statusCode).toEqual(400);
252
+ expect(res.body.status).toBe('error');
253
+ expect(res.body.reason).toBe('not-all-deleted');
254
+ });
255
+ });
256
+ });
257
+
258
+ describe('/access', () => {
259
+ describe('POST /access', () => {
260
+ let sessionUserId, testUserId, fileId, sessionToken;
261
+
262
+ beforeEach(() => {
263
+ sessionUserId = uuidv4();
264
+ testUserId = uuidv4();
265
+ fileId = uuidv4();
266
+ sessionToken = generateSessionToken();
267
+
268
+ createUser(sessionUserId, 'sessionUser', ADMIN_ROLE);
269
+ createSession(sessionUserId, sessionToken);
270
+ createUser(testUserId, 'testUser', ADMIN_ROLE);
271
+ getAccountDb().mutate('INSERT INTO files (id, owner) VALUES (?, ?)', [
272
+ fileId,
273
+ sessionUserId,
274
+ ]);
275
+ });
276
+
277
+ afterEach(() => {
278
+ deleteUser(sessionUserId);
279
+ deleteUser(testUserId);
280
+ getAccountDb().mutate('DELETE FROM files WHERE id = ?', [fileId]);
281
+ });
282
+
283
+ it('should return 200 and grant access to a user', async () => {
284
+ const newUserAccess = {
285
+ fileId,
286
+ userId: testUserId,
287
+ };
288
+
289
+ const res = await request(app)
290
+ .post('/access')
291
+ .send(newUserAccess)
292
+ .set('x-actual-token', sessionToken);
293
+
294
+ expect(res.statusCode).toEqual(200);
295
+ expect(res.body.status).toBe('ok');
296
+ });
297
+
298
+ it('should return 400 if the user already has access', async () => {
299
+ const newUserAccess = {
300
+ fileId,
301
+ userId: testUserId,
302
+ };
303
+
304
+ await request(app)
305
+ .post('/access')
306
+ .send(newUserAccess)
307
+ .set('x-actual-token', sessionToken);
308
+
309
+ const res = await request(app)
310
+ .post('/access')
311
+ .send(newUserAccess)
312
+ .set('x-actual-token', sessionToken);
313
+
314
+ expect(res.statusCode).toEqual(400);
315
+ expect(res.body.status).toBe('error');
316
+ expect(res.body.reason).toBe('user-already-have-access');
317
+ });
318
+ });
319
+
320
+ describe('DELETE /access', () => {
321
+ let sessionUserId, testUserId, fileId, sessionToken;
322
+
323
+ beforeEach(() => {
324
+ sessionUserId = uuidv4();
325
+ testUserId = uuidv4();
326
+ fileId = uuidv4();
327
+ sessionToken = generateSessionToken();
328
+
329
+ createUser(sessionUserId, 'sessionUser', ADMIN_ROLE);
330
+ createSession(sessionUserId, sessionToken);
331
+ createUser(testUserId, 'testUser', ADMIN_ROLE);
332
+ getAccountDb().mutate('INSERT INTO files (id, owner) VALUES (?, ?)', [
333
+ fileId,
334
+ sessionUserId,
335
+ ]);
336
+ getAccountDb().mutate(
337
+ 'INSERT INTO user_access (user_id, file_id) VALUES (?, ?)',
338
+ [testUserId, fileId],
339
+ );
340
+ });
341
+
342
+ afterEach(() => {
343
+ deleteUser(sessionUserId);
344
+ deleteUser(testUserId);
345
+ getAccountDb().mutate('DELETE FROM files WHERE id = ?', [fileId]);
346
+ });
347
+
348
+ it('should return 200 and delete access for the specified user', async () => {
349
+ const deleteAccess = {
350
+ ids: [testUserId],
351
+ };
352
+
353
+ const res = await request(app)
354
+ .delete('/access')
355
+ .send(deleteAccess)
356
+ .query({ fileId })
357
+ .set('x-actual-token', sessionToken);
358
+
359
+ expect(res.statusCode).toEqual(200);
360
+ expect(res.body.status).toBe('ok');
361
+ expect(res.body.data.someDeletionsFailed).toBe(false);
362
+ });
363
+
364
+ it('should return 400 if not all access deletions are successful', async () => {
365
+ const deleteAccess = {
366
+ ids: ['non-existing-id'],
367
+ };
368
+
369
+ const res = await request(app)
370
+ .delete('/access')
371
+ .send(deleteAccess)
372
+ .query({ fileId })
373
+ .set('x-actual-token', sessionToken);
374
+
375
+ expect(res.statusCode).toEqual(400);
376
+ expect(res.body.status).toBe('error');
377
+ expect(res.body.reason).toBe('not-all-deleted');
378
+ });
379
+ });
380
+ });
381
+ });
@@ -0,0 +1,198 @@
1
+ # Integration new bank
2
+
3
+ If the default bank integration does not work for you, you can integrate a new bank by following these steps.
4
+
5
+ 1. Find in [this google doc](https://docs.google.com/spreadsheets/d/1ogpzydzotOltbssrc3IQ8rhBLlIZbQgm5QCiiNJrkyA/edit#gid=489769432) what is the identifier of the bank which you want to integrate.
6
+
7
+ 2. Launch frontend and backend server.
8
+
9
+ 3. In the frontend, create a new linked account selecting the institution which you are interested in.
10
+
11
+ This will trigger the process of fetching the data from the bank and will log the data in the backend. Use this data to fill the logic of the bank class.
12
+
13
+ 4. Create new a bank class based on an existing example in `app-gocardless/banks`.
14
+
15
+ Name of the file and class should follow the existing patterns and be created based on the ID of the integrated institution, found in step 1.
16
+
17
+ 5. Fill the logic of `normalizeAccount`, `normalizeTransaction`, `sortTransactions`, and `calculateStartingBalance` functions.
18
+ You do not need to fill every function, only those which are necessary for the integration to work.
19
+
20
+ You should do it based on the data which you found in the logs.
21
+
22
+ Example logs which help you to fill:
23
+
24
+ - `normalizeAccount` function:
25
+
26
+ ```log
27
+ Available account properties for new institution integration {
28
+ account: '{
29
+ "iban": "PL00000000000000000987654321",
30
+ "currency": "PLN",
31
+ "ownerName": "John Example",
32
+ "displayName": "Product name",
33
+ "product": "Daily account",
34
+ "usage": "PRIV",
35
+ "ownerAddressUnstructured": [
36
+ "POL",
37
+ "UL. Example 1",
38
+ "00-000 Warsaw"
39
+ ],
40
+ "id": "XXXXXXXX-XXXX-XXXXX-XXXXXX-XXXXXXXXX",
41
+ "created": "2023-01-18T12:15:16.502446Z",
42
+ "last_accessed": null,
43
+ "institution_id": "MBANK_RETAIL_BREXPLPW",
44
+ "status": "READY",
45
+ "owner_name": "",
46
+ "institution": {
47
+ "id": "MBANK_RETAIL_BREXPLPW",
48
+ "name": "mBank Retail",
49
+ "bic": "BREXPLPW",
50
+ "transaction_total_days": "90",
51
+ "countries": [
52
+ "PL"
53
+ ],
54
+ "logo": "https://cdn.nordigen.com/ais/MBANK_RETAIL_BREXCZPP.png",
55
+ "supported_payments": {},
56
+ "supported_features": [
57
+ "access_scopes",
58
+ "business_accounts",
59
+ "card_accounts",
60
+ "corporate_accounts",
61
+ "pending_transactions",
62
+ "private_accounts"
63
+ ]
64
+ }
65
+ }'
66
+ }
67
+ ```
68
+
69
+ - `sortTransactions` function:
70
+
71
+ ```log
72
+ Available (first 10) transactions properties for new integration of institution in sortTransactions function
73
+ {
74
+ top10SortedTransactions: '[
75
+ {
76
+ "transactionId": "20220101001",
77
+ "bookingDate": "2022-01-01",
78
+ "valueDate": "2022-01-01",
79
+ "transactionAmount": {
80
+ "amount": "5.01",
81
+ "currency": "EUR"
82
+ },
83
+ "creditorName": "JOHN EXAMPLE",
84
+ "creditorAccount": {
85
+ "iban": "PL00000000000000000987654321"
86
+ },
87
+ "debtorName": "CHRIS EXAMPLE",
88
+ "debtorAccount": {
89
+ "iban": "PL12345000000000000987654321"
90
+ },
91
+ "remittanceInformationUnstructured": "TEST BANK TRANSFER",
92
+ "remittanceInformationUnstructuredArray": [
93
+ "TEST BANK TRANSFER"
94
+ ],
95
+ "balanceAfterTransaction": {
96
+ "balanceAmount": {
97
+ "amount": "448.52",
98
+ "currency": "EUR"
99
+ },
100
+ "balanceType": "interimBooked"
101
+ },
102
+ "internalTransactionId": "casfib7720c2a02c0331cw2"
103
+ }
104
+ ]'
105
+ }
106
+ ```
107
+
108
+ - `calculateStartingBalance` function:
109
+
110
+ ```log
111
+ Available (first 10) transactions properties for new integration of institution in calculateStartingBalance function {
112
+ balances: '[
113
+ {
114
+ "balanceAmount": {
115
+ "amount": "448.52",
116
+ "currency": "EUR"
117
+ },
118
+ "balanceType": "forwardAvailable"
119
+ },
120
+ {
121
+ "balanceAmount": {
122
+ "amount": "448.52",
123
+ "currency": "EUR"
124
+ },
125
+ "balanceType": "interimBooked"
126
+ }
127
+ ]',
128
+ top10SortedTransactions: '[
129
+ {
130
+ "transactionId": "20220101001",
131
+ "bookingDate": "2022-01-01",
132
+ "valueDate": "2022-01-01",
133
+ "transactionAmount": {
134
+ "amount": "5.01",
135
+ "currency": "EUR"
136
+ },
137
+ "creditorName": "JOHN EXAMPLE",
138
+ "creditorAccount": {
139
+ "iban": "PL00000000000000000987654321"
140
+ },
141
+ "debtorName": "CHRIS EXAMPLE",
142
+ "debtorAccount": {
143
+ "iban": "PL12345000000000000987654321"
144
+ },
145
+ "remittanceInformationUnstructured": "TEST BANK TRANSFER",
146
+ "remittanceInformationUnstructuredArray": [
147
+ "TEST BANK TRANSFER"
148
+ ],
149
+ "balanceAfterTransaction": {
150
+ "balanceAmount": {
151
+ "amount": "448.52",
152
+ "currency": "EUR"
153
+ },
154
+ "balanceType": "interimBooked"
155
+ },
156
+ "internalTransactionId": "casfib7720c2a02c0331cw2"
157
+ }
158
+ ]'
159
+ }
160
+ ```
161
+
162
+ 6. Add new bank integration to `BankFactory` class in file `actual-server/app-gocardless/bank-factory.js`
163
+
164
+ 7. Remember to add tests for new bank integration in
165
+
166
+ ## normalizeTransaction
167
+
168
+ This is the most commonly used override as it allows you to change the data that is returned to the client.
169
+
170
+ Please follow the following patterns when implementing a custom normalizeTransaction method:
171
+
172
+ 1. If you need to edit the values of transaction fields (excluding the transaction amount) do not mutate the original transaction object. Instead, create a shallow copy and make your changes there.
173
+ 2. End the function by returning the result of calling the fallback normalizeTransaction method from integration-bank.js
174
+
175
+ E.g.
176
+
177
+ ```js
178
+ import Fallback from './integration-bank.js';
179
+
180
+ export default {
181
+ ...
182
+
183
+ normalizeTransaction(transaction, booked) {
184
+ // create a shallow copy of the transaction object
185
+ const editedTrans = { ...transaction };
186
+
187
+ // make any changes required to the copy
188
+ editedTrans.remittanceInformationUnstructured = transaction.remittanceInformationStructured;
189
+
190
+ // call the fallback method, passing in your edited transaction as the 3rd parameter
191
+ // this will calculate the date, payee name and notes fields based on your changes
192
+ // but leave the original fields available for mapping in the UI
193
+ return Fallback.normalizeTransaction(transaction, booked, editedTrans);
194
+ }
195
+
196
+ ...
197
+ }
198
+ ```