@programisto/edrm-exams 0.3.16 → 0.3.18

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.
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Retourne la base du lien d'invitation au test technique (portail candidat).
3
+ * Utilise la config de l'entité (testInvitationLink) ou dérive depuis domain/domains.
4
+ * Fallback : process.env.TEST_INVITATION_LINK (ex. https://app.programisto.fr/magic?email=).
5
+ */
6
+ export declare function getTestInvitationLinkBase(entity: {
7
+ config?: Record<string, string>;
8
+ domain?: string;
9
+ domains?: string[];
10
+ } | null | undefined): string;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Retourne la base du lien d'invitation au test technique (portail candidat).
3
+ * Utilise la config de l'entité (testInvitationLink) ou dérive depuis domain/domains.
4
+ * Fallback : process.env.TEST_INVITATION_LINK (ex. https://app.programisto.fr/magic?email=).
5
+ */
6
+ export function getTestInvitationLinkBase(entity) {
7
+ if (!entity) {
8
+ return process.env.TEST_INVITATION_LINK || '';
9
+ }
10
+ const config = entity.config;
11
+ if (config && typeof config.testInvitationLink === 'string' && config.testInvitationLink.trim() !== '') {
12
+ const base = config.testInvitationLink.trim();
13
+ return base.includes('?') ? base : `${base}?email=`;
14
+ }
15
+ const domains = entity.domains;
16
+ if (Array.isArray(domains) && domains.length > 0) {
17
+ const appHost = domains.find((d) => String(d).toLowerCase().startsWith('app.'));
18
+ const host = appHost ?? domains[0];
19
+ const base = String(host).toLowerCase().split(':')[0].trim();
20
+ if (base)
21
+ return `https://${base}/magic?email=`;
22
+ }
23
+ const domain = entity.domain;
24
+ if (domain && String(domain).trim() !== '') {
25
+ const base = String(domain).trim().toLowerCase().replace(/^(my\.|jobs\.|api\.)/, '');
26
+ if (base)
27
+ return `https://app.${base}/magic?email=`;
28
+ }
29
+ return process.env.TEST_INVITATION_LINK || '';
30
+ }
@@ -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';
@@ -50,15 +51,22 @@ async function inviteCandidateToTest(payload) {
50
51
  console.warn('[INVITE_TO_TECHNICAL_TEST] No email for contact');
51
52
  return;
52
53
  }
53
- const testLink = (process.env.TEST_INVITATION_LINK || '') + email;
54
+ const linkBase = payload.testInvitationLinkBase ?? process.env.TEST_INVITATION_LINK ?? '';
55
+ const testLink = linkBase + email;
54
56
  const emailUser = process.env.EMAIL_USER;
55
57
  const emailPassword = process.env.EMAIL_PASSWORD;
58
+ const entityIdForMail = payload.entityId != null
59
+ ? (payload.entityId instanceof Types.ObjectId
60
+ ? payload.entityId
61
+ : new Types.ObjectId(String(payload.entityId)))
62
+ : undefined;
56
63
  await enduranceEmitter.emit(enduranceEventTypes.SEND_EMAIL, {
57
64
  template: 'test-invitation',
58
65
  to: email,
59
66
  from: emailUser,
60
67
  emailUser,
61
68
  emailPassword,
69
+ ...(entityIdForMail && { entityId: entityIdForMail }),
62
70
  data: {
63
71
  firstname: contact.firstname,
64
72
  testName: test?.title || '',
@@ -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 { getTestInvitationLinkBase } from '../lib/test-invitation-link.js';
11
12
  import { Types } from 'mongoose';
12
13
  // Fonction utilitaire pour récupérer le nom du job
13
14
  async function getJobName(targetJob) {
@@ -1505,18 +1506,23 @@ class ExamsRouter extends EnduranceRouter {
1505
1506
  });
1506
1507
  await newResult.save();
1507
1508
  const email = contact.email;
1508
- // Construire le lien d'invitation
1509
- const testLink = (process.env.TEST_INVITATION_LINK || '') + email;
1509
+ // Construire le lien d'invitation (URL de l'entité ou défaut env)
1510
+ const testLinkBase = getTestInvitationLinkBase(req.entity);
1511
+ const testLink = testLinkBase + email;
1510
1512
  // Récupérer les credentials d'envoi
1511
1513
  const emailUser = process.env.EMAIL_USER;
1512
1514
  const emailPassword = process.env.EMAIL_PASSWORD;
1513
- // Envoyer l'email via l'event emitter
1515
+ // Envoyer l'email via l'event emitter (entityId pour utiliser le template de l'entité courante, ex. École de Turing)
1516
+ const entityIdForMail = req.entity?._id != null
1517
+ ? (req.entity._id instanceof Types.ObjectId ? req.entity._id : new Types.ObjectId(String(req.entity._id)))
1518
+ : undefined;
1514
1519
  await emitter.emit(eventTypes.SEND_EMAIL, {
1515
1520
  template: 'test-invitation',
1516
1521
  to: email,
1517
1522
  from: emailUser,
1518
1523
  emailUser,
1519
1524
  emailPassword,
1525
+ ...(entityIdForMail && { entityId: entityIdForMail }),
1520
1526
  data: {
1521
1527
  firstname: contact.firstname,
1522
1528
  testName: test?.title || '',
@@ -2298,16 +2304,23 @@ class ExamsRouter extends EnduranceRouter {
2298
2304
  const email = contact.email;
2299
2305
  const emailUser = process.env.EMAIL_USER;
2300
2306
  const emailPassword = process.env.EMAIL_PASSWORD;
2301
- // Construire le lien d'invitation
2302
- const testLink = (process.env.TEST_INVITATION_LINK || '') + email;
2303
- // Envoyer l'email via l'event emitter
2307
+ // Construire le lien d'invitation (URL de l'entité ou défaut env)
2308
+ const testLinkBase = getTestInvitationLinkBase(req.entity);
2309
+ const testLink = testLinkBase + email;
2310
+ // Envoyer l'email via l'event emitter (entityId pour utiliser le template de l'entité courante)
2311
+ const entityIdForReinvite = req.entity?._id != null
2312
+ ? (req.entity._id instanceof Types.ObjectId ? req.entity._id : new Types.ObjectId(String(req.entity._id)))
2313
+ : undefined;
2304
2314
  await emitter.emit(eventTypes.SEND_EMAIL, {
2305
2315
  template: 'test-invitation',
2306
2316
  to: email,
2307
2317
  from: emailUser,
2308
2318
  emailUser,
2309
2319
  emailPassword,
2320
+ ...(entityIdForReinvite && { entityId: entityIdForReinvite }),
2310
2321
  data: {
2322
+ firstname: contact.firstname,
2323
+ testName: test?.title || '',
2311
2324
  testLink
2312
2325
  }
2313
2326
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@programisto/edrm-exams",
4
- "version": "0.3.16",
4
+ "version": "0.3.18",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },