From e99e580d5e913dd6678afd9416a4c07e2ce85c1d Mon Sep 17 00:00:00 2001 From: BryannYeap <e0543723@u.nus.edu> Date: Sat, 15 Oct 2022 08:01:13 +0800 Subject: [PATCH] [offers][feat] Add get offers analysis API --- ...ofileAnalysis.tsx => generateAnalysis.tsx} | 4 +- .../src/pages/offers/test/getAnalysis.tsx | 14 + .../router/offers/offers-analysis-router.ts | 666 +++++++++++------- 3 files changed, 417 insertions(+), 267 deletions(-) rename apps/portal/src/pages/offers/test/{profileAnalysis.tsx => generateAnalysis.tsx} (79%) create mode 100644 apps/portal/src/pages/offers/test/getAnalysis.tsx diff --git a/apps/portal/src/pages/offers/test/profileAnalysis.tsx b/apps/portal/src/pages/offers/test/generateAnalysis.tsx similarity index 79% rename from apps/portal/src/pages/offers/test/profileAnalysis.tsx rename to apps/portal/src/pages/offers/test/generateAnalysis.tsx index 9420653e..fcb969bd 100644 --- a/apps/portal/src/pages/offers/test/profileAnalysis.tsx +++ b/apps/portal/src/pages/offers/test/generateAnalysis.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { trpc } from '~/utils/trpc'; -function profileAnalysis() { +function GenerateAnalysis() { const analysis = trpc.useQuery([ 'offers.analysis.generate', { profileId: 'cl98yxuei002htx1s8lrmwzmy' }, @@ -11,4 +11,4 @@ function profileAnalysis() { return <div>{JSON.stringify(analysis.data)}</div>; } -export default profileAnalysis; +export default GenerateAnalysis; diff --git a/apps/portal/src/pages/offers/test/getAnalysis.tsx b/apps/portal/src/pages/offers/test/getAnalysis.tsx new file mode 100644 index 00000000..d7ae6797 --- /dev/null +++ b/apps/portal/src/pages/offers/test/getAnalysis.tsx @@ -0,0 +1,14 @@ +import React from 'react'; + +import { trpc } from '~/utils/trpc'; + +function GetAnalysis() { + const analysis = trpc.useQuery([ + 'offers.analysis.get', + { profileId: 'cl98yxuei002htx1s8lrmwzmy' }, + ]); + + return <div>{JSON.stringify(analysis.data)}</div>; +} + +export default GetAnalysis; diff --git a/apps/portal/src/server/router/offers/offers-analysis-router.ts b/apps/portal/src/server/router/offers/offers-analysis-router.ts index e71b3963..7ed9028d 100644 --- a/apps/portal/src/server/router/offers/offers-analysis-router.ts +++ b/apps/portal/src/server/router/offers/offers-analysis-router.ts @@ -121,12 +121,7 @@ const specificAnalysisDtoMapper = ( const highestOfferDtoMapper = ( offer: OffersOffer & { OffersFullTime: - | (OffersFullTime & { - baseSalary: OffersCurrency; - bonus: OffersCurrency; - stocks: OffersCurrency; - totalCompensation: OffersCurrency; - }) + | (OffersFullTime & { totalCompensation: OffersCurrency }) | null; OffersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null; company: Company; @@ -146,279 +141,344 @@ const highestOfferDtoMapper = ( }; }; -export const offersAnalysisRouter = createRouter().query('generate', { - input: z.object({ - profileId: z.string(), - }), - async resolve({ ctx, input }) { - await ctx.prisma.offersAnalysis.deleteMany({ - where: { - profileId: input.profileId, - }, - }); +const profileAnalysisDtoMapper = ( + analysisId: string, + profileId: string, + overallHighestOffer: OffersOffer & { + OffersFullTime: + | (OffersFullTime & { totalCompensation: OffersCurrency }) + | null; + OffersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null; + company: Company; + profile: OffersProfile & { background: OffersBackground | null }; + }, + noOfSimilarOffers: number, + overallPercentile: number, + topPercentileOffers: Array<any>, + noOfSimilarCompanyOffers: number, + companyPercentile: number, + topPercentileCompanyOffers: Array<any>, +) => { + return { + companyAnalysis: specificAnalysisDtoMapper( + noOfSimilarCompanyOffers, + companyPercentile, + topPercentileCompanyOffers, + ), + id: analysisId, + overallAnalysis: specificAnalysisDtoMapper( + noOfSimilarOffers, + overallPercentile, + topPercentileOffers, + ), + overallHighestOffer: highestOfferDtoMapper(overallHighestOffer), + profileId, + }; +}; - const offers = await ctx.prisma.offersOffer.findMany({ - include: { - OffersFullTime: { - include: { - baseSalary: true, - bonus: true, - stocks: true, - totalCompensation: true, - }, +export const offersAnalysisRouter = createRouter() + .query('generate', { + input: z.object({ + profileId: z.string(), + }), + async resolve({ ctx, input }) { + await ctx.prisma.offersAnalysis.deleteMany({ + where: { + profileId: input.profileId, }, - OffersIntern: { - include: { - monthlySalary: true, + }); + + const offers = await ctx.prisma.offersOffer.findMany({ + include: { + OffersFullTime: { + include: { + baseSalary: true, + bonus: true, + stocks: true, + totalCompensation: true, + }, }, - }, - company: true, - profile: { - include: { - background: true, + OffersIntern: { + include: { + monthlySalary: true, + }, }, - }, - }, - orderBy: [ - { - OffersFullTime: { - totalCompensation: { - value: 'desc', + company: true, + profile: { + include: { + background: true, }, }, }, - { - OffersIntern: { - monthlySalary: { - value: 'desc', + orderBy: [ + { + OffersFullTime: { + totalCompensation: { + value: 'desc', + }, + }, + }, + { + OffersIntern: { + monthlySalary: { + value: 'desc', + }, }, }, + ], + where: { + profileId: input.profileId, }, - ], - where: { - profileId: input.profileId, - }, - }); - - if (!offers || offers.length === 0) { - throw new TRPCError({ - code: 'NOT_FOUND', - message: 'No offers found on this profile', }); - } - const overallHighestOffer = offers[0]; + if (!offers || offers.length === 0) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: 'No offers found on this profile', + }); + } - // TODO: Shift yoe to background to make it mandatory - if ( - !overallHighestOffer.profile.background || - !overallHighestOffer.profile.background.totalYoe - ) { - throw new TRPCError({ - code: 'BAD_REQUEST', - message: 'Cannot analyse without YOE', - }); - } + const overallHighestOffer = offers[0]; - const yoe = overallHighestOffer.profile.background.totalYoe as number; + // TODO: Shift yoe to background to make it mandatory + if ( + !overallHighestOffer.profile.background || + !overallHighestOffer.profile.background.totalYoe + ) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'Cannot analyse without YOE', + }); + } - let similarOffers = await ctx.prisma.offersOffer.findMany({ - include: { - OffersFullTime: { - include: { - totalCompensation: true, + const yoe = overallHighestOffer.profile.background.totalYoe as number; + + let similarOffers = await ctx.prisma.offersOffer.findMany({ + include: { + OffersFullTime: { + include: { + totalCompensation: true, + }, }, - }, - OffersIntern: { - include: { - monthlySalary: true, + OffersIntern: { + include: { + monthlySalary: true, + }, }, - }, - company: true, - profile: { - include: { - background: { - include: { - experiences: { - include: { - company: true, + company: true, + profile: { + include: { + background: { + include: { + experiences: { + include: { + company: true, + }, }, }, }, }, }, }, - }, - orderBy: [ - { - OffersFullTime: { - totalCompensation: { - value: 'desc', - }, - }, - }, - { - OffersIntern: { - monthlySalary: { - value: 'desc', - }, - }, - }, - ], - where: { - AND: [ + orderBy: [ { - location: overallHighestOffer.location, - }, - { - OR: [ - { - OffersFullTime: { - level: overallHighestOffer.OffersFullTime?.level, - specialization: - overallHighestOffer.OffersFullTime?.specialization, - }, - OffersIntern: { - specialization: - overallHighestOffer.OffersIntern?.specialization, - }, + OffersFullTime: { + totalCompensation: { + value: 'desc', }, - ], + }, }, { - profile: { - background: { - AND: [ - { - totalYoe: { - gte: Math.max(yoe - 1, 0), - lte: yoe + 1, - }, - }, - ], + OffersIntern: { + monthlySalary: { + value: 'desc', }, }, }, ], - }, - }); + where: { + AND: [ + { + location: overallHighestOffer.location, + }, + { + OR: [ + { + OffersFullTime: { + level: overallHighestOffer.OffersFullTime?.level, + specialization: + overallHighestOffer.OffersFullTime?.specialization, + }, + OffersIntern: { + specialization: + overallHighestOffer.OffersIntern?.specialization, + }, + }, + ], + }, + { + profile: { + background: { + AND: [ + { + totalYoe: { + gte: Math.max(yoe - 1, 0), + lte: yoe + 1, + }, + }, + ], + }, + }, + }, + ], + }, + }); - let similarCompanyOffers = similarOffers.filter( - (offer: { companyId: string }) => - offer.companyId === overallHighestOffer.companyId, - ); + let similarCompanyOffers = similarOffers.filter( + (offer: { companyId: string }) => + offer.companyId === overallHighestOffer.companyId, + ); - // CALCULATE PERCENTILES - const overallIndex = binarySearchOfferPercentile( - overallHighestOffer, - similarOffers, - ); - const overallPercentile = overallIndex / similarOffers.length; + // CALCULATE PERCENTILES + const overallIndex = binarySearchOfferPercentile( + overallHighestOffer, + similarOffers, + ); + const overallPercentile = overallIndex / similarOffers.length; - const companyIndex = binarySearchOfferPercentile( - overallHighestOffer, - similarCompanyOffers, - ); - const companyPercentile = companyIndex / similarCompanyOffers.length; + const companyIndex = binarySearchOfferPercentile( + overallHighestOffer, + similarCompanyOffers, + ); + const companyPercentile = companyIndex / similarCompanyOffers.length; - // FIND TOP >=90 PERCENTILE OFFERS - similarOffers = similarOffers.filter( - (offer: { id: string }) => offer.id !== overallHighestOffer.id, - ); - similarCompanyOffers = similarCompanyOffers.filter( - (offer: { id: string }) => offer.id !== overallHighestOffer.id, - ); + // FIND TOP >=90 PERCENTILE OFFERS + similarOffers = similarOffers.filter( + (offer: { id: string }) => offer.id !== overallHighestOffer.id, + ); + similarCompanyOffers = similarCompanyOffers.filter( + (offer: { id: string }) => offer.id !== overallHighestOffer.id, + ); - const noOfSimilarOffers = similarOffers.length; - const similarOffers90PercentileIndex = - Math.floor(noOfSimilarOffers * 0.9) - 1; - const topPercentileOffers = - noOfSimilarOffers > 1 - ? similarOffers.slice( - similarOffers90PercentileIndex, - similarOffers90PercentileIndex + 2, - ) - : similarOffers; + const noOfSimilarOffers = similarOffers.length; + const similarOffers90PercentileIndex = + Math.floor(noOfSimilarOffers * 0.9) - 1; + const topPercentileOffers = + noOfSimilarOffers > 1 + ? similarOffers.slice( + similarOffers90PercentileIndex, + similarOffers90PercentileIndex + 2, + ) + : similarOffers; - const noOfSimilarCompanyOffers = similarCompanyOffers.length; - const similarCompanyOffers90PercentileIndex = - Math.floor(noOfSimilarCompanyOffers * 0.9) - 1; - const topPercentileCompanyOffers = - noOfSimilarCompanyOffers > 1 - ? similarCompanyOffers.slice( - similarCompanyOffers90PercentileIndex, - similarCompanyOffers90PercentileIndex + 2, - ) - : similarCompanyOffers; + const noOfSimilarCompanyOffers = similarCompanyOffers.length; + const similarCompanyOffers90PercentileIndex = + Math.floor(noOfSimilarCompanyOffers * 0.9) - 1; + const topPercentileCompanyOffers = + noOfSimilarCompanyOffers > 1 + ? similarCompanyOffers.slice( + similarCompanyOffers90PercentileIndex, + similarCompanyOffers90PercentileIndex + 2, + ) + : similarCompanyOffers; - const analysis = await ctx.prisma.offersAnalysis.create({ - data: { - companyPercentile, - noOfSimilarCompanyOffers, - noOfSimilarOffers, - overallHighestOffer: { - connect: { - id: overallHighestOffer.id, + const analysis = await ctx.prisma.offersAnalysis.create({ + data: { + companyPercentile, + noOfSimilarCompanyOffers, + noOfSimilarOffers, + overallHighestOffer: { + connect: { + id: overallHighestOffer.id, + }, }, - }, - overallPercentile, - profile: { - connect: { - id: input.profileId, + overallPercentile, + profile: { + connect: { + id: input.profileId, + }, + }, + topCompanyOffers: { + connect: topPercentileCompanyOffers.map((offer) => { + return { id: offer.id }; + }), + }, + topOverallOffers: { + connect: topPercentileOffers.map((offer) => { + return { id: offer.id }; + }), }, }, - topCompanyOffers: { - connect: topPercentileCompanyOffers.map((offer) => { - return { id: offer.id }; - }), - }, - topOverallOffers: { - connect: topPercentileOffers.map((offer) => { - return { id: offer.id }; - }), - }, - }, - include: { - overallHighestOffer: { - include: { - OffersFullTime: { - include: { - totalCompensation: true, + include: { + overallHighestOffer: { + include: { + OffersFullTime: { + include: { + totalCompensation: true, + }, }, - }, - OffersIntern: { - include: { - monthlySalary: true, + OffersIntern: { + include: { + monthlySalary: true, + }, }, - }, - company: true, - profile: { - include: { - background: true, + company: true, + profile: { + include: { + background: true, + }, }, }, }, - }, - topCompanyOffers: { - include: { - OffersFullTime: { - include: { - totalCompensation: true, + topCompanyOffers: { + include: { + OffersFullTime: { + include: { + totalCompensation: true, + }, }, - }, - OffersIntern: { - include: { - monthlySalary: true, + OffersIntern: { + include: { + monthlySalary: true, + }, + }, + company: true, + profile: { + include: { + background: { + include: { + experiences: { + include: { + company: true, + }, + }, + }, + }, + }, }, }, - company: true, - profile: { - include: { - background: { - include: { - experiences: { - include: { - company: true, + }, + topOverallOffers: { + include: { + OffersFullTime: { + include: { + totalCompensation: true, + }, + }, + OffersIntern: { + include: { + monthlySalary: true, + }, + }, + company: true, + profile: { + include: { + background: { + include: { + experiences: { + include: { + company: true, + }, }, }, }, @@ -427,26 +487,97 @@ export const offersAnalysisRouter = createRouter().query('generate', { }, }, }, - topOverallOffers: { - include: { - OffersFullTime: { - include: { - totalCompensation: true, + }); + + return profileAnalysisDtoMapper( + analysis.id, + analysis.profileId, + overallHighestOffer, + noOfSimilarOffers, + overallPercentile, + topPercentileOffers, + noOfSimilarCompanyOffers, + companyPercentile, + topPercentileCompanyOffers, + ); + }, + }) + .query('get', { + input: z.object({ + profileId: z.string(), + }), + async resolve({ ctx, input }) { + const analysis = await ctx.prisma.offersAnalysis.findFirst({ + include: { + overallHighestOffer: { + include: { + OffersFullTime: { + include: { + totalCompensation: true, + }, + }, + OffersIntern: { + include: { + monthlySalary: true, + }, + }, + company: true, + profile: { + include: { + background: true, + }, }, }, - OffersIntern: { - include: { - monthlySalary: true, + }, + topCompanyOffers: { + include: { + OffersFullTime: { + include: { + totalCompensation: true, + }, + }, + OffersIntern: { + include: { + monthlySalary: true, + }, + }, + company: true, + profile: { + include: { + background: { + include: { + experiences: { + include: { + company: true, + }, + }, + }, + }, + }, }, }, - company: true, - profile: { - include: { - background: { - include: { - experiences: { - include: { - company: true, + }, + topOverallOffers: { + include: { + OffersFullTime: { + include: { + totalCompensation: true, + }, + }, + OffersIntern: { + include: { + monthlySalary: true, + }, + }, + company: true, + profile: { + include: { + background: { + include: { + experiences: { + include: { + company: true, + }, }, }, }, @@ -455,23 +586,28 @@ export const offersAnalysisRouter = createRouter().query('generate', { }, }, }, - }, - }); + where: { + profileId: input.profileId, + }, + }); - return { - companyAnalysis: specificAnalysisDtoMapper( - noOfSimilarCompanyOffers, - companyPercentile, - topPercentileCompanyOffers, - ), - id: analysis.id, - overallAnalysis: specificAnalysisDtoMapper( - noOfSimilarOffers, - overallPercentile, - topPercentileOffers, - ), - overallHighestOffer: highestOfferDtoMapper(overallHighestOffer), - profileId: analysis.profileId, - }; - }, -}); + if (!analysis) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: 'No analysis found on this profile', + }); + } + + return profileAnalysisDtoMapper( + analysis.id, + analysis.profileId, + analysis.overallHighestOffer, + analysis.noOfSimilarOffers, + analysis.overallPercentile, + analysis.topOverallOffers, + analysis.noOfSimilarCompanyOffers, + analysis.companyPercentile, + analysis.topCompanyOffers, + ); + }, + });