|
|
@ -5,6 +5,7 @@ import type {
|
|
|
|
Country,
|
|
|
|
Country,
|
|
|
|
OffersBackground,
|
|
|
|
OffersBackground,
|
|
|
|
OffersCurrency,
|
|
|
|
OffersCurrency,
|
|
|
|
|
|
|
|
OffersExperience,
|
|
|
|
OffersFullTime,
|
|
|
|
OffersFullTime,
|
|
|
|
OffersIntern,
|
|
|
|
OffersIntern,
|
|
|
|
OffersOffer,
|
|
|
|
OffersOffer,
|
|
|
@ -13,6 +14,7 @@ import type {
|
|
|
|
PrismaClient,
|
|
|
|
PrismaClient,
|
|
|
|
State,
|
|
|
|
State,
|
|
|
|
} from '@prisma/client';
|
|
|
|
} from '@prisma/client';
|
|
|
|
|
|
|
|
import { JobType } from '@prisma/client';
|
|
|
|
import { TRPCError } from '@trpc/server';
|
|
|
|
import { TRPCError } from '@trpc/server';
|
|
|
|
|
|
|
|
|
|
|
|
import { analysisInclusion } from './analysisInclusion';
|
|
|
|
import { analysisInclusion } from './analysisInclusion';
|
|
|
@ -33,6 +35,27 @@ type Offer = OffersOffer & {
|
|
|
|
profile: OffersProfile & { background: OffersBackground | null };
|
|
|
|
profile: OffersProfile & { background: OffersBackground | null };
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
type SimilarOffer = OffersOffer & {
|
|
|
|
|
|
|
|
company: Company;
|
|
|
|
|
|
|
|
location: City & { state: State & { country: Country } };
|
|
|
|
|
|
|
|
offersFullTime:
|
|
|
|
|
|
|
|
| (OffersFullTime & { totalCompensation: OffersCurrency })
|
|
|
|
|
|
|
|
| null;
|
|
|
|
|
|
|
|
offersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null;
|
|
|
|
|
|
|
|
profile: OffersProfile & {
|
|
|
|
|
|
|
|
background:
|
|
|
|
|
|
|
|
| (OffersBackground & {
|
|
|
|
|
|
|
|
experiences: Array<
|
|
|
|
|
|
|
|
OffersExperience & {
|
|
|
|
|
|
|
|
company: Company | null;
|
|
|
|
|
|
|
|
location: (City & { state: State & { country: Country } }) | null;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
>;
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
| null;
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const getSimilarOffers = async (
|
|
|
|
const getSimilarOffers = async (
|
|
|
|
prisma: PrismaClient<
|
|
|
|
prisma: PrismaClient<
|
|
|
|
Prisma.PrismaClientOptions,
|
|
|
|
Prisma.PrismaClientOptions,
|
|
|
@ -161,28 +184,45 @@ const getSimilarOffers = async (
|
|
|
|
});
|
|
|
|
});
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const searchOfferPercentile = (
|
|
|
|
// OFFERS MUST BE ORDERED
|
|
|
|
offer: Offer,
|
|
|
|
const calculatePercentile = (
|
|
|
|
similarOffers: Array<
|
|
|
|
orderedOffers: Array<SimilarOffer>,
|
|
|
|
OffersOffer & {
|
|
|
|
offerToCalculate: Offer,
|
|
|
|
company: Company;
|
|
|
|
|
|
|
|
offersFullTime:
|
|
|
|
|
|
|
|
| (OffersFullTime & {
|
|
|
|
|
|
|
|
totalCompensation: OffersCurrency;
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
| null;
|
|
|
|
|
|
|
|
offersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null;
|
|
|
|
|
|
|
|
profile: OffersProfile & { background: OffersBackground | null };
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
>,
|
|
|
|
|
|
|
|
) => {
|
|
|
|
) => {
|
|
|
|
for (let i = 0; i < similarOffers.length; i++) {
|
|
|
|
let offerToCalculateIndex = -1;
|
|
|
|
if (similarOffers[i].id === offer.id) {
|
|
|
|
let numberOfNoDuplicateOffers = 0;
|
|
|
|
return i;
|
|
|
|
let lastOfferSalary = -1;
|
|
|
|
|
|
|
|
const offerToCalculateSalary = getSalary(offerToCalculate);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < orderedOffers.length; i++) {
|
|
|
|
|
|
|
|
const offer = orderedOffers[i];
|
|
|
|
|
|
|
|
const salary = getSalary(offer, lastOfferSalary);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (salary !== lastOfferSalary) {
|
|
|
|
|
|
|
|
if (salary === offerToCalculateSalary) {
|
|
|
|
|
|
|
|
offerToCalculateIndex = i;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
numberOfNoDuplicateOffers++;
|
|
|
|
|
|
|
|
lastOfferSalary = salary;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return -1;
|
|
|
|
const percentile =
|
|
|
|
|
|
|
|
numberOfNoDuplicateOffers <= 1
|
|
|
|
|
|
|
|
? 100
|
|
|
|
|
|
|
|
: 100 - (100 * offerToCalculateIndex) / (numberOfNoDuplicateOffers - 1);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return percentile;
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const getSalary = (offer: Offer | SimilarOffer, defaultSalary = 0) => {
|
|
|
|
|
|
|
|
return offer.jobType === JobType.FULLTIME &&
|
|
|
|
|
|
|
|
offer.offersFullTime?.totalCompensation?.baseValue != null
|
|
|
|
|
|
|
|
? offer.offersFullTime.totalCompensation.baseValue
|
|
|
|
|
|
|
|
: offer.jobType === JobType.INTERN &&
|
|
|
|
|
|
|
|
offer.offersIntern?.monthlySalary?.baseValue != null
|
|
|
|
|
|
|
|
? offer.offersIntern.monthlySalary.baseValue
|
|
|
|
|
|
|
|
: defaultSalary;
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export const generateAnalysis = async (params: {
|
|
|
|
export const generateAnalysis = async (params: {
|
|
|
@ -264,9 +304,26 @@ export const generateAnalysis = async (params: {
|
|
|
|
|
|
|
|
|
|
|
|
const overallHighestOffer = offers[0];
|
|
|
|
const overallHighestOffer = offers[0];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const offerIds = offers.map((offer) => offer.id);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// OVERALL ANALYSIS
|
|
|
|
let similarOffers = await getSimilarOffers(ctx.prisma, overallHighestOffer);
|
|
|
|
let similarOffers = await getSimilarOffers(ctx.prisma, overallHighestOffer);
|
|
|
|
|
|
|
|
const overallPercentile = calculatePercentile(
|
|
|
|
|
|
|
|
similarOffers,
|
|
|
|
|
|
|
|
overallHighestOffer,
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const offerIds = offers.map((offer) => offer.id);
|
|
|
|
// Get top offers (excluding user's offers)
|
|
|
|
|
|
|
|
similarOffers = similarOffers.filter((offer) => !offerIds.includes(offer.id));
|
|
|
|
|
|
|
|
const noOfSimilarOffers = similarOffers.length;
|
|
|
|
|
|
|
|
const similarOffers90PercentileIndex = Math.ceil(noOfSimilarOffers * 0.1);
|
|
|
|
|
|
|
|
const topPercentileOffers =
|
|
|
|
|
|
|
|
noOfSimilarOffers > 2
|
|
|
|
|
|
|
|
? similarOffers.slice(
|
|
|
|
|
|
|
|
similarOffers90PercentileIndex,
|
|
|
|
|
|
|
|
similarOffers90PercentileIndex + 2,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
: similarOffers;
|
|
|
|
|
|
|
|
|
|
|
|
// COMPANY ANALYSIS
|
|
|
|
// COMPANY ANALYSIS
|
|
|
|
const companyMap = new Map<string, Offer>();
|
|
|
|
const companyMap = new Map<string, Offer>();
|
|
|
@ -284,21 +341,15 @@ export const generateAnalysis = async (params: {
|
|
|
|
companyOffer,
|
|
|
|
companyOffer,
|
|
|
|
companyOffer.companyId,
|
|
|
|
companyOffer.companyId,
|
|
|
|
);
|
|
|
|
);
|
|
|
|
|
|
|
|
const companyPercentile = calculatePercentile(
|
|
|
|
const companyIndex = searchOfferPercentile(
|
|
|
|
|
|
|
|
companyOffer,
|
|
|
|
|
|
|
|
similarCompanyOffers,
|
|
|
|
similarCompanyOffers,
|
|
|
|
|
|
|
|
companyOffer,
|
|
|
|
);
|
|
|
|
);
|
|
|
|
const companyPercentile =
|
|
|
|
|
|
|
|
similarCompanyOffers.length <= 1
|
|
|
|
|
|
|
|
? 100
|
|
|
|
|
|
|
|
: 100 - (100 * companyIndex) / (similarCompanyOffers.length - 1);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Get top offers (excluding user's offers)
|
|
|
|
// Get top offers (excluding user's offers)
|
|
|
|
similarCompanyOffers = similarCompanyOffers.filter(
|
|
|
|
similarCompanyOffers = similarCompanyOffers.filter(
|
|
|
|
(offer) => !offerIds.includes(offer.id),
|
|
|
|
(offer) => !offerIds.includes(offer.id),
|
|
|
|
);
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const noOfSimilarCompanyOffers = similarCompanyOffers.length;
|
|
|
|
const noOfSimilarCompanyOffers = similarCompanyOffers.length;
|
|
|
|
const similarCompanyOffers90PercentileIndex = Math.ceil(
|
|
|
|
const similarCompanyOffers90PercentileIndex = Math.ceil(
|
|
|
|
noOfSimilarCompanyOffers * 0.1,
|
|
|
|
noOfSimilarCompanyOffers * 0.1,
|
|
|
@ -320,28 +371,6 @@ export const generateAnalysis = async (params: {
|
|
|
|
}),
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// OVERALL ANALYSIS
|
|
|
|
|
|
|
|
const overallIndex = searchOfferPercentile(
|
|
|
|
|
|
|
|
overallHighestOffer,
|
|
|
|
|
|
|
|
similarOffers,
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
const overallPercentile =
|
|
|
|
|
|
|
|
similarOffers.length <= 1
|
|
|
|
|
|
|
|
? 100
|
|
|
|
|
|
|
|
: 100 - (100 * overallIndex) / (similarOffers.length - 1);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
similarOffers = similarOffers.filter((offer) => !offerIds.includes(offer.id));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const noOfSimilarOffers = similarOffers.length;
|
|
|
|
|
|
|
|
const similarOffers90PercentileIndex = Math.ceil(noOfSimilarOffers * 0.1);
|
|
|
|
|
|
|
|
const topPercentileOffers =
|
|
|
|
|
|
|
|
noOfSimilarOffers > 2
|
|
|
|
|
|
|
|
? similarOffers.slice(
|
|
|
|
|
|
|
|
similarOffers90PercentileIndex,
|
|
|
|
|
|
|
|
similarOffers90PercentileIndex + 2,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
: similarOffers;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const analysis = await ctx.prisma.offersAnalysis.create({
|
|
|
|
const analysis = await ctx.prisma.offersAnalysis.create({
|
|
|
|
data: {
|
|
|
|
data: {
|
|
|
|
companyAnalysis: {
|
|
|
|
companyAnalysis: {
|
|
|
|