2026-03-27 11:54:51 +00:00

2036 lines
63 KiB
PHP

<?php
/**
* Simply Schedule Appointments Appointments Model.
*
* @since 0.0.3
* @package Simply_Schedule_Appointments
*/
use League\Period\Period;
/**
* Simply Schedule Appointments Appointments Model.
*
* @since 0.0.3
*/
class SSA_Appointment_Model extends SSA_Db_Model {
protected $slug = 'appointment';
protected $version = '2.7.3';
/**
* Parent plugin class.
*
* @since 0.0.2
*
* @var Simply_Schedule_Appointments
*/
protected $plugin = null;
/**
* Constructor.
*
* @since 0.0.2
*
* @param Simply_Schedule_Appointments $plugin Main plugin object.
*/
public function __construct( $plugin ) {
// $this->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<id>[\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<id>[\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<id>[\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<id>[\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<id>[\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<id>[\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<id>[\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<id>[\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;
}
}