is_hold_with_table = true; $this->selected_table_id = intval($_REQUEST['tableId']); $this->selected_table = \Alexr\Models\Table::find($this->selected_table_id)->toArray(); $this->selected_area_id = intval($_REQUEST['areaId']); $this->selected_price = intval($_REQUEST['price']); //ray('TABLE SELECTED: ', $this->selected_table); $this->hold_booking(); } /** * Create a new booking with status SELECTED or modify an existing one * Called when a user selects a time slot before entering their details * * @return void */ public function hold_booking() { //ray('HOLD BOOKING'); $restaurant_id = sanitize_text_field($_REQUEST['restaurantId']); // If backend then is configuring the widget $isBackend = sanitize_text_field($_REQUEST['isBackend']); if ($isBackend == 'yes') { wp_send_json_success([ 'uuid' => '12345678', 'seconds' => 300 ]); } if (!$this->verify_nonce($restaurant_id)) { wp_send_json_error(['error' => __eva('Cannot hold the booking. Invalid Nonce.')]); } // Basic request validation and data extraction $data = $this->extractHoldBookingData(); // Check restaurant and service $result = $this->validateRestaurantAndService($data); if ($result !== true) { return $result; // Error already sent } // Validate date and time $result = $this->validateDateAndTime($data); if ($result !== true) { return $result; // Error already sent } // Check availability and get tables ++++++++++++++++++++++++ if($data['service']->availability_type === 'tables_layouts') { $tables = [ $this->selected_table ]; } else { $tables = $this->checkAvailabilityForHold($data); // NO cambiar esta condicion, tiene que ser exactamente === false if ($tables === false) { return false; // Error already sent } } // Create booking $booking = \Alexr\Models\Booking::create([ 'restaurant_id' => $data['restaurant']->id, 'status' => \Alexr\Enums\BookingStatus::SELECTED, 'date' => $data['date'], 'party' => $data['guests'], 'children' => $data['children'], 'babies' => $data['babies'], 'time' => $data['time'], 'widget_id' => $data['widgetId'], 'shift_event_id' => $data['serviceId'], 'shift_event_name' => $data['service_name'], 'first_name' => 'Unknown', 'last_name' => 'Unknown', 'phone' => '', 'type' => \Alexr\Enums\BookingType::ONLINE ]); $booking->save(); // If this is a modification, store the reference to the original booking if ($data['modify_booking_uuid']) { $booking->setMeta('modify_booking_uuid', $data['modify_booking_uuid']); } if ($data['service']->availability_type === 'tables_schedule' && $tables) { $booking->duration = $this->calculateBookingDuration( $tables, $data['service'], $data['date'], $data['time'], $data['guests'] ); $booking->save(); // Si hay más de una mesa, podría ser una combinación /* if (count($tables) > 1) { // Intentar calcular duración como combinación $tableIds = array_map(function($table) { return $table['id']; }, $tables); $booking->duration = $this->calculateDurationForTablesCombination( $tableIds, $data['service'], $data['date'], $data['time'], $data['guests'], $duration_type ?? 'end_of_slot' ); //ray('Duration para combination: ' . $booking->duration/3600); } else { // Se extiende hasta la siguiente mesa o no // end_of_slot = hasta el final del schedule // next_slot = hasta la siguiente reserva // end_of_slot, next_slot $duration_type = $data['service']->list_of_tables_with_schedule['durationType'] ?? 'end_of_slot'; // Si es solo una mesa, determinar duración según su schedule $tableId = $tables[0]['id']; $booking->duration = $this->calculateDurationForTableSchedule( $tableId, $data['service'], $data['date'], $data['time'], $data['guests'], $duration_type ); } */ $booking->save(); } else { // Para otros tipos de disponibilidad, usar duración estándar $booking->duration = $data['service']->getDuration($data['guests']); $booking->save(); } // Assign tables if needed $this->assignTablesToHoldBooking($booking, $data, $tables); // Prepare and send response $this->sendHoldBookingResponse($booking, $data, $tables); } /** * Extract and validate booking data from request * * @return array Booking data */ protected function extractHoldBookingData() { $data = [ 'guests' => intval($_REQUEST['guests']), 'children' => isset($_REQUEST['children']) ? max(0, intval($_REQUEST['children'])) : 0, 'babies' => isset($_REQUEST['babies']) ? max(0, intval($_REQUEST['babies'])) : 0, 'date' => sanitize_text_field($_REQUEST['date']), 'time' => intval($_REQUEST['time']), 'serviceId' => intval($_REQUEST['serviceId']), 'widgetId' => intval($_REQUEST['widgetId']), 'restaurant_id' => sanitize_text_field($_REQUEST['restaurantId']), 'exclude_booking_id' => null, 'modify_booking_uuid' => null ]; // Check if this is a modification of an existing booking if (isset($_REQUEST['modify_booking_uuid'])) { $data['modify_booking_uuid'] = sanitize_text_field($_REQUEST['modify_booking_uuid']); $booking_to_modify = \Alexr\Models\Booking::where('uuid', $data['modify_booking_uuid']) ->where('status', '!=', \Alexr\Enums\BookingStatus::DELETED) ->first(); if ($booking_to_modify) { $data['exclude_booking_id'] = $booking_to_modify->id; } } return $data; } /** * Validate restaurant and service * * @param array &$data Booking data (passed by reference) * @return bool|void True on success, void on error (sends JSON error) */ protected function validateRestaurantAndService(&$data) { // Check restaurant $restaurant = \Alexr\Models\Restaurant::where('active', 1) ->where('id', $data['restaurant_id']) ->first(); if (!$restaurant) { wp_send_json_error(['error' => __eva('Restaurant does not exist.')]); return false; } // Check service exists $service = alexr_get_service($data['serviceId']); if (!$service) { wp_send_json_error(['error' => __eva('Service does not exist.')]); return false; } // Store in data for use in other methods $data['restaurant'] = $restaurant; $data['service'] = $service; $data['service_name'] = $service->name; $data['is_service_blocking_tables'] = $service->isBlockingTables(); return true; } /** * Validate date and time * * @param array &$data Booking data (passed by reference) * @return bool|void True on success, void on error (sends JSON error) */ protected function validateDateAndTime(&$data) { $restaurant = $data['restaurant']; $service = $data['service']; $date = $data['date']; $time = $data['time']; // Check with closed days if ($restaurant->isDateClosed($date)) { wp_send_json_error(['error' => __eva('Sorry, this date is closed for reservations.')]); return false; } // Check with shifts closed if ($service->isSlotClosed($date, $time)) { wp_send_json_error(['error' => __eva('Sorry, this slot is already closed.')]); return false; } return true; } /** * Check availability and get tables * * @param array &$data Booking data (passed by reference) * @return array|bool Tables array on success, false on error (sends JSON error) */ protected function checkAvailabilityForHold(&$data) { $service = $data['service']; $date = $data['date']; $time = $data['time']; $guests = $data['guests']; $exclude_booking_id = $data['exclude_booking_id']; // Todavia no lo he probado, pero para obtener la combinacion habria que hacer esto //$availability_type = $data['service']->availability_type; //$return_as_combination = $availability_type == 'tables_schedule'; //$isAvailable = $service->isAvailable($date, $time, $guests, $exclude_booking_id, $return_as_combination); // Check availability, return the tables available // Pass exclude_booking_id to not count the current booking being modified $isAvailable = $service->isAvailable($date, $time, $guests, $exclude_booking_id); /*ray([ 'service: ' => $service->name . ' - ' . $service->availability_type , 'date: ' => $date, 'time: ' => $time, 'guests: ' => $guests, 'exclude_booking_id: ' => $exclude_booking_id, 'isAvailable: ' => $isAvailable ]);*/ if (!$isAvailable) { wp_send_json_error(['error' => __eva('Sorry, this slot is not available any more.')]); return false; } // Return tables if available $tables = null; if (is_array($isAvailable) && !empty($isAvailable)) { $tables = $isAvailable; } return $tables; } /** * Assign tables to a hold booking if needed * * @param \Alexr\Models\Booking $booking Booking object * @param array $data Booking data * @param array|null $tables Tables to assign */ protected function assignTablesToHoldBooking($booking, $data, $tables) { if ($tables && ($data['is_service_blocking_tables'] || $data['service']->availability_type == 'tables_layouts' ) ) { $tables_id = array_map(function($table) { return $table['id']; }, $tables); $booking->tables()->sync($tables_id); $booking->save(); } // Assign tables if the service blocks tables and tables are available /*if ($data['is_service_blocking_tables'] && $tables) { $tables_id = array_map(function($table) { return $table['id']; }, $tables); $booking->tables()->sync($tables_id); $booking->save(); }*/ } /** * Prepare and send the hold booking response * * @param \Alexr\Models\Booking $booking Booking object * @param array $data Booking data */ protected function sendHoldBookingResponse($booking, $data, $tables = null) { $service = $data['service']; $widgetId = $data['widgetId']; $date = $data['date']; $time = $data['time']; $guests = $data['guests']; $can_select_area = 'no'; $max_covers_per_area = null; if (in_array($service->availability_type, ['tables', 'specific_tables', 'tables_schedule'])) { $can_select_area = $service->can_select_area; } else if (in_array($service->availability_type, ['volume_total'])) { $can_select_area = $service->covers_can_select_area; if ($can_select_area == 'yes_area' || $can_select_area == 'yes_panorama_area') { $max_covers_per_area = $service->getMaxCoversPerArea($date, $time, $guests); } } $area_is_required = $service->covers_area_is_required == 1 ? true : false; $show_time_counter = $service->isBlockingTables(); // Ya ha seleccionado la mesa con el CUSTOM LAYOUT if ($this->is_hold_with_table) { $can_select_area = 'no'; $area_is_required = false; $show_time_counter = true; } // Generar mensaje informativo sobre horario si es necesario $topWarningMessage = $this->generateScheduleMessage($booking, $service, $tables); //ray('PRECIO A PAGAR: ' . $this->selected_price); // Detect offer rules that defer payment calculation to the menu step // (e.g. `percentage_of_menu`). The widget uses this flag to know // that even if `payment` is `false` here, it must call the recompute // endpoint after the customer picks the menu. $payment_calculation_required = false; if (method_exists($service, 'getOfferRuleMatched')) { $offer_rule = $service->getOfferRuleMatched($date, $time); if ($offer_rule !== null) { $required = isset($offer_rule['payment_required']) ? $offer_rule['payment_required'] : 'no'; $type = isset($offer_rule['payment_type']) ? $offer_rule['payment_type'] : 'fixed'; $position = isset($offer_rule['display_position']) ? $offer_rule['display_position'] : 'before_form'; // Only the in-booking percentage_of_menu flow needs a mid-booking // recompute. The after_reservation flow defers payment to the // booking-view menu submit, so never flag it here. if (($required === 'yes' || $required === 'yes_save') && $type === 'percentage_of_menu' && $position !== 'after_reservation') { $payment_calculation_required = true; } } } //ray('area_is_required:' . $area_is_required ? 'SI': 'NO'); wp_send_json_success([ 'uuid' => $booking->uuid, 'seconds' => 300, // 5 minutos para pagar 'payment' => $this->gatewayPaymentDetails($service, $booking, $this->selected_price), 'payment_calculation_required' => $payment_calculation_required, 'modify_booking_uuid' => $data['modify_booking_uuid'], // Pass this back to the client 'availability_type' => $service->availability_type, 'can_select_area' => $can_select_area, 'area_is_required' => $area_is_required, // For volume_total 'max_covers_per_area' => $max_covers_per_area, 'show_service_duration' => $service->showServiceDurationInWidget($widgetId), // Show time counter 'show_time_counter' => $show_time_counter, 'topWarningMessage' => $topWarningMessage ]); } /** * Genera un mensaje informativo sobre el horario de la reserva con tables_schedule * * @param \Alexr\Models\Booking $booking Objeto de reserva * @param object $service Objeto de servicio * @return string|null Mensaje formateado o null si no aplica */ protected function generateScheduleMessage($booking, $service, $tables) { //ray('generateScheduleMessage', $service, $tables); // Si no es tables_schedule, no generamos mensaje if ($service->availability_type != 'tables_schedule') { return false; } $time = $booking->time; $date = $booking->date; $duration = $booking->duration; $endTime = $time + $duration; // Verificar si la mesa tiene un schedule que limita su tiempo // Con Last schedule no se dice nada sobre el limite de tiempo $isLastSchedule = true; // Cojemos la primera mesa y comprobamos los schedules if ($tables && count($tables) >= 1) { //ray($booking->tablesList); $tableId = $tables[0]['id']; $dayOfWeek = strtolower(date('D', strtotime($date))); if (isset($service->list_of_tables_with_schedule['tableSchedules'][$tableId][$dayOfWeek])) { $tableSchedule = $service->list_of_tables_with_schedule['tableSchedules'][$tableId][$dayOfWeek]; if ($tableSchedule['type'] === 'custom' && isset($tableSchedule['schedule'])) { // Ordenar schedules por hora de inicio usort($tableSchedule['schedule'], function($a, $b) { return $a['from'] - $b['from']; }); // Encontrar el schedule actual $currentScheduleIndex = -1; foreach ($tableSchedule['schedule'] as $index => $schedule) { if (in_array($time, $schedule['reserve'])) { $currentScheduleIndex = $index; break; } } // Verificar si es el último schedule del día $isLastSchedule = ($currentScheduleIndex === count($tableSchedule['schedule']) - 1); } } } $startTimeFormatted = evavel_seconds_to_Hm($time); $endTimeFormatted = evavel_seconds_to_Hm($endTime); $durationFormatted = evavel_seconds_to_duration($duration); return [ 'isLastSchedule' => $isLastSchedule, 'message' => __eva("Your table is reserved from {start_time} to {end_time} (total time: {duration_time})"), 'start_time' => $startTimeFormatted, 'end_time' => $endTimeFormatted, 'duration_time' => $durationFormatted, 'duration_seconds' => $duration ]; } /** * Remove holded booking * * @return void */ public function hold_booking_remove() { $isBackend = sanitize_text_field($_REQUEST['isBackend']); if ($isBackend == 'yes') { wp_send_json_success(); } $restaurant_id = intval($_REQUEST['restaurantId']); if (!$this->verify_nonce($restaurant_id)) { wp_send_json_error(['error' => __eva('Forbidden to remove the booking.')]); } $booking_uuid = sanitize_text_field($_REQUEST['uuid']); if ($booking_uuid) { // Solo se puede borrar un hold que siga en status SELECTED. // // CONDICION DE CARRERA: el contador de 300s del widget puede expirar // mientras la peticion make_booking todavia esta en vuelo (el servidor // tarda varios segundos enviando email/SMS/WhatsApp). En ese caso este // endpoint llegaba justo despues de completarse la reserva y la marcaba // como DELETED aun con las notificaciones ya enviadas. Al filtrar por // SELECTED, si make_booking ya cambio el status (pending/booked/...) // este UPDATE no afecta a ninguna fila y la reserva queda intacta. \Alexr\Models\Booking::where('uuid', $booking_uuid) ->where('status', \Alexr\Enums\BookingStatus::SELECTED) ->update(['status' => \Alexr\Enums\BookingStatus::DELETED]); } wp_send_json_success(); } }