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>
121 lines
4.1 KiB
TypeScript
121 lines
4.1 KiB
TypeScript
import { Decimal } from 'decimal.js';
|
|
import { describe, expect, it } from 'vitest';
|
|
import { Personne } from '../../src/domain/personne.js';
|
|
import { Tache } from '../../src/domain/tache.js';
|
|
import type { Role } from '../../src/domain/types.js';
|
|
|
|
const developpeur = (overrides: Partial<ConstructorParameters<typeof Personne>[0]> = {}) =>
|
|
new Personne({
|
|
id: 1,
|
|
nom: 'Dev',
|
|
prenom: 'Jane',
|
|
email: 'jane@acadenice.fr',
|
|
capaciteAnnuelle: new Decimal(1000),
|
|
splitFormationPct: new Decimal(20),
|
|
splitAgencePct: new Decimal(80),
|
|
roles: new Set<Role>(['developpeur']),
|
|
statut: 'actif',
|
|
...overrides,
|
|
});
|
|
|
|
describe('Tache — constructeur', () => {
|
|
it('cree une tache valide', () => {
|
|
const t = new Tache({ id: 1, projetId: 5, titre: 'T1', chargeHeures: new Decimal(8) });
|
|
expect(t.statut).toBe('todo');
|
|
});
|
|
|
|
it('throw si charge < 0', () => {
|
|
expect(
|
|
() => new Tache({ id: 1, projetId: 5, titre: 'T1', chargeHeures: new Decimal(-1) }),
|
|
).toThrow();
|
|
});
|
|
});
|
|
|
|
describe('Tache — creerIntervention', () => {
|
|
it('happy path', () => {
|
|
const t = new Tache({ id: 1, projetId: 5, titre: 'T1', chargeHeures: new Decimal(8) });
|
|
const p = developpeur();
|
|
const i = t.creerIntervention(p, new Decimal(3), new Date('2026-05-01'), 100);
|
|
expect(i.heures.toNumber()).toBe(3);
|
|
expect(t.heuresRealisees().toNumber()).toBe(3);
|
|
expect(p.heuresAttribueesAgence.toNumber()).toBe(3);
|
|
});
|
|
|
|
it('throw si role != developpeur', () => {
|
|
const t = new Tache({ id: 1, projetId: 5, titre: 'T1', chargeHeures: new Decimal(8) });
|
|
const p = developpeur({ roles: new Set<Role>(['formateur']) });
|
|
expect(() => t.creerIntervention(p, new Decimal(3), new Date(), 1)).toThrow(/developpeur/);
|
|
});
|
|
|
|
it('throw si heures <= 0', () => {
|
|
const t = new Tache({ id: 1, projetId: 5, titre: 'T1', chargeHeures: new Decimal(8) });
|
|
const p = developpeur();
|
|
expect(() => t.creerIntervention(p, new Decimal(0), new Date(), 1)).toThrow();
|
|
expect(() => t.creerIntervention(p, new Decimal(-1), new Date(), 1)).toThrow();
|
|
});
|
|
|
|
it('throw si personne inactive', () => {
|
|
const t = new Tache({ id: 1, projetId: 5, titre: 'T1', chargeHeures: new Decimal(8) });
|
|
const p = developpeur({ statut: 'inactif' });
|
|
expect(() => t.creerIntervention(p, new Decimal(2), new Date(), 1)).toThrow(/inactiv/);
|
|
});
|
|
|
|
it('annulation exclue du rollup heuresRealisees', () => {
|
|
const t = new Tache({ id: 1, projetId: 5, titre: 'T1', chargeHeures: new Decimal(8) });
|
|
const p = developpeur();
|
|
const i1 = t.creerIntervention(p, new Decimal(3), new Date(), 1);
|
|
t.creerIntervention(p, new Decimal(2), new Date(), 2);
|
|
i1.annuler('test');
|
|
expect(t.heuresRealisees().toNumber()).toBe(2);
|
|
});
|
|
});
|
|
|
|
describe('Tache — transitions de statut', () => {
|
|
it('marquerInProgress', () => {
|
|
const t = new Tache({ id: 1, projetId: 5, titre: 'T1', chargeHeures: new Decimal(8) });
|
|
t.marquerInProgress();
|
|
expect(t.statut).toBe('in_progress');
|
|
});
|
|
|
|
it('marquerReview depuis in_progress', () => {
|
|
const t = new Tache({ id: 1, projetId: 5, titre: 'T1', chargeHeures: new Decimal(8) });
|
|
t.marquerInProgress();
|
|
t.marquerReview();
|
|
expect(t.statut).toBe('review');
|
|
});
|
|
|
|
it('marquerReview invalide depuis todo', () => {
|
|
const t = new Tache({ id: 1, projetId: 5, titre: 'T1', chargeHeures: new Decimal(8) });
|
|
expect(() => t.marquerReview()).toThrow();
|
|
});
|
|
|
|
it('marquerDone depuis review', () => {
|
|
const t = new Tache({ id: 1, projetId: 5, titre: 'T1', chargeHeures: new Decimal(8) });
|
|
t.marquerInProgress();
|
|
t.marquerReview();
|
|
t.marquerDone();
|
|
expect(t.statut).toBe('done');
|
|
});
|
|
|
|
it('marquerDone bloque depuis done', () => {
|
|
const t = new Tache({
|
|
id: 1,
|
|
projetId: 5,
|
|
titre: 'T1',
|
|
chargeHeures: new Decimal(8),
|
|
statut: 'done',
|
|
});
|
|
expect(() => t.marquerDone()).toThrow();
|
|
});
|
|
|
|
it('marquerInProgress bloque depuis abandoned', () => {
|
|
const t = new Tache({
|
|
id: 1,
|
|
projetId: 5,
|
|
titre: 'T1',
|
|
chargeHeures: new Decimal(8),
|
|
statut: 'abandoned',
|
|
});
|
|
expect(() => t.marquerInProgress()).toThrow();
|
|
});
|
|
});
|