'', 'customer' => '', 'appointment' => '', 'appointment_name' => '', 'staff' => '', 'speaker' => '', 'event' => '', 'location' => '', 'description' => '', 'location_type' => '', 'payment_method' => '', 'payment_status' => '', 'payment_details' => '', 'order_total' => '', 'start_date' => '', 'end_date' => '', 'start_time' => '', 'end_time' => '', 'calendar_event' => '', 'random_id' => '', 'seats' => '', 'guests' => '', 'zoom' => '', 'custom_form_data' => '', 'timezone' => '', 'recurrence' => [], 'parent' => 0, 'cancel_reason' => '', 'custom_location_url' => '', 'security_token' => '' ]; /** * Booking Constructor * * @return void */ public function __construct( $booking = 0 ) { if ( $booking instanceof self ) { $this->set_id( $booking->get_id() ); } elseif ( ! empty( $booking->ID ) ) { $this->set_id( $booking->ID ); } elseif ( is_numeric( $booking ) && $booking > 0 ) { $this->set_id( $booking ); } } /** * Get id * * @return integer */ public function get_id() { return $this->id; } /** * Get customer id * * @return integer */ public function get_customer_id() { return $this->get_prop( 'customer' ); } /** * Get attendee id * * @return integer */ public function get_attendees() { $attendees = $this->get_prop( 'attendees' ); return $attendees ? $attendees : []; } /** * Get custom url * * @return string */ public function get_custom_location_url() { $custom_url = $this->get_prop( 'custom_location_url' ); return $custom_url ? $custom_url : ''; } /** * Get staff for the current booking * * @return integer */ public function get_staff_id() { return $this->get_prop( 'staff' ); } /** * Get speaker id * * @return integer */ public function get_speaker_id() { return $this->get_prop( 'speaker' ); } /** * Get booking type * * @return string */ public function get_type() { return $this->get_prop( 'type' ); } /** * Get event id * * @return integer */ public function get_event_id() { return $this->get_prop( 'event' ); } /** * Get customer * * @return User Object */ public function get_customer() { return get_userdata( $this->get_customer_id() ); } /** * Get seats of a booking * * @return array */ public function get_seat() { return $this->get_prop( 'seats' ); } /** * Get guests * * @return array */ public function get_guests() { return $this->get_prop( 'guests' ); } /** * Get custom fields * * @return array */ public function get_custom_fields() { return $this->get_prop( 'custom_form_data' ); } /** * Get billing first name * * @return string */ public function get_billing_first_name() { return $this->get_prop( 'first_name' ); } /** * Get location * * @return string */ public function get_location() { return $this->get_prop( 'location' ); } /** * Get location type * * @return string */ public function get_location_type() { return $this->get_prop( 'location_type' ); } /** * Get booking time * * @return string */ public function get_booking_time() { return $this->get_prop( 'booking_time' ); } /** * Get last name * * @return string */ public function get_billing_last_name() { return $this->get_prop( 'last_name' ); } /** * Get email * * @return string */ public function get_billing_email() { return $this->get_prop( 'email' ); } /** * Get billing phone * * @return string */ public function get_billing_phone() { return $this->get_prop( 'phone' ); } /** * Get timezone * * @return string */ public function get_timezone() { return $this->get_prop( 'timezone' ); } /** * Get description * * @return string */ public function get_description() { return $this->get_prop( 'description' ); } /** * Get billing address * * @param string $adress_type Booking billing address * * @return string */ public function get_billing_address( $adress_type = 'address_1' ) { return $this->get_prop( $adress_type ); } /** * Get billing city * * @return string */ public function get_billing_city() { return $this->get_prop( 'city' ); } /** * Get billing state * * @return string */ public function get_billing_state() { return $this->get_prop( 'state' ); } /** * Get billing post code * * @return string */ public function get_billing_post_code() { return $this->get_prop( 'post_code' ); } /** * Get billing * * @return string */ public function get_billing_country() { return $this->get_prop( 'country' ); } /** * Get payment method * * @return string */ public function get_payment_method() { return $this->get_prop( 'payment_method' ); } /** * Get payment status * * @return string */ public function get_payment_status() { return $this->get_prop( 'payment_status' ); } /** * Get appointment * * @return Appointment */ public function get_appointment() { return $this->get_prop( 'appointment' ); } /** * Get appointment name * * @return string */ public function get_appointment_name() { return $this->get_prop( 'appointment_name' ); } /** * Get calendar event * * @return array */ public function get_event() { return $this->get_prop( 'calendar_event' ); } /** * Get post status * * @return string */ public function get_status() { return get_post( $this->id )->post_status; } /** * Get total * * @return integer */ public function get_total() { return $this->get_prop( 'order_total' ); } /** * Get start date * * @return string */ public function get_start_date() { return $this->get_prop( 'start_date' ); } /** * Get start date * * @return string */ public function get_date() { return $this->get_prop( 'date' ); } /** * Get end date * * @return string */ public function get_end_date() { return $this->get_prop( 'end_date' ); } /** * Get start time * * @return string */ public function get_start_time() { return $this->get_prop( 'start_time' ); } /** * Get end time * * @return string */ public function get_end_time() { return $this->get_prop( 'end_time' ); } /** * Get random id for the current booking * * @return string */ public function get_random_id() { return $this->get_prop( 'random_id' ); } /** * Get payment details * * @return array */ public function get_payment_details() { return $this->get_prop( 'payment_details' ); } /** * Get zoom meeting details * * @return array */ public function get_zoom() { return $this->get_prop( 'zoom' ); } /** * Get recurrences * * @return array */ public function get_recurrence() { return $this->get_prop( 'recurrence' ); } /** * Get parent * * @return integer */ public function get_parent() { return $this->get_prop( 'parent' ); } /** * Get booking cancel reason * * @return integer */ public function get_cancel_reason() { return $this->get_prop( 'cancel_reason' ); } /** * Get booking data * * @param string $prop * * @return mixed */ private function get_prop( $prop = '' ) { return $this->get_metadata( $prop ); } /** * Get metadata * * @param string $prop * * @return mixed */ private function get_metadata( $prop = '' ) { $meta_key = $this->meta_prefix . $prop; return get_post_meta( $this->id, $meta_key, true ); } /** * Generate random id * * @return string */ private function generate_randon_id() { return chr( wp_rand( ord( 'A' ), ord( 'Z' ) ) ) . wp_rand( 575639, 575639 + 400 ); } // Setter. /** * Set booking id * * @param integer $id * * @return void */ public function set_id( $id ) { $this->id = $id; } /** * Set data * * @param array $args Booking Args * * @return void */ public function set_props( $args = [] ) { $this->data = wp_parse_args( $args, $this->data ); $this->appointment = new Appointment( $this->data['appointment'] ); if ( isset( $this->data['type'] ) && 'timetics-event' == $this->data['type'] ) { $this->event = new Event( $this->data['event'] ); } } /** * Save metada for booking * * @return void */ public function save_metadata( $args = [] ) { foreach ( $args as $key => $value ) { // If a key not exists on data it will skip to save the data. if ( ! array_key_exists( $key, $this->data ) ) { continue; } // Prepare meta key. $meta_key = $this->meta_prefix . $key; if ( ! $value ) { $value = $this->get_prop( $key ); } // Update appointment meta data. update_post_meta( $this->id, $meta_key, $value ); } } /** * Save appointment * * @return void */ public function save( $type = '' ) { $args = [ 'post_title' => $this->appointment->get_name(), 'post_type' => $this->pos_type, 'post_status' => $this->data['post_status'], 'post_author' => get_current_user_id(), ]; if ( 'timetics-event' == $type ) { $args = [ 'post_title' => $this->event->get_name(), 'post_type' => $this->pos_type, 'post_status' => $this->data['post_status'], 'post_author' => get_current_user_id(), ]; } $updated = false; if ( ! empty( $this->id ) ) { $this->send_notification( 'update' ); $args['ID'] = $this->id; $updated = true; } // Insert or Update appointment. $booking_id = wp_insert_post( $args ); $this->data['random_id'] = $this->generate_randon_id(); $this->data = apply_filters( 'timetics/event/booking/all_data', $this->data); if ( ! is_wp_error( $booking_id ) ) { $this->set_id( $booking_id ); $this->save_metadata( $this->data ); } if ( ! $updated ) { $this->send_notification( 'create' ); } } /** * Update booking * * @param array $args * * @return bool */ public function update( $args = [] ) { $post = get_post( $this->id )->to_array(); $args = wp_parse_args( $args, $post ); $updated = wp_update_post( $args ); if ( ! is_wp_error( $updated ) ) { $this->save_metadata( $args ); } return $updated; } /** * Find the Booking_Entry (shared per-slot schedule record) this booking occupies. * * @param Appointment|null $meeting Optional pre-built meeting for this booking. * @param string $start_date Slot date. Defaults to the booking's own. * @param string $start_time Slot start. Defaults to the booking's own. * @param string $timezone Timezone of the two above. Defaults to the booking's own. * @return Booking_Entry|null The first matching entry, or null if none. */ private function find_slot_entry( $meeting = null, $start_date = '', $start_time = '', $timezone = '' ) { $meeting = $meeting ?: new Appointment( $this->get_appointment() ); $booking_entry = new Booking_Entry(); $start_date = $start_date ?: $this->get_start_date(); $start_time = $start_time ?: $this->get_start_time(); $timezone = $timezone ?: $this->get_timezone(); $date_time = timetics_convert_timezone( $start_date . ' ' . $start_time, $timezone, $meeting->get_timezone() ); $entries = $booking_entry->find( [ 'staff_id' => $this->get_staff_id(), 'meeting_id' => $this->get_appointment(), 'date' => $date_time->format( 'Y-m-d' ), 'start' => $date_time->format( 'h:i a' ), ] ); return $entries ? $booking_entry->first() : null; } /** * Release the slot held by this booking. * * @return void */ public function release_slot() { if ( ! $this->is_booking() ) { return; } // Idempotency guard: never release the same booking's slot twice. if ( $this->get_prop( 'slot_released' ) ) { return; } $this->release_slot_at( $this->get_start_date(), $this->get_start_time() ); update_post_meta( $this->id, $this->meta_prefix . 'slot_released', 1 ); } /** * Take back the slot this booking released. * * A declined payment marks the booking failed and releases its slot, so an * attempt the customer walks away from does not block the time forever. When * the customer retries on that same booking the slot has to be taken back * before any money moves, otherwise the booking finalizes while the slot still * reads as free and the next customer can book straight over it. * * @return bool True when the slot is held, false when it is no longer free. */ public function reserve_slot() { if ( ! $this->is_booking() ) { return false; } // Nothing was released, so the slot is still held from booking creation. if ( ! $this->get_prop( 'slot_released' ) ) { return true; } $meeting = new Appointment( $this->get_appointment() ); $entry = $this->find_slot_entry( $meeting ); $seats = ! empty( $this->get_seat() ) ? (array) $this->get_seat() : []; $taking = max( 1, count( $seats ) ); if ( $entry ) { $booked = intval( $entry->get_booked() ) + $taking; // Someone else took the time while this booking was in the failed state. if ( $booked > $meeting->get_effective_capacity() ) { return false; } $existing = ! empty( $entry->get_seats() ) ? (array) $entry->get_seats() : []; $entry->update( [ 'booked' => $booked, 'seats' => array_values( array_unique( array_merge( $existing, $seats ) ) ), ] ); } else { // A one-to-one release deletes the entry outright, so recreate it. Entries // are stored in the meeting's timezone, the booking's in the customer's. $start = timetics_convert_timezone( $this->get_start_date() . ' ' . $this->get_start_time(), $this->get_timezone(), $meeting->get_timezone() ); $end = timetics_convert_timezone( $this->get_start_date() . ' ' . $this->get_end_time(), $this->get_timezone(), $meeting->get_timezone() ); $booking_entry = new Booking_Entry(); $booking_entry->create( [ 'meeting_id' => $meeting->get_id(), 'staff_id' => $this->get_staff_id(), 'customer_id' => $this->get_customer(), 'booking_id' => $this->id, 'booked' => $taking, 'date' => $start->format( 'Y-m-d' ), 'start' => $start->format( 'h:i a' ), 'end' => $end->format( 'h:i a' ), 'seats' => $seats, ] ); } delete_post_meta( $this->id, $this->meta_prefix . 'slot_released' ); return true; } /** * Release the entry for one named slot of this booking. * * Rescheduling saves the new time first, so the old slot has to be named * rather than read off the booking. No idempotency guard: the booking lives * on and may release more slots as it moves. * * @param string $start_date Slot date, Y-m-d. * @param string $start_time Slot start. * @param string $timezone Timezone of the two above. Defaults to the booking's own. * * @return void */ public function release_slot_at( $start_date, $start_time, $timezone = '' ) { if ( ! $this->is_booking() ) { return; } $meeting = new Appointment( $this->get_appointment() ); $entry = $this->find_slot_entry( $meeting, $start_date, $start_time, $timezone ); if ( ! $entry ) { if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Debug-only diagnostics for a missing booking entry. error_log( sprintf( 'Timetics release_slot: no Booking_Entry found for booking #%d ( staff %s, meeting %s, slot %s %s ).', $this->id, $this->get_staff_id(), $this->get_appointment(), $start_date, $start_time ) ); } return; } if ( 'one-to-one' === strtolower( $meeting->get_type() ) ) { $entry->delete(); return; } $booked = max( 0, intval( $entry->get_booked() ) - 1 ); $booked_seat = ! empty( $this->get_seat() ) ? $this->get_seat() : []; $existing_seat = ! empty( $entry->get_seats() ) ? $entry->get_seats() : []; $entry->update( [ 'booked' => $booked, 'seats' => array_values( array_diff( $existing_seat, $booked_seat ) ), ] ); } /** * Delete booking * * @return bool | WP_Error */ public function delete() { // Set action for sending notification. $this->send_notification( 'delete' ); return wp_delete_post( $this->id, true ); } /** * Check is a booking or not * * @return bool */ public function is_booking() { $post = get_post( $this->id ); if ( $post && $this->pos_type === $post->post_type ) { return true; } return false; } /** * Get all bookings * * @param array $args Bookings args * * @return array */ public static function all( $args = [] ) { $status = [ 'approved', 'pending', 'cancel', 'completed', ]; $defaults = [ 'post_type' => 'timetics-booking', 'posts_per_page' => 20, 'post_status' => 'any', 'paged' => 1, ]; $args = wp_parse_args( $args, $defaults ); $args = apply_filters( 'timetics/admin/bookings/all', $args, $defaults ); $meta_query = ! empty( $args['meta_query'] ) && is_array( $args['meta_query'] ) ? $args['meta_query'] : []; if ( ! empty( $args['start_date'] ) ) { $meta_query[] = [ 'key' => '_tt_booking_start_date', 'value' => $args['start_date'], 'compare' => '>=', 'type' => 'DATE', ]; } if ( ! empty( $args['meeting'] ) ) { $meta_query[] = [ 'key' => '_tt_booking_appointment', 'value' => $args['meeting'], 'compare' => '=', ]; } if ( ! empty( $args['staff'] ) ) { $meta_query[] = [ 'key' => '_tt_booking_staff', 'value' => $args['staff'], 'compare' => '=', ]; } if ( ! empty( $meta_query ) ) { if ( count( $meta_query ) > 1 && empty( $meta_query['relation'] ) ) { $meta_query['relation'] = 'AND'; } // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Meta query is necessary for filtering bookings $args['meta_query'] = $meta_query; } $post = new WP_Query( $args ); return [ 'total' => $post->found_posts, 'items' => $post->posts, ]; } /** * Create event after creating a booking * * @return bool */ public function create_event() { if ( ! timetics_google_setup() ) { return; } if ('timetics-event' == $this->get_type()) { Event_Booking::instance()->create_online_event($this->get_id()); }else { $this->create_appointment_event(); } } public function create_appointment_event() { // No summary/description passed, so prepare_event() falls back to the // meeting's own name and description. The booking-created email subject // and body used to be reused here, which put the notification copy // ("New meeting scheduled!", greeting, date and time lines) into the // calendar entry instead of the meeting's details. $calendar = new Calendar(); $event_data = $this->prepare_event(); if ( ! $event_data ) { return; } $entry = $this->find_slot_entry(); $event = false; if ( $entry ) { $event = $entry->get_google_event(); if ( ! $event ) { $event = $calendar->create_event( $event_data ); $entry->update( [ 'google_event' => $event, ] ); } } if ( $event ) { update_post_meta( $this->id, $this->meta_prefix . 'calendar_event', $event ); // Also record the bare event id. Google_Calendar_Sync uses this meta // to recognise events Timetics itself created, so that they are not // pulled back in as external events and used to block their own slot. if ( ! empty( $event['id'] ) ) { $this->set_google_event_id( $event['id'] ); } } } /** * Update calendar event * * @return bool */ public function update_event() { if ( ! timetics_google_setup() ) { return; } // Same as create_appointment_event(): the meeting's name and description // belong in the calendar entry, not the reschedule email copy. Keeping // them consistent also stops a reschedule from rewriting the title. $calendar = new Calendar(); $event_data = $this->prepare_event(); $calendar_event = $this->get_event(); if ( ! is_array( $calendar_event ) ) { return; } if ( ! empty( $calendar_event['id'] ) ) { // Update calendar event. $event = $calendar->update_event( $calendar_event['id'], $event_data ); // update calendar event data. update_post_meta( $this->id, $this->meta_prefix . 'calendar_event', $event ); } } /** * Delete calendar event * * @return */ public function delete_event() { if ( ! timetics_google_setup() ) { return; } $calendar = new Calendar(); $calendar_event = $this->get_event(); $access_token = timetics_get_google_access_token( $this->get_staff_id() ); if ( ! is_array( $calendar_event ) ) { return; } if ( ! $access_token ) { return; } if ( ! empty( $calendar_event['id'] ) ) { // Update calendar event. $event = $calendar->delete_event( $calendar_event['id'], $access_token ); // update calendar event data. update_post_meta( $this->id, $this->meta_prefix . 'calendar_event', $event ); // The event no longer exists in Google, so drop the id used by // Google_Calendar_Sync to skip Timetics-created events. $this->set_google_event_id( '' ); } } /** * Prepare event data * * @return array */ private function prepare_event( $data = [] ) { $meeting_id = $this->get_appointment(); $staff_id = $this->get_staff_id(); $customer_id = $this->get_customer_id(); $location_type = $this->get_location_type(); $access_token = timetics_get_google_access_token( $staff_id ); if ( ! $access_token ) { return false; } $meeting = new Appointment( $meeting_id ); $staff = new Staff( $staff_id ); $customer = new Customer( $customer_id ); $time = gmdate( 'h:i a', strtotime( $this->get_start_time() ) ); $date = gmdate( 'd F Y', strtotime( $this->get_start_date() ) ); $location = $this->get_location(); $placeholders = [ '{%meeting_title%}' => $meeting->get_name(), '{%meeting_date%}' => $date, '{%meeting_time%}' => $time, '{%meeting_location%}' => $location, '{%meeting_duration%}' => $meeting->get_duration(), '{%host_name%}' => $staff->get_display_name(), '{%host_email%}' => $staff->get_email(), '{%customer_name%}' => $customer->get_display_name(), '{%customer_email%}' => $customer->get_email(), ]; $summary = ! empty( $data['summary'] ) ? timetics_replace_placeholder( $data['summary'], $placeholders ) : $meeting->get_name(); $description = ! empty( $data['description'] ) ? timetics_replace_placeholder( $data['description'], $placeholders ) : $meeting->get_description(); $description = apply_filters( 'timetics_booking_event_data', $description, $summary, $this ); $event = [ 'summary' => $summary, 'description' => $description, 'start' => [ 'date' => $this->get_start_date(), 'time' => $this->get_start_time(), ], 'end' => [ 'date' => $this->get_end_date(), 'time' => $this->get_end_time(), ], 'attendees' => [ ['email' => $staff->get_email()], ['email' => $customer->get_email()], ], 'timezone' => $this->get_timezone(), 'google_meet' => 'google-meet' === $location_type, 'access_token' => $access_token, ]; return $event; } /** * Get all data of an object * * @return array */ public function to_array() { $data = []; foreach ( $this->data as $key => $value ) { $data[$key] = $this->get_prop( $key ); } return $data; } /** * Send notification after booking * * @return void */ private function send_notification( $action ) { do_action( 'timetics_booking_notification', $this, $action ); } /** * Get booking ids by appointment id * * @param array Collection of appointment ids * * @return array Collection of booking ids */ public static function get_booking_ids_by_appointment( $appointment_ids, $per_page = -1 ) { if ( empty( $appointment_ids ) ) { return []; } $query = new WP_Query([ 'post_type' => 'timetics-booking', 'post_status' => 'any', 'posts_per_page' => $per_page, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Meta query is necessary for filtering bookings by appointment 'meta_query' => [ [ 'key' => '_tt_booking_appointment', 'value' => $appointment_ids, 'compare' => 'IN', ], ], ]); return [ 'total' => $query->found_posts, 'items' => $query->posts, ]; } /** * Get booking ids visible to a given user. * * Union of: * - bookings where the user is the assigned staff (`_tt_booking_staff`) * - bookings whose appointment is authored by the user * * @param integer $user_id WP user id. * * @return int[] Deduped list of booking post ids. */ public static function get_visible_ids_for_user( $user_id ) { $user_id = (int) $user_id; if ( ! $user_id ) { return []; } $staff_ids = get_posts( [ 'post_type' => 'timetics-booking', 'post_status' => 'any', 'posts_per_page' => -1, 'fields' => 'ids', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- needed to scope bookings to current staff user 'meta_query' => [ [ 'key' => '_tt_booking_staff', 'value' => $user_id, ], ], ] ); $appointment_ids = Appointment::get_appointment_ids_by_author( $user_id ); $appointment_booking_ids = []; if ( ! empty( $appointment_ids ) ) { $appointment_booking_ids = get_posts( [ 'post_type' => 'timetics-booking', 'post_status' => 'any', 'posts_per_page' => -1, 'fields' => 'ids', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- needed to scope bookings to appointments owned by current user 'meta_query' => [ [ 'key' => '_tt_booking_appointment', 'value' => $appointment_ids, 'compare' => 'IN', ], ], ] ); } return array_values( array_unique( array_map( 'intval', array_merge( $staff_ids, $appointment_booking_ids ) ) ) ); } /** * Get Google Calendar Event ID for this booking * * @return string */ public function get_google_event_id() { return $this->get_metadata( 'google_event_id' ); } /** * Set Google Calendar Event ID for this booking * * @param string $event_id * @return bool */ public function set_google_event_id( $event_id ) { // Written directly rather than through save_metadata(), which takes an // array and only accepts keys declared in $this->data — neither is true // here, so it silently discarded every write. $meta_key = $this->meta_prefix . 'google_event_id'; if ( ! $event_id ) { return delete_post_meta( $this->id, $meta_key ); } return update_post_meta( $this->id, $meta_key, $event_id ); } public function set_sync_status( $status ) { $meta_key = $this->meta_prefix . 'google_calendar_sync_status'; if ( ! $status ) { return delete_post_meta( $this->id, $meta_key ); } return update_post_meta( $this->id, $meta_key, $status ); } public function get_sync_status() { return $this->get_metadata( 'google_calendar_sync_status' ); } /** * Get all Google Event IDs for all bookings * * @return array */ public function get_all_google_event_ids() { $meta_key = $this->meta_prefix . 'google_event_id'; $posts = get_posts( array( 'post_type' => 'any', 'posts_per_page' => -1, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Meta query is necessary for filtering posts by meta key 'meta_query' => array( array( 'key' => $meta_key, 'value' => '', 'compare' => '!=', ), ), 'fields' => 'ids', ) ); if ( empty( $posts ) ) { return array(); } $event_ids = array(); foreach ( $posts as $post_id ) { $event_id = get_post_meta( $post_id, $meta_key, true ); if ( ! empty( $event_id ) ) { $event_ids[] = $event_id; } } return $event_ids; } /** * Get name. * * @since 1.0.0 * * @return string */ public function get_security_token() { return $this->get_prop( 'security_token' ); } /** * Generate and store a secure reschedule token for a booking * * @param int $booking_id * @return string The generated token */ public function generate_security_token() { $token = bin2hex(random_bytes(4)); // 8 hex chars return $token; } /** * Rotate the booking's security token so the previously-issued one cannot * be reused (e.g. after a payment is approved). * * @return void */ public function rotate_security_token() { $meta_key = $this->meta_prefix . 'security_token'; $new_token = $this->generate_security_token(); update_post_meta( $this->id, $meta_key, $new_token ); } /** * Get the Stripe PaymentIntent id bound to this booking, if any. * * @return string */ public function get_stripe_payment_intent_id() { return (string) get_post_meta( $this->id, '_tt_stripe_payment_intent_id', true ); } /** * Bind a Stripe PaymentIntent id to this booking. Used for replay * protection: a second make_payment call with a different intent will be * rejected. * * @param string $intent_id * @return void */ public function set_stripe_payment_intent_id( $intent_id ) { update_post_meta( $this->id, '_tt_stripe_payment_intent_id', sanitize_text_field( (string) $intent_id ) ); } }