Skip to content

Commit

Permalink
process kyb applications, creating CDD is address is specified in the…
Browse files Browse the repository at this point in the history
… original request
  • Loading branch information
polymath-eric committed May 8, 2024
1 parent d25ee97 commit 0dc3462
Show file tree
Hide file tree
Showing 18 changed files with 429 additions and 63 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Environment
.env
.nx

# compiled output
dist
Expand Down
13 changes: 11 additions & 2 deletions apps/cdd-backend/src/app-redis/app-redis.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,15 +72,24 @@ describe('AppRedisService', () => {
const exampleCode = { code: 'someCode' } as unknown as NetkiAccessLinkModel;

it('should add codes', async () => {
redis.scard.mockResolvedValue(1);
redis.sadd.mockResolvedValue(1);

const result = await service.pushNetkiCodes([exampleCode]);

expect(redis.sadd).toHaveBeenCalledWith(netkiAvailableCodesPrefix, [
JSON.stringify(exampleCode),
]);
expect(result).toEqual({ added: 1, total: 1 });
expect(result).toEqual(1);
});
});

describe('getAccessCodeCount', () => {
it('should return code count', async () => {
redis.scard.mockResolvedValue(3);

const result = await service.getAccessCodeCount();

expect(result).toEqual(3);
});
});

Expand Down
36 changes: 27 additions & 9 deletions apps/cdd-backend/src/app-redis/app-redis.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
netkiAddressPrefixer,
netkiBusinessAppPrefixer,
netkiBusinessAppPrefix,
netkiBusinessToAddressPrefixer,
} from './utils';

@Injectable()
Expand Down Expand Up @@ -81,23 +82,22 @@ export class AppRedisService {
await this.redis.del(netkiAccessCodeKey);
}

async pushNetkiCodes(
newCodes: NetkiAccessLinkModel[]
): Promise<{ added: number; total: number }> {
async pushNetkiCodes(newCodes: NetkiAccessLinkModel[]): Promise<number> {
const added = await this.redis.sadd(
netkiAvailableCodesPrefix,
newCodes.map((link) => JSON.stringify(link))
);

const total = await this.redis.scard(netkiAvailableCodesPrefix);

this.logger.info('added new netki codes', {
attemptedToAdd: newCodes.length,
added,
total,
});

return { added, total };
return added;
}

async getAccessCodeCount(): Promise<number> {
return this.redis.scard(netkiAvailableCodesPrefix);
}

async popNetkiAccessLink(): Promise<NetkiAccessLinkModel | null> {
Expand All @@ -117,8 +117,6 @@ export class AppRedisService {
this.redis.keys(`${netkiBusinessAppPrefix}*`),
]);

console.log({ allocatedIndividualCodes, allocatedBusinessCodes });

return new Set(
[...allocatedIndividualCodes, ...allocatedBusinessCodes].map((code) =>
code
Expand All @@ -128,6 +126,26 @@ export class AppRedisService {
);
}

async setBusinessIdToAddress(
businessId: string,
address: string
): Promise<void> {
const prefixedKey = netkiBusinessToAddressPrefixer(businessId);

this.logger.debug('associating netki business ID to address', {
businessId,
address,
});

await this.redis.set(prefixedKey, address);
}

async getNetkiBusinessAddress(businessId: string): Promise<string | null> {
const businessKey = netkiBusinessToAddressPrefixer(businessId);

return this.redis.get(businessKey);
}

async availableNetkiCodeCount(): Promise<number> {
return await this.redis.scard(netkiAvailableCodesPrefix);
}
Expand Down
4 changes: 4 additions & 0 deletions apps/cdd-backend/src/app-redis/utils.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
export const netkiAvailableCodesPrefix = 'netki-codes' as const;
export const netkiAllocatedCodePrefix = 'netki-allocated-codes:' as const;
export const netkiBusinessAppPrefix = 'netki-business-codes:' as const;
export const netkiBusinessToAddressPrefix = 'netki-business-address:' as const;

export const netkiAddressPrefixer = (id: string) =>
`${netkiAllocatedCodePrefix}${id}`;

export const netkiBusinessAppPrefixer = (id: string) =>
`${netkiBusinessAppPrefix}${id}`;

export const netkiBusinessToAddressPrefixer = (address: string) =>
`${netkiBusinessToAddressPrefix}${address}`;
86 changes: 85 additions & 1 deletion apps/cdd-backend/src/cdd-worker/cdd.processor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import { Polymesh } from '@polymeshassociation/polymesh-sdk';
import { Job } from 'bull';
import { MockPolymesh } from '../test-utils/mocks';
import { CddProcessor } from './cdd.processor';
import { JumioCddJob, MockCddJob, NetkiCddJob } from './types';
import {
JumioCddJob,
MockCddJob,
NetkiBusinessJob,
NetkiCddJob,
} from './types';
import { JumioCallbackDto } from '../jumio/types';

import jumioVerifiedData from '../test-utils/jumio-http/webhook-approved-verified.json';
Expand Down Expand Up @@ -193,6 +198,85 @@ describe('cddProcessor', () => {
processor.generateCdd(mockNetkiCompletedJob)
).rejects.toThrowError();
});

it('should assign the address to business ID in case of a business application', async () => {
mockNetkiCompletedJob.data.value.identity.transaction_identity.identity_access_code.business =
'netkiBusinessId';

mockRedis.getNetkiAddress.mockResolvedValue(null);
mockRedis.getNetkiBusinessApplication.mockResolvedValue({
id: 'someId',
accessCode: 'someCode',
link: 'someLink',
address: 'someAddress',
timestamp: new Date().toISOString(),
});

await processor.generateCdd(mockNetkiCompletedJob);

expect(mockRedis.setBusinessIdToAddress).toHaveBeenCalledWith(
'netkiBusinessId',
'someAddress'
);
});

it('should throw an error if there is no business in the callback', async () => {
mockRedis.getNetkiAddress.mockResolvedValue(null);
mockRedis.getNetkiBusinessApplication.mockResolvedValue({
id: 'someId',
accessCode: 'someCode',
link: 'someLink',
address: 'someAddress',
timestamp: new Date().toISOString(),
});

await expect(
processor.generateCdd(mockNetkiCompletedJob)
).rejects.toThrow();
});
});

describe('netki business job', () => {
let mockNetkiCompletedJob: Job<NetkiBusinessJob>;

beforeEach(() => {
mockNetkiCompletedJob = {
...createMock<Job>(),
data: {
type: 'netki-kyb',
value: {
parent_business: 'someBusinessId',
status: 'accepted',
},
},
};
});

it('should create a cdd claim', async () => {
mockRedis.getNetkiBusinessAddress.mockResolvedValue('someAddress');

await processor.generateCdd(mockNetkiCompletedJob);

expect(mockPolymesh.identities.registerIdentity).toHaveBeenCalledWith(
{
targetAccount: 'someAddress',
createCdd: true,
},
{ signingAccount: 'netkiSignerAddress' }
);
});

it('should not create a cdd claim if status is not accepted', async () => {
mockRedis.getNetkiBusinessAddress.mockResolvedValue('someAddress');

mockNetkiCompletedJob.data.value.status = 'rejected';

await processor.generateCdd(mockNetkiCompletedJob);

expect(
mockPolymesh.identities.registerIdentity
).not.toHaveBeenCalled();
});
});

describe('restarted job', () => {
Expand Down
57 changes: 54 additions & 3 deletions apps/cdd-backend/src/cdd-worker/cdd.processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
JobIdentifier,
JumioCddJob,
NetkiCddJob,
NetkiBusinessJob,
} from './types';
import { Identity } from '@polymeshassociation/polymesh-sdk/types';
import { NetkiBusinessApplicationModel } from '../app-redis/models/netki-business-application.model';
Expand All @@ -36,17 +37,53 @@ export class CddProcessor {
await this.handleNetki(job.data);
} else if (job.data.type === 'mock') {
await this.handleMockJob(job.data);
} else if (job.data.type === 'netki-kyb') {
await this.handleNetkiBusiness(job.data);
} else {
throw new Error('unknown CDD job type encountered');
}

this.logger.info('finished processing CDD job', { jobId: job.id });
}

private async handleNetkiBusiness({
value: { parent_business: businessId, status },
}: NetkiBusinessJob): Promise<void> {
if (status !== 'accepted') {
this.logger.info(
'netki business callback did not have accepted status. No action will be taken'
);

return;
}

const address = await this.redis.getNetkiBusinessAddress(businessId);

if (!address) {
this.logger.info(
'there was no address associated to the business ID - no action will be taken',
{
businessId,
}
);

return;
}

await this.createCddClaim(
{ id: businessId, provider: 'netki' },
address,
'netki'
);
}

private async handleNetki({ value: netki }: NetkiCddJob): Promise<void> {
const {
identity: {
transaction_identity: { client_guid: guid },
transaction_identity: {
client_guid: guid,
identity_access_code: { business },
},
state,
},
} = netki;
Expand Down Expand Up @@ -83,10 +120,24 @@ export class CddProcessor {
await this.createCddClaim(jobId, address, 'netki');
await this.clearAddressApplications(jobId, address);
} else if (businessApplication?.address) {
await this.createCddClaim(jobId, businessApplication.address, 'netki'); // should this get its own key?
if (!business) {
this.logger.error(
'no business ID was in callback for business application',
{ jobId, address: businessApplication.address }
);

throw new Error(
'No business ID was in callback, but it was expected'
);
}

await this.redis.setBusinessIdToAddress(
business,
businessApplication.address
);
} else {
this.logger.info(
'no address was associated to netki application, no CDD claim is being made'
'no address was associated to netki business application, no business ID association will be made'
);
}
} else {
Expand Down
9 changes: 7 additions & 2 deletions apps/cdd-backend/src/cdd-worker/types.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { CddProvider } from '@cdd-onboarding/cdd-types';
import { JumioCallbackDto } from '../jumio/types';
import { MockCddDto } from '../mock-cdd/types';
import { NetkiCallbackDto } from '../netki/types';
import { NetkiBusinessCallbackDto, NetkiCallbackDto } from '../netki/types';

export type CddJob = JumioCddJob | NetkiCddJob | MockCddJob;
export type CddJob = JumioCddJob | NetkiCddJob | NetkiBusinessJob | MockCddJob;

export interface JumioCddJob {
value: JumioCallbackDto;
Expand All @@ -15,6 +15,11 @@ export interface NetkiCddJob {
type: 'netki';
}

export interface NetkiBusinessJob {
value: NetkiBusinessCallbackDto;
type: 'netki-kyb';
}

export interface MockCddJob {
value: MockCddDto;
type: 'mock';
Expand Down
4 changes: 0 additions & 4 deletions apps/cdd-backend/src/cdd/cdd.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import {
EmailDetailsDto,
AddressApplicationsResponse,
AddressApplicationsParamsDto,
BusinessLinkDto,
BusinessLinkResponse,
} from '@cdd-onboarding/cdd-types';
import { Body, Controller, Get, Param, Post, UseGuards } from '@nestjs/common';
import {
Expand Down Expand Up @@ -48,8 +46,6 @@ export class CddController {
return new ProviderLinkResponse(link);
}



@Post('/email')
async emailAddress(@Body() body: EmailDetailsDto): Promise<void> {
return this.cddService.processEmail(body);
Expand Down
26 changes: 26 additions & 0 deletions apps/cdd-backend/src/common/api-key.guard.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { mockHttpContext } from '../test-utils/mocks';
import { ApiKeyGuard } from './api-key.guard';

describe('ApiKeyGuard', () => {
let apiKeyGuard: ApiKeyGuard;
const allowedApiKeys = ['someApiKey'];

beforeEach(() => {
apiKeyGuard = new ApiKeyGuard(allowedApiKeys);
});

describe('canActivate', () => {
it('should return true if the client Authorization header is included in the allowed basic auth', () => {
const httpContext = mockHttpContext('::1', 'Bearer someApiKey', {});
const canActivate = apiKeyGuard.canActivate(httpContext);
expect(canActivate).toBe(true);
});

it('should return false if the client Authorization header is not included in the basic auth', () => {
const httpContext = mockHttpContext('::1', 'Bearer BAD-KEY', {});

const canActivate = apiKeyGuard.canActivate(httpContext);
expect(canActivate).toBe(false);
});
});
});
Loading

0 comments on commit 0dc3462

Please sign in to comment.