JezK
Edit File: booking-automate-follow.php
<?php /** * Plugin Name: Booking Automate Follow-up * Description: Sends automated follow-up emails after booking end with delay, timezone safety, and DB logging. * Version: 1.0 * Author: Amber Goetz * Author URI: https://www.theactivemedia.com/ * License: GPL2 */ if (!defined('ABSPATH')) exit; /*-------------------------------------------------------------- # CREATE CUSTOM LOG TABLE --------------------------------------------------------------*/ register_activation_hook(__FILE__, function () { global $wpdb; $table = $wpdb->prefix . 'baf_email_logs'; $charset = $wpdb->get_charset_collate(); $sql = "CREATE TABLE IF NOT EXISTS $table ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, booking_id BIGINT UNSIGNED, order_id BIGINT UNSIGNED, email VARCHAR(190), status VARCHAR(50), sent_at DATETIME, resend_count INT DEFAULT 0, message TEXT ) $charset;"; require_once ABSPATH . 'wp-admin/includes/upgrade.php'; dbDelta($sql); $queue_table = $wpdb->prefix . 'baf_queue'; $sql_queue = "CREATE TABLE IF NOT EXISTS $queue_table ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, booking_id BIGINT UNSIGNED, order_id BIGINT UNSIGNED, email VARCHAR(190), end_time BIGINT, send_time BIGINT, status VARCHAR(20) DEFAULT 'pending', created_at DATETIME, message TEXT ) $charset;"; dbDelta($sql_queue); }); add_action('init', function () { if (isset($_GET['baf_run_cron']) && $_GET['baf_run_cron'] === '1') { // Optional security token if (!isset($_GET['token']) || $_GET['token'] !== 'zaf8923ksd83jd9') { exit('Unauthorized'); } baf_scan_bookings_callback(); exit('Cron executed'); } }); /*-------------------------------------------------------------- # ADMIN STYLE --------------------------------------------------------------*/ add_action('admin_enqueue_scripts', function($hook){ if (strpos($hook, 'booking-automate-follow') !== false) { wp_enqueue_style( 'baf-admin-style', plugin_dir_url(__FILE__) . 'assets/css/admin-style.css', [], '1.0' ); wp_enqueue_script( 'baf-admin-js', plugin_dir_url(__FILE__) . 'assets/js/baf-admin.js', ['jquery'], '1.0', true ); wp_localize_script( 'baf-admin-js', 'baf_ajax', [ 'ajax_url' => admin_url('admin-ajax.php'), 'resend_nonce' => wp_create_nonce('baf_resend_nonce'), 'test_nonce' => wp_create_nonce('baf_test_email_nonce') ] ); } }); /*-------------------------------------------------------------- # SETTINGS --------------------------------------------------------------*/ function baf_get_settings() { return [ 'delay_hours' => get_option('baf_delay_hours', 8), 'support_email' => get_option('baf_support_email', get_option('admin_email')), 'test_mode' => get_option('baf_test_mode', 1), 'test_email' => get_option('baf_test_email', get_option('admin_email')), ]; } /*-------------------------------------------------------------- # ADMIN MENU --------------------------------------------------------------*/ add_action('admin_menu', function () { add_menu_page( 'Booking Follow-up', 'Booking Follow-up', 'manage_options', 'booking-automate-follow', 'baf_dashboard_page', 'dashicons-email', 56 ); add_submenu_page( 'booking-automate-follow', 'Settings', 'Settings', 'manage_options', 'booking-automate-follow-settings', 'baf_settings_page' ); }); /*-------------------------------------------------------------- # SETTINGS PAGE --------------------------------------------------------------*/ function baf_settings_page() { if (isset($_POST['baf_save'])) { check_admin_referer('baf_save_settings'); update_option('baf_delay_hours', intval($_POST['delay'])); update_option('baf_support_email', sanitize_email($_POST['support'])); update_option('baf_test_mode', isset($_POST['test_mode']) ? 1 : 0); update_option('baf_test_email', sanitize_email($_POST['test_email'])); echo '<div class="notice notice-success is-dismissible"> <p>Settings saved successfully.</p> </div>'; } $s = baf_get_settings(); ?> <div class="wrap baf-admin"> <div class="baf-settings-header"> <div> <h1>Follow-up Settings</h1> <p>Configure automation delay and email behavior</p> </div> </div> <div class="baf-card"> <form method="post"> <?php wp_nonce_field('baf_save_settings'); ?> <div class="baf-field"> <label>Delay (hours after booking ends)</label> <input type="number" name="delay" value="<?php echo esc_attr($s['delay_hours']); ?>"> <small>Email will be sent after this many hours from booking end time.</small> </div> <div class="baf-field"> <label>Support Email</label> <input type="email" name="support" value="<?php echo esc_attr($s['support_email']); ?>"> <small>Used when customer clicks negative feedback.</small> </div> <div class="baf-field baf-toggle"> <label>Test Mode</label> <label class="baf-switch"> <input type="checkbox" name="test_mode" <?php checked($s['test_mode'], 1); ?>> <span class="baf-slider"></span> </label> <small>Only send emails to test address.</small> </div> <div class="baf-field"> <label>Test Email</label> <input type="email" name="test_email" value="<?php echo esc_attr($s['test_email']); ?>"> </div> <div class="baf-actions"> <button class="button button-primary" name="baf_save"> Save Settings </button> </div> </form> <?php if ($s['test_mode']) : ?> <!-- --> <hr style="margin:40px 0;"> <div class="baf-preview-section"> <h2>Email Template Preview</h2> <p>This preview shows exactly how the email will look.</p> <div class="baf-email-preview"> <?php echo baf_get_email_html($s); ?> </div> <div style="margin-top:20px;"> <button id="baf-send-test-email" class="button button-secondary"> Send Test Email to <?php echo esc_html($s['support_email']); ?> </button> <span id="baf-test-result" style="margin-left:15px;"></span> </div> </div> <?php endif; ?> </div> </div> <?php } /*-------------------------------------------------------------- # EMAIL TEMPLATE --------------------------------------------------------------*/ function baf_get_email_html($settings) { $google_link = 'https://www.google.com/maps/place/Zion+Outfitter/@37.1997531,-112.9886757,898m/data=!3m1!1e3!4m8!3m7!1s0x80cac2a2cf7b2c71:0xef8370d90757df58!8m2!3d37.1997531!4d-112.9886757!9m1!1b1!16s%2Fg%2F1vp6_gc3?entry=ttu&g_ep=EgoyMDI2MDMxMS4wIKXMDSoASAFQAw%3D%3D'; $support_mail = 'info@zionoutfitter.com'; return ' <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <style> @media only screen and (max-width: 600px) { .container { width: 100% !important; } .padding { padding: 20px !important; } .logo img { width: 140px !important; } .nav { font-size: 12px !important; white-space: nowrap; } .btn-row td { display: block !important; width: 100% !important; text-align: center !important; padding: 8px 0 !important; } .btn { display: block !important; width: 100% !important; box-sizing: border-box; } } </style> </head> <body style="margin:0;padding:0;background:#f3f4f6;font-family:Arial,Helvetica,sans-serif;"> <table width="100%" cellpadding="0" cellspacing="0" style="padding:30px 0;"> <tr> <td align="center"> <table class="container" width="600" cellpadding="0" cellspacing="0" style="max-width:600px;width:100%;background:#ffffff;border-radius:6px;overflow:hidden;"> <!-- HEADER --> <tr> <td style="padding:20px 30px;border-bottom:1px solid #e5e7eb;"> <table width="100%"> <tr> <td align="left" class="logo"> <img src="https://zionoutfitter.com/wp-content/plugins/booking-automate-follow/email-template-logo.png" width="180" style="display:block;max-width:100%;height:auto;"> </td> <td align="right" class="nav" style="font-size:14px;"> <a href="https://zionoutfitter.com/the-narrows-rentals/" style="color:#f97316;text-decoration:none;margin-left:10px;margin-right: 5px;">Narrow Rentals</a> | <a href="https://zionoutfitter.com/bike-zion/" style="color:#f97316;text-decoration:none;margin:0 5px;">Bike Zion</a> | <a href="https://zionoutfitter.com/" style="color:#f97316;text-decoration:none;margin-left:5px;">Visit Us</a> </td> </tr> </table> </td> </tr> <!-- HERO --> <tr> <td> <img src="https://zionoutfitter.com/wp-content/uploads/2025/09/store-location.webp" width="600" style="width:100%;height:auto;display:block;"> </td> </tr> <!-- CONTENT --> <tr> <td class="padding" style="padding:30px 40px;color:#4b5563;line-height:1.6;"> <h2 style="margin:0 0 15px;font-size:24px;color:#374151;text-align:center;"> Thank you again for adventuring with us! </h2> <p style="margin:0 0 25px;font-size:16px;"> Click below to let us know how great your rental experience was, or send us your honest feedback if you feel we could improve. </p> <!-- BUTTON ROW --> <table align="center" cellpadding="0" cellspacing="0" class="btn-row"> <tr> <td align="center" style="padding:5px;"> <a href="'.$google_link.'" class="btn" style=" display:inline-block; padding:14px 24px; background:#be5a05; color:#ffffff; text-decoration:none; border-radius:30px; font-weight:bold; font-size:16px; "> It was great! </a> </td> <td align="center" style="padding:5px;"> <a href="mailto:'.$support_mail.'" class="btn" style=" display:inline-block; padding:14px 24px; background:#be5a05; color:#ffffff; text-decoration:none; border-radius:30px; font-weight:bold; font-size:16px; "> It wasn’t great… </a> </td> </tr> </table> </td> </tr> </table> </td> </tr> </table> </body> </html>'; } /*-------------------------------------------------------------- # MAIN CRON CALLBACK — FIXED LOGIC --------------------------------------------------------------*/ function baf_scan_bookings_callback() { error_log('BAF CRON TRIGGERED'); global $wpdb; update_option('baf_last_cron_run', current_time('timestamp')); $settings = baf_get_settings(); $now = current_time('timestamp'); $queue_table = $wpdb->prefix . 'baf_queue'; /* =============================== PHASE 1 — COLLECT TODAY BOOKINGS ================================ */ $today_start = strtotime('today midnight', $now); $today_end = strtotime('tomorrow midnight', $now) - 1; $args = [ 'post_type' => 'yith_booking', 'post_status' => 'any', 'posts_per_page' => -1, 'meta_query' => [ [ 'key' => '_to', 'value' => [$today_start, $today_end], 'compare' => 'BETWEEN', 'type' => 'NUMERIC' ] ] ]; $bookings = get_posts($args); foreach ($bookings as $booking) { $booking_id = $booking->ID; $end_time = get_post_meta($booking_id, '_to', true); $order_id = get_post_meta($booking_id, '_order_id', true); if (!$end_time || !$order_id) continue; $order = wc_get_order($order_id); if (!$order) continue; $email = $order->get_billing_email(); if (!$email) continue; // Test Mode Filtering at Queue Level if ($settings['test_mode']) { if ($email !== $settings['test_email']) { continue; // Do not even add to queue } } // Check if already in queue $exists = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM $queue_table WHERE booking_id = %d LIMIT 1", $booking_id ) ); if ($exists) continue; $send_time = $end_time + ($settings['delay_hours'] * HOUR_IN_SECONDS); $wpdb->insert( $queue_table, [ 'booking_id' => $booking_id, 'order_id' => $order_id, 'email' => $email, 'end_time' => $end_time, 'send_time' => $send_time, 'status' => 'pending', 'created_at' => current_time('mysql') ] ); } /* =============================== PHASE 2 — SEND READY QUEUE ITEMS ================================ */ $ready_items = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $queue_table WHERE status = 'pending' AND send_time <= %d LIMIT 10", $now ) ); foreach ($ready_items as $item) { $booking_id = $item->booking_id; $order_id = $item->order_id; $email = $item->email; // Test mode filtering if ($settings['test_mode']) { if ($email !== $settings['test_email']) { continue; } } $subject = "How was your rental experience?"; $content = baf_get_email_html($settings); $headers = ["Content-Type: text/html; charset=UTF-8"]; $status = 'failed'; $message = ''; $sent = wp_mail($email, $subject, $content, $headers); if ($sent) { $status = 'sent'; $wpdb->update( $queue_table, [ 'status' => 'sent' ], ['id' => $item->id], ['%s'], ['%d'] ); update_post_meta($booking_id, '_baf_status', 'sent'); update_post_meta($booking_id, '_baf_sent', $now); } else { $message = 'Mailer returned false'; $wpdb->update( $queue_table, [ 'status' => 'failed', 'message' => $message ], ['id' => $item->id] ); } // Log entry $wpdb->insert( $wpdb->prefix . 'baf_email_logs', [ 'booking_id' => $booking_id, 'order_id' => $order_id, 'email' => $email, 'status' => $status, 'sent_at' => current_time('mysql'), 'message' => $message ] ); } } /*-------------------------------------------------------------- # DASHBOARD WITH EMAIL LOG TABLE --------------------------------------------------------------*/ function baf_dashboard_page() { global $wpdb; $settings = baf_get_settings(); $current_time = current_time('timestamp'); $last_run = get_option('baf_last_cron_run'); $status_label = 'Never Run'; $status_class = 'baf-status-danger'; echo '<div class="wrap baf-admin">'; echo ' <div class="baf-header"> <div> <h1>Booking Follow-up Automation</h1> <p>Automated review follow-up system with logging & retry control</p> </div> </div>'; if ($last_run) { $diff = $current_time - $last_run; if ($diff <= 600) { // 10 minutes $status_label = 'Healthy'; $status_class = 'baf-status-success'; } elseif ($diff <= 1800) { // 30 minutes $status_label = 'Delayed'; $status_class = 'baf-status-warning'; } else { $status_label = 'Not Running'; $status_class = 'baf-status-danger'; } } echo ' <div class="baf-stats"> <div class="baf-stat-card"> <span>Last Cron Run</span> <strong>'.($last_run ? date('Y-m-d H:i:s', $last_run) : 'Never').'</strong> </div> <div class="baf-stat-card"> <span>Cron Status</span> <strong class="'.$status_class.'">'.$status_label.'</strong> </div> <div class="baf-stat-card"> <span>Cron Trigger</span> <strong>External (Every 5 Minutes)</strong> </div> </div> '; /* BOOKINGS TABLE */ $today_start = strtotime('today midnight'); $today_end = strtotime('tomorrow midnight') - 1; $args = [ 'post_type' => 'yith_booking', 'post_status' => 'any', 'posts_per_page' => -1, 'meta_query' => [ [ 'key' => '_to', 'value' => [$today_start, $today_end], 'compare' => 'BETWEEN', 'type' => 'NUMERIC' ] ] ]; $bookings = get_posts($args); echo '<h2>Bookings Status (Today)</h2>'; echo '<table class="widefat striped"><thead> <tr> <th>Booking</th> <th>Email</th> <th>Bokking End Date</th> <th>Status</th> </tr> </thead><tbody>'; $has_rows = false; foreach ($bookings as $b) { $end = get_post_meta($b->ID, '_to', true); $status_meta = get_post_meta($b->ID, '_baf_status', true); $order_id = get_post_meta($b->ID, '_order_id', true); if (!$order_id) continue; $order = wc_get_order($order_id); if (!$order) continue; $email = $order ? $order->get_billing_email() : '—'; if (!$email) continue; if ($settings['test_mode'] && $email !== $settings['test_email']) continue; // Skip if this order already has a successful send log $already_sent = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}baf_email_logs WHERE order_id = %d AND status = %s", $order_id, 'sent' ) ); if ($already_sent > 0) { continue; // Do not show in Bookings Status } $has_rows = true; $end_date = $end ? date('Y-m-d H:i', $end) : '—'; switch ($status_meta) { case 'sent': $status = '<span class="baf-badge success">Sent</span>'; break; case 'failed': $status = '<span class="baf-badge danger">Failed</span>'; break; case 'retrying': $status = '<span class="baf-badge warning">Retrying</span>'; break; default: $status = '<span class="baf-badge info">Awaiting Send</span>'; } echo "<tr> <td>#{$b->ID}</td> <td>{$email}</td> <td>{$end_date}</td> <td>{$status}</td> </tr>"; } /* ===== EMPTY STATE ===== */ if (!$has_rows) { echo '<tr> <td colspan="4" style="text-align:center;padding:40px;"> <div style="font-size:16px;font-weight:600;color:#374151;margin-bottom:6px;"> No follow-ups scheduled </div> <div style="font-size:13px;color:#6b7280;"> There are currently no bookings that require a follow-up email. </div> </td> </tr>'; } echo '</tbody></table>'; /* =============================== SCHEDULING QUEUE TABLE ================================ */ $queue_table = $wpdb->prefix . 'baf_queue'; $today_start = strtotime('today midnight'); $today_end = strtotime('tomorrow midnight') - 1; $queue_items = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $queue_table WHERE send_time BETWEEN %d AND %d ORDER BY send_time ASC", $today_start, $today_end ) ); echo '<h2 style="margin-top:40px;">Email Scheduling Queue (Today)</h2>'; echo '<table class="widefat striped"><thead> <tr> <th>ID</th> <th>Booking</th> <th>Order</th> <th>Email</th> <th>Booking End Time</th> <th>Email Send Time</th> <th>Status</th> <th>Created</th> </tr> </thead><tbody>'; if ($queue_items) { foreach ($queue_items as $item) { $end_time = $item->end_time ? date('Y-m-d H:i', $item->end_time) : '—'; $send_time = $item->send_time ? date('Y-m-d H:i', $item->send_time) : '—'; // Status badge styling switch ($item->status) { case 'sent': $status = '<span class="baf-badge success">Sent</span>'; break; case 'failed': $status = '<span class="baf-badge danger">Failed</span>'; break; default: $status = '<span class="baf-badge processing-info">Scheduled</span>'; } echo '<tr> <td>'.$item->id.'</td> <td>#'.$item->booking_id.'</td> <td>#'.$item->order_id.'</td> <td>'.$item->email.'</td> <td>'.$end_time.'</td> <td>'.$send_time.'</td> <td>'.$status.'</td> <td>'.$item->created_at.'</td> </tr>'; } } else { echo '<tr><td colspan="9" style="text-align:center;padding:30px;"> No scheduled emails in queue. </td></tr>'; } echo '</tbody></table>'; /* =============================== EMAIL LOG TABLE ================================ */ $table = $wpdb->prefix . 'baf_email_logs'; $logs_per_page = 10; $current_page = isset($_GET['log_page']) ? max(1, intval($_GET['log_page'])) : 1; $offset = ($current_page - 1) * $logs_per_page; $search = isset($_GET['baf_search']) ? sanitize_text_field($_GET['baf_search']) : ''; $where = ''; $params = []; if ($search) { if (is_numeric($search)) { $where = "WHERE booking_id = %d"; $params[] = intval($search); } else { $where = "WHERE email LIKE %s"; $params[] = '%' . $wpdb->esc_like($search) . '%'; } } /* TOTAL ROWS */ if ($where) { $total_logs = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $table $where", ...$params ) ); } else { $total_logs = $wpdb->get_var("SELECT COUNT(*) FROM $table"); } /* FETCH LOGS */ $query = "SELECT * FROM $table $where ORDER BY id DESC LIMIT %d OFFSET %d"; $params[] = $logs_per_page; $params[] = $offset; $logs = $wpdb->get_results( $wpdb->prepare($query, ...$params) ); $total_pages = ceil($total_logs / $logs_per_page); echo '<h2 style="margin-top:40px;">Email Send Logs</h2>'; // Search Filter by email and booking ID echo '<form method="get" style="margin-bottom:15px;">'; echo '<input type="hidden" name="page" value="booking-automate-follow">'; $search = isset($_GET['baf_search']) ? sanitize_text_field($_GET['baf_search']) : ''; echo '<input type="text" name="baf_search" value="'.$search.'" placeholder="Search by Booking ID or Email" style="padding:6px 10px;width:260px;margin-right: 10px;">'; echo '<button class="button">Search</button>'; echo '</form>'; echo '<table class="widefat striped"><thead> <tr> <th>ID</th> <th>Booking</th> <th>Order</th> <th>Email</th> <th>Status</th> <th>Sent At</th> <th>Resends</th> <th>Message</th> <th>Action</th> </tr> </thead><tbody>'; if ($logs) { foreach ($logs as $r) { echo '<tr> <td>'.$r->id.'</td> <td>#'.$r->booking_id.'</td> <td>#'.$r->order_id.'</td> <td>'.$r->email.'</td> <td>'.$r->status.'</td> <td>'.$r->sent_at.'</td> <td>'.$r->resend_count.'</td> <td>'.($r->message ?: '—').'</td> <td> <button class="button baf-resend-btn" data-booking="'.$r->booking_id.'" data-order="'.$r->order_id.'"> Resend </button> </td> </tr>'; } } else { echo '<tr><td colspan="9">No email logs yet.</td></tr>'; } echo '</tbody></table></div>'; if ($total_pages > 1) { echo '<div class="tablenav"><div class="tablenav-pages booking-automation" style="margin-top:15px;">'; $range = 2; /* FIRST + PREV */ if ($current_page > 1) { echo '<a style="margin-right:6px;padding:6px 10px;border:1px solid #ccd0d4;text-decoration:none;" href="?page=booking-automate-follow&log_page=1&baf_search='.urlencode($search).'">« First</a>'; echo '<a style="margin-right:6px;padding:6px 10px;border:1px solid #ccd0d4;text-decoration:none;" href="?page=booking-automate-follow&log_page='.($current_page-1).'&baf_search='.urlencode($search).'">‹ Prev</a>'; } /* PAGE NUMBERS AROUND CURRENT PAGE */ for ($i = max(1, $current_page - $range); $i <= min($total_pages, $current_page + $range); $i++) { $active = ($i == $current_page) ? 'style="font-weight:bold;background:#f0f0f1;"' : ''; echo '<a '.$active.' style="margin-right:6px;padding:6px 10px;border:1px solid #ccd0d4;text-decoration:none;" href="?page=booking-automate-follow&log_page='.$i.'&baf_search='.urlencode($search).'">'.$i.'</a>'; } /* NEXT + LAST */ if ($current_page < $total_pages) { echo '<a style="margin-left:6px;padding:6px 10px;border:1px solid #ccd0d4;text-decoration:none;" href="?page=booking-automate-follow&log_page='.($current_page+1).'&baf_search='.urlencode($search).'">Next ›</a>'; echo '<a style="margin-left:6px;padding:6px 10px;border:1px solid #ccd0d4;text-decoration:none;" href="?page=booking-automate-follow&log_page='.$total_pages.'&baf_search='.urlencode($search).'">Last »</a>'; } echo '</div></div>'; } } /*-------------------------------------------------------------- # AJAX MANUAL RESEND --------------------------------------------------------------*/ add_action('wp_ajax_baf_resend_email', 'baf_manual_resend_email'); function baf_manual_resend_email() { check_ajax_referer('baf_resend_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error('Unauthorized'); } global $wpdb; $booking_id = intval($_POST['booking_id']); $order_id = intval($_POST['order_id']); $order = wc_get_order($order_id); if (!$order) { wp_send_json_error('Order not found'); } $email = $order->get_billing_email(); if (!$email) { wp_send_json_error('No email found'); } $subject = "How was your rental experience?"; $content = baf_get_email_html(baf_get_settings()); $status = 'failed'; $message = ''; try { $headers = ["Content-Type: text/html; charset=UTF-8"]; $sent = wp_mail($email, $subject, $content, $headers); if ($sent) { $status = 'sent'; update_post_meta($booking_id, '_baf_status', 'sent'); update_post_meta($booking_id, '_baf_sent', current_time('timestamp')); } else { $message = 'Mailer returned false'; } } catch (Exception $e) { $message = $e->getMessage(); } $table = $wpdb->prefix . 'baf_email_logs'; // Check if log already exists for this order $existing_log_id = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM $table WHERE order_id = %d LIMIT 1", $order_id ) ); if ($existing_log_id) { // Get current resend count $current_count = $wpdb->get_var( $wpdb->prepare( "SELECT resend_count FROM $table WHERE id = %d", $existing_log_id ) ); $new_count = intval($current_count); // ONLY increment if email was successfully sent if ($status === 'sent') { $new_count++; } $wpdb->update( $table, [ 'status' => $status, 'sent_at' => current_time('mysql'), 'message' => $message, 'resend_count' => $new_count ], ['id' => $existing_log_id] ); } else { // Insert new log row (should only happen first time) $wpdb->insert( $table, [ 'booking_id' => $booking_id, 'order_id' => $order_id, 'email' => $email, 'status' => $status, 'sent_at' => current_time('mysql'), 'resend_count' => 0, 'message' => $message ] ); } if ($status === 'sent') { wp_send_json_success('Email resent successfully.'); } else { wp_send_json_error($message ?: 'Resend failed.'); } } /*-------------------------------------------------------------- # AJAX PREVIEW EMAIL TEMPLATE SENDER --------------------------------------------------------------*/ add_action('wp_ajax_baf_send_test_email', 'baf_send_test_email'); function baf_send_test_email() { check_ajax_referer('baf_test_email_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error('Unauthorized'); } $settings = baf_get_settings(); if (!$settings['test_mode']) { wp_send_json_error('Test mode is disabled.'); } $to = $settings['support_email']; if (!$to) { wp_send_json_error('Support email not configured.'); } $subject = "TEST: How was your rental experience?"; $content = baf_get_email_html($settings); $headers = ["Content-Type: text/html; charset=UTF-8"]; $sent = wp_mail($to, $subject, $content, $headers); if ($sent) { wp_send_json_success('Test email sent successfully.'); } else { wp_send_json_error('Failed to send test email.'); } }