Add guest management and GDPR privacy compliance (v0.4.0)
All checks were successful
Create Release Package / build-release (push) Successful in 1m26s

- 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>
This commit is contained in:
2026-01-31 14:59:43 +01:00
parent c66af8e299
commit aab3a4d1aa
9 changed files with 2858 additions and 71 deletions

View File

@@ -12,6 +12,7 @@ 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;
@@ -203,8 +204,8 @@ final class EmailNotifier {
* @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 );
$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 );
@@ -221,31 +222,84 @@ final class EmailNotifier {
$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' => get_post_meta( $booking_id, '_bnb_booking_guest_name', true ),
'guest_email' => get_post_meta( $booking_id, '_bnb_booking_guest_email', true ),
'guest_phone' => get_post_meta( $booking_id, '_bnb_booking_guest_phone', true ),
'guest_notes' => get_post_meta( $booking_id, '_bnb_booking_guest_notes', true ),
'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' ),
'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.
);
}

View File

@@ -16,7 +16,9 @@ use Magdev\WpBnb\Booking\EmailNotifier;
use Magdev\WpBnb\License\Manager as LicenseManager;
use Magdev\WpBnb\PostTypes\Booking;
use Magdev\WpBnb\PostTypes\Building;
use Magdev\WpBnb\PostTypes\Guest;
use Magdev\WpBnb\PostTypes\Room;
use Magdev\WpBnb\Privacy\Manager as PrivacyManager;
use Magdev\WpBnb\Pricing\Season;
use Magdev\WpBnb\Taxonomies\Amenity;
use Magdev\WpBnb\Taxonomies\RoomType;
@@ -92,6 +94,7 @@ final class Plugin {
Building::init();
Room::init();
Booking::init();
Guest::init();
}
/**
@@ -144,8 +147,12 @@ final class Plugin {
// Initialize email notifier.
EmailNotifier::init();
// Initialize privacy manager for GDPR compliance.
PrivacyManager::init();
// Register AJAX handlers.
add_action( 'wp_ajax_wp_bnb_check_availability', array( $this, 'ajax_check_availability' ) );
add_action( 'wp_ajax_wp_bnb_search_guest', array( $this, 'ajax_search_guest' ) );
}
/**
@@ -181,7 +188,7 @@ final class Plugin {
// Check if we're on plugin pages or editing our custom post types.
$is_plugin_page = strpos( $hook_suffix, 'wp-bnb' ) !== false;
$is_our_post_type = in_array( $post_type, array( Building::POST_TYPE, Room::POST_TYPE, Booking::POST_TYPE ), true );
$is_our_post_type = in_array( $post_type, array( Building::POST_TYPE, Room::POST_TYPE, Booking::POST_TYPE, Guest::POST_TYPE ), true );
$is_edit_screen = in_array( $hook_suffix, array( 'post.php', 'post-new.php' ), true );
if ( ! $is_plugin_page && ! ( $is_our_post_type && $is_edit_screen ) ) {
@@ -235,6 +242,10 @@ final class Plugin {
'nights' => __( 'nights', 'wp-bnb' ),
'night' => __( 'night', 'wp-bnb' ),
'calculating' => __( 'Calculating price...', 'wp-bnb' ),
'searchingGuests' => __( 'Searching...', 'wp-bnb' ),
'noGuestsFound' => __( 'No guests found', 'wp-bnb' ),
'selectGuest' => __( 'Select', 'wp-bnb' ),
'guestBlocked' => __( 'Blocked', 'wp-bnb' ),
),
)
);
@@ -875,6 +886,68 @@ final class Plugin {
wp_send_json_success( $result );
}
/**
* AJAX handler for searching guests by email.
*
* @return void
*/
public function ajax_search_guest(): void {
check_ajax_referer( 'wp_bnb_admin_nonce', 'nonce' );
if ( ! current_user_can( 'edit_posts' ) ) {
wp_send_json_error(
array( 'message' => __( 'You do not have permission to perform this action.', 'wp-bnb' ) )
);
}
$search = isset( $_POST['search'] ) ? sanitize_text_field( wp_unslash( $_POST['search'] ) ) : '';
if ( strlen( $search ) < 2 ) {
wp_send_json_success( array( 'guests' => array() ) );
}
// Search by email or name.
$guests = get_posts(
array(
'post_type' => Guest::POST_TYPE,
'post_status' => 'publish',
'posts_per_page' => 10,
'meta_query' => array(
'relation' => 'OR',
array(
'key' => '_bnb_guest_email',
'value' => $search,
'compare' => 'LIKE',
),
array(
'key' => '_bnb_guest_first_name',
'value' => $search,
'compare' => 'LIKE',
),
array(
'key' => '_bnb_guest_last_name',
'value' => $search,
'compare' => 'LIKE',
),
),
)
);
$results = array();
foreach ( $guests as $guest ) {
$status = get_post_meta( $guest->ID, '_bnb_guest_status', true ) ?: 'active';
$results[] = array(
'id' => $guest->ID,
'name' => Guest::get_full_name( $guest->ID ),
'email' => get_post_meta( $guest->ID, '_bnb_guest_email', true ),
'phone' => get_post_meta( $guest->ID, '_bnb_guest_phone', true ),
'status' => $status,
);
}
wp_send_json_success( array( 'guests' => $results ) );
}
/**
* Get Twig environment.
*

View File

@@ -280,41 +280,97 @@ final class Booking {
* @return void
*/
public static function render_guest_meta_box( \WP_Post $post ): void {
$guest_id = get_post_meta( $post->ID, self::META_PREFIX . 'guest_id', true );
$guest_name = get_post_meta( $post->ID, self::META_PREFIX . 'guest_name', true );
$guest_email = get_post_meta( $post->ID, self::META_PREFIX . 'guest_email', true );
$guest_phone = get_post_meta( $post->ID, self::META_PREFIX . 'guest_phone', true );
$adults = get_post_meta( $post->ID, self::META_PREFIX . 'adults', true );
$children = get_post_meta( $post->ID, self::META_PREFIX . 'children', true );
$guest_notes = get_post_meta( $post->ID, self::META_PREFIX . 'guest_notes', true );
// If guest_id exists, get guest data from Guest CPT.
$linked_guest = null;
if ( $guest_id ) {
$linked_guest = get_post( $guest_id );
if ( $linked_guest && Guest::POST_TYPE === $linked_guest->post_type ) {
$guest_name = Guest::get_full_name( $guest_id );
$guest_email = get_post_meta( $guest_id, '_bnb_guest_email', true );
$guest_phone = get_post_meta( $guest_id, '_bnb_guest_phone', true );
} else {
$linked_guest = null;
$guest_id = '';
}
}
?>
<input type="hidden" id="bnb_booking_guest_id" name="bnb_booking_guest_id" value="<?php echo esc_attr( $guest_id ); ?>">
<?php if ( $linked_guest ) : ?>
<div id="bnb-linked-guest-info" class="bnb-linked-guest">
<p>
<span class="dashicons dashicons-admin-users"></span>
<strong><?php echo esc_html( $linked_guest->post_title ); ?></strong>
<a href="<?php echo esc_url( get_edit_post_link( $guest_id ) ); ?>" target="_blank" class="button button-small">
<?php esc_html_e( 'View Guest Profile', 'wp-bnb' ); ?>
</a>
<button type="button" id="bnb-unlink-guest" class="button button-small button-link-delete">
<?php esc_html_e( 'Unlink', 'wp-bnb' ); ?>
</button>
</p>
<?php if ( $guest_email ) : ?>
<p><small><?php echo esc_html( $guest_email ); ?></small></p>
<?php endif; ?>
</div>
<?php endif; ?>
<div id="bnb-guest-search-container" class="bnb-guest-search" <?php echo $linked_guest ? 'style="display:none;"' : ''; ?>>
<table class="form-table">
<tr>
<th scope="row">
<label for="bnb_booking_guest_search"><?php esc_html_e( 'Search Guest', 'wp-bnb' ); ?></label>
</th>
<td>
<input type="text" id="bnb_booking_guest_search" class="regular-text"
placeholder="<?php esc_attr_e( 'Search by email...', 'wp-bnb' ); ?>">
<p class="description"><?php esc_html_e( 'Search for existing guest or enter details below.', 'wp-bnb' ); ?></p>
<div id="bnb-guest-search-results" class="bnb-guest-search-results" style="display:none;"></div>
</td>
</tr>
</table>
</div>
<div id="bnb-guest-fields-container" <?php echo $linked_guest ? 'style="display:none;"' : ''; ?>>
<table class="form-table">
<tr>
<th scope="row">
<label for="bnb_booking_guest_name"><?php esc_html_e( 'Guest Name', 'wp-bnb' ); ?> <span class="required">*</span></label>
</th>
<td>
<input type="text" id="bnb_booking_guest_name" name="bnb_booking_guest_name"
value="<?php echo esc_attr( $guest_name ); ?>" class="regular-text" <?php echo $linked_guest ? '' : 'required'; ?>>
</td>
</tr>
<tr>
<th scope="row">
<label for="bnb_booking_guest_email"><?php esc_html_e( 'Email', 'wp-bnb' ); ?></label>
</th>
<td>
<input type="email" id="bnb_booking_guest_email" name="bnb_booking_guest_email"
value="<?php echo esc_attr( $guest_email ); ?>" class="regular-text">
</td>
</tr>
<tr>
<th scope="row">
<label for="bnb_booking_guest_phone"><?php esc_html_e( 'Phone', 'wp-bnb' ); ?></label>
</th>
<td>
<input type="tel" id="bnb_booking_guest_phone" name="bnb_booking_guest_phone"
value="<?php echo esc_attr( $guest_phone ); ?>" class="regular-text">
</td>
</tr>
</table>
</div>
<table class="form-table">
<tr>
<th scope="row">
<label for="bnb_booking_guest_name"><?php esc_html_e( 'Guest Name', 'wp-bnb' ); ?> <span class="required">*</span></label>
</th>
<td>
<input type="text" id="bnb_booking_guest_name" name="bnb_booking_guest_name"
value="<?php echo esc_attr( $guest_name ); ?>" class="regular-text" required>
</td>
</tr>
<tr>
<th scope="row">
<label for="bnb_booking_guest_email"><?php esc_html_e( 'Email', 'wp-bnb' ); ?></label>
</th>
<td>
<input type="email" id="bnb_booking_guest_email" name="bnb_booking_guest_email"
value="<?php echo esc_attr( $guest_email ); ?>" class="regular-text">
</td>
</tr>
<tr>
<th scope="row">
<label for="bnb_booking_guest_phone"><?php esc_html_e( 'Phone', 'wp-bnb' ); ?></label>
</th>
<td>
<input type="tel" id="bnb_booking_guest_phone" name="bnb_booking_guest_phone"
value="<?php echo esc_attr( $guest_phone ); ?>" class="regular-text">
</td>
</tr>
<tr>
<th scope="row">
<?php esc_html_e( 'Guests', 'wp-bnb' ); ?>
@@ -535,21 +591,48 @@ final class Booking {
}
}
// Guest text fields.
$guest_fields = array( 'guest_name', 'guest_email', 'guest_phone', 'guest_notes' );
foreach ( $guest_fields as $field ) {
$key = 'bnb_booking_' . $field;
if ( isset( $_POST[ $key ] ) ) {
$value = wp_unslash( $_POST[ $key ] );
if ( 'guest_email' === $field ) {
$value = sanitize_email( $value );
} elseif ( 'guest_notes' === $field ) {
$value = sanitize_textarea_field( $value );
} else {
$value = sanitize_text_field( $value );
}
update_post_meta( $post_id, self::META_PREFIX . $field, $value );
// Guest ID (linked guest record).
$guest_id = isset( $_POST['bnb_booking_guest_id'] ) ? absint( $_POST['bnb_booking_guest_id'] ) : 0;
if ( $guest_id ) {
// Verify guest exists.
$guest = get_post( $guest_id );
if ( $guest && Guest::POST_TYPE === $guest->post_type ) {
update_post_meta( $post_id, self::META_PREFIX . 'guest_id', $guest_id );
// Sync guest data from Guest CPT for searching/display purposes.
update_post_meta( $post_id, self::META_PREFIX . 'guest_name', Guest::get_full_name( $guest_id ) );
update_post_meta( $post_id, self::META_PREFIX . 'guest_email', get_post_meta( $guest_id, '_bnb_guest_email', true ) );
update_post_meta( $post_id, self::META_PREFIX . 'guest_phone', get_post_meta( $guest_id, '_bnb_guest_phone', true ) );
} else {
delete_post_meta( $post_id, self::META_PREFIX . 'guest_id' );
}
} else {
delete_post_meta( $post_id, self::META_PREFIX . 'guest_id' );
// Guest text fields (only save if no guest_id).
$guest_fields = array( 'guest_name', 'guest_email', 'guest_phone', 'guest_notes' );
foreach ( $guest_fields as $field ) {
$key = 'bnb_booking_' . $field;
if ( isset( $_POST[ $key ] ) ) {
$value = wp_unslash( $_POST[ $key ] );
if ( 'guest_email' === $field ) {
$value = sanitize_email( $value );
} elseif ( 'guest_notes' === $field ) {
$value = sanitize_textarea_field( $value );
} else {
$value = sanitize_text_field( $value );
}
update_post_meta( $post_id, self::META_PREFIX . $field, $value );
}
}
}
// Guest notes are always saved (per-booking notes).
if ( isset( $_POST['bnb_booking_guest_notes'] ) ) {
update_post_meta(
$post_id,
self::META_PREFIX . 'guest_notes',
sanitize_textarea_field( wp_unslash( $_POST['bnb_booking_guest_notes'] ) )
);
}
// Guest counts.
@@ -664,10 +747,21 @@ final class Booking {
break;
case 'guest':
$guest_id = get_post_meta( $post_id, self::META_PREFIX . 'guest_id', true );
$guest_name = get_post_meta( $post_id, self::META_PREFIX . 'guest_name', true );
$guest_email = get_post_meta( $post_id, self::META_PREFIX . 'guest_email', true );
if ( $guest_name ) {
echo esc_html( $guest_name );
if ( $guest_id ) {
// Linked guest - show link to guest profile.
printf(
'<a href="%s">%s</a>',
esc_url( get_edit_post_link( $guest_id ) ),
esc_html( $guest_name )
);
echo ' <span class="dashicons dashicons-admin-users" style="font-size: 14px; vertical-align: middle;" title="' . esc_attr__( 'Linked Guest', 'wp-bnb' ) . '"></span>';
} else {
echo esc_html( $guest_name );
}
if ( $guest_email ) {
echo '<br><small><a href="mailto:' . esc_attr( $guest_email ) . '">' . esc_html( $guest_email ) . '</a></small>';
}
@@ -1071,6 +1165,47 @@ final class Booking {
return Room::get_building( $room->ID );
}
/**
* Get guest for a booking.
*
* Returns the linked Guest post if guest_id exists, or a stdClass object
* with guest data from booking meta for backward compatibility.
*
* @param int $booking_id Booking post ID.
* @return \WP_Post|\stdClass|null Guest post, virtual guest object, or null.
*/
public static function get_guest( int $booking_id ) {
$guest_id = get_post_meta( $booking_id, self::META_PREFIX . 'guest_id', true );
// If linked to Guest CPT, return the guest post.
if ( $guest_id ) {
$guest = get_post( $guest_id );
if ( $guest && Guest::POST_TYPE === $guest->post_type ) {
return $guest;
}
}
// Otherwise, create a virtual guest object from booking meta.
$guest_name = get_post_meta( $booking_id, self::META_PREFIX . 'guest_name', true );
$guest_email = get_post_meta( $booking_id, self::META_PREFIX . 'guest_email', true );
$guest_phone = get_post_meta( $booking_id, self::META_PREFIX . 'guest_phone', true );
if ( ! $guest_name && ! $guest_email ) {
return null;
}
// Return virtual guest object for backward compatibility.
$virtual_guest = new \stdClass();
$virtual_guest->ID = 0;
$virtual_guest->post_type = 'virtual_guest';
$virtual_guest->post_title = $guest_name ?: '';
$virtual_guest->name = $guest_name ?: '';
$virtual_guest->email = $guest_email ?: '';
$virtual_guest->phone = $guest_phone ?: '';
return $virtual_guest;
}
/**
* Get all bookings for a room.
*

1086
src/PostTypes/Guest.php Normal file

File diff suppressed because it is too large Load Diff

800
src/Privacy/Manager.php Normal file
View File

@@ -0,0 +1,800 @@
<?php
/**
* Privacy Manager for GDPR compliance.
*
* Handles personal data export and erasure for WordPress privacy tools.
*
* @package Magdev\WpBnb\Privacy
*/
declare( strict_types=1 );
namespace Magdev\WpBnb\Privacy;
use Magdev\WpBnb\PostTypes\Booking;
use Magdev\WpBnb\PostTypes\Guest;
/**
* Privacy Manager class for GDPR compliance.
*/
final class Manager {
/**
* Manager instance.
*
* @var Manager|null
*/
private static ?Manager $instance = null;
/**
* Get manager instance.
*
* @return Manager
*/
public static function get_instance(): Manager {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Private constructor to enforce singleton.
*/
private function __construct() {
$this->init_hooks();
}
/**
* Initialize hooks.
*
* @return void
*/
public static function init(): void {
self::get_instance();
}
/**
* Initialize WordPress hooks.
*
* @return void
*/
private function init_hooks(): void {
// Register personal data exporters.
add_filter( 'wp_privacy_personal_data_exporters', array( $this, 'register_exporters' ) );
// Register personal data erasers.
add_filter( 'wp_privacy_personal_data_erasers', array( $this, 'register_erasers' ) );
// Add privacy policy content suggestion.
add_action( 'admin_init', array( $this, 'add_privacy_policy_content' ) );
}
/**
* Register personal data exporters.
*
* @param array $exporters Existing exporters.
* @return array
*/
public function register_exporters( array $exporters ): array {
$exporters['wp-bnb-guest'] = array(
'exporter_friendly_name' => __( 'WP BnB Guest Profile', 'wp-bnb' ),
'callback' => array( $this, 'export_guest_data' ),
);
$exporters['wp-bnb-bookings'] = array(
'exporter_friendly_name' => __( 'WP BnB Booking History', 'wp-bnb' ),
'callback' => array( $this, 'export_booking_data' ),
);
return $exporters;
}
/**
* Register personal data erasers.
*
* @param array $erasers Existing erasers.
* @return array
*/
public function register_erasers( array $erasers ): array {
$erasers['wp-bnb-guest'] = array(
'eraser_friendly_name' => __( 'WP BnB Guest Profile', 'wp-bnb' ),
'callback' => array( $this, 'erase_guest_data' ),
);
$erasers['wp-bnb-bookings'] = array(
'eraser_friendly_name' => __( 'WP BnB Booking History', 'wp-bnb' ),
'callback' => array( $this, 'erase_booking_data' ),
);
return $erasers;
}
/**
* Export guest profile data.
*
* @param string $email Email address to export data for.
* @param int $page Page number for pagination.
* @return array Export data array.
*/
public function export_guest_data( string $email, int $page = 1 ): array {
$export_items = array();
// Find guest by email.
$guest = Guest::get_by_email( $email );
if ( $guest ) {
$data = array();
// Basic information.
$first_name = get_post_meta( $guest->ID, '_bnb_guest_first_name', true );
$last_name = get_post_meta( $guest->ID, '_bnb_guest_last_name', true );
if ( $first_name ) {
$data[] = array(
'name' => __( 'First Name', 'wp-bnb' ),
'value' => $first_name,
);
}
if ( $last_name ) {
$data[] = array(
'name' => __( 'Last Name', 'wp-bnb' ),
'value' => $last_name,
);
}
$data[] = array(
'name' => __( 'Email', 'wp-bnb' ),
'value' => get_post_meta( $guest->ID, '_bnb_guest_email', true ),
);
$phone = get_post_meta( $guest->ID, '_bnb_guest_phone', true );
if ( $phone ) {
$data[] = array(
'name' => __( 'Phone', 'wp-bnb' ),
'value' => $phone,
);
}
// Address.
$street = get_post_meta( $guest->ID, '_bnb_guest_street', true );
if ( $street ) {
$data[] = array(
'name' => __( 'Street Address', 'wp-bnb' ),
'value' => $street,
);
}
$city = get_post_meta( $guest->ID, '_bnb_guest_city', true );
if ( $city ) {
$data[] = array(
'name' => __( 'City', 'wp-bnb' ),
'value' => $city,
);
}
$postal_code = get_post_meta( $guest->ID, '_bnb_guest_postal_code', true );
if ( $postal_code ) {
$data[] = array(
'name' => __( 'Postal Code', 'wp-bnb' ),
'value' => $postal_code,
);
}
$country = get_post_meta( $guest->ID, '_bnb_guest_country', true );
if ( $country ) {
$data[] = array(
'name' => __( 'Country', 'wp-bnb' ),
'value' => $country,
);
}
// Personal details.
$nationality = get_post_meta( $guest->ID, '_bnb_guest_nationality', true );
if ( $nationality ) {
$data[] = array(
'name' => __( 'Nationality', 'wp-bnb' ),
'value' => $nationality,
);
}
$date_of_birth = get_post_meta( $guest->ID, '_bnb_guest_date_of_birth', true );
if ( $date_of_birth ) {
$data[] = array(
'name' => __( 'Date of Birth', 'wp-bnb' ),
'value' => $date_of_birth,
);
}
// ID information (sensitive).
$id_type = get_post_meta( $guest->ID, '_bnb_guest_id_type', true );
if ( $id_type ) {
$data[] = array(
'name' => __( 'ID Type', 'wp-bnb' ),
'value' => $id_type,
);
}
$id_number = get_post_meta( $guest->ID, '_bnb_guest_id_number', true );
if ( $id_number ) {
$data[] = array(
'name' => __( 'ID Number', 'wp-bnb' ),
'value' => $id_number,
);
}
$id_expiry = get_post_meta( $guest->ID, '_bnb_guest_id_expiry', true );
if ( $id_expiry ) {
$data[] = array(
'name' => __( 'ID Expiry Date', 'wp-bnb' ),
'value' => $id_expiry,
);
}
// Consent information.
$consent_data = get_post_meta( $guest->ID, '_bnb_guest_consent_data', true );
$data[] = array(
'name' => __( 'Data Processing Consent', 'wp-bnb' ),
'value' => $consent_data ? __( 'Yes', 'wp-bnb' ) : __( 'No', 'wp-bnb' ),
);
$consent_marketing = get_post_meta( $guest->ID, '_bnb_guest_consent_marketing', true );
$data[] = array(
'name' => __( 'Marketing Consent', 'wp-bnb' ),
'value' => $consent_marketing ? __( 'Yes', 'wp-bnb' ) : __( 'No', 'wp-bnb' ),
);
$consent_date = get_post_meta( $guest->ID, '_bnb_guest_consent_date', true );
if ( $consent_date ) {
$data[] = array(
'name' => __( 'Consent Date', 'wp-bnb' ),
'value' => $consent_date,
);
}
// Notes and preferences.
$preferences = get_post_meta( $guest->ID, '_bnb_guest_preferences', true );
if ( $preferences ) {
$data[] = array(
'name' => __( 'Guest Preferences', 'wp-bnb' ),
'value' => $preferences,
);
}
if ( ! empty( $data ) ) {
$export_items[] = array(
'group_id' => 'wp-bnb-guest',
'group_label' => __( 'Guest Profile', 'wp-bnb' ),
'group_description' => __( 'Your guest profile information stored by WP BnB.', 'wp-bnb' ),
'item_id' => 'guest-' . $guest->ID,
'data' => $data,
);
}
}
return array(
'data' => $export_items,
'done' => true,
);
}
/**
* Export booking history data.
*
* @param string $email Email address to export data for.
* @param int $page Page number for pagination.
* @return array Export data array.
*/
public function export_booking_data( string $email, int $page = 1 ): array {
$export_items = array();
$per_page = 20;
$offset = ( $page - 1 ) * $per_page;
// Find bookings by email (both direct and through guest_id).
$bookings = get_posts(
array(
'post_type' => Booking::POST_TYPE,
'post_status' => 'any',
'posts_per_page' => $per_page,
'offset' => $offset,
'meta_query' => array(
'relation' => 'OR',
array(
'key' => '_bnb_booking_guest_email',
'value' => $email,
),
),
)
);
// Also check via guest_id.
$guest = Guest::get_by_email( $email );
if ( $guest ) {
$bookings_by_id = get_posts(
array(
'post_type' => Booking::POST_TYPE,
'post_status' => 'any',
'posts_per_page' => $per_page,
'meta_query' => array(
array(
'key' => '_bnb_booking_guest_id',
'value' => $guest->ID,
),
),
)
);
// Merge and dedupe.
$existing_ids = wp_list_pluck( $bookings, 'ID' );
foreach ( $bookings_by_id as $booking ) {
if ( ! in_array( $booking->ID, $existing_ids, true ) ) {
$bookings[] = $booking;
}
}
}
foreach ( $bookings as $booking ) {
$data = array();
$reference = get_post_meta( $booking->ID, '_bnb_booking_reference', true );
if ( ! $reference ) {
$reference = 'BNB-' . $booking->ID;
}
$data[] = array(
'name' => __( 'Booking Reference', 'wp-bnb' ),
'value' => $reference,
);
$room_id = get_post_meta( $booking->ID, '_bnb_booking_room_id', true );
if ( $room_id ) {
$room = get_post( $room_id );
if ( $room ) {
$data[] = array(
'name' => __( 'Room', 'wp-bnb' ),
'value' => $room->post_title,
);
}
}
$check_in = get_post_meta( $booking->ID, '_bnb_booking_check_in', true );
if ( $check_in ) {
$data[] = array(
'name' => __( 'Check-in Date', 'wp-bnb' ),
'value' => $check_in,
);
}
$check_out = get_post_meta( $booking->ID, '_bnb_booking_check_out', true );
if ( $check_out ) {
$data[] = array(
'name' => __( 'Check-out Date', 'wp-bnb' ),
'value' => $check_out,
);
}
$status = get_post_meta( $booking->ID, '_bnb_booking_status', true );
if ( $status ) {
$statuses = Booking::get_booking_statuses();
$data[] = array(
'name' => __( 'Status', 'wp-bnb' ),
'value' => $statuses[ $status ] ?? $status,
);
}
$adults = get_post_meta( $booking->ID, '_bnb_booking_adults', true );
if ( $adults ) {
$data[] = array(
'name' => __( 'Adults', 'wp-bnb' ),
'value' => $adults,
);
}
$children = get_post_meta( $booking->ID, '_bnb_booking_children', true );
if ( $children ) {
$data[] = array(
'name' => __( 'Children', 'wp-bnb' ),
'value' => $children,
);
}
$price = get_post_meta( $booking->ID, '_bnb_booking_calculated_price', true );
if ( $price ) {
$currency = get_option( 'wp_bnb_currency', 'CHF' );
$data[] = array(
'name' => __( 'Total Price', 'wp-bnb' ),
'value' => number_format( (float) $price, 2 ) . ' ' . $currency,
);
}
$guest_notes = get_post_meta( $booking->ID, '_bnb_booking_guest_notes', true );
if ( $guest_notes ) {
$data[] = array(
'name' => __( 'Guest Notes', 'wp-bnb' ),
'value' => $guest_notes,
);
}
if ( ! empty( $data ) ) {
$export_items[] = array(
'group_id' => 'wp-bnb-bookings',
'group_label' => __( 'Booking History', 'wp-bnb' ),
'group_description' => __( 'Your booking history with WP BnB.', 'wp-bnb' ),
'item_id' => 'booking-' . $booking->ID,
'data' => $data,
);
}
}
// Check if there are more bookings.
$total_bookings = $this->count_bookings_by_email( $email );
$done = ( $offset + $per_page ) >= $total_bookings;
return array(
'data' => $export_items,
'done' => $done,
);
}
/**
* Erase guest profile data.
*
* @param string $email Email address to erase data for.
* @param int $page Page number for pagination.
* @return array Erasure result array.
*/
public function erase_guest_data( string $email, int $page = 1 ): array {
$items_removed = 0;
$items_retained = 0;
$messages = array();
// Find guest by email.
$guest = Guest::get_by_email( $email );
if ( $guest ) {
// Check if guest has active bookings.
$active_bookings = get_posts(
array(
'post_type' => Booking::POST_TYPE,
'post_status' => 'publish',
'posts_per_page' => 1,
'meta_query' => array(
'relation' => 'AND',
array(
'relation' => 'OR',
array(
'key' => '_bnb_booking_guest_id',
'value' => $guest->ID,
),
array(
'key' => '_bnb_booking_guest_email',
'value' => $email,
),
),
array(
'key' => '_bnb_booking_status',
'value' => array( 'pending', 'confirmed', 'checked_in' ),
'compare' => 'IN',
),
),
)
);
if ( ! empty( $active_bookings ) ) {
// Cannot delete - has active bookings.
$messages[] = __( 'Guest profile retained due to active bookings.', 'wp-bnb' );
$items_retained = 1;
} else {
// Anonymize the guest profile instead of deleting.
$this->anonymize_guest( $guest->ID );
$items_removed = 1;
$messages[] = __( 'Guest profile anonymized.', 'wp-bnb' );
}
}
return array(
'items_removed' => $items_removed,
'items_retained' => $items_retained,
'messages' => $messages,
'done' => true,
);
}
/**
* Erase booking data.
*
* @param string $email Email address to erase data for.
* @param int $page Page number for pagination.
* @return array Erasure result array.
*/
public function erase_booking_data( string $email, int $page = 1 ): array {
$items_removed = 0;
$items_retained = 0;
$messages = array();
$per_page = 20;
// Find completed bookings (can be anonymized).
$bookings = get_posts(
array(
'post_type' => Booking::POST_TYPE,
'post_status' => 'any',
'posts_per_page' => $per_page,
'meta_query' => array(
'relation' => 'AND',
array(
'key' => '_bnb_booking_guest_email',
'value' => $email,
),
array(
'key' => '_bnb_booking_status',
'value' => array( 'checked_out', 'cancelled' ),
'compare' => 'IN',
),
),
)
);
// Also find by guest_id.
$guest = Guest::get_by_email( $email );
if ( $guest ) {
$more_bookings = get_posts(
array(
'post_type' => Booking::POST_TYPE,
'post_status' => 'any',
'posts_per_page' => $per_page,
'meta_query' => array(
'relation' => 'AND',
array(
'key' => '_bnb_booking_guest_id',
'value' => $guest->ID,
),
array(
'key' => '_bnb_booking_status',
'value' => array( 'checked_out', 'cancelled' ),
'compare' => 'IN',
),
),
)
);
$existing_ids = wp_list_pluck( $bookings, 'ID' );
foreach ( $more_bookings as $booking ) {
if ( ! in_array( $booking->ID, $existing_ids, true ) ) {
$bookings[] = $booking;
}
}
}
foreach ( $bookings as $booking ) {
$this->anonymize_booking( $booking->ID );
++$items_removed;
}
// Check for active bookings that can't be erased.
$active_bookings = get_posts(
array(
'post_type' => Booking::POST_TYPE,
'post_status' => 'publish',
'posts_per_page' => -1,
'fields' => 'ids',
'meta_query' => array(
'relation' => 'AND',
array(
'key' => '_bnb_booking_guest_email',
'value' => $email,
),
array(
'key' => '_bnb_booking_status',
'value' => array( 'pending', 'confirmed', 'checked_in' ),
'compare' => 'IN',
),
),
)
);
$items_retained = count( $active_bookings );
if ( $items_retained > 0 ) {
$messages[] = sprintf(
/* translators: %d: Number of bookings */
_n(
'%d booking retained due to active status.',
'%d bookings retained due to active status.',
$items_retained,
'wp-bnb'
),
$items_retained
);
}
if ( $items_removed > 0 ) {
$messages[] = sprintf(
/* translators: %d: Number of bookings */
_n(
'%d booking anonymized.',
'%d bookings anonymized.',
$items_removed,
'wp-bnb'
),
$items_removed
);
}
return array(
'items_removed' => $items_removed,
'items_retained' => $items_retained,
'messages' => $messages,
'done' => true,
);
}
/**
* Anonymize a guest record.
*
* @param int $guest_id Guest post ID.
* @return bool True on success.
*/
public function anonymize_guest( int $guest_id ): bool {
$anonymized = __( '[Deleted]', 'wp-bnb' );
// Update post title.
wp_update_post(
array(
'ID' => $guest_id,
'post_title' => $anonymized,
)
);
// Anonymize personal data.
update_post_meta( $guest_id, '_bnb_guest_first_name', $anonymized );
update_post_meta( $guest_id, '_bnb_guest_last_name', '' );
update_post_meta( $guest_id, '_bnb_guest_email', 'deleted-' . $guest_id . '@anonymized.local' );
update_post_meta( $guest_id, '_bnb_guest_phone', '' );
update_post_meta( $guest_id, '_bnb_guest_street', '' );
update_post_meta( $guest_id, '_bnb_guest_city', '' );
update_post_meta( $guest_id, '_bnb_guest_postal_code', '' );
update_post_meta( $guest_id, '_bnb_guest_country', '' );
update_post_meta( $guest_id, '_bnb_guest_nationality', '' );
update_post_meta( $guest_id, '_bnb_guest_date_of_birth', '' );
update_post_meta( $guest_id, '_bnb_guest_id_type', '' );
update_post_meta( $guest_id, '_bnb_guest_id_number', '' );
update_post_meta( $guest_id, '_bnb_guest_id_expiry', '' );
update_post_meta( $guest_id, '_bnb_guest_preferences', '' );
update_post_meta( $guest_id, '_bnb_guest_notes', '' );
update_post_meta( $guest_id, '_bnb_guest_status', 'inactive' );
return true;
}
/**
* Anonymize a booking record.
*
* @param int $booking_id Booking post ID.
* @return bool True on success.
*/
public function anonymize_booking( int $booking_id ): bool {
$anonymized = __( '[Deleted]', 'wp-bnb' );
// Remove guest reference.
delete_post_meta( $booking_id, '_bnb_booking_guest_id' );
// Anonymize guest data stored in booking.
update_post_meta( $booking_id, '_bnb_booking_guest_name', $anonymized );
update_post_meta( $booking_id, '_bnb_booking_guest_email', '' );
update_post_meta( $booking_id, '_bnb_booking_guest_phone', '' );
update_post_meta( $booking_id, '_bnb_booking_guest_notes', '' );
return true;
}
/**
* Count bookings by email.
*
* @param string $email Email address.
* @return int Count of bookings.
*/
private function count_bookings_by_email( string $email ): int {
$count = 0;
// Direct email match.
$direct = get_posts(
array(
'post_type' => Booking::POST_TYPE,
'post_status' => 'any',
'posts_per_page' => -1,
'fields' => 'ids',
'meta_query' => array(
array(
'key' => '_bnb_booking_guest_email',
'value' => $email,
),
),
)
);
$count += count( $direct );
// Guest ID match.
$guest = Guest::get_by_email( $email );
if ( $guest ) {
$by_guest_id = get_posts(
array(
'post_type' => Booking::POST_TYPE,
'post_status' => 'any',
'posts_per_page' => -1,
'fields' => 'ids',
'post__not_in' => $direct,
'meta_query' => array(
array(
'key' => '_bnb_booking_guest_id',
'value' => $guest->ID,
),
),
)
);
$count += count( $by_guest_id );
}
return $count;
}
/**
* Add privacy policy content suggestion.
*
* @return void
*/
public function add_privacy_policy_content(): void {
if ( ! function_exists( 'wp_add_privacy_policy_content' ) ) {
return;
}
$content = sprintf(
'<h2>%s</h2>
<p>%s</p>
<h3>%s</h3>
<p>%s</p>
<ul>
<li>%s</li>
<li>%s</li>
<li>%s</li>
<li>%s</li>
<li>%s</li>
</ul>
<h3>%s</h3>
<p>%s</p>
<ul>
<li>%s</li>
<li>%s</li>
<li>%s</li>
</ul>
<h3>%s</h3>
<p>%s</p>
<h3>%s</h3>
<p>%s</p>',
__( 'Accommodation Booking', 'wp-bnb' ),
__( 'When you make a booking with us, we collect and process the following personal data to fulfill your reservation and comply with legal requirements.', 'wp-bnb' ),
__( 'What personal data we collect', 'wp-bnb' ),
__( 'We collect the following information when you make a booking:', 'wp-bnb' ),
__( 'Name and contact information (email, phone)', 'wp-bnb' ),
__( 'Address for billing and guest registration', 'wp-bnb' ),
__( 'Identity document information (as required by local regulations)', 'wp-bnb' ),
__( 'Booking details (dates, room preferences, special requests)', 'wp-bnb' ),
__( 'Payment information (processed securely by payment providers)', 'wp-bnb' ),
__( 'Why we collect this data', 'wp-bnb' ),
__( 'We use your personal data for the following purposes:', 'wp-bnb' ),
__( 'Processing and managing your booking', 'wp-bnb' ),
__( 'Communicating with you about your reservation', 'wp-bnb' ),
__( 'Complying with legal guest registration requirements', 'wp-bnb' ),
__( 'How long we retain your data', 'wp-bnb' ),
__( 'We retain your booking data for the period required by law for guest registration and accounting purposes, typically 10 years. After this period, your data will be anonymized or deleted.', 'wp-bnb' ),
__( 'Your rights', 'wp-bnb' ),
__( 'You have the right to access, correct, or request deletion of your personal data. To exercise these rights, please contact us using the information provided on this website. Note that some data may need to be retained for legal compliance purposes.', 'wp-bnb' )
);
wp_add_privacy_policy_content( 'WP BnB', wp_kses_post( $content ) );
}
}