@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,162 @@
1
+ import { mockTransactionAmount } from '../services/tests/fixtures.js';
2
+ import { sortByBookingDateOrValueDate } from '../utils.js';
3
+
4
+ describe('utils', () => {
5
+ describe('#sortByBookingDate', () => {
6
+ it('sorts transactions by bookingDate field from newest to oldest', () => {
7
+ const transactions = [
8
+ {
9
+ bookingDate: '2023-01-01',
10
+ transactionAmount: mockTransactionAmount,
11
+ },
12
+ {
13
+ bookingDate: '2023-01-20',
14
+ transactionAmount: mockTransactionAmount,
15
+ },
16
+ {
17
+ bookingDate: '2023-01-10',
18
+ transactionAmount: mockTransactionAmount,
19
+ },
20
+ ];
21
+ expect(sortByBookingDateOrValueDate(transactions)).toEqual([
22
+ {
23
+ bookingDate: '2023-01-20',
24
+ transactionAmount: mockTransactionAmount,
25
+ },
26
+ {
27
+ bookingDate: '2023-01-10',
28
+ transactionAmount: mockTransactionAmount,
29
+ },
30
+ {
31
+ bookingDate: '2023-01-01',
32
+ transactionAmount: mockTransactionAmount,
33
+ },
34
+ ]);
35
+ });
36
+
37
+ it('should sort by valueDate if bookingDate is missing', () => {
38
+ const transactions = [
39
+ {
40
+ valueDate: '2023-01-01',
41
+ transactionAmount: mockTransactionAmount,
42
+ },
43
+ {
44
+ valueDate: '2023-01-20',
45
+ transactionAmount: mockTransactionAmount,
46
+ },
47
+ {
48
+ valueDate: '2023-01-10',
49
+ transactionAmount: mockTransactionAmount,
50
+ },
51
+ ];
52
+ expect(sortByBookingDateOrValueDate(transactions)).toEqual([
53
+ {
54
+ valueDate: '2023-01-20',
55
+ transactionAmount: mockTransactionAmount,
56
+ },
57
+ {
58
+ valueDate: '2023-01-10',
59
+ transactionAmount: mockTransactionAmount,
60
+ },
61
+ {
62
+ valueDate: '2023-01-01',
63
+ transactionAmount: mockTransactionAmount,
64
+ },
65
+ ]);
66
+ });
67
+
68
+ it('should use bookingDate primarily even if bookingDateTime is on an other date', () => {
69
+ const transactions = [
70
+ {
71
+ bookingDate: '2023-01-01',
72
+ bookingDateTime: '2023-01-01T00:00:00Z',
73
+ transactionAmount: mockTransactionAmount,
74
+ },
75
+ {
76
+ bookingDate: '2023-01-10',
77
+ bookingDateTime: '2023-01-01T12:00:00Z',
78
+ transactionAmount: mockTransactionAmount,
79
+ },
80
+ {
81
+ bookingDate: '2023-01-01',
82
+ bookingDateTime: '2023-01-01T12:00:00Z',
83
+ transactionAmount: mockTransactionAmount,
84
+ },
85
+ {
86
+ bookingDate: '2023-01-10',
87
+ bookingDateTime: '2023-01-01T00:00:00Z',
88
+ transactionAmount: mockTransactionAmount,
89
+ },
90
+ ];
91
+ expect(sortByBookingDateOrValueDate(transactions)).toEqual([
92
+ {
93
+ bookingDate: '2023-01-10',
94
+ bookingDateTime: '2023-01-01T12:00:00Z',
95
+ transactionAmount: mockTransactionAmount,
96
+ },
97
+ {
98
+ bookingDate: '2023-01-10',
99
+ bookingDateTime: '2023-01-01T00:00:00Z',
100
+ transactionAmount: mockTransactionAmount,
101
+ },
102
+ {
103
+ bookingDate: '2023-01-01',
104
+ bookingDateTime: '2023-01-01T12:00:00Z',
105
+ transactionAmount: mockTransactionAmount,
106
+ },
107
+ {
108
+ bookingDate: '2023-01-01',
109
+ bookingDateTime: '2023-01-01T00:00:00Z',
110
+ transactionAmount: mockTransactionAmount,
111
+ },
112
+ ]);
113
+ });
114
+
115
+ it('should sort on booking date if value date is widely off', () => {
116
+ const transactions = [
117
+ {
118
+ bookingDate: '2023-01-01',
119
+ valueDateTime: '2023-01-31T00:00:00Z',
120
+ transactionAmount: mockTransactionAmount,
121
+ },
122
+ {
123
+ bookingDate: '2023-01-02',
124
+ valueDateTime: '2023-01-02T12:00:00Z',
125
+ transactionAmount: mockTransactionAmount,
126
+ },
127
+ {
128
+ bookingDate: '2023-01-30',
129
+ valueDateTime: '2023-01-01T12:00:00Z',
130
+ transactionAmount: mockTransactionAmount,
131
+ },
132
+ {
133
+ bookingDate: '2023-01-30',
134
+ valueDateTime: '2023-01-01T00:00:00Z',
135
+ transactionAmount: mockTransactionAmount,
136
+ },
137
+ ];
138
+ expect(sortByBookingDateOrValueDate(transactions)).toEqual([
139
+ {
140
+ bookingDate: '2023-01-30',
141
+ valueDateTime: '2023-01-01T12:00:00Z',
142
+ transactionAmount: mockTransactionAmount,
143
+ },
144
+ {
145
+ bookingDate: '2023-01-30',
146
+ valueDateTime: '2023-01-01T00:00:00Z',
147
+ transactionAmount: mockTransactionAmount,
148
+ },
149
+ {
150
+ bookingDate: '2023-01-02',
151
+ valueDateTime: '2023-01-02T12:00:00Z',
152
+ transactionAmount: mockTransactionAmount,
153
+ },
154
+ {
155
+ bookingDate: '2023-01-01',
156
+ valueDateTime: '2023-01-31T00:00:00Z',
157
+ transactionAmount: mockTransactionAmount,
158
+ },
159
+ ]);
160
+ });
161
+ });
162
+ });
@@ -0,0 +1,16 @@
1
+ import { inspect } from 'util';
2
+
3
+ export function handleError(func) {
4
+ return (req, res) => {
5
+ func(req, res).catch(err => {
6
+ console.log('Error', req.originalUrl, inspect(err, { depth: null }));
7
+ res.send({
8
+ status: 'ok',
9
+ data: {
10
+ error_code: 'INTERNAL_ERROR',
11
+ error_type: err.message ? err.message : 'internal-error',
12
+ },
13
+ });
14
+ });
15
+ };
16
+ }
@@ -0,0 +1,45 @@
1
+ export const printIban = account => {
2
+ if (account.iban) {
3
+ return '(XXX ' + account.iban.slice(-4) + ')';
4
+ } else {
5
+ return '';
6
+ }
7
+ };
8
+
9
+ const compareDates = (
10
+ /** @type {string | number | Date | undefined} */ a,
11
+ /** @type {string | number | Date | undefined} */ b,
12
+ ) => {
13
+ if (a == null && b == null) {
14
+ return 0;
15
+ } else if (a == null) {
16
+ return 1;
17
+ } else if (b == null) {
18
+ return -1;
19
+ }
20
+
21
+ return +new Date(a) - +new Date(b);
22
+ };
23
+
24
+ /**
25
+ * @type {(function(*, *): number)[]}
26
+ */
27
+ const compareFunctions = [
28
+ (a, b) => compareDates(a.bookingDate, b.bookingDate),
29
+ (a, b) => compareDates(a.bookingDateTime, b.bookingDateTime),
30
+ (a, b) => compareDates(a.valueDate, b.valueDate),
31
+ (a, b) => compareDates(a.valueDateTime, b.valueDateTime),
32
+ ];
33
+
34
+ export const sortByBookingDateOrValueDate = (transactions = []) =>
35
+ transactions.sort((a, b) => {
36
+ for (const sortFunction of compareFunctions) {
37
+ const result = sortFunction(b, a);
38
+ if (result !== 0) {
39
+ return result;
40
+ }
41
+ }
42
+ return 0;
43
+ });
44
+
45
+ export const amountToInteger = n => Math.round(n * 100);
@@ -0,0 +1,108 @@
1
+ import express from 'express';
2
+
3
+ import { disableOpenID, enableOpenID, isAdmin } from './account-db.js';
4
+ import {
5
+ isValidRedirectUrl,
6
+ loginWithOpenIdFinalize,
7
+ } from './accounts/openid.js';
8
+ import { checkPassword } from './accounts/password.js';
9
+ import * as UserService from './services/user-service.js';
10
+ import {
11
+ errorMiddleware,
12
+ requestLoggerMiddleware,
13
+ validateSessionMiddleware,
14
+ } from './util/middlewares.js';
15
+
16
+ const app = express();
17
+ app.use(express.json());
18
+ app.use(express.urlencoded({ extended: true }));
19
+ app.use(requestLoggerMiddleware);
20
+ export { app as handlers };
21
+
22
+ app.post('/enable', validateSessionMiddleware, async (req, res) => {
23
+ if (!isAdmin(res.locals.user_id)) {
24
+ res.status(403).send({
25
+ status: 'error',
26
+ reason: 'forbidden',
27
+ details: 'permission-not-found',
28
+ });
29
+ return;
30
+ }
31
+
32
+ const { error } = (await enableOpenID(req.body)) || {};
33
+
34
+ if (error) {
35
+ res.status(500).send({ status: 'error', reason: error });
36
+ return;
37
+ }
38
+ res.send({ status: 'ok' });
39
+ });
40
+
41
+ app.post('/disable', validateSessionMiddleware, async (req, res) => {
42
+ if (!isAdmin(res.locals.user_id)) {
43
+ res.status(403).send({
44
+ status: 'error',
45
+ reason: 'forbidden',
46
+ details: 'permission-not-found',
47
+ });
48
+ return;
49
+ }
50
+
51
+ const { error } = (await disableOpenID(req.body)) || {};
52
+
53
+ if (error) {
54
+ res.status(401).send({ status: 'error', reason: error });
55
+ return;
56
+ }
57
+ res.send({ status: 'ok' });
58
+ });
59
+
60
+ app.post('/config', async (req, res) => {
61
+ const { cnt: ownerCount } = UserService.getOwnerCount() || {};
62
+
63
+ if (ownerCount > 0) {
64
+ res.status(400).send({ status: 'error', reason: 'already-bootstraped' });
65
+ return;
66
+ }
67
+
68
+ if (!checkPassword(req.body.password)) {
69
+ res.status(400).send({ status: 'error', reason: 'invalid-password' });
70
+ return;
71
+ }
72
+
73
+ const auth = UserService.getOpenIDConfig();
74
+
75
+ if (!auth) {
76
+ res
77
+ .status(500)
78
+ .send({ status: 'error', reason: 'OpenID configuration not found' });
79
+ return;
80
+ }
81
+
82
+ try {
83
+ const openIdConfig = JSON.parse(auth.extra_data);
84
+ res.send({ status: 'ok', data: { openId: openIdConfig } });
85
+ } catch (error) {
86
+ res
87
+ .status(500)
88
+ .send({ status: 'error', reason: 'Invalid OpenID configuration' });
89
+ }
90
+ });
91
+
92
+ app.get('/callback', async (req, res) => {
93
+ const { error, url } = await loginWithOpenIdFinalize(req.query);
94
+
95
+ if (error) {
96
+ res.status(400).send({ status: 'error', reason: error });
97
+ return;
98
+ }
99
+
100
+ if (!isValidRedirectUrl(url)) {
101
+ res.status(400).send({ status: 'error', reason: 'Invalid redirect URL' });
102
+ return;
103
+ }
104
+
105
+ res.redirect(url);
106
+ });
107
+
108
+ app.use(errorMiddleware);
@@ -0,0 +1,215 @@
1
+ import express from 'express';
2
+
3
+ import { handleError } from '../app-gocardless/util/handle-error.js';
4
+ import { SecretName, secretsService } from '../services/secrets-service.js';
5
+ import { requestLoggerMiddleware } from '../util/middlewares.js';
6
+
7
+ import { pluggyaiService } from './pluggyai-service.js';
8
+
9
+ const app = express();
10
+ export { app as handlers };
11
+ app.use(express.json());
12
+ app.use(requestLoggerMiddleware);
13
+
14
+ app.post(
15
+ '/status',
16
+ handleError(async (req, res) => {
17
+ const clientId = secretsService.get(SecretName.pluggyai_clientId);
18
+ const configured = clientId != null;
19
+
20
+ res.send({
21
+ status: 'ok',
22
+ data: {
23
+ configured,
24
+ },
25
+ });
26
+ }),
27
+ );
28
+
29
+ app.post(
30
+ '/accounts',
31
+ handleError(async (req, res) => {
32
+ try {
33
+ const itemIds = secretsService
34
+ .get(SecretName.pluggyai_itemIds)
35
+ .split(',')
36
+ .map(item => item.trim());
37
+
38
+ let accounts = [];
39
+
40
+ for (const item of itemIds) {
41
+ const partial = await pluggyaiService.getAccountsByItemId(item);
42
+ accounts = accounts.concat(partial.results);
43
+ }
44
+
45
+ res.send({
46
+ status: 'ok',
47
+ data: {
48
+ accounts,
49
+ },
50
+ });
51
+ } catch (error) {
52
+ res.send({
53
+ status: 'ok',
54
+ data: {
55
+ error: error.message,
56
+ },
57
+ });
58
+ }
59
+ }),
60
+ );
61
+
62
+ app.post(
63
+ '/transactions',
64
+ handleError(async (req, res) => {
65
+ const { accountId, startDate } = req.body;
66
+
67
+ try {
68
+ const transactions = await pluggyaiService.getTransactions(
69
+ accountId,
70
+ startDate,
71
+ );
72
+
73
+ const account = await pluggyaiService.getAccountById(accountId);
74
+
75
+ let startingBalance = parseInt(
76
+ Math.round(account.balance * 100).toString(),
77
+ );
78
+ if (account.type === 'CREDIT') {
79
+ startingBalance = -startingBalance;
80
+ }
81
+ const date = getDate(new Date(account.updatedAt));
82
+
83
+ const balances = [
84
+ {
85
+ balanceAmount: {
86
+ amount: startingBalance,
87
+ currency: account.currencyCode,
88
+ },
89
+ balanceType: 'expected',
90
+ referenceDate: date,
91
+ },
92
+ ];
93
+
94
+ const all = [];
95
+ const booked = [];
96
+ const pending = [];
97
+
98
+ for (const trans of transactions) {
99
+ const newTrans = {};
100
+
101
+ newTrans.booked = !(trans.status === 'PENDING');
102
+
103
+ const transactionDate = new Date(trans.date);
104
+
105
+ if (transactionDate < startDate && !trans.sandbox) {
106
+ continue;
107
+ }
108
+
109
+ newTrans.date = getDate(transactionDate);
110
+ newTrans.payeeName = getPayeeName(trans);
111
+ newTrans.notes = trans.descriptionRaw || trans.description;
112
+
113
+ if (account.type === 'CREDIT') {
114
+ if (trans.amountInAccountCurrency) {
115
+ trans.amountInAccountCurrency *= -1;
116
+ }
117
+
118
+ trans.amount *= -1;
119
+ }
120
+
121
+ let amountInCurrency = trans.amountInAccountCurrency ?? trans.amount;
122
+ amountInCurrency = Math.round(amountInCurrency * 100) / 100;
123
+
124
+ newTrans.transactionAmount = {
125
+ amount: amountInCurrency,
126
+ currency: trans.currencyCode,
127
+ };
128
+
129
+ newTrans.transactionId = trans.id;
130
+ newTrans.sortOrder = transactionDate.getTime();
131
+
132
+ delete trans.amount;
133
+
134
+ const finalTrans = { ...flattenObject(trans), ...newTrans };
135
+ if (newTrans.booked) {
136
+ booked.push(finalTrans);
137
+ } else {
138
+ pending.push(finalTrans);
139
+ }
140
+ all.push(finalTrans);
141
+ }
142
+
143
+ const sortFunction = (a, b) => b.sortOrder - a.sortOrder;
144
+
145
+ const bookedSorted = booked.sort(sortFunction);
146
+ const pendingSorted = pending.sort(sortFunction);
147
+ const allSorted = all.sort(sortFunction);
148
+
149
+ res.send({
150
+ status: 'ok',
151
+ data: {
152
+ balances,
153
+ startingBalance,
154
+ transactions: {
155
+ all: allSorted,
156
+ booked: bookedSorted,
157
+ pending: pendingSorted,
158
+ },
159
+ },
160
+ });
161
+ } catch (error) {
162
+ res.send({
163
+ status: 'ok',
164
+ data: {
165
+ error: error.message,
166
+ },
167
+ });
168
+ }
169
+ return;
170
+ }),
171
+ );
172
+
173
+ function getDate(date) {
174
+ return date.toISOString().split('T')[0];
175
+ }
176
+
177
+ function flattenObject(obj, prefix = '') {
178
+ const result = {};
179
+
180
+ for (const [key, value] of Object.entries(obj)) {
181
+ const newKey = prefix ? `${prefix}.${key}` : key;
182
+
183
+ if (value === null) {
184
+ continue;
185
+ }
186
+
187
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
188
+ Object.assign(result, flattenObject(value, newKey));
189
+ } else {
190
+ result[newKey] = value;
191
+ }
192
+ }
193
+
194
+ return result;
195
+ }
196
+
197
+ function getPayeeName(trans) {
198
+ if (trans.merchant && (trans.merchant.name || trans.merchant.businessName)) {
199
+ return trans.merchant.name || trans.merchant.businessName || '';
200
+ }
201
+
202
+ if (trans.paymentData) {
203
+ const { receiver, payer } = trans.paymentData;
204
+
205
+ if (trans.type === 'DEBIT' && receiver) {
206
+ return receiver.name || receiver.documentNumber?.value || '';
207
+ }
208
+
209
+ if (trans.type === 'CREDIT' && payer) {
210
+ return payer.name || payer.documentNumber?.value || '';
211
+ }
212
+ }
213
+
214
+ return '';
215
+ }
@@ -0,0 +1,120 @@
1
+ import { PluggyClient } from 'pluggy-sdk';
2
+
3
+ import { SecretName, secretsService } from '../services/secrets-service.js';
4
+
5
+ let pluggyClient = null;
6
+
7
+ function getPluggyClient() {
8
+ if (!pluggyClient) {
9
+ const clientId = secretsService.get(SecretName.pluggyai_clientId);
10
+ const clientSecret = secretsService.get(SecretName.pluggyai_clientSecret);
11
+
12
+ pluggyClient = new PluggyClient({
13
+ clientId,
14
+ clientSecret,
15
+ });
16
+ }
17
+
18
+ return pluggyClient;
19
+ }
20
+
21
+ export const pluggyaiService = {
22
+ isConfigured: () => {
23
+ return !!(
24
+ secretsService.get(SecretName.pluggyai_clientId) &&
25
+ secretsService.get(SecretName.pluggyai_clientSecret) &&
26
+ secretsService.get(SecretName.pluggyai_itemIds)
27
+ );
28
+ },
29
+
30
+ getAccountsByItemId: async itemId => {
31
+ try {
32
+ const client = getPluggyClient();
33
+ const { results, total, ...rest } = await client.fetchAccounts(itemId);
34
+ return {
35
+ results,
36
+ total,
37
+ ...rest,
38
+ hasError: false,
39
+ errors: {},
40
+ };
41
+ } catch (error) {
42
+ console.error(`Error fetching accounts: ${error.message}`);
43
+ throw error;
44
+ }
45
+ },
46
+ getAccountById: async accountId => {
47
+ try {
48
+ const client = getPluggyClient();
49
+ const account = await client.fetchAccount(accountId);
50
+ return {
51
+ ...account,
52
+ hasError: false,
53
+ errors: {},
54
+ };
55
+ } catch (error) {
56
+ console.error(`Error fetching account: ${error.message}`);
57
+ throw error;
58
+ }
59
+ },
60
+
61
+ getTransactionsByAccountId: async (accountId, startDate, pageSize, page) => {
62
+ try {
63
+ const client = getPluggyClient();
64
+
65
+ const account = await pluggyaiService.getAccountById(accountId);
66
+
67
+ // the sandbox data doesn't move the dates automatically so the
68
+ // transactions are often older than 90 days. The owner on one of the
69
+ // sandbox accounts is set to John Doe so in these cases we'll ignore
70
+ // the start date.
71
+ const sandboxAccount = account.owner === 'John Doe';
72
+
73
+ if (sandboxAccount) startDate = '2000-01-01';
74
+
75
+ const transactions = await client.fetchTransactions(accountId, {
76
+ from: startDate,
77
+ pageSize,
78
+ page,
79
+ });
80
+
81
+ if (sandboxAccount) {
82
+ transactions.results = transactions.results.map(t => ({
83
+ ...t,
84
+ sandbox: true,
85
+ }));
86
+ }
87
+
88
+ return {
89
+ ...transactions,
90
+ hasError: false,
91
+ errors: {},
92
+ };
93
+ } catch (error) {
94
+ console.error(`Error fetching transactions: ${error.message}`);
95
+ throw error;
96
+ }
97
+ },
98
+ getTransactions: async (accountId, startDate) => {
99
+ let transactions = [];
100
+ let result = await pluggyaiService.getTransactionsByAccountId(
101
+ accountId,
102
+ startDate,
103
+ 500,
104
+ 1,
105
+ );
106
+ transactions = transactions.concat(result.results);
107
+ const totalPages = result.totalPages;
108
+ while (result.page !== totalPages) {
109
+ result = await pluggyaiService.getTransactionsByAccountId(
110
+ accountId,
111
+ startDate,
112
+ 500,
113
+ result.page + 1,
114
+ );
115
+ transactions = transactions.concat(result.results);
116
+ }
117
+
118
+ return transactions;
119
+ },
120
+ };