feat: Calculer automatiquement les moyennes après chaque saisie de notes
Some checks failed
CI / Backend Tests (push) Has been cancelled
CI / Frontend Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Naming Conventions (push) Has been cancelled
CI / Build Check (push) Has been cancelled

Les enseignants ont besoin de moyennes à jour immédiatement après la
publication ou modification des notes, sans attendre un batch nocturne.

Le système recalcule via Domain Events synchrones : statistiques
d'évaluation (min/max/moyenne/médiane), moyennes matières pondérées
(normalisation /20), et moyenne générale par élève. Les résultats sont
stockés dans des tables dénormalisées avec cache Redis (TTL 5 min).

Trois endpoints API exposent les données avec contrôle d'accès par rôle.
Une commande console permet le backfill des données historiques au
déploiement.
This commit is contained in:
2026-03-30 06:22:03 +02:00
parent b70d5ec2ad
commit b7dc27f2a5
786 changed files with 118783 additions and 316 deletions

58
frontend/e2e/helpers.ts Normal file
View File

@@ -0,0 +1,58 @@
import { execSync } from 'child_process';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
export const projectRoot = join(__dirname, '../..');
export const composeFile = join(projectRoot, 'compose.yaml');
export function execWithRetry(command: string, maxRetries = 3): string {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return execSync(command, { encoding: 'utf-8' });
} catch (error) {
if (attempt === maxRetries) throw error;
// Wait before retry: 1s, 2s, 3s
execSync(`sleep ${attempt}`);
}
}
throw new Error('Unreachable');
}
export function runSql(sql: string) {
execWithRetry(
`docker compose -f "${composeFile}" exec -T php php bin/console dbal:run-sql "${sql}" 2>&1`
);
}
export function clearCache() {
try {
execWithRetry(
`docker compose -f "${composeFile}" exec -T php php bin/console cache:pool:clear paginated_queries.cache 2>&1`
);
} catch {
// Cache pool may not exist
}
}
export function resolveDeterministicIds(tenantId: string): { schoolId: string; academicYearId: string } {
const output = execWithRetry(
`docker compose -f "${composeFile}" exec -T php php -r '` +
`require "/app/vendor/autoload.php"; ` +
`$t="${tenantId}"; $ns="6ba7b814-9dad-11d1-80b4-00c04fd430c8"; ` +
`echo Ramsey\\Uuid\\Uuid::uuid5($ns,"school-$t")->toString()."\\n"; ` +
`$m=(int)date("n"); $s=$m>=9?(int)date("Y"):(int)date("Y")-1; $e=$s+1; ` +
`echo Ramsey\\Uuid\\Uuid::uuid5($ns,"$t:$s-$e")->toString();` +
`' 2>&1`
).trim();
const [schoolId, academicYearId] = output.split('\n');
return { schoolId: schoolId!, academicYearId: academicYearId! };
}
export function createTestUser(tenant: string, email: string, password: string, role: string) {
execWithRetry(
`docker compose -f "${composeFile}" exec -T php php bin/console app:dev:create-test-user --tenant=${tenant} --email=${email} --password=${password} --role=${role} 2>&1`
);
}