Skip to content

Commit

Permalink
Merge pull request #310 from g-ongenae/feature/bank_details_required
Browse files Browse the repository at this point in the history
[FEAT] Add event handler for `bank_details_required`
  • Loading branch information
meriamBenSassi authored Jun 25, 2021
2 parents 1857921 + 3c31ffd commit 8ebe81a
Show file tree
Hide file tree
Showing 8 changed files with 562 additions and 4 deletions.
2 changes: 1 addition & 1 deletion config/env/test.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
],
"bridge":{
"synchronizationWaitingTime": 0,
"synchronizationTimeout": 0
"synchronizationTimeout": 2
},
"banksUserIdPassword": "random_pass"
}
82 changes: 82 additions & 0 deletions src/aggregator/services/bridge/bridge-v2.utils.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { HttpModule } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';

import { AlgoanModule } from '../../../algoan/algoan.module';
import { AccountLoanType, AccountType, AccountUsage } from '../../../algoan/dto/analysis.enum';
import { Account, AccountTransaction } from '../../../algoan/dto/analysis.inputs';
import { AppModule } from '../../../app.module';
import { AggregatorModule } from '../../aggregator.module';
import { mockAccount, mockPersonalInformation, mockTransaction } from '../../interfaces/bridge-mock';
import { AggregatorService } from '../aggregator.service';
import { mapBridgeAccount, mapBridgeTransactions } from './bridge-v2.utils';

describe('Bridge Utils for Algoan v2 (Customer, Analysis)', () => {
let aggregatorService: AggregatorService;
let aggregatorSpy;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [AppModule, HttpModule, AlgoanModule, AggregatorModule],
}).compile();

aggregatorService = module.get<AggregatorService>(AggregatorService);
aggregatorSpy = jest
.spyOn(aggregatorService, 'getResourceName')
.mockReturnValue(Promise.resolve('mockResourceName'));
});

it('should map the bridge account to a banksUser', async () => {
const expectedAccounts: Account[] = [
{
balance: 100,
balanceDate: '2019-04-06T13:53:12.000Z',
currency: 'USD',
type: AccountType.CREDIT_CARD,
usage: AccountUsage.PERSONAL,
owners: [{ name: ' DUPONT' }],
iban: 'mockIban',
name: 'mockBridgeAccountName',
bank: { id: '6', name: 'mockResourceName' },
details: {
loan: {
amount: 140200,
startDate: '2013-01-09T23:00:00.000Z',
endDate: '2026-12-30T23:00:00.000Z',
payment: 1000,
interestRate: 1.25,
remainingCapital: 100000,
type: AccountLoanType.OTHER,
},
},
aggregator: { id: '1234' },
},
];

const mappedAccount = await mapBridgeAccount(
[mockAccount],
mockPersonalInformation,
'mockAccessToken',
aggregatorService,
);

expect(aggregatorSpy).toHaveBeenCalledWith('mockAccessToken', mockAccount.bank.resource_uri, undefined);
expect(mappedAccount).toEqual(expectedAccounts);
});

it('should map the bridge transactions to banksUser', async () => {
const expectedTransaction: AccountTransaction[] = [
{
dates: { debitedAt: '2019-04-06T13:53:12.000Z' },
description: 'mockRawDescription',
amount: 30,
currency: 'USD',
isComing: false,
aggregator: { id: '23', category: 'mockResourceName' },
},
];

const mappedTransaction = await mapBridgeTransactions([mockTransaction], 'mockAccessToken', aggregatorService);

expect(mappedTransaction).toEqual(expectedTransaction);
expect(aggregatorSpy).toBeCalledWith('mockAccessToken', mockTransaction.category.resource_uri, undefined);
});
});
164 changes: 164 additions & 0 deletions src/aggregator/services/bridge/bridge-v2.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import * as moment from 'moment-timezone';

import { AccountLoanType, AccountType, AccountUsage } from '../../../algoan/dto/analysis.enum';
import { Account, AccountOwner, AccountTransaction } from '../../../algoan/dto/analysis.inputs';
import {
BridgeAccount,
BridgeAccountType,
BridgeTransaction,
BridgeUserInformation,
} from '../../interfaces/bridge.interface';
import { AggregatorService } from '../aggregator.service';
import { ClientConfig } from './bridge.client';

/**
* mapBridgeAccount transforms a bridge array of accounts into
* an array of Banks User accounts
* @param accounts array of accounts from Bridge
* @param accessToken permanent access token for Bridge api
*/
export const mapBridgeAccount = async (
accounts: BridgeAccount[],
userInfo: BridgeUserInformation[],
accessToken: string,
aggregator: AggregatorService,
clientConfig?: ClientConfig,
): Promise<Account[]> =>
Promise.all(
accounts.map(async (account) =>
fromBridgeToAlgoanAccounts(account, userInfo, accessToken, aggregator, clientConfig),
),
);

/**
* Converts a single Bridge account instance to Algoan format
* @param account
* @param accessToken permanent access token for Bridge api
*/
const fromBridgeToAlgoanAccounts = async (
account: BridgeAccount,
userInfo: BridgeUserInformation[],
accessToken: string,
aggregator: AggregatorService,
clientConfig?: ClientConfig,
): Promise<Account> => ({
balance: account.balance,
balanceDate: new Date(mapDate(account.updated_at)).toISOString(),
currency: account.currency_code,
type: mapAccountType(account.type),
usage: mapUsageType(account.is_pro),
owners: mapUserInfo(account.item.id, userInfo),
// eslint-disable-next-line
iban: account.iban !== null ? account.iban : undefined,
bic: undefined,
name: account.name,
bank: {
id: account.bank?.id?.toString(),
name: await aggregator.getResourceName(accessToken, account.bank.resource_uri, clientConfig),
},
details: {
savings: mapAccountType(account.type) === AccountType.SAVINGS ? {} : undefined,
loan:
// eslint-disable-next-line
account.loan_details !== null && account.loan_details !== undefined
? {
amount: account.loan_details.borrowed_capital,
startDate: new Date(mapDate(account.loan_details.opening_date)).toISOString(),
endDate: new Date(mapDate(account.loan_details.maturity_date)).toISOString(),
payment: account.loan_details.next_payment_amount,
interestRate: account.loan_details.interest_rate,
remainingCapital: account.loan_details.remaining_capital,
// ? QUESTION: Mapping of account.loan_details.type to AccountLoanType?
type: AccountLoanType.OTHER,
}
: undefined,
},
aggregator: {
id: account.id.toString(),
},
});

/**
* mapDate transforms an iso date in string into a timestamp or undefined
* @param isoDate date from bridge, if null returns undefined
*/
const mapDate = (isoDate: string): number =>
isoDate ? moment.tz(isoDate, 'Europe/Paris').toDate().getTime() : moment().toDate().getTime();

/**
* AccountTypeMapping
*/
interface AccountTypeMapping {
[index: string]: AccountType;
}

const ACCOUNT_TYPE_MAPPING: AccountTypeMapping = {
[BridgeAccountType.CHECKING]: AccountType.CHECKING,
[BridgeAccountType.SAVINGS]: AccountType.SAVINGS,
[BridgeAccountType.SECURITIES]: AccountType.SAVINGS,
[BridgeAccountType.CARD]: AccountType.CREDIT_CARD,
[BridgeAccountType.LOAN]: AccountType.LOAN,
[BridgeAccountType.SHARE_SAVINGS_PLAN]: AccountType.SAVINGS,
[BridgeAccountType.LIFE_INSURANCE]: AccountType.SAVINGS,
};

/**
* mapAccountType map the banksUser type from the bridge type
* @param accountType bridge type
*/
// eslint-disable-next-line no-null/no-null
const mapAccountType = (accountType: BridgeAccountType): AccountType => ACCOUNT_TYPE_MAPPING[accountType] || null;

/**
* mapUsageType map the banksUser usage from the bridge type
* @param isPro Bridge boolean
*/
const mapUsageType = (isPro: boolean): AccountUsage => (isPro ? AccountUsage.PROFESSIONAL : AccountUsage.PERSONAL);

/**
* mapUserInfo map the user personal information with the account
*/
const mapUserInfo = (itemId: number, userInfo: BridgeUserInformation[]): AccountOwner[] | undefined => {
const NOT_FOUND: number = -1;
const index: number = userInfo.findIndex(({ item_id }): boolean => item_id === itemId);

return index !== NOT_FOUND
? [
{
name: [userInfo[index].first_name, userInfo[index].last_name].join(' '),
},
]
: undefined;
};

/**
* mapBridgeTransactions transforms a bridge transaction wrapper into
* an array of banks user transactions
*
* @param bridgeTransactions TransactionWrapper from Bridge
* @param accessToken permanent access token for Bridge api
*/
export const mapBridgeTransactions = async (
bridgeTransactions: BridgeTransaction[],
accessToken: string,
aggregator: AggregatorService,
clientConfig?: ClientConfig,
): Promise<AccountTransaction[]> =>
Promise.all(
bridgeTransactions.map(
async (transaction: BridgeTransaction): Promise<AccountTransaction> => ({
dates: {
debitedAt: !transaction.is_future ? moment.tz(transaction.date, 'Europe/Paris').toISOString() : undefined,
bookedAt: transaction.is_future ? moment.tz(transaction.date, 'Europe/Paris').toISOString() : undefined,
},
description: transaction.raw_description,
amount: transaction.amount,
currency: transaction.currency_code,
isComing: transaction.is_future,
aggregator: {
id: transaction.id.toString(),
category: await aggregator.getResourceName(accessToken, transaction.category.resource_uri, clientConfig),
},
}),
),
);
4 changes: 2 additions & 2 deletions src/algoan/dto/analysis.inputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export interface AccountOwner {
}

/**
* Accoun tBank
* Account tBank
*/
export interface AccountBank {
id?: string;
Expand All @@ -50,7 +50,7 @@ export interface AccountBank {
*/
export interface AccountDetails {
savings?: AccountDetailsSavings;
loans?: AccountDetailsLoans;
loan?: AccountDetailsLoans;
}

/**
Expand Down
19 changes: 19 additions & 0 deletions src/hooks/dto/bank-details-required.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';

/**
* DTO for the event `banks_details_required`
*/
export class BanksDetailsRequiredDTO {
/** Id of the customer */
@IsString()
@IsNotEmpty()
public readonly customerId: string;
/** Id of the analysis */
@IsString()
@IsNotEmpty()
public readonly analysisId: string;
/** Temporary code to connect the user */
@IsString()
@IsOptional()
public readonly temporaryCode?: string;
}
8 changes: 7 additions & 1 deletion src/hooks/dto/event.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Type } from 'class-transformer';
import { Allow, IsInt, IsNotEmpty, IsOptional, ValidateNested } from 'class-validator';

import { AggregatorLinkRequiredDTO } from './aggregator-link-required.dto';
import { BanksDetailsRequiredDTO } from './bank-details-required.dto';
import { BankreaderRequiredDTO } from './bankreader-required.dto';
import { ServiceAccountCreatedDTO } from './service-account-created.dto';
import { ServiceAccountDeletedDTO } from './service-account-deleted.dto';
Expand All @@ -10,7 +11,12 @@ import { SubscriptionDTO } from './subscription.dto';
/**
* Events payload types
*/
type Events = ServiceAccountCreatedDTO | ServiceAccountDeletedDTO | BankreaderRequiredDTO | AggregatorLinkRequiredDTO;
type Events =
| ServiceAccountCreatedDTO
| ServiceAccountDeletedDTO
| BankreaderRequiredDTO
| AggregatorLinkRequiredDTO
| BanksDetailsRequiredDTO;

/**
* Event
Expand Down
Loading

0 comments on commit 8ebe81a

Please sign in to comment.