Skip to content

Commit

Permalink
feat(hooks.service): handle aggregator_link_required
Browse files Browse the repository at this point in the history
  • Loading branch information
g-ongenae committed Jun 10, 2021
1 parent 735da62 commit 0165109
Show file tree
Hide file tree
Showing 5 changed files with 160 additions and 14 deletions.
9 changes: 7 additions & 2 deletions src/hooks/controllers/hooks.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { EventName } from '@algoan/rest';
import { ContextIdFactory } from '@nestjs/core';
import { Test, TestingModule } from '@nestjs/testing';

import { AggregatorModule } from '../../aggregator/aggregator.module';
Expand All @@ -14,14 +15,18 @@ describe('Hooks Controller', () => {
let hooksService: HooksService;

beforeEach(async () => {
// To mock scoped DI
const contextId = ContextIdFactory.create();
jest.spyOn(ContextIdFactory, 'getByRequest').mockImplementation(() => contextId);

const module: TestingModule = await Test.createTestingModule({
imports: [AppModule, AggregatorModule, AlgoanModule, ConfigModule],
providers: [HooksService],
controllers: [HooksController],
}).compile();

controller = module.get<HooksController>(HooksController);
hooksService = module.get<HooksService>(HooksService);
controller = await module.resolve<HooksController>(HooksController, contextId);
hooksService = await module.resolve<HooksService>(HooksService, contextId);
});

it('should be defined', () => {
Expand Down
13 changes: 13 additions & 0 deletions src/hooks/dto/aggregator-link-required.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { IsNotEmpty, IsString } from 'class-validator';

/**
* Payload for event `aggregator_link_required`
*/
export class AggregatorLinkRequiredDTO {
/**
* Customer identifier
*/
@IsString()
@IsNotEmpty()
public customerId: string;
}
5 changes: 3 additions & 2 deletions src/hooks/dto/event.dto.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { Type } from 'class-transformer';
import { Allow, IsInt, IsNotEmpty, IsOptional, ValidateNested } from 'class-validator';

import { AggregatorLinkRequiredDTO } from './aggregator-link-required.dto';
import { BankreaderRequiredDTO } from './bankreader-required.dto';
import { ServiceAccountCreatedDTO } from './service-account-created.dto';
import { ServiceAccountDeletedDTO } from './service-account-deleted.dto';
import { SubscriptionDTO } from './subscription.dto';
import { BankreaderRequiredDTO } from './bankreader-required.dto';

/**
* Events payload types
*/
type Events = ServiceAccountCreatedDTO | ServiceAccountDeletedDTO | BankreaderRequiredDTO;
type Events = ServiceAccountCreatedDTO | ServiceAccountDeletedDTO | BankreaderRequiredDTO | AggregatorLinkRequiredDTO;

/**
* Event
Expand Down
78 changes: 71 additions & 7 deletions src/hooks/services/hooks.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable max-lines */
import {
AccountType,
Algoan,
Expand All @@ -16,17 +17,24 @@ import {
SubscriptionEvent,
UsageType,
} from '@algoan/rest';
/* eslint-disable max-lines */
import { ContextIdFactory } from '@nestjs/core';
import { Test, TestingModule } from '@nestjs/testing';
import { config } from 'node-config-ts';

import { AggregatorModule } from '../../aggregator/aggregator.module';
import { mockAccount, mockPersonalInformation, mockTransaction } from '../../aggregator/interfaces/bridge-mock';
import { AggregatorService } from '../../aggregator/services/aggregator.service';
import { mapBridgeAccount, mapBridgeTransactions } from '../../aggregator/services/bridge/bridge.utils';
import { AlgoanModule } from '../../algoan/algoan.module';
import { customerMock } from '../../algoan/dto/customer.objects.mock';
import { AlgoanAnalysisService } from '../../algoan/services/algoan-analysis.service';
import { AlgoanCustomerService } from '../../algoan/services/algoan-customer.service';
import { AlgoanHttpService } from '../../algoan/services/algoan-http.service';
import { AlgoanService } from '../../algoan/services/algoan.service';
import { AppModule } from '../../app.module';
import { CONFIG } from '../../config/config.module';
import { ConfigModule } from '../../config/config.module';
import { AggregatorLinkRequiredDTO } from '../dto/aggregator-link-required.dto';
import { BankreaderLinkRequiredDTO } from '../dto/bandreader-link-required.dto';
import { EventDTO } from '../dto/event.dto';
import { HooksService } from './hooks.service';
Expand All @@ -35,6 +43,11 @@ describe('HooksService', () => {
let hooksService: HooksService;
let aggregatorService: AggregatorService;
let algoanService: AlgoanService;
let algoanHttpService: AlgoanHttpService;
let algoanCustomerService: AlgoanCustomerService;
let algoanAnalysisService: AlgoanAnalysisService;
let serviceAccount: ServiceAccount;

const mockEvent = {
subscription: {
id: 'mockEventSubId',
Expand Down Expand Up @@ -87,16 +100,34 @@ describe('HooksService', () => {
);

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [AppModule, AggregatorModule, AlgoanModule, ConfigModule],
providers: [HooksService],
// To mock scoped DI
const contextId = ContextIdFactory.create();
jest.spyOn(ContextIdFactory, 'getByRequest').mockImplementation(() => contextId);

const moduleRef: TestingModule = await Test.createTestingModule({
imports: [AppModule, AlgoanModule, AggregatorModule, ConfigModule],
providers: [
HooksService,
{
provide: CONFIG,
useValue: config,
},
{
provide: ServiceAccount,
useValue: mockServiceAccount,
},
],
}).compile();

jest.spyOn(Algoan.prototype, 'initRestHooks').mockResolvedValue();

hooksService = module.get<HooksService>(HooksService);
aggregatorService = module.get<AggregatorService>(AggregatorService);
algoanService = module.get<AlgoanService>(AlgoanService);
hooksService = await moduleRef.resolve<HooksService>(HooksService, contextId);
aggregatorService = await moduleRef.resolve<AggregatorService>(AggregatorService, contextId);
algoanService = await moduleRef.resolve<AlgoanService>(AlgoanService, contextId);
algoanHttpService = await moduleRef.resolve<AlgoanHttpService>(AlgoanHttpService, contextId);
algoanCustomerService = await moduleRef.resolve<AlgoanCustomerService>(AlgoanCustomerService, contextId);
algoanAnalysisService = await moduleRef.resolve<AlgoanAnalysisService>(AlgoanAnalysisService, contextId);
serviceAccount = await moduleRef.resolve<ServiceAccount>(ServiceAccount, contextId);
await algoanService.onModuleInit();
});

Expand All @@ -116,6 +147,15 @@ describe('HooksService', () => {
.mockResolvedValue(({} as unknown) as ISubscriptionEvent & { id: string });
jest.spyOn(algoanService.algoanClient, 'getServiceAccountBySubscriptionId').mockReturnValue(mockServiceAccount);
});

it('handles aggregator link required', async () => {
mockEvent.subscription.eventName = EventName.AGGREGATOR_LINK_REQUIRED;
const spy = jest.spyOn(hooksService, 'handleAggregatorLinkRequired').mockResolvedValue();
await hooksService.handleWebhook(mockEvent as EventDTO, 'mockSignature');

expect(spy).toBeCalledWith(mockServiceAccount, mockEvent.payload);
});

it('handles bankreader link required', async () => {
mockEvent.subscription.eventName = EventName.BANKREADER_LINK_REQUIRED;
const spy = jest.spyOn(hooksService, 'handleBankreaderLinkRequiredEvent').mockResolvedValue();
Expand All @@ -133,6 +173,30 @@ describe('HooksService', () => {
});
});

it('generates a redirect url on aggregator link required', async () => {
const mockEventPayload: AggregatorLinkRequiredDTO = { customerId: customerMock.id };
const algoanAuthenticateSpy = jest.spyOn(algoanHttpService, 'authenticate').mockReturnValue();
const getCustomerSpy = jest.spyOn(algoanCustomerService, 'getCustomerById').mockResolvedValue(customerMock);
const updateCustomerSpy = jest.spyOn(algoanCustomerService, 'updateCustomer').mockResolvedValue(customerMock);
const aggregatorSpy = jest
.spyOn(aggregatorService, 'generateRedirectUrl')
.mockReturnValue(Promise.resolve('mockRedirectUrl'));
mockServiceAccount.config = mockServiceAccountConfig;
await hooksService.handleAggregatorLinkRequired(mockServiceAccount, mockEventPayload);

expect(algoanAuthenticateSpy).toBeCalledWith(mockServiceAccount.clientId, mockServiceAccount.clientSecret);
expect(getCustomerSpy).toBeCalledWith(mockEventPayload.customerId);
expect(aggregatorSpy).toBeCalledWith(
customerMock.id,
customerMock.aggregationDetails?.callbackUrl,
customerMock.personalDetails?.contact?.email,
mockServiceAccountConfig,
);
expect(updateCustomerSpy).toBeCalledWith(customerMock.id, {
aggregationDetails: { aggregatorName: 'BRIDGE', redirectUrl: 'mockRedirectUrl' },
});
});

it('generates a redirect url on bankreader link required', async () => {
const serviceAccountSpy = jest
.spyOn(mockServiceAccount, 'getBanksUserById')
Expand Down
69 changes: 66 additions & 3 deletions src/hooks/services/hooks.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import {
Subscription,
SubscriptionEvent,
} from '@algoan/rest';
import { Injectable, Logger, UnauthorizedException } from '@nestjs/common';
import { Inject, Injectable, Logger, UnauthorizedException } from '@nestjs/common';
import * as delay from 'delay';
import { isEmpty } from 'lodash';
import * as moment from 'moment';
import { config } from 'node-config-ts';
import { config, Config } from 'node-config-ts';

import {
AuthenticationResponse,
Expand All @@ -25,7 +25,14 @@ import {
import { AggregatorService } from '../../aggregator/services/aggregator.service';
import { ClientConfig } from '../../aggregator/services/bridge/bridge.client';
import { mapBridgeAccount, mapBridgeTransactions } from '../../aggregator/services/bridge/bridge.utils';
import { AggregationDetailsAggregatorName, AggregationDetailsMode } from '../../algoan/dto/customer.enums';
import { AggregationDetails, Customer } from '../../algoan/dto/customer.objects';
import { AlgoanAnalysisService } from '../../algoan/services/algoan-analysis.service';
import { AlgoanCustomerService } from '../../algoan/services/algoan-customer.service';
import { AlgoanHttpService } from '../../algoan/services/algoan-http.service';
import { AlgoanService } from '../../algoan/services/algoan.service';
import { CONFIG } from '../../config/config.module';
import { AggregatorLinkRequiredDTO } from '../dto/aggregator-link-required.dto';
import { BankreaderLinkRequiredDTO } from '../dto/bandreader-link-required.dto';
import { BankreaderRequiredDTO } from '../dto/bankreader-required.dto';
import { EventDTO } from '../dto/event.dto';
Expand All @@ -40,7 +47,14 @@ export class HooksService {
*/
private readonly logger: Logger = new Logger(HooksService.name);

constructor(private readonly algoanService: AlgoanService, private readonly aggregator: AggregatorService) {}
constructor(
@Inject(CONFIG) private readonly _config: Config,
private readonly algoanHttpService: AlgoanHttpService,
private readonly algoanCustomerService: AlgoanCustomerService,
private readonly algoanAnalysisService: AlgoanAnalysisService,
private readonly algoanService: AlgoanService,
private readonly aggregator: AggregatorService,
) {}

/**
* Handle Algoan webhooks
Expand Down Expand Up @@ -90,6 +104,10 @@ export class HooksService {

try {
switch (event.subscription.eventName) {
case EventName.AGGREGATOR_LINK_REQUIRED:
await this.handleAggregatorLinkRequired(serviceAccount, event.payload as AggregatorLinkRequiredDTO);
break;

case EventName.BANKREADER_LINK_REQUIRED:
await this.handleBankreaderLinkRequiredEvent(serviceAccount, event.payload as BankreaderLinkRequiredDTO);
break;
Expand All @@ -116,6 +134,51 @@ export class HooksService {
void se.update({ status: EventStatus.PROCESSED });
}

/**
* Handle the "aggregator_link_required" event
* Looks for a callback URL and generates a new redirect URL
* @param serviceAccount Concerned Algoan service account attached to the subscription
* @param payload Payload sent, containing the Banks User id
*/
public async handleAggregatorLinkRequired(
serviceAccount: ServiceAccount,
payload: AggregatorLinkRequiredDTO,
): Promise<void> {
// Authenticate to algoan
this.algoanHttpService.authenticate(serviceAccount.clientId, serviceAccount.clientSecret);

// Get user information and client config
const customer: Customer = await this.algoanCustomerService.getCustomerById(payload.customerId);
this.logger.debug({ customer, serviceAccount }, `Found Customer with id ${customer.id}`);

const aggregationDetails: AggregationDetails = {
aggregatorName: AggregationDetailsAggregatorName.BRIDGE,
};
switch (customer.aggregationDetails?.mode) {
case AggregationDetailsMode.REDIRECT:
// Generates a redirect URL
aggregationDetails.redirectUrl = await this.aggregator.generateRedirectUrl(
customer.id,
customer.aggregationDetails?.callbackUrl,
customer.personalDetails?.contact?.email,
serviceAccount.config as ClientConfig,
);
break;

default:
throw new Error(`Invalid bank connection mode ${customer.aggregationDetails?.mode}`);
}

// Update user with redirect link information and userId if provided
await this.algoanCustomerService.updateCustomer(payload.customerId, {
aggregationDetails,
});

this.logger.debug(`Updated Customer ${payload.customerId}`);

return;
}

/**
* Handle the "bankreader_link_required" event
* Looks for a callback URL and generates a new redirect URL
Expand Down

0 comments on commit 0165109

Please sign in to comment.