Add GoCardless bank integration for American Express AESUDEF1 (#239)

This commit is contained in:
Johannes Löthberg
2023-08-09 09:27:47 +02:00
committed by GitHub
parent 9102d974a5
commit 9c9f6645d3
13 changed files with 129 additions and 17 deletions

View File

@@ -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:

View File

@@ -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;

View File

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

View File

@@ -5,7 +5,7 @@ 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
*/
@@ -13,6 +13,14 @@ export interface IBank {
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
*/

View File

@@ -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 {
@@ -16,6 +16,10 @@ export default {
};
},
normalizeTransaction(transaction, _booked) {
return transaction;
},
sortTransactions(transactions = []) {
return transactions.sort((a, b) => {
return (

View File

@@ -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',
@@ -31,6 +31,11 @@ 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',
@@ -38,6 +43,7 @@ export default {
);
return sortByBookingDate(transactions);
},
calculateStartingBalance(sortedTransactions = [], balances = []) {
console.log(
'Available (first 10) transactions properties for new integration of institution in calculateStartingBalance function',

View File

@@ -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 {
@@ -16,6 +16,10 @@ export default {
};
},
normalizeTransaction(transaction, _booked) {
return transaction;
},
sortTransactions(transactions = []) {
return transactions.sort(
(a, b) => Number(b.transactionId) - Number(a.transactionId),

View File

@@ -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 {
@@ -16,6 +16,10 @@ export default {
};
},
normalizeTransaction(transaction, _booked) {
return transaction;
},
sortTransactions(transactions = []) {
return transactions.sort((a, b) => {
const [aTime, aSeq] = a.transactionId.split('-');

View File

@@ -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
*/

View File

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

View File

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

View File

@@ -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);
});
});

View File

@@ -0,0 +1,6 @@
---
category: Enhancements
authors: [kyrias]
---
Add American Express AESUDEF1 GoCardless bank integration