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