Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add GoCardless bank integration for American Express AESUDEF1 #239

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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