register_placement_hook(); } private function register_placement_hook() { $is_block_theme = wp_is_block_theme(); $use_wc_hook = (bool) apply_filters('aieo_dmm_complementary_fse_use_hook', ! $is_block_theme); if ($use_wc_hook) { $hook = (string) apply_filters('aieo_dmm_complementary_hook', 'woocommerce_before_add_to_cart_button'); $priority = (int) apply_filters('aieo_dmm_complementary_priority', 25); add_action($hook, array($this, 'render_for_current_product'), $priority); } $fallback_priority = (int) apply_filters('aieo_dmm_complementary_fallback_priority', 31); add_action('woocommerce_simple_add_to_cart', array($this, 'render_fallback_for_current_product'), $fallback_priority); add_action('woocommerce_after_add_to_cart_form', array($this, 'render_fallback_for_current_product'), $fallback_priority); } private $rendered_for = array(); public function render_subtab() { $this->render_template('complementary.php', array( 'rows' => $this->fetch_all_with_labels(), 'module' => $this, 'has_legacy_pods_data' => $this->detect_legacy_pods_data(), )); } public function enqueue_admin_assets() { } public function fetch_all_with_labels() { global $wpdb; $table = $wpdb->prefix . 'aieo_dmm_complementary'; if (!aieo_dmm_table_exists($table)) { return array(); } $rows = $wpdb->get_results("SELECT * FROM {$table} ORDER BY enabled DESC, id DESC", ARRAY_A); if (!is_array($rows)) { return array(); } $out = array(); foreach ($rows as $row) { $out[] = $this->hydrate_row($row); } return $out; } private function hydrate_row(array $row) { $pids = json_decode($row['product_ids'] ?? '[]', true); if (!is_array($pids)) { $pids = array(); } $pids = array_values(array_filter(array_map('intval', $pids))); $row['product_ids'] = $pids; $row['product_names'] = array_map(array($this, 'resolve_product_name'), $pids); $row['source_product_id'] = (int) ($row['source_product_id'] ?? 0); $row['source_product_name'] = $this->resolve_product_name($row['source_product_id']); $row['enabled'] = !empty($row['enabled']) ? 1 : 0; $row['offer_title'] = (string) ($row['offer_title'] ?? ''); $row['button_text'] = (string) ($row['button_text'] ?? ''); $row['placement'] = (string) ($row['placement'] ?? 'before_add_to_cart'); $row['checkboxes_state'] = (string) ($row['checkboxes_state'] ?? 'unchecked'); return $row; } private function resolve_product_name($product_id) { $product_id = (int) $product_id; if ($product_id <= 0) { return ''; } $title = get_the_title($product_id); return ($title !== '' && $title !== null) ? (string) $title : ('#' . $product_id); } public function ajax_save() { AIEO_DMM::verify_ajax_request(); global $wpdb; $table = $wpdb->prefix . 'aieo_dmm_complementary'; if (!aieo_dmm_table_exists($table)) { wp_send_json_error(array('message' => __('Table missing.', 'ai-eshop-optimizer'))); } $id = isset($_POST['id']) ? absint(wp_unslash($_POST['id'])) : 0; $is_update = $id > 0; $source_id = isset($_POST['source_product_id']) ? absint(wp_unslash($_POST['source_product_id'])) : 0; if ($source_id <= 0) { wp_send_json_error(array('message' => __('Pick a source product.', 'ai-eshop-optimizer'))); } $product_ids = array(); if (isset($_POST['product_ids']) && is_array($_POST['product_ids'])) { $product_ids = array_values(array_filter(array_map('absint', wp_unslash($_POST['product_ids'])))); } $product_ids = array_slice(array_unique($product_ids), 0, 3); if (empty($product_ids)) { wp_send_json_error(array('message' => __('Pick at least one complementary product.', 'ai-eshop-optimizer'))); } if (in_array($source_id, $product_ids, true)) { wp_send_json_error(array('message' => __('A product can\'t complement itself — remove it from the complementary list.', 'ai-eshop-optimizer'))); } if (!$is_update) { $existing = (int) $wpdb->get_var($wpdb->prepare( "SELECT id FROM {$table} WHERE source_product_id = %d LIMIT 1", $source_id )); if ($existing > 0) { wp_send_json_error(array( 'message' => __('A complementary set already exists for this source product. Edit it instead.', 'ai-eshop-optimizer'), 'existing_id' => $existing, )); } } else { $existing = (int) $wpdb->get_var($wpdb->prepare( "SELECT id FROM {$table} WHERE source_product_id = %d AND id <> %d LIMIT 1", $source_id, $id )); if ($existing > 0) { wp_send_json_error(array('message' => __('Another row already targets this source product.', 'ai-eshop-optimizer'))); } } $placement_allowed = array('before_add_to_cart', 'after_add_to_cart', 'after_summary'); $placement = isset($_POST['placement']) ? sanitize_key(wp_unslash($_POST['placement'])) : 'before_add_to_cart'; if (!in_array($placement, $placement_allowed, true)) { $placement = 'before_add_to_cart'; } $checkboxes_state = (isset($_POST['checkboxes_state']) && sanitize_text_field(wp_unslash($_POST['checkboxes_state'])) === 'checked') ? 'checked' : 'unchecked'; $data = array( 'enabled' => !empty($_POST['enabled']) ? 1 : 0, 'label' => isset($_POST['label']) ? sanitize_text_field(wp_unslash($_POST['label'])) : '', 'source_product_id' => $source_id, 'product_ids' => wp_json_encode($product_ids), 'offer_title' => isset($_POST['offer_title']) ? sanitize_text_field(wp_unslash($_POST['offer_title'])) : '', 'button_text' => isset($_POST['button_text']) ? sanitize_text_field(wp_unslash($_POST['button_text'])) : '', 'placement' => $placement, 'checkboxes_state' => $checkboxes_state, ); $formats = array('%d', '%s', '%d', '%s', '%s', '%s', '%s', '%s'); if ($is_update) { $wpdb->update($table, $data, array('id' => $id), $formats, array('%d')); } else { $wpdb->insert($table, $data, $formats); $id = (int) $wpdb->insert_id; } AIEO_DMM_Cache::bump_version('complementary'); $row = $this->hydrate_row( $wpdb->get_row($wpdb->prepare("SELECT * FROM {$table} WHERE id = %d", $id), ARRAY_A) ?: array() ); wp_send_json_success(array( 'id' => $id, 'is_update' => $is_update, 'row' => $row, 'message' => __('Complementary set saved.', 'ai-eshop-optimizer'), )); } public function ajax_delete() { AIEO_DMM::verify_ajax_request(); $id = isset($_POST['id']) ? absint(wp_unslash($_POST['id'])) : 0; if ($id <= 0) { wp_send_json_error(array('message' => __('Invalid id.', 'ai-eshop-optimizer'))); } global $wpdb; $wpdb->delete($wpdb->prefix . 'aieo_dmm_complementary', array('id' => $id), array('%d')); AIEO_DMM_Cache::bump_version('complementary'); wp_send_json_success(); } public function ajax_search_products() { AIEO_DMM::verify_ajax_request(); $q = isset($_GET['q']) ? sanitize_text_field(wp_unslash($_GET['q'])) : ''; if (!function_exists('wc_get_products')) { wp_send_json(array('results' => array())); } $products = wc_get_products(array( 'status' => array('publish', 'private'), 'limit' => 25, 's' => $q, 'orderby' => 'title', 'order' => 'ASC', )); $results = array(); foreach ($products as $p) { $results[] = array('id' => (int) $p->get_id(), 'text' => $p->get_name() . ' (#' . (int) $p->get_id() . ')'); } wp_send_json(array('results' => $results)); } public function ajax_import_csv() { AIEO_DMM::verify_ajax_request(); if (empty($_FILES['csv']['tmp_name']) || !is_uploaded_file($_FILES['csv']['tmp_name'])) { wp_send_json_error(array('message' => __('No CSV uploaded.', 'ai-eshop-optimizer'))); } $fh = fopen($_FILES['csv']['tmp_name'], 'r'); if (!$fh) { wp_send_json_error(array('message' => __('Could not open uploaded file.', 'ai-eshop-optimizer'))); } $header = fgetcsv($fh); if (!$header) { fclose($fh); wp_send_json_error(array('message' => __('Empty CSV.', 'ai-eshop-optimizer'))); } $idx = array_flip(array_map('trim', $header)); $required = array('source_sku', 'product_1_sku'); foreach ($required as $col) { if (!isset($idx[$col])) { fclose($fh); wp_send_json_error(array( 'message' => sprintf(__('Missing required column: %s', 'ai-eshop-optimizer'), $col), )); } } global $wpdb; $table = $wpdb->prefix . 'aieo_dmm_complementary'; $stats = array('inserted' => 0, 'updated' => 0, 'skipped' => 0, 'errors' => array()); while (($row = fgetcsv($fh)) !== false) { $get = function ($key) use ($row, $idx) { return isset($idx[$key]) && isset($row[$idx[$key]]) ? trim((string) $row[$idx[$key]]) : ''; }; $source_sku = $get('source_sku'); if ($source_sku === '') { $stats['skipped']++; continue; } $source_id = wc_get_product_id_by_sku($source_sku); if (!$source_id) { $stats['errors'][] = sprintf(__('Unknown source SKU: %s', 'ai-eshop-optimizer'), $source_sku); $stats['skipped']++; continue; } $pids = array(); foreach (array('product_1_sku', 'product_2_sku', 'product_3_sku') as $k) { $sku = $get($k); if ($sku === '') { continue; } $pid = wc_get_product_id_by_sku($sku); if ($pid) { $pids[] = (int) $pid; } else { $stats['errors'][] = sprintf(__('Unknown SKU: %1$s (in row for %2$s)', 'ai-eshop-optimizer'), $sku, $source_sku); } } if (empty($pids)) { $stats['skipped']++; continue; } $pids = array_slice(array_unique($pids), 0, 3); if (in_array((int) $source_id, $pids, true)) { $pids = array_values(array_diff($pids, array((int) $source_id))); } if (empty($pids)) { $stats['skipped']++; continue; } $data = array( 'enabled' => $get('enabled') === '0' ? 0 : 1, 'label' => $get('label'), 'source_product_id' => (int) $source_id, 'product_ids' => wp_json_encode($pids), 'offer_title' => $get('offer_title'), 'button_text' => $get('button_text'), 'placement' => 'before_add_to_cart', 'checkboxes_state' => 'unchecked', ); $formats = array('%d', '%s', '%d', '%s', '%s', '%s', '%s', '%s'); $existing_id = (int) $wpdb->get_var($wpdb->prepare( "SELECT id FROM {$table} WHERE source_product_id = %d", (int) $source_id )); if ($existing_id > 0) { $wpdb->update($table, $data, array('id' => $existing_id), $formats, array('%d')); $stats['updated']++; } else { $wpdb->insert($table, $data, $formats); $stats['inserted']++; } } fclose($fh); AIEO_DMM_Cache::bump_version('complementary'); wp_send_json_success(array( 'message' => sprintf( __('Import complete: %1$d inserted, %2$d updated, %3$d skipped.', 'ai-eshop-optimizer'), $stats['inserted'], $stats['updated'], $stats['skipped'] ), 'stats' => $stats, )); } public function ajax_export_csv() { AIEO_DMM::verify_ajax_request(); global $wpdb; $rows = $wpdb->get_results( "SELECT * FROM " . $wpdb->prefix . "aieo_dmm_complementary ORDER BY id ASC", ARRAY_A ); $tmp = fopen('php://temp', 'w+'); fputcsv($tmp, array('source_sku', 'product_1_sku', 'product_2_sku', 'product_3_sku', 'label', 'enabled', 'offer_title', 'button_text', 'placement')); foreach ((array) $rows as $row) { $source = wc_get_product((int) $row['source_product_id']); if (!$source) { continue; } $pids = json_decode($row['product_ids'] ?? '[]', true); if (!is_array($pids)) { $pids = array(); } $pid_skus = array_pad(array_map(function ($id) { $p = wc_get_product((int) $id); return $p ? (string) $p->get_sku() : ''; }, $pids), 3, ''); fputcsv($tmp, array( (string) $source->get_sku(), $pid_skus[0], $pid_skus[1], $pid_skus[2], (string) $row['label'], !empty($row['enabled']) ? '1' : '0', (string) $row['offer_title'], (string) $row['button_text'], (string) $row['placement'], )); } rewind($tmp); $csv = stream_get_contents($tmp); fclose($tmp); wp_send_json_success(array( 'filename' => 'aieo-complementary-' . gmdate('Ymd-His') . '.csv', 'mime' => 'text/csv', 'b64' => base64_encode($csv), )); } private function detect_legacy_pods_data() { global $wpdb; $table = $wpdb->prefix . 'pods_promo_campaign'; if (!aieo_dmm_table_exists($table)) { return false; } $count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table} WHERE is_active = 1 AND campaign_type = 1" ); return $count > 0; } public function ajax_migrate_pods() { AIEO_DMM::verify_ajax_request(); global $wpdb; $table = $wpdb->prefix . 'aieo_dmm_complementary'; $legacy_table = $wpdb->prefix . 'pods_promo_campaign'; if (!aieo_dmm_table_exists($legacy_table)) { wp_send_json_error(array('message' => __('No legacy `wp_pods_promo_campaign` table — nothing to migrate.', 'ai-eshop-optimizer'))); } $stats = array('inserted' => 0, 'updated' => 0, 'skipped' => 0); $rows = $wpdb->get_results( "SELECT name, source_product, product_1, product_2, product_3 FROM {$legacy_table} WHERE is_active = 1 AND campaign_type = 1", ARRAY_A ); if (!is_array($rows)) { wp_send_json_error(array('message' => __('Could not read the legacy table.', 'ai-eshop-optimizer'))); } $resolve_pid = function ($value) { $value = trim((string) $value); if ($value === '') { return 0; } if (ctype_digit($value)) { $pid = (int) $value; if ($pid > 0 && get_post_type($pid) === 'product') { return $pid; } } return (int) wc_get_product_id_by_sku($value); }; foreach ($rows as $r) { $source_id = $resolve_pid($r['source_product'] ?? ''); if (!$source_id) { $stats['skipped']++; continue; } $pids = array(); foreach (array('product_1', 'product_2', 'product_3') as $k) { $pid = $resolve_pid($r[$k] ?? ''); if ($pid) { $pids[] = $pid; } } if (empty($pids)) { $stats['skipped']++; continue; } $pids = array_slice(array_unique($pids), 0, 3); $data = array( 'enabled' => 1, 'label' => isset($r['name']) ? (string) $r['name'] : '', 'source_product_id' => (int) $source_id, 'product_ids' => wp_json_encode($pids), 'offer_title' => '', 'button_text' => '', 'placement' => 'before_add_to_cart', 'checkboxes_state' => 'unchecked', ); $formats = array('%d', '%s', '%d', '%s', '%s', '%s', '%s', '%s'); $existing_id = (int) $wpdb->get_var($wpdb->prepare( "SELECT id FROM {$table} WHERE source_product_id = %d", (int) $source_id )); if ($existing_id > 0) { $wpdb->update($table, $data, array('id' => $existing_id), $formats, array('%d')); $stats['updated']++; } else { $wpdb->insert($table, $data, $formats); $stats['inserted']++; } } AIEO_DMM_Cache::bump_version('complementary'); wp_send_json_success(array( 'message' => sprintf( __('Migration complete: %1$d inserted, %2$d updated, %3$d skipped. You can now deactivate Pods.', 'ai-eshop-optimizer'), $stats['inserted'], $stats['updated'], $stats['skipped'] ), 'stats' => $stats, )); } public function enqueue_frontend_assets() { if (!is_product()) { return; } $css_rel = 'assets/css/aieo-complementary.css'; $js_rel = 'assets/js/aieo-complementary.js'; $css_path = AIEO_DIR . $css_rel; $js_path = AIEO_DIR . $js_rel; $css_ver = file_exists($css_path) ? filemtime($css_path) : AIEO_VERSION; $js_ver = file_exists($js_path) ? filemtime($js_path) : AIEO_VERSION; wp_enqueue_style('aieo-complementary', AIEO_URI . $css_rel, array(), $css_ver); $a = self::get_appearance_settings(); $cb_bg = trim((string) ($a['btn_bg_color'] ?? '')); $cb_text = trim((string) ($a['btn_text_color'] ?? '')); $cb_border = trim((string) ($a['btn_border_color'] ?? '')); $cb_bw = (string) ($a['btn_border_width'] ?? ''); $cb_br = (string) ($a['btn_border_radius'] ?? ''); $theme = function_exists('rooster_design_authority') && 'theme' === rooster_design_authority(); $bg = (string) $a['bg_color']; $border = (string) $a['border_color']; $radius = (int) $a['border_radius'] . 'px'; if ($theme) { if (0 === strcasecmp($bg, '#fafafa')) { $bg = 'var(--wp--preset--color--bg)'; } if (0 === strcasecmp($border, '#e2e2e2')) { $border = 'var(--wp--preset--color--line)'; } if (8 === (int) $a['border_radius']) { $radius = 'var(--aieo-shape-card,8px)'; } } $title_size = ($theme && 15 === (int) $a['title_size']) ? 'var(--wp--preset--font-size--base,1rem)' : ((int) $a['title_size'] . 'px'); $vars = sprintf( '.aieo-compl{' . '--aieo-compl-bg:%s;' . '--aieo-compl-border:%s;' . '--aieo-compl-border-w:%dpx;' . '--aieo-compl-border-r:%s;' . '--aieo-compl-title-size:%s;' . '--aieo-compl-text-size:%dpx;' . '--aieo-compl-btn-bg:%s;' . '--aieo-compl-btn-text:%s;' . '--aieo-compl-btn-border:%s;' . '--aieo-compl-btn-border-w:%s;' . '--aieo-compl-btn-border-r:%s;' . '}', esc_attr($bg), esc_attr($border), (int) $a['border_width'], esc_attr($radius), esc_attr($title_size), (int) $a['text_size'], $cb_bg !== '' ? esc_attr($cb_bg) : ($theme ? 'var(--wp--custom--rooster--button--background,var(--wp--preset--color--accent))' : 'var(--aieo-btn-bg,#222222)'), $cb_text !== '' ? esc_attr($cb_text) : ($theme ? 'var(--wp--custom--rooster--button--text,var(--wp--preset--color--on-accent,var(--wp--preset--color--bg)))' : 'var(--aieo-btn-fg,#ffffff)'), $cb_border !== '' ? esc_attr($cb_border) : ($theme ? 'transparent' : 'var(--aieo-btn-border,#222222)'), $cb_bw !== '' ? ((int) $cb_bw . 'px') : 'var(--aieo-btn-border-w,0px)', $cb_br !== '' ? ((int) $cb_br . 'px') : ($theme ? 'var(--aieo-shape-button,8px)' : 'var(--aieo-btn-radius,4px)') ); wp_add_inline_style('aieo-complementary', $vars); $iapi_on = method_exists('AIEO_DMM', 'is_iapi_enabled') && AIEO_DMM::is_iapi_enabled('complementary'); if ($iapi_on && defined('AIEO_RUNTIME_DIR') && file_exists(AIEO_RUNTIME_DIR . 'assets/js/aieo-complementary-iapi.js') ) { wp_register_script_module( '@aieo/complementary-iapi', AIEO_Asset_Min::module_src(AIEO_RUNTIME_DIR . 'assets/js/aieo-complementary-iapi.js', AIEO_RUNTIME_URI . 'assets/js/aieo-complementary-iapi.js'), array('@wordpress/interactivity', '@aieo/iapi-bootstrap'), (string) filemtime(AIEO_RUNTIME_DIR . 'assets/js/aieo-complementary-iapi.js'), array( 'fetchpriority' => 'low' ) ); wp_enqueue_script_module('@aieo/complementary-iapi'); } else { wp_enqueue_script('aieo-complementary', AIEO_URI . $js_rel, array('jquery'), $js_ver, array('in_footer' => true, 'strategy' => 'defer')); } $compl_data = array( 'ajaxUrl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('aieo_dmm_complementary_add'), 'i18n' => array( 'adding' => __('Adding…', 'ai-eshop-optimizer'), 'added' => __('Added to cart.', 'ai-eshop-optimizer'), 'failed' => __('Could not add — please try again.', 'ai-eshop-optimizer'), 'pickOne' => __('Please tick at least one product first.', 'ai-eshop-optimizer'), 'pickVariation' => __('Pick a variation for each variable product.', 'ai-eshop-optimizer'), ), ); if ($iapi_on) { add_action('wp_print_footer_scripts', static function () use ($compl_data) { echo '' . "\n"; }, 5); } else { wp_localize_script('aieo-complementary', 'aieoComplementary', $compl_data); } } public function render_for_current_product() { global $product; if (!$product || !is_a($product, 'WC_Product')) { return; } $row = $this->get_row_for_source((int) $product->get_id()); if (!$row || empty($row['enabled'])) { return; } $this->render_picker_html($row); } public function render_fallback_for_current_product() { global $product; if (!$product || !is_a($product, 'WC_Product')) { return; } if ($product->is_purchasable() && $product->is_in_stock()) { return; } $row = $this->get_row_for_source((int) $product->get_id()); if (!$row || empty($row['enabled'])) { return; } $this->render_picker_html($row); } public function get_row_for_source($source_product_id) { global $wpdb; $table = $wpdb->prefix . 'aieo_dmm_complementary'; if (!aieo_dmm_table_exists($table)) { return null; } return AIEO_DMM_Cache::remember('complementary', 'src_' . (int) $source_product_id, function () use ($wpdb, $table, $source_product_id) { $row = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$table} WHERE source_product_id = %d AND enabled = 1 LIMIT 1", (int) $source_product_id ), ARRAY_A); if (!$row) { return null; } return $this->hydrate_row($row); }, AIEO_DMM_Cache::TTL_SHORT); } public static function add_eligibility_row($rows, $product_id, $audience_key) { if (!is_array($rows)) { $rows = array(); } $product_id = (int) $product_id; if ($product_id === 0) { return $rows; } if (!class_exists('AIEO_Effective_Eligibility')) { return $rows; } $r = self::is_eligible_for($product_id, (string) $audience_key); if (!empty($r['eligible'])) { $rows[AIEO_Effective_Eligibility::SURFACE_COMPLEMENTARY] = $r; } return $rows; } public static function is_eligible_for($product_id, $audience_key = 'guest') { $product_id = (int) $product_id; if ($product_id <= 0) { return array('eligible' => false); } if (class_exists('AIEO_DMM_Settings') && !AIEO_DMM_Settings::is_module_enabled('complementary')) { return array('eligible' => false); } $instance = self::eligibility_instance(); $row = $instance->get_row_for_source($product_id); if (!$row || empty($row['product_ids'])) { return array('eligible' => false); } return array( 'eligible' => true, 'offer_id' => isset($row['id']) ? (int) $row['id'] : null, 'payload' => array( 'count' => count((array) $row['product_ids']), ), ); } private static function eligibility_instance() { static $inst = null; if ($inst === null) { if (class_exists('AIEO_DMM') && method_exists('AIEO_DMM', 'instance')) { $mod = AIEO_DMM::instance()->get_module('complementary'); $inst = ($mod instanceof self) ? $mod : new self(); } else { $inst = new self(); } } return $inst; } public function render_picker_html(array $row) { $source_id = (int) ($row['source_product_id'] ?? 0); if ($source_id > 0 && !empty($this->rendered_for[$source_id])) { return; } $title = $row['offer_title'] !== '' ? $row['offer_title'] : $this->default_offer_title(); $btn = $row['button_text'] !== '' ? $row['button_text'] : $this->default_button_text(); $checked = ($row['checkboxes_state'] === 'checked') ? ' checked' : ''; $items = array(); foreach ($row['product_ids'] as $pid) { $p = wc_get_product((int) $pid); if (!$p || !$p->is_purchasable() || !$p->is_in_stock()) { continue; } $items[] = $p; } if (empty($items)) { return; } if ($source_id > 0) { $this->rendered_for[$source_id] = true; } if (!wp_style_is('aieo-complementary', 'enqueued')) { $this->enqueue_frontend_assets(); } ?>

cart) { wp_send_json_error(array('message' => __('Cart unavailable.', 'ai-eshop-optimizer'))); } $ids = isset($_POST['product_ids']) && is_array($_POST['product_ids']) ? array_values(array_filter(array_map('absint', wp_unslash($_POST['product_ids'])))) : array(); if (empty($ids)) { wp_send_json_error(array('message' => __('No products selected.', 'ai-eshop-optimizer'))); } $ids = array_slice(array_unique($ids), 0, 6); $variation_map = array(); if (isset($_POST['variations']) && is_array($_POST['variations'])) { foreach (wp_unslash($_POST['variations']) as $pid_key => $vid_val) { $pid_key = absint($pid_key); $vid_val = absint($vid_val); if ($pid_key > 0 && $vid_val > 0) { $variation_map[$pid_key] = $vid_val; } } } $source_pid = isset($_POST['source_id']) ? absint(wp_unslash($_POST['source_id'])) : 0; $attribution = class_exists('AIEO_Attribution') ? AIEO_Attribution::instance() : null; $added = 0; $logged_events = array(); foreach ($ids as $pid) { $product = wc_get_product($pid); if (!$product || !$product->is_purchasable() || !$product->is_in_stock()) { continue; } $variation_id = 0; $variation_attrs = array(); $unit_price_for_log = (float) $product->get_price(); if ($product->is_type('variable')) { $explicit_vid = isset($variation_map[(int) $pid]) ? (int) $variation_map[(int) $pid] : 0; if ($explicit_vid > 0) { $variation = wc_get_product($explicit_vid); if ($variation && $variation->is_type('variation') && $variation->is_purchasable() && $variation->is_in_stock() && (int) $variation->get_parent_id() === (int) $pid) { $variation_id = $explicit_vid; $variation_attrs = $variation->get_variation_attributes(); $unit_price_for_log = (float) $variation->get_price(); } } if ($variation_id === 0) { $picked = self::pick_default_variation($product); if (!$picked) { continue; } $variation_id = (int) $picked['id']; $variation_attrs = (array) $picked['attributes']; $unit_price_for_log = (float) $picked['price']; } } $key = WC()->cart->add_to_cart((int) $pid, 1, $variation_id, $variation_attrs, array()); if (!$key) { continue; } if (isset(WC()->cart->cart_contents[$key])) { WC()->cart->cart_contents[$key]['aieo_attribution_source'] = 'complementary'; } $added++; if ($attribution && method_exists($attribution, 'log_event')) { $ev_id = wp_generate_uuid4(); $attribution->log_event(array( 'event_id' => $ev_id, 'product_id' => (int) $pid, 'quantity' => 1, 'unit_price' => $unit_price_for_log, 'source' => 'complementary', 'source_pid' => $source_pid, )); $logged_events[] = array( 'event_id' => $ev_id, 'product_id' => (int) $pid, 'quantity' => 1, ); } } if ($added === 0) { wp_send_json_error(array('message' => __('Could not add any of the selected products.', 'ai-eshop-optimizer'))); } WC()->cart->calculate_totals(); ob_start(); woocommerce_mini_cart(); $mini = ob_get_clean(); wp_send_json_success(array( 'added' => $added, 'cart_url' => wc_get_cart_url(), 'aieo_events' => $logged_events, 'fragments' => apply_filters('woocommerce_add_to_cart_fragments', array( 'div.widget_shopping_cart_content' => '
' . $mini . '
', )), )); } public static function get_variations_for_product($product) { if (!is_object($product) || !$product->is_type('variable')) { return array(); } $out = array(); foreach ($product->get_available_variations() as $v) { if (empty($v['is_purchasable']) || empty($v['is_in_stock'])) { continue; } $label_parts = array(); foreach ((array) $v['attributes'] as $attr_key => $attr_val) { if ($attr_val === '' || $attr_val === null) { continue; } $taxonomy = preg_replace('/^attribute_/', '', (string) $attr_key); $is_global = (strpos($taxonomy, 'pa_') === 0); $value_disp = (string) $attr_val; if ($is_global && taxonomy_exists($taxonomy)) { $term = get_term_by('slug', $attr_val, $taxonomy); if ($term && !is_wp_error($term)) { $value_disp = (string) $term->name; } } $key_disp = function_exists('wc_attribute_label') ? wc_attribute_label($taxonomy) : ucfirst(str_replace(array('-', '_'), ' ', $taxonomy)); $label_parts[] = $key_disp . ': ' . $value_disp; } $out[] = array( 'variation_id' => (int) $v['variation_id'], 'label' => implode(' / ', $label_parts) ?: ('#' . (int) $v['variation_id']), ); } return $out; } private static function pick_default_variation($product) { if (!is_callable(array($product, 'get_available_variations'))) { return null; } $variations = $product->get_available_variations(); if (empty($variations) || !is_array($variations)) { return null; } $defaults = method_exists($product, 'get_default_attributes') ? $product->get_default_attributes() : array(); if (!empty($defaults)) { foreach ($variations as $v) { if (empty($v['is_in_stock']) || empty($v['is_purchasable'])) { continue; } $attrs = isset($v['attributes']) && is_array($v['attributes']) ? $v['attributes'] : array(); $matches = true; foreach ($defaults as $a_key => $a_val) { $expected = strtolower((string) $a_val); $actual = strtolower((string) ($attrs['attribute_' . $a_key] ?? $attrs[$a_key] ?? '')); if ($expected !== '' && $actual !== '' && $expected !== $actual) { $matches = false; break; } } if ($matches) { return array( 'id' => (int) ($v['variation_id'] ?? 0), 'price' => (float) ($v['display_price'] ?? 0), 'attributes' => $attrs, ); } } } $cheapest = null; foreach ($variations as $v) { if (empty($v['is_in_stock']) || empty($v['is_purchasable'])) { continue; } $price = (float) ($v['display_price'] ?? 0); if ($cheapest === null || $price < $cheapest['price']) { $cheapest = array( 'id' => (int) ($v['variation_id'] ?? 0), 'price' => $price, 'attributes' => isset($v['attributes']) && is_array($v['attributes']) ? $v['attributes'] : array(), ); } } return $cheapest; } const APPEARANCE_OPTION = 'aieo_dmm_complementary_appearance'; public static function get_appearance_settings(): array { $stored = get_option(self::APPEARANCE_OPTION, array()); if (!is_array($stored)) { $stored = array(); } return array_merge(array( 'title_size' => 15, 'text_size' => 14, 'bg_color' => '#fafafa', 'border_color' => '#e2e2e2', 'border_width' => 1, 'border_radius' => 8, 'btn_bg_color' => '', 'btn_text_color' => '', 'btn_border_color' => '', 'btn_border_width' => '', 'btn_border_radius' => '', ), $stored); } public function ajax_save_appearance(): void { AIEO_DMM::verify_ajax_request(); $cur = self::get_appearance_settings(); $num_clamps = array( 'title_size' => array(8, 48), 'text_size' => array(8, 32), 'border_width' => array(0, 10), 'border_radius' => array(0, 40), 'btn_border_width' => array(0, 10), 'btn_border_radius' => array(0, 40), ); foreach ($num_clamps as $k => list($min, $max)) { if (isset($_POST[$k])) { $cur[$k] = max($min, min($max, (int) wp_unslash($_POST[$k]))); } } foreach (array('bg_color', 'border_color', 'btn_bg_color', 'btn_text_color', 'btn_border_color') as $k) { if (isset($_POST[$k])) { $v = sanitize_hex_color(wp_unslash((string) $_POST[$k])); if ($v !== null && $v !== '') { $cur[$k] = $v; } } } update_option(self::APPEARANCE_OPTION, $cur, false); wp_send_json_success(array('message' => __('Complementary Products appearance saved.', 'ai-eshop-optimizer'))); } }