diff --git a/__tests__/notifications/index.ts b/__tests__/notifications/index.ts index 205e75422..d68b098eb 100644 --- a/__tests__/notifications/index.ts +++ b/__tests__/notifications/index.ts @@ -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, + }; + + 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', + }, + ]); + }); +}); diff --git a/__tests__/workers/opportunity/parseCVProfile.ts b/__tests__/workers/opportunity/parseCVProfile.ts index 7e049c5ff..7c0f78087 100644 --- a/__tests__/workers/opportunity/parseCVProfile.ts +++ b/__tests__/workers/opportunity/parseCVProfile.ts @@ -1,7 +1,7 @@ import { createGarmrMock, createMockBrokkrTransport, - expectSuccessfulTypedBackground, + invokeTypedNotificationWorker, saveFixtures, } from '../../helpers'; import { DataSource } from 'typeorm'; @@ -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; @@ -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); @@ -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); @@ -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); @@ -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); @@ -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); @@ -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); @@ -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); @@ -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); + }); }); diff --git a/src/notifications/common.ts b/src/notifications/common.ts index cbafa002e..76ee864b3 100644 --- a/src/notifications/common.ts +++ b/src/notifications/common.ts @@ -79,6 +79,7 @@ export enum NotificationType { PollResult = 'poll_result', PollResultAuthor = 'poll_result_author', WarmIntro = 'warm_intro', + ParsedCVProfile = 'parsed_cv_profile', } export enum NotificationPreferenceType { diff --git a/src/notifications/generate.ts b/src/notifications/generate.ts index ae315aff8..b279fd70c 100644 --- a/src/notifications/generate.ts +++ b/src/notifications/generate.ts @@ -206,6 +206,9 @@ export const notificationTitleMap: Record< `Your poll has ended! Check the results for: ${ctx.post.title}`, warm_intro: (ctx: NotificationWarmIntroContext) => `We just sent an intro email to you and ${ctx.recruiter.name} from ${ctx.organization.name}!`, + parsed_cv_profile: () => { + return `Your CV was successfully parsed and your experiences are added to your profile.`; + }, }; export const generateNotificationMap: Record< @@ -593,4 +596,15 @@ export const generateNotificationMap: Record< `We reached out to them and received a positive response. Our team will be here to assist you with anything you need. contact us`, ); }, + 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()); + }, }; diff --git a/src/workers/index.ts b/src/workers/index.ts index e33a2489a..11a5f60ee 100644 --- a/src/workers/index.ts +++ b/src/workers/index.ts @@ -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'; @@ -150,7 +149,6 @@ export const typedWorkers: BaseTypedWorker[] = [ extractCVMarkdown, candidateAcceptedOpportunitySlack, recruiterRejectedCandidateMatchEmail, - parseCVProfileWorker, ]; export const personalizedDigestWorkers: Worker[] = [ diff --git a/src/workers/newNotificationV2Mail.ts b/src/workers/newNotificationV2Mail.ts index 516700269..cfc582138 100644 --- a/src/workers/newNotificationV2Mail.ts +++ b/src/workers/newNotificationV2Mail.ts @@ -127,6 +127,7 @@ export const notificationToTemplateId: Record = { poll_result: '84', poll_result_author: '84', warm_intro: '85', + parsed_cv_profile: '', }; type TemplateData = Record & { @@ -1146,6 +1147,9 @@ const notificationToTemplateData: Record = { cc: recruiter?.email, }; }, + parsed_cv_profile: async () => { + return null; + }, }; const formatTemplateDate = (data: T): T => { diff --git a/src/workers/notifications/index.ts b/src/workers/notifications/index.ts index a5fdd3282..98b94f70d 100644 --- a/src/workers/notifications/index.ts +++ b/src/workers/notifications/index.ts @@ -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 @@ -122,6 +123,7 @@ const notificationWorkers: TypedNotificationWorker[] = [ pollResultAuthorNotification, pollResultNotification, warmIntroNotification, + parseCVProfileWorker, ]; export const workers = [...notificationWorkers.map(notificationWorkerToWorker)]; diff --git a/src/workers/opportunity/parseCVProfile.ts b/src/workers/opportunity/parseCVProfile.ts index f13b75601..e8b13a86c 100644 --- a/src/workers/opportunity/parseCVProfile.ts +++ b/src/workers/opportunity/parseCVProfile.ts @@ -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) { @@ -34,14 +30,11 @@ export const parseCVProfileWorker: TypedWorker<'api.v1.candidate-preference-upda return; } - const user: Pick | null = await con - .getRepository(User) - .findOne({ - select: ['flags'], - where: { - id: userId, - }, - }); + const user = await con.getRepository(User).findOne({ + where: { + id: userId, + }, + }); if (!user) { return; @@ -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(