src/Services/InscripcionCursoService.php line 29

Open in your IDE?
  1. <?php
  2. namespace App\Services;
  3. use App\Entity\FormularioRespuestas;
  4. use App\Entity\Participante;
  5. use App\Entity\ParticipanteGrupoDetalle;
  6. use App\Entity\ParticipantesGrupo;
  7. use App\Exception\NoEncontradoException;
  8. use App\Interfaces\GrupoRepositoryInterface;
  9. use App\Interfaces\ParticipanteGrupoDetalleRepositoryInterface;
  10. use App\Interfaces\ParticipantesGrupoRepositoryInterface;
  11. use App\Services\EventDomain\InscripcionCreatedEvent;
  12. use Psr\EventDispatcher\EventDispatcherInterface;
  13. use Psr\Log\LoggerInterface;
  14. use Symfony\Component\HttpFoundation\File\Exception\FileException;
  15. use Symfony\Component\HttpFoundation\File\File;
  16. use Symfony\Component\HttpFoundation\File\UploadedFile;
  17. use Symfony\Component\Security\Core\Security;
  18. /**
  19. * Servicio para gestionar las inscripciones a cursos
  20. */
  21. class InscripcionCursoService
  22. {
  23. /**
  24. * Constructor
  25. */
  26. public function __construct(
  27. private GrupoRepositoryInterface $grupoRepository,
  28. private ParticipantesGrupoRepositoryInterface $participantesGrupoRepository,
  29. private ParticipanteGrupoDetalleRepositoryInterface $participanteGrupoDetalleRepository,
  30. private Security $security,
  31. private LoggerInterface $logger,
  32. private ?RenderPdfChromeService $pdfRenderer = null,
  33. private ?ValidService $validService = null,
  34. private EventDispatcherInterface $eventDispatcher
  35. )
  36. {
  37. }
  38. /**
  39. * Crea o actualiza una inscripción a un curso.
  40. *
  41. * Soporta dos escenarios:
  42. * - Inscripción directa: el participante se inscribe él mismo
  43. * - Inscripción delegada: una empresa inscribió previamente al empleado (estado PREINSCRITO)
  44. * y el empleado firma posteriormente el Anexo 1
  45. *
  46. * @param int $grupoId ID del grupo (curso)
  47. * @param FormularioRespuestas $respuesta Respuesta del formulario
  48. * @param string $pdfContent Contenido del PDF generado
  49. * @param string $firmaXmlBase64 Firma XML en base64
  50. * @return bool True si la inscripción se realizó correctamente
  51. * @throws NoEncontradoException Si no se encuentra el usuario, propietario de contenido, grupo o entidad
  52. */
  53. public function crearInscripcion($grupoId, FormularioRespuestas $respuesta, string $pdfContent, string $firmaXmlBase64): bool
  54. {
  55. // === VALIDACIONES INICIALES ===
  56. $usuarioHermes = $this->security->getUser();
  57. if (!$usuarioHermes) {
  58. throw new NoEncontradoException('Usuario no encontrado');
  59. }
  60. $propietariosContenido = $usuarioHermes->getPropietarioContenidos();
  61. if (!$propietariosContenido || count($propietariosContenido) === 0) {
  62. throw new NoEncontradoException('No se encontró el propietario de contenido asociado al usuario');
  63. }
  64. $grupo = $this->grupoRepository->findById($grupoId);
  65. if (!$grupo) {
  66. throw new NoEncontradoException('Curso no encontrado');
  67. }
  68. if (
  69. !$grupo->getSolicitudAccionFormativa() ||
  70. !$grupo->getSolicitudAccionFormativa()->getExpediente() ||
  71. !$grupo->getSolicitudAccionFormativa()->getExpediente()->getExpedientePerteneAEntidad()
  72. ) {
  73. throw new NoEncontradoException('No se encontró la entidad asociada al curso');
  74. }
  75. $entidadColaboradora = $grupo->getSolicitudAccionFormativa()
  76. ->getExpediente()
  77. ->getExpedientePerteneAEntidad()
  78. ->getEntidadColaboradora();
  79. if (!$entidadColaboradora) {
  80. throw new NoEncontradoException('No se encontró la entidad colaboradora asociada al curso');
  81. }
  82. // === FASE 1: DETECCIÓN - Buscar inscripción existente en TODOS los roles ===
  83. $inscripcion = null;
  84. $participanteAsociado = null;
  85. $primerParticipanteDisponible = null;
  86. foreach ($propietariosContenido as $propietarioContenido) {
  87. if ($propietarioContenido instanceof Participante) {
  88. // Guardar el primer participante disponible para crear nueva inscripción si es necesario
  89. if ($primerParticipanteDisponible === null) {
  90. $primerParticipanteDisponible = $propietarioContenido;
  91. }
  92. // Buscar inscripción activa para este participante
  93. $inscripcionExistente = $this->participantesGrupoRepository->findActiveByParticipanteEntidadAndGrupo(
  94. $propietarioContenido,
  95. $entidadColaboradora,
  96. $grupo
  97. );
  98. if ($inscripcionExistente) {
  99. $inscripcion = $inscripcionExistente;
  100. $participanteAsociado = $propietarioContenido;
  101. $this->logger->info("Inscripción existente encontrada para grupo {$grupoId}, participante ID: {$propietarioContenido->getId()}");
  102. break; // Detener búsqueda al encontrar inscripción existente
  103. }
  104. }
  105. }
  106. // === FASE 2: DECISIÓN - Crear nueva o usar existente ===
  107. $esNuevaInscripcion = false;
  108. if (!$inscripcion) {
  109. // No se encontró inscripción existente, crear nueva
  110. if (!$primerParticipanteDisponible) {
  111. $this->logger->warning("No se encontró ningún participante válido para inscribir en el grupo {$grupoId}");
  112. throw new NoEncontradoException('No se encontró ningún participante válido para realizar la inscripción');
  113. }
  114. $participanteAsociado = $primerParticipanteDisponible;
  115. $esNuevaInscripcion = true;
  116. // Crear ParticipanteGrupoDetalle
  117. $participanteGrupoDetalle = (new ParticipanteGrupoDetalle())
  118. ->setParticipante($participanteAsociado)
  119. ->setEntidadColaboradora($entidadColaboradora);
  120. $this->participanteGrupoDetalleRepository->save($participanteGrupoDetalle);
  121. // Crear nueva inscripción
  122. $inscripcion = (new ParticipantesGrupo())
  123. ->setParticipanteGrupoDetalle($participanteGrupoDetalle)
  124. ->setGrupo($grupo)
  125. ->setFechaInscripcion(new \DateTime())
  126. ->setIdExterno("{$participanteGrupoDetalle->getIdExterno()}::{$grupo->getIdExterno()}");
  127. }
  128. // === FASE 3: ACTUALIZACIÓN - Datos comunes para nueva o existente ===
  129. $inscripcion
  130. ->setIdFormularioRespuesta($respuesta->getId())
  131. ->setFirmaXml($firmaXmlBase64);
  132. // Manejo del PDF (solo si no existe ya uno firmado)
  133. if (!$inscripcion->getPdfFirmado()) {
  134. try {
  135. $tempFile = $this->crearArchivoTemporal($pdfContent);
  136. $pdfFile = new UploadedFile(
  137. $tempFile,
  138. basename($tempFile),
  139. 'application/pdf',
  140. null,
  141. true
  142. );
  143. $inscripcion->setPdfFile($pdfFile);
  144. } catch (FileException $e) {
  145. throw new \RuntimeException("Error procesando PDF: " . $e->getMessage());
  146. }
  147. }
  148. // === PERSISTENCIA ===
  149. $this->participantesGrupoRepository->save($inscripcion);
  150. $accion = $esNuevaInscripcion ? 'Creada' : 'Actualizada';
  151. $this->logger->info("{$accion} inscripción para grupo {$grupoId}, participante ID: {$participanteAsociado->getId()}");
  152. $this->eventDispatcher->dispatch(new InscripcionCreatedEvent($inscripcion, []));
  153. return true;
  154. }
  155. /**
  156. * Verifica si un usuario está inscrito en un grupo
  157. *
  158. * @param int $grupoId ID del grupo
  159. * @return bool True si está inscrito, false en caso contrario
  160. */
  161. public function verificarInscripcion($grupoId): bool
  162. {
  163. // Verificar que el usuario está autenticado
  164. $usuarioHermes = $this->security->getUser();
  165. if (!$usuarioHermes) {
  166. return false;
  167. }
  168. $propietariosContenido = $usuarioHermes->getPropietarioContenidos();
  169. if (!$propietariosContenido || count($propietariosContenido) === 0) {
  170. return false;
  171. }
  172. // Obtener el grupo
  173. $grupo = $this->grupoRepository->findById($grupoId);
  174. if (!$grupo) {
  175. return false;
  176. }
  177. if (!$grupo->getSolicitudAccionFormativa() ||
  178. !$grupo->getSolicitudAccionFormativa()->getExpediente() ||
  179. !$grupo->getSolicitudAccionFormativa()->getExpediente()->getExpedientePerteneAEntidad()) {
  180. return false;
  181. }
  182. $entidadColaboradora = $grupo->getSolicitudAccionFormativa()
  183. ->getExpediente()
  184. ->getExpedientePerteneAEntidad()
  185. ->getEntidadColaboradora();
  186. if (!$entidadColaboradora) {
  187. return false;
  188. }
  189. // Buscar inscripciones activas
  190. foreach ($propietariosContenido as $propietarioContenido) {
  191. if ($propietarioContenido->getType() === 'participante') {
  192. $inscripcion = $this->participantesGrupoRepository->findActiveByParticipanteEntidadAndGrupo(
  193. $propietarioContenido,
  194. $entidadColaboradora,
  195. $grupo
  196. );
  197. if ($inscripcion) {
  198. return true;
  199. }
  200. }
  201. }
  202. return false;
  203. }
  204. /**
  205. * Genera un PDF de la solicitud de inscripción a curso
  206. *
  207. * @param FormularioRespuestas $respuesta La respuesta del formulario
  208. * @return string|null Contenido del PDF generado o null si no se pudo generar
  209. * @throws \Exception Si ocurre un error en la generación
  210. */
  211. public function generarPdfInscripcion(FormularioRespuestas $respuesta): ?string
  212. {
  213. if (!$this->pdfRenderer) {
  214. throw new \Exception('El servicio de renderizado de PDF no está disponible');
  215. }
  216. // Procesar la respuesta JSON para la plantilla
  217. $jsonString = $respuesta->getJsonFormulario();
  218. if (empty($jsonString)) {
  219. $this->logger->warning('JSON vacío en la respuesta del formulario ID: ' . $respuesta->getId());
  220. $json = [];
  221. } else {
  222. $json = json_decode($jsonString, true);
  223. if (json_last_error() !== JSON_ERROR_NONE) {
  224. $this->logger->error('Error al decodificar JSON: ' . json_last_error_msg() . ' en formulario ID: ' . $respuesta->getId());
  225. $json = [];
  226. }
  227. }
  228. $respuestaSJson = [];
  229. foreach ($json['campos'] ?? $json ?? [] as $key => $value) {
  230. $respuestaSJson[] = [
  231. 'nombre' => $value['nombre'] ?? '',
  232. 'tipo' => $value['tipo'] ?? '',
  233. 'valor' => $value['valor'] ?? '',
  234. ];
  235. }
  236. // Determinar si se trata de un evento o un formulario regular
  237. $isEvent = false;
  238. if ($respuesta->getEvento()) {
  239. $isEvent = true;
  240. }
  241. // Preparar los datos para la plantilla
  242. $templateData = [
  243. 'isEvent' => $isEvent,
  244. 'formulario' => $respuesta->getFormulario(),
  245. 'respuestaSJson' => $respuestaSJson,
  246. 'respuesta' => $respuesta,
  247. 'solicitud_categoria' => $respuesta->getFormulario()?->getCategorias()?->first(),
  248. 'mostrar_header_footer' => true, // Para incluir cabecera y pie en el PDF
  249. ];
  250. try {
  251. // Renderizar a PDF
  252. return $this->pdfRenderer->__invoke(
  253. 'all/solicitud_categoria/show_pdf.html.twig',
  254. $templateData
  255. );
  256. } catch (\Exception $e) {
  257. $this->logger->error('Error al generar el PDF de inscripción: ' . $e->getMessage());
  258. throw $e;
  259. }
  260. }
  261. /**
  262. * Firma un documento PDF de inscripción usando VALid
  263. *
  264. * @param string $pdfContent Contenido del PDF a firmar
  265. * @param string $accessToken Token de acceso de VALid
  266. * @return array Datos de la firma incluyendo el archivo firmado temporal
  267. * @throws \Exception Si ocurre un error durante la firma
  268. */
  269. public function firmarDocumentoInscripcion(string $pdfContent, string $accessToken): string
  270. {
  271. if (!$this->validService) {
  272. throw new \Exception('El servicio de firma VALid no está disponible');
  273. }
  274. // Verificar espacio disponible en sistema de archivos
  275. $tempDir = sys_get_temp_dir();
  276. $freeSpace = disk_free_space($tempDir);
  277. if ($freeSpace < strlen($pdfContent) * 2) { // Necesitamos al menos el doble del tamaño para PDF original y firmado
  278. throw new \Exception('No hay suficiente espacio disponible para crear archivos temporales');
  279. }
  280. // Crear un archivo temporal para el PDF con nombre seguro
  281. $tempFile = tempnam($tempDir, 'inscripcion_');
  282. if ($tempFile === false) {
  283. throw new \Exception('No se pudo crear un archivo temporal para la firma');
  284. }
  285. // Establecer permisos restrictivos
  286. chmod($tempFile, 0600);
  287. try {
  288. // Escribir el contenido del PDF al archivo temporal
  289. file_put_contents($tempFile, $pdfContent);
  290. // Crear un objeto File para el documento
  291. $pdfFile = new File($tempFile);
  292. // Opciones de firma (pueden personalizarse según las necesidades)
  293. $opciones = [
  294. 'motivo' => 'Inscripción a curso',
  295. 'ubicacion' => 'Conforcat',
  296. 'contactInfo' => 'Plataforma Conforcat',
  297. ];
  298. // Llamar al servicio de firma
  299. $resultadoFirma = $this->validService->firmarDocumento($accessToken, $pdfFile, $opciones);
  300. // Verificar que se ha obtenido un documento firmado
  301. $resultado = json_decode($resultadoFirma, true);
  302. if ($resultado['status'] !== 'ok') {
  303. throw new \Exception('No se obtuvo el documento firmado del servicio VALid');
  304. }
  305. return $resultado['evidence'];
  306. } catch (\Exception $e) {
  307. // Asegurarse de limpiar los archivos temporales incluso en caso de error
  308. if (file_exists($tempFile)) {
  309. unlink($tempFile);
  310. }
  311. // También limpiar el archivo firmado si existe
  312. if (isset($tempFileFirmado) && file_exists($tempFileFirmado)) {
  313. unlink($tempFileFirmado);
  314. }
  315. $this->logger->error('Error al firmar el documento de inscripción: ' . $e->getMessage(), [
  316. 'exception' => $e,
  317. 'trace' => $e->getTraceAsString(),
  318. ]);
  319. throw $e;
  320. }
  321. }
  322. /**
  323. * Obtiene la URL de autenticación VALid para el proceso de firma
  324. *
  325. * @param string $state Identificador único para el estado de la sesión
  326. * @return string URL de autenticación
  327. */
  328. public function obtenerUrlAutenticacionValid(string $state): string
  329. {
  330. if (!$this->validService) {
  331. throw new \Exception('El servicio de firma VALid no está disponible');
  332. }
  333. return $this->validService->getAuthorizationUrl($state, true);
  334. }
  335. /**
  336. * Obtiene un token de acceso de VALid a partir del código de autorización
  337. *
  338. * @param string $authorizationCode Código de autorización recibido de VALid
  339. * @return array Información del token de acceso
  340. */
  341. public function obtenerTokenValid(string $authorizationCode): array
  342. {
  343. if (!$this->validService) {
  344. throw new \Exception('El servicio de firma VALid no está disponible');
  345. }
  346. return $this->validService->getAccessToken($authorizationCode);
  347. }
  348. /**
  349. * Obtiene información del usuario de VALid
  350. *
  351. * @param string $accessToken Token de acceso de VALid
  352. * @return array Información del usuario
  353. */
  354. public function obtenerInfoUsuarioValid(string $accessToken): array
  355. {
  356. if (!$this->validService) {
  357. throw new \Exception('El servicio de firma VALid no está disponible');
  358. }
  359. return $this->validService->getUserInfo($accessToken);
  360. }
  361. private function crearArchivoTemporal(string $contenido): string
  362. {
  363. $tempFileName = tempnam(sys_get_temp_dir(), 'pdf_');
  364. if (file_put_contents($tempFileName, $contenido) === false) {
  365. throw new \RuntimeException("No se pudo escribir en el archivo temporal");
  366. }
  367. return $tempFileName;
  368. }
  369. }