<?php
namespace App\Services;
use App\Entity\FormularioRespuestas;
use App\Entity\Participante;
use App\Entity\ParticipanteGrupoDetalle;
use App\Entity\ParticipantesGrupo;
use App\Exception\NoEncontradoException;
use App\Interfaces\GrupoRepositoryInterface;
use App\Interfaces\ParticipanteGrupoDetalleRepositoryInterface;
use App\Interfaces\ParticipantesGrupoRepositoryInterface;
use App\Services\EventDomain\InscripcionCreatedEvent;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\File\Exception\FileException;
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\Security\Core\Security;
/**
* Servicio para gestionar las inscripciones a cursos
*/
class InscripcionCursoService
{
/**
* Constructor
*/
public function __construct(
private GrupoRepositoryInterface $grupoRepository,
private ParticipantesGrupoRepositoryInterface $participantesGrupoRepository,
private ParticipanteGrupoDetalleRepositoryInterface $participanteGrupoDetalleRepository,
private Security $security,
private LoggerInterface $logger,
private ?RenderPdfChromeService $pdfRenderer = null,
private ?ValidService $validService = null,
private EventDispatcherInterface $eventDispatcher
)
{
}
/**
* Crea o actualiza una inscripción a un curso.
*
* Soporta dos escenarios:
* - Inscripción directa: el participante se inscribe él mismo
* - Inscripción delegada: una empresa inscribió previamente al empleado (estado PREINSCRITO)
* y el empleado firma posteriormente el Anexo 1
*
* @param int $grupoId ID del grupo (curso)
* @param FormularioRespuestas $respuesta Respuesta del formulario
* @param string $pdfContent Contenido del PDF generado
* @param string $firmaXmlBase64 Firma XML en base64
* @return bool True si la inscripción se realizó correctamente
* @throws NoEncontradoException Si no se encuentra el usuario, propietario de contenido, grupo o entidad
*/
public function crearInscripcion($grupoId, FormularioRespuestas $respuesta, string $pdfContent, string $firmaXmlBase64): bool
{
// === VALIDACIONES INICIALES ===
$usuarioHermes = $this->security->getUser();
if (!$usuarioHermes) {
throw new NoEncontradoException('Usuario no encontrado');
}
$propietariosContenido = $usuarioHermes->getPropietarioContenidos();
if (!$propietariosContenido || count($propietariosContenido) === 0) {
throw new NoEncontradoException('No se encontró el propietario de contenido asociado al usuario');
}
$grupo = $this->grupoRepository->findById($grupoId);
if (!$grupo) {
throw new NoEncontradoException('Curso no encontrado');
}
if (
!$grupo->getSolicitudAccionFormativa() ||
!$grupo->getSolicitudAccionFormativa()->getExpediente() ||
!$grupo->getSolicitudAccionFormativa()->getExpediente()->getExpedientePerteneAEntidad()
) {
throw new NoEncontradoException('No se encontró la entidad asociada al curso');
}
$entidadColaboradora = $grupo->getSolicitudAccionFormativa()
->getExpediente()
->getExpedientePerteneAEntidad()
->getEntidadColaboradora();
if (!$entidadColaboradora) {
throw new NoEncontradoException('No se encontró la entidad colaboradora asociada al curso');
}
// === FASE 1: DETECCIÓN - Buscar inscripción existente en TODOS los roles ===
$inscripcion = null;
$participanteAsociado = null;
$primerParticipanteDisponible = null;
foreach ($propietariosContenido as $propietarioContenido) {
if ($propietarioContenido instanceof Participante) {
// Guardar el primer participante disponible para crear nueva inscripción si es necesario
if ($primerParticipanteDisponible === null) {
$primerParticipanteDisponible = $propietarioContenido;
}
// Buscar inscripción activa para este participante
$inscripcionExistente = $this->participantesGrupoRepository->findActiveByParticipanteEntidadAndGrupo(
$propietarioContenido,
$entidadColaboradora,
$grupo
);
if ($inscripcionExistente) {
$inscripcion = $inscripcionExistente;
$participanteAsociado = $propietarioContenido;
$this->logger->info("Inscripción existente encontrada para grupo {$grupoId}, participante ID: {$propietarioContenido->getId()}");
break; // Detener búsqueda al encontrar inscripción existente
}
}
}
// === FASE 2: DECISIÓN - Crear nueva o usar existente ===
$esNuevaInscripcion = false;
if (!$inscripcion) {
// No se encontró inscripción existente, crear nueva
if (!$primerParticipanteDisponible) {
$this->logger->warning("No se encontró ningún participante válido para inscribir en el grupo {$grupoId}");
throw new NoEncontradoException('No se encontró ningún participante válido para realizar la inscripción');
}
$participanteAsociado = $primerParticipanteDisponible;
$esNuevaInscripcion = true;
// Crear ParticipanteGrupoDetalle
$participanteGrupoDetalle = (new ParticipanteGrupoDetalle())
->setParticipante($participanteAsociado)
->setEntidadColaboradora($entidadColaboradora);
$this->participanteGrupoDetalleRepository->save($participanteGrupoDetalle);
// Crear nueva inscripción
$inscripcion = (new ParticipantesGrupo())
->setParticipanteGrupoDetalle($participanteGrupoDetalle)
->setGrupo($grupo)
->setFechaInscripcion(new \DateTime())
->setIdExterno("{$participanteGrupoDetalle->getIdExterno()}::{$grupo->getIdExterno()}");
}
// === FASE 3: ACTUALIZACIÓN - Datos comunes para nueva o existente ===
$inscripcion
->setIdFormularioRespuesta($respuesta->getId())
->setFirmaXml($firmaXmlBase64);
// Manejo del PDF (solo si no existe ya uno firmado)
if (!$inscripcion->getPdfFirmado()) {
try {
$tempFile = $this->crearArchivoTemporal($pdfContent);
$pdfFile = new UploadedFile(
$tempFile,
basename($tempFile),
'application/pdf',
null,
true
);
$inscripcion->setPdfFile($pdfFile);
} catch (FileException $e) {
throw new \RuntimeException("Error procesando PDF: " . $e->getMessage());
}
}
// === PERSISTENCIA ===
$this->participantesGrupoRepository->save($inscripcion);
$accion = $esNuevaInscripcion ? 'Creada' : 'Actualizada';
$this->logger->info("{$accion} inscripción para grupo {$grupoId}, participante ID: {$participanteAsociado->getId()}");
$this->eventDispatcher->dispatch(new InscripcionCreatedEvent($inscripcion, []));
return true;
}
/**
* Verifica si un usuario está inscrito en un grupo
*
* @param int $grupoId ID del grupo
* @return bool True si está inscrito, false en caso contrario
*/
public function verificarInscripcion($grupoId): bool
{
// Verificar que el usuario está autenticado
$usuarioHermes = $this->security->getUser();
if (!$usuarioHermes) {
return false;
}
$propietariosContenido = $usuarioHermes->getPropietarioContenidos();
if (!$propietariosContenido || count($propietariosContenido) === 0) {
return false;
}
// Obtener el grupo
$grupo = $this->grupoRepository->findById($grupoId);
if (!$grupo) {
return false;
}
if (!$grupo->getSolicitudAccionFormativa() ||
!$grupo->getSolicitudAccionFormativa()->getExpediente() ||
!$grupo->getSolicitudAccionFormativa()->getExpediente()->getExpedientePerteneAEntidad()) {
return false;
}
$entidadColaboradora = $grupo->getSolicitudAccionFormativa()
->getExpediente()
->getExpedientePerteneAEntidad()
->getEntidadColaboradora();
if (!$entidadColaboradora) {
return false;
}
// Buscar inscripciones activas
foreach ($propietariosContenido as $propietarioContenido) {
if ($propietarioContenido->getType() === 'participante') {
$inscripcion = $this->participantesGrupoRepository->findActiveByParticipanteEntidadAndGrupo(
$propietarioContenido,
$entidadColaboradora,
$grupo
);
if ($inscripcion) {
return true;
}
}
}
return false;
}
/**
* Genera un PDF de la solicitud de inscripción a curso
*
* @param FormularioRespuestas $respuesta La respuesta del formulario
* @return string|null Contenido del PDF generado o null si no se pudo generar
* @throws \Exception Si ocurre un error en la generación
*/
public function generarPdfInscripcion(FormularioRespuestas $respuesta): ?string
{
if (!$this->pdfRenderer) {
throw new \Exception('El servicio de renderizado de PDF no está disponible');
}
// Procesar la respuesta JSON para la plantilla
$jsonString = $respuesta->getJsonFormulario();
if (empty($jsonString)) {
$this->logger->warning('JSON vacío en la respuesta del formulario ID: ' . $respuesta->getId());
$json = [];
} else {
$json = json_decode($jsonString, true);
if (json_last_error() !== JSON_ERROR_NONE) {
$this->logger->error('Error al decodificar JSON: ' . json_last_error_msg() . ' en formulario ID: ' . $respuesta->getId());
$json = [];
}
}
$respuestaSJson = [];
foreach ($json['campos'] ?? $json ?? [] as $key => $value) {
$respuestaSJson[] = [
'nombre' => $value['nombre'] ?? '',
'tipo' => $value['tipo'] ?? '',
'valor' => $value['valor'] ?? '',
];
}
// Determinar si se trata de un evento o un formulario regular
$isEvent = false;
if ($respuesta->getEvento()) {
$isEvent = true;
}
// Preparar los datos para la plantilla
$templateData = [
'isEvent' => $isEvent,
'formulario' => $respuesta->getFormulario(),
'respuestaSJson' => $respuestaSJson,
'respuesta' => $respuesta,
'solicitud_categoria' => $respuesta->getFormulario()?->getCategorias()?->first(),
'mostrar_header_footer' => true, // Para incluir cabecera y pie en el PDF
];
try {
// Renderizar a PDF
return $this->pdfRenderer->__invoke(
'all/solicitud_categoria/show_pdf.html.twig',
$templateData
);
} catch (\Exception $e) {
$this->logger->error('Error al generar el PDF de inscripción: ' . $e->getMessage());
throw $e;
}
}
/**
* Firma un documento PDF de inscripción usando VALid
*
* @param string $pdfContent Contenido del PDF a firmar
* @param string $accessToken Token de acceso de VALid
* @return array Datos de la firma incluyendo el archivo firmado temporal
* @throws \Exception Si ocurre un error durante la firma
*/
public function firmarDocumentoInscripcion(string $pdfContent, string $accessToken): string
{
if (!$this->validService) {
throw new \Exception('El servicio de firma VALid no está disponible');
}
// Verificar espacio disponible en sistema de archivos
$tempDir = sys_get_temp_dir();
$freeSpace = disk_free_space($tempDir);
if ($freeSpace < strlen($pdfContent) * 2) { // Necesitamos al menos el doble del tamaño para PDF original y firmado
throw new \Exception('No hay suficiente espacio disponible para crear archivos temporales');
}
// Crear un archivo temporal para el PDF con nombre seguro
$tempFile = tempnam($tempDir, 'inscripcion_');
if ($tempFile === false) {
throw new \Exception('No se pudo crear un archivo temporal para la firma');
}
// Establecer permisos restrictivos
chmod($tempFile, 0600);
try {
// Escribir el contenido del PDF al archivo temporal
file_put_contents($tempFile, $pdfContent);
// Crear un objeto File para el documento
$pdfFile = new File($tempFile);
// Opciones de firma (pueden personalizarse según las necesidades)
$opciones = [
'motivo' => 'Inscripción a curso',
'ubicacion' => 'Conforcat',
'contactInfo' => 'Plataforma Conforcat',
];
// Llamar al servicio de firma
$resultadoFirma = $this->validService->firmarDocumento($accessToken, $pdfFile, $opciones);
// Verificar que se ha obtenido un documento firmado
$resultado = json_decode($resultadoFirma, true);
if ($resultado['status'] !== 'ok') {
throw new \Exception('No se obtuvo el documento firmado del servicio VALid');
}
return $resultado['evidence'];
} catch (\Exception $e) {
// Asegurarse de limpiar los archivos temporales incluso en caso de error
if (file_exists($tempFile)) {
unlink($tempFile);
}
// También limpiar el archivo firmado si existe
if (isset($tempFileFirmado) && file_exists($tempFileFirmado)) {
unlink($tempFileFirmado);
}
$this->logger->error('Error al firmar el documento de inscripción: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
]);
throw $e;
}
}
/**
* Obtiene la URL de autenticación VALid para el proceso de firma
*
* @param string $state Identificador único para el estado de la sesión
* @return string URL de autenticación
*/
public function obtenerUrlAutenticacionValid(string $state): string
{
if (!$this->validService) {
throw new \Exception('El servicio de firma VALid no está disponible');
}
return $this->validService->getAuthorizationUrl($state, true);
}
/**
* Obtiene un token de acceso de VALid a partir del código de autorización
*
* @param string $authorizationCode Código de autorización recibido de VALid
* @return array Información del token de acceso
*/
public function obtenerTokenValid(string $authorizationCode): array
{
if (!$this->validService) {
throw new \Exception('El servicio de firma VALid no está disponible');
}
return $this->validService->getAccessToken($authorizationCode);
}
/**
* Obtiene información del usuario de VALid
*
* @param string $accessToken Token de acceso de VALid
* @return array Información del usuario
*/
public function obtenerInfoUsuarioValid(string $accessToken): array
{
if (!$this->validService) {
throw new \Exception('El servicio de firma VALid no está disponible');
}
return $this->validService->getUserInfo($accessToken);
}
private function crearArchivoTemporal(string $contenido): string
{
$tempFileName = tempnam(sys_get_temp_dir(), 'pdf_');
if (file_put_contents($tempFileName, $contenido) === false) {
throw new \RuntimeException("No se pudo escribir en el archivo temporal");
}
return $tempFileName;
}
}