@mostajs/training 0.1.0

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.
Files changed (47) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/README.md +12 -0
  3. package/docs/00-PROPOSITION-PLAN-TRAINING-18062026.md +13 -0
  4. package/docs/01-ETUDE-ETAT-ART-TRAINING-18062026.md +216 -0
  5. package/docs/01-ETUDE-ETAT-ART-TRAINING-18062026.pdf +0 -0
  6. package/docs/03-PLAN-DEV-TRAINING.md +117 -0
  7. package/docs/03-PLAN-DEV-TRAINING.pdf +0 -0
  8. package/docs/04-PLAN-TESTS.md +82 -0
  9. package/docs/04-PLAN-TESTS.pdf +0 -0
  10. package/docs/05-PLAN-SUIVI-MONITORING.md +49 -0
  11. package/docs/05-PLAN-SUIVI-MONITORING.pdf +0 -0
  12. package/docs/PROPOSITION-MODULE-TRAINING.md +136 -0
  13. package/docs/PROPOSITION-MODULE-TRAINING.pdf +0 -0
  14. package/docs/old-dpf/01-ETUDE-ETAT-ART-TRAINING-18062026.pdf +0 -0
  15. package/docs/old-dpf/03-PLAN-DEV-TRAINING.pdf +0 -0
  16. package/docs/old-dpf/04-PLAN-TESTS.pdf +0 -0
  17. package/docs/old-dpf/05-PLAN-SUIVI-MONITORING.pdf +0 -0
  18. package/docs/old-dpf/PROPOSITION-MODULE-TRAINING.pdf +0 -0
  19. package/docs/results-training-20260618/01-lms.json +42 -0
  20. package/docs/results-training-20260618/02-sis.json +42 -0
  21. package/docs/results-training-20260618/10-catalog.json +42 -0
  22. package/docs/results-training-20260618/11-cefr.json +42 -0
  23. package/docs/results-training-20260618/20-sessions.json +42 -0
  24. package/docs/results-training-20260618/30-enroll.json +42 -0
  25. package/docs/results-training-20260618/40-centre.json +42 -0
  26. package/docs/results-training-20260618/41-centre-fr.json +42 -0
  27. package/docs/results-training-20260618/50-incub.json +42 -0
  28. package/docs/results-training-20260618/51-incub-fr.json +42 -0
  29. package/docs/results-training-20260618/60-standards.json +42 -0
  30. package/docs/results-training-20260618/70-ao.json +42 -0
  31. package/docs/results-training-20260618/71-agrement.json +42 -0
  32. package/docs/results-training-20260618/80-acad-lms.json +1 -0
  33. package/docs/results-training-20260618/81-acad-sched.json +1 -0
  34. package/docs/results-training-20260618/SOURCES.md +48 -0
  35. package/docs/results-training-20260618/academic-only.csv +2 -0
  36. package/docs/results-training-20260618/all-results.csv +130 -0
  37. package/docs/results-training-20260618/vendors-only.csv +11 -0
  38. package/docs/scripts-recherche-training-18062026.sh +84 -0
  39. package/docs/scripts-recherche-training-portable-18062026.sh +74 -0
  40. package/examples/cohorte/run.mjs +13 -0
  41. package/llms.txt +14 -0
  42. package/package.json +26 -0
  43. package/src/index.js +4 -0
  44. package/src/memory-repo.js +8 -0
  45. package/src/schemas.js +17 -0
  46. package/src/training.js +61 -0
  47. package/test-scripts/unit/training.test.mjs +45 -0
@@ -0,0 +1,61 @@
1
+ // @mostajs/training — catalogue + sessions + inscriptions. Author: Dr Hamid MADANI <drmdh@msn.com>
2
+ // Compose (injectés) : numbering (code session), notifications (ouverture/confirmation). Délègue éval/paiement/présence.
3
+
4
+ export function createTraining({ repositories, numbering, notifications, now = () => new Date() } = {}) {
5
+ if (!repositories?.courses) throw new Error('createTraining: repositories requis');
6
+ const { courses, levels, sessions, enrollments } = repositories;
7
+ const confirmedCount = (sessionId) => enrollments.count((e) => e.sessionId === sessionId && e.status === 'confirmed');
8
+ const code = (course) => (numbering?.next ? numbering.next('session') : `${(course.title || 'SES').slice(0, 3).toUpperCase()}-${now().getFullYear()}-${Math.floor(now().getTime() % 100000)}`);
9
+
10
+ return {
11
+ catalog: {
12
+ createCourse: (dto) => courses.create({ active: true, ...dto }),
13
+ getCourse: (id) => courses.findById(id),
14
+ listCourses: (f = {}) => courses.find((c) => Object.entries(f).every(([k, v]) => c[k] === v)),
15
+ async addLevel(courseId, dto) { if (!(await courses.findById(courseId))) throw new Error('cours introuvable'); return levels.create({ courseId, ...dto }); },
16
+ listLevels: (courseId) => levels.find((l) => l.courseId === courseId).then((a) => a.sort((x, y) => x.order - y.order)),
17
+ },
18
+
19
+ sessions: {
20
+ async open(dto) {
21
+ const course = await courses.findById(dto.courseId); if (!course) throw new Error('cours introuvable');
22
+ const s = await sessions.create({ status: 'open', capacity: 0, ...dto, code: dto.code || code(course) });
23
+ notifications?.notify?.({ type: 'session.opened', sessionId: s.id });
24
+ return s;
25
+ },
26
+ get: (id) => sessions.findById(id),
27
+ listOpen: () => sessions.find((s) => s.status === 'open'),
28
+ setInstructor: (id, instructorId) => sessions.update(id, { instructorId }),
29
+ schedule: (id, { startDate, endDate, scheduleRef } = {}) => sessions.update(id, { startDate, endDate, scheduleRef }),
30
+ run: (id) => sessions.update(id, { status: 'running' }),
31
+ close: (id) => sessions.update(id, { status: 'closed' }),
32
+ async cancel(id) { const s = await sessions.update(id, { status: 'cancelled' }); notifications?.notify?.({ type: 'session.cancelled', sessionId: id }); return s; },
33
+ },
34
+
35
+ enrollments: {
36
+ async enroll(sessionId, learnerId) {
37
+ const s = await sessions.findById(sessionId); if (!s) throw new Error('session introuvable');
38
+ if (!['open', 'full'].includes(s.status)) throw new Error(`session non ouverte (${s.status})`);
39
+ const dup = (await enrollments.find((e) => e.sessionId === sessionId && e.learnerId === learnerId && e.status === 'confirmed'))[0];
40
+ if (dup) return dup;
41
+ if (s.capacity > 0 && (await confirmedCount(sessionId)) >= s.capacity) throw new Error('CapacityReached');
42
+ const e = await enrollments.create({ sessionId, learnerId, status: 'confirmed', enrolledAt: now() });
43
+ if (s.capacity > 0 && (await confirmedCount(sessionId)) >= s.capacity) await sessions.update(sessionId, { status: 'full' });
44
+ notifications?.notify?.({ type: 'enrollment.confirmed', sessionId, learnerId });
45
+ return e;
46
+ },
47
+ async cancel(enrollmentId) {
48
+ const e = await enrollments.update(enrollmentId, { status: 'cancelled' }); if (!e) return null;
49
+ const s = await sessions.findById(e.sessionId);
50
+ if (s && s.status === 'full') await sessions.update(s.id, { status: 'open' });
51
+ return e;
52
+ },
53
+ complete: (enrollmentId) => enrollments.update(enrollmentId, { status: 'completed' }),
54
+ listBySession: (sessionId) => enrollments.find((e) => e.sessionId === sessionId),
55
+ listByLearner: (learnerId) => enrollments.find((e) => e.learnerId === learnerId),
56
+ seats: async (sessionId) => { const s = await sessions.findById(sessionId); return { capacity: s.capacity, taken: await confirmedCount(sessionId) }; },
57
+ },
58
+
59
+ repositories,
60
+ };
61
+ }
@@ -0,0 +1,45 @@
1
+ // @mostajs/training — tests (DEVRULES §5). node test-scripts/unit/training.test.mjs
2
+ import assert from 'node:assert/strict';
3
+ import { createTraining, createMemoryRepositories } from '../../src/index.js';
4
+ let pass=0; const test=async(n,f)=>{await f();pass++;console.log(' ✓',n);};
5
+ const mk=(o={})=>createTraining({ repositories:createMemoryRepositories(), ...o });
6
+
7
+ await test('catalogue : cours + niveaux ordonnés', async()=>{
8
+ const t=mk(); const c=await t.catalog.createCourse({title:'Anglais',category:'langues'});
9
+ await t.catalog.addLevel(c.id,{order:2,label:'A2'}); await t.catalog.addLevel(c.id,{order:1,label:'A1'});
10
+ const ls=await t.catalog.listLevels(c.id); assert.deepEqual(ls.map(l=>l.label),['A1','A2']);
11
+ });
12
+ await test('session : open + code généré', async()=>{
13
+ const t=mk(); const c=await t.catalog.createCourse({title:'Mémoire'});
14
+ const s=await t.sessions.open({courseId:c.id, capacity:2});
15
+ assert.equal(s.status,'open'); assert.ok(s.code);
16
+ });
17
+ await test('inscription + contrôle de capacité (full)', async()=>{
18
+ const t=mk(); const c=await t.catalog.createCourse({title:'X'}); const s=await t.sessions.open({courseId:c.id,capacity:2});
19
+ await t.enrollments.enroll(s.id,'l1'); await t.enrollments.enroll(s.id,'l2');
20
+ assert.equal((await t.sessions.get(s.id)).status,'full');
21
+ await assert.rejects(()=>t.enrollments.enroll(s.id,'l3'), /CapacityReached/);
22
+ });
23
+ await test('annulation libère une place (full→open)', async()=>{
24
+ const t=mk(); const c=await t.catalog.createCourse({title:'X'}); const s=await t.sessions.open({courseId:c.id,capacity:1});
25
+ const e=await t.enrollments.enroll(s.id,'l1'); assert.equal((await t.sessions.get(s.id)).status,'full');
26
+ await t.enrollments.cancel(e.id); assert.equal((await t.sessions.get(s.id)).status,'open');
27
+ });
28
+ await test('inscription idempotente + listes', async()=>{
29
+ const t=mk(); const c=await t.catalog.createCourse({title:'X'}); const s=await t.sessions.open({courseId:c.id,capacity:0});
30
+ await t.enrollments.enroll(s.id,'l1'); await t.enrollments.enroll(s.id,'l1');
31
+ assert.equal((await t.enrollments.listBySession(s.id)).filter(e=>e.status==='confirmed').length,1);
32
+ assert.equal((await t.enrollments.listByLearner('l1')).length,1);
33
+ });
34
+ await test('refus inscription si session non ouverte', async()=>{
35
+ const t=mk(); const c=await t.catalog.createCourse({title:'X'}); const s=await t.sessions.open({courseId:c.id,capacity:5});
36
+ await t.sessions.cancel(s.id);
37
+ await assert.rejects(()=>t.enrollments.enroll(s.id,'l1'), /non ouverte/);
38
+ });
39
+ await test('notifications composées (ouverture/confirmation)', async()=>{
40
+ const ev=[]; const t=mk({ notifications:{ notify:(e)=>ev.push(e.type) } });
41
+ const c=await t.catalog.createCourse({title:'X'}); const s=await t.sessions.open({courseId:c.id,capacity:5});
42
+ await t.enrollments.enroll(s.id,'l1');
43
+ assert.ok(ev.includes('session.opened') && ev.includes('enrollment.confirmed'));
44
+ });
45
+ console.log(`\n✅ @mostajs/training — ${pass} tests OK`);