Some checks are pending
CI / Lint bridge (Biome) (push) Waiting to run
CI / Type-check bridge (push) Blocked by required conditions
CI / Tests unit bridge (push) Blocked by required conditions
CI / Tests integration bridge (push) Blocked by required conditions
CI / Security scan (push) Waiting to run
CI / Docker build + healthcheck (push) Blocked by required conditions
Modele OO complet (cf docs/12-uml-class-diagram.md) en TypeScript strict :
- Personne (pivot multi-roles, splits formation/agence, heuresRestantes*)
- Formation -> Bloc -> Module composition + heuresRestantes rollup
- Module.creerAttribution avec validation RG-01 (capacite) + role formateur
- Attribution lifecycle : demarrer/saisirHeuresRealisees/cloturer/annuler
- Client -> Projet -> Tache composition + lierFormationPedagogique
- Tache.creerIntervention avec validation role developpeur + heures > 0 + actif
- Schemas zod pour runtime validation (z.infer types exposes)
- Decimal.js partout pour les heures (zero erreur flottante)
Patterns appliques :
- Statuts comme discriminated unions ('actif' | 'inactif' | ...)
- Statuts annules exclus des rollups (annulation libere capacite)
- _appliquerHeures* en pseudo-private (convention underscore, pas de friend en TS)
- Warn surcharge Personne non bloquant (overbooking volontaire possible) — RG-01 Module reste bloquante
Tests : 111 pass / 0 fail. Coverage domain : 97.86% lines, 98.57% funcs.
tsc strict EXIT 0, biome ci EXIT 0.
Hors scope (a venir) :
- Repository pattern (Bloc 3 avec routes Hono)
- rapportPDF (Phase 2.4)
- Tests adapters Bloc 1 (Bloc 6 via bridge-tester)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
150 lines
5.2 KiB
TypeScript
150 lines
5.2 KiB
TypeScript
import { Decimal } from 'decimal.js';
|
|
import { describe, expect, it } from 'vitest';
|
|
import { Module } from '../../src/domain/module.js';
|
|
import { Personne } from '../../src/domain/personne.js';
|
|
import type { Role } from '../../src/domain/types.js';
|
|
|
|
const formateurActif = () =>
|
|
new Personne({
|
|
id: 1,
|
|
nom: 'X',
|
|
prenom: 'Y',
|
|
email: 'x@y.fr',
|
|
capaciteAnnuelle: new Decimal(1000),
|
|
splitFormationPct: new Decimal(50),
|
|
splitAgencePct: new Decimal(50),
|
|
roles: new Set<Role>(['formateur']),
|
|
statut: 'actif',
|
|
});
|
|
|
|
describe('Module — constructeur', () => {
|
|
it('cree un module valide', () => {
|
|
const m = new Module({ id: 1, blocId: 10, nom: 'M1', heuresPrevues: new Decimal(40) });
|
|
expect(m.statut).toBe('a_attribuer');
|
|
});
|
|
|
|
it('throw si heuresPrevues < 0', () => {
|
|
expect(
|
|
() => new Module({ id: 1, blocId: 10, nom: 'M1', heuresPrevues: new Decimal(-1) }),
|
|
).toThrow();
|
|
});
|
|
});
|
|
|
|
describe('Module — creerAttribution happy path', () => {
|
|
it('cree attribution + maj rollups + statut module', () => {
|
|
const m = new Module({ id: 1, blocId: 10, nom: 'M1', heuresPrevues: new Decimal(40) });
|
|
const p = formateurActif();
|
|
const a = m.creerAttribution(p, new Decimal(20), null, null, 100);
|
|
expect(a.id).toBe(100);
|
|
expect(m.attributions.length).toBe(1);
|
|
expect(m.heuresAttribuees().toNumber()).toBe(20);
|
|
expect(p.heuresAttribueesFormation.toNumber()).toBe(20);
|
|
expect(m.statut).toBe('attribue');
|
|
});
|
|
});
|
|
|
|
describe('Module — creerAttribution validations', () => {
|
|
it('throw si personne sans role formateur', () => {
|
|
const m = new Module({ id: 1, blocId: 10, nom: 'M1', heuresPrevues: new Decimal(40) });
|
|
const p = formateurActif();
|
|
p.retirerRole('formateur');
|
|
p.ajouterRole('developpeur');
|
|
expect(() => m.creerAttribution(p, new Decimal(10), null, null, 1)).toThrow(/formateur/);
|
|
});
|
|
|
|
it('throw si personne inactive', () => {
|
|
const m = new Module({ id: 1, blocId: 10, nom: 'M1', heuresPrevues: new Decimal(40) });
|
|
const p = formateurActif();
|
|
p.inactiver();
|
|
expect(() => m.creerAttribution(p, new Decimal(10), null, null, 1)).toThrow(/inactiv/);
|
|
});
|
|
|
|
it('throw si heures <= 0', () => {
|
|
const m = new Module({ id: 1, blocId: 10, nom: 'M1', heuresPrevues: new Decimal(40) });
|
|
const p = formateurActif();
|
|
expect(() => m.creerAttribution(p, new Decimal(0), null, null, 1)).toThrow();
|
|
});
|
|
|
|
it('RG-01 violation : SUM(heures) > heuresPrevues', () => {
|
|
const m = new Module({ id: 1, blocId: 10, nom: 'M1', heuresPrevues: new Decimal(40) });
|
|
const p = formateurActif();
|
|
m.creerAttribution(p, new Decimal(30), null, null, 1);
|
|
expect(() => m.creerAttribution(p, new Decimal(15), null, null, 2)).toThrow(/RG-01/);
|
|
});
|
|
|
|
it('warning si heures > capacite formation restante de la personne (ne throw pas)', () => {
|
|
const m = new Module({ id: 1, blocId: 10, nom: 'M1', heuresPrevues: new Decimal(1000) });
|
|
const p = new Personne({
|
|
id: 1,
|
|
nom: 'X',
|
|
prenom: 'Y',
|
|
email: 'x@y.fr',
|
|
capaciteAnnuelle: new Decimal(100),
|
|
splitFormationPct: new Decimal(50),
|
|
splitAgencePct: new Decimal(50),
|
|
roles: new Set<Role>(['formateur']),
|
|
statut: 'actif',
|
|
});
|
|
// capacite formation = 50. On attribue 80.
|
|
const a = m.creerAttribution(p, new Decimal(80), null, null, 1);
|
|
expect(a.heuresAttribuees.toNumber()).toBe(80);
|
|
expect(p.heuresAttribueesFormation.toNumber()).toBe(80);
|
|
});
|
|
|
|
it('RG-01 ignore les attributions annulees', () => {
|
|
const m = new Module({ id: 1, blocId: 10, nom: 'M1', heuresPrevues: new Decimal(40) });
|
|
const p = formateurActif();
|
|
const a1 = m.creerAttribution(p, new Decimal(40), null, null, 1);
|
|
a1.annuler('test');
|
|
// La nouvelle attribution doit etre acceptee maintenant.
|
|
const a2 = m.creerAttribution(p, new Decimal(20), null, null, 2);
|
|
expect(a2.heuresAttribuees.toNumber()).toBe(20);
|
|
});
|
|
});
|
|
|
|
describe('Module — heuresRealisees / heuresRestantes', () => {
|
|
it('calcule heures realisees en ignorant annulations', () => {
|
|
const m = new Module({ id: 1, blocId: 10, nom: 'M1', heuresPrevues: new Decimal(50) });
|
|
const p = formateurActif();
|
|
const a = m.creerAttribution(p, new Decimal(30), null, null, 1);
|
|
a.saisirHeuresRealisees(new Decimal(10));
|
|
expect(m.heuresRealisees().toNumber()).toBe(10);
|
|
expect(m.heuresRestantes().toNumber()).toBe(20);
|
|
});
|
|
});
|
|
|
|
describe('Module — transitions de statut', () => {
|
|
it('cloturer', () => {
|
|
const m = new Module({ id: 1, blocId: 10, nom: 'M1', heuresPrevues: new Decimal(40) });
|
|
m.cloturer();
|
|
expect(m.statut).toBe('realise');
|
|
});
|
|
|
|
it('annuler depuis a_attribuer', () => {
|
|
const m = new Module({ id: 1, blocId: 10, nom: 'M1', heuresPrevues: new Decimal(40) });
|
|
m.annuler();
|
|
expect(m.statut).toBe('annule');
|
|
});
|
|
|
|
it('annuler bloque depuis realise', () => {
|
|
const m = new Module({
|
|
id: 1,
|
|
blocId: 10,
|
|
nom: 'M1',
|
|
heuresPrevues: new Decimal(40),
|
|
statut: 'realise',
|
|
});
|
|
expect(() => m.annuler()).toThrow();
|
|
});
|
|
|
|
it('cloturer bloque depuis annule', () => {
|
|
const m = new Module({
|
|
id: 1,
|
|
blocId: 10,
|
|
nom: 'M1',
|
|
heuresPrevues: new Decimal(40),
|
|
statut: 'annule',
|
|
});
|
|
expect(() => m.cloturer()).toThrow();
|
|
});
|
|
});
|