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);