version = $this->version.'.'.time(); // dev mode parent::__construct( $plugin ); $this->hooks(); } /** * Initiate our hooks. * * @since 0.0.2 */ public function hooks() { add_filter( 'ssa/appointment/before_insert', array( $this, 'cleanup_customer_information' ), 5, 1 ); add_filter( 'ssa/appointment/before_update', array( $this, 'cleanup_customer_information' ), 5, 1 ); add_filter( 'ssa/appointment/before_update', array( $this, 'prevent_canceling_a_reserved_appointment' ), 1, 2 ); add_filter( 'ssa/appointment/before_insert', array( $this, 'default_appointment_status' ), 5, 1 ); add_filter( 'ssa/appointment/before_update', array( $this, 'merge_customer_information' ), 10, 3 ); add_action( 'ssa/appointment/after_insert', array( $this, 'update_rescheduled_to_appointment_id' ), 10, 3 ); add_filter( 'ssa/appointment/after_get', array( $this, 'format_multiline_customer_information' ), 10, 1 ); } public function format_multiline_customer_information( $item ) { if( ! isset( $item['customer_information'] ) || ! is_array( $item['customer_information'] ) ){ return $item; } $appointment_type_object = new SSA_Appointment_Type_Object( $item["appointment_type_id"] ); $fields = []; // added to avoid executing the code block on a string in basic edition if( is_array( $appointment_type_object->custom_customer_information ) ){ foreach( $appointment_type_object->custom_customer_information as $field ){ $field_label = $field["field"]; $fields[] = $field_label; if( "multi-text" === $field["type"] && isset( $item['customer_information'][$field_label] ) && is_string( $item['customer_information'][$field_label] ) ){ $item['customer_information'][$field_label] = nl2br( $item['customer_information'][$field_label] ); } } } foreach( $item['customer_information'] as $key => $value ){ if( ! in_array( $key, $fields ) && is_string( $item['customer_information'][$key] ) ){ $item['customer_information'][$key] = nl2br( $item['customer_information'][$key] ); } } return $item; } public static function get_booked_statuses() { return array( 'booked' ); } public static function is_a_booked_status( $status ) { return in_array( $status, self::get_booked_statuses() ); } public static function get_reserved_statuses() { return array( 'pending_payment', 'pending_form' ); } public static function is_a_reserved_status( $status ) { return in_array( $status, self::get_reserved_statuses() ); } public static function get_canceled_statuses() { return array( 'canceled' ); } public static function is_a_canceled_status( $status ) { return in_array( $status, self::get_canceled_statuses() ); } public static function get_abandoned_statuses() { return array( 'abandoned' ); } public static function is_an_abandoned_status( $status ) { return in_array( $status, self::get_abandoned_statuses() ); } public static function get_unavailable_statuses() { return array_merge( self::get_booked_statuses(), self::get_reserved_statuses() ); } public static function is_a_unavailable_status( $status ) { return in_array( $status, self::get_unavailable_statuses() ); } public static function is_a_available_status( $status ) { return ! self::is_a_unavailable_status( $status ); } /** * Check if an appointment got reassigned to another team member on update * * @param array $data_after * @param array $data_before * @return boolean */ public static function is_appointment_reassigned( $data_after, $data_before ) { if ( ! class_exists( 'SSA_Staff' ) ) { return false; } // Use isset since the staff_ids still could be empty when reassigned if ( ! isset( $data_before["staff_ids"] ) || ! isset( $data_after["staff_ids"] ) ) { return false; } if ( empty( $data_before["staff_ids"] ) && empty( $data_after["staff_ids"] ) ) { return false; } $intersection = array_intersect( $data_before["staff_ids"], $data_after["staff_ids"] ); if ( count( $intersection ) === count( $data_before["staff_ids"] ) && count( $intersection ) === count( $data_after["staff_ids"] ) ) { return false; } return true; } public function merge_customer_information( $data, $data_before, $appointment_id ) { if ( empty( $data['customer_information'] ) ) { return $data; } if ( empty( $data_before['customer_information'] ) ) { $data_before['customer_information'] = array(); } // using array_replace instead of array_merge to overwrite values and prevent duplicate keys $data['customer_information'] = array_replace( $data_before['customer_information'], $data['customer_information'] ); return $data; } /** * A reserved appointment ( pending_payment or pending_form ) should only be marked as abandoned * Check if status before is either pending_payment or pending_form * Check if status after is canceled * If so, change canceled to abandoned * * @param array $data_after * @param array $data_before * @return array */ public function prevent_canceling_a_reserved_appointment( $data_after = array(), $data_before = array() ) { if ( empty( $data_before['status'] ) || empty( $data_after['status'] ) ) { return $data_after; } if ( ! SSA_Appointment_Model::is_a_reserved_status( $data_before['status'] ) ) { return $data_after; } if ( SSA_Appointment_Model::is_a_canceled_status( $data_after['status'] ) ) { $data_after['status'] = 'abandoned'; } return $data_after; } public function cleanup_customer_information( $data ) { if ( empty( $data['customer_information'] ) || ! is_array( $data['customer_information'] ) ) { return $data; } foreach ( $data['customer_information'] as &$info ) { if ( is_string( $info ) ) { $info = trim( $info ); // sanitize $info = sanitize_textarea_field($info); } } return $data; } public function default_appointment_status( $data ) { // We want to allow "pending_form" status if it's provided if ( ! empty( $data['status'] ) && $data['status'] === 'pending_form' ) { return $data; } $data['status'] = 'booked'; return $data; } public function debug() { } public function belongs_to() { return array( // 'Author' => array( // 'model' => 'WP_User_Model', // 'foreign_key' => 'author_id', // ), 'AppointmentType' => array( 'model' => $this->plugin->appointment_type_model, 'foreign_key' => 'appointment_type_id', ), ); } public function has_many() { return array( 'Payment' => array( 'model' => $this->plugin->payment_model, 'foreign_key' => 'appointment_id', ), 'Revision' => array( 'model' => $this->plugin->revision_model, 'foreign_key' => 'appointment_id', ), ); } protected $schema = array( 'appointment_type_id' => array( 'field' => 'appointment_type_id', 'label' => 'Appointment Type ID', 'default_value' => 0, 'format' => '%d', 'mysql_type' => 'BIGINT', 'mysql_length' => 20, 'mysql_unsigned' => true, 'mysql_allow_null' => false, 'mysql_extra' => '', 'cache_key' => false, ), 'rescheduled_from_appointment_id' => array( 'field' => 'rescheduled_from_appointment_id', 'label' => 'Rescheduled from Appointment ID', 'default_value' => 0, 'format' => '%d', 'mysql_type' => 'BIGINT', 'mysql_length' => 20, 'mysql_unsigned' => true, 'mysql_allow_null' => false, 'mysql_extra' => '', 'cache_key' => false, ), 'rescheduled_to_appointment_id' => array( 'field' => 'rescheduled_to_appointment_id', 'label' => 'Rescheduled to Appointment ID', 'default_value' => 0, 'format' => '%d', 'mysql_type' => 'BIGINT', 'mysql_length' => 20, 'mysql_unsigned' => true, 'mysql_allow_null' => false, 'mysql_extra' => '', 'cache_key' => false, ), 'group_id' => array( 'field' => 'group_id', 'label' => 'Group ID', 'default_value' => 0, 'format' => '%d', 'mysql_type' => 'BIGINT', 'mysql_length' => 20, 'mysql_unsigned' => true, 'mysql_allow_null' => false, 'mysql_extra' => '', 'cache_key' => false, ), 'author_id' => array( 'field' => 'author_id', 'label' => 'Author ID', 'default_value' => 0, 'format' => '%d', 'mysql_type' => 'BIGINT', 'mysql_length' => 20, 'mysql_unsigned' => true, 'mysql_allow_null' => false, 'mysql_extra' => '', 'cache_key' => false, ), 'customer_id' => array( 'field' => 'customer_id', 'label' => 'Customer ID', 'default_value' => 0, 'format' => '%d', 'mysql_type' => 'BIGINT', 'mysql_length' => 20, 'mysql_unsigned' => true, 'mysql_allow_null' => false, 'mysql_extra' => '', 'cache_key' => false, ), 'customer_information' => array( 'field' => 'customer_information', 'label' => 'Customer Information', 'default_value' => false, 'format' => '%s', 'mysql_type' => 'TEXT', 'mysql_length' => false, 'mysql_unsigned' => false, 'mysql_allow_null' => false, 'mysql_extra' => '', 'cache_key' => false, 'encoder' => 'json', ), 'customer_timezone' => array( 'field' => 'customer_timezone', 'label' => 'Customer Timezone', 'default_value' => false, 'format' => '%s', 'mysql_type' => 'TINYTEXT', 'mysql_length' => false, 'mysql_unsigned' => false, 'mysql_allow_null' => false, 'mysql_extra' => '', 'cache_key' => false, ), 'customer_locale' => array( 'field' => 'customer_locale', 'label' => 'Customer Locale', 'default_value' => false, 'format' => '%s', 'mysql_type' => 'TINYTEXT', 'mysql_length' => false, 'mysql_unsigned' => false, 'mysql_allow_null' => false, 'mysql_extra' => '', 'cache_key' => false, ), 'start_date' => array( 'field' => 'start_date', 'label' => 'Start Date', 'default_value' => false, 'format' => '%s', 'mysql_type' => 'datetime', 'mysql_length' => '', 'mysql_unsigned' => false, 'mysql_allow_null' => true, 'mysql_extra' => '', 'cache_key' => false, ), 'end_date' => array( 'field' => 'end_date', 'label' => 'End Date', 'default_value' => false, 'format' => '%s', 'mysql_type' => 'datetime', 'mysql_length' => '', 'mysql_unsigned' => false, 'mysql_allow_null' => true, 'mysql_extra' => '', 'cache_key' => false, ), 'title' => array( 'field' => 'title', 'label' => 'Title', 'default_value' => false, 'format' => '%s', 'mysql_type' => 'TINYTEXT', 'mysql_length' => false, 'mysql_unsigned' => false, 'mysql_allow_null' => false, 'mysql_extra' => '', 'cache_key' => false, ), 'description' => array( 'field' => 'description', 'label' => 'Description', 'default_value' => false, 'format' => '%s', 'mysql_type' => 'TEXT', 'mysql_length' => '', 'mysql_unsigned' => false, 'mysql_allow_null' => false, 'mysql_extra' => '', 'cache_key' => false, ), 'payment_method' => array( 'field' => 'payment_method', 'label' => 'Payment Method', 'default_value' => '', 'format' => '%s', 'mysql_type' => 'TINYTEXT', 'mysql_length' => false, 'mysql_unsigned' => false, 'mysql_allow_null' => false, 'mysql_extra' => '', 'cache_key' => false, ), 'payment_received' => array( 'field' => 'payment_received', 'label' => 'Payment Received', 'default_value' => false, 'format' => '%s', 'mysql_type' => 'DECIMAL(9,2)', 'mysql_length' => '', 'mysql_unsigned' => false, 'mysql_allow_null' => false, 'mysql_extra' => '', 'cache_key' => false, ), 'mailchimp_list_id' => array( 'field' => 'mailchimp_list_id', 'label' => 'MailChimp List ID', 'default_value' => false, 'format' => '%s', 'mysql_type' => 'TINYTEXT', 'mysql_length' => false, 'mysql_unsigned' => false, 'mysql_allow_null' => false, 'mysql_extra' => '', 'cache_key' => false, ), 'google_calendar_id' => array( 'field' => 'google_calendar_id', 'label' => 'Google Calendar ID', 'default_value' => false, 'format' => '%s', 'mysql_type' => 'TINYTEXT', 'mysql_length' => false, 'mysql_unsigned' => false, 'mysql_allow_null' => false, 'mysql_extra' => '', 'cache_key' => false, ), 'google_calendar_event_id' => array( 'field' => 'google_calendar_event_id', 'label' => 'Google Calendar Event ID', 'default_value' => false, 'format' => '%s', 'mysql_type' => 'TINYTEXT', 'mysql_length' => false, 'mysql_unsigned' => false, 'mysql_allow_null' => false, 'mysql_extra' => '', 'cache_key' => false, ), 'web_meeting_password' => array( 'field' => 'web_meeting_password', 'label' => 'Web Meeting Password', 'default_value' => false, 'format' => '%s', 'mysql_type' => 'TINYTEXT', 'mysql_length' => false, 'mysql_unsigned' => false, 'mysql_allow_null' => false, 'mysql_extra' => '', 'cache_key' => false, ), 'web_meeting_id' => array( 'field' => 'web_meeting_id', 'label' => 'Web Meeting ID', 'default_value' => false, 'format' => '%s', 'mysql_type' => 'TINYTEXT', 'mysql_length' => false, 'mysql_unsigned' => false, 'mysql_allow_null' => false, 'mysql_extra' => '', 'cache_key' => false, ), 'web_meeting_url' => array( 'field' => 'web_meeting_url', 'label' => 'Web Meeting Url', 'default_value' => false, 'format' => '%s', 'mysql_type' => 'TINYTEXT', 'mysql_length' => false, 'mysql_unsigned' => false, 'mysql_allow_null' => false, 'mysql_extra' => '', 'cache_key' => false, ), 'allow_sms' => array( 'field' => 'allow_sms', 'label' => 'Allow SMS', 'default_value' => false, 'format' => '%s', 'mysql_type' => 'VARCHAR', 'mysql_length' => '1', 'mysql_unsigned' => false, 'mysql_allow_null' => false, 'mysql_extra' => '', 'cache_key' => false, ), 'status' => array( 'field' => 'status', 'label' => 'Status', 'default_value' => 'booked', 'format' => '%s', 'mysql_type' => 'VARCHAR', 'mysql_length' => '16', 'mysql_unsigned' => false, 'mysql_allow_null' => false, 'mysql_extra' => '', 'cache_key' => false, ), 'date_created' => array( 'field' => 'date_created', 'label' => 'Date Created', 'default_value' => false, 'format' => '%s', 'mysql_type' => 'datetime', 'mysql_length' => '', 'mysql_unsigned' => false, 'mysql_allow_null' => true, 'mysql_extra' => '', 'cache_key' => false, ), 'date_modified' => array( 'field' => 'date_modified', 'label' => 'Date Modified', 'default_value' => false, 'format' => '%s', 'mysql_type' => 'datetime', 'mysql_length' => '', 'mysql_unsigned' => false, 'mysql_allow_null' => true, 'mysql_extra' => '', 'cache_key' => false, ), 'expiration_date' => array( 'field' => 'expiration_date', 'label' => 'Expiration Date', 'default_value' => false, 'format' => '%s', 'mysql_type' => 'datetime', 'mysql_length' => '', 'mysql_unsigned' => false, 'mysql_allow_null' => true, 'mysql_extra' => '', 'cache_key' => false, ), ); public $indexes = array( 'customer_id' => array( 'customer_id' ), 'start_date' => array( 'start_date' ), 'end_date' => array( 'end_date' ), 'status' => array( 'status' ), 'date_created' => array( 'date_created' ), ); public function filter_where_conditions( $where, $args ) { global $wpdb; // Check if both customer_id and customer_information are provided if ( ! empty( $args['customer_id'] ) && ! empty( $args['customer_information'] ) ) { $customer_id_condition = $wpdb->prepare( 'customer_id=%d', sanitize_text_field( $args['customer_id'] ) ); $email = sanitize_text_field( $args['customer_information'] ); $email_condition = $wpdb->prepare( 'customer_information LIKE %s', '%' . $wpdb->esc_like( '"Email":"' . $email . '"' ) . '%' ); $where .= ' AND (' . $customer_id_condition . ' OR ' . $email_condition . ')'; } else { // Add individual conditions if only one is provided if ( ! empty( $args['customer_id'] ) ) { $where .= $wpdb->prepare( ' AND customer_id=%d', sanitize_text_field( $args['customer_id'] ) ); } if ( ! empty( $args['customer_information'] ) ) { $email = sanitize_text_field( $args['customer_information'] ); $where .= $wpdb->prepare( ' AND customer_information LIKE %s', '%' . $wpdb->esc_like( '"Email":"' . $email . '"' ) . '%' ); } } if ( ! empty( $args['group_id'] ) ) { $where .= $wpdb->prepare( ' AND group_id=%d', sanitize_text_field( $args['group_id'] ) ); } // If querying by label_id -> convert label_id to appointmnet_type_id(s) if ( ! empty( $args['label_id'] ) ) { $appointment_types = $this->plugin->appointment_type_model->query( array( 'label_id' => $args['label_id'] )); if( ! empty( $appointment_types ) ) { $appointment_type_ids = array_map( function($type) { return $type['id']; }, $appointment_types); if( ! empty( $args['appointment_type_id'] ) ) { if( ! is_array( $args['appointment_type_id'] ) ) { $args['appointment_type_id'] = array( $args['appointment_type_id'] ); } $args['appointment_type_id'] = array_unique( array_merge( $appointment_type_ids, $args['appointment_type_id'] ) ); } else { $args['appointment_type_id'] = $appointment_type_ids; } } // Means we're filtering by label but no matching appointmnent types were found nor passed as arguments // Nothing to query, nothing to return if( empty( $args['appointment_type_id'] ) ) { $where .= ' AND 1=2 '; return $where; } } if ( ! empty( $args['appointment_type_id'] ) ) { if ( is_array( $args['appointment_type_id'] ) ) { $where .= ' AND ('; foreach ( $args['appointment_type_id'] as $key => $appointment_type_id ) { $where .= $wpdb->prepare( "`appointment_type_id` = '" . '%s' . "' ", $appointment_type_id ); if ( $key + 1 < count( $args['appointment_type_id'] ) ) { $where .= 'OR '; } } $where .= ') '; } else { $where .= $wpdb->prepare( " AND `appointment_type_id` = '" . '%s' . "' ", sanitize_text_field( $args['appointment_type_id'] ) ); } } if ( ! empty( $args['exclude_ids'] ) ) { if ( is_array( $args['exclude_ids'] ) ) { $where .= ' AND ('; $where .= $wpdb->prepare( '`id` NOT IN (' . implode( ', ', array_fill( 0, count( $args['exclude_ids'] ), '%d' ) ) . ')', $args['exclude_ids'] ); $where .= ') '; } else { $where .= $wpdb->prepare( " AND `id` != '" . '%d' . "' ", sanitize_text_field( $args['exclude_ids'] ) ); } } if ( isset( $args['intersects_period'] ) ) { if ( $args['intersects_period'] instanceof Period ) { $start_date_string = $args['intersects_period']->getStartDate()->format( 'Y-m-d H:i:s' ); $end_date_string = $args['intersects_period']->getEndDate()->format( 'Y-m-d H:i:s' ); // it should END in the queried period // OR // it should START in the queried period // OR // it should CONTAIN the queried period $where .= " AND ( (end_date >= '{$start_date_string}' AND end_date <= '{$end_date_string}' ) OR (start_date <= '{$end_date_string}' AND start_date >= '{$start_date_string}' ) OR (start_date <= '{$start_date_string}' AND end_date >= '{$end_date_string}' ) )"; } } // specific rows by google_calendar_event_id if ( isset( $args['google_calendar_event_id'] ) ) { $where .= $wpdb->prepare( " AND `google_calendar_event_id` = '" . '%s' . "' ", $args['google_calendar_event_id'] ); } if ( isset( $args['search'] ) ) { $where .= $wpdb->prepare( " AND `customer_information` LIKE '" . '%s' . "' ", "%" . $args['search'] . "%" ); } return $where; } public function create_item_permissions_check( $request ) { return $this->nonce_permissions_check( $request ); } /** * Check if a given request has access to update a specific item * * @param WP_REST_Request $request Full data about the request. * @return WP_Error|bool */ public function update_item_permissions_check( $request ) { if ( true === $this->get_item_permissions_check( $request ) ) { return true; } return false; } public function group_cancel( $request ) { $params = $request->get_params(); if ( empty( $params['id'] ) ) { return; } $appointment_arrays = $this->query( array( 'number' => -1, 'group_id' => $params['id'], ) ); foreach ( $appointment_arrays as $appointment_array ) { if ( 'canceled' === $appointment_array['status'] ) { continue; } $this->update( $appointment_array['id'], array( 'status' => 'canceled', ) ); } return true; } public function group_delete( $request ) { $params = $request->get_params(); if ( empty( $params['id'] ) ) { return; } $appointment_arrays = $this->query( array( 'number' => -1, 'group_id' => $params['id'], ) ); foreach ( $appointment_arrays as $appointment_array ) { $this->delete( $appointment_array['id'] ); } return true; } public function is_prospective_appointment_available( SSA_Appointment_Type_Object $appointment_type, DateTimeImmutable $start_date, $args = array() ) { $period = new Period( $start_date->sub( $appointment_type->get_buffered_duration_interval() ), $start_date->add( $appointment_type->get_buffered_duration_interval() ) ); $args = shortcode_atts( array( 'cache_level_read' => false, // check the database, bypass cache 'cache_level_write' => false, // don't cache the narrow-range query 'excluded_appointment_ids' => array(), ), $args ); $availability_query = new SSA_Availability_Query( $appointment_type, $period, $args ); $prospective_appointment = SSA_Appointment_Factory::create( $appointment_type, array( 'id' => 0, 'start_date' => $start_date->format( 'Y-m-d H:i:s' ), ) ); $is_period_available = $availability_query->is_prospective_appointment_bookable( $prospective_appointment ); return $is_period_available; } public function get_item( $request ) { $response = parent::get_item( $request ); $params = $request->get_params(); if( !empty( $params['fetch'] )){ $appointment_object = new SSA_Appointment_Object( $response->data['data']['id'] ); $response->data['data'] = $appointment_object->get_data( 0, $params['fetch'] ); } return $response; } public function create_item( $request ) { $params = $request->get_params(); $params = shortcode_atts( array_merge( $this->get_field_defaults(), array( 'appointment_type_id' => '', 'rescheduled_to_appointment_id' => '', 'start_date' => '', 'customer_information' => array(), 'post_information' => array(), 'customer_id' => 0, 'fetch' => array(), 'mepr_membership' => array(), 'staff_ids' => array(), 'selected_resources' => array(), 'opt_in_notifications' => false, ) ), $params ); if ( empty( $params['appointment_type_id'] ) ) { return array( 'error' => array( 'code' => 'appointment_type_required', 'message' => __( 'An error ocurred, please choose an appointment type before booking.', 'simply-schedule-appointments' ), 'data' => array(), ), ); } $appointment_type = SSA_Appointment_Type_Object::instance( $params['appointment_type_id'] ); $appointment_type_duration = $appointment_type->duration; if ( empty( $appointment_type_duration ) ) { return array( 'error' => array( 'code' => 'appointment_type_not_found', 'message' => __( 'An error ocurred, that appointment type was not found.', 'simply-schedule-appointments' ), 'data' => array(), ), ); } if ( ! empty( $params['customer_information']['Email'] ) ) { $user_by_email = get_user_by( 'email', sanitize_text_field( $params['customer_information']['Email'] ) ); if ( ! empty( $user_by_email ) ) { $params['customer_id'] = $user_by_email->ID; } } if ( empty( $params['customer_id'] ) ) { if ( ! current_user_can( 'ssa_manage_appointments' ) ) { $params['customer_id'] = get_current_user_id(); } } // if ( empty( $params['selected_resources'] ) ) { // $params['selected_resources'] = get_current_user_id(); // } $request->set_body_params( $params ); // Double check availability before we insert $appointment_type = SSA_Appointment_Type_Object::instance( $params['appointment_type_id'] ); $start_date = new DateTimeImmutable( $params['start_date'] ); $is_period_available = $this->is_prospective_appointment_available( $appointment_type, $start_date ); if ( empty( $is_period_available ) ) { return array( 'error' => array( 'code' => 'appointment_unavailable', 'message' => __( 'Sorry, that time was just booked and is no longer available', 'simply-schedule-appointments' ), 'data' => array(), ), ); } $prospective_appointment = SSA_Appointment_Factory::create( $appointment_type, array( 'id' => 0, 'start_date' => $start_date->format( 'Y-m-d H:i:s' ), ) ); $params['end_date'] = $prospective_appointment->end_date; // if ( apply_filters( 'ssa/scalability/preemptively_clear_cache', false ) ) { // $this->plugin->availability_cache_invalidation->invalidate_prospective_appointment( $prospective_appointment ); // } // extract meta data // Store booking page information if ( isset( $params['post_information'] ) ) { $appointment_meta = array(); if ( isset( $params['post_information']['booking_url'] ) && ! empty( $params['post_information']['booking_url'] ) ) { $appointment_meta['booking_url'] = esc_url( $params['post_information']['booking_url'] ); } if ( isset( $params['post_information']['booking_post_id'] ) && ! empty( $params['post_information']['booking_post_id'] ) ) { $appointment_meta['booking_post_id'] = intval( $params['post_information']['booking_post_id'] ); } if ( isset( $params['post_information']['booking_title'] ) && ! empty( $params['post_information']['booking_title'] ) ) { $appointment_meta['booking_title'] = html_entity_decode( urldecode( esc_attr( $params['post_information']['booking_title'] ) ) ); } if ( ! empty( $appointment_meta ) ) { $params['meta_data'] = $appointment_meta; } } // we've duplicated the code from class-td-api-model.php so we use explicit (modified) $params, rather than the original $request object passed into the API so we can update the end date $insert_id = $this->insert( $params ); if ( empty( $insert_id ) ) { $response = array( 'response_code' => '500', 'error' => 'Not created', 'data' => array(), ); } elseif ( is_wp_error( $insert_id ) ) { $response = array( 'response_code' => '500', 'error' => true, 'data' => $insert_id, ); } elseif ( is_array( $insert_id ) && ! empty( $insert_id['error']['code'] ) ) { $response = array( 'response_code' => $insert_id['error']['code'], 'error' => $insert_id['error']['message'], 'data' => empty( $insert_id['error']['data'] ) ? [] : $insert_id['error']['data'], ); } else { $response = array( 'response_code' => 200, 'error' => '', 'data' => $this->get( $insert_id ), ); } $response = new WP_REST_Response( $response, 200 ); if ( is_a( $response->data['data'], 'WP_Error' ) ) { return $response; } if ( ! empty( $response->data['error'] ) ) { if ( $response->data['error'] == 'Not created' ) { if ( current_user_can( 'ssa_manage_appointments' ) ) { $response->data['error'] = __( 'Could not insert appointment into database.', 'simply-schedule-appointments' ); } else { $response->data['error'] = __( 'There was a problem booking your appointment.', 'simply-schedule-appointments' ); } } return $response; } $appointment_object = new SSA_Appointment_Object( $response->data['data']['id'] ); $response->data['data'] = $appointment_object->get_data( 0, $params['fetch'] ); // $response->data['data']['ics']['customer'] = $appointment_object->get_ics( 'customer' )['file_url']; // if ( current_user_can( 'ssa_manage_site_settings' ) ) { // $response->data['data']['ics']['staff'] = $appointment_object->get_ics( 'staff' )['file_url']; // } // $response->data['data']['gcal']['customer'] = $appointment_object->get_gcal_add_link( 'customer' ); return $response; } public function update_item( $request ) { $item_id = $request['id']; $params = $request->get_params(); if ( ! empty( $params['appointment_type_id'] ) ) { $appointment_type = new SSA_Appointment_Type_Object( $params['appointment_type_id'] ); } else { $appointment = new SSA_Appointment_Object( $item_id ); $appointment_type = $appointment->get_appointment_type(); } /** @var \undefined|\SSA_Appointment_Object $appointment */ /** @var SSA_Appointment_Type_Object $appointment_type */ if ( empty( $params['fetch'] ) ) { $params['fetch'] = array(); $request->set_param( 'fetch', $params['fetch'] ); } // TODO: allow staff_id to be specified by booking app // if ( isset( $params['staff_ids'] ) ) { // if ( ! current_user_can( 'ssa_manage_others_appointments' ) ) { // $params['staff_ids'] = false; // } // } if ( empty( $params['start_date'] ) || '0000-00-00 00:00:00' == $params['start_date'] ) { if ( isset( $params['start_date'] ) ) { unset( $params['start_date'] ); } if ( isset( $params['end_date'] ) ) { unset( $params['end_date'] ); } } if ( ! empty( $params['start_date'] ) ) { try { $start_date = ssa_datetime( $params['start_date'] ); if ( empty( $start_date ) ) { return 'invalid_start_date'; } // if the time was changed, confirm availability if( isset( $appointment->data['start_date'] ) && $appointment->data['start_date'] !== $params['start_date'] ){ // Make sure to exclude current appointmnent while checking availability $args = array( 'excluded_appointment_ids' => array( $item_id ) ); $is_period_available = $this->is_prospective_appointment_available( $appointment_type, $start_date, $args ); if ( empty( $is_period_available ) ) { return array( 'error' => array( 'code' => 'appointment_unavailable', 'message' => __( 'Sorry, that time was just booked and is no longer available', 'simply-schedule-appointments' ), 'data' => array(), ), ); } } } catch ( Exception $e ) { return 'invalid_start_date'; } $bookable_period = Period::after( $start_date, $appointment_type->get_duration_interval() ); $params['end_date'] = $bookable_period->getEndDate()->format( 'Y-m-d H:i:s' ); } // updating appointment meta was not needed here, so we're only using it for tracking the rescheduling if( isset( $params['start_date'] ) && isset( $appointment->data['start_date'] ) && $appointment->data['start_date'] !== $params['start_date'] ){ // construct meta data $appointment_meta = $this->get_metas( $item_id ); if ( ! isset( $appointment_meta['rescheduled_from_start_dates'] ) ) { $appointment_meta['rescheduled_from_start_dates'] = array(); } // add the previous start date to the meta $appointment_meta['rescheduled_from_start_dates'][] = $appointment->data['start_date']; // include the meta data so it gets updated $params['meta_data'] = $appointment_meta; $capacity_type = $appointment_type->capacity_type; // if belonged to a group event, remove old event, inherit new one if it exists, or keep empty if ( ! empty( $capacity_type ) && $capacity_type === 'group' ) { $previous_group_appointments_array = $this->plugin->capacity->get_matching_group_appointments( $appointment->data, $appointment_type ); // only detach if there are other appointments in the group if ( ! empty( $previous_group_appointments_array ) && count( $previous_group_appointments_array ) > 1 ) { // default to the current appointment id as group_id, maybe override in the next step $params['group_id'] = $item_id; // also detach from shared details $params['web_meeting_password'] = ''; $params['web_meeting_id'] = ''; $params['web_meeting_url'] = ''; // we remove ref to calendar details // because if this was the parent, it should not move all group with it // if it's not the parent, these will already be empty $params['google_calendar_id'] = ''; $params['google_calendar_event_id'] = ''; } $group_appointments_array = $this->plugin->capacity->get_matching_group_appointments( $params, $appointment_type ); if ( ! empty( $group_appointments_array ) ) { // find the parent appointment foreach( $group_appointments_array as $index => $group_appointment ){ if( $group_appointment['id'] != $item_id && $group_appointment['id'] == $group_appointment['group_id']){ // copy select fields over from group parent $params['group_id'] = $group_appointment['group_id']; $params['web_meeting_password'] = $group_appointment['web_meeting_password']; $params['web_meeting_id'] = $group_appointment['web_meeting_id']; $params['web_meeting_url'] = $group_appointment['web_meeting_url']; break; } } } } } // if existing appointment has status of booked, prevent updating it to a status of abandoned // this can happen in edge cases where the booking app sends an update to mark a booked appointment as abandoned // this is not supported behavior so we just prevent it. if ( isset( $params['status'] ) && in_array( $params['status'], ['abandoned', 'pending_form']) && isset( $appointment->data['status'] ) && 'booked' === $appointment->data['status'] ) { // log stack trace ssa_debug_log( "Cannot update status from booked to " . $params['status'], 10 ); ssa_debug_log( ssa_get_stack_trace(), 10 ); return array( 'error' => array( 'code' => 'invalid_status', 'message' => __( 'Cannot update status to abandoned', 'simply-schedule-appointments' ), 'data' => array(), ), ); } $this->update( $item_id, $params ); $response_array = array( 'response_code' => 200, 'error' => '', 'data' => $this->get( $item_id ), ); $response = new WP_REST_Response( $response_array, 200 ); $appointment_object = new SSA_Appointment_Object( $item_id ); $response->data['data'] = $appointment_object->get_data( 0, $params['fetch'] ); if ( is_a( $response->data['data'], 'WP_Error' ) ) { return $response; } return $response; } public function register_custom_routes() { $namespace = $this->api_namespace . '/v' . $this->api_version; $base = $this->get_api_base(); register_rest_route( $namespace, '/' . $base . '/(?P[\d]+)/ics', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_item_ics' ), 'permission_callback' => array( $this, 'get_item_permissions_check' ), 'args' => array( 'context' => array( 'default' => 'view', ), ), ), ) ); register_rest_route( $namespace, '/' . $base . '/(?P[\d]+)/ics/download/customer', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'download_item_ics_customer' ), 'permission_callback' => array( $this, 'id_token_permissions_check' ), 'args' => array( 'context' => array( 'default' => 'view', ), ), ), ) ); // Since the ability to download the "staff" ics is only available to staff, we need to check if the current user has permissions. register_rest_route( $namespace, '/' . $base . '/(?P[\d]+)/ics/download/staff', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'download_item_ics_staff' ), 'permission_callback' => array( $this, 'get_item_permissions_check' ), 'args' => array( 'context' => array( 'default' => 'view', ), ), ), ) ); register_rest_route( $namespace, '/' . $base . '/(?P[\d]+)/meta', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_item_meta' ), 'permission_callback' => array( $this, 'get_item_permissions_check' ), 'args' => array( 'context' => array( 'default' => 'view', ), ), ), ) ); register_rest_route( $namespace, '/' . $base . '/(?P[\d]+)/meta', array( array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'update_item_meta' ), 'permission_callback' => array( $this, 'get_item_permissions_check' ), 'args' => array( 'context' => array( 'default' => 'view', ), ), ), ) ); register_rest_route( $namespace, '/' . $base . '/groups/(?P[\d]+)/cancel', array( array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'group_cancel' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), 'args' => array( 'context' => array( 'default' => 'view', ), ), ), ) ); register_rest_route( $namespace, '/' . $base . '/groups/(?P[\d]+)/delete', array( array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'group_delete' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), 'args' => array( 'context' => array( 'default' => 'view', ), ), ), ) ); register_rest_route( $namespace, '/' . $base . '/purge', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'purge_appointments' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), ), ) ); register_rest_route( $namespace, '/' . $base . '/availability/(?P[\d]+)', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'availability' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), ), ) ); } /** * Check if the appointment time slot is still available * Needed when changing the status from canceled/abandoned to booked * * @since 6.4.17 * * @param WP_REST_Request $request the Request object. * @return WP_REST_Response */ public function availability( WP_REST_Request $request ) { $params = $request->get_params(); $appointment_obj = new SSA_Appointment_Object( $params['id'] ); // Don't check for reserved statuses if ( $appointment_obj->is_reserved() ) { $data = array( 'result' => 'reserved', 'message' => __( 'This time slot is reserved for this appointment', 'simply-schedule-appointments' ), ); $response = array( 'response_code' => 200, 'error' => '', 'data' => $data, ); return new WP_REST_Response( $response, 200 ); } // check if a time slot is still available $is_period_available = $this->is_prospective_appointment_available( $appointment_obj->get_appointment_type(), $appointment_obj->start_date_datetime ); if ( empty( $is_period_available ) ) { // time slot already booked $data = array( 'result' => 'unavailable', 'message' => __( 'The time slot for this appointment was booked and is no longer available.', 'simply-schedule-appointments' ), ); } else { $data = array( 'result' => 'available', 'message' => __( 'The time slot for this appointment is still available.', 'simply-schedule-appointments' ), ); } $response = array( 'response_code' => 200, 'error' => '', 'data' => $data, ); return new WP_REST_Response( $response, 200 ); } /** * Get a collection of items * * @param WP_REST_Request $request Full data about the request. * @return WP_Error|WP_REST_Response */ public function get_items( $request ) { $params = $request->get_params(); if ( is_user_logged_in() && ! current_user_can( 'ssa_manage_others_appointments' ) ) { global $wpdb; if ( ! current_user_can( 'ssa_manage_appointments' ) ) { $params['customer_id'] = get_current_user_id(); } else { $params['append_where_sql'] = $wpdb->prepare( ' AND id IN (SELECT appointment_id FROM ' . ssa()->staff_appointment_model->get_table_name() . ' WHERE staff_id = %d)', $this->plugin->staff_model->get_staff_id_for_user_id( get_current_user_id() ) ); } } $schema = $this->get_schema(); // Check if format=ics is defined. $is_ics = false; if ( isset( $params['format'] ) && 'ics' === $params['format'] ) { $is_ics = true; unset( $params['format'] ); } $data = $this->query( $params ); // If complete_group is set, fetch additional appointments to complete any partial groups if ( ! empty( $params['complete_group'] ) ) { $data = $this->complete_group_appointments( $data ); } foreach( $data as $index => $appointment ) { $data[$index] = $this->format_multiline_customer_information($appointment); } if ( $is_ics ) { $appointments = array_map( function( $row ) { return SSA_Appointment_Object::from_data( $row ); }, $data ); $ics_exporter = new SSA_Ics_Exporter(); $ics_exporter->template = 'customer'; $ics_feed = $ics_exporter->get_ics_feed( $appointments, 'staff' ); foreach ( $ics_feed['headers'] as $header_key => $header_value ) { header( $header_key . ': ' . $header_value ); } echo $ics_feed['data']; // phpcs:ignore WordPress.Security.EscapeOutput exit; } else { $data = $this->prepare_collection_for_api_response( $data ); $response = array( 'response_code' => 200, 'error' => '', 'data' => $data, ); return new WP_REST_Response( $response, 200 ); } } public function get_items_permissions_check( $request ) { if ( current_user_can( 'ssa_manage_others_appointments' ) ) { return true; } if ( current_user_can( 'ssa_manage_appointments' ) ) { return true; } $settings = ssa()->settings->get(); $params = $request->get_params(); if ( ! empty( $params['token'] ) && $params['token'] == $settings['global']['public_read_access_token'] ) { return true; } if ( true === parent::get_item_permissions_check( $request ) ) { return true; } if ( true === $this->id_token_permissions_check( $request ) ) { return true; } if ( true === $this->token_permissions_check( $request ) ) { return true; } return false; } public function get_item_permissions_check( $request ) { if ( current_user_can( 'ssa_manage_others_appointments' ) ) { return true; } if ( current_user_can( 'ssa_manage_appointments' ) ) { $params = $request->get_params(); if ( ! empty( $params['id'] ) && $this->plugin->staff_appointment_model->user_has_appointment_id( get_current_user_id(), (int) $params['id'] ) ) { return true; } } $params = $request->get_params(); if ( true === parent::get_item_permissions_check( $request ) ) { return true; } if ( true === $this->id_token_permissions_check( $request ) ) { return true; } if ( is_user_logged_in() ) { $appointment = new SSA_Appointment_Object( $params['id'] ); if ( $appointment->customer_id == get_current_user_id() ) { return true; } } return apply_filters( 'ssa/appointment/get_item_permissions_check', false, $params, $request ); } /** * Given a specific appointment ID, return the download url for the .ics file(s). * * @param WP_REST_Request $request Full data about the request. * @return WP_REST_Response $response The response data. */ public function get_item_ics( $request ) { $params = $request->get_params(); $appointment = new SSA_Appointment_Object( $params['id'] ); $customer_ics = $appointment->get_ics_download_url( 'customer' ); $response = array( 'customer' => $customer_ics, ); if ( current_user_can( 'ssa_manage_appointments' ) ) { $staff_ics = $appointment->get_ics_download_url( 'customer' ); $response['staff'] = $staff_ics; } return new WP_REST_Response( $response, 200 ); } /** * Returns the REST API base for the ICS endpoint. * * @since 5.4.4 * * @return string */ public function get_ics_endpoints_base() { $namespace = $this->api_namespace . '/v' . $this->api_version; $base = $this->get_api_base(); return get_rest_url( null, $namespace . '/' . $base . '/' ); } /** * Given a specific appointment ID, generate the .ics file content for download, set up the headers, and print the content. * * @since 5.4.4 * * @param WP_REST_Request $request Full data about the request. * @return WP_REST_Response $response The response data. */ public function download_item_ics_customer( $request ) { $params = $request->get_params(); $appointment_object = new SSA_Appointment_Object( $params['id'] ); $customer_ics = $appointment_object->get_ics( 'customer' ); foreach ( $customer_ics['headers'] as $header_key => $header_value ) { header( $header_key . ': ' . $header_value ); } echo $customer_ics['data']; // phpcs:ignore WordPress.Security.EscapeOutput exit; } /** * Given a specific appointment ID, generate the .ics file content for download, set up the headers, and print the content. * * @since 5.4.4 * * @param WP_REST_Request $request Full data about the request. * @return WP_REST_Response $response The response data. */ public function download_item_ics_staff( $request ) { $params = $request->get_params(); $appointment_object = new SSA_Appointment_Object( $params['id'] ); $staff_ics = $appointment_object->get_ics( 'staff' ); foreach ( $staff_ics['headers'] as $header_key => $header_value ) { header( $header_key . ': ' . $header_value ); } echo $staff_ics['data']; // phpcs:ignore WordPress.Security.EscapeOutput exit; } public function insert( $data, $type = '' ) { $response = array(); $wp_error = new WP_Error(); if ( empty( $data['appointment_type_id'] ) ) { $wp_error->add( 422, 'appointment_type_id required' ); } if ( empty( $data['start_date'] ) ) { $wp_error->add( 422, 'start_date required' ); } if ( empty( $data['customer_information'] ) ) { if ( empty( $data['status'] ) || $data['status'] !== 'pending_form' ) { $wp_error->add( 422, 'customer_information required' ); } } if ( ! empty( $wp_error->errors ) ) { return $wp_error; } ssa_defensive_timezone_fix(); $data['appointment_type_id'] = sanitize_text_field( $data['appointment_type_id'] ); $data['start_date'] = sanitize_text_field( $data['start_date'] ); if ( empty( $data['start_date'] ) ) { return 'invalid_start_date'; } try { $start_date = ssa_datetime( $data['start_date'] ); if ( empty( $start_date ) ) { return 'invalid_start_date'; } } catch ( Exception $e ) { return 'invalid_start_date'; } $appointment_type = $this->plugin->appointment_type_model->get( $data['appointment_type_id'] ); $bookable_period = Period::after( $start_date, new DateInterval( 'PT' . $appointment_type['duration'] . 'M' ) ); $data['end_date'] = $bookable_period->getEndDate()->format( 'Y-m-d H:i:s' ); if ( false !== strpos( $data['customer_timezone'], 'Etc/' ) ) { $data['customer_timezone'] = ''; } $appointment_id = parent::insert( $data, $type ); ssa_defensive_timezone_reset(); return $appointment_id; } public function get_staff_ids( $appointment_id, $appointment = array() ) { if ( ! $this->plugin->settings_installed->is_enabled( 'staff' ) ) { return array(); } $cache_key = 'appointment/' . $appointment_id . '/staff_ids'; $cached = ssa_cache_get( $cache_key ); if ( $cached !== false ) { return $cached; } $data = $this->plugin->staff_appointment_model->get_staff_ids( $appointment_id ); ssa_cache_set( $cache_key, $data, '', 5 ); return $data; } public function get_selected_resources($appointment_id, $appointment = array()) { if ( ! $this->plugin->settings_installed->is_enabled( 'resources' ) ) { return array(); } $cache_key = 'appointment/' . $appointment_id . '/resources'; $cached = ssa_cache_get( $cache_key ); if ( $cached !== false ) { return $cached; } $data = $this->plugin->resource_appointment_model->get_resources( $appointment_id ); ssa_cache_set( $cache_key, $data, '', 5 ); return $data; } public function get_public_edit_url( $appointment_id, $appointment = array() ) { $appointment['id'] = $appointment_id; $token = $this->get_id_token( $appointment ); // Get Correct Url and Add Appointment Token $url = home_url(); $settings_global = ssa()->settings->get()['global']; $edit_appointment_page_id = apply_filters( 'ssa/edit_appointment_page_id', $settings_global['edit_appointment_page_id'], $appointment_id ); $edit_appointment_page_link = get_permalink( $edit_appointment_page_id ); if ( !empty($edit_appointment_page_id) && !empty($edit_appointment_page_link)) { $url = $edit_appointment_page_link; } $url = add_query_arg( array( 'appointment_action' => 'edit', 'appointment_token' => $token . $appointment_id,) , $url ); return $url; } public function get_admin_edit_url( $appointment_id, $appointment = array() ) { $url = $this->plugin->wp_admin->url( 'ssa/appointment/' . $appointment_id ); return $url; } public function prepare_item_for_response( $item, $recursive = 0 ) { $item = parent::prepare_item_for_response( $item, $recursive ); if ( $recursive >= 0 ) { $item['public_edit_url'] = $this->get_public_edit_url( $item['id'], $item ); $item['public_token'] = $this->get_id_token( $item['id'] ); $item['staff_ids'] = $this->get_staff_ids( $item['id'] ); $item['selected_resources'] = $this->get_selected_resources( $item['id'] ); $item['label_id'] = $this->get_label_id( $item['id'] ); $item['rescheduling_note'] = $this->get_rescheduling_note( $item['id'] ); $item['meta'] = $this->get_metas( $item['id'] ); $item['payments'] = $this->get_payments_for_appointment( $item['id'] ); } return $item; } public function get_rescheduling_note( $id ) { $meta = $this->get_metas( $id, array( 'rescheduling_note' ) ); return isset( $meta['rescheduling_note'] ) ? $meta['rescheduling_note'] : ""; } /** * Get the payments for a specific appointment. * Only returns payments if the payments module is installed and enabled. * * @param [type] $appointment_id * @return void */ public function get_payments_for_appointment( $appointment_id ) { if ( ! $this->plugin->settings_installed->is_enabled( 'payments' ) ) { return array(); } return $this->plugin->payment_model->query( array( 'appointment_id' => $appointment_id ) ); } /** * DEPRECATED: Alias function for backward compatibility. * * This function is deprecated and should not be used in new code. * It is provided only for backward compatibility with older versions. * Please use the recommended alternative bulk_meta_update() instead. * * @deprecated Deprecated since version 6.5.8 * * @see bulk_meta_update() */ public function update_metas( $appointment_id, array $meta_keys_and_values ) { return $this->plugin->appointment_meta_model->bulk_meta_update( $appointment_id, $meta_keys_and_values ); } public function get_item_meta( $request ) { $params = $request->get_params(); $appointment_id = esc_attr( $params['id'] ); $data = array(); if ( empty( $params['keys'] ) ) { $data = $this->get_metas( $appointment_id ); } elseif ( is_string( $params['keys'] ) ) { $data = array( $params['keys'] => $this->get_meta( $appointment_id, $params['keys'] ), ); } elseif ( is_array( $params['keys'] ) ) { $data = $this->get_metas( $appointment_id, $params['keys'] ); } $response = array( 'response_code' => 200, 'error' => '', 'data' => $data, ); return new WP_REST_Response( $response, 200 ); } public function update_item_meta( $request ) { $params = $request->get_params(); $appointment_id = esc_attr( $params['id'] ); if(isset($params['meta'])){ $metas = $params['meta']; } else { $metas = $params; } $meta_keys_and_values = array(); $excluded_keys = array( 'id', 'context' ); foreach ( $metas as $key => $value ) { if ( in_array( $key, $excluded_keys ) ) { continue; } $meta_keys_and_values[ $key ] = esc_attr( trim( $value ) ); } $this->plugin->{$this->slug.'_meta_model'}->bulk_meta_update( $appointment_id, $meta_keys_and_values ); $response = array( 'response_code' => 200, 'error' => '', 'data' => $meta_keys_and_values, ); return new WP_REST_Response( $response, 200 ); } public function get_metas( $appointment_id, array $meta_keys = array() ) { $data = array(); if ( empty( $meta_keys ) ) { // return all keys and values $rows = $this->plugin->appointment_meta_model->query( array( 'appointment_id' => $appointment_id, ) ); foreach ( $rows as $key => $row ) { $data[ $row['meta_key'] ] = $row['meta_value']; } } if ( count( $meta_keys ) > 3 ) { // For performance, perform single SQL query and filter in PHP // instead of running lots of individual queries against meta table $rows = $this->plugin->appointment_meta_model->query( array( 'appointment_id' => $appointment_id, ) ); foreach ( $rows as $key => $row ) { if ( ! empty( $meta_keys ) && ! in_array( $row['meta_key'], $meta_keys ) ) { continue; // request only asked for certain keys and this isn't one of them } $data[ $row['meta_key'] ] = $row['meta_value']; } foreach ( $meta_keys as $key ) { if ( ! isset( $data[ $key ] ) ) { $data[ $key ] = null; } } foreach ( $data as $key => $value ) { if ( ! in_array( $key, $meta_keys ) ) { unset( $data[ $key ] ); } } } else { foreach ( $meta_keys as $meta_key ) { $data[ $meta_key ] = $this->get_meta( $appointment_id, $meta_key ); } } return $data; } public function get_meta( $appointment_id, $meta_key ) { $data = $this->plugin->appointment_meta_model->query( array( 'appointment_id' => $appointment_id, 'meta_key' => $meta_key, 'order_by' => 'id', 'order' => 'DESC', 'limit' => 1, ) ); if ( empty( $data['0'] ) ) { return null; } return $data['0']['meta_value']; } public function delete_abandoned( DateTimeImmutable $date_modified_max = null ) { global $wpdb; if ( empty( $date_modified_max ) ) { $date_modified_max = ssa_datetime( '-1 day' ); } $sql = 'DELETE FROM ' . $this->get_table_name() . ' WHERE status = "abandoned" AND date_modified < %s'; $sql = $wpdb->prepare( $sql, $date_modified_max->format( 'Y-m-d H:i:s' ) ); $wpdb->get_results( $sql ); } public function update_rescheduled_to_appointment_id( $appointment_id, $data, $data_before = array() ) { if ( empty( $data['rescheduled_from_appointment_id'] ) ) { return; } $rescheduled_from_appointment_id = $data['rescheduled_from_appointment_id']; if ( $rescheduled_from_appointment_id ) { // Get Payment ID of Previous Appointment $payments = $this->plugin->payment_model->query( array( 'appointment_id' => $rescheduled_from_appointment_id, ) ); foreach ($payments as $key => $payment) { // Update Associated Payments to Point to New Appointment $this->plugin->payment_model->update( $payment["id"], ["appointment_id" => $appointment_id] ); } $this->update( $rescheduled_from_appointment_id, array( 'rescheduled_to_appointment_id' => $appointment_id, ) ); } } /** * Purge past and / or deleted appointments. Also generates a .csv backup file and returns it's url. * * @since 4.8.9 * * @param WP_REST_Request $request the Request object. * @return WP_REST_Response */ public function purge_appointments( WP_REST_Request $request ) { $params = $request->get_params(); global $wpdb; $date_modified_max = ssa_datetime(); // Combine past and future canceled appointments into one condition. if ( isset( $params['purge_past_canceled_appointments'] ) && 'true' === $params['purge_past_canceled_appointments'] && isset( $params['purge_future_canceled_appointments'] ) && 'true' === $params['purge_future_canceled_appointments'] ) { $params['purge_all_canceled_appointments'] = 'true'; unset( $params['purge_past_canceled_appointments'] ); unset( $params['purge_future_canceled_appointments'] ); } // Unset past canceled if past appointments is already selected if ( isset( $params['purge_past_appointments'] ) && 'true' === $params['purge_past_appointments'] && isset( $params['purge_past_canceled_appointments'] ) && 'true' === $params['purge_past_canceled_appointments'] ) { unset( $params['purge_past_canceled_appointments'] ); } $conditions = array(); if ( isset( $params['purge_abandoned_appointments'] ) && 'true' === $params['purge_abandoned_appointments'] ) { $conditions[] = 'status = "abandoned"'; } if ( isset( $params['purge_past_appointments'] ) && 'true' === $params['purge_past_appointments'] ) { $conditions[] = $wpdb->prepare( 'end_date < %s', $date_modified_max->format( 'Y-m-d' ) ); } if ( isset( $params['purge_past_appointments'] ) && 'false' === $params['purge_abandoned_appointments'] && isset( $params['purge_past_canceled_appointments'] ) && 'true' === $params['purge_past_canceled_appointments'] ) { $conditions[] = $wpdb->prepare( '(status = "canceled" AND end_date < %s)', $date_modified_max->format( 'Y-m-d' ) ); } if ( isset( $params['purge_future_canceled_appointments'] ) && 'true' === $params['purge_future_canceled_appointments'] ) { $conditions[] = $wpdb->prepare( '(status = "canceled" AND end_date > %s)', $date_modified_max->format( 'Y-m-d' ) ); } if ( isset( $params['purge_all_canceled_appointments'] ) && 'true' === $params['purge_all_canceled_appointments'] ) { $conditions[] = 'status = "canceled"'; } if ( empty( $conditions ) ) { return new WP_REST_Response( __( 'Nothing to delete.', 'simply-schedule-appointments' ), 404 ); } // first, let's get a list of all appointments that will be deleted. $sql = 'SELECT * FROM ' . $this->get_table_name() . ' WHERE ' . implode( ' OR ', $conditions ); $list = $wpdb->get_results( $sql, ARRAY_A ); if ( empty( $list ) ) { return new WP_REST_Response( __( 'No appointments found to be deleted.', 'simply-schedule-appointments' ), 404 ); } // before we delete the appointments, we need to generate a .csv file containing a backup. $csv = $this->generate_appointments_backup( $list ); // if something went wrong, bail. if ( is_wp_error( $csv ) ) { return new WP_REST_Response( $csv->get_error_message(), 500 ); } $sql = 'DELETE FROM ' . $this->get_table_name() . ' WHERE ' . implode( ' OR ', $conditions ); $sql = $wpdb->prepare( $sql, $date_modified_max->format( 'Y-m-d' ) ); $results = $wpdb->query( $sql ); if ( false === $results ) { return new WP_REST_Response( __( 'Something went wrong while deleting the appointments. Please try again.', 'simply-schedule-appointments' ), 500 ); } return new WP_REST_Response( $csv, 200 ); } /** * Given some search conditions, generate and store a .csv file of appointments. * * @since 4.8.9 * * @param array $list An array of appointments. * @return array|WP_Error */ public function generate_appointments_backup( $list ) { $csv = $this->plugin->csv_exporter->get_csv( $list ); return $csv; } public function get_label_id( $id ){ $appointment_object = new SSA_Appointment_Object( $id ); return $appointment_object->get_label_id(); } /** * Complete group appointments by fetching any missing appointments from partial groups. * * When appointments are fetched with pagination, group appointments may be split across pages. * This method ensures all appointments belonging to the same group are returned together. * * @since 6.7.0 * * @param array $data The initially fetched appointments. * @return array The appointments with any missing group members added. */ public function complete_group_appointments( $data ) { if ( empty( $data ) ) { return $data; } // Collect all group_ids and track which appointment IDs we already have $group_ids = array(); $existing_appt_ids = array(); foreach ( $data as $appointment ) { if ( ! empty( $appointment['group_id'] ) && $appointment['group_id'] > 0 ) { $group_ids[] = $appointment['group_id']; } $existing_appt_ids[] = $appointment['id']; } $group_ids = array_unique( $group_ids ); // No groups found, return original data if ( empty( $group_ids ) ) { return $data; } // Query for appointments in each group that we don't already have foreach ( $group_ids as $group_id ) { $group_appointments = $this->query( array( 'group_id' => $group_id, 'number' => -1, ) ); foreach ( $group_appointments as $appointment ) { if ( ! in_array( $appointment['id'], $existing_appt_ids, true ) ) { $data[] = $appointment; $existing_appt_ids[] = $appointment['id']; } } } return $data; } }