__( 'Acuity Scheduling', 'advanced-form-integration' ),
'tasks' => array(
'create_appointment' => __( 'Create Appointment', 'advanced-form-integration' ),
),
);
return $actions;
}
/**
* Add settings tab entry.
*
* @param array $providers Current tabs.
*
* @return array
*/
public function register_settings_tab( $providers ) {
$providers['acuity'] = __( 'Acuity Scheduling', 'advanced-form-integration' );
return $providers;
}
/**
* Render Acuity settings screen.
*
* @param string $current_tab Current tab slug.
*/
public function settings_view( $current_tab ) {
if ( 'acuity' !== $current_tab ) {
return;
}
// Load Account Manager
if ( ! class_exists( 'ADFOIN_Account_Manager' ) ) {
require_once plugin_dir_path( __FILE__ ) . '../../includes/class-adfoin-account-manager.php';
}
$fields = array(
array(
'name' => 'user_id',
'label' => __( 'User ID', 'advanced-form-integration' ),
'type' => 'text',
'required' => true,
'placeholder' => __( 'Enter your Acuity User ID', 'advanced-form-integration' ),
'show_in_table' => true,
),
array(
'name' => 'api_key',
'label' => __( 'API Key', 'advanced-form-integration' ),
'type' => 'text',
'required' => true,
'placeholder' => __( 'Enter your Acuity API Key', 'advanced-form-integration' ),
'mask' => true, // Mask API key in table
'show_in_table' => true,
),
);
$instructions = '
- ' . esc_html__( 'Log in to your Acuity Scheduling account.', 'advanced-form-integration' ) . '
- ' . esc_html__( 'Go to Integrations > API > View Credentials.', 'advanced-form-integration' ) . '
- ' . esc_html__( 'Copy your User ID and API Key.', 'advanced-form-integration' ) . '
- ' . esc_html__( 'Click "Add Account" and enter your credentials.', 'advanced-form-integration' ) . '
';
ADFOIN_Account_Manager::render_settings_view( 'acuity', 'Acuity Scheduling', $fields, $instructions );
}
public function get_credentials() {
if ( ! class_exists( 'ADFOIN_Account_Manager' ) ) {
require_once plugin_dir_path( __FILE__ ) . '../../includes/class-adfoin-account-manager.php';
}
ADFOIN_Account_Manager::ajax_get_credentials( 'acuity' );
}
public function save_credentials() {
if ( ! class_exists( 'ADFOIN_Account_Manager' ) ) {
require_once plugin_dir_path( __FILE__ ) . '../../includes/class-adfoin-account-manager.php';
}
ADFOIN_Account_Manager::ajax_save_credentials( 'acuity', array( 'user_id', 'api_key' ) );
}
public function set_credentials( $cred_id ) {
$credentials = adfoin_read_credentials( 'acuity' );
$this->cred_id = '';
$this->user_id = '';
$this->api_key = '';
// Backward compatibility: If no credId, use the first available credential
if ( empty( $cred_id ) && ! empty( $credentials ) && is_array( $credentials ) ) {
$first_credential = reset( $credentials );
$cred_id = isset( $first_credential['id'] ) ? $first_credential['id'] : '';
}
foreach( $credentials as $single ) {
if( $cred_id && $cred_id == $single['id'] ) {
$this->cred_id = $single['id'];
$this->user_id = isset( $single['user_id'] ) ? $single['user_id'] : '';
$this->api_key = isset( $single['api_key'] ) ? $single['api_key'] : '';
return;
}
}
}
/**
* AJAX: fetch appointment types.
*/
public function ajax_get_appointment_types() {
adfoin_verify_nonce();
$cred_id = isset( $_POST['credId'] ) ? sanitize_text_field( wp_unslash( $_POST['credId'] ) ) : '';
$this->set_credentials( $cred_id );
$response = $this->api_request( 'appointment-types', 'GET' );
if ( is_wp_error( $response ) ) {
/* translators: %s error message from Acuity. */
wp_send_json_error( sprintf( __( 'Could not load appointment types: %s', 'advanced-form-integration' ), $response->get_error_message() ) );
}
$body = json_decode( wp_remote_retrieve_body( $response ), true );
$types = array();
if ( is_array( $body ) ) {
foreach ( $body as $type ) {
if ( isset( $type['id'], $type['name'] ) ) {
$inactive = isset( $type['active'] ) && ! $type['active'];
$types[ $type['id'] ] = $type['name'] . ( $inactive ? ' ' . __( '(inactive)', 'advanced-form-integration' ) : '' );
}
}
}
wp_send_json_success( $types );
}
/**
* AJAX: fetch calendars.
*/
public function ajax_get_calendars() {
adfoin_verify_nonce();
$cred_id = isset( $_POST['credId'] ) ? sanitize_text_field( wp_unslash( $_POST['credId'] ) ) : '';
$this->set_credentials( $cred_id );
$response = $this->api_request( 'calendars', 'GET' );
if ( is_wp_error( $response ) ) {
/* translators: %s error message from Acuity. */
wp_send_json_error( sprintf( __( 'Could not load calendars: %s', 'advanced-form-integration' ), $response->get_error_message() ) );
}
$body = json_decode( wp_remote_retrieve_body( $response ), true );
$calendars = array();
if ( is_array( $body ) ) {
foreach ( $body as $calendar ) {
if ( isset( $calendar['id'], $calendar['name'] ) ) {
$calendars[ $calendar['id'] ] = $calendar['name'];
}
}
}
wp_send_json_success( $calendars );
}
public function ajax_get_credentials_list() {
adfoin_verify_nonce();
if ( ! class_exists( 'ADFOIN_Account_Manager' ) ) {
require_once plugin_dir_path( __FILE__ ) . '../../includes/class-adfoin-account-manager.php';
}
$fields = array(
array( 'name' => 'user_id', 'mask' => false ),
array( 'name' => 'api_key', 'mask' => true ),
);
ADFOIN_Account_Manager::ajax_get_credentials_list( 'acuity', $fields );
}
/**
* Print action template markup.
*/
public function action_fields() {
?>
strtoupper( $method ),
'headers' => array(
'Authorization' => 'Basic ' . base64_encode( $this->user_id . ':' . $this->api_key ),
'Content-Type' => 'application/json',
'Accept' => 'application/json',
),
'timeout' => 20,
);
if ( in_array( $args['method'], array( 'POST', 'PUT', 'PATCH' ), true ) && ! empty( $data ) ) {
$args['body'] = wp_json_encode( $data );
}
if ( empty( $this->user_id ) || empty( $this->api_key ) ) {
$response = new WP_Error( 'adfoin_acuity_missing_creds', __( 'Connect Acuity Scheduling before running this action.', 'advanced-form-integration' ) );
} else {
$response = wp_remote_request( esc_url_raw( $url ), $args );
}
$code = is_wp_error( $response ) ? 0 : (int) wp_remote_retrieve_response_code( $response );
if ( ! empty( $record ) ) {
if ( $code >= 400 ) {
$response['response']['message'] = adfoin_acuity_error_message( $response );
} elseif ( $code ) {
$body = json_decode( wp_remote_retrieve_body( $response ), true );
$message = isset( $body['id'] ) ? sprintf( 'OK (appointment #%s)', $body['id'] ) : 'OK';
if ( $notes ) {
$message .= ' - ' . implode( '; ', $notes );
}
$response['response']['message'] = $message;
}
adfoin_add_to_log( $response, $url, $args, $record );
}
if ( is_wp_error( $response ) ) {
return $response;
}
if ( $code >= 400 ) {
return new WP_Error( 'adfoin_acuity_http_error', adfoin_acuity_error_message( $response ) );
}
return $response;
}
}
$adfoin_acuity = ADFOIN_Acuity::get_instance();
/**
* Readable error from an Acuity response, e.g. "Attribute "email" is required. (required_email)".
*
* @param array|WP_Error $response HTTP response.
*
* @return string
*/
function adfoin_acuity_error_message( $response ) {
if ( is_wp_error( $response ) ) {
return $response->get_error_message();
}
$body = json_decode( wp_remote_retrieve_body( $response ), true );
if ( ! empty( $body['message'] ) ) {
return $body['message'] . ( ! empty( $body['error'] ) ? ' (' . $body['error'] . ')' : '' );
}
$code = wp_remote_retrieve_response_code( $response );
return trim( $code . ' ' . wp_remote_retrieve_response_message( $response ) );
}
/**
* Log a submission that was not sent to Acuity, with the reason.
*
* @param array $record Log record.
* @param string $message Reason.
*/
function adfoin_acuity_log_skipped( $record, $message ) {
adfoin_add_to_log( new WP_Error( 'acuity_not_sent', $message ), ADFOIN_Acuity::api_base . 'appointments', array( 'method' => 'POST' ), $record );
}
/**
* Map field keys to API payload keys.
*
* @return array
*/
function adfoin_acuity_field_map() {
return array(
'datetime' => 'datetime',
'firstName' => 'firstName',
'lastName' => 'lastName',
'email' => 'email',
'phone' => 'phone',
'timezone' => 'timezone',
'certificate' => 'certificate',
'notes' => 'notes',
'fieldDefinitions' => 'fields',
'addonIds' => 'addonIDs',
'labelId' => 'labels',
);
}
/**
* Job queue listener.
*
* @param array $data Job payload.
*/
function adfoin_acuity_job_queue( $data ) {
if ( ( $data['action_provider'] ?? '' ) !== 'acuity' || ( $data['task'] ?? '' ) !== 'create_appointment' ) {
return;
}
adfoin_acuity_send_data( $data['record'], $data['posted_data'] );
}
add_action( 'adfoin_job_queue', 'adfoin_acuity_job_queue', 10, 1 );
/**
* Main dispatcher for Acuity action.
*
* @param array $record Log record.
* @param array $posted_data Trigger payload.
*/
function adfoin_acuity_send_data( $record, $posted_data ) {
$record_data = json_decode( $record['data'], true );
if ( isset( $record_data['action_data']['cl'] ) && adfoin_check_conditional_logic( $record_data['action_data']['cl'], $posted_data ) ) {
return;
}
$field_data = isset( $record_data['field_data'] ) ? $record_data['field_data'] : array();
$notes = array();
$payload = adfoin_acuity_prepare_payload( $field_data, $posted_data, $notes );
if ( is_wp_error( $payload ) ) {
adfoin_acuity_log_skipped( $record, $payload->get_error_message() );
return;
}
adfoin_acuity_book( $field_data, $payload, $record, $notes );
}
/**
* Whether the integration books as an admin.
*
* @param array $field_data Field configuration.
*
* @return bool
*/
function adfoin_acuity_is_admin_mode( $field_data ) {
return isset( $field_data['adminMode'] ) && 'admin' === $field_data['adminMode'];
}
/**
* Send the appointment to Acuity. Shared with Acuity [PRO].
*
* @param array $field_data Field configuration.
* @param array $payload Request payload.
* @param array $record Log record.
* @param array $notes Notes for the log message.
*
* @return array|WP_Error
*/
function adfoin_acuity_book( $field_data, $payload, $record, $notes = array() ) {
$query_args = array();
if ( adfoin_acuity_is_admin_mode( $field_data ) ) {
$query_args['admin'] = 'true';
}
if ( ! empty( $field_data['noEmail'] ) ) {
$query_args['noEmail'] = 'true';
}
$acuity = ADFOIN_Acuity::get_instance();
$cred_id = isset( $field_data['credId'] ) ? $field_data['credId'] : '';
$acuity->set_credentials( $cred_id );
return $acuity->api_request( 'appointments', 'POST', $payload, $query_args, $record, $notes );
}
/**
* Parse the "Form Fields (JSON)" value into Acuity's [{id, value}] list.
*
* @param string $value JSON.
*
* @return array|false False when the value is not a valid list.
*/
function adfoin_acuity_parse_fields_json( $value ) {
$decoded = json_decode( $value, true );
if ( ! is_array( $decoded ) ) {
return false;
}
$fields = array();
foreach ( $decoded as $item ) {
if ( ! is_array( $item ) || empty( $item['id'] ) || ! array_key_exists( 'value', $item ) ) {
return false;
}
$fields[] = array(
'id' => absint( $item['id'] ),
'value' => is_array( $item['value'] ) ? implode( ', ', $item['value'] ) : (string) $item['value'],
);
}
return $fields;
}
/**
* Build request payload.
*
* @param array $field_data Field configuration.
* @param array $posted_data Trigger data.
* @param array $notes Filled with notes about values that were not sent.
*
* @return array|WP_Error
*/
function adfoin_acuity_prepare_payload( $field_data, $posted_data, &$notes = array() ) {
$appointment_type = isset( $field_data['appointmentTypeId'] ) ? sanitize_text_field( $field_data['appointmentTypeId'] ) : '';
$admin = adfoin_acuity_is_admin_mode( $field_data );
if ( ! $appointment_type ) {
return new WP_Error( 'adfoin_acuity_missing_type', __( 'Appointment type is required.', 'advanced-form-integration' ) );
}
$payload = array(
'appointmentTypeID' => (int) $appointment_type,
);
if ( isset( $field_data['calendarId'] ) && '' !== $field_data['calendarId'] ) {
$payload['calendarID'] = (int) $field_data['calendarId'];
}
if ( $admin && empty( $payload['calendarID'] ) ) {
return new WP_Error( 'adfoin_acuity_missing_calendar', __( 'Booking as admin needs a calendar. Choose one in the integration.', 'advanced-form-integration' ) );
}
$field_map = adfoin_acuity_field_map();
foreach ( $field_map as $field_key => $api_key ) {
if ( empty( $field_data[ $field_key ] ) ) {
continue;
}
$value = trim( (string) adfoin_get_parsed_values( $field_data[ $field_key ], $posted_data ) );
if ( '' === $value ) {
continue;
}
switch ( $field_key ) {
case 'fieldDefinitions':
$fields = adfoin_acuity_parse_fields_json( $value );
if ( false === $fields ) {
$notes[] = __( 'Form Fields (JSON) was not sent: it must be a JSON list like [{"id":123,"value":"Answer"}]', 'advanced-form-integration' );
} elseif ( $fields ) {
$payload[ $api_key ] = $fields;
}
break;
case 'addonIds':
$ids = array_values(
array_filter(
array_map(
'absint',
array_map( 'trim', explode( ',', $value ) )
)
)
);
if ( ! empty( $ids ) ) {
$payload[ $api_key ] = $ids;
}
break;
case 'labelId':
$label_id = absint( $value );
if ( $label_id ) {
$payload[ $api_key ] = array(
array(
'id' => $label_id,
),
);
}
break;
case 'notes':
if ( $admin ) {
$payload[ $api_key ] = $value;
} else {
$notes[] = __( 'Notes not sent: Acuity saves notes only when booking as admin', 'advanced-form-integration' );
}
break;
default:
$payload[ $api_key ] = $value;
break;
}
}
// Separate date and time fields, used when the combined field is not mapped.
if ( empty( $payload['datetime'] ) ) {
$date = isset( $field_data['date'] ) ? trim( (string) adfoin_get_parsed_values( $field_data['date'], $posted_data ) ) : '';
$time = isset( $field_data['time'] ) ? trim( (string) adfoin_get_parsed_values( $field_data['time'], $posted_data ) ) : '';
if ( '' !== $date && '' !== $time ) {
$payload['datetime'] = $date . ' ' . $time;
}
}
$required = array(
'datetime' => __( 'Appointment date & time (or both Date and Time)', 'advanced-form-integration' ),
'firstName' => __( 'First name', 'advanced-form-integration' ),
'lastName' => __( 'Last name', 'advanced-form-integration' ),
);
if ( ! $admin ) {
$required['email'] = __( 'Email', 'advanced-form-integration' );
}
foreach ( $required as $api_key => $label ) {
if ( empty( $payload[ $api_key ] ) ) {
return new WP_Error(
'adfoin_acuity_missing_required',
sprintf(
/* translators: %s field name. */
__( '%s is required for Acuity appointments.', 'advanced-form-integration' ),
$label
)
);
}
}
return $payload;
}