Skip to content

Commit

Permalink
Add GoCardless bank integration for American Express AESUDEF1 (#239)
Browse files Browse the repository at this point in the history
  • Loading branch information
kyrias committed Aug 9, 2023
1 parent 9102d97 commit 9c9f664
Show file tree
Hide file tree
Showing 13 changed files with 129 additions and 17 deletions.
2 changes: 1 addition & 1 deletion src/app-gocardless/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ your bank.
Create new a bank class based on `app-gocardless/banks/sandboxfinance-sfin0000.js`. Name of the file and class should be
created based on the ID of the integrated institution.

Fill the logic of `normalizeAccount`, `sortTransactions`, and `calculateStartingBalance` functions.
Fill the logic of `normalizeAccount`, `normalizeTransaction`, `sortTransactions`, and `calculateStartingBalance` functions.
You should do it based on the data which you found in the logs.

Example logs which help you to fill:
Expand Down
11 changes: 9 additions & 2 deletions src/app-gocardless/bank-factory.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import AmericanExpressAesudef1 from './banks/american-express-aesudef1.js';
import IngPlIngbplpw from './banks/ing-pl-ingbplpw.js';
import IntegrationBank from './banks/integration-bank.js';
import MbankRetailBrexplpw from './banks/mbank-retail-brexplpw.js';
import SandboxfinanceSfin0000 from './banks/sandboxfinance-sfin0000.js';

const banks = [MbankRetailBrexplpw, SandboxfinanceSfin0000, IngPlIngbplpw];
const banks = [
AmericanExpressAesudef1,
IngPlIngbplpw,
MbankRetailBrexplpw,
SandboxfinanceSfin0000,
];

export default (institutionId) =>
banks.find((b) => b.institutionId === institutionId) || IntegrationBank;
banks.find((b) => b.institutionIds.includes(institutionId)) ||
IntegrationBank;
55 changes: 55 additions & 0 deletions src/app-gocardless/banks/american-express-aesudef1.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { amountToInteger, sortByBookingDate } from '../utils.js';

/** @type {import('./bank.interface.js').IBank} */
export default {
institutionIds: ['AMERICAN_EXPRESS_AESUDEF1'],

normalizeAccount(account) {
return {
account_id: account.id,
institution: account.institution,
// The `iban` field for these American Express cards is actually a masked
// version of the PAN. No IBAN is provided.
mask: account.iban.slice(-5),
iban: null,
name: [account.details, `(${account.iban.slice(-5)})`].join(' '),
official_name: account.details,
// The Actual account `type` field is legacy and is currently not used
// for anything, so we leave it as the default of `checking`.
type: 'checking',
};
},

normalizeTransaction(transaction, _booked) {
/**
* The American Express Europe integration sends the actual date of
* purchase as `bookingDate`, and `valueDate` appears to contain a date
* related to the actual booking date, though sometimes offset by a day
* compared to the American Express website.
*/
delete transaction.valueDate;
return transaction;
},

sortTransactions(transactions = []) {
return sortByBookingDate(transactions);
},

/**
* For SANDBOXFINANCE_SFIN0000 we don't know what balance was
* after each transaction so we have to calculate it by getting
* current balance from the account and subtract all the transactions
*
* As a current balance we use `interimBooked` balance type because
* it includes transaction placed during current day
*/
calculateStartingBalance(sortedTransactions = [], balances = []) {
const currentBalance = balances.find(
(balance) => 'information' === balance.balanceType.toString(),
);

return sortedTransactions.reduce((total, trans) => {
return total - amountToInteger(trans.transactionAmount.amount);
}, amountToInteger(currentBalance.balanceAmount.amount));
},
};
10 changes: 9 additions & 1 deletion src/app-gocardless/banks/bank.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,22 @@ import {
import { Transaction, Balance } from '../gocardless-node.types.js';

export interface IBank {
institutionId: string;
institutionIds: string[];
/**
* Returns normalized object with required data for the frontend
*/
normalizeAccount: (
account: DetailedAccountWithInstitution,
) => NormalizedAccountDetails;

/**
* Returns a normalized transaction object
*/
normalizeTransaction: (
transaction: Transaction,
booked: boolean,
) => Transaction | null;

/**
* Function sorts an array of transactions from newest to oldest
*/
Expand Down
6 changes: 5 additions & 1 deletion src/app-gocardless/banks/ing-pl-ingbplpw.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { printIban, amountToInteger } from '../utils.js';

/** @type {import('./bank.interface.js').IBank} */
export default {
institutionId: 'ING_PL_INGBPLPW',
institutionIds: ['ING_PL_INGBPLPW'],

normalizeAccount(account) {
return {
Expand All @@ -16,6 +16,10 @@ export default {
};
},

normalizeTransaction(transaction, _booked) {
return transaction;
},

sortTransactions(transactions = []) {
return transactions.sort((a, b) => {
return (
Expand Down
8 changes: 7 additions & 1 deletion src/app-gocardless/banks/integration-bank.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const SORTED_BALANCE_TYPE_LIST = [

/** @type {import('./bank.interface.js').IBank} */
export default {
institutionId: 'IntegrationBank',
institutionIds: ['IntegrationBank'],
normalizeAccount(account) {
console.log(
'Available account properties for new institution integration',
Expand All @@ -31,13 +31,19 @@ export default {
type: 'checking',
};
},

normalizeTransaction(transaction, _booked) {
return transaction;
},

sortTransactions(transactions = []) {
console.log(
'Available (first 10) transactions properties for new integration of institution in sortTransactions function',
{ top10Transactions: JSON.stringify(transactions.slice(0, 10)) },
);
return sortByBookingDate(transactions);
},

calculateStartingBalance(sortedTransactions = [], balances = []) {
console.log(
'Available (first 10) transactions properties for new integration of institution in calculateStartingBalance function',
Expand Down
6 changes: 5 additions & 1 deletion src/app-gocardless/banks/mbank-retail-brexplpw.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { printIban, amountToInteger } from '../utils.js';

/** @type {import('./bank.interface.js').IBank} */
export default {
institutionId: 'MBANK_RETAIL_BREXPLPW',
institutionIds: ['MBANK_RETAIL_BREXPLPW'],

normalizeAccount(account) {
return {
Expand All @@ -16,6 +16,10 @@ export default {
};
},

normalizeTransaction(transaction, _booked) {
return transaction;
},

sortTransactions(transactions = []) {
return transactions.sort(
(a, b) => Number(b.transactionId) - Number(a.transactionId),
Expand Down
6 changes: 5 additions & 1 deletion src/app-gocardless/banks/sandboxfinance-sfin0000.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { printIban, amountToInteger } from '../utils.js';

/** @type {import('./bank.interface.js').IBank} */
export default {
institutionId: 'SANDBOXFINANCE_SFIN0000',
institutionIds: ['SANDBOXFINANCE_SFIN0000'],

normalizeAccount(account) {
return {
Expand All @@ -16,6 +16,10 @@ export default {
};
},

normalizeTransaction(transaction, _booked) {
return transaction;
},

sortTransactions(transactions = []) {
return transactions.sort((a, b) => {
const [aTime, aSeq] = a.transactionId.split('-');
Expand Down
5 changes: 5 additions & 0 deletions src/app-gocardless/gocardless.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ export type NormalizedAccountDetails = {
};

export type GetTransactionsParams = {
/**
* Id of the institution from GoCardless
*/
institutionId: string;

/**
* Id of account from the GoCardless app
*/
Expand Down
13 changes: 12 additions & 1 deletion src/app-gocardless/services/gocardless-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ export const goCardlessService = {
const [accountMetadata, transactions, accountBalance] = await Promise.all([
goCardlessService.getAccountMetadata(accountId),
goCardlessService.getTransactions({
institutionId: institution_id,
accountId,
startDate,
endDate,
Expand Down Expand Up @@ -429,7 +430,7 @@ export const goCardlessService = {
* @throws {ServiceError}
* @returns {Promise<import('../gocardless.types.js').GetTransactionsResponse>}
*/
getTransactions: async ({ accountId, startDate, endDate }) => {
getTransactions: async ({ institutionId, accountId, startDate, endDate }) => {
const response = await client.getTransactions({
accountId,
dateFrom: startDate,
Expand All @@ -438,6 +439,16 @@ export const goCardlessService = {

handleGoCardlessError(response);

const bankAccount = BankFactory(institutionId);
response.transactions.booked = response.transactions.booked
.map((transaction) => bankAccount.normalizeTransaction(transaction, true))
.filter((transaction) => transaction);
response.transactions.pending = response.transactions.pending
.map((transaction) =>
bankAccount.normalizeTransaction(transaction, false),
)
.filter((transaction) => transaction);

return response;
},

Expand Down
2 changes: 2 additions & 0 deletions src/app-gocardless/services/tests/gocardless-service.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,7 @@ describe('goCardlessService', () => {

expect(
await goCardlessService.getTransactions({
institutionId: 'SANDBOXFINANCE_SFIN0000',
accountId,
startDate: '',
endDate: '',
Expand Down Expand Up @@ -530,6 +531,7 @@ describe('goCardlessService', () => {

await expect(() =>
goCardlessService.getTransactions({
institutionId: 'SANDBOXFINANCE_SFIN0000',
accountId,
startDate: '',
endDate: '',
Expand Down
16 changes: 8 additions & 8 deletions src/app-gocardless/tests/bank-factory.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,30 @@ import IntegrationBank from '../banks/integration-bank.js';

describe('BankFactory', () => {
it('should return MbankRetailBrexplpw when institutionId is mbank-retail-brexplpw', () => {
const institutionId = MbankRetailBrexplpw.institutionId;
const institutionId = MbankRetailBrexplpw.institutionIds[0];
const result = BankFactory(institutionId);

expect(result.institutionId).toBe(institutionId);
expect(result.institutionIds).toContain(institutionId);
});

it('should return SandboxfinanceSfin0000 when institutionId is sandboxfinance-sfin0000', () => {
const institutionId = SandboxfinanceSfin0000.institutionId;
const institutionId = SandboxfinanceSfin0000.institutionIds[0];
const result = BankFactory(institutionId);

expect(result.institutionId).toBe(institutionId);
expect(result.institutionIds).toContain(institutionId);
});

it('should return IngPlIngbplpw when institutionId is ing-pl-ingbplpw', () => {
const institutionId = IngPlIngbplpw.institutionId;
const institutionId = IngPlIngbplpw.institutionIds[0];
const result = BankFactory(institutionId);

expect(result.institutionId).toBe(institutionId);
expect(result.institutionIds).toContain(institutionId);
});

it('should return IntegrationBank when institutionId is not found', () => {
const institutionId = IntegrationBank.institutionId;
const institutionId = IntegrationBank.institutionIds[0];
const result = BankFactory(institutionId);

expect(result.institutionId).toBe(institutionId);
expect(result.institutionIds).toContain(institutionId);
});
});
6 changes: 6 additions & 0 deletions upcoming-release-notes/239.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
category: Enhancements
authors: [kyrias]
---

Add American Express AESUDEF1 GoCardless bank integration

0 comments on commit 9c9f664

Please sign in to comment.