'list_experiments', 'experiment_get' => 'get_experiment', 'experiment_subjects' => 'subjects', 'experiment_preflight' => 'preflight', 'experiment_create' => 'create', 'experiment_action' => 'act', 'experiment_apply_winner' => 'apply_winner', 'experiment_history' => 'history', 'experiment_value' => 'value', 'experiment_note' => 'save_note', ); foreach ( $actions as $action => $method ) { add_action( 'wp_ajax_agencoen_' . $action, function () use ( $method ) { check_ajax_referer( self::NONCE_ACTION, 'nonce' ); $this->require_manage(); $this->require_connected(); $this->require_woo(); $this->$method(); } ); } } /* --------------------------------------------------------------------- */ /** * The list. */ public function list_experiments() { check_ajax_referer( self::NONCE_ACTION, 'nonce' ); $this->require_manage(); $fresh = ! empty( $_POST['fresh'] ); $result = $this->plugin->experiments()->all( (bool) $fresh ); $this->respond( $result ); } /** * One experiment. */ public function get_experiment() { check_ajax_referer( self::NONCE_ACTION, 'nonce' ); $this->require_manage(); $id = isset( $_POST['experimentId'] ) ? sanitize_text_field( wp_unslash( $_POST['experimentId'] ) ) : ''; if ( '' === $id ) { wp_send_json_error( array( 'message' => __( 'Which experiment?', 'agentic-commerce-engine' ) ) ); } $fresh = ! empty( $_POST['fresh'] ); $this->respond( $this->plugin->experiments()->one( $id, (bool) $fresh ) ); } /** * Finished tests and what each one found. */ public function history() { check_ajax_referer( self::NONCE_ACTION, 'nonce' ); $this->require_manage(); $query = array( 'verdict' => isset( $_POST['verdict'] ) ? sanitize_text_field( wp_unslash( $_POST['verdict'] ) ) : '', 'q' => isset( $_POST['q'] ) ? sanitize_text_field( wp_unslash( $_POST['q'] ) ) : '', 'page' => isset( $_POST['page'] ) ? absint( $_POST['page'] ) : 0, ); $this->respond( $this->plugin->experiments()->history( $query ) ); } /** * What testing has been worth. */ public function value() { check_ajax_referer( self::NONCE_ACTION, 'nonce' ); $this->require_manage(); $fresh = ! empty( $_POST['fresh'] ); $this->respond( $this->plugin->experiments()->value( (bool) $fresh ) ); } /** * Save what the merchant learned from a finished test. */ public function save_note() { check_ajax_referer( self::NONCE_ACTION, 'nonce' ); $this->require_manage(); $id = isset( $_POST['experimentId'] ) ? sanitize_text_field( wp_unslash( $_POST['experimentId'] ) ) : ''; if ( '' === $id ) { wp_send_json_error( array( 'message' => __( 'Which experiment?', 'agentic-commerce-engine' ) ) ); } // `sanitize_textarea_field`, not `sanitize_text_field`: a note is prose // and its line breaks are part of what the merchant wrote. $notes = isset( $_POST['notes'] ) ? sanitize_textarea_field( wp_unslash( $_POST['notes'] ) ) : ''; $this->respond( $this->plugin->experiments()->save_note( $id, $notes ) ); } /** * Products and categories to choose between. */ public function subjects() { check_ajax_referer( self::NONCE_ACTION, 'nonce' ); $this->require_manage(); $category = isset( $_POST['category'] ) ? sanitize_text_field( wp_unslash( $_POST['category'] ) ) : ''; $this->respond( $this->plugin->experiments()->subjects( '' !== $category ? $category : null ) ); } /** * The costless power check. */ public function preflight() { check_ajax_referer( self::NONCE_ACTION, 'nonce' ); $this->require_manage(); $this->respond( $this->plugin->experiments()->preflight( $this->design_from_request() ) ); } /** * Create, which spends credits. */ public function create() { check_ajax_referer( self::NONCE_ACTION, 'nonce' ); $this->require_manage(); $body = $this->design_from_request(); $name = isset( $_POST['name'] ) ? sanitize_text_field( wp_unslash( $_POST['name'] ) ) : ''; if ( '' === $name ) { wp_send_json_error( array( 'message' => __( 'Give this test a name.', 'agentic-commerce-engine' ) ) ); } $body['name'] = $name; $body['goal'] = isset( $_POST['goal'] ) ? sanitize_text_field( wp_unslash( $_POST['goal'] ) ) : 'checkout'; $body['acceptSlow'] = ! empty( $_POST['acceptSlow'] ); $this->respond( $this->plugin->experiments()->create( $body ) ); } /** * A lifecycle action. */ public function act() { check_ajax_referer( self::NONCE_ACTION, 'nonce' ); $this->require_manage(); $id = isset( $_POST['experimentId'] ) ? sanitize_text_field( wp_unslash( $_POST['experimentId'] ) ) : ''; $action = isset( $_POST['experimentAction'] ) ? sanitize_text_field( wp_unslash( $_POST['experimentAction'] ) ) : ''; if ( '' === $id || '' === $action ) { wp_send_json_error( array( 'message' => __( 'Nothing to do.', 'agentic-commerce-engine' ) ) ); } $extra = array(); if ( 'rename' === $action ) { $name = isset( $_POST['name'] ) ? sanitize_text_field( wp_unslash( $_POST['name'] ) ) : ''; if ( '' === $name ) { wp_send_json_error( array( 'message' => __( 'Give this test a name.', 'agentic-commerce-engine' ) ) ); } $extra['name'] = $name; } $this->respond( $this->plugin->experiments()->act( $id, $action, $extra ) ); } /** * Keep the winning version for every shopper. */ public function apply_winner() { check_ajax_referer( self::NONCE_ACTION, 'nonce' ); $this->require_manage(); $id = isset( $_POST['experimentId'] ) ? sanitize_text_field( wp_unslash( $_POST['experimentId'] ) ) : ''; $arm = isset( $_POST['arm'] ) ? sanitize_text_field( wp_unslash( $_POST['arm'] ) ) : ''; if ( '' === $id || '' === $arm ) { wp_send_json_error( array( 'message' => __( 'Nothing to do.', 'agentic-commerce-engine' ) ) ); } $this->respond( $this->plugin->experiments()->apply_winner( $id, $arm ) ); } /* --------------------------------------------------------------------- */ /** * The design half of a preflight or create body, resolved server-side. * * The PRODUCT IDS ARE RESOLVED HERE and not accepted from the browser. * A category holding three thousand products would otherwise be a form post * the size of the catalogue, and one a client could edit into a set the * merchant never chose. * * @return array */ private function design_from_request() { check_ajax_referer( self::NONCE_ACTION, 'nonce' ); $type = isset( $_POST['type'] ) ? sanitize_text_field( wp_unslash( $_POST['type'] ) ) : 'visitor_split'; $category = isset( $_POST['category'] ) ? sanitize_text_field( wp_unslash( $_POST['category'] ) ) : ''; $levels = isset( $_POST['levels'] ) ? (int) $_POST['levels'] : 2; $days = isset( $_POST['days'] ) ? (int) $_POST['days'] : 28; if ( ! in_array( $type, array( 'visitor_split', 'product_holdout' ), true ) ) { $type = 'visitor_split'; } $levels = max( 2, min( 4, $levels ) ); $days = max( 7, min( 180, $days ) ); $subjects = $this->plugin->experiments()->subjects( '' !== $category ? $category : null ); if ( is_wp_error( $subjects ) ) { wp_send_json_error( $this->error_payload( $subjects ) ); } $ids = isset( $subjects['ids'] ) && is_array( $subjects['ids'] ) ? $subjects['ids'] : array(); if ( empty( $ids ) ) { wp_send_json_error( array( 'message' => __( 'No products matched that selection.', 'agentic-commerce-engine' ) ) ); } return array( 'type' => $type, 'factors' => array( array( 'contentTypeId' => 'product-description', 'levels' => array_slice( array( 'a', 'b', 'c', 'd' ), 0, $levels ), ), ), 'subjectIds' => array_slice( $ids, 0, self::MAX_SUBJECTS ), 'days' => $days, ); } /** * One shape for every answer. * * A platform refusal is the MERCHANT'S answer, not an internal error: * "not enough traffic", "a split is already running", "too few products" * are all things they can act on, so the message travels through unchanged * rather than being replaced with something generic. * * @param array|\WP_Error $result Result. */ private function respond( $result ) { if ( is_wp_error( $result ) ) { wp_send_json_error( $this->error_payload( $result ) ); } wp_send_json_success( $result ); } }