| 1 |
<?php |
| 2 |
/** |
| 3 |
* Customer ↔ WordPress user bridge for the Native Checkout account area. |
| 4 |
* |
| 5 |
* Native checkout is guest-first: a booking only stores the customer's |
| 6 |
* name/email in post meta. To power a "My Account" dashboard we need a |
| 7 |
* real WordPress user behind each customer, so this class: |
| 8 |
* |
| 9 |
* - Auto-creates (or links) a WP user when a booking is completed, |
| 10 |
* hooked onto `eshb_native_checkout_booking_created`. |
| 11 |
* - Stores fast, queryable flat meta on each booking |
| 12 |
* (`_eshb_customer_user_id`, `_eshb_customer_email`) so the account |
| 13 |
* pages can list "my bookings" without LIKE-scanning serialized meta. |
| 14 |
* - Resolves which bookings belong to the current user and answers the |
| 15 |
* ownership question used by every account/cancellation action. |
| 16 |
* |
| 17 |
* @package EasyHotel\NativeCheckout\Account |
| 18 |
*/ |
| 19 |
|
| 20 |
if ( ! defined( 'ABSPATH' ) ) exit; |
| 21 |
|
| 22 |
class ESHB_Native_Account_Customer { |
| 23 |
|
| 24 |
/** Flat, queryable booking meta keys. */ |
| 25 |
const META_USER_ID = '_eshb_customer_user_id'; |
| 26 |
const META_EMAIL = '_eshb_customer_email'; |
| 27 |
|
| 28 |
/** Custom role assigned to checkout customers. */ |
| 29 |
const ROLE = 'eshb_customer'; |
| 30 |
|
| 31 |
public function __construct() { |
| 32 |
// Ensure the customer role exists (for installs already activated). |
| 33 |
add_action( 'init', [ $this, 'register_role' ] ); |
| 34 |
|
| 35 |
// Link / create the WP user right after a native-checkout booking |
| 36 |
// is persisted. Priority 5 so the user exists before other |
| 37 |
// booking_created listeners (e.g. emails) run. |
| 38 |
add_action( 'eshb_native_checkout_booking_created', [ $this, 'link_booking_to_user' ], 5, 4 ); |
| 39 |
} |
| 40 |
|
| 41 |
/** |
| 42 |
* Register the "Easy Hotel Customer" role if it doesn't exist yet. |
| 43 |
* Customers only need front-end read access. |
| 44 |
*/ |
| 45 |
public function register_role() { |
| 46 |
if ( null === get_role( self::ROLE ) ) { |
| 47 |
add_role( self::ROLE, __( 'Easy Hotel Customer', 'easy-hotel' ), [ 'read' => true ] ); |
| 48 |
} |
| 49 |
} |
| 50 |
|
| 51 |
/** |
| 52 |
* Ensure a WordPress user exists for the booking's customer and stamp |
| 53 |
* the booking with queryable ownership meta. |
| 54 |
* |
| 55 |
* @param int $booking_id Booking post id. |
| 56 |
* @param array $reservation Reservation payload (unused but part of the hook signature). |
| 57 |
* @param array $customer Customer details captured at checkout. |
| 58 |
* @param array $pricing Pricing payload (unused). |
| 59 |
*/ |
| 60 |
public function link_booking_to_user( $booking_id, $reservation, $customer, $pricing ) { |
| 61 |
$email = isset( $customer['email'] ) ? sanitize_email( $customer['email'] ) : ''; |
| 62 |
if ( ! $booking_id ) { |
| 63 |
return; |
| 64 |
} |
| 65 |
|
| 66 |
$user_id = $this->ensure_user( $customer ); |
| 67 |
|
| 68 |
if ( $user_id ) { |
| 69 |
update_post_meta( $booking_id, self::META_USER_ID, (int) $user_id ); |
| 70 |
// Attribute authorship too, so standard WP tooling sees the link. |
| 71 |
wp_update_post( [ 'ID' => $booking_id, 'post_author' => (int) $user_id ] ); |
| 72 |
} |
| 73 |
if ( $email ) { |
| 74 |
update_post_meta( $booking_id, self::META_EMAIL, strtolower( $email ) ); |
| 75 |
} |
| 76 |
} |
| 77 |
|
| 78 |
/** |
| 79 |
* Find or create the WordPress user for a customer array. |
| 80 |
* |
| 81 |
* Order of resolution: |
| 82 |
* 1. A logged-in user is treated as the booking owner. |
| 83 |
* 2. An existing account with the same email is reused. |
| 84 |
* 3. Otherwise a new customer account is created and notified. |
| 85 |
* |
| 86 |
* Auto-creation can be disabled via the |
| 87 |
* `eshb_native_account_auto_create_user` filter. |
| 88 |
* |
| 89 |
* @param array $customer Customer details (email, first_name, last_name). |
| 90 |
* @return int User id, or 0 when none could be resolved. |
| 91 |
*/ |
| 92 |
public function ensure_user( array $customer ) { |
| 93 |
if ( is_user_logged_in() ) { |
| 94 |
return get_current_user_id(); |
| 95 |
} |
| 96 |
|
| 97 |
$email = isset( $customer['email'] ) ? sanitize_email( $customer['email'] ) : ''; |
| 98 |
if ( ! $email || ! is_email( $email ) ) { |
| 99 |
return 0; |
| 100 |
} |
| 101 |
|
| 102 |
$existing = get_user_by( 'email', $email ); |
| 103 |
if ( $existing instanceof WP_User ) { |
| 104 |
return (int) $existing->ID; |
| 105 |
} |
| 106 |
|
| 107 |
/** |
| 108 |
* Whether to auto-create a customer account at checkout. |
| 109 |
* |
| 110 |
* @param bool $create |
| 111 |
* @param array $customer |
| 112 |
*/ |
| 113 |
if ( ! apply_filters( 'eshb_native_account_auto_create_user', true, $customer ) ) { |
| 114 |
return 0; |
| 115 |
} |
| 116 |
|
| 117 |
return $this->create_customer_user( $customer, $email ); |
| 118 |
} |
| 119 |
|
| 120 |
/** |
| 121 |
* Create a new customer WP user from checkout details. |
| 122 |
* |
| 123 |
* @return int New user id, or 0 on failure. |
| 124 |
*/ |
| 125 |
private function create_customer_user( array $customer, $email ) { |
| 126 |
$username = $this->generate_username( $email ); |
| 127 |
|
| 128 |
$userdata = [ |
| 129 |
'user_login' => $username, |
| 130 |
'user_email' => $email, |
| 131 |
'user_pass' => wp_generate_password( 24, true, true ), |
| 132 |
'first_name' => sanitize_text_field( $customer['first_name'] ?? '' ), |
| 133 |
'last_name' => sanitize_text_field( $customer['last_name'] ?? '' ), |
| 134 |
'display_name' => trim( ( $customer['first_name'] ?? '' ) . ' ' . ( $customer['last_name'] ?? '' ) ), |
| 135 |
/** |
| 136 |
* Role assigned to auto-created customers. |
| 137 |
* |
| 138 |
* @param string $role |
| 139 |
*/ |
| 140 |
'role' => apply_filters( 'eshb_native_account_customer_role', self::ROLE ), |
| 141 |
]; |
| 142 |
|
| 143 |
$user_id = wp_insert_user( $userdata ); |
| 144 |
if ( is_wp_error( $user_id ) || ! $user_id ) { |
| 145 |
return 0; |
| 146 |
} |
| 147 |
|
| 148 |
// Send WordPress' "set your password" notification so the customer |
| 149 |
// can log in to manage their booking. Wrapped so a mail failure |
| 150 |
// never breaks the checkout completion. |
| 151 |
try { |
| 152 |
wp_new_user_notification( $user_id, null, 'user' ); |
| 153 |
} catch ( \Throwable $e ) { |
| 154 |
unset( $e ); |
| 155 |
} |
| 156 |
|
| 157 |
/** |
| 158 |
* Fires after a customer account is auto-created at checkout. |
| 159 |
* |
| 160 |
* @param int $user_id |
| 161 |
* @param array $customer |
| 162 |
*/ |
| 163 |
do_action( 'eshb_native_account_user_created', $user_id, $customer ); |
| 164 |
|
| 165 |
return (int) $user_id; |
| 166 |
} |
| 167 |
|
| 168 |
/** |
| 169 |
* Build a unique, readable username from an email local-part. |
| 170 |
*/ |
| 171 |
private function generate_username( $email ) { |
| 172 |
$base = sanitize_user( current( explode( '@', $email ) ), true ); |
| 173 |
if ( empty( $base ) ) { |
| 174 |
$base = 'customer'; |
| 175 |
} |
| 176 |
$username = $base; |
| 177 |
$i = 1; |
| 178 |
while ( username_exists( $username ) ) { |
| 179 |
$username = $base . $i; |
| 180 |
$i++; |
| 181 |
} |
| 182 |
return $username; |
| 183 |
} |
| 184 |
|
| 185 |
/* ----------------------------------------------------------------- |
| 186 |
* Ownership / lookup |
| 187 |
* -------------------------------------------------------------- */ |
| 188 |
|
| 189 |
/** |
| 190 |
* Booking ids owned by a user, newest first. |
| 191 |
* |
| 192 |
* Matches on the fast flat meta first, then falls back to the email |
| 193 |
* stored inside the serialized customer meta (legacy bookings created |
| 194 |
* before this feature), back-filling the flat meta as it goes so the |
| 195 |
* slow path only ever runs once per booking. |
| 196 |
* |
| 197 |
* @param WP_User|int|null $user Defaults to the current user. |
| 198 |
* @return int[] Booking post ids. |
| 199 |
*/ |
| 200 |
public function get_user_booking_ids( $user = null ) { |
| 201 |
$user = $this->resolve_user( $user ); |
| 202 |
if ( ! $user ) { |
| 203 |
return []; |
| 204 |
} |
| 205 |
|
| 206 |
$email = strtolower( $user->user_email ); |
| 207 |
|
| 208 |
$query = new WP_Query( [ |
| 209 |
'post_type' => 'eshb_booking', |
| 210 |
'post_status' => 'any', |
| 211 |
'posts_per_page' => -1, |
| 212 |
'fields' => 'ids', |
| 213 |
'no_found_rows' => true, |
| 214 |
'orderby' => 'date', |
| 215 |
'order' => 'DESC', |
| 216 |
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query |
| 217 |
'meta_query' => [ |
| 218 |
'relation' => 'OR', |
| 219 |
[ 'key' => self::META_USER_ID, 'value' => (int) $user->ID ], |
| 220 |
[ 'key' => self::META_EMAIL, 'value' => $email ], |
| 221 |
[ |
| 222 |
'key' => 'eshb_booking_customer_details_metaboxes', |
| 223 |
'value' => '"' . $email . '"', |
| 224 |
'compare' => 'LIKE', |
| 225 |
], |
| 226 |
], |
| 227 |
] ); |
| 228 |
|
| 229 |
$ids = array_map( 'intval', $query->posts ); |
| 230 |
|
| 231 |
// Back-fill flat meta for any legacy booking matched via the |
| 232 |
// serialized-email path so future lookups hit the fast index. |
| 233 |
foreach ( $ids as $id ) { |
| 234 |
if ( '' === get_post_meta( $id, self::META_EMAIL, true ) ) { |
| 235 |
update_post_meta( $id, self::META_EMAIL, $email ); |
| 236 |
} |
| 237 |
} |
| 238 |
|
| 239 |
return $ids; |
| 240 |
} |
| 241 |
|
| 242 |
/** |
| 243 |
* Whether the given user owns the booking. Used to gate every |
| 244 |
* customer-facing booking action server-side. |
| 245 |
* |
| 246 |
* @param int $booking_id |
| 247 |
* @param WP_User|int|null $user Defaults to current user. |
| 248 |
*/ |
| 249 |
public function user_owns_booking( $booking_id, $user = null ) { |
| 250 |
$booking_id = (int) $booking_id; |
| 251 |
$user = $this->resolve_user( $user ); |
| 252 |
if ( ! $booking_id || ! $user || get_post_type( $booking_id ) !== 'eshb_booking' ) { |
| 253 |
return false; |
| 254 |
} |
| 255 |
|
| 256 |
$owner_id = (int) get_post_meta( $booking_id, self::META_USER_ID, true ); |
| 257 |
if ( $owner_id && $owner_id === (int) $user->ID ) { |
| 258 |
return true; |
| 259 |
} |
| 260 |
|
| 261 |
// Fall back to an email comparison against the authoritative |
| 262 |
// serialized customer meta (covers legacy / unlinked bookings). |
| 263 |
$customer = get_post_meta( $booking_id, 'eshb_booking_customer_details_metaboxes', true ); |
| 264 |
$email = is_array( $customer ) ? strtolower( (string) ( $customer['email'] ?? '' ) ) : ''; |
| 265 |
|
| 266 |
return $email !== '' && $email === strtolower( $user->user_email ); |
| 267 |
} |
| 268 |
|
| 269 |
/** |
| 270 |
* Normalize a user argument (id / WP_User / null) to a WP_User. |
| 271 |
* |
| 272 |
* @return WP_User|null |
| 273 |
*/ |
| 274 |
private function resolve_user( $user ) { |
| 275 |
if ( $user instanceof WP_User ) { |
| 276 |
return $user; |
| 277 |
} |
| 278 |
if ( is_numeric( $user ) && $user > 0 ) { |
| 279 |
$obj = get_user_by( 'id', (int) $user ); |
| 280 |
return $obj instanceof WP_User ? $obj : null; |
| 281 |
} |
| 282 |
if ( is_user_logged_in() ) { |
| 283 |
return wp_get_current_user(); |
| 284 |
} |
| 285 |
return null; |
| 286 |
} |
| 287 |
} |
| 288 |
|