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();
?>
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 '';
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);
}
}
}