@programisto/edrm-exams 0.3.15 → 0.3.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,4 @@
1
+ import { Types } from 'mongoose';
1
2
  import { enduranceListener, enduranceEmitter, enduranceEventTypes } from '@programisto/endurance';
2
3
  import Test from '../models/test.model.js';
3
4
  import TestResult from '../models/test-result.model.js';
@@ -53,12 +54,18 @@ async function inviteCandidateToTest(payload) {
53
54
  const testLink = (process.env.TEST_INVITATION_LINK || '') + email;
54
55
  const emailUser = process.env.EMAIL_USER;
55
56
  const emailPassword = process.env.EMAIL_PASSWORD;
57
+ const entityIdForMail = payload.entityId != null
58
+ ? (payload.entityId instanceof Types.ObjectId
59
+ ? payload.entityId
60
+ : new Types.ObjectId(String(payload.entityId)))
61
+ : undefined;
56
62
  await enduranceEmitter.emit(enduranceEventTypes.SEND_EMAIL, {
57
63
  template: 'test-invitation',
58
64
  to: email,
59
65
  from: emailUser,
60
66
  emailUser,
61
67
  emailPassword,
68
+ ...(entityIdForMail && { entityId: entityIdForMail }),
62
69
  data: {
63
70
  firstname: contact.firstname,
64
71
  testName: test?.title || '',
@@ -7,6 +7,7 @@ export declare enum ExperienceLevel {
7
7
  EXPERT = "EXPERT"
8
8
  }
9
9
  declare class Candidate extends EnduranceSchema {
10
+ entityId?: Types.ObjectId;
10
11
  contact: Types.ObjectId;
11
12
  experienceLevel: string;
12
13
  yearsOfExperience: number;
@@ -20,6 +20,7 @@ export var ExperienceLevel;
20
20
  })(ExperienceLevel || (ExperienceLevel = {}));
21
21
  /* eslint-enable no-unused-vars */
22
22
  let Candidate = class Candidate extends EnduranceSchema {
23
+ entityId;
23
24
  contact;
24
25
  experienceLevel;
25
26
  yearsOfExperience;
@@ -32,6 +33,10 @@ let Candidate = class Candidate extends EnduranceSchema {
32
33
  return CandidateModel;
33
34
  }
34
35
  };
36
+ __decorate([
37
+ EnduranceModelType.prop({ type: Types.ObjectId, ref: 'Entity', required: false }),
38
+ __metadata("design:type", Types.ObjectId)
39
+ ], Candidate.prototype, "entityId", void 0);
35
40
  __decorate([
36
41
  EnduranceModelType.prop({ required: true, ref: 'Contact' }),
37
42
  __metadata("design:type", Types.ObjectId)
@@ -5,6 +5,7 @@ import TestResult from '../models/test-result.model.js';
5
5
  import Test from '../models/test.model.js';
6
6
  import TestJob from '../models/test-job.model.js';
7
7
  import jwt from 'jsonwebtoken';
8
+ import { Types } from 'mongoose';
8
9
  // Fonction utilitaire pour récupérer le nom du job
9
10
  async function getJobName(targetJob) {
10
11
  // Si c'est déjà une string (ancien format), on la retourne directement
@@ -23,6 +24,29 @@ async function getJobName(targetJob) {
23
24
  }
24
25
  return 'Job inconnu';
25
26
  }
27
+ /** Entité par défaut : candidats sans entityId lui sont rattachés. */
28
+ function isDefaultEntity(req) {
29
+ if (!req?.entity)
30
+ return false;
31
+ const slug = req.entity.slug;
32
+ return req.entity.isDefault === true || slug === 'programisto' || slug === 'progamisto';
33
+ }
34
+ /** Filtre MongoDB pour les candidats de l'entité courante (évite les doublons quand un même contact existe sur plusieurs entités). */
35
+ function buildCandidateEntityFilter(req) {
36
+ if (!req?.entity?._id)
37
+ return {};
38
+ const eid = req.entity._id instanceof Types.ObjectId ? req.entity._id : new Types.ObjectId(String(req.entity._id));
39
+ if (isDefaultEntity(req)) {
40
+ return {
41
+ $or: [
42
+ { entityId: eid },
43
+ { entityId: null },
44
+ { entityId: { $exists: false } }
45
+ ]
46
+ };
47
+ }
48
+ return { entityId: eid };
49
+ }
26
50
  class CandidateRouter extends EnduranceRouter {
27
51
  constructor() {
28
52
  super(EnduranceAuthMiddleware.getInstance());
@@ -204,6 +228,7 @@ class CandidateRouter extends EnduranceRouter {
204
228
  const search = req.query.search || '';
205
229
  const sortBy = req.query.sortBy || 'lastname';
206
230
  const sortOrder = req.query.sortOrder || 'asc';
231
+ const entityFilter = buildCandidateEntityFilter(req);
207
232
  let contactIds = [];
208
233
  let total = 0;
209
234
  if (search) {
@@ -217,27 +242,27 @@ class CandidateRouter extends EnduranceRouter {
217
242
  };
218
243
  const contacts = await ContactModel.find(contactQuery);
219
244
  contactIds = contacts.map(contact => contact._id);
220
- // Compter les candidats avec ces contacts
221
- total = await CandidateModel.countDocuments({ contact: { $in: contactIds } });
245
+ // Compter les candidats avec ces contacts (scopé entité : éviter doublons multi-entités)
246
+ total = await CandidateModel.countDocuments({ contact: { $in: contactIds }, ...entityFilter });
222
247
  }
223
248
  else {
224
- // Pas de recherche, compter tous les candidats
225
- total = await CandidateModel.countDocuments();
249
+ // Pas de recherche, compter tous les candidats (scopé entité)
250
+ total = await CandidateModel.countDocuments(entityFilter);
226
251
  }
227
252
  // Construction du tri pour les contacts
228
253
  const allowedSortFields = ['firstname', 'lastname', 'email'];
229
254
  const sortField = allowedSortFields.includes(sortBy) ? sortBy : 'lastname';
230
255
  let candidates;
231
256
  if (search && contactIds.length > 0) {
232
- // Récupérer les candidats avec les contacts trouvés
233
- candidates = await CandidateModel.find({ contact: { $in: contactIds } })
257
+ // Récupérer les candidats avec les contacts trouvés (un seul par contact/entité)
258
+ candidates = await CandidateModel.find({ contact: { $in: contactIds }, ...entityFilter })
234
259
  .skip(skip)
235
260
  .limit(limit)
236
261
  .exec();
237
262
  }
238
263
  else if (!search) {
239
- // Récupérer tous les candidats
240
- candidates = await CandidateModel.find()
264
+ // Récupérer les candidats de l'entité courante
265
+ candidates = await CandidateModel.find(entityFilter)
241
266
  .skip(skip)
242
267
  .limit(limit)
243
268
  .exec();
@@ -8,6 +8,7 @@ import Candidate from '../models/candidate.model.js';
8
8
  import ContactModel from '../models/contact.model.js';
9
9
  import { generateLiveMessage } from '../lib/openai.js';
10
10
  import { computeScoresByCategory } from '../lib/score-utils.js';
11
+ import { Types } from 'mongoose';
11
12
  // Fonction utilitaire pour récupérer le nom du job
12
13
  async function getJobName(targetJob) {
13
14
  // Si c'est déjà une string (ancien format), on la retourne directement
@@ -1465,38 +1466,61 @@ class ExamsRouter extends EnduranceRouter {
1465
1466
  if (!test) {
1466
1467
  return res.status(404).json({ message: 'Test not found' });
1467
1468
  }
1468
- const categories = test.categories.map(cat => ({ categoryId: cat.categoryId }));
1469
- const newResult = new TestResult({
1470
- candidateId,
1471
- testId,
1472
- categories,
1473
- state: 'pending',
1474
- invitationDate: Date.now()
1475
- });
1476
- await newResult.save();
1477
- // Récupérer l'email du candidat
1469
+ // Récupérer le candidat et le contact (avant de créer le TestResult pour pouvoir utiliser le candidat scopé entité)
1478
1470
  const candidate = await Candidate.findById(candidateId);
1479
1471
  if (!candidate) {
1480
1472
  return res.status(404).json({ message: 'Candidate not found' });
1481
1473
  }
1482
- // Récupérer le contact pour obtenir l'email
1483
1474
  const contact = await ContactModel.findById(candidate.contact);
1484
1475
  if (!contact) {
1485
1476
  return res.status(404).json({ message: 'Contact not found' });
1486
1477
  }
1478
+ // Multi-entité : s'assurer qu'un Candidate existe pour l'entité courante (même contact).
1479
+ // Permet à un candidat déjà présent sur une autre entité de se connecter à l'espace candidat de cette entité.
1480
+ let entityCandidate = candidate;
1481
+ if (req.entity?._id) {
1482
+ const entityId = req.entity._id instanceof Types.ObjectId ? req.entity._id : new Types.ObjectId(String(req.entity._id));
1483
+ const existing = await Candidate.findOne({ contact: contact._id, entityId });
1484
+ if (!existing) {
1485
+ entityCandidate = new Candidate({
1486
+ contact: contact._id,
1487
+ entityId,
1488
+ skills: Array.isArray(candidate.skills) ? candidate.skills : [],
1489
+ experienceLevel: candidate.experienceLevel,
1490
+ yearsOfExperience: candidate.yearsOfExperience ?? 0
1491
+ });
1492
+ await entityCandidate.save();
1493
+ }
1494
+ else {
1495
+ entityCandidate = existing;
1496
+ }
1497
+ }
1498
+ const categories = test.categories.map((cat) => ({ categoryId: cat.categoryId }));
1499
+ const newResult = new TestResult({
1500
+ candidateId: entityCandidate._id,
1501
+ testId,
1502
+ categories,
1503
+ state: 'pending',
1504
+ invitationDate: Date.now()
1505
+ });
1506
+ await newResult.save();
1487
1507
  const email = contact.email;
1488
1508
  // Construire le lien d'invitation
1489
1509
  const testLink = (process.env.TEST_INVITATION_LINK || '') + email;
1490
1510
  // Récupérer les credentials d'envoi
1491
1511
  const emailUser = process.env.EMAIL_USER;
1492
1512
  const emailPassword = process.env.EMAIL_PASSWORD;
1493
- // Envoyer l'email via l'event emitter
1513
+ // Envoyer l'email via l'event emitter (entityId pour utiliser le template de l'entité courante, ex. École de Turing)
1514
+ const entityIdForMail = req.entity?._id != null
1515
+ ? (req.entity._id instanceof Types.ObjectId ? req.entity._id : new Types.ObjectId(String(req.entity._id)))
1516
+ : undefined;
1494
1517
  await emitter.emit(eventTypes.SEND_EMAIL, {
1495
1518
  template: 'test-invitation',
1496
1519
  to: email,
1497
1520
  from: emailUser,
1498
1521
  emailUser,
1499
1522
  emailPassword,
1523
+ ...(entityIdForMail && { entityId: entityIdForMail }),
1500
1524
  data: {
1501
1525
  firstname: contact.firstname,
1502
1526
  testName: test?.title || '',
@@ -2280,13 +2304,17 @@ class ExamsRouter extends EnduranceRouter {
2280
2304
  const emailPassword = process.env.EMAIL_PASSWORD;
2281
2305
  // Construire le lien d'invitation
2282
2306
  const testLink = (process.env.TEST_INVITATION_LINK || '') + email;
2283
- // Envoyer l'email via l'event emitter
2307
+ // Envoyer l'email via l'event emitter (entityId pour utiliser le template de l'entité courante)
2308
+ const entityIdForReinvite = req.entity?._id != null
2309
+ ? (req.entity._id instanceof Types.ObjectId ? req.entity._id : new Types.ObjectId(String(req.entity._id)))
2310
+ : undefined;
2284
2311
  await emitter.emit(eventTypes.SEND_EMAIL, {
2285
2312
  template: 'test-invitation',
2286
2313
  to: email,
2287
2314
  from: emailUser,
2288
2315
  emailUser,
2289
2316
  emailPassword,
2317
+ ...(entityIdForReinvite && { entityId: entityIdForReinvite }),
2290
2318
  data: {
2291
2319
  testLink
2292
2320
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@programisto/edrm-exams",
4
- "version": "0.3.15",
4
+ "version": "0.3.17",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },