[offers[chore] Generate analysis for seeded data

pull/501/head
Stuart Long Chay Boon 3 years ago
parent 000867653b
commit 5cfdf6e87a

@ -1,221 +1,264 @@
import reader from "xlsx"; import reader from 'xlsx';
import { PrismaClient } from '@prisma/client'; import { PrismaClient } from '@prisma/client';
import crypto from 'crypto'; import crypto from 'crypto';
import { baseCurrencyString } from '../src/utils/offers/currency'; import { baseCurrencyString } from '../src/utils/offers/currency';
import { convert } from '../src/utils/offers/currency/currencyExchange'; import { convert } from '../src/utils/offers/currency/currencyExchange';
import { generateAnalysis } from '../src/utils/offers/analysisGeneration';
const prisma = new PrismaClient(); const prisma = new PrismaClient();
// Reading our test file // Reading our test file
const file = reader.readFile('/Users/stuartlong/Desktop/tech-interview-handbook/apps/portal/prisma/salaries.xlsx') const file = reader.readFile('prisma/salaries.xlsx');
let data: Array<excelData> = [] let data: Array<ExcelData> = [];
type excelData = { type ExcelData = {
Timestamp: Date; Timestamp: Date;
Type: string; Type: string;
Company: string; Company: string;
Role: string, Role: string;
Income?: number | string; Income?: number | string;
Stocks?: number | string; Stocks?: number | string;
SignOn?: number | string; SignOn?: number | string;
TC?: number | string; TC?: number | string;
Bonus?: number | string; Bonus?: number | string;
Comments?: string Comments?: string;
} };
const sheets = file.SheetNames const sheets = file.SheetNames;
for(let i = 0; i < sheets.length; i++) for (let i = 0; i < sheets.length; i++) {
{ const temp = reader.utils.sheet_to_json(file.Sheets[file.SheetNames[i]]);
const temp = reader.utils.sheet_to_json( temp.forEach((res: ExcelData) => {
file.Sheets[file.SheetNames[i]]) data.push(res);
temp.forEach((res: excelData) => { });
data.push(res)
})
} }
function xlSerialToJsDate(xlSerial){ function xlSerialToJsDate(xlSerial) {
return new Date(Date.UTC(0, 0, xlSerial - 1)); return new Date(Date.UTC(0, 0, xlSerial - 1));
} }
function generateSpecialization() { function generateSpecialization() {
const specializations = ["Frontend", "Backend", "Fullstack"]; const specializations = ['Frontend', 'Backend', 'Fullstack'];
return specializations[Math.floor((Math.random() * 300)) % 3]; return specializations[Math.floor(Math.random() * 300) % 3];
} }
async function seedSalaries() { const createdProfileIds: Array<string> = [];
console.log('Seeding from salaries sheet...');
const seedSalaries = async () => {
const companyIdMappings = {}; console.log('Seeding from salaries sheet...');
(await prisma.company.findMany()).forEach((company) => {
companyIdMappings[company.name] = company.id const companyIdMappings = {};
}); (await prisma.company.findMany()).forEach((company) => {
console.log(companyIdMappings); companyIdMappings[company.name] = company.id;
});
const createdProfileIds : Array<string> = [];
//seed here //seed here
(await Promise.all([ return await Promise.all(
data.map(async (data: excelData) => { data.map(async (data: excelData) => {
// only add swe roles // only add swe roles
if (data.Role.toUpperCase() === 'SOFTWARE ENGINEER') { if (data.Role.toUpperCase() === 'SOFTWARE ENGINEER') {
if (data.Income && typeof (data.Income) === "number") { if (data.Income && typeof data.Income === 'number') {
// check if we have company id // check if we have company id
// console.log(data.Income) // console.log(data.Income)
// console.log() // console.log()
if (companyIdMappings[data.Company]) { if (companyIdMappings[data.Company]) {
const token = crypto.createHash('sha256').update(xlSerialToJsDate(data.Timestamp).toString()).digest('hex') const token = crypto
if (data.Type.toUpperCase() === 'INTERNSHIP') { .createHash('sha256')
// create profile .update(xlSerialToJsDate(data.Timestamp).toString())
const dataAdded = await prisma.offersProfile.create({ .digest('hex');
data: { if (data.Type.toUpperCase() === 'INTERNSHIP') {
profileName: crypto.randomUUID().substring(0, 10), // create profile
createdAt: xlSerialToJsDate(data.Timestamp), const dataAdded = await prisma.offersProfile.create({
editToken: token, data: {
background: { profileName: crypto.randomUUID().substring(0, 10),
create: { createdAt: xlSerialToJsDate(data.Timestamp),
totalYoe: 0 editToken: token,
} background: {
}, create: {
offers: { totalYoe: 0,
create: { },
comments: data.Comments ?? "", },
company: { offers: {
connect: { create: {
id: companyIdMappings[data.Company] comments: data.Comments ?? '',
} company: {
}, connect: {
jobType: "INTERN", id: companyIdMappings[data.Company],
location: "Singapore, Singapore", // TODO: DEFAULT AS SG },
monthYearReceived: xlSerialToJsDate(data.Timestamp), },
negotiationStrategy: "", jobType: 'INTERN',
offersIntern: { location: 'Singapore, Singapore', // TODO: DEFAULT AS SG
create: { monthYearReceived: xlSerialToJsDate(data.Timestamp),
internshipCycle: "Summer", negotiationStrategy: '',
monthlySalary: { offersIntern: {
create: { create: {
baseCurrency: baseCurrencyString, internshipCycle: 'Summer',
baseValue: await convert( monthlySalary: {
data.Income, create: {
'SGD', // assume sgd baseCurrency: baseCurrencyString,
baseCurrencyString, baseValue: await convert(
), data.Income,
currency: 'SGD', // assume sgd 'SGD', // assume sgd
value: data.Income baseCurrencyString,
} ),
}, currency: 'SGD', // assume sgd
specialization: generateSpecialization(), // TODO: check about this value: data.Income,
startYear: xlSerialToJsDate(data.Timestamp).getFullYear(), },
title: data.Role // TODO: check about this },
} specialization: generateSpecialization(), // TODO: check about this
} startYear: xlSerialToJsDate(
} data.Timestamp,
} ).getFullYear(),
} title: data.Role, // TODO: check about this
}) },
},
console.log(dataAdded) },
createdProfileIds.push(dataAdded.id) },
} else { },
// assume rest full time });
const dataAdded = await prisma.offersProfile.create({
data: { console.log('Profile created:', dataAdded.id);
profileName: crypto.randomUUID().substring(0, 10), createdProfileIds.push(dataAdded.id);
createdAt: xlSerialToJsDate(data.Timestamp),
editToken: token,
background: {
create: {
totalYoe: 0
}
},
offers: {
create: {
comments: data.Comments ?? "",
company: {
connect: {
id: companyIdMappings[data.Company]
}
},
jobType: "FULLTIME",
location: "Singapore, Singapore", // TODO: DEFAULT AS SG
monthYearReceived: xlSerialToJsDate(data.Timestamp),
negotiationStrategy: "",
offersFullTime: {
create: {
baseSalary: {
create: {
baseCurrency: baseCurrencyString,
baseValue: await convert(
data.Income,
'SGD', // assume sgd
baseCurrencyString,
),
currency: 'SGD', // assume sgd
value: data.Income
}
},
bonus: {
create: {
baseCurrency: baseCurrencyString,
baseValue: await convert(
data.Bonus ? (typeof data.Bonus === 'number' ? data.Bonus : 0) : 0,
'SGD',
baseCurrencyString,
),
currency: 'SGD',
value: data.Bonus ? (typeof data.Bonus === 'number' ? data.Bonus : 0) : 0,
}
},
level: data.Type,
specialization: generateSpecialization(), // TODO: check about this
stocks: {
create: {
baseCurrency: baseCurrencyString,
baseValue: await convert(
data.Stocks ? (typeof data.Stocks === "number" ? data.Stocks : 0) : 0,
'SGD',
baseCurrencyString,
),
currency: 'SGD',
value: data.Stocks ? (typeof data.Stocks === "number" ? data.Stocks : 0) : 0,
}
},
title: data.Role, // TODO: check about this
totalCompensation: {
create: {
baseCurrency: baseCurrencyString,
baseValue: await convert(
data.TC ? (typeof data.TC === "number" ? data.TC : 0) : 0,
'SGD',
baseCurrencyString,
),
currency: 'SGD',
value: data.TC ? (typeof data.TC === "number" ? data.TC : 0) : 0,
}
},
}
}
}
}
}
})
console.log(dataAdded)
createdProfileIds.push(dataAdded.id)
}
} else {
console.log("Invalid Company: " + data.Company)
}
} else { } else {
console.log("Invalid Income not a number: " + data.Income) // assume rest full time
const dataAdded = await prisma.offersProfile.create({
data: {
profileName: crypto.randomUUID().substring(0, 10),
createdAt: xlSerialToJsDate(data.Timestamp),
editToken: token,
background: {
create: {
totalYoe: 0,
},
},
offers: {
create: {
comments: data.Comments ?? '',
company: {
connect: {
id: companyIdMappings[data.Company],
},
},
jobType: 'FULLTIME',
location: 'Singapore, Singapore', // TODO: DEFAULT AS SG
monthYearReceived: xlSerialToJsDate(data.Timestamp),
negotiationStrategy: '',
offersFullTime: {
create: {
baseSalary: {
create: {
baseCurrency: baseCurrencyString,
baseValue: await convert(
data.Income,
'SGD', // assume sgd
baseCurrencyString,
),
currency: 'SGD', // assume sgd
value: data.Income,
},
},
bonus: {
create: {
baseCurrency: baseCurrencyString,
baseValue: await convert(
data.Bonus
? typeof data.Bonus === 'number'
? data.Bonus
: 0
: 0,
'SGD',
baseCurrencyString,
),
currency: 'SGD',
value: data.Bonus
? typeof data.Bonus === 'number'
? data.Bonus
: 0
: 0,
},
},
level: data.Type,
specialization: generateSpecialization(), // TODO: check about this
stocks: {
create: {
baseCurrency: baseCurrencyString,
baseValue: await convert(
data.Stocks
? typeof data.Stocks === 'number'
? data.Stocks
: 0
: 0,
'SGD',
baseCurrencyString,
),
currency: 'SGD',
value: data.Stocks
? typeof data.Stocks === 'number'
? data.Stocks
: 0
: 0,
},
},
title: data.Role, // TODO: check about this
totalCompensation: {
create: {
baseCurrency: baseCurrencyString,
baseValue: await convert(
data.TC
? typeof data.TC === 'number'
? data.TC
: 0
: 0,
'SGD',
baseCurrencyString,
),
currency: 'SGD',
value: data.TC
? typeof data.TC === 'number'
? data.TC
: 0
: 0,
},
},
},
},
},
},
},
});
console.log('Profile created:', dataAdded.id);
createdProfileIds.push(dataAdded.id);
} }
} } else {
}) console.log('Invalid Company: ' + data.Company);
]).then((_data) => { }
console.log('Seeding from salaries sheet complete') } else {
})); console.log('Invalid Income not a number: ' + data.Income);
} }
}
}),
);
};
seedSalaries() const generateAllAnalysis = async () => {
return await Promise.all(
createdProfileIds.map(async (profileId) => {
const analysis = await generateAnalysis({
ctx: { prisma, session: null },
input: { profileId },
});
console.log('Analysis generated for profile with id:', profileId);
}),
);
};
Promise.all([seedSalaries()])
.then(() => generateAllAnalysis())
.then((_data) => {
console.log('Seeding from salaries sheet complete');
})
.then(async () => { .then(async () => {
await prisma.$disconnect(); await prisma.$disconnect();
}) })
@ -225,10 +268,6 @@ seedSalaries()
process.exit(1); process.exit(1);
}); });
// Printing data console.log(xlSerialToJsDate(data[0].Timestamp));
// console.log(data.splice(0,100))
// // console.table(data.splice(0,100))
console.log(xlSerialToJsDate(data[0].Timestamp))
export {} export {};

@ -235,7 +235,7 @@ model OffersProfile {
model OffersBackground { model OffersBackground {
id String @id @default(cuid()) id String @id @default(cuid())
totalYoe Int totalYoe Int @default(0)
specificYoes OffersSpecificYoe[] specificYoes OffersSpecificYoe[]
experiences OffersExperience[] experiences OffersExperience[]

@ -8,7 +8,7 @@ function GenerateAnalysis() {
return ( return (
<div> <div>
{JSON.stringify( {JSON.stringify(
analysisMutation.mutate({ profileId: 'cl9lwe9m902k5utskjs52wc0j' }), analysisMutation.mutate({ profileId: 'cl9luzsqh0005utr2d7jpjabt' }),
)} )}
</div> </div>
); );

@ -5,7 +5,7 @@ import { trpc } from '~/utils/trpc';
function GetAnalysis() { function GetAnalysis() {
const analysis = trpc.useQuery([ const analysis = trpc.useQuery([
'offers.analysis.get', 'offers.analysis.get',
{ profileId: 'cl9jo3e0k004ai9c0zmfzo50j' }, { profileId: 'cl9luzsqh0005utr2d7jpjabt' },
]); ]);
return <div>{JSON.stringify(analysis.data)}</div>; return <div>{JSON.stringify(analysis.data)}</div>;

@ -1,386 +0,0 @@
import type { Session } from 'next-auth';
import type {
City,
Company,
Country,
OffersBackground,
OffersCurrency,
OffersFullTime,
OffersIntern,
OffersOffer,
OffersProfile,
Prisma,
PrismaClient,
State,
} from '@prisma/client';
import { TRPCError } from '@trpc/server';
import { profileAnalysisDtoMapper } from '../../mappers/offers-mappers';
const searchOfferPercentile = (
offer: OffersOffer & {
company: Company;
offersFullTime:
| (OffersFullTime & {
baseSalary: OffersCurrency;
bonus: OffersCurrency;
stocks: OffersCurrency;
totalCompensation: OffersCurrency;
})
| null;
offersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null;
profile: OffersProfile & { background: OffersBackground | null };
},
similarOffers: Array<
OffersOffer & {
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++) {
if (similarOffers[i].id === offer.id) {
return i;
}
}
return -1;
};
export const generateAnalysis = async (params: {
ctx: {
prisma: PrismaClient<
Prisma.PrismaClientOptions,
never,
Prisma.RejectOnNotFound | Prisma.RejectPerOperation | undefined
>;
session: Session | null;
};
input: { profileId: string };
}) => {
const { ctx, input } = params;
await ctx.prisma.offersAnalysis.deleteMany({
where: {
profileId: input.profileId,
},
});
const offers = await ctx.prisma.offersOffer.findMany({
include: {
company: true,
offersFullTime: {
include: {
baseSalary: true,
bonus: true,
stocks: true,
totalCompensation: true,
},
},
offersIntern: {
include: {
monthlySalary: true,
},
},
profile: {
include: {
background: true,
},
},
},
orderBy: [
{
offersFullTime: {
totalCompensation: {
baseValue: 'desc',
},
},
},
{
offersIntern: {
monthlySalary: {
baseValue: 'desc',
},
},
},
],
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];
// TODO: Shift yoe out of background to make it mandatory
if (
!overallHighestOffer.profile.background ||
overallHighestOffer.profile.background.totalYoe == null
) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'YOE not found',
});
}
const yoe = overallHighestOffer.profile.background.totalYoe as number;
const monthYearReceived = new Date(overallHighestOffer.monthYearReceived);
monthYearReceived.setFullYear(monthYearReceived.getFullYear() - 1);
let similarOffers = await ctx.prisma.offersOffer.findMany({
include: {
company: true,
offersFullTime: {
include: {
totalCompensation: true,
},
},
offersIntern: {
include: {
monthlySalary: true,
},
},
profile: {
include: {
background: {
include: {
experiences: {
include: {
company: true,
},
},
},
},
},
},
},
orderBy: [
{
offersFullTime: {
totalCompensation: {
baseValue: 'desc',
},
},
},
{
offersIntern: {
monthlySalary: {
baseValue: 'desc',
},
},
},
],
where: {
AND: [
{
location: overallHighestOffer.location,
},
{
monthYearReceived: {
gte: monthYearReceived,
},
},
{
OR: [
{
offersFullTime: {
title: overallHighestOffer.offersFullTime?.title,
},
offersIntern: {
title: overallHighestOffer.offersIntern?.title,
},
},
],
},
{
profile: {
background: {
AND: [
{
totalYoe: {
gte: Math.max(yoe - 1, 0),
lte: yoe + 1,
},
},
],
},
},
},
],
},
});
let similarCompanyOffers = similarOffers.filter(
(offer) => offer.companyId === overallHighestOffer.companyId,
);
// CALCULATE PERCENTILES
const overallIndex = searchOfferPercentile(
overallHighestOffer,
similarOffers,
);
const overallPercentile =
similarOffers.length === 0
? 100
: (100 * overallIndex) / similarOffers.length;
const companyIndex = searchOfferPercentile(
overallHighestOffer,
similarCompanyOffers,
);
const companyPercentile =
similarCompanyOffers.length === 0
? 100
: (100 * companyIndex) / similarCompanyOffers.length;
// FIND TOP >=90 PERCENTILE OFFERS, DOESN'T GIVE 100th PERCENTILE
// e.g. If there only 4 offers, it gives the 2nd and 3rd offer
similarOffers = similarOffers.filter(
(offer) => offer.id !== overallHighestOffer.id,
);
similarCompanyOffers = similarCompanyOffers.filter(
(offer) => offer.id !== overallHighestOffer.id,
);
const noOfSimilarOffers = similarOffers.length;
const similarOffers90PercentileIndex = Math.ceil(noOfSimilarOffers * 0.1);
const topPercentileOffers =
noOfSimilarOffers > 2
? similarOffers.slice(
similarOffers90PercentileIndex,
similarOffers90PercentileIndex + 2,
)
: similarOffers;
const noOfSimilarCompanyOffers = similarCompanyOffers.length;
const similarCompanyOffers90PercentileIndex = Math.ceil(
noOfSimilarCompanyOffers * 0.1,
);
const topPercentileCompanyOffers =
noOfSimilarCompanyOffers > 2
? 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: {
company: true,
offersFullTime: {
include: {
totalCompensation: true,
},
},
offersIntern: {
include: {
monthlySalary: true,
},
},
profile: {
include: {
background: true,
},
},
},
},
topCompanyOffers: {
include: {
company: true,
offersFullTime: {
include: {
totalCompensation: true,
},
},
offersIntern: {
include: {
monthlySalary: true,
},
},
profile: {
include: {
background: {
include: {
experiences: {
include: {
company: true,
},
},
},
},
},
},
},
},
topOverallOffers: {
include: {
company: true,
offersFullTime: {
include: {
totalCompensation: true,
},
},
offersIntern: {
include: {
monthlySalary: true,
},
},
profile: {
include: {
background: {
include: {
experiences: {
include: {
company: true,
},
},
},
},
},
},
},
},
},
});
return profileAnalysisDtoMapper(analysis);
};
Loading…
Cancel
Save