diff --git a/backend/web/server/plugins/sosynpl/consts.js b/backend/web/server/plugins/sosynpl/consts.js index b6d10d4d64..8d96a7a83f 100644 --- a/backend/web/server/plugins/sosynpl/consts.js +++ b/backend/web/server/plugins/sosynpl/consts.js @@ -406,7 +406,7 @@ const DEFAULT_SEARCH_RADIUS=20 module.exports={ SOSYNPL, ROLES, COMPANY_SIZE, WORK_MODE, WORK_MODE_REMOTE, WORK_MODE_REMOTE_SITE, WORK_MODE_SITE, WORK_DURATION, WORK_DURATION_LESS_1_MONTH, WORK_DURATION_MORE_6_MONTH, WORK_DURATION__1_TO_6_MONTHS, VALID_STATUS, VALID_STATUS_PENDING, SOURCE, - DISCRIMINATOR_KEY, DISC_CUSTOMER, DISC_FREELANCE, DISC_ADMIN, DISC_CUSTOMER_FREELANCE, EXPERIENCE, ROLE_CUSTOMER, ROLE_FREELANCE, ROLE_ADMIN, + DISCRIMINATOR_KEY, DISC_CUSTOMER, DISC_FREELANCE, DISC_ADMIN, DISC_CUSTOMER_FREELANCE, EXPERIENCE, EXPERIENCE_EXPERIMENTED, EXPERIENCE_EXPERT, EXPERIENCE_JUNIOR, EXPERIENCE_SENIOR, ROLE_CUSTOMER, ROLE_FREELANCE, ROLE_ADMIN, LEGAL_STATUS, DEACTIVATION_REASON, SUSPEND_REASON, ACTIVITY_STATE, ACTIVITY_STATE_ACTIVE, ACTIVITY_STATE_STANDBY, ACTIVITY_STATE_SUSPENDED, ACTIVITY_STATE_DISABLED, MOBILITY, MOBILITY_CITY, MOBILITY_FRANCE, MOBILITY_REGIONS, AVAILABILITY, AVAILABILITY_UNDEFINED, AVAILABILITY_UNDEFINED, AVAILABILITY_ON, AVAILABILITY_OFF, SS_PILAR, @@ -421,6 +421,6 @@ module.exports={ SOSYNPL_COMMISSION_VAT_RATE, APPLICATION_REFUSE_REASON,REFUSE_REASON_CANCELED, REFUSE_REASON_PROVIDED, APPLICATION_STATUS_ACCEPTED, MISSION_STATUS, MISSION_STATUS_TO_COME, MISSION_STATUS_CURRENT, MISSION_STATUS_FREELANCE_FINISHED, MISSION_STATUS_CUSTOMER_FINISHED, MISSION_STATUS_CLOSED, REPORT_STATUS, REPORT_STATUS_DRAFT, REPORT_STATUS_DISPUTE, REPORT_STATUS_SENT, REPORT_STATUS_PAID, REPORT_STATUS_ACCEPTED, - SEARCH_MODE, DEFAULT_SEARCH_RADIUS, DURATION_UNIT_DAYS, DURATION_FILTERS + SEARCH_MODE, DEFAULT_SEARCH_RADIUS, DURATION_UNIT_DAYS, DURATION_FILTERS, DURATION_MONTH,SOURCE_RECOMMANDATION, } diff --git a/backend/web/server/plugins/sosynpl/search.js b/backend/web/server/plugins/sosynpl/search.js index 870beb6c70..ddf9d9c0c8 100644 --- a/backend/web/server/plugins/sosynpl/search.js +++ b/backend/web/server/plugins/sosynpl/search.js @@ -5,31 +5,31 @@ const lodash=require('lodash') const CustomerFreelance = require("../../models/CustomerFreelance") const User = require("../../models/User") -const { ROLE_FREELANCE, DEFAULT_SEARCH_RADIUS, AVAILABILITY_ON, ANNOUNCE_STATUS_ACTIVE, DURATION_FILTERS, WORK_MODE, WORK_MODE_SITE, WORK_MODE_REMOTE, WORK_MODE_REMOTE_SITE, WORK_DURATION_LESS_1_MONTH, WORK_DURATION_MORE_6_MONTH, WORK_DURATION__1_TO_6_MONTHS, MOBILITY_FRANCE, MOBILITY_NONE, DURATION_UNIT_DAYS } = require("./consts") +const { ROLE_FREELANCE, DEFAULT_SEARCH_RADIUS, AVAILABILITY_ON, ANNOUNCE_STATUS_ACTIVE, DURATION_FILTERS, WORK_MODE, WORK_MODE_SITE, WORK_MODE_REMOTE, WORK_MODE_REMOTE_SITE, WORK_DURATION_LESS_1_MONTH, WORK_DURATION_MORE_6_MONTH, WORK_DURATION__1_TO_6_MONTHS, MOBILITY_FRANCE, MOBILITY_NONE, DURATION_UNIT_DAYS, MOBILITY_CITY, MOBILITY_REGIONS } = require("./consts") const { computeDistanceKm } = require('../../../utils/functions') const Announce = require('../../models/Announce') const { REGIONS_FULL } = require('../../../utils/consts') const computeSuggestedFreelances = async (userId, params, data) => { - // if (!data.job || !data.start_date) { - // console.log("missing attributes on announce") - // return [] - // } + if (!data.job || !data.start_date) { + console.log("missing attributes on announce") + return [] + } const MAP_WORKMODE = { - 0: 'WORK_MODE_SITE', - 5: 'WORK_MODE_REMOTE', + 0: WORK_MODE_SITE, + 5: WORK_MODE_REMOTE, } - const workMode = MAP_WORKMODE[data.homework_days] || 'WORK_MODE_REMOTE_SITE' + const workMode = MAP_WORKMODE[data.homework_days] || WORK_MODE_REMOTE_SITE const durationDays = data.duration*DURATION_UNIT_DAYS[data.duration_unit] const workDuration = durationDays < 30 - ? 'WORK_DURATION_LESS_1_MONTH' + ? WORK_DURATION_LESS_1_MONTH : durationDays > 180 - ? 'WORK_DURATION_MORE_6_MONTH' - : 'WORK_DURATION__1_TO_6_MONTHS' + ? WORK_DURATION_MORE_6_MONTH + : WORK_DURATION__1_TO_6_MONTHS const getRegionFromZipcode = (zipcode) => { const departmentCode = zipcode.toString().substring(0, 2) @@ -43,31 +43,39 @@ const computeSuggestedFreelances = async (userId, params, data) => { if (data.homework_days === 5) { return {} } - if (data.mobility === 'MOBILITY_FRANCE') { - return { mobility: 'MOBILITY_FRANCE' } + if (data.mobility === MOBILITY_FRANCE) { + return { mobility: MOBILITY_FRANCE } } - if (data.mobility === 'MOBILITY_NONE') { - const regionKey = getRegionFromZipcode(data.city.zipcode) + if (data.mobility === MOBILITY_NONE) { + const regionKey = getRegionFromZipcode(data.city.zip_code) return { $or: [ { - mobility: 'MOBILITY_CITY', + mobility: MOBILITY_CITY, $expr: { $lt: [ - computeDistanceKm(data.city, '$mobility_city'), + computeDistanceKm(data.city, "$mobility_city"), '$mobility_city_distance', ], }, }, { - mobility: 'MOBILITY_REGIONS', + mobility: MOBILITY_REGIONS, mobility_regions: { $in: [regionKey] }, }, ], } } - return {} + return true + } + + const availabilityFilter = { + $or: [ + { availability: AVAILABILITY_ON }, + { available_from: { $lte: data.start_date } }, + ], } + const filter = { main_job: data.job, work_sector: { $in: data.sectors }, @@ -77,17 +85,15 @@ const computeSuggestedFreelances = async (userId, params, data) => { main_experience: { $in: data.experience }, work_mode: workMode, work_duration: workDuration, - // ...mobilityFilter(), - // $or: [ - // { available: true }, - // { available_from: { $lte: data.start_date } }, - // ], + $and:[ + mobilityFilter(), + availabilityFilter + ], } - - return CustomerFreelance.find(filter) + const suggestions = await CustomerFreelance.find(filter) + return suggestions } - const PROFILE_TEXT_SEARCH_FIELDS=['position', 'description', 'motivation'] const searchFreelances = async (userId, params, data, fields) => { diff --git a/backend/web/tests/sosynpl/search.test.js b/backend/web/tests/sosynpl/search.test.js index 21b0865553..91d35c196d 100644 --- a/backend/web/tests/sosynpl/search.test.js +++ b/backend/web/tests/sosynpl/search.test.js @@ -1,6 +1,5 @@ const mongoose = require('mongoose') const moment = require('moment') -const { faker } = require('@faker-js/faker') const { MONGOOSE_OPTIONS, loadFromDb } = require('../../server/utils/database') const Announce = require('../../server/models/Announce') const Job = require('../../server/models/Job') @@ -11,6 +10,9 @@ const LanguageLevel = require('../../server/models/LanguageLevel') const CustomerFreelance = require('../../server/models/CustomerFreelance') const SoftSkill = require('../../server/models/SoftSkill') const JobFile = require('../../server/models/JobFile') +const { LANGUAGE_LEVEL_ADVANCED } = require('../../utils/consts') +const { EXPERIENCE_EXPERT, DURATION_MONTH, MOBILITY_FRANCE, SOURCE_RECOMMANDATION, WORK_MODE_REMOTE_SITE, WORK_DURATION__1_TO_6_MONTHS, AVAILABILITY_ON, AVAILABILITY_OFF, MOBILITY_CITY, MOBILITY_NONE } = require('../../server/plugins/sosynpl/consts') +const { computeDistanceKm } = require('../../utils/functions') require('../../server/plugins/sosynpl/functions') require('../../server/plugins/sosynpl/announce') require('../../server/models/JobFile') @@ -18,7 +20,7 @@ require('../../server/models/Application') describe('Search', () => { - let job, sector, expertise1, expertise2, expertise3, software, language, announce, customerFreelance + let job, sector, expertise1, expertise2, expertise3, software, language, announce, customerFreelance, rouen, msa, dieppe let softSkillComm, softSkillConflict, softSkillTeamWork beforeAll(async () => { @@ -32,31 +34,48 @@ describe('Search', () => { expertise2 = await Expertise.create({ name: 'Java' }) expertise3 = await Expertise.create({ name: 'Python' }) software = await Software.create({ name: 'VS Code' }) - language = await LanguageLevel.create({ language: 'fr', level: 'LANGUAGE_LEVEL_ADVANCED' }) + language = await LanguageLevel.create({ language: 'fr', level: LANGUAGE_LEVEL_ADVANCED }) softSkillComm = await SoftSkill.create({ name: 'Communication', value: 'SOFT_SKILL_COMM' }) softSkillTeamWork = await SoftSkill.create({ name: 'TeamWork', value: 'SOFT_SKILL_TEAMWORK'}) softSkillConflict = await SoftSkill.create({ name: 'Conflict', value: 'SOFT_SKILL_CONFLICT'}) - + rouen = { + address: 'Place du Vieux-Marché', + city: 'Rouen', + zip_code: '76000', + country: 'France', + latitude: 49.4431, + longitude: 1.0993, + } + msa = { + address: 'Place Colbert', + city: 'Mont Saint Aignan', + zip_code: '76130', + country: 'France', + latitude: 49.4655, + longitude: 1.0877, + } + dieppe = { + address: 'Place Nationale', + city: 'Dieppe', + zip_code: '76200', + country: 'France', + latitude: 49.9225, + longitude: 1.0781, + } announce = await Announce.create({ user: new mongoose.Types.ObjectId(), job: job._id, - title: faker.name.jobTitle(), - experience: ['EXPERIENCE_EXPERT'], - start_date: moment(), - duration: faker.datatype.number({ min: 1, max: 12 }), - duration_unit: 'DURATION_MONTH', + title: 'Senior Developer', + experience: [EXPERIENCE_EXPERT], + start_date: new Date(2024, 6, 20), + duration: 3, + duration_unit: DURATION_MONTH, sectors: [sector._id], - city: { - address: faker.address.streetAddress(), - city: faker.address.city(), - zip_code: faker.address.zipCode(), - country: faker.address.country() - }, - homework_days: faker.datatype.number({ min: 0, max: 5 }), - mobility: 'MOBILITY_FRANCE', - mobility_days_per_month: faker.datatype.number({ min: 1, max: 30 }), - budget: faker.datatype.number({ min: 1000, max: 10000 }), - budget_hidden: faker.datatype.boolean(), + homework_days: 3, + mobility: MOBILITY_NONE, + mobility_days_per_month: 10, + budget: 5000, + budget_hidden: false, expertises: [expertise1._id, expertise2._id, expertise3._id], pinned_expertises: [expertise1._id], softwares: [software._id], @@ -64,18 +83,22 @@ describe('Search', () => { gold_soft_skills: [softSkillComm._id], silver_soft_skills: [softSkillTeamWork._id], bronze_soft_skills: [softSkillConflict._id], + city: msa, }) customerFreelance = await CustomerFreelance.create({ - password: faker.internet.password(), - email: faker.internet.email(), - lastname: faker.name.lastName(), - firstname: faker.name.firstName(), - source: 'SOURCE_RECOMMANDATION', + password: 'password123', + availability: AVAILABILITY_OFF, + available_from: new Date(2024,6,19), + available_days_per_week: 5, + email: 'sample@example.com', + lastname: 'Doe', + firstname: 'John', + source: SOURCE_RECOMMANDATION, curriculum: new mongoose.Types.ObjectId(), experience: new mongoose.Types.ObjectId(), - motivation: faker.lorem.sentence(), - main_job: job._id, + motivation: 'Motivated to work on challenging projects', + main_job: job, gold_soft_skills: [softSkillComm._id], silver_soft_skills: [softSkillTeamWork._id], bronze_soft_skills: [softSkillConflict._id], @@ -83,21 +106,23 @@ describe('Search', () => { expertises: [expertise1._id], siren: '923145171', legal_status: 'EI', - company_name: faker.company.name(), - position: faker.name.jobTitle(), + company_name: 'Sample Company', + position: 'Lead Developer', softwares: [software._id], languages: [language._id], - main_experience: 'EXPERIENCE_EXPERT', - work_mode: 'WORK_MODE_REMOTE_SITE', - work_duration: ['WORK_DURATION__1_TO_6_MONTHS'], - mobility: 'MOBILITY_FRANCE', + main_experience: EXPERIENCE_EXPERT, + work_mode: WORK_MODE_REMOTE_SITE, + work_duration: [WORK_DURATION__1_TO_6_MONTHS], + mobility: MOBILITY_CITY, + mobility_city: rouen, + mobility_city_distance: 10, cgu_accepted: true, phone: '0606060606', address: { - address: faker.address.streetAddress(), - city: faker.address.city(), - zip_code: faker.address.zipCode(), - country: faker.address.country() + address: '123 Main St', + city: 'Sample City', + zip_code: '12345', + country: 'Sample Country' } }) }) @@ -109,12 +134,11 @@ describe('Search', () => { test('should find suggested freelances based on announce criteria', async () => { const loadedAnnounce = await loadFromDb({model:'announce', id:announce._id, - fields:'suggested_freelances,gold_soft_skills,silver_soft_skills,bronze_soft_skills,job,sectors,expertises,softwares,languages,experience,_duration_days,duration_unit,duration'.split(',') + fields:'city,mobility,suggested_freelances,gold_soft_skills,silver_soft_skills,bronze_soft_skills,start_date,job,sectors,expertises,softwares,languages,experience,_duration_days,duration_unit,duration'.split(',') }) - - console.log(customerFreelance) - console.log(announce) - console.log("suggestion:",loadedAnnounce[0].suggested_freelances) - console.log("freelance:",customerFreelance.fullname) + const suggestion = loadedAnnounce[0].suggested_freelances[0] + // console.dir(customerFreelance, { depth: null, colors: true, maxArrayLength: null }) + // console.dir(announce, { depth: null, colors: true, maxArrayLength: null }) + expect(String(customerFreelance._id)).toMatch(String(suggestion.id)) }) }) diff --git a/backend/web/utils/consts.js b/backend/web/utils/consts.js index 370adffb4f..05d9299df7 100644 --- a/backend/web/utils/consts.js +++ b/backend/web/utils/consts.js @@ -275,7 +275,7 @@ module.exports = { ALL_SERVICES, ALF_CONDS, CANCEL_MODE, CUSTOM_PRESTATIONS_FLTR, generate_id, GID_LEN, CESU, NEEDED_VAR, - SKILLS, LANGUAGES, MAX_DESCRIPTION_LENGTH, EXPIRATION_DELAY, + SKILLS, LANGUAGES, LANGUAGE_LEVEL_ADVANCED, LANGUAGE_LEVEL_BEGINNER, LANGUAGE_LEVEL_INTERMEDIATE, LANGUAGE_LEVEL_NATIVE, MAX_DESCRIPTION_LENGTH, EXPIRATION_DELAY, CLOSE_NOTIFICATION_DELAY, ACCOUNT_MIN_AGE, COMPANY_SIZE, COMPANY_ACTIVITY, BUDGET_PERIOD, PRO, PART, CREASHOP_MODE, MONTH_PERIOD, YEAR_PERIOD, DASHBOARD_MODE, MICROSERVICE_MODE, CARETAKER_MODE, REGISTER_MODE,