Skip to content

Commit

Permalink
send slack messages for applications requiring Polymesh team action
Browse files Browse the repository at this point in the history
  • Loading branch information
F-OBrien committed Aug 22, 2024
1 parent 6150013 commit a07c346
Show file tree
Hide file tree
Showing 6 changed files with 986 additions and 70 deletions.
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 @@ -2,7 +2,7 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest';
import { Test, TestingModule } from '@nestjs/testing';
import { Polymesh } from '@polymeshassociation/polymesh-sdk';
import { Job } from 'bull';
import { MockPolymesh } from '../test-utils/mocks';
import { MockPolymesh, slackAppMock } from '../test-utils/mocks';
import { CddProcessor } from './cdd.processor';
import {
JumioCddJob,
Expand Down Expand Up @@ -49,6 +49,9 @@ describe('cddProcessor', () => {
mockPolymesh = module.get<typeof mockPolymesh>(Polymesh);
mockAddressBook = module.get<typeof mockAddressBook>(AddressBookService);

// eslint-disable-next-line @typescript-eslint/no-explicit-any
(processor as any).slackApp = slackAppMock;

mockRun = jest.fn().mockResolvedValue('test-tx-result');
mockPolymesh.identities.registerIdentity.mockResolvedValue({
run: mockRun,
Expand Down Expand Up @@ -167,6 +170,32 @@ describe('cddProcessor', () => {
};
});

it('should send a Slack message if the status is hold', async () => {
mockNetkiCompletedJob.data.value.identity.state = 'hold';
await processor.generateCdd(mockNetkiCompletedJob);

expect(slackAppMock.client.chat.postMessage).toHaveBeenNthCalledWith(
1,
{
token: expect.any(String),
channel: expect.any(String),
text: 'A Netki Onboarding application requires review',
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: ':warning: Netki CDD application with access code *xyz* has been placed on *HOLD*.\n:mag: Please review and process in the Netki dashboard.',
},
},
],
}
);
expect(
mockPolymesh.identities.registerIdentity
).not.toHaveBeenCalled();
});

it('should create CDD claim and clear previous link attempts', async () => {
await processor.generateCdd(mockNetkiCompletedJob);

Expand Down Expand Up @@ -248,6 +277,7 @@ describe('cddProcessor', () => {
business: {
parent_business: 'someBusinessId',
status: 'accepted',
name: 'Some Business Name',
},
},
},
Expand All @@ -268,6 +298,60 @@ describe('cddProcessor', () => {
);
});

it('should send a slack message if business application status is open', async () => {
mockNetkiCompletedJob.data.value.business.status = 'open';

await processor.generateCdd(mockNetkiCompletedJob);

expect(slackAppMock.client.chat.postMessage).toHaveBeenNthCalledWith(
2,
{
token: expect.any(String),
channel: expect.any(String),
text: 'New Netki business application requires review',
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: ':bell: Netki Business CDD application received from *Some Business Name*.\n:mag: Please review and process in the Netki dashboard.',
},
},
],
}
);
expect(
mockPolymesh.identities.registerIdentity
).not.toHaveBeenCalled();
});

it('should send a slack message if business application status is hold', async () => {
mockNetkiCompletedJob.data.value.business.status = 'hold';

await processor.generateCdd(mockNetkiCompletedJob);

expect(slackAppMock.client.chat.postMessage).toHaveBeenNthCalledWith(
3,
{
token: expect.any(String),
channel: expect.any(String),
text: 'Netki business application on requires review',
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: ':warning: Netki Business CDD application from *Some Business Name* was placed on *HOLD*.\n:mag: Please review and process in the Netki dashboard.',
},
},
],
}
);
expect(
mockPolymesh.identities.registerIdentity
).not.toHaveBeenCalled();
});

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

Expand Down
195 changes: 132 additions & 63 deletions apps/cdd-backend/src/cdd-worker/cdd.processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,23 @@ import {
} from './types';
import { Identity } from '@polymeshassociation/polymesh-sdk/types';
import { NetkiBusinessApplicationModel } from '../app-redis/models/netki-business-application.model';
import { App as SlackApp } from '@slack/bolt';

@Processor()
export class CddProcessor {
private slackApp: SlackApp;

constructor(
private readonly polymesh: Polymesh,
private readonly signerLookup: AddressBookService,
private readonly redis: AppRedisService,
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger
) {}
) {
this.slackApp = new SlackApp({
signingSecret: process.env.SLACK_SIGNING_SECRET,
token: process.env.SLACK_BOT_TOKEN,
});
}

@Process()
async generateCdd(job: Job<CddJob>) {
Expand All @@ -48,43 +56,79 @@ export class CddProcessor {

private async handleNetkiBusiness({
value: {
business: { parent_business: businessId, status },
business: { parent_business: businessId, status, name },
},
}: 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);
switch (status) {
case 'open':
await this.slackApp.client.chat.postMessage({
token: process.env.SLACK_BOT_TOKEN,
channel: process.env.SLACK_CHANNEL || '',
text: 'New Netki business application requires review',
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `:bell: Netki Business CDD application received from *${name}*.\n:mag: Please review and process in the Netki dashboard.`,
},
},
],
});
break;

case 'hold':
await this.slackApp.client.chat.postMessage({
token: process.env.SLACK_BOT_TOKEN,
channel: process.env.SLACK_CHANNEL || '',
text: 'Netki business application on requires review',
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `:warning: Netki Business CDD application from *${name}* was placed on *HOLD*.\n:mag: Please review and process in the Netki dashboard.`,
},
},
],
});
break;

case 'accepted': {
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,
}
);

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

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

await this.createCddClaim(
{ id: businessId, provider: 'netki' },
address,
'netki'
);
default:
this.logger.info(
`netki business callback with status ${status} does not have a handler. No action will be taken`
);
}
}

private async handleNetki({ value: netki }: NetkiCddJob): Promise<void> {
const {
identity: {
transaction_identity: {
client_guid: guid,
identity_access_code: { business },
identity_access_code: { business, code },
},
state,
},
Expand All @@ -109,50 +153,75 @@ export class CddProcessor {
state,
});

if (state === 'restarted') {
this.logger.debug('handling netki restart', { jobId });
if (individualAddress) {
await this.handleNetkiRestart(jobId, individualAddress, netki);
} else if (businessApplication) {
await this.handleNetkiRestartForBusiness(
jobId,
businessApplication,
netki
);
}
} else if (state === 'completed') {
this.logger.debug('handling netki completed', { jobId });
if (individualAddress) {
await this.createCddClaim(jobId, individualAddress, 'netki');
await this.clearAddressApplications(jobId, individualAddress);
} else if (businessApplication?.address) {
if (!business) {
this.logger.error(
'no business ID was in callback for business application',
{ jobId, address: businessApplication.address }
switch (state) {
case 'restarted':
this.logger.debug('handling netki restart', { jobId });
if (individualAddress) {
await this.handleNetkiRestart(jobId, individualAddress, netki);
} else if (businessApplication) {
await this.handleNetkiRestartForBusiness(
jobId,
businessApplication,
netki
);
}
break;

case 'completed':
this.logger.debug('handling netki completed', { jobId });
if (individualAddress) {
await this.createCddClaim(jobId, individualAddress, 'netki');
await this.clearAddressApplications(jobId, individualAddress);
} else if (businessApplication?.address) {
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'
);
}

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 business application, no business ID association will be made'
);
}

await this.redis.setBusinessIdToAddress(
business,
businessApplication.address
);
} else {
break;

case 'hold':
await this.slackApp.client.chat.postMessage({
token: process.env.SLACK_BOT_TOKEN,
channel: process.env.SLACK_CHANNEL || '',
text: 'A Netki Onboarding application requires review',
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `:warning: Netki CDD application with access code *${code}* has been placed on *HOLD*.\n:mag: Please review and process in the Netki dashboard.`,
},
},
],
});
break;

default:
this.logger.info(
'no address was associated to netki business application, no business ID association will be made'
`netki state ${state} did not have a handler - no action taken`,
{
jobId,
address: individualAddress,
businessApplicationId: businessApplication?.id,
state,
}
);
}
} else {
this.logger.info('netki state did not have a handler - no action taken', {
jobId,
address: individualAddress,
businessApplicationId: businessApplication?.id,
state,
});
}

this.logger.info('netki CDD job completed successfully', {
Expand Down
1 change: 1 addition & 0 deletions apps/cdd-backend/src/netki/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const NetkiBusinessCallbackZ = extendApi(
business: z.object({
parent_business: z.string(),
status: z.string(),
name: z.string(),
}),
})
);
Expand Down
8 changes: 8 additions & 0 deletions apps/cdd-backend/src/test-utils/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,11 @@ export class MockPolymesh {
},
};
}

export const slackAppMock = {
client: {
chat: {
postMessage: jest.fn().mockResolvedValue(true),
},
},
};
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
"@polymeshassociation/hashicorp-vault-signing-manager": "^3.3.0",
"@polymeshassociation/local-signing-manager": "^3.2.0",
"@polymeshassociation/polymesh-sdk": "^24.6.0",
"@slack/bolt": "^3.21.1",
"await-to-js": "^3.0.0",
"axios": "^1.3.4",
"bull": "^4.10.3",
Expand Down
Loading

0 comments on commit a07c346

Please sign in to comment.