30 ) { return; } // Check if we've already sent this notification. $sent = get_post_meta( $post_id, '_bnb_booking_new_email_sent', true ); if ( $sent ) { return; } // Mark as sent before sending to prevent duplicates. update_post_meta( $post_id, '_bnb_booking_new_email_sent', '1' ); // Send admin notification for new booking. self::send_admin_new_booking( $post_id ); } /** * Send new booking notification to admin. * * @param int $booking_id Booking post ID. * @return bool Whether email was sent. */ public static function send_admin_new_booking( int $booking_id ): bool { $booking = get_post( $booking_id ); if ( ! $booking ) { return false; } $data = self::get_booking_data( $booking_id ); $to = get_option( 'admin_email' ); $subject = sprintf( /* translators: 1: Site name, 2: Booking reference */ __( '[%1$s] New Booking: %2$s', 'wp-bnb' ), get_bloginfo( 'name' ), $data['booking_reference'] ); $message = self::get_email_template( 'admin-new-booking', $data ); return self::send_email( $to, $subject, $message ); } /** * Send confirmation email to guest. * * @param int $booking_id Booking post ID. * @return bool Whether email was sent. */ public static function send_guest_confirmation( int $booking_id ): bool { $data = self::get_booking_data( $booking_id ); if ( empty( $data['guest_email'] ) ) { return false; } $subject = sprintf( /* translators: 1: Site name, 2: Booking reference */ __( '[%1$s] Booking Confirmed: %2$s', 'wp-bnb' ), get_bloginfo( 'name' ), $data['booking_reference'] ); $message = self::get_email_template( 'booking-confirmed', $data ); return self::send_email( $data['guest_email'], $subject, $message ); } /** * Send confirmation notification to admin. * * @param int $booking_id Booking post ID. * @return bool Whether email was sent. */ public static function send_admin_confirmation( int $booking_id ): bool { $data = self::get_booking_data( $booking_id ); $to = get_option( 'admin_email' ); $subject = sprintf( /* translators: 1: Site name, 2: Booking reference */ __( '[%1$s] Booking Confirmed: %2$s', 'wp-bnb' ), get_bloginfo( 'name' ), $data['booking_reference'] ); $message = self::get_email_template( 'admin-booking-confirmed', $data ); return self::send_email( $to, $subject, $message ); } /** * Send cancellation email. * * @param int $booking_id Booking post ID. * @return bool Whether emails were sent. */ public static function send_cancellation( int $booking_id ): bool { $data = self::get_booking_data( $booking_id ); $result = true; // Send to admin. $admin_subject = sprintf( /* translators: 1: Site name, 2: Booking reference */ __( '[%1$s] Booking Cancelled: %2$s', 'wp-bnb' ), get_bloginfo( 'name' ), $data['booking_reference'] ); $admin_message = self::get_email_template( 'admin-booking-cancelled', $data ); $result = self::send_email( get_option( 'admin_email' ), $admin_subject, $admin_message ) && $result; // Send to guest if email exists. if ( ! empty( $data['guest_email'] ) ) { $guest_subject = sprintf( /* translators: 1: Site name, 2: Booking reference */ __( '[%1$s] Booking Cancelled: %2$s', 'wp-bnb' ), get_bloginfo( 'name' ), $data['booking_reference'] ); $guest_message = self::get_email_template( 'booking-cancelled', $data ); $result = self::send_email( $data['guest_email'], $guest_subject, $guest_message ) && $result; } return $result; } /** * Get booking data for email templates. * * @param int $booking_id Booking post ID. * @return array Booking data. */ private static function get_booking_data( int $booking_id ): array { $booking = get_post( $booking_id ); $room = Booking::get_room( $booking_id ); $building = Booking::get_building( $booking_id ); $check_in = get_post_meta( $booking_id, '_bnb_booking_check_in', true ); $check_out = get_post_meta( $booking_id, '_bnb_booking_check_out', true ); $status = get_post_meta( $booking_id, '_bnb_booking_status', true ); $price = get_post_meta( $booking_id, '_bnb_booking_calculated_price', true ); $adults = get_post_meta( $booking_id, '_bnb_booking_adults', true ); $children = get_post_meta( $booking_id, '_bnb_booking_children', true ); $nights = 0; if ( $check_in && $check_out ) { $nights = Booking::calculate_nights( $check_in, $check_out ); } $statuses = Booking::get_booking_statuses(); // Get guest data - prefer Guest CPT if linked, fallback to booking meta. $guest_data = self::get_guest_data( $booking_id ); return array( 'booking_id' => $booking_id, 'booking_reference' => $booking ? $booking->post_title : '', 'guest_name' => $guest_data['name'], 'guest_first_name' => $guest_data['first_name'], 'guest_last_name' => $guest_data['last_name'], 'guest_email' => $guest_data['email'], 'guest_phone' => $guest_data['phone'], 'guest_notes' => $guest_data['notes'], 'guest_full_address' => $guest_data['full_address'], 'adults' => $adults ?: 1, 'children' => $children ?: 0, 'room_name' => $room ? $room->post_title : '', 'room_id' => $room ? $room->ID : 0, 'building_name' => $building ? $building->post_title : '', 'building_id' => $building ? $building->ID : 0, 'check_in_date' => $check_in ? wp_date( get_option( 'date_format' ), strtotime( $check_in ) ) : '', 'check_out_date' => $check_out ? wp_date( get_option( 'date_format' ), strtotime( $check_out ) ) : '', 'check_in_raw' => $check_in, 'check_out_raw' => $check_out, 'nights' => $nights, 'total_price' => $price ? Calculator::formatPrice( (float) $price ) : '', 'status' => $statuses[ $status ] ?? $status, 'status_raw' => $status, 'site_name' => get_bloginfo( 'name' ), 'site_url' => home_url(), 'admin_email' => get_option( 'admin_email' ), 'booking_url' => admin_url( 'post.php?post=' . $booking_id . '&action=edit' ), ); } /** * Get guest data from Guest CPT or booking meta. * * @param int $booking_id Booking post ID. * @return array Guest data with keys: name, first_name, last_name, email, phone, notes, full_address. */ private static function get_guest_data( int $booking_id ): array { $guest_id = get_post_meta( $booking_id, '_bnb_booking_guest_id', true ); // Try to get data from Guest CPT. if ( $guest_id ) { $guest = get_post( $guest_id ); if ( $guest && Guest::POST_TYPE === $guest->post_type ) { $first_name = get_post_meta( $guest_id, '_bnb_guest_first_name', true ); $last_name = get_post_meta( $guest_id, '_bnb_guest_last_name', true ); return array( 'name' => Guest::get_full_name( $guest_id ), 'first_name' => $first_name, 'last_name' => $last_name, 'email' => get_post_meta( $guest_id, '_bnb_guest_email', true ), 'phone' => get_post_meta( $guest_id, '_bnb_guest_phone', true ), 'notes' => get_post_meta( $guest_id, '_bnb_guest_notes', true ), 'full_address' => Guest::get_formatted_address( $guest_id ), ); } } // Fallback to booking meta (legacy bookings). $guest_name = get_post_meta( $booking_id, '_bnb_booking_guest_name', true ); // Try to split name into first/last for legacy data. $name_parts = explode( ' ', $guest_name, 2 ); $first_name = $name_parts[0] ?? ''; $last_name = $name_parts[1] ?? ''; return array( 'name' => $guest_name, 'first_name' => $first_name, 'last_name' => $last_name, 'email' => get_post_meta( $booking_id, '_bnb_booking_guest_email', true ), 'phone' => get_post_meta( $booking_id, '_bnb_booking_guest_phone', true ), 'notes' => get_post_meta( $booking_id, '_bnb_booking_guest_notes', true ), 'full_address' => '', // Legacy bookings don't have full address. ); } /** * Get email template with placeholders replaced. * * @param string $template_name Template name. * @param array $data Template data. * @return string HTML email content. */ private static function get_email_template( string $template_name, array $data ): string { $template = self::get_template_content( $template_name ); // Replace placeholders. foreach ( $data as $key => $value ) { $template = str_replace( '{' . $key . '}', (string) $value, $template ); } return $template; } /** * Get template content. * * @param string $template_name Template name. * @return string Template HTML. */ private static function get_template_content( string $template_name ): string { // Built-in templates. Could be extended to load from files. $templates = array( 'admin-new-booking' => self::template_admin_new_booking(), 'booking-confirmed' => self::template_booking_confirmed(), 'admin-booking-confirmed' => self::template_admin_booking_confirmed(), 'booking-cancelled' => self::template_booking_cancelled(), 'admin-booking-cancelled' => self::template_admin_booking_cancelled(), ); return $templates[ $template_name ] ?? ''; } /** * Send HTML email. * * @param string $to Recipient email. * @param string $subject Email subject. * @param string $message HTML message. * @return bool Whether email was sent. */ private static function send_email( string $to, string $subject, string $message ): bool { $headers = array( 'Content-Type: text/html; charset=UTF-8', 'From: ' . get_bloginfo( 'name' ) . ' <' . get_option( 'admin_email' ) . '>', ); /** * Filter email recipients. * * @param string $to Recipient email. * @param string $subject Email subject. * @param string $message Email message. */ $to = apply_filters( 'wp_bnb_booking_email_recipients', $to, $subject, $message ); /** * Filter email subject. * * @param string $subject Email subject. */ $subject = apply_filters( 'wp_bnb_booking_email_subject', $subject ); /** * Filter email content. * * @param string $message Email message. */ $message = apply_filters( 'wp_bnb_booking_email_content', $message ); return wp_mail( $to, $subject, $message, $headers ); } /** * Get base email styles. * * @return string CSS styles. */ private static function get_email_styles(): string { return ' body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; font-size: 14px; line-height: 1.6; color: #333; } .email-container { max-width: 600px; margin: 0 auto; padding: 20px; } .email-header { background: #135e96; color: #fff; padding: 20px; text-align: center; } .email-header h1 { margin: 0; font-size: 24px; } .email-body { background: #fff; padding: 30px; border: 1px solid #ddd; } .email-footer { padding: 20px; text-align: center; font-size: 12px; color: #666; } .booking-details { background: #f9f9f9; padding: 15px; margin: 20px 0; border-left: 4px solid #135e96; } .booking-details h3 { margin-top: 0; color: #135e96; } .detail-row { margin: 8px 0; } .detail-label { font-weight: 600; display: inline-block; min-width: 120px; } .btn { display: inline-block; padding: 10px 20px; background: #135e96; color: #fff; text-decoration: none; border-radius: 4px; margin-top: 15px; } .status-badge { display: inline-block; padding: 4px 10px; border-radius: 3px; font-size: 12px; font-weight: 600; text-transform: uppercase; } .status-pending { background: #dba617; color: #fff; } .status-confirmed { background: #00a32a; color: #fff; } .status-cancelled { background: #d63638; color: #fff; } '; } /** * Template: Admin new booking notification. * * @return string Template HTML. */ private static function template_admin_new_booking(): string { $styles = self::get_email_styles(); return <<
A new booking has been created and is awaiting confirmation.
Dear {guest_name},
Great news! Your booking has been confirmed. We look forward to welcoming you.
If you have any questions or need to make changes to your reservation, please contact us at {admin_email}.
Thank you for choosing us!
Booking {booking_reference} has been confirmed.
Dear {guest_name},
We're writing to confirm that your booking has been cancelled.
If you have any questions or would like to make a new reservation, please contact us at {admin_email}.
We hope to welcome you in the future.
Booking {booking_reference} has been cancelled.