generated from algoan/nestjs-connector-boilerplate
-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #310 from g-ongenae/feature/bank_details_required
[FEAT] Add event handler for `bank_details_required`
- Loading branch information
Showing
8 changed files
with
562 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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), | ||
}, | ||
}), | ||
), | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.