Files
wp-bnb/src/Booking/EmailNotifier.php
magdev aab3a4d1aa
All checks were successful
Create Release Package / build-release (push) Successful in 1m26s
Add guest management and GDPR privacy compliance (v0.4.0)
- Create Guest CPT with personal info, address, ID/passport tracking
- Add guest-booking integration with AJAX search and linking
- Implement GDPR compliance via WordPress Privacy API (export/erasure)
- Update EmailNotifier to use Guest CPT data with new placeholders
- Add CSS styles for guest search, linked display, and privacy UI

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 14:59:43 +01:00

642 lines
20 KiB
PHP

<?php
/**
* Email notifier for bookings.
*
* Handles sending email notifications for booking events.
*
* @package Magdev\WpBnb\Booking
*/
declare( strict_types=1 );
namespace Magdev\WpBnb\Booking;
use Magdev\WpBnb\PostTypes\Booking;
use Magdev\WpBnb\PostTypes\Guest;
use Magdev\WpBnb\PostTypes\Room;
use Magdev\WpBnb\Pricing\Calculator;
/**
* EmailNotifier class.
*/
final class EmailNotifier {
/**
* Initialize the email notifier.
*
* @return void
*/
public static function init(): void {
add_action( 'wp_bnb_booking_status_changed', array( self::class, 'on_status_change' ), 10, 3 );
add_action( 'save_post_' . Booking::POST_TYPE, array( self::class, 'on_booking_created' ), 20, 2 );
}
/**
* Handle status change event.
*
* @param int $booking_id Booking post ID.
* @param string $old_status Previous status.
* @param string $new_status New status.
* @return void
*/
public static function on_status_change( int $booking_id, string $old_status, string $new_status ): void {
switch ( $new_status ) {
case 'confirmed':
self::send_guest_confirmation( $booking_id );
self::send_admin_confirmation( $booking_id );
break;
case 'cancelled':
self::send_cancellation( $booking_id );
break;
}
}
/**
* Handle booking created event.
*
* @param int $post_id Post ID.
* @param \WP_Post $post Post object.
* @return void
*/
public static function on_booking_created( int $post_id, \WP_Post $post ): void {
// Skip if autosave or revision.
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return;
}
if ( wp_is_post_revision( $post_id ) ) {
return;
}
// Check if this is a new booking (created in the last 30 seconds).
$created = get_post_time( 'U', true, $post_id );
if ( time() - $created > 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 <<<HTML
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>{$styles}</style>
</head>
<body>
<div class="email-container">
<div class="email-header">
<h1>New Booking Received</h1>
</div>
<div class="email-body">
<p>A new booking has been created and is awaiting confirmation.</p>
<div class="booking-details">
<h3>Booking Details</h3>
<div class="detail-row"><span class="detail-label">Reference:</span> {booking_reference}</div>
<div class="detail-row"><span class="detail-label">Status:</span> <span class="status-badge status-{status_raw}">{status}</span></div>
<div class="detail-row"><span class="detail-label">Room:</span> {room_name}</div>
<div class="detail-row"><span class="detail-label">Building:</span> {building_name}</div>
<div class="detail-row"><span class="detail-label">Check-in:</span> {check_in_date}</div>
<div class="detail-row"><span class="detail-label">Check-out:</span> {check_out_date}</div>
<div class="detail-row"><span class="detail-label">Nights:</span> {nights}</div>
<div class="detail-row"><span class="detail-label">Total:</span> {total_price}</div>
</div>
<div class="booking-details">
<h3>Guest Information</h3>
<div class="detail-row"><span class="detail-label">Name:</span> {guest_name}</div>
<div class="detail-row"><span class="detail-label">Email:</span> {guest_email}</div>
<div class="detail-row"><span class="detail-label">Phone:</span> {guest_phone}</div>
<div class="detail-row"><span class="detail-label">Adults:</span> {adults}</div>
<div class="detail-row"><span class="detail-label">Children:</span> {children}</div>
</div>
<a href="{booking_url}" class="btn">View Booking</a>
</div>
<div class="email-footer">
<p>This email was sent from {site_name}</p>
</div>
</div>
</body>
</html>
HTML;
}
/**
* Template: Guest booking confirmed.
*
* @return string Template HTML.
*/
private static function template_booking_confirmed(): string {
$styles = self::get_email_styles();
return <<<HTML
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>{$styles}</style>
</head>
<body>
<div class="email-container">
<div class="email-header">
<h1>Booking Confirmed</h1>
</div>
<div class="email-body">
<p>Dear {guest_name},</p>
<p>Great news! Your booking has been confirmed. We look forward to welcoming you.</p>
<div class="booking-details">
<h3>Your Booking Details</h3>
<div class="detail-row"><span class="detail-label">Confirmation:</span> {booking_reference}</div>
<div class="detail-row"><span class="detail-label">Room:</span> {room_name}</div>
<div class="detail-row"><span class="detail-label">Location:</span> {building_name}</div>
<div class="detail-row"><span class="detail-label">Check-in:</span> {check_in_date}</div>
<div class="detail-row"><span class="detail-label">Check-out:</span> {check_out_date}</div>
<div class="detail-row"><span class="detail-label">Duration:</span> {nights} nights</div>
<div class="detail-row"><span class="detail-label">Guests:</span> {adults} adults, {children} children</div>
<div class="detail-row"><span class="detail-label">Total:</span> <strong>{total_price}</strong></div>
</div>
<p>If you have any questions or need to make changes to your reservation, please contact us at {admin_email}.</p>
<p>Thank you for choosing us!</p>
</div>
<div class="email-footer">
<p>{site_name}<br>{site_url}</p>
</div>
</div>
</body>
</html>
HTML;
}
/**
* Template: Admin booking confirmed.
*
* @return string Template HTML.
*/
private static function template_admin_booking_confirmed(): string {
$styles = self::get_email_styles();
return <<<HTML
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>{$styles}</style>
</head>
<body>
<div class="email-container">
<div class="email-header">
<h1>Booking Confirmed</h1>
</div>
<div class="email-body">
<p>Booking {booking_reference} has been confirmed.</p>
<div class="booking-details">
<h3>Booking Summary</h3>
<div class="detail-row"><span class="detail-label">Guest:</span> {guest_name}</div>
<div class="detail-row"><span class="detail-label">Room:</span> {room_name}</div>
<div class="detail-row"><span class="detail-label">Dates:</span> {check_in_date} - {check_out_date}</div>
<div class="detail-row"><span class="detail-label">Total:</span> {total_price}</div>
</div>
<a href="{booking_url}" class="btn">View Booking</a>
</div>
<div class="email-footer">
<p>This email was sent from {site_name}</p>
</div>
</div>
</body>
</html>
HTML;
}
/**
* Template: Guest booking cancelled.
*
* @return string Template HTML.
*/
private static function template_booking_cancelled(): string {
$styles = self::get_email_styles();
return <<<HTML
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>{$styles}</style>
</head>
<body>
<div class="email-container">
<div class="email-header" style="background: #d63638;">
<h1>Booking Cancelled</h1>
</div>
<div class="email-body">
<p>Dear {guest_name},</p>
<p>We're writing to confirm that your booking has been cancelled.</p>
<div class="booking-details">
<h3>Cancelled Booking</h3>
<div class="detail-row"><span class="detail-label">Reference:</span> {booking_reference}</div>
<div class="detail-row"><span class="detail-label">Room:</span> {room_name}</div>
<div class="detail-row"><span class="detail-label">Dates:</span> {check_in_date} - {check_out_date}</div>
</div>
<p>If you have any questions or would like to make a new reservation, please contact us at {admin_email}.</p>
<p>We hope to welcome you in the future.</p>
</div>
<div class="email-footer">
<p>{site_name}<br>{site_url}</p>
</div>
</div>
</body>
</html>
HTML;
}
/**
* Template: Admin booking cancelled.
*
* @return string Template HTML.
*/
private static function template_admin_booking_cancelled(): string {
$styles = self::get_email_styles();
return <<<HTML
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>{$styles}</style>
</head>
<body>
<div class="email-container">
<div class="email-header" style="background: #d63638;">
<h1>Booking Cancelled</h1>
</div>
<div class="email-body">
<p>Booking {booking_reference} has been cancelled.</p>
<div class="booking-details">
<h3>Cancelled Booking</h3>
<div class="detail-row"><span class="detail-label">Guest:</span> {guest_name}</div>
<div class="detail-row"><span class="detail-label">Email:</span> {guest_email}</div>
<div class="detail-row"><span class="detail-label">Room:</span> {room_name}</div>
<div class="detail-row"><span class="detail-label">Dates:</span> {check_in_date} - {check_out_date}</div>
</div>
<a href="{booking_url}" class="btn">View Booking</a>
</div>
<div class="email-footer">
<p>This email was sent from {site_name}</p>
</div>
</div>
</body>
</html>
HTML;
}
}