getSecurityUser(); $tenantId = TenantId::fromString($user->tenantId()); $file = $request->files->get('file'); if (!$file instanceof UploadedFile) { throw new BadRequestHttpException('Un fichier CSV ou XLSX est requis.'); } if ($file->getSize() > self::MAX_FILE_SIZE) { throw new BadRequestHttpException('Le fichier dépasse la taille maximale de 10 Mo.'); } $extension = strtolower($file->getClientOriginalExtension()); if (!in_array($extension, ['csv', 'txt', 'xlsx', 'xls'], true)) { throw new BadRequestHttpException('Extension non supportée. Utilisez CSV ou XLSX.'); } $allowedMimeTypes = [ 'text/csv', 'text/plain', 'application/csv', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.ms-excel', ]; $mimeType = $file->getMimeType(); if ($mimeType === null || !in_array($mimeType, $allowedMimeTypes, true)) { throw new BadRequestHttpException('Type de fichier non supporté. Utilisez CSV ou XLSX.'); } try { $result = $this->orchestrator->analyzeFile( $file->getPathname(), $extension, $file->getClientOriginalName(), $tenantId, ); } catch (FichierImportInvalideException|InvalidArgumentException $e) { throw new BadRequestHttpException($e->getMessage()); } $batch = $result['batch']; $suggestedMapping = $result['suggestedMapping']; return new JsonResponse([ 'id' => (string) $batch->id, 'filename' => $batch->originalFilename, 'totalRows' => $batch->totalRows, 'columns' => $batch->detectedColumns, 'detectedFormat' => ($batch->detectedFormat ?? KnownImportFormat::CUSTOM)->value, 'suggestedMapping' => $this->serializeMapping($suggestedMapping), 'preview' => $this->serializeRows(array_slice($batch->lignes(), 0, 5)), ], Response::HTTP_CREATED); } /** * T7.2 : Valider et appliquer le mapping des colonnes. */ #[Route('/{id}/mapping', methods: ['POST'], name: 'api_import_students_mapping')] public function mapping(string $id, Request $request): JsonResponse { $user = $this->getSecurityUser(); $batch = $this->getBatch($id, TenantId::fromString($user->tenantId())); $data = $request->toArray(); /** @var array $mappingData */ $mappingData = $data['mapping'] ?? []; /** @var string $formatValue */ $formatValue = $data['format'] ?? ''; $format = KnownImportFormat::tryFrom($formatValue) ?? KnownImportFormat::CUSTOM; /** @var array $mappingFields */ $mappingFields = []; foreach ($mappingData as $column => $fieldValue) { $field = StudentImportField::tryFrom($fieldValue); if ($field !== null) { $mappingFields[$column] = $field; } } try { $columnMapping = ColumnMapping::creer($mappingFields, $format); } catch (MappingIncompletException $e) { throw new BadRequestHttpException($e->getMessage()); } $this->orchestrator->applyMapping($batch, $columnMapping); return new JsonResponse([ 'id' => (string) $batch->id, 'mapping' => $this->serializeMapping($columnMapping->mapping), 'totalRows' => $batch->totalRows, ]); } /** * T7.3 : Preview avec validation et erreurs. */ #[Route('/{id}/preview', methods: ['GET'], name: 'api_import_students_preview')] public function preview(string $id): JsonResponse { $user = $this->getSecurityUser(); $tenantId = TenantId::fromString($user->tenantId()); $batch = $this->getBatch($id, $tenantId); $academicYearIdRaw = $this->academicYearResolver->resolve('current'); $academicYearId = $academicYearIdRaw !== null ? AcademicYearId::fromString($academicYearIdRaw) : null; $result = $this->orchestrator->generatePreview($batch, $tenantId, $academicYearId); return new JsonResponse([ 'id' => (string) $batch->id, 'totalRows' => $result['report']->totalRows, 'validCount' => $result['report']->importedCount, 'errorCount' => $result['report']->errorCount, 'rows' => $this->serializeRows($result['validatedRows']), 'unknownClasses' => $result['unknownClasses'], ]); } /** * T7.4 : Confirmer et lancer l'import. */ #[Route('/{id}/confirm', methods: ['POST'], name: 'api_import_students_confirm')] public function confirm(string $id, Request $request): JsonResponse { $user = $this->getSecurityUser(); $tenantId = TenantId::fromString($user->tenantId()); $batch = $this->getBatch($id, $tenantId); $data = $request->toArray(); /** @var bool $createMissingClasses */ $createMissingClasses = $data['createMissingClasses'] ?? false; /** @var bool $importValidOnly */ $importValidOnly = $data['importValidOnly'] ?? true; $academicYearId = $this->academicYearResolver->resolve('current') ?? throw new BadRequestHttpException('Impossible de résoudre l\'année scolaire courante.'); $schoolName = $this->tenantContext->getCurrentTenantConfig()->subdomain; $this->orchestrator->prepareForConfirmation( $batch, $createMissingClasses, $importValidOnly, $tenantId, AcademicYearId::fromString($academicYearId), ); $this->commandBus->dispatch(new ImportStudentsCommand( batchId: (string) $batch->id, tenantId: $user->tenantId(), schoolName: $schoolName, academicYearId: $academicYearId, createMissingClasses: $createMissingClasses, )); return new JsonResponse([ 'id' => (string) $batch->id, 'status' => 'processing', 'message' => 'Import lancé. Suivez la progression via GET /status.', ], Response::HTTP_ACCEPTED); } /** * T7.5 : Statut et progression. */ #[Route('/{id}/status', methods: ['GET'], name: 'api_import_students_status')] public function status(string $id): JsonResponse { $user = $this->getSecurityUser(); $batch = $this->getBatch($id, TenantId::fromString($user->tenantId())); return new JsonResponse([ 'id' => (string) $batch->id, 'status' => $batch->status->value, 'totalRows' => $batch->totalRows, 'importedCount' => $batch->importedCount, 'errorCount' => $batch->errorCount, 'progression' => $batch->progression(), 'completedAt' => $batch->completedAt?->format(DateTimeInterface::ATOM), ]); } /** * T7.6 : Télécharger le rapport. */ #[Route('/{id}/report', methods: ['GET'], name: 'api_import_students_report')] public function report(string $id): JsonResponse { $user = $this->getSecurityUser(); $batch = $this->getBatch($id, TenantId::fromString($user->tenantId())); $report = ImportReport::fromValidatedRows($batch->lignes()); return new JsonResponse([ 'id' => (string) $batch->id, 'status' => $batch->status->value, 'totalRows' => $report->totalRows, 'importedCount' => $batch->importedCount, 'errorCount' => $batch->errorCount, 'report' => $report->lignesRapport(), 'errors' => array_map( static fn (ImportRow $row) => [ 'line' => $row->lineNumber, 'errors' => array_map( static fn (ImportRowError $error) => [ 'column' => $error->column, 'message' => $error->message, ], $row->errors, ), ], $report->errorRows, ), ]); } private function getSecurityUser(): SecurityUser { $user = $this->security->getUser(); if (!$user instanceof SecurityUser) { throw new AccessDeniedHttpException(); } return $user; } private function getBatch(string $id, TenantId $tenantId): StudentImportBatch { try { $batch = $this->importBatchRepository->get(ImportBatchId::fromString($id)); } catch (ImportBatchNotFoundException|InvalidArgumentException) { throw new NotFoundHttpException('Import non trouvé.'); } if ((string) $batch->tenantId !== (string) $tenantId) { throw new NotFoundHttpException('Import non trouvé.'); } return $batch; } /** * @param array $mapping * * @return array */ private function serializeMapping(array $mapping): array { $result = []; foreach ($mapping as $column => $field) { $result[$column] = $field->value; } return $result; } /** * @param list $rows * * @return list> */ private function serializeRows(array $rows): array { return array_map( static fn (ImportRow $row) => [ 'line' => $row->lineNumber, 'data' => $row->mappedData, 'valid' => $row->estValide(), 'errors' => array_map( static fn (ImportRowError $error) => [ 'column' => $error->column, 'message' => $error->message, ], $row->errors, ), ], $rows, ); } }