From 05ee329750539d37e7356371c096de1b58ff11ff Mon Sep 17 00:00:00 2001 From: Alexis Janvier Date: Fri, 1 May 2020 18:22:04 +0200 Subject: [PATCH 1/2] feat: migration --- .../20200501153317_extend-jobposting-model.js | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 apps/api/migrations/20200501153317_extend-jobposting-model.js diff --git a/apps/api/migrations/20200501153317_extend-jobposting-model.js b/apps/api/migrations/20200501153317_extend-jobposting-model.js new file mode 100644 index 0000000..576d116 --- /dev/null +++ b/apps/api/migrations/20200501153317_extend-jobposting-model.js @@ -0,0 +1,23 @@ +exports.up = function (knex) { + return knex.schema.table('job_posting', (table) => { + table.jsonb('base_salary').nullable(); + table + .enum('job_location_type', [ + 'office', + 'remote', + 'remote and office', + 'remote or office', + ]) + .notNullable() + .defaultTo('office'); + table.boolean('job_immediate_start').defaultTo(false); + }); +}; + +exports.down = function (knex) { + return knex.schema.table('job_posting', (table) => { + table.dropColumn('base_salary'); + table.dropColumn('job_location_type'); + table.dropColumn('job_immediate_start'); + }); +}; From 7bbab9ba06f7d20e7653227e55db2ed327a9a722 Mon Sep 17 00:00:00 2001 From: Alexis Janvier Date: Mon, 4 May 2020 09:12:13 +0200 Subject: [PATCH 2/2] feat: preparation des offres avant persistance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit et mise à jours des fixtures --- apps/api/fixtures/fixed-fixtures.json | 19 ++++- apps/api/src/job-posting/repository.js | 63 ++++++++++++++++- apps/api/src/job-posting/repository.spec.js | 78 +++++++++++++++++++++ tests-e2e/api/job-postings.spec.js | 3 + 4 files changed, 159 insertions(+), 4 deletions(-) diff --git a/apps/api/fixtures/fixed-fixtures.json b/apps/api/fixtures/fixed-fixtures.json index 0ad27fe..34b466f 100644 --- a/apps/api/fixtures/fixed-fixtures.json +++ b/apps/api/fixtures/fixed-fixtures.json @@ -29,7 +29,12 @@ "experienceRequirements": "* Vous êtes Bac+4 / 5 issu(e) d’une formation supérieure (Ecole d’ingénieurs, Master, DESS…), avec une forte dominante mathématique * Vous avez déjà 4 ans d’expérience en Data Science * Vous avez des bases solides requises en algorithmes de Machine Learning * Vous maitrisez le langage python * Vous présentez un intérêt pour le NLP ou souhaitez approfondir vos connaissances en traitement de données non structurées * Vous êtes intéressé(e) par d’autres technos (Spark, SQL, …) et pouvez vous adapter à un contexte technologique qui évolue * Vous avez une très bonne communication orale * Autonome, passionné(e) par la data et le travail dans une start up * Une connaissance de l’adtech est un plus * Mais surtout : vous avez une passion pour un projet startup qui a de grandes ambitions et une volonté à toute épreuve !", "jobStartDate": "2020-05-02", "skills": "Machine learning, Python, Spark, SQL ", - "validThrough": "2020-04-15" + "validThrough": "2020-04-15", + "jobLocationType": "office", + "jobImmediateStart": false, + "baseSalary": { + "value": 45 + } }, { "title": "Ingénieur Lead Full Stack technico-fonctionnel", @@ -61,7 +66,13 @@ "experienceRequirements": "* Expert en développement web. Maîtrise des langages de programmation web * back/front (php, go, bash, js, etc ..), des frameworks (laravel, vue, react, etc ..) * Maîtrise en développement mobile * Connaissance et maitrise des ops: server, monitoring, backup, sécurité * Connaissance du métier de la BU", "jobStartDate": "2020-05-01", "skills": "Php, Go, Bash, Js, Laravel, Vue, React, admin sys", - "validThrough": "2020-02-02" + "validThrough": "2020-02-02", + "jobLocationType": "office", + "jobImmediateStart": true, + "baseSalary": { + "minValue": 45, + "maxValue": 55 + } }, { "title": "R&D Software Engineer", @@ -93,6 +104,8 @@ "experienceRequirements": "★ Master in software engineering ★ You can translate real-world problems into automated real-time decision solutions. ★ You can build decision algorithms that work efficiently in regimes with high uncertainty. ★ You have experience in object-oriented and/or functional programming ★ You are deliverable-focused with a pragmatic and business-oriented attitude. ★ You are a team player, yet able to work independently. ★ You are eager to look for creative solutions using state of the art techniques. ★ You have an interest in Energy and IoT applications", "jobStartDate": "2020-06-01", "skills": "Java, Kotlin, Docker, Sql, Mongo, Devops", - "validThrough": "" + "validThrough": "", + "jobLocationType": "office", + "jobImmediateStart": false } ] diff --git a/apps/api/src/job-posting/repository.js b/apps/api/src/job-posting/repository.js index adcb9ba..9eacb5d 100644 --- a/apps/api/src/job-posting/repository.js +++ b/apps/api/src/job-posting/repository.js @@ -193,6 +193,65 @@ const getOne = async (id) => { .catch((error) => ({ error })); }; +const jobLocationTypes = [ + 'office', + 'remote', + 'remote and office', + 'remote or office', +]; + +/** + * format baseSalary data in a well formated way. + * + * @param {object} apiBaseSalary - data about baseSalary from API + * @returns {object} baseSalary well formated for db save + */ +const formatBaseSalary = (apiBaseSalary) => { + if ( + !apiBaseSalary || + (!apiBaseSalary.value && + (!apiBaseSalary.minValue || !apiBaseSalary.maxValue)) + ) { + return null; + } + const { + currency = 'EUR brut annuel', + minValue = null, + maxValue = null, + value = null, + } = apiBaseSalary; + + return { + currency, + minValue, + maxValue, + value: value + ? value + : minValue && maxValue + ? Math.round((parseInt(minValue, 10) + parseInt(maxValue, 10)) / 2) + : null, + }; +}; + +/** + * Prepare data from API for create or update a jobPosting in db. + * + * @param {object} apiData - data from API + * @returns {object} data well formated for db save + */ +const prepareDataForDb = (apiData) => { + const formatedBaseSalary = formatBaseSalary(apiData.baseSalary); + return { + ...apiData, + jobLocationType: jobLocationTypes.includes(apiData.jobLocationType) + ? apiData.jobLocationType + : 'office', + baseSalary: formatedBaseSalary + ? JSON.stringify(formatedBaseSalary) + : null, + }; +}; + /** * Return the created jobPosting * @@ -212,7 +271,7 @@ const createOne = async (apiData) => { return client(tableName) .returning('id') - .insert(apiData) + .insert(prepareDataForDb(apiData)) .then(([newJobPostingId]) => { return getOneByIdQuery(client, newJobPostingId).then( formatJobPostingForAPI @@ -290,9 +349,11 @@ const updateOne = async (id, apiData) => { module.exports = { createOne, deleteOne, + formatBaseSalary, formatJobPostingForAPI, getOne, getPaginatedList, + prepareDataForDb, renameFiltersFromAPI, updateOne, }; diff --git a/apps/api/src/job-posting/repository.spec.js b/apps/api/src/job-posting/repository.spec.js index 31b4b9a..e6c4191 100644 --- a/apps/api/src/job-posting/repository.spec.js +++ b/apps/api/src/job-posting/repository.spec.js @@ -1,4 +1,6 @@ const { + formatBaseSalary, + prepareDataForDb, formatJobPostingForAPI, renameFiltersFromAPI, } = require('./repository'); @@ -38,6 +40,82 @@ describe('jobPosting repository', () => { }); }); + describe('prepareDataForDb', () => { + it('should set default job location type if it is not set', () => { + const formatedData = prepareDataForDb({}); + expect(formatedData.jobLocationType).toEqual('office'); + }); + + it('should set default job location type if ray data is not valid', () => { + const formatedData = prepareDataForDb({ + jobLocationType: 'maison', + }); + expect(formatedData.jobLocationType).toEqual('office'); + }); + + it('should stringify base salary if it is not null', () => { + const formatedData = prepareDataForDb({ + jobLocationType: 'maison', + baseSalary: { value: 10 }, + }); + expect(formatedData.baseSalary).toEqual( + JSON.stringify({ + currency: 'EUR brut annuel', + minValue: null, + maxValue: null, + value: 10, + }) + ); + }); + }); + describe('formatBaseSalary', () => { + it('should return null if base salary is null', () => { + expect(formatBaseSalary(null)).toBeNull(); + }); + + it('should return null if base salary neither value neither minValue AND maxValue', () => { + expect(formatBaseSalary({})).toBeNull(); + expect(formatBaseSalary({ minValue: 10 })).toBeNull(); + expect(formatBaseSalary({ maxValue: 20 })).toBeNull(); + expect( + formatBaseSalary({ minValue: 10, maxValue: 20 }) + ).not.toBeNull(); + expect(formatBaseSalary({ value: 20 })).not.toBeNull(); + }); + + it('should compute value from minValue and maxValue if value is not set', () => { + const baseSalary = formatBaseSalary({ + minValue: 10, + maxValue: 20, + }); + expect(baseSalary.value).toEqual(15); + }); + + it('should not compute value from minValue and maxValue if value is not set', () => { + const baseSalary = formatBaseSalary({ + minValue: 10, + maxValue: 20, + value: 12, + }); + expect(baseSalary.value).toEqual(12); + }); + + it('should set default currency if currency is not set', () => { + const baseSalary = formatBaseSalary({ + value: 12, + }); + expect(baseSalary.currency).toEqual('EUR brut annuel'); + }); + + it('should not set default currency if currency is set', () => { + const baseSalary = formatBaseSalary({ + currency: 'USD', + value: 12, + }); + expect(baseSalary.currency).toEqual('USD'); + }); + }); + describe('formatJobPostingForAPI', () => { it('should return an empty object if return from db is null', () => { expect(formatJobPostingForAPI(null)).toEqual({}); diff --git a/tests-e2e/api/job-postings.spec.js b/tests-e2e/api/job-postings.spec.js index 9b38f29..0c2371a 100644 --- a/tests-e2e/api/job-postings.spec.js +++ b/tests-e2e/api/job-postings.spec.js @@ -647,6 +647,9 @@ describe('JobPostings API Endpoints', () => { jobStartDate: '2020-05-02', skills: 'JavaScript, Devops, Php, ...', validThrough: null, + baseSalary: null, + jobImmediateStart: false, + jobLocationType: 'office', hiringOrganization: { name: 'Flexcity', image: