prepare("SELECT valor FROM configuracion WHERE clave = :clave LIMIT 1"); $stmt->execute([':clave' => $clave]); $resultado = $stmt->fetch(); return $resultado ? $resultado['valor'] : null; } // ✅ FUNCIÓN MEJORADA: Genera código aleatorio SIN verificar (la verificación se hace en el INSERT) function generarCodigoReserva($fecha, $codigo_cabina) { $fecha_limpia = str_replace('-', '', $fecha); // Usar 4 dígitos + timestamp para máxima unicidad $num_aleatorio = str_pad(random_int(1000, 9999), 4, '0', STR_PAD_LEFT); return "PETSTOP-{$fecha_limpia}-{$codigo_cabina}-{$num_aleatorio}"; } function generarReferenciaBizum() { $caracteres = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; $referencia = 'BIZUM-'; for ($i = 0; $i < 6; $i++) { $referencia .= $caracteres[random_int(0, strlen($caracteres) - 1)]; } return $referencia; } function generarCodigoQR($reserva_id) { return 'QR_' . $reserva_id . '_' . time() . '_' . bin2hex(random_bytes(8)); } // ✅ CORREGIDO: Sin FOR UPDATE, incluye 'pendiente_pago' function cabinaDisponible($pdo, $cabina_id, $fecha, $hora_inicio, $hora_fin) { $sql = "SELECT id FROM reservas WHERE cabina_id = :cabina_id AND fecha = :fecha AND estado IN ('pendiente_pago', 'pagada_pendiente_aprobacion', 'confirmada', 'activa') AND (hora_inicio < :hora_fin AND hora_fin_prevista > :hora_inicio) LIMIT 1"; $stmt = $pdo->prepare($sql); $stmt->execute([ ':cabina_id' => $cabina_id, ':fecha' => $fecha, ':hora_inicio' => $hora_inicio, ':hora_fin' => $hora_fin ]); return $stmt->fetch() === false; } try { switch ($method) { case 'GET': $usuario_id = isset($_GET['usuario_id']) ? (int)$_GET['usuario_id'] : null; if (!$usuario_id) { http_response_code(400); echo json_encode(['success' => false, 'message' => 'Falta el parámetro usuario_id']); exit(); } $sql = "SELECT r.id, r.codigo, r.fecha, r.hora_inicio, r.hora_fin_prevista, r.hora_fin_real, r.duracion_bloques, r.importe, r.estado, r.referencia_bizum, r.codigo_qr, r.qr_activo, r.operador_aprobacion, r.fecha_aprobacion, r.hora_entrada_real, r.hora_salida_real, m.nombre AS mascota_nombre, m.especie, m.raza, c.codigo AS cabina_codigo, c.fila, c.nivel FROM reservas r JOIN mascotas m ON r.mascota_id = m.id JOIN cabinas c ON r.cabina_id = c.id WHERE r.usuario_id = :usuario_id"; $params = [':usuario_id' => $usuario_id]; if (isset($_GET['estado'])) { $sql .= " AND r.estado = :estado"; $params[':estado'] = $_GET['estado']; } $sql .= " ORDER BY r.fecha DESC, r.hora_inicio DESC"; $stmt = $pdo->prepare($sql); $stmt->execute($params); $reservas = $stmt->fetchAll(); echo json_encode([ 'success' => true, 'message' => 'Reservas obtenidas correctamente', 'data' => $reservas, 'total' => count($reservas) ], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); break; case 'POST': $input = getJsonInput(); $campos_requeridos = ['usuario_id', 'mascota_id', 'cabina_id', 'fecha', 'hora_inicio', 'hora_fin_prevista']; foreach ($campos_requeridos as $campo) { if (!isset($input[$campo]) || empty($input[$campo])) { http_response_code(400); echo json_encode(['success' => false, 'message' => "Falta el campo obligatorio: $campo"]); exit(); } } $usuario_id = (int)$input['usuario_id']; $mascota_id = (int)$input['mascota_id']; $cabina_id = (int)$input['cabina_id']; $fecha = $input['fecha']; $hora_inicio = $input['hora_inicio']; $hora_fin_prevista = $input['hora_fin_prevista']; $stmtMascota = $pdo->prepare("SELECT id, peso FROM mascotas WHERE id = :id AND usuario_id = :usuario_id AND estado = 'activa'"); $stmtMascota->execute([':id' => $mascota_id, ':usuario_id' => $usuario_id]); if (!$stmtMascota->fetch()) { http_response_code(403); echo json_encode(['success' => false, 'message' => 'La mascota no existe o no te pertenece']); exit(); } $stmtCabina = $pdo->prepare("SELECT id, codigo, estado FROM cabinas WHERE id = :id"); $stmtCabina->execute([':id' => $cabina_id]); $cabina = $stmtCabina->fetch(); if (!$cabina) { http_response_code(404); echo json_encode(['success' => false, 'message' => 'La cabina no existe']); exit(); } if ($cabina['estado'] !== 'disponible') { http_response_code(400); echo json_encode(['success' => false, 'message' => 'La cabina no está disponible']); exit(); } if (!cabinaDisponible($pdo, $cabina_id, $fecha, $hora_inicio, $hora_fin_prevista)) { http_response_code(409); echo json_encode(['success' => false, 'message' => '⚠️ La cabina ya está reservada en ese horario. Por favor, elige otra.']); exit(); } $inicio = new DateTime($hora_inicio); $fin = new DateTime($hora_fin_prevista); $diferencia = $inicio->diff($fin); $minutos_totales = ($diferencia->h * 60) + $diferencia->i; if ($minutos_totales <= 0) { http_response_code(400); echo json_encode(['success' => false, 'message' => 'La hora de fin debe ser posterior a la hora de inicio']); exit(); } $duracion_bloques = ceil($minutos_totales / 30); $precio_bloque = floatval(getConfig($pdo, 'precio_bloque')); $iva = floatval(getConfig($pdo, 'iva_aplicable')); $importe = ($precio_bloque * $duracion_bloques) * (1 + $iva); $referencia_bizum = generarReferenciaBizum(); $codigo_qr_temp = 'QR_TEMP_' . time(); // ✅ REINTENTO AUTOMÁTICO: Hasta 3 intentos si hay colisión de código $max_intentos = 3; $intentos = 0; $exito = false; while ($intentos < $max_intentos && !$exito) { $intentos++; $codigo_reserva = generarCodigoReserva($fecha, $cabina['codigo']); $pdo->beginTransaction(); try { $stmtInsert = $pdo->prepare(" INSERT INTO reservas (codigo, usuario_id, mascota_id, cabina_id, fecha, hora_inicio, hora_fin_prevista, duracion_bloques, importe, estado, referencia_bizum, codigo_qr, qr_activo) VALUES (:codigo, :usuario_id, :mascota_id, :cabina_id, :fecha, :hora_inicio, :hora_fin_prevista, :duracion_bloques, :importe, 'pendiente_pago', :referencia_bizum, :codigo_qr, 0) "); $stmtInsert->execute([ ':codigo' => $codigo_reserva, ':usuario_id' => $usuario_id, ':mascota_id' => $mascota_id, ':cabina_id' => $cabina_id, ':fecha' => $fecha, ':hora_inicio' => $hora_inicio, ':hora_fin_prevista' => $hora_fin_prevista, ':duracion_bloques' => $duracion_bloques, ':importe' => $importe, ':referencia_bizum' => $referencia_bizum, ':codigo_qr' => $codigo_qr_temp ]); $nuevo_id = $pdo->lastInsertId(); $codigo_qr_final = generarCodigoQR($nuevo_id); $pdo->prepare("UPDATE reservas SET codigo_qr = :codigo_qr WHERE id = :id") ->execute([':codigo_qr' => $codigo_qr_final, ':id' => $nuevo_id]); $pdo->commit(); $exito = true; echo json_encode([ 'success' => true, 'message' => 'Reserva creada correctamente. Pendiente de pago con Bizum.', 'data' => [ 'id' => (int)$nuevo_id, 'codigo' => $codigo_reserva, 'referencia_bizum' => $referencia_bizum, 'codigo_qr' => $codigo_qr_final, 'qr_activo' => false, 'fecha' => $fecha, 'hora_inicio' => $hora_inicio, 'hora_fin_prevista' => $hora_fin_prevista, 'duracion_bloques' => $duracion_bloques, 'importe' => $importe, 'cabina_codigo' => $cabina['codigo'], 'estado' => 'pendiente_pago' ] ], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); } catch (PDOException $e) { $pdo->rollBack(); // Si es error de clave duplicada (código 23000) y quedan intentos, reintentar if ($e->getCode() == 23000 && $intentos < $max_intentos) { // Esperar un microsegundo y reintentar con nuevo código usleep(1000); continue; } else { // Error no recuperable o sin más intentos http_response_code(500); echo json_encode(['success' => false, 'message' => 'Error al guardar la reserva', 'error' => $e->getMessage()]); break; } } } // Si agotó todos los intentos if (!$exito && $intentos >= $max_intentos) { http_response_code(500); echo json_encode(['success' => false, 'message' => 'No se pudo generar un código único después de múltiples intentos. Por favor, inténtalo más tarde.']); } break; case 'DELETE': $input = getJsonInput(); if (!isset($input['id']) || !isset($input['usuario_id'])) { http_response_code(400); echo json_encode(['success' => false, 'message' => 'Faltan campos obligatorios: id y usuario_id']); exit(); } $reserva_id = (int)$input['id']; $usuario_id = (int)$input['usuario_id']; $stmtReserva = $pdo->prepare("SELECT r.id, r.cabina_id, r.estado FROM reservas r WHERE r.id = :id AND r.usuario_id = :usuario_id"); $stmtReserva->execute([':id' => $reserva_id, ':usuario_id' => $usuario_id]); $reserva = $stmtReserva->fetch(); if (!$reserva) { http_response_code(403); echo json_encode(['success' => false, 'message' => 'La reserva no existe o no te pertenece']); exit(); } if ($reserva['estado'] === 'cancelada' || $reserva['estado'] === 'finalizada') { http_response_code(400); echo json_encode(['success' => false, 'message' => 'No se puede cancelar una reserva ' . $reserva['estado']]); exit(); } $pdo->beginTransaction(); try { $pdo->prepare("UPDATE reservas SET estado = 'cancelada', qr_activo = 0 WHERE id = :id") ->execute([':id' => $reserva_id]); if ($reserva['estado'] === 'confirmada' || $reserva['estado'] === 'activa') { $pdo->prepare("UPDATE cabinas SET estado = 'disponible' WHERE id = :id") ->execute([':id' => $reserva['cabina_id']]); } $pdo->commit(); echo json_encode(['success' => true, 'message' => 'Reserva cancelada correctamente'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); } catch (Exception $e) { $pdo->rollBack(); http_response_code(500); echo json_encode(['success' => false, 'message' => 'Error al cancelar', 'error' => $e->getMessage()]); } break; default: http_response_code(405); echo json_encode(['success' => false, 'message' => 'Método no permitido. Use GET, POST o DELETE.']); break; } } catch (PDOException $e) { http_response_code(500); echo json_encode(['success' => false, 'message' => 'Error en el servidor', 'error' => $e->getMessage()]); } catch (Exception $e) { http_response_code(500); echo json_encode(['success' => false, 'message' => 'Error inesperado', 'error' => $e->getMessage()]); } ?>