'', 'title' => '', 'content' => '', 'start_time' => '', 'end_time' => '', 'date' => '', 'buffer_before' => '', 'buffer_after' => '', 'duration' => '', 'status' => '', 'payment_status' => '', 'customer_id' => '', 'customer_name' => '', 'team_member_id' => '', 'service_id' => '', 'additional_duration_id' => '', 'group_booking' => array(), 'order_id' => '', 'quantity' => 0, 'price' => 0, 'subtotal' => 0, 'total' => 0, 'note' => '', 'source' => '', 'location_id' => '', 'location' => array(), 'extra_services' => array(), 'security_token' => '', 'total_attendees' => 1, 'timezone' => '', 'cancel_note' => '', ); /** * Accepted post statuses for appointments * * @var array */ public static $accepted_statuses = array( 'pending', 'active', 'cancelled', 'canceled', 'completed', 'approved', 'not_now', ); /** * Get post type * * @return string Post type name */ public function get_post_type() { return 'booktics-appointment'; } /** * Get accepted statuses * * @return array Accepted statuses */ public static function get_accepted_statuses() { return self::$accepted_statuses; } /** * Specify data which should be serialized to JSON. * * @return array */ public function jsonSerialize(): array { $data = $this->to_array(); $service = Service_Model::find( $this->service_id ); $db = new Booktics_Database(); $customer = ( new Guest_Model( $db ) )->find( array( 'id' => $this->customer_id ) ); $team_member = ( new Team_Member_Model() )->find( $this->team_member_id ); $appointment_data = array( 'service' => $service, 'team_member' => $team_member, 'customer' => $customer, ); return array_merge( $data, $appointment_data ); } /** * Get appointments for a specific date and service/team member * * @param string $date * @param int $team_member_id * * @return array * @throws Exception */ public function get_appointments_for_slot( string $date, int $team_member_id ): array { $args = array( 'post_type' => $this->get_post_type(), 'post_status' => booktics_get_option( 'appointment_statuses_for_blocked_slot', array( 'publish', 'active', 'pending' ) ), 'posts_per_page' => - 1, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Meta query is necessary for filtering appointments by meta fields 'meta_query' => array( 'relation' => 'AND', array( 'key' => 'date', 'value' => $date, 'compare' => '=', ), array( 'key' => 'team_member_id', 'value' => $team_member_id, 'compare' => '=', ), ), ); $query = new \WP_Query( $args ); return array_map( function ( $post ) { return new static( $post ); }, $query->posts ); } /** * Override load_attributes to include payment status from orders * * @return void */ protected function load_attributes() { parent::load_attributes(); if ( $this->order_id ) { $db = new Booktics_Database(); $order_model = ( new Order_Model( $db ) )->find( array( 'id' => $this->order_id ) ); $this->attributes['payment_status'] = $order_model->payment_status ?? 'pending'; } else { $this->attributes['payment_status'] = 'pending'; } } /** * Get appointments count by order ID * * @param int $order_id Order ID * * @return int Number of appointments */ public function get_appointments_count_by_order_id( $order_id ) { $args = array( 'post_type' => $this->get_post_type(), 'post_status' => array( 'publish', 'active', 'approved', 'completed', 'pending' ), 'posts_per_page' => - 1, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Meta query is necessary for filtering appointments by meta fields 'meta_query' => array( array( 'key' => 'order_id', 'value' => $order_id, 'compare' => '=', ), ), ); $query = new \WP_Query( $args ); return $query->found_posts; } /** * Get appointments count by team member ID * * @param int $team_member_id Team member ID * * @return int Number of appointments */ public function get_appointments_count_by_team_member_id( $team_member_id ) { $args = array( 'post_type' => $this->get_post_type(), 'post_status' => array( 'publish', 'active', 'pending' ), 'posts_per_page' => - 1, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Meta query is necessary for filtering appointments by meta fields 'meta_query' => array( array( 'key' => 'team_member_id', 'value' => $team_member_id, 'compare' => '=', ), ), ); $query = new \WP_Query( $args ); return $query->found_posts; } /** * Get reschedule link * * @return string Reschedule link */ public function get_reschedule_link() { if ( ! booktics_get_option( 'allow_customer_rescheduling', false ) ) { return ''; } $this->add_security_token_if_not_exists(); $service_permalink = $this->get_associated_service_permalink(); $reschedule_url = add_query_arg( [ 'service_id' => $this->service_id, 'appointment_id' => $this->id, 'booking_action' => 'reschedule', 'appointment_token' => $this->security_token, 'order_id' => $this->order_id, 'team_member_id' => $this->team_member_id, ], $service_permalink ); return esc_url( $reschedule_url ); } /** * Get cancel link * * @return string Cancel link */ public function get_cancel_link() { if ( ! booktics_get_option( 'allow_customer_cancellation', false ) ) { return ''; } $this->add_security_token_if_not_exists(); $service_permalink = $this->get_associated_service_permalink(); $cancel_url = add_query_arg( [ 'service_id' => $this->service_id, 'appointment_id' => $this->id, 'booking_action' => 'cancel', 'appointment_token' => $this->security_token, 'order_id' => $this->order_id, 'team_member_id' => $this->team_member_id, ], $service_permalink ); return esc_url( $cancel_url ); } /** * Get associated service permalink * * @return string Service permalink */ private function get_associated_service_permalink() { if ( ! $this->service_id ) { return ''; } $service = Service_Model::find( $this->service_id ); return $service->link ?? ''; } /** * Generate a security token for the appointment * This generated token is used to verify access to the appointment rescheduling and cancellation * * @return string Security token */ public function generate_security_token() { $token = bin2hex( random_bytes(4) ); // 8 hex chars return $token; } /** * Check if the security token exists, if not generate and save it to the post meta. * This token is used to verify access to the appointment rescheduling and cancellation. * * @return void */ public function add_security_token_if_not_exists() { if ( empty( $this->security_token ) ) { $this->security_token = $this->generate_security_token(); $this->save(); } } }