*/ private array $emails = []; /** * Booking IDs → new payment status, captured before meta update. * * @var array */ private array $pending_status_actions = []; /** * Singleton instance. * * @since 1.0.0 * @return self */ public static function instance(): self { return self::$instance ??= new self(); } /** * Register email types and lifecycle hooks. * * @since 1.0.0 */ private function __construct() { $this->register_emails(); $this->register_hooks(); if (is_admin()) { add_action('admin_init', [$this, 'register_settings']); add_action('admin_init', [$this, 'handle_actions']); add_action('admin_enqueue_scripts', [$this, 'enqueue_assets']); } } /** * Admin URL for the Booking Settings tab (email UI lives there). * * @since 1.0.0 * * @param array $args Extra query args. * @return string */ public function get_booking_tab_url(array $args = []): string { $url = add_query_arg( array_merge( [ 'page' => self::PAGE_SLUG, ], $args ), admin_url('admin.php') ); return $url . '#tab-booking'; } /** * View data for the Booking Settings email section. * * @since 1.0.0 * * @return array{ * emails: array, * page_slug: string * } */ public function get_settings_view_data(): array { return [ 'emails' => $this->emails, 'page_slug' => self::PAGE_SLUG, ]; } /** * All registered email type instances keyed by ID. * * @since 1.0.0 * * @return array */ public function get_emails(): array { return $this->emails; } /** * Get a registered email type by ID. * * @since 1.0.0 * @param string $id Email type slug. * @return AbstractEmail|null */ public function get_email(string $id): ?AbstractEmail { return $this->emails[$id] ?? null; } /** * Send new-booking emails (customer confirmation + admin notice). * * @since 1.0.0 * @param int $booking_id Booking post ID. * @return bool True when both customer and admin emails are accepted. */ public function send_booking_emails(int $booking_id): bool { if ($booking_id <= 0 || get_post_type($booking_id) !== 'bizdexul_booking') { return false; } $booking = new BookingDetails($booking_id); /** * Filters whether booking confirmation emails should be sent. * * @param bool $should_send Whether to send. * @param int $booking_id Booking post ID. * @param BookingDetails $booking Booking model. */ if (!(bool) apply_filters('bizdex_uplisting_should_send_booking_emails', true, $booking_id, $booking)) { return false; } /** * Fires before booking confirmation emails are sent. * * @param int $booking_id Booking post ID. * @param BookingDetails $booking Booking model. */ do_action('bizdex_uplisting_before_send_booking_emails', $booking_id, $booking); $customer = $this->get_email('customer_booking_confirmation'); $admin = $this->get_email('admin_new_booking'); $customer_sent = $customer instanceof AbstractEmail ? $customer->send($booking) : false; $admin_sent = $admin instanceof AbstractEmail ? $admin->send($booking) : false; if (!$customer_sent || !$admin_sent) { if (function_exists('bizdex_uplisting_log')) { bizdex_uplisting_log('Email delivery failed for booking #' . $booking_id); } } /** * Fires after booking confirmation emails are attempted. * * @param int $booking_id Booking post ID. * @param BookingDetails $booking Booking model. * @param bool $customer_sent Whether the customer email was accepted. * @param bool $admin_sent Whether the admin email was accepted. */ do_action('bizdex_uplisting_after_send_booking_emails', $booking_id, $booking, $customer_sent, $admin_sent); return $customer_sent && $admin_sent; } /** * Send the booking-cancelled email to the guest. * * @since 1.0.0 * @param int $booking_id Booking post ID. * @return bool */ public function send_cancelled_email(int $booking_id): bool { $email = $this->get_email('booking_cancelled'); if (!$email instanceof AbstractEmail) { return false; } $booking = new BookingDetails($booking_id); /** * Filters whether the cancelled booking email should be sent. * * @param bool $should_send Whether to send. * @param int $booking_id Booking post ID. * @param BookingDetails $booking Booking model. */ if (!(bool) apply_filters('bizdex_uplisting_should_send_cancelled_email', true, $booking_id, $booking)) { return false; } return $email->send($booking); } /** * Send the payment-received email to the guest. * * @since 1.0.0 * @param int $booking_id Booking post ID. * @return bool */ public function send_payment_received_email(int $booking_id): bool { $email = $this->get_email('payment_received'); if (!$email instanceof AbstractEmail) { return false; } $booking = new BookingDetails($booking_id); /** * Filters whether the payment-received email should be sent. * * @param bool $should_send Whether to send. * @param int $booking_id Booking post ID. * @param BookingDetails $booking Booking model. */ if (!(bool) apply_filters('bizdex_uplisting_should_send_payment_received_email', true, $booking_id, $booking)) { return false; } return $email->send($booking); } /** * Sample booking-shaped data for previews and test emails. * * @since 1.0.0 * * @return array */ public function get_sample_data(): array { $data = [ 'booking_id' => 1001, 'property_id' => 0, 'first_name' => 'Jane', 'last_name' => 'Doe', 'email' => 'jane.doe@example.com', 'phone' => '+1 555 0100', 'check_in' => gmdate('Y-m-d', strtotime('+7 days')), 'check_out' => gmdate('Y-m-d', strtotime('+10 days')), 'adults' => 2, 'kids' => 1, 'total_amount' => 450.00, 'property_title' => 'Sample Beach House', 'property_url' => home_url('/'), 'edit_url' => Bookings::get_admin_url(), 'site_name' => (string) get_bloginfo('name'), ]; /** * Filters sample data used for email previews and test sends. * * @param array $data */ $filtered = apply_filters('bizdex_uplisting_email_preview_data', $data); return is_array($filtered) ? $filtered : $data; } /** * Render an email HTML preview without sending. * * @since 1.0.0 * @param AbstractEmail $email Email type to preview. * @return string */ public function get_preview_html(AbstractEmail $email): string { $data = $this->get_sample_data(); $data['email_heading'] = $email->get_heading($data); $data['additional_content'] = $email->get_additional_content($data); $data['email_id'] = $email->get_id(); $html = $email->get_content($data); /** * Filters the HTML used for admin email preview. * * @param string $html * @param AbstractEmail $email * @param array $data */ return (string) apply_filters('bizdex_uplisting_email_preview_html', $html, $email, $data); } /** * Send a test email using sample data. * * @since 1.0.0 * @param AbstractEmail $email Email type to send. * @param string $to Recipient address. * @return bool */ public function send_test(AbstractEmail $email, string $to): bool { if (!is_email($to)) { return false; } $data = $this->get_sample_data(); $data['email_heading'] = $email->get_heading($data); $data['additional_content'] = $email->get_additional_content($data); $data['email_id'] = $email->get_id(); $subject = $email->get_subject($data); $message = $email->get_content($data); /** * Filters the subject prefix/label for test emails. * * @param string $subject * @param AbstractEmail $email * @param array $data */ $subject = (string) apply_filters('bizdex_uplisting_email_test_subject', '[Test] ' . $subject, $email, $data); /** * Fires before a test email is sent from admin. * * @param string $to * @param AbstractEmail $email * @param array $data */ do_action('bizdex_uplisting_before_email_test_send', $to, $email, $data); $sent = $email->mail($to, $subject, $message); /** * Fires after a test email send attempt. * * @param bool $sent * @param string $to * @param AbstractEmail $email */ do_action('bizdex_uplisting_after_email_test_send', $sent, $to, $email); return $sent; } /** * Enqueue email settings styles on the plugin settings page. * * @since 1.0.0 * @param string $hook Current admin page hook. * @return void */ public function enqueue_assets(string $hook): void { if (!str_contains($hook, self::PAGE_SLUG)) { return; } // Script + localize live in AdminManager::enqueue_admin_assets (merged admin.js). if (!wp_style_is('bizdex-uplisting-email-settings', 'registered')) { wp_register_style( 'bizdex-uplisting-email-settings', BIZDEX_UPLISTING_PLUGIN_URL . 'assets/css/email-settings.css', [], BIZDEX_UPLISTING_VERSION ); } wp_enqueue_style('bizdex-uplisting-email-settings'); } /** * Register per-email options (sender options are registered by AdminManager). * * @since 1.0.0 */ public function register_settings(): void { foreach ($this->emails as $email) { register_setting(BIZDEX_UPLISTING_SETTINGS_GROUP, $email->option_key('subject'), [ 'type' => 'string', 'sanitize_callback' => static function ($value) use ($email): string { $value = sanitize_text_field((string) $value); return $value !== '' ? $value : $email->get_default_subject(); }, 'default' => $email->get_default_subject(), ]); register_setting(BIZDEX_UPLISTING_SETTINGS_GROUP, $email->option_key('heading'), [ 'type' => 'string', 'sanitize_callback' => static function ($value) use ($email): string { $value = sanitize_text_field((string) $value); return $value !== '' ? $value : $email->get_default_heading(); }, 'default' => $email->get_default_heading(), ]); register_setting(BIZDEX_UPLISTING_SETTINGS_GROUP, $email->option_key('additional_content'), [ 'type' => 'string', 'sanitize_callback' => 'sanitize_textarea_field', 'default' => '', ]); } } /** * Handle preview and test-email requests on the settings page. * * Capability and nonce are required before any request values are used. * * @since 1.0.0 */ public function handle_actions(): void { if (!is_admin() || !Request::has('get', 'email_action')) { return; } if (!current_user_can('manage_options')) { wp_die( esc_html__('You are not allowed to perform this action.', 'advanced-connector-for-uplisting'), '', ['response' => 403] ); } Request::require_admin_nonce(self::EMAIL_ACTION_NONCE); $page = Request::key('get', 'page'); $action = Request::key('get', 'email_action'); $email_id = Request::key('get', 'email_id'); if ($page !== self::PAGE_SLUG || !in_array($action, ['preview', 'test'], true) || $email_id === '') { return; } $email = $this->get_email($email_id); if (!$email instanceof AbstractEmail) { return; } if ($action === 'preview') { // Full HTML email document: kses_post would strip