__( '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 = '
  1. ' . esc_html__( 'Log in to your Acuity Scheduling account.', 'advanced-form-integration' ) . '
  2. ' . esc_html__( 'Go to Integrations > API > View Credentials.', 'advanced-form-integration' ) . '
  3. ' . esc_html__( 'Copy your User ID and API Key.', 'advanced-form-integration' ) . '
  4. ' . 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; }