Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Send slack messages for new Business application or application put on hold #102

Merged
merged 2 commits into from
Aug 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.sample.worker
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,7 @@ REDIS_PASSWORD=someSecret
OTLP_EXPORT_URL='http://localhost:4318' # Telemetry is disabled without this set
OTLP_HOSTNAME # defaults to `os.hostname`
OTLP_SERVICE # defaults to `PolymeshCdd`

SLACK_SIGNING_SECRET # Slack signs the requests we send you using this secret
SLACK_BOT_TOKEN # Bot User OAuth Token
SLACK_CHANNEL # Name of the slack channel to post messages to
2 changes: 2 additions & 0 deletions apps/cdd-backend/src/cdd-worker/cdd-worker.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import { AppRedisModule } from '../app-redis/app-redis.module';
import { AppBullModule } from '../app-bull/app-bull.module';
import { PolymeshModule } from '../polymesh/polymesh.module';
import { CddProcessor } from './cdd.processor';
import { SlackMessageModule } from '../slack/slackMessage.module';

@Module({
imports: [
PolymeshModule,
AppRedisModule,
AppBullModule,
SlackMessageModule,
BullModule.registerQueue({}),
],
providers: [CddProcessor],
Expand Down
51 changes: 50 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 @@ -20,6 +20,7 @@ import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { AppRedisService } from '../app-redis/app-redis.service';
import { AddressBookService } from '../polymesh/address-book.service';
import { Account, Identity } from '@polymeshassociation/polymesh-sdk/types';
import { SlackMessageService } from '../slack/slackMessage.service';

describe('cddProcessor', () => {
let mockRedis: DeepMocked<AppRedisService>;
Expand All @@ -28,6 +29,7 @@ describe('cddProcessor', () => {
let mockPolymesh: MockPolymesh;
let mockAddressBook: DeepMocked<AddressBookService>;
let processor: CddProcessor;
let mockSlackMessage: DeepMocked<SlackMessageService>;
let mockRun: jest.Mock;

beforeEach(async () => {
Expand All @@ -36,6 +38,10 @@ describe('cddProcessor', () => {
CddProcessor,
{ provide: Polymesh, useValue: new MockPolymesh() },
{ provide: AppRedisService, useValue: createMock<AppRedisService>() },
{
provide: SlackMessageService,
useValue: createMock<SlackMessageService>(),
},
{ provide: WINSTON_MODULE_PROVIDER, useValue: createMock<Logger>() },
{
provide: AddressBookService,
Expand All @@ -48,7 +54,7 @@ describe('cddProcessor', () => {
mockRedis = module.get<typeof mockRedis>(AppRedisService);
mockPolymesh = module.get<typeof mockPolymesh>(Polymesh);
mockAddressBook = module.get<typeof mockAddressBook>(AddressBookService);

mockSlackMessage = module.get<typeof mockSlackMessage>(SlackMessageService);
mockRun = jest.fn().mockResolvedValue('test-tx-result');
mockPolymesh.identities.registerIdentity.mockResolvedValue({
run: mockRun,
Expand Down Expand Up @@ -167,6 +173,19 @@ 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(mockSlackMessage.sendMessage).toHaveBeenCalledWith({
header: 'A Netki Onboarding application requires review',
body: ':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 +267,7 @@ describe('cddProcessor', () => {
business: {
parent_business: 'someBusinessId',
status: 'accepted',
name: 'Some Business Name',
},
},
},
Expand All @@ -268,6 +288,35 @@ 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(mockSlackMessage.sendMessage).toHaveBeenCalledWith({
header: 'New Netki business application requires review',
body: ':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(mockSlackMessage.sendMessage).toHaveBeenCalledWith({
header: 'Netki business application on requires review',
body: ':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
155 changes: 94 additions & 61 deletions apps/cdd-backend/src/cdd-worker/cdd.processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@ import {
} from './types';
import { Identity } from '@polymeshassociation/polymesh-sdk/types';
import { NetkiBusinessApplicationModel } from '../app-redis/models/netki-business-application.model';
import { SlackMessageService } from '../slack/slackMessage.service';

@Processor()
export class CddProcessor {
constructor(
private readonly polymesh: Polymesh,
private readonly signerLookup: AddressBookService,
private readonly redis: AppRedisService,
private readonly slackMessageService: SlackMessageService,
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger
) {}

Expand All @@ -48,43 +50,59 @@ 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.slackMessageService.sendMessage({
header: 'New Netki business application requires review',
body: `:bell: Netki Business CDD application received from *${name}*.\n:mag: Please review and process in the Netki dashboard.`,
});
break;

case 'hold':
await this.slackMessageService.sendMessage({
header: 'Netki business application on requires review',
body: `: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 +127,65 @@ 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'
);
}
break;

await this.redis.setBusinessIdToAddress(
business,
businessApplication.address
);
} else {
case 'hold':
await this.slackMessageService.sendMessage({
header: 'A Netki Onboarding application requires review',
body: `: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
19 changes: 19 additions & 0 deletions apps/cdd-backend/src/config/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,20 @@ const configZ = z
.describe('Polymesh chain node ws url'),
})
.describe('Polymesh chain related config'),

slackApp: z
.object({
signingSecret: z
.string()
.describe('Slack signing secret for verifying requests'),
botToken: z
.string()
.describe('Slack bot OAuth token for authentication'),
channel: z
.string()
.describe('Slack channel ID where messages will be posted'),
})
.describe('Slack app related config'),
})
.describe('config values needed for "worker" mode');

Expand Down Expand Up @@ -64,6 +78,11 @@ export const workerEnvConfig = (): WorkerConfig => {
polymesh: {
nodeUrl: process.env.MESH_NODE_URL,
},
slackApp: {
signingSecret: process.env.SLACK_SIGNING_SECRET || '',
botToken: process.env.SLACK_BOT_TOKEN || '',
channel: process.env.SLACK_CHANNEL || '',
},
};

return configZ.parse(rawConfig);
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
24 changes: 24 additions & 0 deletions apps/cdd-backend/src/slack/slackMessage.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { workerEnvConfig } from '../config/worker';
import { SlackMessageService } from './slackMessage.service';
import { App as SlackApp } from '@slack/bolt';

@Module({
imports: [ConfigModule.forFeature(() => workerEnvConfig())],
providers: [
{
provide: SlackApp,
inject: [ConfigService],
useFactory: (config: ConfigService) => {
return new SlackApp({
signingSecret: config.getOrThrow('slackApp.signingSecret'),
token: config.getOrThrow('slackApp.botToken'),
});
},
},
SlackMessageService,
],
exports: [SlackMessageService],
})
export class SlackMessageModule {}
Loading
Loading