mailManager == null) { $this->mailManager = new MailManager($this->restaurant_id); } return $this->mailManager; } protected function getEmailLinksColor() { $color = $this->getMailManager()->config->email_link_color; return $color ? $color : '#0ca5e9'; } protected function getEmailButtonBackgroundColor() { $color = $this->getMailManager()->config->email_button_bg_color; return $color ? $color : 'black'; } protected function getEmailButtonTextColor() { $color = $this->getMailManager()->config->email_button_text_color; return $color ? $color : 'white'; } public function parseEmailTags($message, $force_lang = null) { $language = $this->language; if (!$language) { $language = $this->restaurant->language; } if ($force_lang != null) { $language = $force_lang; } // Date formatted $date_format = $this->getDateFormat(); //$date_formatted = Carbon::createFromFormat('Y-m-d', $this->date) // ->locale($language) // ->translatedFormat($date_format); $date_formatted = evavel_date_createFromFormatTranslate('Y-m-d', $this->date, $language, $date_format); $day_formatted = evavel_day_translate($this->date, $language); // Current date formatted //$current_date_formatted = evavel_now_timezone($this->restaurant->timezone) // ->locale($language) // ->translatedFormat($date_format); $current_date_formatted = evavel_date_translate( evavel_now_timezone_formatted($this->restaurant->timezone, $date_format) , $language ); $restaurant_link = $this->restaurant->link_web; $restaurant_facebook = $this->restaurant->link_facebook; $restaurant_instagram = $this->restaurant->link_instagram; $mybooking_link = evavel_view_booking_url($this->uuid); $site_link = evavel_site_url(); $update_link = evavel_edit_booking_url($this->uuid); $payment_amount = ''; $payment_receipt = ''; if (defined('ALEXR_PRO_VERSION') && $this->payment) { $payment_amount = $this->paidAmountFormatted; if ($payment_amount != '') { // Fetching receipt link takes time to call the stripe API if (preg_match('#\{payment_receipt\}#', $message)){ $payment_link = $this->paymentReceiptLink; if ($payment_link) { $payment_receipt = "".__eva('Receipt').""; } } } } $style_color = 'style="color: '.$this->getEmailLinksColor().'"'; $tags = [ '{restaurant}' => $this->restaurant->name, '{restaurant_phone}' => $this->restaurant->dial_code.' '.$this->restaurant->phone, '{name}' => $this->name, '{first_name}' => $this->first_name, '{last_name}' => $this->last_name, '{email}' => $this->email, '{phone}' => '('.$this->dial_code . ')' . $this->phone, '{service}' => $this->shift_event_name, '{party}' => $this->party, '{time}' => $this->getTimeFormatted(), '{end_time}' => $this->getEndTimeFormatted(), '{duration}' => $this->toDuration($this->duration), '{date}' => $date_formatted, '{day}' => $day_formatted, '{current_date}'=> $current_date_formatted, '{message}' => $this->notes, '{tags}' => $this->getTagsNames(), '{area_table}' => $this->getAreaTable(), '{area_table_assigned}' => $this->getAreaTableAssigned(), '{country}' => Countries::country($this->country_code), '{custom_fields}' => $this->customFieldsEmailHtml(), '{menu}' => $this->getMenuHtml(), '{mybooking}' => $this->parseButtonBookingLink($mybooking_link, $language), '{mybooking_link}' => ''.$mybooking_link.'', '{site_link}' => ''.$site_link.'', '{restaurant_link}' => ''.$restaurant_link.'', '{restaurant_facebook}' => ''.$restaurant_facebook.'', '{restaurant_instagram}' => ''.$restaurant_instagram.'', '{note_from_us}' => $this->restaurant->note_from_us, '{reservation_policy}' => $this->restaurant->reservation_policy, '{reservation_number}' => $this->uuid, '{add_to_calendar}' => $this->getAddToCalendar(true), '{add_to_calendar_text}' => $this->getAddToCalendar(false), '{social}' => $this->getSocialLinks(true), '{social_text}' => $this->getSocialLinks(false), '{payment_amount}' => $payment_amount, '{payment_receipt}' => $payment_receipt, '{update_button}' => ''.alexr_translate_x('Update reservation', $this->restaurant->language).'', '{number_reservations}' => $this->get_metrics(), '{booking_modified}' => $this->get_booking_modified_tag(), '{cancel_reason}' => $this->cancelReason ]; $tags = $this->filter_pro($tags); foreach ($tags as $tag => $replace) { if ($replace == null) $replace = ''; $message = str_replace($tag, $replace, $message); } if (defined('ALEXR_PRO_VERSION')) { $message = $this->filter_payment_section($message); } return $message; } protected function get_booking_modified_tag() { $modified_bookings = Booking::where('parent_booking_id', $this->id) ->where('status', '=', BookingStatus::DELETED) ->orderBy('id', 'ASC') ->get(); $text = "
"; foreach($modified_bookings as $booking) { $text .= "
".$booking->party.' - '.$booking->date.' - '.$this->toHour($booking->time).' ('.$booking->date_created.')
'; } $text .= "
"; return $text; } protected function parseButtonBookingLink($booking_link, $language = null) { if ($language == null) { $language = $this->language; } $style = "background-color: " . $this->getEmailButtonBackgroundColor() . '; color: ' . $this->getEmailButtonTextColor() . ';'; ob_start(); ?> '.$booking_link.''; } protected function get_metrics() { // General Metrics for the booking date $date = $this->date; $count_date_pending = Booking::where('date', $date) ->where('uuid', '!=', $this->uuid) ->where('status', BookingStatus::PENDING) ->get(); $count_date_booked = Booking::where('date', $date) ->where('uuid', '!=', $this->uuid) ->where('status', BookingStatus::BOOKED) ->get(); $seatings_pending = 0; foreach($count_date_pending as $item){ $seatings_pending += intval($item->party); } $seatings_confirmed = 0; foreach($count_date_booked as $item){ $seatings_confirmed += intval($item->party); } $message = $seatings_confirmed.' '.__eva('seatings Confirmed').'
'.$seatings_pending.' '.__eva('seatings Pending'); return $message; } protected function filter_pro($list) { if (defined('ALEXR_PRO_VERSION')) { return $list; } $pro = ['message', 'tags', 'note_from_us', 'reservation_policy', 'add_to_calendar', 'add_to_calendar_text', 'social', 'social_text', 'custom_fields']; foreach ($pro as $key) { unset($list['{'.$key.'}']); } return $list; } protected function filter_payment_section($message) { $paid_amount = $this->paidAmount; // If has payment then just remove section tags and leac the content if ($paid_amount > 0) { $message = str_replace('[payment]', '', $message); $message = str_replace('[/payment]', '', $message); return $message; } // If no payment I have to remove the full section else { $message = preg_replace('#\[payment\].+\[\/payment\]#', '', $message); } return $message; } protected function getTagsNames() { $tags_names = ''; $btags = $this->tags; if ($btags) { // Filter tags that are public only $btags = $btags->filter(function($tag){ return $tag->group->is_private != 1 ; }); $btags = $btags->map(function($tag) { return $tag->name; })->toArray(); $tags_names = implode(', ', $btags); } return $tags_names; } protected function getAreaTable() { $text = ''; if ($this->area_selected_id != null) { $text .= $this->areaSelectedName; } if ($this->table_selected_id != null) { $text .= ' ('.$this->tableSelectedName.')'; } return $text; } protected function getAreaTableAssigned() { $list = $this->tablesList; if (empty($list)) { return '-'; } $names = Booking::tablesNamesWithAreaArray($list); return join(', ', $names); } /** * Renders the customer's selected offer menu as an HTML block for the * {menu} email tag. Reads $this->settings['offer_menu'] — the same blob the * booking view and Kitchen Prep use. Returns an empty string when the * booking has no menu selection, so the tag simply disappears. */ protected function getMenuHtml() { $settings = is_array($this->settings) ? $this->settings : []; if (empty($settings['offer_menu']) || !is_array($settings['offer_menu'])) { return ''; } $offer_menu = $settings['offer_menu']; $lines = (isset($offer_menu['lines']) && is_array($offer_menu['lines'])) ? $offer_menu['lines'] : []; if (empty($lines)) { return ''; } $language = $this->language ? $this->language : $this->restaurant->language; $symbol = \Alexr\Enums\CurrencyType::symbolFor($this->restaurant->currency); $rows = ''; $total = 0; foreach ($lines as $line) { $qty = isset($line['quantity']) ? intval($line['quantity']) : 0; if ($qty <= 0) continue; $name = $this->resolveMenuItemName(isset($line['name']) ? $line['name'] : '', $language); if ($name === '') $name = '-'; $subtotal = isset($line['subtotal']) ? floatval($line['subtotal']) : 0; $total += $subtotal; $price_cell = $subtotal > 0 ? ($symbol . $this->formatMenuPrice($subtotal)) : ''; $rows .= '' . '' . '' . $qty . '× ' . esc_html($name) . '' . '' . $price_cell . '' . ''; } if ($rows === '') return ''; $total_row = ''; if ($total > 0) { $total_row = '' . '' . alexr_translate_x('Total', $language) . '' . '' . $symbol . $this->formatMenuPrice($total) . '' . ''; } return '' . $rows . $total_row . '
'; } /** * Item names in a stored menu line come from the offer builder and may be a * multilingual object { en: '...', es: '...' } or a plain string. Resolve to * the booking language, then English, then the first non-empty value. */ protected function resolveMenuItemName($name, $language) { if (is_array($name)) { if (isset($name[$language]) && $name[$language] !== '') return $name[$language]; if (isset($name['en']) && $name['en'] !== '') return $name['en']; foreach ($name as $value) { if ($value !== '' && $value !== null) return $value; } return ''; } return (string) $name; } protected function formatMenuPrice($amount) { $formatted = number_format((float) $amount, 2); return preg_replace('/\.00$/', '', $formatted); } public function getTimeFormatted() { return $this->toHour($this->time, $this->restaurant->time_format); } public function getEndTimeFormatted() { $end_time = intval($this->time) + intval($this->duration); return $this->toHour($end_time, $this->restaurant->time_format); } public function getDateFormat() { $date_format = $this->restaurant->date_format; if ($date_format == 'locale') { $date_format = 'l j F Y'; } else { $date_formats = alexr_config('app.date_formats_carbon'); $date_format = $date_formats[ $date_format ] ?? 'l j F Y'; } return $date_format; } protected function getAddToCalendar($use_logos = false) { $calendar_links = $this->getCalendarLinks(); if ($use_logos) { $style = "margin: 0px 20px; display: inline-block;"; $logos = [ 'apple' => '', 'google' => '', 'outlook' => '', ]; } else { $style = "color: " . $this->getEmailLinksColor() . "; margin: 0px 20px; text-decoration: none; display: inline-block;"; $logos = [ 'apple' => 'Apple', 'google' => 'Google', 'outlook' => 'Outlook' ]; } ob_start(); ?>
$link) { echo ''.$logos[$key].''; } ?>
restaurant->link_facebook; $link_instagram = $this->restaurant->link_instagram; $facebook_logo = 'facebook'; $instagram_logo = 'instagram'; if ($use_logos) { $facebook_logo = ''; $instagram_logo = ''; } ob_start(); echo '
'; echo ''.$facebook_logo.''; echo ''.$instagram_logo.''; echo '
'; return ob_get_clean(); } // $type es para la notificacion protected function sendEmailTemplate($template = 'booking_pending', $lang = null, $type = null, $type_id = null) { if (!$type) { $type = 'email'; } if (!$lang) { $lang = $this->restaurant->language; } $is_admin_email = preg_match('#_admin#', $template); // Usar un custom template? $custom_template = $is_admin_email ? null : $this->getTemplateWithTimeConstraints($template, $lang); if (is_array($custom_template)) { $subject = $custom_template['subject']; $content = $custom_template['content']; } else { $emailTemplate = EmailTemplate::where('restaurant_id', $this->restaurant_id)->first(); // If no email template exists, create a default one if (!$emailTemplate) { $emailTemplate = new EmailTemplate(); $emailTemplate->restaurant_id = $this->restaurant_id; // Load default values $defaultValues = $emailTemplate->defaultValue(); foreach ($defaultValues as $key => $value) { $emailTemplate->{$key} = $value; } } // Check if it enabled $enable = $emailTemplate->{$template.'_enable'}; if ($enable !== 1) return false; $subject_b64 = $emailTemplate->{$template}['subject_'.$lang] ?? $emailTemplate->{$template}['subject_en']; $content_b64 = $emailTemplate->{$template}['content_'.$lang] ?? $emailTemplate->{$template}['content_en']; //ray('mail template: ' . $template, $emailTemplate, $subject_b64, $content_b64); $subject = base64_decode($subject_b64); $content = base64_decode($content_b64); //ray($subject, $content); } $subject = $this->parseEmailTags($subject); $content = $this->parseEmailTags($content); // Booking user email $to = $this->email; // Is admin email then use the setting if ($is_admin_email) { $to = $emailTemplate->{$template.'_email'}; // Use the restaurant email if (empty($to)) { $to = $this->restaurant->email; } $to_arr = explode(',', $to); $to = []; foreach($to_arr as $to_email) { $to_email = trim($to_email); if (filter_var($to_email, FILTER_VALIDATE_EMAIL)){ $to[] = $to_email; } } if (empty($to)) return false; } $mailManager = $this->getMailManager(); // PIXEL manager // Necesito general la notificacion primero para ponerla en el pixel $notification = null; if (!preg_match('#_admin#', $template)){ $notification = $this->storeEmailSent($to, $subject, $content, 'pending', $type, $type_id); } $pixel_code = alexr_get_pixel_code($this->uuid, $notification ? $notification->id : 0); $reply_to = $is_admin_email ? $this->email : null; $result = $mailManager->send_email($to, $subject, $content, $pixel_code, $is_admin_email, $reply_to); //$mm = new MailManager($this->restaurant_id); //$result = $mm->send_email($to, $subject, $content); // Save in the history of the booking when it is email to user if (!preg_match('#_admin#', $template)){ if (!$notification) { $this->storeEmailSent($to, $subject, $content, $result, $type, $type_id); } else { $this->updateEmailSent($notification->id, $result); } } return $result; } // Customer emails //----------------------------------------------------------------------- public function sendEmailPending($lang = 'en') { return $this->sendEmailTemplate('booking_pending', $lang); } public function sendEmailPendingPayment($lang = 'en') { return $this->sendEmailTemplate('booking_pending_payment', $lang, 'email_pending_payment'); } public function sendEmailBooked($lang = 'en') { return $this->sendEmailTemplate('booking_booked', $lang); } public function sendEmailConfirmed($lang = 'en') { return $this->sendEmailTemplate('booking_confirmed', $lang); } public function sendEmailModified($lang = 'en') { return $this->sendEmailTemplate('booking_modified', $lang); } public function sendEmailDenied($lang = 'en') { return $this->sendEmailTemplate('booking_denied', $lang); } public function sendEmailCancelled($lang = 'en') { return $this->sendEmailTemplate('booking_cancelled', $lang); } public function sendEmailNoShow($lang = 'en') { return $this->sendEmailTemplate('booking_no_show', $lang); } public function sendEmailFinished($lang = 'en') { return $this->sendEmailTemplate('booking_finished', $lang); } // Guest: "pick your menu" email when the menu is still pending after booking. public function sendEmailMenuPending($lang = 'en') { return $this->sendEmailTemplate('menu_pending', $lang); } // Admin: notification when the customer has finished selecting the menu. public function sendEmailMenuSubmittedAdmin($lang = null) { return $this->sendEmailTemplate('menu_submitted_admin', $lang); } public function sendEmailCustom($subject, $content) { $to = $this->email; $mm = new MailManager($this->restaurant_id); $subject = $this->parseEmailTags($subject); $content = $this->parseEmailTags($content); $result = $mm->send_email($to, $subject, $content); //$this->storeEmailSent($to, $subject, $content, $result); $this->storeEmailSent($to, $subject, $content, $result, 'email'); return $result; } public function sendEmailCustomId($email_id, $language = 'en') { $customEmail = EmailCustom::find( $email_id ); // Si no se encuentra el email personalizado o no esta activo, retornar error. // EmailCustom::find puede devolver null si el id no existe. if ( ! $customEmail || ! $customEmail->email_active ) { return [ 'success' => false, 'error' => 'Custom email not found or not active' ]; } // Obtener el contenido del email para el idioma especificado $emailContent = $customEmail->email_custom; // Construir las claves para el idioma específico $subjectKey = 'subject_' . $language; $contentKey = 'content_' . $language; $subject = ''; $content = ''; // Intentar obtener el contenido para el idioma especificado if (isset($emailContent[$subjectKey]) && !empty($emailContent[$subjectKey])) { $subject = base64_decode($emailContent[$subjectKey]); } if (isset($emailContent[$contentKey]) && !empty($emailContent[$contentKey])) { $content = base64_decode($emailContent[$contentKey]); } // Si no hay contenido para el idioma especificado, intentar con idioma por defecto if (empty($subject) && empty($content)) { $defaultLanguage = $this->restaurant->language ?? 'en'; $defaultSubjectKey = 'subject_' . $defaultLanguage; $defaultContentKey = 'content_' . $defaultLanguage; if (isset($emailContent[$defaultSubjectKey]) && !empty($emailContent[$defaultSubjectKey])) { $subject = base64_decode($emailContent[$defaultSubjectKey]); } if (isset($emailContent[$defaultContentKey]) && !empty($emailContent[$defaultContentKey])) { $content = base64_decode($emailContent[$defaultContentKey]); } } // Si aún no hay contenido, buscar cualquier idioma disponible if (empty($subject) && empty($content)) { foreach ($emailContent as $key => $value) { if (strpos($key, 'subject_') === 0 && !empty($value)) { $subject = base64_decode($value); $langCode = str_replace('subject_', '', $key); $contentKey = 'content_' . $langCode; if (isset($emailContent[$contentKey]) && !empty($emailContent[$contentKey])) { $content = base64_decode($emailContent[$contentKey]); } break; } } } // Si no hay subject o content, retornar error if (empty($subject) && empty($content)) { return [ 'success' => false, 'error' => 'Empty email content' ]; } // Parsear los tags del email usando la función existente $subject = $this->parseEmailTags($subject, $language); $content = $this->parseEmailTags($content, $language); // Destino del email $to = $this->email; // Crear instancia de MailManager y enviar el email $mm = new \Alexr\Mail\MailManager($this->restaurant_id); $result = $mm->send_email($to, $subject, $content); // Guardar el historial del email enviado $this->storeEmailSent($to, $subject, $content, $result, 'email'); return $result; } // Admin emails //----------------------------------------------------------------------- public function sendEmailAdminPending($lang = null) { return $this->sendEmailTemplate('booking_pending_admin', $lang); } public function sendEmailAdminBooked($lang = null) { return $this->sendEmailTemplate('booking_booked_admin', $lang); } public function sendEmailAdminConfirmed($lang = null) { return $this->sendEmailTemplate('booking_confirmed_admin', $lang); } public function sendEmailAdminCancelled($lang = null) { return $this->sendEmailTemplate('booking_cancelled_admin', $lang); } public function sendEmailAdminModified($lang = null) { return $this->sendEmailTemplate('booking_modified_admin', $lang); } /** * After-reservation offer flow: send the per-rule custom "pick your menu" * email + WhatsApp to the customer (with the booking-view link) when the * menu is still pending. Idempotent via the `pending_cta_sent` flag. * * Used both at booking creation (percentage_of_menu, no payment) and after * a successful payment (fixed / per_seat, charged at booking). * * @param string|null $lang * * @return bool true when it sent the customer notification (so callers can * skip the default status email), false otherwise. */ public function notifyAfterReservationMenuPending($lang = null) { $booking = $this; if (!$lang) $lang = $booking->language ?: 'en'; $settings = is_array($booking->settings) ? $booking->settings : []; $offer_menu = (isset($settings['offer_menu']) && is_array($settings['offer_menu'])) ? $settings['offer_menu'] : null; // Only when a menu is still pending and not already notified. if (!$offer_menu) return false; $menu_status = isset($offer_menu['menu_status']) ? $offer_menu['menu_status'] : ''; if ($menu_status !== 'pending') return false; if (!empty($offer_menu['pending_cta_sent'])) return false; // The matched rule must be an after_reservation offer. $service = alexr_get_service($booking->shift_event_id); if (!$service || !method_exists($service, 'getOfferRuleMatched')) return false; $rule = $service->getOfferRuleMatched($booking->date, $booking->time, $booking->party); if ($rule === null) return false; if ((isset($rule['display_position']) ? $rule['display_position'] : '') !== 'after_reservation') return false; // Customer "pick your menu" notification — the system-wide Menu Pending // email + WhatsApp messages (Communications -> Emails / WhatsApp Messages). // Each respects its own Active toggle. $booking->sendEmailMenuPending($lang); if (method_exists($booking, 'sendWhatsappMenuPending')) { $booking->sendWhatsappMenuPending($lang); } // Idempotency guard. $offer_menu['pending_cta_sent'] = true; $settings['offer_menu'] = $offer_menu; $booking->settings = $settings; $booking->save(); return true; } // General funcion used after payment succeed //----------------------------------------------------------------------- public function sendBookingEmail() { $booking = $this; $lang = $booking->language; if ($booking->status == BookingStatus::PENDING) { if ($booking->notifyAfterReservationMenuPending($lang)) { $booking->sendEmailAdminPending(); } else { $booking->sendEmailPending($lang); $booking->sendEmailAdminPending(); } } else if ($booking->status == BookingStatus::BOOKED) { if ($booking->notifyAfterReservationMenuPending($lang)) { $booking->sendEmailAdminBooked(); } else { $booking->sendEmailBooked($lang); $booking->sendEmailAdminBooked(); } } else if ($booking->status == BookingStatus::FINISHED) { $booking->sendEmailFinished($lang); } else if ($booking->status == BookingStatus::DENIED) { $booking->sendEmailDenied($lang); } else if ($booking->status == BookingStatus::CANCELLED) { $booking->sendEmailCancelled($lang); } else if ($booking->status == BookingStatus::NO_SHOW) { $booking->sendEmailNoShow($lang); } } }