Skip to content
This repository was archived by the owner on Feb 10, 2025. It is now read-only.

Move notes normalisation to the server and expose new bank sync fields #558

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 34 additions & 3 deletions src/app-gocardless/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ If the default bank integration does not work for you, you can integrate a new b

This will trigger the process of fetching the data from the bank and will log the data in the backend. Use this data to fill the logic of the bank class.

4. Create new a bank class based on `app-gocardless/banks/sandboxfinance-sfin0000.js`.
4. Create new a bank class based on an existing example in `app-gocardless/banks`.

Name of the file and class should be created based on the ID of the integrated institution, found in step 1.
Name of the file and class should follow the existing patterns and be created based on the ID of the integrated institution, found in step 1.

5. Fill the logic of `normalizeAccount`, `normalizeTransaction`, `sortTransactions`, and `calculateStartingBalance` functions.
You do not need to fill every function, only those which are necessary for the integration to work.
Expand Down Expand Up @@ -69,7 +69,7 @@ If the default bank integration does not work for you, you can integrate a new b
- `sortTransactions` function:

```log
Available (first 10) transactions properties for new integration of institution in sortTransactions function
Available (first 10) transactions properties for new integration of institution in sortTransactions function
{
top10SortedTransactions: '[
{
Expand Down Expand Up @@ -162,3 +162,34 @@ If the default bank integration does not work for you, you can integrate a new b
6. Add new bank integration to `BankFactory` class in file `actual-server/app-gocardless/bank-factory.js`

7. Remember to add tests for new bank integration in

## normalizeTransaction
This is the most commonly used override as it allows you to change the data that is returned to the client.

Please follow the following patterns when implementing a custom normalizeTransaction method:
1. If you need to edit the values of transaction fields (excluding the transaction amount) do not mutate the original transaction object. Instead, create a shallow copy and make your changes there.
2. End the function by returning the result of calling the fallback normalizeTransaction method from integration-bank.js

E.g.
```js
import Fallback from './integration-bank.js';

export default {
...

normalizeTransaction(transaction, booked) {
// create a shallow copy of the transaction object
const editedTrans = { ...transaction };

// make any changes required to the copy
editedTrans.remittanceInformationUnstructured = transaction.remittanceInformationStructured;

// call the fallback method, passing in your edited transaction as the 3rd parameter
// this will calculate the date, payee name and notes fields based on your changes
// but leave the original fields available for mapping in the UI
return Fallback.normalizeTransaction(transaction, booked, editedTrans);
}

...
}
```
16 changes: 6 additions & 10 deletions src/app-gocardless/banks/abanca_caglesmm.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import Fallback from './integration-bank.js';

import { formatPayeeName } from '../../util/payee-name.js';

/** @type {import('./bank.interface.js').IBank} */
export default {
...Fallback,
Expand All @@ -13,14 +11,12 @@ export default {
],

// Abanca transactions doesn't get the creditorName/debtorName properly
normalizeTransaction(transaction, _booked) {
transaction.creditorName = transaction.remittanceInformationStructured;
transaction.debtorName = transaction.remittanceInformationStructured;
normalizeTransaction(transaction, booked) {
const editedTrans = { ...transaction };

editedTrans.creditorName = transaction.remittanceInformationStructured;
editedTrans.debtorName = transaction.remittanceInformationStructured;

return {
...transaction,
payeeName: formatPayeeName(transaction),
date: transaction.bookingDate || transaction.valueDate,
};
return Fallback.normalizeTransaction(transaction, booked, editedTrans);
},
};
21 changes: 10 additions & 11 deletions src/app-gocardless/banks/abnamro_abnanl2a.js
Original file line number Diff line number Diff line change
@@ -1,32 +1,31 @@
import Fallback from './integration-bank.js';

import { amountToInteger } from '../utils.js';
import { formatPayeeName } from '../../util/payee-name.js';

/** @type {import('./bank.interface.js').IBank} */
export default {
...Fallback,

institutionIds: ['ABNAMRO_ABNANL2A'],

normalizeTransaction(transaction, _booked) {
normalizeTransaction(transaction, booked) {
const editedTrans = { ...transaction };

// There is no remittanceInformationUnstructured, so we'll make it
transaction.remittanceInformationUnstructured =
editedTrans.remittanceInformationUnstructured =
transaction.remittanceInformationUnstructuredArray.join(', ');

// Remove clutter to extract the payee from remittanceInformationUnstructured ...
// ... when not otherwise provided.
const payeeName = transaction.remittanceInformationUnstructuredArray
.map((el) => el.match(/^(?:.*\*)?(.+),PAS\d+$/))
.find((match) => match)?.[1];
transaction.debtorName = transaction.debtorName || payeeName;
transaction.creditorName = transaction.creditorName || payeeName;

return {
...transaction,
payeeName: formatPayeeName(transaction),
date: transaction.valueDateTime.slice(0, 10),
};
editedTrans.debtorName = transaction.debtorName || payeeName;
editedTrans.creditorName = transaction.creditorName || payeeName;

editedTrans.date = transaction.valueDateTime.slice(0, 10);

return Fallback.normalizeTransaction(transaction, booked, editedTrans);
},

sortTransactions(transactions = []) {
Expand Down
9 changes: 0 additions & 9 deletions src/app-gocardless/banks/american_express_aesudef1.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import Fallback from './integration-bank.js';

import { amountToInteger } from '../utils.js';
import { formatPayeeName } from '../../util/payee-name.js';

/** @type {import('./bank.interface.js').IBank} */
export default {
Expand All @@ -21,14 +20,6 @@ export default {
};
},

normalizeTransaction(transaction, _booked) {
return {
...transaction,
payeeName: formatPayeeName(transaction),
date: transaction.bookingDate,
};
},

/**
* For AMERICAN_EXPRESS_AESUDEF1 we don't know what balance was
* after each transaction so we have to calculate it by getting
Expand Down
16 changes: 6 additions & 10 deletions src/app-gocardless/banks/bancsabadell_bsabesbbb.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import Fallback from './integration-bank.js';

import { formatPayeeName } from '../../util/payee-name.js';

/** @type {import('./bank.interface.js').IBank} */
export default {
...Fallback,

institutionIds: ['BANCSABADELL_BSABESBB'],

// Sabadell transactions don't get the creditorName/debtorName properly
normalizeTransaction(transaction, _booked) {
normalizeTransaction(transaction, booked) {
const editedTrans = { ...transaction };

const amount = transaction.transactionAmount.amount;

// The amount is negative for outgoing transactions, positive for incoming transactions.
Expand All @@ -23,13 +23,9 @@ export default {
const creditorName = isCreditorPayee ? payeeName : null;
const debtorName = isCreditorPayee ? null : payeeName;

transaction.creditorName = creditorName;
transaction.debtorName = debtorName;
editedTrans.creditorName = creditorName;
editedTrans.debtorName = debtorName;

return {
...transaction,
payeeName: formatPayeeName(transaction),
date: transaction.bookingDate || transaction.valueDate,
};
return Fallback.normalizeTransaction(transaction, booked, editedTrans);
},
};
13 changes: 11 additions & 2 deletions src/app-gocardless/banks/bank.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ import {
} from '../gocardless.types.js';
import { Transaction, Balance } from '../gocardless-node.types.js';

type TransactionExtended = Transaction & {
date?: string;
payeeName?: string;
notes?: string;
remittanceInformationUnstructuredArrayString?: string;
remittanceInformationStructuredArrayString?: string;
};

export interface IBank {
institutionIds: string[];

Expand All @@ -23,9 +31,10 @@ export interface IBank {
* transaction date.
*/
normalizeTransaction: (
transaction: Transaction,
transaction: TransactionExtended,
booked: boolean,
) => (Transaction & { date?: string; payeeName: string }) | null;
editedTransaction?: TransactionExtended,
) => TransactionExtended | null;

/**
* Function sorts an array of transactions from newest to oldest
Expand Down
6 changes: 4 additions & 2 deletions src/app-gocardless/banks/bank_of_ireland_b365_bofiie2d.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ export default {
institutionIds: ['BANK_OF_IRELAND_B365_BOFIIE2D'],

normalizeTransaction(transaction, booked) {
transaction.remittanceInformationUnstructured = fixupPayee(
const editedTrans = { ...transaction };

editedTrans.remittanceInformationUnstructured = fixupPayee(
transaction.remittanceInformationUnstructured,
);

return Fallback.normalizeTransaction(transaction, booked);
return Fallback.normalizeTransaction(transaction, booked, editedTrans);
},
};

Expand Down
20 changes: 8 additions & 12 deletions src/app-gocardless/banks/bankinter_bkbkesmm.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,24 @@
import Fallback from './integration-bank.js';

import { formatPayeeName } from '../../util/payee-name.js';

/** @type {import('./bank.interface.js').IBank} */
export default {
...Fallback,

institutionIds: ['BANKINTER_BKBKESMM'],

normalizeTransaction(transaction, _booked) {
transaction.remittanceInformationUnstructured =
normalizeTransaction(transaction, booked) {
const editedTrans = { ...transaction };

editedTrans.remittanceInformationUnstructured =
transaction.remittanceInformationUnstructured
.replaceAll(/\/Txt\/(\w\|)?/gi, '')
.replaceAll(';', ' ');

transaction.debtorName = transaction.debtorName?.replaceAll(';', ' ');
transaction.creditorName =
editedTrans.debtorName = transaction.debtorName?.replaceAll(';', ' ');
editedTrans.creditorName =
transaction.creditorName?.replaceAll(';', ' ') ??
transaction.remittanceInformationUnstructured;
editedTrans.remittanceInformationUnstructured;

return {
...transaction,
payeeName: formatPayeeName(transaction),
date: transaction.bookingDate || transaction.valueDate,
};
return Fallback.normalizeTransaction(transaction, booked, editedTrans);
},
};
13 changes: 4 additions & 9 deletions src/app-gocardless/banks/belfius_gkccbebb.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import Fallback from './integration-bank.js';

import { formatPayeeName } from '../../util/payee-name.js';

/** @type {import('./bank.interface.js').IBank} */
export default {
...Fallback,
Expand All @@ -11,12 +9,9 @@ export default {
// The problem is that we have transaction with duplicated transaction ids.
// This is not expected and the nordigen api has a work-around for some backs
// They will set an internalTransactionId which is unique
normalizeTransaction(transaction, _booked) {
return {
...transaction,
transactionId: transaction.internalTransactionId,
payeeName: formatPayeeName(transaction),
date: transaction.bookingDate || transaction.valueDate,
};
normalizeTransaction(transaction, booked) {
transaction.transactionId = transaction.internalTransactionId;

return Fallback.normalizeTransaction(transaction, booked);
},
};
34 changes: 5 additions & 29 deletions src/app-gocardless/banks/berliner_sparkasse_beladebexxx.js
Original file line number Diff line number Diff line change
@@ -1,35 +1,15 @@
import Fallback from './integration-bank.js';

import { amountToInteger } from '../utils.js';
import { formatPayeeName } from '../../util/payee-name.js';

/** @type {import('./bank.interface.js').IBank} */
export default {
...Fallback,

institutionIds: ['BERLINER_SPARKASSE_BELADEBEXXX'],

/**
* Following the GoCardless documentation[0] we should prefer `bookingDate`
* here, though some of their bank integrations uses the date field
* differently from what's described in their documentation and so it's
* sometimes necessary to use `valueDate` instead.
*
* [0]: https://nordigen.zendesk.com/hc/en-gb/articles/7899367372829-valueDate-and-bookingDate-for-transactions
*/
normalizeTransaction(transaction, _booked) {
const date =
transaction.bookingDate ||
transaction.bookingDateTime ||
transaction.valueDate ||
transaction.valueDateTime;

// If we couldn't find a valid date field we filter out this transaction
// and hope that we will import it again once the bank has processed the
// transaction further.
if (!date) {
return null;
}
normalizeTransaction(transaction, booked) {
const editedTrans = { ...transaction };

let remittanceInformationUnstructured;

Expand All @@ -53,15 +33,11 @@ export default {
transaction.creditorName ||
transaction.debtorName;

transaction.creditorName = usefulCreditorName;
transaction.remittanceInformationUnstructured =
editedTrans.creditorName = usefulCreditorName;
editedTrans.remittanceInformationUnstructured =
remittanceInformationUnstructured;

return {
...transaction,
payeeName: formatPayeeName(transaction),
date: transaction.bookingDate || transaction.valueDate,
};
return Fallback.normalizeTransaction(transaction, booked, editedTrans);
},

/**
Expand Down
16 changes: 6 additions & 10 deletions src/app-gocardless/banks/bnp_be_gebabebb.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import Fallback from './integration-bank.js';

import { formatPayeeName } from '../../util/payee-name.js';

/** @type {import('./bank.interface.js').IBank} */
export default {
...Fallback,
Expand All @@ -22,7 +20,9 @@ export default {
* The goal of the normalization is to place any relevant information of the additionalInformation
* field in the remittanceInformationUnstructuredArray field.
*/
normalizeTransaction(transaction, _booked) {
normalizeTransaction(transaction, booked) {
const editedTrans = { ...transaction };

// Extract the creditor name to fill it in with information from the
// additionalInformation field in case it's not yet defined.
let creditorName = transaction.creditorName;
Expand All @@ -49,7 +49,7 @@ export default {
additionalInformationObject[key] = value;
}
// Keep existing unstructuredArray and add atmPosName and narrative
transaction.remittanceInformationUnstructuredArray = [
editedTrans.remittanceInformationUnstructuredArray = [
transaction.remittanceInformationUnstructuredArray ?? '',
additionalInformationObject?.atmPosName ?? '',
additionalInformationObject?.narrative ?? '',
Expand All @@ -66,12 +66,8 @@ export default {
}
}

transaction.creditorName = creditorName;
editedTrans.creditorName = creditorName;

return {
...transaction,
payeeName: formatPayeeName(transaction),
date: transaction.valueDate || transaction.bookingDate,
};
return Fallback.normalizeTransaction(transaction, booked, editedTrans);
},
};
Loading