From c12c318a0b85890d55d74c7a262c5a48dcc1c4a7 Mon Sep 17 00:00:00 2001 From: BryannYeap <e0543723@u.nus.edu> Date: Sat, 15 Oct 2022 05:04:38 +0800 Subject: [PATCH] [offers][chore] Save the generate offer profile analysis into db --- .../migrations/20221014205230_/migration.sql | 3 + apps/portal/prisma/schema.prisma | 4 +- .../src/pages/offers/test/createProfile.tsx | 18 +- .../src/pages/offers/test/profileAnalysis.tsx | 2 +- .../router/offers/offers-analysis-router.ts | 155 +++++++++++++++--- 5 files changed, 149 insertions(+), 33 deletions(-) create mode 100644 apps/portal/prisma/migrations/20221014205230_/migration.sql diff --git a/apps/portal/prisma/migrations/20221014205230_/migration.sql b/apps/portal/prisma/migrations/20221014205230_/migration.sql new file mode 100644 index 00000000..aec827dc --- /dev/null +++ b/apps/portal/prisma/migrations/20221014205230_/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "OffersAnalysis" ALTER COLUMN "overallPercentile" SET DATA TYPE DOUBLE PRECISION, +ALTER COLUMN "companyPercentile" SET DATA TYPE DOUBLE PRECISION; diff --git a/apps/portal/prisma/schema.prisma b/apps/portal/prisma/schema.prisma index e30230bb..268d8d27 100644 --- a/apps/portal/prisma/schema.prisma +++ b/apps/portal/prisma/schema.prisma @@ -365,12 +365,12 @@ model OffersAnalysis { offerId String @unique // OVERALL - overallPercentile Int + overallPercentile Float noOfSimilarOffers Int topOverallOffers OffersOffer[] @relation("TopOverallOffers") // Company - companyPercentile Int + companyPercentile Float noOfSimilarCompanyOffers Int topCompanyOffers OffersOffer[] @relation("TopCompanyOffers") } diff --git a/apps/portal/src/pages/offers/test/createProfile.tsx b/apps/portal/src/pages/offers/test/createProfile.tsx index 44740940..f9140452 100644 --- a/apps/portal/src/pages/offers/test/createProfile.tsx +++ b/apps/portal/src/pages/offers/test/createProfile.tsx @@ -102,7 +102,7 @@ function Test() { ], experiences: [ { - companyId: 'cl95u79f000007im531ysjg79', + companyId: 'cl98yuqk80007txhgjtjp8fk4', durationInMonths: 24, jobType: 'FULLTIME', level: 'Junior', @@ -151,7 +151,7 @@ function Test() { }, }, // Comments: '', - companyId: 'cl95u79f000007im531ysjg79', + companyId: 'cl98yuqk80007txhgjtjp8fk4', jobType: 'FULLTIME', location: 'Singapore, Singapore', monthYearReceived: new Date('2022-09-30T07:58:54.000Z'), @@ -180,7 +180,7 @@ function Test() { }, }, comments: undefined, - companyId: 'cl95u79f000007im531ysjg79', + companyId: 'cl98yuqk80007txhgjtjp8fk4', jobType: 'FULLTIME', location: 'Singapore, Singapore', monthYearReceived: new Date('2022-09-30T07:58:54.000Z'), @@ -261,7 +261,7 @@ function Test() { slug: 'meta', updatedAt: new Date('2022-10-12T16:19:05.196Z'), }, - companyId: 'cl95u79f000007im531ysjg79', + companyId: 'cl98yuqk80007txhgjtjp8fk4', durationInMonths: 24, id: 'cl96stky6002iw32gpt6t87s2', jobType: 'FULLTIME', @@ -368,7 +368,7 @@ function Test() { slug: 'meta', updatedAt: new Date('2022-10-12T16:19:05.196Z'), }, - companyId: 'cl95u79f000007im531ysjg79', + companyId: 'cl98yuqk80007txhgjtjp8fk4', id: 'cl976t4de00047iygl0zbce11', jobType: 'FULLTIME', location: 'Singapore, Singapore', @@ -421,7 +421,7 @@ function Test() { slug: 'meta', updatedAt: new Date('2022-10-12T16:19:05.196Z'), }, - companyId: 'cl95u79f000007im531ysjg79', + companyId: 'cl98yuqk80007txhgjtjp8fk4', id: 'cl96stky80031w32gau9mu1gs', jobType: 'FULLTIME', location: 'Singapore, Singapore', @@ -474,7 +474,7 @@ function Test() { slug: 'meta', updatedAt: new Date('2022-10-12T16:19:05.196Z'), }, - companyId: 'cl95u79f000007im531ysjg79', + companyId: 'cl98yuqk80007txhgjtjp8fk4', id: 'cl96stky9003bw32gc3l955vr', jobType: 'FULLTIME', location: 'Singapore, Singapore', @@ -527,7 +527,7 @@ function Test() { slug: 'meta', updatedAt: new Date('2022-10-12T16:19:05.196Z'), }, - companyId: 'cl95u79f000007im531ysjg79', + companyId: 'cl98yuqk80007txhgjtjp8fk4', id: 'cl976wf28000t7iyga4noyz7s', jobType: 'FULLTIME', location: 'Singapore, Singapore', @@ -580,7 +580,7 @@ function Test() { slug: 'meta', updatedAt: new Date('2022-10-12T16:19:05.196Z'), }, - companyId: 'cl95u79f000007im531ysjg79', + companyId: 'cl98yuqk80007txhgjtjp8fk4', id: 'cl96tbb3o0051w32gjrpaiiit', jobType: 'FULLTIME', location: 'Singapore, Singapore', diff --git a/apps/portal/src/pages/offers/test/profileAnalysis.tsx b/apps/portal/src/pages/offers/test/profileAnalysis.tsx index 34f55d26..9420653e 100644 --- a/apps/portal/src/pages/offers/test/profileAnalysis.tsx +++ b/apps/portal/src/pages/offers/test/profileAnalysis.tsx @@ -5,7 +5,7 @@ import { trpc } from '~/utils/trpc'; function profileAnalysis() { const analysis = trpc.useQuery([ 'offers.analysis.generate', - { profileId: 'cl96stky5002ew32gx2kale2x' }, + { profileId: 'cl98yxuei002htx1s8lrmwzmy' }, ]); return <div>{JSON.stringify(analysis.data)}</div>; 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 c620cde5..882e84c3 100644 --- a/apps/portal/src/server/router/offers/offers-analysis-router.ts +++ b/apps/portal/src/server/router/offers/offers-analysis-router.ts @@ -8,6 +8,7 @@ import type { OffersOffer, OffersProfile, } from '@prisma/client'; +import { JobType } from '@prisma/client'; import { TRPCError } from '@trpc/server'; import { createRouter } from '../context'; @@ -31,14 +32,32 @@ const binarySearchOfferPercentile = ( let start = 0; let end = similarOffers.length - 1; + const salary = + offer.jobType === JobType.FULLTIME + ? offer.OffersFullTime?.totalCompensation.value + : offer.OffersIntern?.monthlySalary.value; + + if (!salary) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'Cannot analyse without salary', + }); + } + while (start <= end) { const mid = Math.floor((start + end) / 2); - if (similarOffers[mid].id === offer.id) { + const similarOffer = similarOffers[mid]; + const similarSalary = + similarOffer.jobType === JobType.FULLTIME + ? similarOffer.OffersFullTime?.totalCompensation.value + : similarOffer.OffersIntern?.monthlySalary.value; + + if (similarSalary === salary) { return mid; } - if (offer.id < similarOffers[mid].id) { + if (salary < similarSalary) { end = mid - 1; } else { start = mid + 1; @@ -199,30 +218,29 @@ export const offersAnalysisRouter = createRouter().query('generate', { }); let similarCompanyOffers = similarOffers.filter( - (offer) => offer.companyId === overallHighestOffer.companyId, + (offer: { companyId: string }) => + offer.companyId === overallHighestOffer.companyId, ); // CALCULATE PERCENTILES - const highestOfferAgainstOverallIndex = binarySearchOfferPercentile( + const overallIndex = binarySearchOfferPercentile( overallHighestOffer, similarOffers, ); - const highestOfferAgainstOverallPercentile = - highestOfferAgainstOverallIndex / similarOffers.length; + const overallPercentile = overallIndex / similarOffers.length; - const highestOfferAgainstCompanyIndex = binarySearchOfferPercentile( + const companyIndex = binarySearchOfferPercentile( overallHighestOffer, similarCompanyOffers, ); - const highestOfferAgainstCompanyPercentile = - highestOfferAgainstCompanyIndex / similarCompanyOffers.length; + const companyPercentile = companyIndex / similarCompanyOffers.length; // FIND TOP >=90 PERCENTILE OFFERS similarOffers = similarOffers.filter( - (offer) => offer.id !== overallHighestOffer.id, + (offer: { id: string }) => offer.id !== overallHighestOffer.id, ); similarCompanyOffers = similarCompanyOffers.filter( - (offer) => offer.id !== overallHighestOffer.id, + (offer: { id: string }) => offer.id !== overallHighestOffer.id, ); const noOfSimilarOffers = similarOffers.length; @@ -247,18 +265,113 @@ export const offersAnalysisRouter = createRouter().query('generate', { ) : similarCompanyOffers; - return { - company: { - highestOfferAgainstCompanyPercentile, + const analysis = await ctx.prisma.offersAnalysis.create({ + data: { + companyPercentile, noOfSimilarCompanyOffers, - topPercentileCompanyOffers, - }, - overall: { - highestOfferAgainstOverallPercentile, noOfSimilarOffers, - topPercentileOffers, + overallHighestOffer: { + connect: { + id: overallHighestOffer.id, + }, + }, + overallPercentile, + profile: { + connect: { + id: input.profileId, + }, + }, + topCompanyOffers: { + connect: topPercentileCompanyOffers.map((offer) => { + return { id: offer.id }; + }), + }, + topOverallOffers: { + connect: topPercentileOffers.map((offer) => { + return { id: offer.id }; + }), + }, }, - overallHighestOffer, - }; + include: { + overallHighestOffer: { + include: { + OffersFullTime: { + include: { + totalCompensation: true, + }, + }, + OffersIntern: { + include: { + monthlySalary: true, + }, + }, + company: true, + profile: { + include: { + background: true, + }, + }, + }, + }, + topCompanyOffers: { + include: { + OffersFullTime: { + include: { + totalCompensation: true, + }, + }, + OffersIntern: { + include: { + monthlySalary: 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, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }); + + return analysis; }, });