diff --git a/apps/portal/prisma/migrations/20221021231817_/migration.sql b/apps/portal/prisma/migrations/20221021231817_/migration.sql new file mode 100644 index 00000000..3820338d --- /dev/null +++ b/apps/portal/prisma/migrations/20221021231817_/migration.sql @@ -0,0 +1,12 @@ +/* + Warnings: + + - Added the required column `baseValue` to the `OffersCurrency` table without a default value. This is not possible if the table is not empty. + - Added the required column `updatedAt` to the `OffersCurrency` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "OffersCurrency" ADD COLUMN "baseCurrency" TEXT NOT NULL DEFAULT 'USD', +ADD COLUMN "baseValue" INTEGER NOT NULL, +ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL; diff --git a/apps/portal/prisma/migrations/20221021233952_change_currency_values_to_float/migration.sql b/apps/portal/prisma/migrations/20221021233952_change_currency_values_to_float/migration.sql new file mode 100644 index 00000000..089e963d --- /dev/null +++ b/apps/portal/prisma/migrations/20221021233952_change_currency_values_to_float/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "OffersCurrency" ALTER COLUMN "value" SET DATA TYPE DOUBLE PRECISION, +ALTER COLUMN "baseValue" SET DATA TYPE DOUBLE PRECISION; diff --git a/apps/portal/prisma/schema.prisma b/apps/portal/prisma/schema.prisma index 7f968286..67a4f6d3 100644 --- a/apps/portal/prisma/schema.prisma +++ b/apps/portal/prisma/schema.prisma @@ -205,9 +205,9 @@ model OffersBackground { totalYoe Int specificYoes OffersSpecificYoe[] - experiences OffersExperience[] // For extensibility in the future + experiences OffersExperience[] - educations OffersEducation[] // For extensibility in the future + educations OffersEducation[] profile OffersProfile @relation(fields: [offersProfileId], references: [id], onDelete: Cascade) offersProfileId String @unique @@ -251,10 +251,16 @@ model OffersExperience { } model OffersCurrency { - id String @id @default(cuid()) - value Int + id String @id @default(cuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + value Float currency String + baseValue Float + baseCurrency String @default("USD") + // Experience OffersExperienceTotalCompensation OffersExperience? @relation("ExperienceTotalCompensation") OffersExperienceMonthlySalary OffersExperience? @relation("ExperienceMonthlySalary") diff --git a/apps/portal/src/pages/offers/test/createProfile.tsx b/apps/portal/src/pages/offers/test/createProfile.tsx index ab240dbf..7928ab12 100644 --- a/apps/portal/src/pages/offers/test/createProfile.tsx +++ b/apps/portal/src/pages/offers/test/createProfile.tsx @@ -40,7 +40,7 @@ function Test() { deleteCommentMutation.mutate({ id: 'cl97fprun001j7iyg6ev9x983', profileId: 'cl96stky5002ew32gx2kale2x', - token: 'afca11e436d21bde24543718fa957c6c625335439dc504f24ee35eae7b5ef1', + token: '24bafa6fef803f447d7f2e229b14cb8ee43f0c22dffbe41ee1c1e5e6e870f117', userId: 'cl97dl51k001e7iygd5v5gt58', }); }; @@ -84,7 +84,7 @@ function Test() { const handleLink = () => { addToUserProfileMutation.mutate({ profileId: 'cl9efyn9p004ww3u42mjgl1vn', - token: 'afca11e436d21bde24543718fa957c6c625335439dc504f24ee35eae7b5ef1ba', + token: '24bafa6fef803f447d7f2e229b14cb8ee43f0c22dffbe41ee1c1e5e6e870f117', userId: 'cl9ehvpng0000w3ec2mpx0bdd', }); }; @@ -103,11 +103,10 @@ function Test() { ], experiences: [ { - companyId: 'cl9h0bqu50000txxwkhmshhxz', + companyId: 'cl9j4yawz0003utlp1uaa1t8o', durationInMonths: 24, jobType: 'FULLTIME', level: 'Junior', - // "monthlySalary": undefined, specialization: 'Front End', title: 'Software Engineer', totalCompensation: { @@ -132,7 +131,7 @@ function Test() { { comments: 'I am a Raffles Institution almumni', // Comments: '', - companyId: 'cl9h0bqu50000txxwkhmshhxz', + companyId: 'cl9j4yawz0003utlp1uaa1t8o', jobType: 'FULLTIME', location: 'Singapore, Singapore', monthYearReceived: new Date('2022-09-30T07:58:54.000Z'), @@ -161,7 +160,7 @@ function Test() { }, { comments: '', - companyId: 'cl9h0bqu50000txxwkhmshhxz', + companyId: 'cl9j4yawz0003utlp1uaa1t8o', jobType: 'FULLTIME', location: 'Singapore, Singapore', monthYearReceived: new Date('2022-09-30T07:58:54.000Z'), @@ -192,14 +191,14 @@ function Test() { }); }; - const profileId = 'cl9i68fv60000tthj8t3zkox0'; // Remember to change this filed after testing deleting + const profileId = 'cl9j50xzk008vutfqg6mta2ey'; // Remember to change this filed after testing deleting const data = trpc.useQuery( [ `offers.profile.listOne`, { profileId, token: - 'd14666ff76e267c9e99445844b41410e83874936d0c07e664db73ff0ea76919e', + '24bafa6fef803f447d7f2e229b14cb8ee43f0c22dffbe41ee1c1e5e6e870f117', }, ], { @@ -223,7 +222,7 @@ function Test() { const handleDelete = (id: string) => { deleteMutation.mutate({ profileId: id, - token: 'e7effd2a40adba2deb1ddea4fb9f1e6c3c98ab0a85a88ed1567fc2a107fdb445', + token: '24bafa6fef803f447d7f2e229b14cb8ee43f0c22dffbe41ee1c1e5e6e870f117', }); }; @@ -257,15 +256,15 @@ function Test() { createdAt: new Date('2022-10-12T16:19:05.196Z'), description: 'Meta Platforms, Inc., doing business as Meta and formerly named Facebook, Inc., and TheFacebook, Inc., is an American multinational technology conglomerate based in Menlo Park, California. The company owns Facebook, Instagram, and WhatsApp, among other products and services.', - id: 'cl9h0bqug0003txxwgkac0x40', + id: 'cl9j4yawz0003utlp1uaa1t8o', logoUrl: 'https://logo.clearbit.com/meta.com', name: 'Meta', slug: 'meta', updatedAt: new Date('2022-10-12T16:19:05.196Z'), }, - companyId: 'cl9h0bqug0003txxwgkac0x40', + companyId: 'cl9j4yawz0003utlp1uaa1t8o', durationInMonths: 24, - // Id: 'cl9h0bqug0003txxwgkac0x40', + // Id: 'cl9j4yawz0003utlp1uaa1t8o', jobType: 'FULLTIME', level: 'Junior', monthlySalary: null, @@ -309,13 +308,13 @@ function Test() { createdAt: new Date('2022-10-12T16:19:05.196Z'), description: 'Meta Platforms, Inc., doing business as Meta and formerly named Facebook, Inc., and TheFacebook, Inc., is an American multinational technology conglomerate based in Menlo Park, California. The company owns Facebook, Instagram, and WhatsApp, among other products and services.', - id: 'cl9h0bqug0003txxwgkac0x40', + id: 'cl9j4yawz0003utlp1uaa1t8o', logoUrl: 'https://logo.clearbit.com/meta.com', name: 'Meta', slug: 'meta', updatedAt: new Date('2022-10-12T16:19:05.196Z'), }, - companyId: 'cl9h0bqug0003txxwgkac0x40', + companyId: 'cl9j4yawz0003utlp1uaa1t8o', id: 'cl9i68fve000ntthj5h9yvqnh', jobType: 'FULLTIME', location: 'Singapore, Singapore', @@ -362,13 +361,13 @@ function Test() { // createdAt: new Date('2022-10-12T16:19:05.196Z'), // description: // 'Meta Platforms, Inc., doing business as Meta and formerly named Facebook, Inc., and TheFacebook, Inc., is an American multinational technology conglomerate based in Menlo Park, California. The company owns Facebook, Instagram, and WhatsApp, among other products and services.', - // id: 'cl9h0bqug0003txxwgkac0x40', + // id: 'cl9j4yawz0003utlp1uaa1t8o', // logoUrl: 'https://logo.clearbit.com/meta.com', // name: 'Meta', // slug: 'meta', // updatedAt: new Date('2022-10-12T16:19:05.196Z'), // }, - // companyId: 'cl9h0bqug0003txxwgkac0x40', + // companyId: 'cl9j4yawz0003utlp1uaa1t8o', // id: 'cl9i68fvf000ytthj0ltsqt1d', // jobType: 'FULLTIME', // location: 'Singapore, Singapore', @@ -415,13 +414,13 @@ function Test() { // createdAt: new Date('2022-10-12T16:19:05.196Z'), // description: // 'Meta Platforms, Inc., doing business as Meta and formerly named Facebook, Inc., and TheFacebook, Inc., is an American multinational technology conglomerate based in Menlo Park, California. The company owns Facebook, Instagram, and WhatsApp, among other products and services.', - // id: 'cl9h0bqug0003txxwgkac0x40', + // id: 'cl9j4yawz0003utlp1uaa1t8o', // logoUrl: 'https://logo.clearbit.com/meta.com', // name: 'Meta', // slug: 'meta', // updatedAt: new Date('2022-10-12T16:19:05.196Z'), // }, - // companyId: 'cl9h0bqug0003txxwgkac0x40', + // companyId: 'cl9j4yawz0003utlp1uaa1t8o', // id: 'cl96stky9003bw32gc3l955vr', // jobType: 'FULLTIME', // location: 'Singapore, Singapore', @@ -468,13 +467,13 @@ function Test() { // createdAt: new Date('2022-10-12T16:19:05.196Z'), // description: // 'Meta Platforms, Inc., doing business as Meta and formerly named Facebook, Inc., and TheFacebook, Inc., is an American multinational technology conglomerate based in Menlo Park, California. The company owns Facebook, Instagram, and WhatsApp, among other products and services.', - // id: 'cl9h0bqug0003txxwgkac0x40', + // id: 'cl9j4yawz0003utlp1uaa1t8o', // logoUrl: 'https://logo.clearbit.com/meta.com', // name: 'Meta', // slug: 'meta', // updatedAt: new Date('2022-10-12T16:19:05.196Z'), // }, - // companyId: 'cl9h0bqug0003txxwgkac0x40', + // companyId: 'cl9j4yawz0003utlp1uaa1t8o', // id: 'cl976wf28000t7iyga4noyz7s', // jobType: 'FULLTIME', // location: 'Singapore, Singapore', @@ -521,13 +520,13 @@ function Test() { // createdAt: new Date('2022-10-12T16:19:05.196Z'), // description: // 'Meta Platforms, Inc., doing business as Meta and formerly named Facebook, Inc., and TheFacebook, Inc., is an American multinational technology conglomerate based in Menlo Park, California. The company owns Facebook, Instagram, and WhatsApp, among other products and services.', - // id: 'cl9h0bqug0003txxwgkac0x40', + // id: 'cl9j4yawz0003utlp1uaa1t8o', // logoUrl: 'https://logo.clearbit.com/meta.com', // name: 'Meta', // slug: 'meta', // updatedAt: new Date('2022-10-12T16:19:05.196Z'), // }, - // companyId: 'cl9h0bqug0003txxwgkac0x40', + // companyId: 'cl9j4yawz0003utlp1uaa1t8o', // id: 'cl96tbb3o0051w32gjrpaiiit', // jobType: 'FULLTIME', // location: 'Singapore, Singapore', @@ -570,7 +569,7 @@ function Test() { // }, ], // ProfileName: 'ailing bryann stuart ziqing', - token: 'd3509cb890f0bae0a785afdd6c1c074a140706ab1d155ed338ec22dcca5c92f1', + token: '24bafa6fef803f447d7f2e229b14cb8ee43f0c22dffbe41ee1c1e5e6e870f117', userId: null, }); }; diff --git a/apps/portal/src/server/router/offers/offers-profile-router.ts b/apps/portal/src/server/router/offers/offers-profile-router.ts index b8552018..8ce57731 100644 --- a/apps/portal/src/server/router/offers/offers-profile-router.ts +++ b/apps/portal/src/server/router/offers/offers-profile-router.ts @@ -1,5 +1,6 @@ import crypto, { randomUUID } from 'crypto'; import { z } from 'zod'; +import { JobType } from '@prisma/client'; import * as trpc from '@trpc/server'; import { @@ -7,6 +8,9 @@ import { createOfferProfileResponseMapper, profileDtoMapper, } from '~/mappers/offers-mappers'; +import { baseCurrencyString } from '~/utils/offers/currency'; +import { convert } from '~/utils/offers/currency/currency-exchange'; +import { createValidationRegex } from '~/utils/offers/zodRegex'; import { createRouter } from '../context'; @@ -31,7 +35,7 @@ const offer = z.object({ company: company.nullish(), companyId: z.string(), id: z.string().optional(), - jobType: z.string(), + jobType: z.string().regex(createValidationRegex(Object.keys(JobType), null)), location: z.string(), monthYearReceived: z.date(), negotiationStrategy: z.string(), @@ -73,7 +77,10 @@ const experience = z.object({ companyId: z.string().nullish(), durationInMonths: z.number().nullish(), id: z.string().optional(), - jobType: z.string().nullish(), + jobType: z + .string() + .regex(createValidationRegex(Object.keys(JobType), null)) + .nullish(), level: z.string().nullish(), location: z.string().nullish(), monthlySalary: valuation.nullish(), @@ -94,15 +101,6 @@ const education = z.object({ type: z.string().nullish(), }); -// Const reply = z.object({ -// createdAt: z.date().nullish(), -// id: z.string().optional(), -// messages: z.string().nullish(), -// profileId: z.string().nullish(), -// replyingToId: z.string().nullish(), -// userId: z.string().nullish(), -// }); - export const offersProfileRouter = createRouter() .query('listOne', { input: z.object({ @@ -282,11 +280,11 @@ export const offersProfileRouter = createRouter() })), }, experiences: { - create: input.background.experiences.map((x) => { + create: input.background.experiences.map(async (x) => { if ( - x.jobType === 'FULLTIME' && - x.totalCompensation?.currency !== undefined && - x.totalCompensation.value !== undefined + x.jobType === JobType.FULLTIME && + x.totalCompensation?.currency != null && + x.totalCompensation?.value != null ) { if (x.companyId) { return { @@ -302,8 +300,14 @@ export const offersProfileRouter = createRouter() title: x.title, totalCompensation: { create: { - currency: x.totalCompensation?.currency, - value: x.totalCompensation?.value, + baseCurrency: baseCurrencyString, + baseValue: await convert( + x.totalCompensation.value, + x.totalCompensation.currency, + baseCurrencyString, + ), + currency: x.totalCompensation.currency, + value: x.totalCompensation.value, }, }, }; @@ -312,20 +316,27 @@ export const offersProfileRouter = createRouter() durationInMonths: x.durationInMonths, jobType: x.jobType, level: x.level, + location: x.location, specialization: x.specialization, title: x.title, totalCompensation: { create: { - currency: x.totalCompensation?.currency, - value: x.totalCompensation?.value, + baseCurrency: baseCurrencyString, + baseValue: await convert( + x.totalCompensation.value, + x.totalCompensation.currency, + baseCurrencyString, + ), + currency: x.totalCompensation.currency, + value: x.totalCompensation.value, }, }, }; } if ( - x.jobType === 'INTERN' && - x.monthlySalary?.currency !== undefined && - x.monthlySalary.value !== undefined + x.jobType === JobType.INTERN && + x.monthlySalary?.currency != null && + x.monthlySalary?.value != null ) { if (x.companyId) { return { @@ -338,8 +349,14 @@ export const offersProfileRouter = createRouter() jobType: x.jobType, monthlySalary: { create: { - currency: x.monthlySalary?.currency, - value: x.monthlySalary?.value, + baseCurrency: baseCurrencyString, + baseValue: await convert( + x.monthlySalary.value, + x.monthlySalary.currency, + baseCurrencyString, + ), + currency: x.monthlySalary.currency, + value: x.monthlySalary.value, }, }, specialization: x.specialization, @@ -351,8 +368,14 @@ export const offersProfileRouter = createRouter() jobType: x.jobType, monthlySalary: { create: { - currency: x.monthlySalary?.currency, - value: x.monthlySalary?.value, + baseCurrency: baseCurrencyString, + baseValue: await convert( + x.monthlySalary.value, + x.monthlySalary.currency, + baseCurrencyString, + ), + currency: x.monthlySalary.currency, + value: x.monthlySalary.value, }, }, specialization: x.specialization, @@ -379,107 +402,141 @@ export const offersProfileRouter = createRouter() }, editToken: token, offers: { - create: input.offers.map((x) => { - if ( - x.jobType === 'INTERN' && - x.offersIntern && - x.offersIntern.internshipCycle && - x.offersIntern.monthlySalary?.currency && - x.offersIntern.monthlySalary.value && - x.offersIntern.startYear - ) { - return { - comments: x.comments, - company: { - connect: { - id: x.companyId, + create: await Promise.all( + input.offers.map(async (x) => { + if ( + x.jobType === JobType.INTERN && + x.offersIntern && + x.offersIntern.internshipCycle != null && + x.offersIntern.monthlySalary?.currency != null && + x.offersIntern.monthlySalary?.value != null && + x.offersIntern.startYear != null + ) { + return { + comments: x.comments, + company: { + connect: { + id: x.companyId, + }, }, - }, - jobType: x.jobType, - location: x.location, - monthYearReceived: x.monthYearReceived, - negotiationStrategy: x.negotiationStrategy, - offersIntern: { - create: { - internshipCycle: x.offersIntern.internshipCycle, - monthlySalary: { - create: { - currency: x.offersIntern.monthlySalary?.currency, - value: x.offersIntern.monthlySalary?.value, + jobType: x.jobType, + location: x.location, + monthYearReceived: x.monthYearReceived, + negotiationStrategy: x.negotiationStrategy, + offersIntern: { + create: { + internshipCycle: x.offersIntern.internshipCycle, + monthlySalary: { + create: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + x.offersIntern.monthlySalary.value, + x.offersIntern.monthlySalary.currency, + baseCurrencyString, + ), + currency: x.offersIntern.monthlySalary.currency, + value: x.offersIntern.monthlySalary.value, + }, }, + specialization: x.offersIntern.specialization, + startYear: x.offersIntern.startYear, + title: x.offersIntern.title, }, - specialization: x.offersIntern.specialization, - startYear: x.offersIntern.startYear, - title: x.offersIntern.title, }, - }, - }; - } - if ( - x.jobType === 'FULLTIME' && - x.offersFullTime && - x.offersFullTime.baseSalary?.currency && - x.offersFullTime.baseSalary?.value && - x.offersFullTime.bonus?.currency && - x.offersFullTime.bonus?.value && - x.offersFullTime.stocks?.currency && - x.offersFullTime.stocks?.value && - x.offersFullTime.totalCompensation?.currency && - x.offersFullTime.totalCompensation?.value && - x.offersFullTime.level - ) { - return { - comments: x.comments, - company: { - connect: { - id: x.companyId, + }; + } + if ( + x.jobType === JobType.FULLTIME && + x.offersFullTime && + x.offersFullTime.baseSalary?.currency != null && + x.offersFullTime.baseSalary?.value != null && + x.offersFullTime.bonus?.currency != null && + x.offersFullTime.bonus?.value != null && + x.offersFullTime.stocks?.currency != null && + x.offersFullTime.stocks?.value != null && + x.offersFullTime.totalCompensation?.currency != null && + x.offersFullTime.totalCompensation?.value != null && + x.offersFullTime.level != null && + x.offersFullTime.title != null && + x.offersFullTime.specialization != null + ) { + return { + comments: x.comments, + company: { + connect: { + id: x.companyId, + }, }, - }, - jobType: x.jobType, - location: x.location, - monthYearReceived: x.monthYearReceived, - negotiationStrategy: x.negotiationStrategy, - offersFullTime: { - create: { - baseSalary: { - create: { - currency: x.offersFullTime.baseSalary?.currency, - value: x.offersFullTime.baseSalary?.value, + jobType: x.jobType, + location: x.location, + monthYearReceived: x.monthYearReceived, + negotiationStrategy: x.negotiationStrategy, + offersFullTime: { + create: { + baseSalary: { + create: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + x.offersFullTime.baseSalary.value, + x.offersFullTime.baseSalary.currency, + baseCurrencyString, + ), + currency: x.offersFullTime.baseSalary.currency, + value: x.offersFullTime.baseSalary.value, + }, }, - }, - bonus: { - create: { - currency: x.offersFullTime.bonus?.currency, - value: x.offersFullTime.bonus?.value, + bonus: { + create: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + x.offersFullTime.bonus.value, + x.offersFullTime.bonus.currency, + baseCurrencyString, + ), + currency: x.offersFullTime.bonus.currency, + value: x.offersFullTime.bonus.value, + }, }, - }, - level: x.offersFullTime.level, - specialization: x.offersFullTime.specialization, - stocks: { - create: { - currency: x.offersFullTime.stocks?.currency, - value: x.offersFullTime.stocks?.value, + level: x.offersFullTime.level, + specialization: x.offersFullTime.specialization, + stocks: { + create: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + x.offersFullTime.stocks.value, + x.offersFullTime.stocks.currency, + baseCurrencyString, + ), + currency: x.offersFullTime.stocks.currency, + value: x.offersFullTime.stocks.value, + }, }, - }, - title: x.offersFullTime.title, - totalCompensation: { - create: { - currency: - x.offersFullTime.totalCompensation?.currency, - value: x.offersFullTime.totalCompensation?.value, + title: x.offersFullTime.title, + totalCompensation: { + create: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + x.offersFullTime.totalCompensation.value, + x.offersFullTime.totalCompensation.currency, + baseCurrencyString, + ), + currency: + x.offersFullTime.totalCompensation.currency, + value: x.offersFullTime.totalCompensation.value, + }, }, }, }, - }, - }; - } + }; + } - // Throw error - throw new trpc.TRPCError({ - code: 'BAD_REQUEST', - message: 'Missing fields.', - }); - }), + // Throw error + throw new trpc.TRPCError({ + code: 'BAD_REQUEST', + message: 'Missing fields.', + }); + }), + ), }, profileName: randomUUID().substring(0, 10), }, @@ -510,7 +567,7 @@ export const offersProfileRouter = createRouter() return deletedProfile.id; } - // TODO: Throw 401 + throw new trpc.TRPCError({ code: 'UNAUTHORIZED', message: 'Invalid token.', @@ -535,7 +592,6 @@ export const offersProfileRouter = createRouter() totalYoe: z.number(), }), createdAt: z.string().optional(), - // Discussion: z.array(reply), id: z.string(), isEditable: z.boolean().nullish(), offers: z.array(offer), @@ -573,19 +629,21 @@ export const offersProfileRouter = createRouter() }); // Delete educations - const educationsId = (await ctx.prisma.offersEducation.findMany({ - where: { - backgroundId: input.background.id - } - })).map((x) => x.id) + const educationsId = ( + await ctx.prisma.offersEducation.findMany({ + where: { + backgroundId: input.background.id, + }, + }) + ).map((x) => x.id); for (const id of educationsId) { if (!input.background.educations.map((x) => x.id).includes(id)) { await ctx.prisma.offersEducation.delete({ where: { - id - } - }) + id, + }, + }); } } @@ -626,19 +684,21 @@ export const offersProfileRouter = createRouter() } // Delete experiences - const experiencesId = (await ctx.prisma.offersExperience.findMany({ - where: { - backgroundId: input.background.id - } - })).map((x) => x.id) + const experiencesId = ( + await ctx.prisma.offersExperience.findMany({ + where: { + backgroundId: input.background.id, + }, + }) + ).map((x) => x.id); for (const id of experiencesId) { if (!input.background.experiences.map((x) => x.id).includes(id)) { await ctx.prisma.offersExperience.delete({ where: { - id - } - }) + id, + }, + }); } } @@ -660,6 +720,12 @@ export const offersProfileRouter = createRouter() if (exp.monthlySalary) { await ctx.prisma.offersCurrency.update({ data: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + exp.monthlySalary.value, + exp.monthlySalary.currency, + baseCurrencyString, + ), currency: exp.monthlySalary.currency, value: exp.monthlySalary.value, }, @@ -672,6 +738,12 @@ export const offersProfileRouter = createRouter() if (exp.totalCompensation) { await ctx.prisma.offersCurrency.update({ data: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + exp.totalCompensation.value, + exp.totalCompensation.currency, + baseCurrencyString, + ), currency: exp.totalCompensation.currency, value: exp.totalCompensation.value, }, @@ -683,9 +755,9 @@ export const offersProfileRouter = createRouter() } else if (!exp.id) { // Create new experience if ( - exp.jobType === 'FULLTIME' && - exp.totalCompensation?.currency !== undefined && - exp.totalCompensation.value !== undefined + exp.jobType === JobType.FULLTIME && + exp.totalCompensation?.currency != null && + exp.totalCompensation?.value != null ) { if (exp.companyId) { await ctx.prisma.offersBackground.update({ @@ -700,12 +772,19 @@ export const offersProfileRouter = createRouter() durationInMonths: exp.durationInMonths, jobType: exp.jobType, level: exp.level, + location: exp.location, specialization: exp.specialization, title: exp.title, totalCompensation: { create: { - currency: exp.totalCompensation?.currency, - value: exp.totalCompensation?.value, + baseCurrency: baseCurrencyString, + baseValue: await convert( + exp.totalCompensation.value, + exp.totalCompensation.currency, + baseCurrencyString, + ), + currency: exp.totalCompensation.currency, + value: exp.totalCompensation.value, }, }, }, @@ -723,12 +802,19 @@ export const offersProfileRouter = createRouter() durationInMonths: exp.durationInMonths, jobType: exp.jobType, level: exp.level, + location: exp.location, specialization: exp.specialization, title: exp.title, totalCompensation: { create: { - currency: exp.totalCompensation?.currency, - value: exp.totalCompensation?.value, + baseCurrency: baseCurrencyString, + baseValue: await convert( + exp.totalCompensation.value, + exp.totalCompensation.currency, + baseCurrencyString, + ), + currency: exp.totalCompensation.currency, + value: exp.totalCompensation.value, }, }, }, @@ -740,9 +826,9 @@ export const offersProfileRouter = createRouter() }); } } else if ( - exp.jobType === 'INTERN' && - exp.monthlySalary?.currency !== undefined && - exp.monthlySalary.value !== undefined + exp.jobType === JobType.INTERN && + exp.monthlySalary?.currency != null && + exp.monthlySalary?.value != null ) { if (exp.companyId) { await ctx.prisma.offersBackground.update({ @@ -756,10 +842,17 @@ export const offersProfileRouter = createRouter() }, durationInMonths: exp.durationInMonths, jobType: exp.jobType, + location: exp.location, monthlySalary: { create: { - currency: exp.monthlySalary?.currency, - value: exp.monthlySalary?.value, + baseCurrency: baseCurrencyString, + baseValue: await convert( + exp.monthlySalary.value, + exp.monthlySalary.currency, + baseCurrencyString, + ), + currency: exp.monthlySalary.currency, + value: exp.monthlySalary.value, }, }, specialization: exp.specialization, @@ -778,10 +871,17 @@ export const offersProfileRouter = createRouter() create: { durationInMonths: exp.durationInMonths, jobType: exp.jobType, + location: exp.location, monthlySalary: { create: { - currency: exp.monthlySalary?.currency, - value: exp.monthlySalary?.value, + baseCurrency: baseCurrencyString, + baseValue: await convert( + exp.monthlySalary.value, + exp.monthlySalary.currency, + baseCurrencyString, + ), + currency: exp.monthlySalary.currency, + value: exp.monthlySalary.value, }, }, specialization: exp.specialization, @@ -799,19 +899,21 @@ export const offersProfileRouter = createRouter() } // Delete specific yoes - const yoesId = (await ctx.prisma.offersSpecificYoe.findMany({ - where: { - backgroundId: input.background.id - } - })).map((x) => x.id) + const yoesId = ( + await ctx.prisma.offersSpecificYoe.findMany({ + where: { + backgroundId: input.background.id, + }, + }) + ).map((x) => x.id); for (const id of yoesId) { if (!input.background.specificYoes.map((x) => x.id).includes(id)) { await ctx.prisma.offersSpecificYoe.delete({ where: { - id - } - }) + id, + }, + }); } } @@ -845,19 +947,21 @@ export const offersProfileRouter = createRouter() } // Delete specific offers - const offers = (await ctx.prisma.offersOffer.findMany({ - where: { - profileId: input.id - } - })).map((x) => x.id) + const offers = ( + await ctx.prisma.offersOffer.findMany({ + where: { + profileId: input.id, + }, + }) + ).map((x) => x.id); for (const id of offers) { if (!input.offers.map((x) => x.id).includes(id)) { await ctx.prisma.offersOffer.delete({ where: { - id - } - }) + id, + }, + }); } } @@ -869,6 +973,10 @@ export const offersProfileRouter = createRouter() data: { comments: offerToUpdate.comments, companyId: offerToUpdate.companyId, + jobType: + offerToUpdate.jobType === JobType.FULLTIME + ? JobType.FULLTIME + : JobType.INTERN, location: offerToUpdate.location, monthYearReceived: offerToUpdate.monthYearReceived, negotiationStrategy: offerToUpdate.negotiationStrategy, @@ -878,21 +986,7 @@ export const offersProfileRouter = createRouter() }, }); - if ( - offerToUpdate.jobType === 'INTERN' || - offerToUpdate.jobType === 'FULLTIME' - ) { - await ctx.prisma.offersOffer.update({ - data: { - jobType: offerToUpdate.jobType, - }, - where: { - id: offerToUpdate.id, - }, - }); - } - - if (offerToUpdate.offersIntern?.monthlySalary) { + if (offerToUpdate.offersIntern?.monthlySalary != null) { await ctx.prisma.offersIntern.update({ data: { internshipCycle: @@ -907,6 +1001,12 @@ export const offersProfileRouter = createRouter() }); await ctx.prisma.offersCurrency.update({ data: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + offerToUpdate.offersIntern.monthlySalary.value, + offerToUpdate.offersIntern.monthlySalary.currency, + baseCurrencyString, + ), currency: offerToUpdate.offersIntern.monthlySalary.currency, value: offerToUpdate.offersIntern.monthlySalary.value, }, @@ -916,7 +1016,7 @@ export const offersProfileRouter = createRouter() }); } - if (offerToUpdate.offersFullTime?.totalCompensation) { + if (offerToUpdate.offersFullTime?.totalCompensation != null) { await ctx.prisma.offersFullTime.update({ data: { level: offerToUpdate.offersFullTime.level ?? undefined, @@ -927,9 +1027,15 @@ export const offersProfileRouter = createRouter() id: offerToUpdate.offersFullTime.id, }, }); - if (offerToUpdate.offersFullTime.baseSalary) { + if (offerToUpdate.offersFullTime.baseSalary != null) { await ctx.prisma.offersCurrency.update({ data: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + offerToUpdate.offersFullTime.baseSalary.value, + offerToUpdate.offersFullTime.baseSalary.currency, + baseCurrencyString, + ), currency: offerToUpdate.offersFullTime.baseSalary.currency, value: offerToUpdate.offersFullTime.baseSalary.value, }, @@ -941,6 +1047,12 @@ export const offersProfileRouter = createRouter() if (offerToUpdate.offersFullTime.bonus) { await ctx.prisma.offersCurrency.update({ data: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + offerToUpdate.offersFullTime.bonus.value, + offerToUpdate.offersFullTime.bonus.currency, + baseCurrencyString, + ), currency: offerToUpdate.offersFullTime.bonus.currency, value: offerToUpdate.offersFullTime.bonus.value, }, @@ -952,6 +1064,12 @@ export const offersProfileRouter = createRouter() if (offerToUpdate.offersFullTime.stocks) { await ctx.prisma.offersCurrency.update({ data: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + offerToUpdate.offersFullTime.stocks.value, + offerToUpdate.offersFullTime.stocks.currency, + baseCurrencyString, + ), currency: offerToUpdate.offersFullTime.stocks.currency, value: offerToUpdate.offersFullTime.stocks.value, }, @@ -962,6 +1080,12 @@ export const offersProfileRouter = createRouter() } await ctx.prisma.offersCurrency.update({ data: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + offerToUpdate.offersFullTime.totalCompensation.value, + offerToUpdate.offersFullTime.totalCompensation.currency, + baseCurrencyString, + ), currency: offerToUpdate.offersFullTime.totalCompensation.currency, value: offerToUpdate.offersFullTime.totalCompensation.value, @@ -974,12 +1098,12 @@ export const offersProfileRouter = createRouter() } else { // Create new offer if ( - offerToUpdate.jobType === 'INTERN' && + offerToUpdate.jobType === JobType.INTERN && offerToUpdate.offersIntern && - offerToUpdate.offersIntern.internshipCycle && - offerToUpdate.offersIntern.monthlySalary?.currency && - offerToUpdate.offersIntern.monthlySalary.value && - offerToUpdate.offersIntern.startYear + offerToUpdate.offersIntern.internshipCycle != null && + offerToUpdate.offersIntern.monthlySalary?.currency != null && + offerToUpdate.offersIntern.monthlySalary?.value != null && + offerToUpdate.offersIntern.startYear != null ) { await ctx.prisma.offersProfile.update({ data: { @@ -1001,11 +1125,18 @@ export const offersProfileRouter = createRouter() offerToUpdate.offersIntern.internshipCycle, monthlySalary: { create: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + offerToUpdate.offersIntern.monthlySalary.value, + offerToUpdate.offersIntern.monthlySalary + .currency, + baseCurrencyString, + ), currency: offerToUpdate.offersIntern.monthlySalary - ?.currency, + .currency, value: - offerToUpdate.offersIntern.monthlySalary?.value, + offerToUpdate.offersIntern.monthlySalary.value, }, }, specialization: @@ -1023,17 +1154,18 @@ export const offersProfileRouter = createRouter() }); } if ( - offerToUpdate.jobType === 'FULLTIME' && + offerToUpdate.jobType === JobType.FULLTIME && offerToUpdate.offersFullTime && - offerToUpdate.offersFullTime.baseSalary?.currency && - offerToUpdate.offersFullTime.baseSalary?.value && - offerToUpdate.offersFullTime.bonus?.currency && - offerToUpdate.offersFullTime.bonus?.value && - offerToUpdate.offersFullTime.stocks?.currency && - offerToUpdate.offersFullTime.stocks?.value && - offerToUpdate.offersFullTime.totalCompensation?.currency && - offerToUpdate.offersFullTime.totalCompensation?.value && - offerToUpdate.offersFullTime.level + offerToUpdate.offersFullTime.baseSalary?.currency != null && + offerToUpdate.offersFullTime.baseSalary?.value != null && + offerToUpdate.offersFullTime.bonus?.currency != null && + offerToUpdate.offersFullTime.bonus?.value != null && + offerToUpdate.offersFullTime.stocks?.currency != null && + offerToUpdate.offersFullTime.stocks?.value != null && + offerToUpdate.offersFullTime.totalCompensation?.currency != + null && + offerToUpdate.offersFullTime.totalCompensation?.value != null && + offerToUpdate.offersFullTime.level != null ) { await ctx.prisma.offersProfile.update({ data: { @@ -1053,18 +1185,31 @@ export const offersProfileRouter = createRouter() create: { baseSalary: { create: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + offerToUpdate.offersFullTime.baseSalary.value, + offerToUpdate.offersFullTime.baseSalary + .currency, + baseCurrencyString, + ), currency: offerToUpdate.offersFullTime.baseSalary - ?.currency, + .currency, value: - offerToUpdate.offersFullTime.baseSalary?.value, + offerToUpdate.offersFullTime.baseSalary.value, }, }, bonus: { create: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + offerToUpdate.offersFullTime.bonus.value, + offerToUpdate.offersFullTime.bonus.currency, + baseCurrencyString, + ), currency: - offerToUpdate.offersFullTime.bonus?.currency, - value: offerToUpdate.offersFullTime.bonus?.value, + offerToUpdate.offersFullTime.bonus.currency, + value: offerToUpdate.offersFullTime.bonus.value, }, }, level: offerToUpdate.offersFullTime.level, @@ -1072,20 +1217,34 @@ export const offersProfileRouter = createRouter() offerToUpdate.offersFullTime.specialization, stocks: { create: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + offerToUpdate.offersFullTime.stocks.value, + offerToUpdate.offersFullTime.stocks.currency, + baseCurrencyString, + ), currency: - offerToUpdate.offersFullTime.stocks?.currency, - value: offerToUpdate.offersFullTime.stocks?.value, + offerToUpdate.offersFullTime.stocks.currency, + value: offerToUpdate.offersFullTime.stocks.value, }, }, title: offerToUpdate.offersFullTime.title, totalCompensation: { create: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + offerToUpdate.offersFullTime.totalCompensation + .value, + offerToUpdate.offersFullTime.totalCompensation + .currency, + baseCurrencyString, + ), currency: offerToUpdate.offersFullTime.totalCompensation - ?.currency, + .currency, value: offerToUpdate.offersFullTime.totalCompensation - ?.value, + .value, }, }, }, @@ -1102,46 +1261,6 @@ export const offersProfileRouter = createRouter() } const result = await ctx.prisma.offersProfile.findFirst({ - include: { - background: { - include: { - educations: true, - experiences: { - include: { - company: true, - monthlySalary: true, - totalCompensation: true, - }, - }, - specificYoes: true, - }, - }, - discussion: { - include: { - replies: true, - replyingTo: true, - user: true, - }, - }, - offers: { - include: { - company: true, - offersFullTime: { - include: { - baseSalary: true, - bonus: true, - stocks: true, - totalCompensation: true, - }, - }, - offersIntern: { - include: { - monthlySalary: true, - }, - }, - }, - }, - }, where: { id: input.id, }, diff --git a/apps/portal/src/server/router/offers/offers.ts b/apps/portal/src/server/router/offers/offers.ts index 7e511164..a68b19dd 100644 --- a/apps/portal/src/server/router/offers/offers.ts +++ b/apps/portal/src/server/router/offers/offers.ts @@ -7,6 +7,7 @@ import { } from '~/mappers/offers-mappers'; import { convert } from '~/utils/offers/currency/currency-exchange'; import { Currency } from '~/utils/offers/currency/CurrencyEnum'; +import { createValidationRegex } from '~/utils/offers/zodRegex'; import { createRouter } from '../context'; @@ -23,14 +24,6 @@ const sortingKeysMap = { totalYoe: 'totalYoe', }; -const createSortByValidationRegex = () => { - const startsWithPlusOrMinusOnly = '^[+-]{1}'; - const sortingKeysRegex = Object.entries(sortingKeysMap) - .map((entry) => entry[0]) - .join('|'); - return new RegExp(startsWithPlusOrMinusOnly + '(' + sortingKeysRegex + ')'); -}; - const yoeCategoryMap: Record = { 0: 'Internship', 1: 'Fresh Grad', @@ -59,7 +52,10 @@ export const offersRouter = createRouter().query('list', { offset: z.number().nonnegative(), salaryMax: z.number().nonnegative().nullish(), salaryMin: z.number().nonnegative().nullish(), - sortBy: z.string().regex(createSortByValidationRegex()).nullish(), + sortBy: z + .string() + .regex(createValidationRegex(Object.keys(sortingKeysMap), '[+-]{1}')) + .nullish(), title: z.string().nullish(), yoeCategory: z.number().min(0).max(3), yoeMax: z.number().max(100).nullish(), @@ -70,8 +66,6 @@ export const offersRouter = createRouter().query('list', { const yoeMin = input.yoeMin ? input.yoeMin : yoeRange?.minYoe; const yoeMax = input.yoeMax ? input.yoeMax : yoeRange?.maxYoe; - // Const orderBy = getSortingOrderAndKey(input.sortBy, input.yoeCategory); - if (!input.sortBy) { input.sortBy = '-' + sortingKeysMap.monthYearReceived; } @@ -338,112 +332,6 @@ export const offersRouter = createRouter().query('list', { ); } - // SORTING - // data = data.sort((offer1, offer2) => { - // const defaultReturn = - // offer2.monthYearReceived.getTime() - offer1.monthYearReceived.getTime(); - - // if (!input.sortBy) { - // return defaultReturn; - // } - - // const order = input.sortBy.charAt(0); - // const sortingKey = input.sortBy.substring(1); - - // if (order === ascOrder) { - // return (() => { - // if (sortingKey === 'monthYearReceived') { - // return ( - // offer1.monthYearReceived.getTime() - - // offer2.monthYearReceived.getTime() - // ); - // } - - // if (sortingKey === 'totalCompensation') { - // const salary1 = offer1.offersFullTime?.totalCompensation.value - // ? offer1.offersFullTime?.totalCompensation.value - // : offer1.offersIntern?.monthlySalary.value; - - // const salary2 = offer2.offersFullTime?.totalCompensation.value - // ? offer2.offersFullTime?.totalCompensation.value - // : offer2.offersIntern?.monthlySalary.value; - - // if (salary1 == null || salary2 == null) { - // throw new TRPCError({ - // code: 'NOT_FOUND', - // message: 'Total Compensation or Salary not found', - // }); - // } - - // return salary1 - salary2; - // } - - // if (sortingKey === 'totalYoe') { - // const yoe1 = offer1.profile.background?.totalYoe; - // const yoe2 = offer2.profile.background?.totalYoe; - - // if (yoe1 == null || yoe2 == null) { - // throw new TRPCError({ - // code: 'NOT_FOUND', - // message: 'Total years of experience not found', - // }); - // } - - // return yoe1 - yoe2; - // } - - // return defaultReturn; - // })(); - // } - - // if (order === descOrder) { - // return (() => { - // if (sortingKey === 'monthYearReceived') { - // return ( - // offer2.monthYearReceived.getTime() - - // offer1.monthYearReceived.getTime() - // ); - // } - - // if (sortingKey === 'totalCompensation') { - // const salary1 = offer1.offersFullTime?.totalCompensation.value - // ? offer1.offersFullTime?.totalCompensation.value - // : offer1.offersIntern?.monthlySalary.value; - - // const salary2 = offer2.offersFullTime?.totalCompensation.value - // ? offer2.offersFullTime?.totalCompensation.value - // : offer2.offersIntern?.monthlySalary.value; - - // if (salary1 == null || salary2 == null) { - // throw new TRPCError({ - // code: 'NOT_FOUND', - // message: 'Total Compensation or Salary not found', - // }); - // } - - // return salary2 - salary1; - // } - - // if (sortingKey === 'totalYoe') { - // const yoe1 = offer1.profile.background?.totalYoe; - // const yoe2 = offer2.profile.background?.totalYoe; - - // if (yoe1 == null || yoe2 == null) { - // throw new TRPCError({ - // code: 'NOT_FOUND', - // message: 'Total years of experience not found', - // }); - // } - - // return yoe2 - yoe1; - // } - - // return defaultReturn; - // })(); - // } - // return defaultReturn; - // }); - const startRecordIndex: number = input.limit * input.offset; const endRecordIndex: number = startRecordIndex + input.limit <= data.length diff --git a/apps/portal/src/utils/offers/currency/currency-exchange.ts b/apps/portal/src/utils/offers/currency/currency-exchange.ts index 31e02067..4c94209a 100644 --- a/apps/portal/src/utils/offers/currency/currency-exchange.ts +++ b/apps/portal/src/utils/offers/currency/currency-exchange.ts @@ -6,7 +6,11 @@ export const convert = async ( ) => { fromCurrency = fromCurrency.trim().toLowerCase(); toCurrency = toCurrency.trim().toLowerCase(); - const url = ['https://cdn.jsdelivr.net/gh/fawazahmed0/currency-api@1/latest/currencies', fromCurrency, toCurrency].join('/'); + const url = [ + 'https://cdn.jsdelivr.net/gh/fawazahmed0/currency-api@1/latest/currencies', + fromCurrency, + toCurrency, + ].join('/'); return await fetch(url + '.json') .then((res) => res.json()) diff --git a/apps/portal/src/utils/offers/currency/index.tsx b/apps/portal/src/utils/offers/currency/index.tsx index 373d2984..1e219c45 100644 --- a/apps/portal/src/utils/offers/currency/index.tsx +++ b/apps/portal/src/utils/offers/currency/index.tsx @@ -1,5 +1,9 @@ import type { Money } from '~/components/offers/types'; +import { Currency } from './CurrencyEnum'; + +export const baseCurrencyString = Currency.USD.toString(); + export function convertMoneyToString({ currency, value }: Money) { if (!value) { return '-'; diff --git a/apps/portal/src/utils/offers/zodRegex.ts b/apps/portal/src/utils/offers/zodRegex.ts new file mode 100644 index 00000000..614b76d4 --- /dev/null +++ b/apps/portal/src/utils/offers/zodRegex.ts @@ -0,0 +1,8 @@ +export const createValidationRegex = ( + keywordArray: Array, + prepend: string | null | undefined, +) => { + const sortingKeysRegex = keywordArray.join('|'); + prepend = prepend != null ? prepend : ''; + return new RegExp('^' + prepend + '(' + sortingKeysRegex + ')$'); +};