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>
177 lines
5.6 KiB
TypeScript
177 lines
5.6 KiB
TypeScript
import { Decimal } from 'decimal.js';
|
|
import { describe, expect, it } from 'vitest';
|
|
import { Personne } from '../../src/domain/personne.js';
|
|
import type { Role } from '../../src/domain/types.js';
|
|
|
|
const baseProps = (overrides: Partial<ConstructorParameters<typeof Personne>[0]> = {}) => ({
|
|
id: 1,
|
|
nom: 'Dupont',
|
|
prenom: 'Pierre',
|
|
email: 'pierre@acadenice.fr',
|
|
capaciteAnnuelle: new Decimal(1000),
|
|
splitFormationPct: new Decimal(60),
|
|
splitAgencePct: new Decimal(40),
|
|
roles: new Set<Role>(['formateur']),
|
|
statut: 'actif' as const,
|
|
...overrides,
|
|
});
|
|
|
|
describe('Personne — constructeur', () => {
|
|
it('cree une personne valide', () => {
|
|
const p = new Personne(baseProps());
|
|
expect(p.id).toBe(1);
|
|
expect(p.statut).toBe('actif');
|
|
});
|
|
|
|
it('throw si splits != 100', () => {
|
|
expect(
|
|
() =>
|
|
new Personne(
|
|
baseProps({ splitFormationPct: new Decimal(70), splitAgencePct: new Decimal(40) }),
|
|
),
|
|
).toThrow(/100/);
|
|
});
|
|
|
|
it('throw si capaciteAnnuelle < 0', () => {
|
|
expect(() => new Personne(baseProps({ capaciteAnnuelle: new Decimal(-1) }))).toThrow();
|
|
});
|
|
|
|
it('throw si splitFormationPct hors [0,100]', () => {
|
|
expect(
|
|
() =>
|
|
new Personne(
|
|
baseProps({ splitFormationPct: new Decimal(-10), splitAgencePct: new Decimal(110) }),
|
|
),
|
|
).toThrow();
|
|
});
|
|
|
|
it('throw si splitAgencePct hors [0,100]', () => {
|
|
expect(
|
|
() =>
|
|
new Personne(
|
|
baseProps({ splitFormationPct: new Decimal(150), splitAgencePct: new Decimal(-50) }),
|
|
),
|
|
).toThrow();
|
|
});
|
|
|
|
it('admin pur capacite=0 splits=0/100 — accepte uniquement si somme=100', () => {
|
|
const p = new Personne(
|
|
baseProps({
|
|
capaciteAnnuelle: new Decimal(0),
|
|
splitFormationPct: new Decimal(0),
|
|
splitAgencePct: new Decimal(100),
|
|
roles: new Set<Role>(['admin']),
|
|
}),
|
|
);
|
|
expect(p.capaciteAnnuelle.toNumber()).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('Personne — calculs heures restantes', () => {
|
|
it('cas nominal split 60/40, capacite 1000', () => {
|
|
const p = new Personne(baseProps());
|
|
expect(p.capaciteFormation().toNumber()).toBe(600);
|
|
expect(p.capaciteAgence().toNumber()).toBe(400);
|
|
expect(p.heuresRestantesFormation().toNumber()).toBe(600);
|
|
expect(p.heuresRestantesAgence().toNumber()).toBe(400);
|
|
expect(p.heuresRestantesTotal().toNumber()).toBe(1000);
|
|
});
|
|
|
|
it('avec heures attribuees', () => {
|
|
const p = new Personne(
|
|
baseProps({
|
|
heuresAttribueesFormation: new Decimal(150),
|
|
heuresAttribueesAgence: new Decimal(100),
|
|
}),
|
|
);
|
|
expect(p.heuresRestantesFormation().toNumber()).toBe(450);
|
|
expect(p.heuresRestantesAgence().toNumber()).toBe(300);
|
|
expect(p.heuresRestantesTotal().toNumber()).toBe(750);
|
|
});
|
|
|
|
it('cas zero capacite', () => {
|
|
const p = new Personne(
|
|
baseProps({
|
|
capaciteAnnuelle: new Decimal(0),
|
|
splitFormationPct: new Decimal(50),
|
|
splitAgencePct: new Decimal(50),
|
|
}),
|
|
);
|
|
expect(p.heuresRestantesTotal().toNumber()).toBe(0);
|
|
expect(p.heuresRestantesFormation().toNumber()).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('Personne — gestion roles', () => {
|
|
it('ajouterRole idempotent', () => {
|
|
const p = new Personne(baseProps());
|
|
p.ajouterRole('formateur');
|
|
p.ajouterRole('formateur');
|
|
expect(p.roles.size).toBe(1);
|
|
});
|
|
|
|
it('ajouterRole nouveau', () => {
|
|
const p = new Personne(baseProps());
|
|
p.ajouterRole('developpeur');
|
|
expect(p.hasRole('developpeur')).toBe(true);
|
|
expect(p.roles.size).toBe(2);
|
|
});
|
|
|
|
it('retirerRole sans attributions actives', () => {
|
|
const p = new Personne(baseProps());
|
|
p.retirerRole('formateur');
|
|
expect(p.hasRole('formateur')).toBe(false);
|
|
});
|
|
|
|
it('retirerRole formateur bloque si attributions actives', () => {
|
|
const p = new Personne(baseProps());
|
|
expect(() => p.retirerRole('formateur', { activeAttributions: 2 })).toThrow(/attributions/);
|
|
});
|
|
|
|
it('retirerRole developpeur bloque si interventions actives', () => {
|
|
const p = new Personne(baseProps({ roles: new Set<Role>(['formateur', 'developpeur']) }));
|
|
expect(() => p.retirerRole('developpeur', { activeInterventions: 1 })).toThrow(/interventions/);
|
|
});
|
|
});
|
|
|
|
describe('Personne — transitions de statut', () => {
|
|
it('activer / inactiver', () => {
|
|
const p = new Personne(baseProps({ statut: 'inactif' }));
|
|
p.activer();
|
|
expect(p.statut).toBe('actif');
|
|
p.inactiver();
|
|
expect(p.statut).toBe('inactif');
|
|
});
|
|
});
|
|
|
|
describe('Personne — mutations rollups', () => {
|
|
it('appliquer heures formation accumule', () => {
|
|
const p = new Personne(baseProps());
|
|
p._appliquerHeuresFormation(new Decimal(100));
|
|
p._appliquerHeuresFormation(new Decimal(50));
|
|
expect(p.heuresAttribueesFormation.toNumber()).toBe(150);
|
|
});
|
|
|
|
it('appliquer heures agence accumule', () => {
|
|
const p = new Personne(baseProps());
|
|
p._appliquerHeuresAgence(new Decimal(80));
|
|
expect(p.heuresAttribueesAgence.toNumber()).toBe(80);
|
|
});
|
|
|
|
it('throw si rollup formation devient negatif', () => {
|
|
const p = new Personne(baseProps());
|
|
expect(() => p._appliquerHeuresFormation(new Decimal(-10))).toThrow();
|
|
});
|
|
|
|
it('throw si rollup agence devient negatif', () => {
|
|
const p = new Personne(baseProps());
|
|
expect(() => p._appliquerHeuresAgence(new Decimal(-5))).toThrow();
|
|
});
|
|
|
|
it('warn quand surcharge formation (depasse capacite)', () => {
|
|
const p = new Personne(baseProps({ capaciteAnnuelle: new Decimal(100) }));
|
|
// capacite formation = 60. Apply 80 → warn mais accepte.
|
|
p._appliquerHeuresFormation(new Decimal(80));
|
|
expect(p.heuresAttribueesFormation.toNumber()).toBe(80);
|
|
});
|
|
});
|