[offers][feat] Add get offers analysis API

This commit is contained in:
BryannYeap
2022-10-15 08:01:13 +08:00
parent 56632892ce
commit e99e580d5e
3 changed files with 443 additions and 293 deletions

View File

@ -2,7 +2,7 @@ import React from 'react';
import { trpc } from '~/utils/trpc'; import { trpc } from '~/utils/trpc';
function profileAnalysis() { function GenerateAnalysis() {
const analysis = trpc.useQuery([ const analysis = trpc.useQuery([
'offers.analysis.generate', 'offers.analysis.generate',
{ profileId: 'cl98yxuei002htx1s8lrmwzmy' }, { profileId: 'cl98yxuei002htx1s8lrmwzmy' },
@ -11,4 +11,4 @@ function profileAnalysis() {
return <div>{JSON.stringify(analysis.data)}</div>; return <div>{JSON.stringify(analysis.data)}</div>;
} }
export default profileAnalysis; export default GenerateAnalysis;

View File

@ -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;

View File

@ -121,12 +121,7 @@ const specificAnalysisDtoMapper = (
const highestOfferDtoMapper = ( const highestOfferDtoMapper = (
offer: OffersOffer & { offer: OffersOffer & {
OffersFullTime: OffersFullTime:
| (OffersFullTime & { | (OffersFullTime & { totalCompensation: OffersCurrency })
baseSalary: OffersCurrency;
bonus: OffersCurrency;
stocks: OffersCurrency;
totalCompensation: OffersCurrency;
})
| null; | null;
OffersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null; OffersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null;
company: Company; company: Company;
@ -146,279 +141,344 @@ const highestOfferDtoMapper = (
}; };
}; };
export const offersAnalysisRouter = createRouter().query('generate', { const profileAnalysisDtoMapper = (
input: z.object({ analysisId: string,
profileId: z.string(), profileId: string,
}), overallHighestOffer: OffersOffer & {
async resolve({ ctx, input }) { OffersFullTime:
await ctx.prisma.offersAnalysis.deleteMany({ | (OffersFullTime & { totalCompensation: OffersCurrency })
where: { | null;
profileId: input.profileId, 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({ export const offersAnalysisRouter = createRouter()
include: { .query('generate', {
OffersFullTime: { input: z.object({
include: { profileId: z.string(),
baseSalary: true, }),
bonus: true, async resolve({ ctx, input }) {
stocks: true, await ctx.prisma.offersAnalysis.deleteMany({
totalCompensation: true, where: {
}, profileId: input.profileId,
}, },
OffersIntern: { });
include: {
monthlySalary: true, const offers = await ctx.prisma.offersOffer.findMany({
}, include: {
},
company: true,
profile: {
include: {
background: true,
},
},
},
orderBy: [
{
OffersFullTime: { OffersFullTime: {
totalCompensation: { include: {
value: 'desc', baseSalary: true,
bonus: true,
stocks: true,
totalCompensation: true,
}, },
}, },
},
{
OffersIntern: { OffersIntern: {
monthlySalary: { include: {
value: 'desc', monthlySalary: true,
},
},
company: true,
profile: {
include: {
background: true,
}, },
}, },
}, },
], orderBy: [
where: { {
profileId: input.profileId, OffersFullTime: {
}, totalCompensation: {
}); value: 'desc',
if (!offers || offers.length === 0) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'No offers found on this profile',
});
}
const overallHighestOffer = offers[0];
// 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 yoe = overallHighestOffer.profile.background.totalYoe as number;
let similarOffers = await ctx.prisma.offersOffer.findMany({
include: {
OffersFullTime: {
include: {
totalCompensation: true,
},
},
OffersIntern: {
include: {
monthlySalary: true,
},
},
company: true,
profile: {
include: {
background: {
include: {
experiences: {
include: {
company: true,
},
},
}, },
}, },
}, },
},
},
orderBy: [
{
OffersFullTime: {
totalCompensation: {
value: 'desc',
},
},
},
{
OffersIntern: {
monthlySalary: {
value: 'desc',
},
},
},
],
where: {
AND: [
{ {
location: overallHighestOffer.location, OffersIntern: {
}, monthlySalary: {
{ value: 'desc',
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,
},
},
],
}, },
}, },
}, },
], ],
}, where: {
}); profileId: input.profileId,
},
});
let similarCompanyOffers = similarOffers.filter( if (!offers || offers.length === 0) {
(offer: { companyId: string }) => throw new TRPCError({
offer.companyId === overallHighestOffer.companyId, code: 'NOT_FOUND',
); message: 'No offers found on this profile',
});
}
// CALCULATE PERCENTILES const overallHighestOffer = offers[0];
const overallIndex = binarySearchOfferPercentile(
overallHighestOffer,
similarOffers,
);
const overallPercentile = overallIndex / similarOffers.length;
const companyIndex = binarySearchOfferPercentile( // TODO: Shift yoe to background to make it mandatory
overallHighestOffer, if (
similarCompanyOffers, !overallHighestOffer.profile.background ||
); !overallHighestOffer.profile.background.totalYoe
const companyPercentile = companyIndex / similarCompanyOffers.length; ) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'Cannot analyse without YOE',
});
}
// FIND TOP >=90 PERCENTILE OFFERS const yoe = overallHighestOffer.profile.background.totalYoe as number;
similarOffers = similarOffers.filter(
(offer: { id: string }) => offer.id !== overallHighestOffer.id,
);
similarCompanyOffers = similarCompanyOffers.filter(
(offer: { id: string }) => offer.id !== overallHighestOffer.id,
);
const noOfSimilarOffers = similarOffers.length; let similarOffers = await ctx.prisma.offersOffer.findMany({
const similarOffers90PercentileIndex = include: {
Math.floor(noOfSimilarOffers * 0.9) - 1; OffersFullTime: {
const topPercentileOffers = include: {
noOfSimilarOffers > 1 totalCompensation: true,
? similarOffers.slice( },
similarOffers90PercentileIndex, },
similarOffers90PercentileIndex + 2, OffersIntern: {
) include: {
: similarOffers; monthlySalary: true,
},
const noOfSimilarCompanyOffers = similarCompanyOffers.length; },
const similarCompanyOffers90PercentileIndex = company: true,
Math.floor(noOfSimilarCompanyOffers * 0.9) - 1; profile: {
const topPercentileCompanyOffers = include: {
noOfSimilarCompanyOffers > 1 background: {
? similarCompanyOffers.slice( include: {
similarCompanyOffers90PercentileIndex, experiences: {
similarCompanyOffers90PercentileIndex + 2, include: {
) company: true,
: similarCompanyOffers; },
},
const analysis = await ctx.prisma.offersAnalysis.create({ },
data: { },
companyPercentile, },
noOfSimilarCompanyOffers,
noOfSimilarOffers,
overallHighestOffer: {
connect: {
id: overallHighestOffer.id,
}, },
}, },
overallPercentile, orderBy: [
profile: { {
connect: {
id: input.profileId,
},
},
topCompanyOffers: {
connect: topPercentileCompanyOffers.map((offer) => {
return { id: offer.id };
}),
},
topOverallOffers: {
connect: topPercentileOffers.map((offer) => {
return { id: offer.id };
}),
},
},
include: {
overallHighestOffer: {
include: {
OffersFullTime: { OffersFullTime: {
include: { totalCompensation: {
totalCompensation: true, value: 'desc',
},
},
OffersIntern: {
include: {
monthlySalary: true,
},
},
company: true,
profile: {
include: {
background: true,
}, },
}, },
}, },
}, {
topCompanyOffers: {
include: {
OffersFullTime: {
include: {
totalCompensation: true,
},
},
OffersIntern: { OffersIntern: {
include: { monthlySalary: {
monthlySalary: true, value: 'desc',
}, },
}, },
company: true, },
profile: { ],
include: { where: {
AND: [
{
location: overallHighestOffer.location,
},
{
OR: [
{
OffersFullTime: {
level: overallHighestOffer.OffersFullTime?.level,
specialization:
overallHighestOffer.OffersFullTime?.specialization,
},
OffersIntern: {
specialization:
overallHighestOffer.OffersIntern?.specialization,
},
},
],
},
{
profile: {
background: { background: {
include: { AND: [
experiences: { {
include: { totalYoe: {
company: true, gte: Math.max(yoe - 1, 0),
lte: yoe + 1,
},
},
],
},
},
},
],
},
});
let similarCompanyOffers = similarOffers.filter(
(offer: { companyId: string }) =>
offer.companyId === overallHighestOffer.companyId,
);
// CALCULATE PERCENTILES
const overallIndex = binarySearchOfferPercentile(
overallHighestOffer,
similarOffers,
);
const overallPercentile = overallIndex / similarOffers.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,
);
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 analysis = await ctx.prisma.offersAnalysis.create({
data: {
companyPercentile,
noOfSimilarCompanyOffers,
noOfSimilarOffers,
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 };
}),
},
},
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,
},
}, },
}, },
}, },
@ -427,51 +487,127 @@ export const offersAnalysisRouter = createRouter().query('generate', {
}, },
}, },
}, },
topOverallOffers: { });
include: {
OffersFullTime: {
include: {
totalCompensation: true,
},
},
OffersIntern: {
include: {
monthlySalary: true,
},
},
company: true,
profile: {
include: {
background: {
include: {
experiences: {
include: {
company: true,
},
},
},
},
},
},
},
},
},
});
return { return profileAnalysisDtoMapper(
companyAnalysis: specificAnalysisDtoMapper( analysis.id,
noOfSimilarCompanyOffers, analysis.profileId,
companyPercentile, overallHighestOffer,
topPercentileCompanyOffers,
),
id: analysis.id,
overallAnalysis: specificAnalysisDtoMapper(
noOfSimilarOffers, noOfSimilarOffers,
overallPercentile, overallPercentile,
topPercentileOffers, topPercentileOffers,
), noOfSimilarCompanyOffers,
overallHighestOffer: highestOfferDtoMapper(overallHighestOffer), companyPercentile,
profileId: analysis.profileId, 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,
},
},
},
},
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,
},
},
},
},
},
},
},
},
},
where: {
profileId: input.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,
);
},
});