examenService = $examenService; } public function procesoexamen(Request $request) { $postulante = $request->user(); $procesos = Proceso::where('activo', 1) ->whereNotExists(function ($q) use ($postulante) { $q->select(\DB::raw(1)) ->from('examenes') ->join('area_proceso', 'area_proceso.id', '=', 'examenes.area_proceso_id') ->whereColumn('area_proceso.proceso_id', 'procesos.id') ->where('examenes.postulante_id', $postulante->id); }) ->select('id', 'nombre', 'requiere_pago') ->get(); return response()->json($procesos); } public function areas(Request $request) { $procesoId = $request->query('proceso_id'); $areas = Area::select('areas.id', 'areas.nombre') ->whereHas('procesos', function ($query) use ($procesoId) { $query->where('procesos.id', $procesoId) ->where('procesos.activo', 1); }) ->with(['procesos' => function ($q) use ($procesoId) { $q->where('procesos.id', $procesoId) ->select('procesos.id') ->withPivot('id', 'proceso_id', 'area_id'); }]) ->get() ->map(function ($area) { $pivot = $area->procesos->first()->pivot; return [ 'area_id' => $area->id, 'nombre' => $area->nombre, 'area_proceso_id' => $pivot->id, ]; }); return response()->json($areas); } public function crearExamen(Request $request) { $postulante = $request->user(); $request->validate([ 'area_proceso_id' => 'required|exists:area_proceso,id', ]); // 馃敟 Obtener TODO desde el pivot $areaProceso = \DB::table('area_proceso') ->join('procesos', 'procesos.id', '=', 'area_proceso.proceso_id') ->join('areas', 'areas.id', '=', 'area_proceso.area_id') ->where('area_proceso.id', $request->area_proceso_id) ->select( 'area_proceso.id', 'area_proceso.area_id', 'area_proceso.proceso_id', 'procesos.requiere_pago' ) ->first(); if (!$areaProceso) { return response()->json(['message' => 'Relaci贸n 谩rea-proceso inv谩lida'], 400); } $yaDioProceso = Examen::join('area_proceso', 'area_proceso.id', '=', 'examenes.area_proceso_id') ->where('examenes.postulante_id', $postulante->id) ->where('area_proceso.proceso_id', $areaProceso->proceso_id) ->exists(); if ($yaDioProceso) { return response()->json([ 'message' => 'Ya rendiste un examen para este proceso' ], 400); } $pagado = 0; $pagoId = null; // 馃挵 Validaci贸n de pago if ($areaProceso->requiere_pago) { $request->validate([ 'tipo_pago' => 'required|in:pyto_peru,banco_nacion,caja', 'codigo_pago' => 'required', ]); $response = $this->validarPago( $request->tipo_pago, $request->codigo_pago, $postulante->dni ); if (!$response['estado']) { return response()->json(['message' => 'Pago inv谩lido'], 400); } $pago = Pago::firstOrCreate( [ 'codigo_pago' => $request->codigo_pago, 'tipo_pago' => $request->tipo_pago, ], [ 'postulante_id' => $postulante->id, 'monto' => $response['monto'], 'fecha_pago' => $response['fecha_pago'], ] ); if ($pago->utilizado) { return response()->json(['message' => 'Pago ya utilizado'], 400); } $pago->update(['utilizado' => true]); $pagado = 1; $pagoId = $pago->id; } // 馃 Crear / actualizar examen $examen = Examen::updateOrCreate( ['postulante_id' => $postulante->id, 'area_proceso_id' => $areaProceso->id, // 馃敟 pivot 'pagado' => $pagado, 'tipo_pago' => $request->tipo_pago ?? null, 'pago_id' => $pagoId, ] ); return response()->json([ 'success' => true, 'examen_id' => $examen->id, 'mensaje' => 'Examen creado correctamente', ]); } public function validarPago($tipoPago, $codigoPago, $dni) { return match ($tipoPago) { 'pyto_peru' => $this->validarPagoPytoPeru($codigoPago, $dni), 'banco_nacion' => $this->validarPagoBancoNacion($codigoPago, $dni), 'caja' => $this->validarPagoCaja($codigoPago, $dni), default => ['estado' => false], }; } private function validarPagoBancoNacion($secuencia, $dni) { $url = 'https://inscripciones.admision.unap.edu.pe/api/get-pagos-banco-secuencia'; $response = Http::post($url, [ 'secuencia' => $secuencia, ]); if (!$response->successful()) { \Log::error('Error en la solicitud a la API', ['status' => $response->status(), 'body' => $response->body()]); return ['estado' => false, 'message' => 'Error en la solicitud a la API']; } $data = $response->json(); if (isset($data['estado']) && $data['estado'] === true) { foreach ($data['datos'] as $pago) { if (isset($pago['dni']) && trim($pago['dni']) == trim($dni)) { return [ 'estado' => true, 'monto' => $pago['imp_pag'], 'fecha_pago' => $pago['fch_pag'], ]; } } } return ['estado' => false, 'message' => 'Datos no v谩lidos o no encontrados']; } private function validarPagoPytoPeru($authorizationCode, $dni) { $url = "https://service2.unap.edu.pe/PAYMENTS_MNG/v1/{$dni}/8/"; $response = Http::get($url); if ($response->successful()) { $data = $response->json(); if (isset($data['data'][0]['autorizationCode']) && $data['data'][0]['autorizationCode'] === $authorizationCode) { return [ 'estado' => true, 'monto' => $data['data'][0]['total'], 'fecha_pago' => $data['data'][0]['confirmedDate'], ]; } } return ['estado' => false, 'message' => 'El c贸digo de autorizaci贸n no coincide.']; } private function validarPagoCaja($codigoPago, $dni) { $url = "https://inscripciones.admision.unap.edu.pe/api/get-pago-caja/{$dni}/{$codigoPago}"; $response = Http::get($url); if (!$response->successful()) { \Log::error('Error API Caja', [ 'status' => $response->status(), 'body' => $response->body() ]); return ['estado' => false, 'message' => 'Error en la API de Caja']; } $data = $response->json(); if ( isset($data['paymentTitle'], $data['paymentAmount'], $data['paymentDatetime']) && trim($data['paymentTitle']) === trim($codigoPago) ) { return [ 'estado' => true, 'monto' => (float) $data['paymentAmount'], 'fecha_pago' => $data['paymentDatetime'], ]; } return ['estado' => false, 'message' => 'Pago no v谩lido o no encontrado']; } public function miExamenActual(Request $request) { $postulante = $request->user(); $examen = Examen::join('area_proceso', 'area_proceso.id', '=', 'examenes.area_proceso_id') ->join('areas', 'areas.id', '=', 'area_proceso.area_id') ->join('procesos', 'procesos.id', '=', 'area_proceso.proceso_id') ->where('examenes.postulante_id', $postulante->id) ->select( 'examenes.id', 'examenes.pagado', 'examenes.tipo_pago', 'examenes.intentos', 'areas.id as area_id', 'areas.nombre as area_nombre', 'procesos.id as proceso_id', 'procesos.nombre as proceso_nombre', 'procesos.intentos_maximos as proceso_intentos_maximos' ) ->latest('examenes.created_at') ->first(); if (!$examen) { return response()->json([ 'success' => true, 'mensaje' => 'No tienes ex谩menes asignados actualmente', 'examen' => null ]); } return response()->json([ 'success' => true, 'examen' => [ 'id' => $examen->id, 'intentos' => $examen->intentos, 'intentos_maximos' => $examen->proceso_intentos_maximos, 'proceso' => [ 'id' => $examen->proceso_id, 'nombre' => $examen->proceso_nombre, ], 'area' => [ 'id' => $examen->area_id, 'nombre' => $examen->area_nombre, ], 'pagado' => $examen->pagado, 'tipo_pago' => $examen->tipo_pago ?? null, ] ]); } public function generarPreguntas($examenId) { $examen = Examen::findOrFail($examenId); $postulante = request()->user(); if ($examen->postulante_id !== $postulante->id) { return response()->json([ 'success' => false, 'message' => 'No autorizado' ], 403); } if ($examen->preguntasAsignadas()->exists()) { return response()->json([ 'success' => true, 'message' => 'El examen ya tiene preguntas generadas', 'ya_tiene_preguntas' => true, 'total_preguntas' => $examen->preguntasAsignadas()->count() ]); } $resultado = $this->examenService->generarPreguntasExamen($examen); if (!$resultado['success']) { return response()->json($resultado, 400); } return response()->json([ 'success' => true, 'message' => 'Preguntas generadas exitosamente', 'ya_tiene_preguntas' => false, 'total_preguntas' => $examen->preguntasAsignadas()->count() ]); } public function iniciarExamen(Request $request) { $request->validate([ 'examen_id' => 'required|exists:examenes,id' ]); $examen = Examen::findOrFail($request->examen_id); $postulante = $request->user(); if ($examen->postulante_id !== $postulante->id) { return response()->json([ 'success' => false, 'message' => 'No autorizado' ], 403); } $areaProceso = \DB::table('area_proceso') ->join('procesos', 'area_proceso.proceso_id', '=', 'procesos.id') ->join('areas', 'area_proceso.area_id', '=', 'areas.id') ->where('area_proceso.id', $examen->area_proceso_id) ->select( 'procesos.id as proceso_id', 'procesos.nombre as proceso_nombre', 'procesos.duracion as proceso_duracion', 'procesos.intentos_maximos as proceso_intentos_maximos', 'areas.nombre as area_nombre' ) ->first(); if (!$examen->preguntasAsignadas()->exists()) { return response()->json([ 'success' => false, 'message' => 'El examen no tiene preguntas generadas' ], 400); } if ($areaProceso && $examen->intentos >= $areaProceso->proceso_intentos_maximos) { return response()->json([ 'success' => false, 'message' => 'Has alcanzado el n煤mero m谩ximo de intentos para este proceso' ], 403); } $examen->increment('intentos'); $examen->update([ 'hora_inicio' => now(), 'estado' => 'en_progreso' ]); $preguntas = $this->examenService->obtenerPreguntasExamen($examen); return response()->json([ 'success' => true, 'examen' => [ 'id' => $examen->id, 'estado' => $examen->estado, 'hora_inicio' => $examen->hora_inicio, 'intentos' => $examen->intentos, 'intentos_maximos' => $areaProceso->proceso_intentos_maximos ?? null, 'proceso' => $areaProceso->proceso_nombre ?? null, 'duracion' => $areaProceso->proceso_duracion ?? null, 'area' => $areaProceso->area_nombre ?? null ], 'preguntas' => $preguntas ]); } public function responderPregunta($preguntaAsignadaId, Request $request) { $request->validate([ 'respuesta' => 'nullable|string' ]); $preguntaAsignada = PreguntaAsignada::with(['examen', 'pregunta']) ->findOrFail($preguntaAsignadaId); $postulante = $request->user(); if ($preguntaAsignada->examen->postulante_id !== $postulante->id) { return response()->json([ 'success' => false, 'message' => 'No autorizado' ], 403); } $resultado = $this->examenService->guardarRespuesta( $preguntaAsignada, $request->respuesta ); return response()->json($resultado); } public function finalizarExamen($examenId) { $examen = Examen::findOrFail($examenId); $postulante = request()->user(); // Validar que el examen le pertenezca al postulante autenticado if ((int) $examen->postulante_id !== (int) $postulante->id) { return response()->json([ 'success' => false, 'message' => 'No autorizado' ], 403); } // (Opcional) Evitar finalizar 2 veces if ($examen->estado === 'finalizado') { return response()->json([ 'success' => false, 'message' => 'El examen ya est谩 finalizado' ], 409); } // Finalizar examen $examen->update([ 'estado' => 'finalizado', 'hora_fin' => now(), ]); return response()->json([ 'success' => true, 'message' => 'Examen finalizado exitosamente' ]); } public function calificarExamen($examenId, Request $request) { $postulante = $request->user(); if (!$postulante) { return response()->json(['success' => false, 'mensaje' => 'No autenticado.'], 401); } return DB::transaction(function () use ($examenId, $postulante) { // 1) Validar examen del postulante $examen = DB::table('examenes') ->where('id', $examenId) ->where('postulante_id', $postulante->id) ->first(); if (!$examen) { return response()->json([ 'success' => false, 'mensaje' => 'No se encontr贸 un examen para este postulante.' ], 404); } // 2) Si ya est谩 calificado, devolver existente (y opcionalmente recalcular orden) $existente = DB::table('resultados_examenes') ->where('examen_id', $examen->id) ->first(); if ($existente) { // Obtener proceso_id para poder recalcular si deseas (opcional) $procesoId = DB::table('area_proceso') ->where('id', $examen->area_proceso_id) ->value('proceso_id'); if ($procesoId) $this->recalcularOrdenMerito($procesoId); return response()->json([ 'success' => true, 'mensaje' => 'Examen ya calificado.', 'total_puntos' => (float)$existente->total_puntos, 'total_correctas' => (int)$existente->total_correctas, 'total_incorrectas' => (int)$existente->total_incorrectas, 'total_nulas' => (int)$existente->total_nulas, 'porcentaje_correctas' => (float)$existente->porcentaje_correctas, 'calificacion_sobre_20' => (float)$existente->calificacion_sobre_20, 'orden_merito' => $existente->orden_merito, 'correctas_por_curso' => json_decode($existente->correctas_por_curso, true), 'incorrectas_por_curso' => json_decode($existente->incorrectas_por_curso ?? '[]', true), 'preguntas_totales_por_curso' => json_decode($existente->preguntas_totales_por_curso ?? '[]', true), ]); } // 3) Obtener configuraci贸n de calificaci贸n desde proceso -> calificaciones $cfg = DB::table('area_proceso as ap') ->join('procesos as pr', 'pr.id', '=', 'ap.proceso_id') ->join('calificaciones as ca', 'ca.id', '=', 'pr.calificacion_id') ->where('ap.id', $examen->area_proceso_id) ->select( 'pr.id as proceso_id', 'ca.puntos_correcta', 'ca.puntos_incorrecta', 'ca.puntos_nula', 'ca.puntaje_maximo' ) ->first(); if (!$cfg) { return response()->json([ 'success' => false, 'mensaje' => 'No se ha definido un tipo de calificaci贸n para este proceso.' ], 400); } $puntosCorrecta = (float) $cfg->puntos_correcta; $puntosIncorrecta = (float) $cfg->puntos_incorrecta; $puntosNula = (float) $cfg->puntos_nula; $puntajeMaximo = (float) $cfg->puntaje_maximo; // 4) Traer preguntas asignadas con su pregunta y curso $items = DB::table('preguntas_asignadas as pa') ->join('preguntas as p', 'p.id', '=', 'pa.pregunta_id') ->join('cursos as c', 'c.id', '=', 'p.curso_id') ->where('pa.examen_id', $examen->id) ->select( 'pa.id as pa_id', 'pa.estado as pa_estado', 'pa.respuesta_usuario', 'p.respuesta_correcta', 'c.nombre as curso_nombre' ) ->get(); if ($items->isEmpty()) { return response()->json([ 'success' => false, 'mensaje' => 'El examen no tiene preguntas asignadas.' ], 422); } // 5) Calificar y actualizar cada pregunta_asignada $totalPuntos = 0.0; $totalCorrectas = 0; $totalIncorrectas = 0; $totalNulas = 0; $correctasPorCurso = []; $incorrectasPorCurso = []; $preguntasTotalesPorCurso = []; foreach ($items as $row) { $curso = $row->curso_nombre; $preguntasTotalesPorCurso[$curso] = ($preguntasTotalesPorCurso[$curso] ?? 0) + 1; $correctasPorCurso[$curso] = $correctasPorCurso[$curso] ?? 0; $incorrectasPorCurso[$curso] = $incorrectasPorCurso[$curso] ?? 0; $nuevoEsCorrecta = 2; // 2 = blanco $nuevoPuntaje = $puntosNula; // nula/blanco if ($row->pa_estado === 'anulada') { // anulada => nula $nuevoEsCorrecta = 2; $nuevoPuntaje = $puntosNula; $totalNulas++; } else { $ru = trim((string) $row->respuesta_usuario); $rc = trim((string) $row->respuesta_correcta); if ($ru === '') { // blanco $nuevoEsCorrecta = 2; $nuevoPuntaje = $puntosNula; $totalNulas++; } else { $ruN = mb_strtoupper($ru); $rcN = mb_strtoupper($rc); if ($rcN !== '' && $ruN === $rcN) { $nuevoEsCorrecta = 1; $nuevoPuntaje = $puntosCorrecta; $totalCorrectas++; $correctasPorCurso[$curso]++; } else { $nuevoEsCorrecta = 0; $nuevoPuntaje = $puntosIncorrecta; $totalIncorrectas++; $incorrectasPorCurso[$curso]++; } } } $totalPuntos += (float) $nuevoPuntaje; DB::table('preguntas_asignadas') ->where('id', $row->pa_id) ->update([ 'es_correcta' => $nuevoEsCorrecta, 'puntaje' => $nuevoPuntaje, 'updated_at' => now(), ]); } // 6) Resumen $totalPreguntas = (int) $items->count(); $porcentajeCorrectas = $totalPreguntas > 0 ? ($totalCorrectas / $totalPreguntas) * 100 : 0; $calificacionSobre20 = ($puntajeMaximo > 0) ? ($totalPuntos / $puntajeMaximo) * 20 : 0; $correctasPorCursoFormato = []; foreach ($correctasPorCurso as $curso => $corr) { $y = $preguntasTotalesPorCurso[$curso] ?? 0; $correctasPorCursoFormato[$curso] = "{$corr} de {$y}"; } // 7) Guardar resultado en resultados_examenes $resultadoId = DB::table('resultados_examenes')->insertGetId([ 'postulante_id' => $postulante->id, 'examen_id' => $examen->id, 'total_puntos' => round($totalPuntos, 3), 'correctas_por_curso' => json_encode($correctasPorCursoFormato), 'incorrectas_por_curso' => json_encode($incorrectasPorCurso), 'preguntas_totales_por_curso' => json_encode($preguntasTotalesPorCurso), 'total_correctas' => $totalCorrectas, 'total_incorrectas' => $totalIncorrectas, 'total_nulas' => $totalNulas, 'porcentaje_correctas' => round($porcentajeCorrectas, 2), 'calificacion_sobre_20' => round($calificacionSobre20, 2), 'created_at' => now(), 'updated_at' => now(), ]); // 8) Recalcular orden de m茅rito por proceso $this->recalcularOrdenMerito($cfg->proceso_id); // 9) Leer orden_merito ya asignado (opcional) $orden = DB::table('resultados_examenes')->where('id', $resultadoId)->value('orden_merito'); DB::table('examenes')->where('id', $examen->id)->update([ 'estado' => 'calificado', 'hora_fin' => now(), ]); return response()->json([ 'success' => true, 'mensaje' => 'Examen calificado exitosamente.', 'examen_id' => $examen->id, 'proceso_id' => $cfg->proceso_id, 'total_puntos' => round($totalPuntos, 2), 'total_correctas' => $totalCorrectas, 'total_incorrectas' => $totalIncorrectas, 'total_nulas' => $totalNulas, 'porcentaje_correctas' => round($porcentajeCorrectas, 2), 'calificacion_sobre_20' => round($calificacionSobre20, 2), 'orden_merito' => $orden, 'correctas_por_curso' => $correctasPorCursoFormato, 'incorrectas_por_curso' => $incorrectasPorCurso, 'preguntas_totales_por_curso' => $preguntasTotalesPorCurso, ]); }); } public function recalcularOrdenMerito($procesoId): void { DB::statement(" UPDATE resultados_examenes r JOIN ( SELECT r2.id, ROW_NUMBER() OVER ( ORDER BY r2.total_puntos DESC, COALESCE(r2.updated_at, r2.created_at) ASC ) AS nuevo_orden FROM resultados_examenes r2 JOIN examenes e ON e.id = r2.examen_id JOIN area_proceso ap ON ap.id = e.area_proceso_id WHERE ap.proceso_id = ? ) x ON x.id = r.id SET r.orden_merito = x.nuevo_orden ", [$procesoId]); } }