addMinutes(-10)->format('Y-m-d H:i:s'); $limit_date = evavel_date_now()->addMinutes(-15)->format('Y-m-d H:i:s'); // Limpiamos los "holds" (status SELECTED) que quedaron abandonados. // // PROBLEMA HISTORICO: a veces se borraban reservas que el cliente SI llego a // completar (con email de confirmacion ya enviado) porque la reserva se // quedaba en SELECTED sin email por una condicion de carrera entre make_booking // y este cron. Para evitarlo aplicamos dos protecciones: // // 1) Solo consideramos holds REALMENTE abandonados: ademas de email NULL, // exigimos customer_id NULL y phone vacio. En cuanto make_booking persiste // los datos del cliente, la reserva deja de cumplir estas condiciones. // 2) Nunca borramos una reserva que ya tenga alguna notificacion registrada // (email/SMS/WhatsApp) en booking_notifications. Si al cliente ya se le // aviso, la reserva es real y debe quedar visible. // // Lo hacemos una a una para que cada cambio de estado quede registrado bien. $candidates = \Alexr\Models\Booking::where('status', \Alexr\Enums\BookingStatus::SELECTED) ->where('restaurant_id', $restaurantId) ->where('date_created', '<', $limit_date) ->whereIsNull('email') ->whereIsNull('customer_id') ->where('phone', '') ->get(); foreach ($candidates as $candidate) { // Proteccion: si ya se le envio cualquier notificacion al cliente, // la reserva es real -> no la borramos. $already_notified = \Alexr\Models\BookingNotification::where('booking_id', $candidate->id) ->first(); if ($already_notified) { continue; } $candidate->status = \Alexr\Enums\BookingStatus::DELETED; $candidate->save(); } } /** * Red de seguridad: restaura reservas marcadas como DELETED por error. * * Si una reserva online reciente esta en DELETED pero tiene notificaciones * enviadas con status booked/pending, es que el cliente SI completo la reserva * y algo la borro despues (p.ej. el contador del widget expirando justo cuando * make_booking estaba enviando las confirmaciones). Se le devuelve el status * que tenia al notificar, SIN reenviar ningun mensaje. * * Para no resucitar borrados legitimos hechos desde el dashboard, solo se * restaura si el borrado NO paso por el modelo: un borrado manual actualiza * date_modified despues de las notificaciones, mientras que el borrado erroneo * (UPDATE directo por uuid) deja date_modified congelado en el save de * make_booking, a segundos de la ultima notificacion. */ function alexr_restore_wrongly_deleted_bookings($restaurantId) { $limit_date = evavel_date_now()->addMinutes(-30)->format('Y-m-d H:i:s'); $deleted_bookings = \Alexr\Models\Booking::where('status', \Alexr\Enums\BookingStatus::DELETED) ->where('restaurant_id', $restaurantId) ->where('type', \Alexr\Enums\BookingType::ONLINE) ->where('date_created', '>', $limit_date) ->get(); foreach ($deleted_bookings as $booking) { // Los registros historicos de modificaciones se guardan como DELETED a proposito if ($booking->parent_booking_id) { continue; } $notifications = \Alexr\Models\BookingNotification::where('booking_id', $booking->id) ->orderBy('id', 'DESC') ->get(); if (count($notifications) == 0) { continue; } // Status que tenia la reserva cuando se notifico al cliente $restore_status = null; $last_notification_date = null; foreach ($notifications as $notification) { $payload = $notification->payload; if ($last_notification_date === null) { $last_notification_date = $notification->date_created; } if (is_array($payload) && isset($payload['status']) && in_array($payload['status'], [\Alexr\Enums\BookingStatus::BOOKED, \Alexr\Enums\BookingStatus::PENDING])) { $restore_status = $payload['status']; break; } } if (!$restore_status) { continue; } // Borrado manual desde el dashboard => date_modified posterior a las // notificaciones => no tocar. Margen de 120s para los saves que hace // make_booking justo despues de notificar (tracking, missed call...). $threshold = evavel_new_date($last_notification_date)->addSeconds(120)->format('Y-m-d H:i:s'); if ($booking->date_modified > $threshold) { continue; } $booking->status = $restore_status; $booking->save(); evavel_log_to_file( \Evavel\Log\Log::CRON_RESTORE_DELETED_BOOKING, 'booking_id='.$booking->id.' uuid='.$booking->uuid.' restored_to='.$restore_status ); } } function alexr_remove_pending_payment_bookings($restaurantId) { // I give 2 hours to the user to make the payment //$limit_date = \Carbon\Carbon::now()->addMinutes(-240)->format('Y-m-d H:i:s'); $limit_date = evavel_date_now()->addMinutes(-240)->format('Y-m-d H:i:s'); \Alexr\Models\Booking::where('status', \Alexr\Enums\BookingStatus::PENDING_PAYMENT) ->where('restaurant_id', $restaurantId) ->where('date_created', '<', $limit_date) ->update(['status' => \Alexr\Enums\BookingStatus::DELETED]); //->delete(); } function alexr_remove_permanently_deleted_bookings($restaurantId) { // @TODO - por ahora no voy a borrar reservas por si acaso // mejor que se borren luego manualmente return; // 1 year old deleted bookings so can be debugged //$limit_date = \Carbon\Carbon::now()->addDays(-365)->format('Y-m-d H:i:s'); $limit_date = evavel_date_now()->addDays(-365)->format('Y-m-d H:i:s'); \Alexr\Models\Booking::where('status', \Alexr\Enums\BookingStatus::DELETED) ->where('restaurant_id', $restaurantId) ->where('date_created', '<', $limit_date) ->delete(); } function alexr_delete_bookings_where_customer_null() { \Alexr\Models\Booking::whereIsNull('customer_id')->delete(); } function alexr_create_new_customer(\Alexr\Models\Booking $booking) { $customer = new Customer(); $customer->restaurant_id = $booking->restaurant_id; $customer->email = $booking->email; $customer->first_name = $booking->first_name; $customer->last_name = $booking->last_name; $customer->name = $booking->first_name.' '.$booking->last_name; $customer->phone = $booking->phone; $customer->country_code = $booking->country_code; $customer->dial_code_country = $booking->dial_code_country; $customer->dial_code = $booking->dial_code; $customer->language = $booking->language; $customer->birthday = $booking->birthday; $customer->save(); return $customer; } function alexr_transform_textarea_to_new_lines($text) { $text = str_replace('
', "\r\n", $text); $text = str_replace('
', "\r\n", $text); if ($text == null || $text == 'null') return ''; return $text; } function alexr_transform_new_lines_to_br($text) { if ($text == null) return $text; return nl2br($text); } function alexr_convertArrayToBase64(&$arr) { foreach($arr as $key => $value) { if (is_array($value)) { $arr[$key] = alexr_convertArrayToBase64($value); } else if (is_string($value)) { $arr[$key] = base64_encode($value); } } return $arr; } function cplus_application_config($key, $default = false) { return alexr_config($key, $default = false); } function alexr_config($key, $default = false) { static $config_files = []; $keys = explode('.', $key); if (!is_array($keys)) return $default; // Cache the file $file_name = $keys[0].'.php'; if (!isset($config_files[$file_name])) { $config_files[$file_name] = include ALEXR_DIR_CONFIG_FILES.$file_name; } $arr = $config_files[$file_name]; for ($i = 1; $i < count($keys); $i++){ $arr = $arr[$keys[$i]]; } return $arr; } function alexr_config_tags($restaurant_id, $type = 'customers') { $restaurant = \Alexr\Models\Restaurant::find($restaurant_id); $language = $restaurant ? $restaurant->language : 'en'; $tags = alexr_config('tags.' . $language . '.' . $type); // Default to english if (!$tags) { $tags = alexr_config('tags.en.' . $type); } return $tags; } function alexr_view_booking_url($booking) { return ALEXR_SITE_URL.'?'.ALEXR_GET_VIEW_BOOKING.'='.$booking->uuid; } // Public voucher page (?myvoucher={uuid}) — purchase confirmation + gateway // return target for the Gift Vouchers flow (addon_gift_cards). function alexr_view_voucher_url($uuid) { return ALEXR_SITE_URL.'?'.ALEXR_GET_VIEW_VOUCHER.'='.$uuid; } // HARD server-side gate for the Gift Vouchers addon: selling / paying / redeeming // a voucher requires `addon_gift_cards` to be an active paid addon of the tenant. // (The dashboard can still configure products in DEMO mode without it.) // Accepts a Restaurant model or a restaurant id. function alexr_gift_cards_enabled($restaurant) { if (!is_object($restaurant)) { $restaurant = \Alexr\Models\Restaurant::find(intval($restaurant)); } if (!$restaurant || !method_exists($restaurant, 'getPaidAddons')) { return false; } $addons = $restaurant->getPaidAddons(); return is_array($addons) && in_array('addon_gift_cards', $addons, true); } function alexr_base64_decode($base64_string) { $buffer = ''; $length = strlen($base64_string); $position = 0; $chunk_size = 4096; //8192; // Ajusta este valor según sea necesario $output = ''; while ($position < $length) { $chunk = substr($base64_string, $position, $chunk_size); $buffer .= $chunk; $position += $chunk_size; // Asegurarse de que el buffer tenga un múltiplo de 4 caracteres $buffer_length = strlen($buffer); $remainder = $buffer_length % 4; if ($remainder > 0) { $padding = 4 - $remainder; $buffer .= str_repeat('=', $padding); $buffer_length += $padding; } // Decodificar el buffer $decoded = base64_decode($buffer, true); if ($decoded !== false) { $output .= $decoded; $buffer = ''; } else { // Si la decodificación falla, retroceder un poco $buffer = substr($buffer, -3); } } // Decodificar cualquier residuo en el buffer if ($buffer !== '') { $decoded = base64_decode($buffer, true); if ($decoded !== false) { $output .= $decoded; } } return $output; } // Ejemplo de uso: // $decodedContent = base64_decode_large($largeBase64String);