@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,61 @@
1
+ import { amountToInteger } from '../utils.js';
2
+
3
+ import Fallback from './integration-bank.js';
4
+
5
+ /** @type {import('./bank.interface.js').IBank} */
6
+ export default {
7
+ ...Fallback,
8
+
9
+ institutionIds: ['BERLINER_SPARKASSE_BELADEBEXXX'],
10
+
11
+ normalizeTransaction(transaction, booked) {
12
+ const editedTrans = { ...transaction };
13
+
14
+ let remittanceInformationUnstructured;
15
+
16
+ if (transaction.remittanceInformationUnstructured) {
17
+ remittanceInformationUnstructured =
18
+ transaction.remittanceInformationUnstructured;
19
+ } else if (transaction.remittanceInformationStructured) {
20
+ remittanceInformationUnstructured =
21
+ transaction.remittanceInformationStructured;
22
+ } else if (transaction.remittanceInformationStructuredArray?.length > 0) {
23
+ remittanceInformationUnstructured =
24
+ transaction.remittanceInformationStructuredArray?.join(' ');
25
+ }
26
+
27
+ if (transaction.additionalInformation) {
28
+ remittanceInformationUnstructured +=
29
+ ' ' + transaction.additionalInformation;
30
+ }
31
+
32
+ const usefulCreditorName =
33
+ transaction.ultimateCreditor ||
34
+ transaction.creditorName ||
35
+ transaction.debtorName;
36
+
37
+ editedTrans.creditorName = usefulCreditorName;
38
+ editedTrans.remittanceInformationUnstructured =
39
+ remittanceInformationUnstructured;
40
+
41
+ return Fallback.normalizeTransaction(transaction, booked, editedTrans);
42
+ },
43
+
44
+ /**
45
+ * For SANDBOXFINANCE_SFIN0000 we don't know what balance was
46
+ * after each transaction so we have to calculate it by getting
47
+ * current balance from the account and subtract all the transactions
48
+ *
49
+ * As a current balance we use `interimBooked` balance type because
50
+ * it includes transaction placed during current day
51
+ */
52
+ calculateStartingBalance(sortedTransactions = [], balances = []) {
53
+ const currentBalance = balances.find(
54
+ balance => 'interimAvailable' === balance.balanceType,
55
+ );
56
+
57
+ return sortedTransactions.reduce((total, trans) => {
58
+ return total - amountToInteger(trans.transactionAmount.amount);
59
+ }, amountToInteger(currentBalance.balanceAmount.amount));
60
+ },
61
+ };
@@ -0,0 +1,73 @@
1
+ import Fallback from './integration-bank.js';
2
+
3
+ /** @type {import('./bank.interface.js').IBank} */
4
+ export default {
5
+ ...Fallback,
6
+
7
+ institutionIds: [
8
+ 'FINTRO_BE_GEBABEBB',
9
+ 'HELLO_BE_GEBABEBB',
10
+ 'BNP_BE_GEBABEBB',
11
+ ],
12
+
13
+ /** BNP_BE_GEBABEBB provides a lot of useful information via the 'additionalField'
14
+ * There does not seem to be a specification of this field, but the following information is contained in its subfields:
15
+ * - for pending transactions: the 'atmPosName'
16
+ * - for booked transactions: the 'narrative'.
17
+ * This narrative subfield is most useful as it contains information required to identify the transaction,
18
+ * especially in case of debit card or instant payment transactions.
19
+ * Do note that the narrative subfield ALSO contains the remittance information if any.
20
+ * The goal of the normalization is to place any relevant information of the additionalInformation
21
+ * field in the remittanceInformationUnstructuredArray field.
22
+ */
23
+ normalizeTransaction(transaction, booked) {
24
+ const editedTrans = { ...transaction };
25
+
26
+ // Extract the creditor name to fill it in with information from the
27
+ // additionalInformation field in case it's not yet defined.
28
+ let creditorName = transaction.creditorName;
29
+
30
+ if (transaction.additionalInformation) {
31
+ const additionalInformationObject = {};
32
+ const additionalInfoRegex = /(, )?([^:]+): ((\[.*?\])|([^,]*))/g;
33
+ const matches =
34
+ transaction.additionalInformation.matchAll(additionalInfoRegex);
35
+ if (matches) {
36
+ let creditorNameFromNarrative; // Possible value for creditorName
37
+ for (const match of matches) {
38
+ const key = match[2].trim();
39
+ let value = (match[4] || match[5]).trim();
40
+ if (key === 'narrative') {
41
+ // Set narrativeName to the first element in the "narrative" array.
42
+ const first_value = value.matchAll(/'(.+?)'/g)?.next().value;
43
+ creditorNameFromNarrative = first_value
44
+ ? first_value[1].trim()
45
+ : undefined;
46
+ }
47
+ // Remove square brackets and single quotes and commas
48
+ value = value.replace(/[[\]',]/g, '');
49
+ additionalInformationObject[key] = value;
50
+ }
51
+ // Keep existing unstructuredArray and add atmPosName and narrative
52
+ editedTrans.remittanceInformationUnstructuredArray = [
53
+ transaction.remittanceInformationUnstructuredArray ?? '',
54
+ additionalInformationObject?.atmPosName ?? '',
55
+ additionalInformationObject?.narrative ?? '',
56
+ ].filter(Boolean);
57
+
58
+ // If the creditor name doesn't exist in the original transactions,
59
+ // set it to the atmPosName or narrativeName if they exist; otherwise
60
+ // leave empty and let the default rules handle it.
61
+ creditorName =
62
+ creditorName ??
63
+ additionalInformationObject?.atmPosName ??
64
+ creditorNameFromNarrative ??
65
+ null;
66
+ }
67
+ }
68
+
69
+ editedTrans.creditorName = creditorName;
70
+
71
+ return Fallback.normalizeTransaction(transaction, booked, editedTrans);
72
+ },
73
+ };
@@ -0,0 +1,34 @@
1
+ import Fallback from './integration-bank.js';
2
+ import { extractPayeeNameFromRemittanceInfo } from './util/extract-payeeName-from-remittanceInfo.js';
3
+
4
+ /** @type {import('./bank.interface.js').IBank} */
5
+ export default {
6
+ ...Fallback,
7
+
8
+ institutionIds: ['CBC_CREGBEBB'],
9
+
10
+ /**
11
+ * For negative amounts, the only payee information we have is returned in
12
+ * remittanceInformationUnstructured.
13
+ */
14
+ normalizeTransaction(transaction, booked) {
15
+ const editedTrans = { ...transaction };
16
+
17
+ if (Number(transaction.transactionAmount.amount) > 0) {
18
+ editedTrans.payeeName =
19
+ transaction.debtorName ||
20
+ transaction.remittanceInformationUnstructured ||
21
+ 'undefined';
22
+ } else {
23
+ editedTrans.payeeName =
24
+ transaction.creditorName ||
25
+ extractPayeeNameFromRemittanceInfo(
26
+ transaction.remittanceInformationUnstructured,
27
+ ['Paiement', 'Domiciliation', 'Transfert', 'Ordre permanent'],
28
+ ) ||
29
+ 'undefined';
30
+ }
31
+
32
+ return Fallback.normalizeTransaction(transaction, booked, editedTrans);
33
+ },
34
+ };
@@ -0,0 +1,51 @@
1
+ import Fallback from './integration-bank.js';
2
+
3
+ /** @type {import('./bank.interface.js').IBank} */
4
+ export default {
5
+ ...Fallback,
6
+
7
+ institutionIds: ['COMMERZBANK_COBADEFF'],
8
+
9
+ normalizeTransaction(transaction, booked) {
10
+ const editedTrans = { ...transaction };
11
+
12
+ // remittanceInformationUnstructured is limited to 140 chars thus ...
13
+ // ... missing information form remittanceInformationUnstructuredArray ...
14
+ // ... so we recreate it.
15
+ editedTrans.remittanceInformationUnstructured =
16
+ transaction.remittanceInformationUnstructuredArray.join(' ');
17
+
18
+ // The limitations of remittanceInformationUnstructuredArray ...
19
+ // ... can result in split keywords. We fix these. Other ...
20
+ // ... splits will need to be fixed by user with rules.
21
+ const keywords = [
22
+ 'End-to-End-Ref.:',
23
+ 'Mandatsref:',
24
+ 'Gläubiger-ID:',
25
+ 'SEPA-BASISLASTSCHRIFT',
26
+ 'Kartenzahlung',
27
+ 'Dauerauftrag',
28
+ ];
29
+ keywords.forEach(keyword => {
30
+ editedTrans.remittanceInformationUnstructured =
31
+ editedTrans.remittanceInformationUnstructured.replace(
32
+ // There can be spaces in keywords
33
+ RegExp(keyword.split('').join('\\s*'), 'gi'),
34
+ ', ' + keyword + ' ',
35
+ );
36
+ });
37
+
38
+ // Clean up remittanceInformation, deduplicate payee (removing slashes ...
39
+ // ... that are added to the remittanceInformation field), and ...
40
+ // ... remove clutter like "End-to-End-Ref.: NOTPROVIDED"
41
+ const payee = transaction.creditorName || transaction.debtorName || '';
42
+ editedTrans.remittanceInformationUnstructured =
43
+ editedTrans.remittanceInformationUnstructured
44
+ .replace(/\s*(,)?\s+/g, '$1 ')
45
+ .replace(RegExp(payee.split(' ').join('(/*| )'), 'gi'), ' ')
46
+ .replace(', End-to-End-Ref.: NOTPROVIDED', '')
47
+ .trim();
48
+
49
+ return Fallback.normalizeTransaction(transaction, booked, editedTrans);
50
+ },
51
+ };
@@ -0,0 +1,39 @@
1
+ import { amountToInteger } from '../utils.js';
2
+
3
+ import Fallback from './integration-bank.js';
4
+
5
+ /** @type {import('./bank.interface.js').IBank} */
6
+ export default {
7
+ ...Fallback,
8
+
9
+ institutionIds: ['DANSKEBANK_DABANO22'],
10
+
11
+ normalizeTransaction(transaction, booked) {
12
+ const editedTrans = { ...transaction };
13
+
14
+ /**
15
+ * Danske Bank appends the EndToEndID: NOTPROVIDED to
16
+ * remittanceInformationUnstructured, cluttering the data.
17
+ *
18
+ * We clean thais up by removing any instances of this string from all transactions.
19
+ *
20
+ */
21
+ editedTrans.remittanceInformationUnstructured =
22
+ transaction.remittanceInformationUnstructured.replace(
23
+ '\nEndToEndID: NOTPROVIDED',
24
+ '',
25
+ );
26
+
27
+ return Fallback.normalizeTransaction(transaction, booked, editedTrans);
28
+ },
29
+
30
+ calculateStartingBalance(sortedTransactions = [], balances = []) {
31
+ const currentBalance = balances.find(
32
+ balance => balance.balanceType === 'interimAvailable',
33
+ );
34
+
35
+ return sortedTransactions.reduce((total, trans) => {
36
+ return total - amountToInteger(trans.transactionAmount.amount);
37
+ }, amountToInteger(currentBalance.balanceAmount.amount));
38
+ },
39
+ };
@@ -0,0 +1,18 @@
1
+ import Fallback from './integration-bank.js';
2
+
3
+ /** @type {import('./bank.interface.js').IBank} */
4
+ export default {
5
+ ...Fallback,
6
+
7
+ institutionIds: ['DIREKT_HELADEF1822'],
8
+
9
+ normalizeTransaction(transaction, booked) {
10
+ const editedTrans = { ...transaction };
11
+
12
+ editedTrans.remittanceInformationUnstructured =
13
+ transaction.remittanceInformationUnstructured ??
14
+ transaction.remittanceInformationStructured;
15
+
16
+ return Fallback.normalizeTransaction(transaction, booked, editedTrans);
17
+ },
18
+ };
@@ -0,0 +1,50 @@
1
+ import d from 'date-fns';
2
+
3
+ import { formatPayeeName } from '../../util/payee-name.js';
4
+ import { title } from '../../util/title/index.js';
5
+
6
+ import Fallback from './integration-bank.js';
7
+
8
+ /** @type {import('./bank.interface.js').IBank} */
9
+ export default {
10
+ ...Fallback,
11
+
12
+ institutionIds: ['EASYBANK_BAWAATWW'],
13
+
14
+ // If date is same, sort by transactionId
15
+ sortTransactions: (transactions = []) =>
16
+ transactions.sort((a, b) => {
17
+ const diff =
18
+ +new Date(b.valueDate || b.bookingDate) -
19
+ +new Date(a.valueDate || a.bookingDate);
20
+ if (diff !== 0) return diff;
21
+ return parseInt(b.transactionId) - parseInt(a.transactionId);
22
+ }),
23
+
24
+ normalizeTransaction(transaction, booked) {
25
+ const editedTrans = { ...transaction };
26
+
27
+ let payeeName = formatPayeeName(transaction);
28
+ if (!payeeName) payeeName = extractPayeeName(transaction);
29
+ editedTrans.payeeName = payeeName;
30
+
31
+ return Fallback.normalizeTransaction(transaction, booked, editedTrans);
32
+ },
33
+ };
34
+
35
+ /**
36
+ * Extracts the payee name from the remittanceInformationStructured
37
+ * @param {import('../gocardless-node.types.js').Transaction} transaction
38
+ */
39
+ function extractPayeeName(transaction) {
40
+ const structured = transaction.remittanceInformationStructured;
41
+ // The payee name is betweeen the transaction timestamp (11.07. 11:36) and the location, that starts with \\
42
+ const regex = /\d{2}\.\d{2}\. \d{2}:\d{2}(.*)\\\\/;
43
+ const matches = structured.match(regex);
44
+ if (matches && matches.length > 1 && matches[1]) {
45
+ return title(matches[1]);
46
+ } else {
47
+ // As a fallback if still no payee is found, the whole information is used
48
+ return structured;
49
+ }
50
+ }
@@ -0,0 +1,40 @@
1
+ import { amountToInteger } from '../utils.js';
2
+
3
+ import Fallback from './integration-bank.js';
4
+
5
+ /** @type {import('./bank.interface.js').IBank} */
6
+ export default {
7
+ ...Fallback,
8
+
9
+ institutionIds: ['ENTERCARD_SWEDNOKK'],
10
+
11
+ normalizeTransaction(transaction, booked) {
12
+ const editedTrans = { ...transaction };
13
+
14
+ // GoCardless's Entercard integration returns forex transactions with the
15
+ // foreign amount in `transactionAmount`, but at least the amount actually
16
+ // billed to the account is now available in
17
+ // `remittanceInformationUnstructured`.
18
+ const remittanceInformationUnstructured =
19
+ transaction.remittanceInformationUnstructured;
20
+ if (remittanceInformationUnstructured.startsWith('billingAmount: ')) {
21
+ transaction.transactionAmount = {
22
+ amount: remittanceInformationUnstructured.substring(15),
23
+ currency: 'SEK',
24
+ };
25
+ }
26
+
27
+ editedTrans.date = transaction.valueDate;
28
+
29
+ return Fallback.normalizeTransaction(transaction, booked, editedTrans);
30
+ },
31
+
32
+ calculateStartingBalance(sortedTransactions = [], balances = []) {
33
+ return sortedTransactions.reduce(
34
+ (total, trans) => {
35
+ return total - amountToInteger(trans.transactionAmount.amount);
36
+ },
37
+ amountToInteger(balances[0]?.balanceAmount?.amount || 0),
38
+ );
39
+ },
40
+ };
@@ -0,0 +1,46 @@
1
+ import { formatPayeeName } from '../../util/payee-name.js';
2
+
3
+ import Fallback from './integration-bank.js';
4
+
5
+ /** @type {import('./bank.interface.js').IBank} */
6
+ export default {
7
+ ...Fallback,
8
+
9
+ institutionIds: ['FORTUNEO_FTNOFRP1XXX'],
10
+
11
+ normalizeTransaction(transaction, booked) {
12
+ const editedTrans = { ...transaction };
13
+
14
+ // Most of the information from the transaction is in the remittanceInformationUnstructuredArray field.
15
+ // We extract the creditor and debtor names from this field.
16
+ // The remittanceInformationUnstructuredArray field usually contain keywords like "Vir" for
17
+ // bank transfers or "Carte 03/06" for card payments, as well as the date.
18
+ // We remove these keywords to get a cleaner payee name.
19
+ const keywordsToRemove = [
20
+ 'VIR INST',
21
+ 'VIR',
22
+ 'PRLV',
23
+ 'ANN CARTE',
24
+ 'CARTE \\d{2}\\/\\d{2}',
25
+ ];
26
+
27
+ const details =
28
+ transaction.remittanceInformationUnstructuredArray.join(' ');
29
+ const amount = transaction.transactionAmount.amount;
30
+
31
+ const regex = new RegExp(keywordsToRemove.join('|'), 'g');
32
+ const payeeName = details.replace(regex, '').trim();
33
+
34
+ // The amount is negative for outgoing transactions, positive for incoming transactions.
35
+ const isCreditorPayee = parseFloat(amount) < 0;
36
+
37
+ // The payee name is the creditor name for outgoing transactions and the debtor name for incoming transactions.
38
+ const creditorName = isCreditorPayee ? payeeName : null;
39
+ const debtorName = isCreditorPayee ? null : payeeName;
40
+
41
+ editedTrans.creditorName = creditorName;
42
+ editedTrans.debtorName = debtorName;
43
+
44
+ return Fallback.normalizeTransaction(transaction, booked, editedTrans);
45
+ },
46
+ };
@@ -0,0 +1,74 @@
1
+ import Fallback from './integration-bank.js';
2
+
3
+ /** @type {import('./bank.interface.js').IBank} */
4
+ export default {
5
+ ...Fallback,
6
+
7
+ institutionIds: ['HYPE_HYEEIT22'],
8
+
9
+ normalizeTransaction(transaction, booked) {
10
+ const editedTrans = { ...transaction };
11
+
12
+ /** Online card payments - identified by "crd" transaction code
13
+ * always start with PAGAMENTO PRESSO + <payee name>
14
+ */
15
+ if (transaction.proprietaryBankTransactionCode === 'crd') {
16
+ // remove PAGAMENTO PRESSO and set payee name
17
+ editedTrans.debtorName =
18
+ transaction.remittanceInformationUnstructured?.slice(
19
+ 'PAGAMENTO PRESSO '.length,
20
+ );
21
+ }
22
+ /**
23
+ * In-app money transfers (p2p) and bank transfers (bon) have remittance info structure like
24
+ * DENARO (INVIATO/RICEVUTO) (A/DA) {payee_name} - {payment_info} (p2p)
25
+ * HAI (INVIATO/RICEVUTO) UN BONIFICO (A/DA) {payee_name} - {payment_info} (bon)
26
+ */
27
+ if (
28
+ transaction.proprietaryBankTransactionCode === 'p2p' ||
29
+ transaction.proprietaryBankTransactionCode === 'bon'
30
+ ) {
31
+ // keep only {payment_info} portion of remittance info
32
+ // NOTE: if {payee_name} contains dashes (unlikely / impossible?), this probably gets bugged!
33
+ const infoIdx =
34
+ transaction.remittanceInformationUnstructured.indexOf(' - ') + 3;
35
+ editedTrans.remittanceInformationUnstructured =
36
+ infoIdx === -1
37
+ ? transaction.remittanceInformationUnstructured
38
+ : transaction.remittanceInformationUnstructured.slice(infoIdx).trim();
39
+ }
40
+ /**
41
+ * CONVERT ESCAPED UNICODE TO CODEPOINTS
42
+ * p2p payments allow user to write arbitrary unicode strings as messages
43
+ * gocardless reports unicode codepoints as \Uxxxx
44
+ * so it groups them in 4bytes bundles
45
+ * the code below assumes this is always the case
46
+ */
47
+ if (transaction.proprietaryBankTransactionCode === 'p2p') {
48
+ let str = transaction.remittanceInformationUnstructured;
49
+ let idx = str.indexOf('\\U');
50
+ let start_idx = idx;
51
+ let codepoints = [];
52
+ while (idx !== -1) {
53
+ codepoints.push(parseInt(str.slice(idx + 2, idx + 6), 16));
54
+ const next_idx = str.indexOf('\\U', idx + 6);
55
+ if (next_idx === idx + 6) {
56
+ idx = next_idx;
57
+ continue;
58
+ }
59
+ str =
60
+ str.slice(0, start_idx) +
61
+ String.fromCodePoint(...codepoints) +
62
+ str.slice(idx + 6);
63
+ codepoints = [];
64
+ idx = str.indexOf('\\U'); // slight inefficiency?
65
+ start_idx = idx;
66
+ }
67
+ editedTrans.remittanceInformationUnstructured = str;
68
+ }
69
+
70
+ editedTrans.date = transaction.valueDate || transaction.bookingDate;
71
+
72
+ return Fallback.normalizeTransaction(transaction, booked, editedTrans);
73
+ },
74
+ };
@@ -0,0 +1,70 @@
1
+ import Fallback from './integration-bank.js';
2
+
3
+ /** @type {import('./bank.interface.js').IBank} */
4
+ export default {
5
+ ...Fallback,
6
+
7
+ institutionIds: ['ING_INGBROBU'],
8
+
9
+ normalizeTransaction(transaction, booked) {
10
+ const editedTrans = { ...transaction };
11
+
12
+ //Merchant transactions all have the same transactionId of 'NOTPROVIDED'.
13
+ //For booked transactions, this can be set to the internalTransactionId
14
+ //For pending transactions, this needs to be removed for them to show up in Actual
15
+
16
+ //For deduplication to work better, payeeName needs to be standardized
17
+ //and converted from a pending transaction form ("payeeName":"Card no: xxxxxxxxxxxx1111"') to a booked transaction form ("payeeName":"Card no: Xxxx Xxxx Xxxx 1111")
18
+ if (transaction.transactionId === 'NOTPROVIDED') {
19
+ //Some corner case transactions only have the `proprietaryBankTransactionCode` field, this need to be copied to `remittanceInformationUnstructured`
20
+ if (
21
+ transaction.proprietaryBankTransactionCode &&
22
+ !transaction.remittanceInformationUnstructured
23
+ ) {
24
+ editedTrans.remittanceInformationUnstructured =
25
+ transaction.proprietaryBankTransactionCode;
26
+ }
27
+
28
+ if (booked) {
29
+ transaction.transactionId = transaction.internalTransactionId;
30
+ if (
31
+ transaction.remittanceInformationUnstructured &&
32
+ transaction.remittanceInformationUnstructured
33
+ .toLowerCase()
34
+ .includes('card no:')
35
+ ) {
36
+ editedTrans.creditorName =
37
+ transaction.remittanceInformationUnstructured.split(',')[0];
38
+ //Catch all case for other types of payees
39
+ } else {
40
+ editedTrans.creditorName =
41
+ transaction.remittanceInformationUnstructured;
42
+ }
43
+ } else {
44
+ transaction.transactionId = null;
45
+
46
+ if (
47
+ transaction.remittanceInformationUnstructured &&
48
+ transaction.remittanceInformationUnstructured
49
+ .toLowerCase()
50
+ .includes('card no:')
51
+ ) {
52
+ editedTrans.creditorName =
53
+ transaction.remittanceInformationUnstructured.replace(
54
+ /x{4}/g,
55
+ 'Xxxx ',
56
+ );
57
+ //Catch all case for other types of payees
58
+ } else {
59
+ editedTrans.creditorName =
60
+ transaction.remittanceInformationUnstructured;
61
+ }
62
+ //Remove remittanceInformationUnstructured from pending transactions, so the `notes` field remains empty (there is no merchant information)
63
+ //Once booked, the right `notes` (containing the merchant) will be populated
64
+ editedTrans.remittanceInformationUnstructured = null;
65
+ }
66
+ }
67
+
68
+ return Fallback.normalizeTransaction(transaction, booked, editedTrans);
69
+ },
70
+ };
@@ -0,0 +1,47 @@
1
+ import { amountToInteger } from '../utils.js';
2
+
3
+ import Fallback from './integration-bank.js';
4
+
5
+ /** @type {import('./bank.interface.js').IBank} */
6
+ export default {
7
+ ...Fallback,
8
+
9
+ institutionIds: ['ING_INGDDEFF'],
10
+
11
+ normalizeTransaction(transaction, booked) {
12
+ const editedTrans = { ...transaction };
13
+
14
+ const remittanceInformationMatch = /remittanceinformation:(.*)$/.exec(
15
+ transaction.remittanceInformationUnstructured,
16
+ );
17
+
18
+ editedTrans.remittanceInformationUnstructured = remittanceInformationMatch
19
+ ? remittanceInformationMatch[1]
20
+ : transaction.remittanceInformationUnstructured;
21
+
22
+ return Fallback.normalizeTransaction(transaction, booked, editedTrans);
23
+ },
24
+
25
+ sortTransactions(transactions = []) {
26
+ return transactions.sort((a, b) => {
27
+ const diff =
28
+ +new Date(b.valueDate || b.bookingDate) -
29
+ +new Date(a.valueDate || a.bookingDate);
30
+ if (diff) return diff;
31
+ const idA = parseInt(a.transactionId);
32
+ const idB = parseInt(b.transactionId);
33
+ if (!isNaN(idA) && !isNaN(idB)) return idB - idA;
34
+ return 0;
35
+ });
36
+ },
37
+
38
+ calculateStartingBalance(sortedTransactions = [], balances = []) {
39
+ const currentBalance = balances.find(
40
+ balance => 'interimBooked' === balance.balanceType,
41
+ );
42
+
43
+ return sortedTransactions.reduce((total, trans) => {
44
+ return total - amountToInteger(trans.transactionAmount.amount);
45
+ }, amountToInteger(currentBalance.balanceAmount.amount));
46
+ },
47
+ };
@@ -0,0 +1,46 @@
1
+ import { amountToInteger } from '../utils.js';
2
+
3
+ import Fallback from './integration-bank.js';
4
+
5
+ /** @type {import('./bank.interface.js').IBank} */
6
+ export default {
7
+ ...Fallback,
8
+
9
+ institutionIds: ['ING_PL_INGBPLPW'],
10
+
11
+ normalizeTransaction(transaction, booked) {
12
+ const editedTrans = { ...transaction };
13
+
14
+ editedTrans.date = transaction.valueDate;
15
+
16
+ return Fallback.normalizeTransaction(transaction, booked, editedTrans);
17
+ },
18
+
19
+ sortTransactions(transactions = []) {
20
+ return transactions.sort((a, b) => {
21
+ return (
22
+ Number(b.transactionId.substr(2)) - Number(a.transactionId.substr(2))
23
+ );
24
+ });
25
+ },
26
+
27
+ calculateStartingBalance(sortedTransactions = [], balances = []) {
28
+ if (sortedTransactions.length) {
29
+ const oldestTransaction =
30
+ sortedTransactions[sortedTransactions.length - 1];
31
+ const oldestKnownBalance = amountToInteger(
32
+ oldestTransaction.balanceAfterTransaction.balanceAmount.amount,
33
+ );
34
+ const oldestTransactionAmount = amountToInteger(
35
+ oldestTransaction.transactionAmount.amount,
36
+ );
37
+
38
+ return oldestKnownBalance - oldestTransactionAmount;
39
+ } else {
40
+ return amountToInteger(
41
+ balances.find(balance => 'interimBooked' === balance.balanceType)
42
+ .balanceAmount.amount,
43
+ );
44
+ }
45
+ },
46
+ };