Skip to content
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
34 changes: 34 additions & 0 deletions __tests__/notifications/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2170,3 +2170,37 @@ describe('warm intro notifications', () => {
expect(actual.attachments.length).toEqual(0);
});
});

describe('parsed_cv_profile notifications', () => {
beforeEach(async () => {
jest.resetAllMocks();
await saveFixtures(con, User, usersFixture);
});

it('should notify when parsed CV profile is ready', async () => {
const type = NotificationType.ParsedCVProfile;
const ctx: NotificationUserContext = {
userIds: ['1'],
user: usersFixture[0] as Reference<User>,
};

const actual = generateNotificationV2(type, ctx);
expect(actual.notification.type).toEqual(type);
expect(actual.userIds).toEqual(['1']);
expect(actual.notification.public).toEqual(true);
expect(actual.notification.referenceId).toEqual('1');
expect(actual.notification.targetUrl).toEqual(
'http://localhost:5002/idoshamun',
);
expect(actual.attachments!.length).toEqual(0);
expect(actual.avatars).toEqual([
{
image: 'https://daily.dev/ido.jpg',
name: 'Ido',
referenceId: '1',
targetUrl: 'http://localhost:5002/idoshamun',
type: 'user',
},
]);
});
});
121 changes: 88 additions & 33 deletions __tests__/workers/opportunity/parseCVProfile.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {
createGarmrMock,
createMockBrokkrTransport,
expectSuccessfulTypedBackground,
invokeTypedNotificationWorker,
saveFixtures,
} from '../../helpers';
import { DataSource } from 'typeorm';
Expand All @@ -15,6 +15,8 @@ import type { ServiceClient } from '../../../src/types';
import * as brokkrCommon from '../../../src/common/brokkr';
import { UserExperience } from '../../../src/entity/user/experiences/UserExperience';
import { getSecondsTimestamp, updateFlagsStatement } from '../../../src/common';
import type { NotificationUserContext } from '../../../src/notifications';
import { NotificationType } from '../../../src/notifications/common';

let con: DataSource;

Expand Down Expand Up @@ -70,10 +72,13 @@ describe('parseCVProfile worker', () => {
'parseCV',
);

await expectSuccessfulTypedBackground<'api.v1.candidate-preference-updated'>(
worker,
payload,
);
const result =
await invokeTypedNotificationWorker<'api.v1.candidate-preference-updated'>(
worker,
payload,
);

expect(result).toBeDefined();

expect(parseCVSpy).toHaveBeenCalledTimes(1);

Expand Down Expand Up @@ -101,10 +106,13 @@ describe('parseCVProfile worker', () => {
'parseCV',
);

await expectSuccessfulTypedBackground<'api.v1.candidate-preference-updated'>(
worker,
payload,
);
const result =
await invokeTypedNotificationWorker<'api.v1.candidate-preference-updated'>(
worker,
payload,
);

expect(result).toBeUndefined();

expect(parseCVSpy).toHaveBeenCalledTimes(0);

Expand Down Expand Up @@ -133,10 +141,13 @@ describe('parseCVProfile worker', () => {
'parseCV',
);

await expectSuccessfulTypedBackground<'api.v1.candidate-preference-updated'>(
worker,
payload,
);
const result =
await invokeTypedNotificationWorker<'api.v1.candidate-preference-updated'>(
worker,
payload,
);

expect(result).toBeUndefined();

expect(parseCVSpy).toHaveBeenCalledTimes(0);

Expand Down Expand Up @@ -165,10 +176,13 @@ describe('parseCVProfile worker', () => {
'parseCV',
);

await expectSuccessfulTypedBackground<'api.v1.candidate-preference-updated'>(
worker,
payload,
);
const result =
await invokeTypedNotificationWorker<'api.v1.candidate-preference-updated'>(
worker,
payload,
);

expect(result).toBeUndefined();

expect(parseCVSpy).toHaveBeenCalledTimes(0);

Expand Down Expand Up @@ -198,10 +212,13 @@ describe('parseCVProfile worker', () => {
'parseCV',
);

await expectSuccessfulTypedBackground<'api.v1.candidate-preference-updated'>(
worker,
payload,
);
const result =
await invokeTypedNotificationWorker<'api.v1.candidate-preference-updated'>(
worker,
payload,
);

expect(result).toBeUndefined();

expect(parseCVSpy).toHaveBeenCalledTimes(0);

Expand Down Expand Up @@ -240,10 +257,13 @@ describe('parseCVProfile worker', () => {
'parseCV',
);

await expectSuccessfulTypedBackground<'api.v1.candidate-preference-updated'>(
worker,
payload,
);
const result =
await invokeTypedNotificationWorker<'api.v1.candidate-preference-updated'>(
worker,
payload,
);

expect(result).toBeUndefined();

expect(parseCVSpy).toHaveBeenCalledTimes(0);

Expand Down Expand Up @@ -273,10 +293,13 @@ describe('parseCVProfile worker', () => {
'parseCV',
);

await expectSuccessfulTypedBackground<'api.v1.candidate-preference-updated'>(
worker,
payload,
);
const result =
await invokeTypedNotificationWorker<'api.v1.candidate-preference-updated'>(
worker,
payload,
);

expect(result).toBeUndefined();

expect(parseCVSpy).toHaveBeenCalledTimes(1);

Expand Down Expand Up @@ -320,14 +343,46 @@ describe('parseCVProfile worker', () => {
'parseCV',
);

await expectSuccessfulTypedBackground<'api.v1.candidate-preference-updated'>(
worker,
payload,
);
const result =
await invokeTypedNotificationWorker<'api.v1.candidate-preference-updated'>(
worker,
payload,
);

expect(result).toBeUndefined();

expect(parseCVSpy).toHaveBeenCalledTimes(1);

const user = await con.getRepository(User).findOneBy({ id: userId });
expect(user?.flags.lastCVParseAt).toBe(parseDate.toISOString());
});

it('should send notification after successful parsing', async () => {
const userId = '1-pcpw';

const payload = new CandidatePreferenceUpdated({
payload: {
userId,
cv: {
blob: userId,
bucket: 'bucket-test',
lastModified: getSecondsTimestamp(new Date()),
},
},
});

const result =
await invokeTypedNotificationWorker<'api.v1.candidate-preference-updated'>(
worker,
payload,
);

expect(result!.length).toEqual(1);
expect(result![0].type).toEqual(NotificationType.ParsedCVProfile);

const postContext = result![0].ctx as NotificationUserContext;

expect(postContext.userIds).toEqual(['1-pcpw']);
expect(postContext.user.id).toEqual(userId);
});
});
1 change: 1 addition & 0 deletions src/notifications/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export enum NotificationType {
PollResult = 'poll_result',
PollResultAuthor = 'poll_result_author',
WarmIntro = 'warm_intro',
ParsedCVProfile = 'parsed_cv_profile',
}

export enum NotificationPreferenceType {
Expand Down
14 changes: 14 additions & 0 deletions src/notifications/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,9 @@ export const notificationTitleMap: Record<
`<b>Your poll has ended!</b> Check the results for: <b>${ctx.post.title}</b>`,
warm_intro: (ctx: NotificationWarmIntroContext) =>
`We just sent an intro email to you and <b>${ctx.recruiter.name}</b> from <b>${ctx.organization.name}</b>!`,
parsed_cv_profile: () => {
return `Your CV was successfully parsed and your experiences are added to <u>your profile</u>.`;
},
};

export const generateNotificationMap: Record<
Expand Down Expand Up @@ -593,4 +596,15 @@ export const generateNotificationMap: Record<
`<span>We reached out to them and received a positive response. Our team will be here to assist you with anything you need. <a href="mailto:[email protected]" target="_blank" class="text-text-link">contact us</a></span>`,
);
},
parsed_cv_profile: (
builder: NotificationBuilder,
ctx: NotificationUserContext,
) => {
return builder
.icon(NotificationIcon.Bell)
.referenceUser(ctx.user)
.avatarUser(ctx.user)
.targetUser(ctx.user)
.uniqueKey(new Date().toISOString());
},
};
2 changes: 0 additions & 2 deletions src/workers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ import { storeCandidateApplicationScore } from './opportunity/storeCandidateAppl
import { extractCVMarkdown } from './extractCVMarkdown';
import candidateAcceptedOpportunitySlack from './candidateAcceptedOpportunitySlack';
import recruiterRejectedCandidateMatchEmail from './recruiterRejectedCandidateMatchEmail';
import { parseCVProfileWorker } from './opportunity/parseCVProfile';

export { Worker } from './worker';

Expand Down Expand Up @@ -150,7 +149,6 @@ export const typedWorkers: BaseTypedWorker<any>[] = [
extractCVMarkdown,
candidateAcceptedOpportunitySlack,
recruiterRejectedCandidateMatchEmail,
parseCVProfileWorker,
];

export const personalizedDigestWorkers: Worker[] = [
Expand Down
4 changes: 4 additions & 0 deletions src/workers/newNotificationV2Mail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ export const notificationToTemplateId: Record<NotificationType, string> = {
poll_result: '84',
poll_result_author: '84',
warm_intro: '85',
parsed_cv_profile: '',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good opportunity to send email and maybe call out why this profile is important (job etc) nice to have later on

Copy link
Contributor Author

@capJavert capJavert Nov 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trigger is the worker, I just moved it to notification worker so I can easily send notification from it.

};

type TemplateData = Record<string, unknown> & {
Expand Down Expand Up @@ -1146,6 +1147,9 @@ const notificationToTemplateData: Record<NotificationType, TemplateDataFunc> = {
cc: recruiter?.email,
};
},
parsed_cv_profile: async () => {
return null;
},
};

const formatTemplateDate = <T extends TemplateData>(data: T): T => {
Expand Down
2 changes: 2 additions & 0 deletions src/workers/notifications/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { pollResultAuthorNotification } from './pollResultAuthorNotification';
import { pollResultNotification } from './pollResultNotification';
import { articleNewCommentCommentCommented } from './articleNewCommentCommentCommented';
import { warmIntroNotification } from './warmIntroNotification';
import { parseCVProfileWorker } from '../opportunity/parseCVProfile';

export function notificationWorkerToWorker(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down Expand Up @@ -122,6 +123,7 @@ const notificationWorkers: TypedNotificationWorker<any>[] = [
pollResultAuthorNotification,
pollResultNotification,
warmIntroNotification,
parseCVProfileWorker,
];

export const workers = [...notificationWorkers.map(notificationWorkerToWorker)];
39 changes: 21 additions & 18 deletions src/workers/opportunity/parseCVProfile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,20 @@ import {
BrokkrParseRequest,
CandidatePreferenceUpdated,
} from '@dailydotdev/schema';
import type { TypedWorker } from '../worker';
import type { TypedNotificationWorker } from '../worker';
import { User } from '../../entity/user/User';
import { getBrokkrClient } from '../../common/brokkr';
import { isProd, updateFlagsStatement } from '../../common/utils';
import { updateFlagsStatement } from '../../common/utils';
import { importUserExperienceFromJSON } from '../../common/profile/import';
import { logger } from '../../logger';
import { NotificationType } from '../../notifications/common';
import type { NotificationUserContext } from '../../notifications';

export const parseCVProfileWorker: TypedWorker<'api.v1.candidate-preference-updated'> =
export const parseCVProfileWorker: TypedNotificationWorker<'api.v1.candidate-preference-updated'> =
{
subscription: 'api.parse-cv-profile',
parseMessage: ({ data }) => CandidatePreferenceUpdated.fromBinary(data),
handler: async ({ data }, con) => {
if (isProd) {
// disabled for now so I can merge the code and will enable after backfill

return;
}

handler: async (data, con) => {
const { userId, cv } = data.payload || {};

if (!cv?.blob || !cv?.bucket) {
Expand All @@ -34,14 +30,11 @@ export const parseCVProfileWorker: TypedWorker<'api.v1.candidate-preference-upda
return;
}

const user: Pick<User, 'flags'> | null = await con
.getRepository(User)
.findOne({
select: ['flags'],
where: {
id: userId,
},
});
const user = await con.getRepository(User).findOne({
where: {
id: userId,
},
});

if (!user) {
return;
Expand Down Expand Up @@ -94,6 +87,16 @@ export const parseCVProfileWorker: TypedWorker<'api.v1.candidate-preference-upda
userId,
transaction: true,
});

return [
{
type: NotificationType.ParsedCVProfile,
ctx: {
user,
userIds: [userId],
} as NotificationUserContext,
},
];
} catch (error) {
// revert to previous date on error
await con.getRepository(User).update(
Expand Down
Loading