isset($field['name']) ? sanitize_text_field($field['name']) : '', 'slug' => isset($field['slug']) ? sanitize_key($field['slug']) : '', 'type' => isset($field['type']) ? sanitize_text_field($field['type']) : 'text', 'value' => isset($field['value']) ? sanitize_text_field($field['value']) : '', 'definition_id' => isset($field['definition_id']) ? intval($field['definition_id']) : 0, ]; if (empty($sanitized_field['slug']) && !empty($sanitized_field['name'])) { $sanitized_field['slug'] = sanitize_key($sanitized_field['name']); } if (isset($field['unit'])) { $sanitized_field['unit'] = sanitize_text_field($field['unit']); } // Assign a new definition_id if the field is new if (empty($sanitized_field['definition_id'])) { $sanitized_field['definition_id'] = $next_field_id++; } $processed_fields[] = $sanitized_field; } // Sort fields by definition_id usort($processed_fields, function($a, $b) { return ($a['definition_id'] ?? 0) <=> ($b['definition_id'] ?? 0); }); $criterion_post = array( 'post_title' => wp_strip_all_tags($criterion_title), 'post_content' => wp_kses_post($criterion_description), 'post_type' => AFFILILABS_CPT_CRITERION, 'post_status' => 'publish', 'meta_input' => array( AFFILILABS_META_KEY_CRITERION => $processed_fields, AFFILILABS_META_KEY_LINKED_PRODUCTS => $linked_products, AFFILILABS_META_KEY_LINKED_CATEGORIES => $linked_categories, ), ); AFFL_LoggerService::debug('AFFLCriteriaService::save - Criterion Post Data: ' . esc_html(print_r($criterion_post, true)), 'DEBUG'); if ($criterion_id) { $criterion_post['ID'] = $criterion_id; $criterion_id = wp_update_post($criterion_post); } else { $criterion_id = wp_insert_post($criterion_post); } if (is_wp_error($criterion_id)) { AFFL_LoggerService::log('AFFLCriteriaService::save - WP_Error during post save: ' . esc_html($criterion_id->get_error_message()), 'ERROR'); throw new Exception('Error saving criterion: ' . esc_html($criterion_id->get_error_message())); } if (empty($criterion_id) || !is_numeric($criterion_id) || $criterion_id <= 0) { throw new Exception('Failed to create or update criterion post. Invalid ID returned.'); } AFFL_LoggerService::debug('AFFLCriteriaService::save - Criterion ID before final validation: ' . esc_html($criterion_id ?? 'null'), 'DEBUG'); if (empty($criterion_id) || !is_numeric($criterion_id) || $criterion_id <= 0) { throw new Exception('Failed to create or update criterion post. Invalid ID returned.'); } return $criterion_id; } public static function delete($criterion_id) { if (empty($criterion_id)) { throw new Exception('No criterion ID provided for deletion.'); } $was_deleted = wp_delete_post($criterion_id, true); if (!$was_deleted) { throw new Exception('There was an error deleting the criterion. Please try again.'); } return true; } /** * Get criteria associated with specified categories * @param array $category_ids Array of category IDs * @return array Array of criterion data */ public static function get_criterions_by_categories($category_ids) { if (empty($category_ids) || !is_array($category_ids)) { return []; } $all_criterions = get_posts(array( 'post_type' => AFFILILABS_CPT_CRITERION, 'posts_per_page' => -1, )); $criterions = array(); foreach ($all_criterions as $criterion_post) { $linked_categories = get_post_meta($criterion_post->ID, AFFILILABS_META_KEY_LINKED_CATEGORIES, true); if (is_array($linked_categories) && !empty($linked_categories)) { foreach ($linked_categories as $category_id) { if (in_array($category_id, $category_ids)) { $criterions[] = self::format_criterion_data($criterion_post); break; } } } } return $criterions; } /** * Get all criteria * @return array Array of criterion data */ public static function get_all() { $args = array( 'post_type' => AFFILILABS_CPT_CRITERION, 'posts_per_page' => -1, ); $criterion_posts = get_posts($args); $criterions = array(); foreach ($criterion_posts as $criterion_post) { $criterions[] = self::format_criterion_data($criterion_post); } return $criterions; } /** * Format criterion data into consistent array structure * @param WP_Post $criterion_post WordPress post object * @return array Formatted criterion data */ private static function format_criterion_data($criterion_post) { if (!$criterion_post || $criterion_post->post_type !== AFFILILABS_CPT_CRITERION) { return null; } $fields = get_post_meta($criterion_post->ID, AFFILILABS_META_KEY_CRITERION, true); $linked_products = get_post_meta($criterion_post->ID, AFFILILABS_META_KEY_LINKED_PRODUCTS, true); $linked_categories = get_post_meta($criterion_post->ID, AFFILILABS_META_KEY_LINKED_CATEGORIES, true); if (!is_array($fields)) $fields = array(); if (!is_array($linked_products)) $linked_products = array(); if (!is_array($linked_categories)) $linked_categories = array(); return array( 'id' => $criterion_post->ID, 'title' => $criterion_post->post_title, 'description' => $criterion_post->post_content, 'fields' => $fields, 'linked_products' => $linked_products, 'linked_categories' => $linked_categories, ); } /** * Get a specific criterion by ID * @param int $criterion_id Criterion ID * @return array|null Criterion data or null if not found */ public static function get_criterion($criterion_id) { if (empty($criterion_id)) { return null; } $criterion_post = get_post($criterion_id); if (!$criterion_post || $criterion_post->post_type !== AFFILILABS_CPT_CRITERION) { return null; } return self::format_criterion_data($criterion_post); } /** * Get field values for a specific product and criterion * @param int $product_id Product ID * @param int $criterion_id Criterion ID * @return array Array of field values */ public static function get_criterion_values_for_product($product_id, $criterion_id) { if (empty($product_id) || empty($criterion_id)) { return array(); } $criterion = self::get_criterion($criterion_id); if (empty($criterion) || empty($criterion['fields'])) { return array(); } $all_criterion_values = get_post_meta($product_id, AFFILILABS_META_KEY_PRODUCT_CRITERION_VALUES, true); if (!is_array($all_criterion_values) || !isset($all_criterion_values[$criterion_id])) { return array(); } $product_criterion_values = $all_criterion_values[$criterion_id]; if (isset($product_criterion_values['fields']) && isset($product_criterion_values['fields']['items']) && is_array($product_criterion_values['fields']['items'])) { return $product_criterion_values['fields']['items']; } return array(); } /** * Get all criteria associated with a product * @param int $product_id Product ID * @return array Array of criterion data */ public static function get_criteria_for_product($product_id) { if (empty($product_id)) { return array(); } $product_categories = wp_get_post_terms($product_id, AFFILILABS_TAXONOMY_PRODUCT_CATEGORY, array("fields" => "ids")); $criterions = array(); if ($product_categories && !is_wp_error($product_categories)) { $category_criterions = self::get_criterions_by_categories($product_categories); $criterions = array_merge($criterions, $category_criterions); } $linked_criterions = self::get_criterions_by_linked_products([$product_id]); $criterions = array_merge($criterions, $linked_criterions); $unique_fields = array(); foreach ($criterions as $field) { if (!empty($field) && isset($field['id'])) { $unique_fields[$field['id']] = $field; } } return array_values($unique_fields); } /** * Get criteria directly linked to products * @param array $product_ids Array of product IDs * @return array Array of criterion data */ public static function get_criterions_by_linked_products($product_ids) { if (empty($product_ids) || !is_array($product_ids)) { return array(); } $all_criterions = get_posts(array( 'post_type' => AFFILILABS_CPT_CRITERION, 'posts_per_page' => -1, )); $criterions = array(); foreach ($all_criterions as $criterion_post) { $linked_products = get_post_meta($criterion_post->ID, AFFILILABS_META_KEY_LINKED_PRODUCTS, true); if (is_array($linked_products) && !empty($linked_products)) { foreach ($linked_products as $product_id) { if (in_array($product_id, $product_ids)) { $criterions[] = self::format_criterion_data($criterion_post); break; } } } } return $criterions; } /** * Get multiple criteria by their IDs. * * @param array $criteria_ids Array of criterion IDs. * @return array Array of formatted criterion data. Returns empty array if no IDs or no criteria found. */ public static function get_criteria_by_ids(array $criteria_ids): array { if (empty($criteria_ids) || !is_array($criteria_ids)) { return []; } $args = array( 'post_type' => AFFILILABS_CPT_CRITERION, 'posts_per_page' => -1, 'post_status' => 'publish', 'post__in' => $criteria_ids, 'orderby' => 'post__in', ); $criterion_posts = get_posts($args); $formatted_criteria = array(); foreach ($criterion_posts as $criterion_post) { $formatted_criteria[] = self::format_criterion_data($criterion_post); } return $formatted_criteria; } /** * Get fields for a specific criterion. * * @param int $criterion_id The ID of the criterion. * @return array Array of fields or empty array if not found. */ public static function get_criterion_fields($criterion_id) { if (empty($criterion_id)) { return []; } $criterion_post = get_post($criterion_id); if (!$criterion_post || $criterion_post->post_type !== AFFILILABS_CPT_CRITERION) { return []; } $fields = get_post_meta($criterion_id, AFFILILABS_META_KEY_CRITERION, true); if (!is_array($fields)) { return explode(',', $fields); } return $fields; } /** * Get AI suggested criteria based on selected products. * * @param int $list_id The ID of the current list. * @param array $product_ids An array of product IDs selected in the list. * @param string $language The selected AI language. * @return array An array with 'status' and 'suggestions' or 'message'. * @throws Exception If an error occurs during the API call or data processing. */ public static function get_ai_suggested_criteria(int $list_id, array $product_ids, string $language): array { if (empty($product_ids)) { AFFL_LoggerService::log('No product IDs provided for AI criteria suggestion.', 'WARNING'); return ['status' => 'error', 'message' => 'No products selected for AI criteria suggestion.']; } if (empty($language)) { AFFL_LoggerService::log('No language provided for AI criteria suggestion.', 'WARNING'); return ['status' => 'error', 'message' => 'AI language is missing.']; } $products_data = AFFLProductService::get_products_by_ids($product_ids); $current_list_data = AFFLComparisonTableService::get_table_data($list_id); $current_criteria_details = $current_list_data['linked_criteria_details'] ?? []; $data_for_ai = [ 'list_id' => $list_id, 'products' => $products_data, 'criteria' => $current_criteria_details, 'language' => $language, ]; AFFL_LoggerService::debug('AI Suggest Criteria - Data for AI: ' . esc_html(json_encode($data_for_ai, JSON_PRETTY_PRINT)), 'DEBUG'); $data = AFFL_AIService::request('ai/suggest_criteria', [ 'type' => 'suggest_criteria', 'data' => $data_for_ai, 'language' => $language ]); $suggested_criteria_json_string = $data['data'] ?? '[]'; $suggested_criteria = json_decode($suggested_criteria_json_string, true); if (json_last_error() !== JSON_ERROR_NONE) { AFFL_LoggerService::log('Failed to decode nested AI Suggest Criteria JSON string. Error: ' . esc_html(json_last_error_msg()) . ". String: " . esc_html($suggested_criteria_json_string), 'ERROR'); throw new Exception('Failed to decode nested AI Suggestion API response JSON: ' . esc_html(json_last_error_msg())); } if (!is_array($data) || !isset($data['status']) || $data['status'] !== 'ok' || !is_array($suggested_criteria)) { AFFL_LoggerService::log('Unexpected AI Suggest Criteria API response structure or status. Decoded suggestions: ' . esc_html(print_r($suggested_criteria, true)), 'WARNING'); throw new Exception('Unexpected AI Suggestion API response structure or status.' . esc_html($data['message'] ?? '') ); } $current_list_criteria_ids = AFFLComparisonTableService::get_table_data($list_id)['criteria_ids'] ?? []; foreach ($suggested_criteria as &$suggestion) { $suggestion['is_existing_criterion'] = false; $suggestion['existing_criterion_title'] = ''; if (isset($suggestion['id']) && $suggestion['id'] !== -1 && in_array($suggestion['id'], $current_list_criteria_ids)) { $suggestion['is_existing_criterion'] = true; $existing_criterion_post = get_post($suggestion['id']); if ($existing_criterion_post) { $suggestion['existing_criterion_title'] = $existing_criterion_post->post_title; } } } unset($suggestion); // Break the reference to avoid unintended modifications return ['status' => 'ok', 'suggestions' => $suggested_criteria]; } /** * Creates a new criterion post or updates an existing one based on AI suggestion. * * @param int $list_id The ID of the list to which the criterion will be linked. * @param array $suggested_criterion The AI suggested criterion data. * @return array An array with 'status' and 'criterion_id' or 'message'. * @throws Exception If an error occurs during saving or updating. */ public static function create_or_update_criterion_from_suggestion(int $list_id, array $suggested_criterion): array { $criterion_id = isset($suggested_criterion['id']) && $suggested_criterion['id'] !== -1 ? intval($suggested_criterion['id']) : null; $criterion_name = sanitize_text_field($suggested_criterion['criteria_name'] ?? ''); $category_id = intval($suggested_criterion['category_id'] ?? 0); $fields = $suggested_criterion['fields'] ?? []; if (empty($criterion_name)) { return ['status' => 'error', 'message' => 'Criterion name cannot be empty.']; } if ($list_id <= 1) { return ['status' => 'error', 'message' => 'Invalid List ID']; } $criterion_data = [ 'title' => $criterion_name, 'description' => '', 'fields' => $fields, 'linked_products' => [], 'linked_categories' => $category_id ? [$category_id] : [], ]; if ($criterion_id) { $criterion_data['id'] = $criterion_id; } $new_criterion_id = null; try { $new_criterion_id = self::save($criterion_data); } catch (Exception $e) { AFFL_LoggerService::log('Error saving AI suggested criterion: ' . esc_html($e->getMessage()), 'ERROR'); return ['status' => 'error', 'message' => 'Error saving suggested criterion: ' . $e->getMessage()]; } $current_criteria_ids = get_post_meta($list_id, AFFILILABS_META_KEY_LIST_CRITERIA_IDS, true); if (!is_array($current_criteria_ids)) { $current_criteria_ids = []; } if (!in_array($new_criterion_id, $current_criteria_ids)) { $current_criteria_ids[] = $new_criterion_id; update_post_meta($list_id, AFFILILABS_META_KEY_LIST_CRITERIA_IDS, array_map('intval', $current_criteria_ids)); } return ['status' => 'ok', 'criterion_id' => $new_criterion_id]; } public static function unlink_criterion_from_category($criterion_id, $category_id) { $linked_categories = get_post_meta($criterion_id, AFFILILABS_META_KEY_LINKED_CATEGORIES, true); if (is_array($linked_categories) && !empty($linked_categories)) { $updated_categories = array_diff($linked_categories, [$category_id]); update_post_meta($criterion_id, AFFILILABS_META_KEY_LINKED_CATEGORIES, $updated_categories); } } public static function get_ai_suggested_criteria_for_category(int $category_id, string $language): array { $products_data = AFFLProductService::get_products_by_category($category_id); $term = get_term($category_id, AFFILILABS_TAXONOMY_PRODUCT_CATEGORY); $category_name = ''; if ($term && !is_wp_error($term)) { $category_name = $term->name; } $current_criteria_details = self::get_criterions_by_categories([$category_id]); $data_for_ai = [ 'category_id' => $category_id, 'category_name' => $category_name, 'products' => $products_data, 'criteria' => $current_criteria_details ]; AFFL_LoggerService::debug('AI Suggest Criteria for Category - Data for AI: ' . esc_html(json_encode($data_for_ai, JSON_PRETTY_PRINT)), 'DEBUG'); $data = AFFL_AIService::request('ai/suggest_criteria', [ 'type' => 'suggest_criteria', 'data' => $data_for_ai, 'language' => $language ]); $suggested_criteria_json_string = $data['data'] ?? '[]'; $suggested_criteria = json_decode($suggested_criteria_json_string, true); if (json_last_error() !== JSON_ERROR_NONE) { AFFL_LoggerService::log('Failed to decode nested AI Suggest Criteria JSON string. Error: ' . esc_html(json_last_error_msg()) . ". String: " . esc_html($suggested_criteria_json_string), 'ERROR'); throw new Exception('Failed to decode nested AI Suggestion API response JSON: ' . esc_html(json_last_error_msg())); } if (!is_array($data) || !isset($data['status']) || $data['status'] !== 'ok' || !is_array($suggested_criteria)) { AFFL_LoggerService::log('Unexpected AI Suggest Criteria API response structure or status. Decoded suggestions: ' . esc_html(print_r($suggested_criteria, true)), 'WARNING'); throw new Exception('Unexpected AI Suggestion API response structure or status.' . esc_html($data['message'] ?? '') ); } AFFL_LoggerService::debug('AI Suggest Criteria for Category - Suggestions: ' . esc_html(print_r($suggested_criteria, true)), 'DEBUG'); //@TODO: Analyze a better way to handle this; likely @REMOVE this snippet below $current_category_criteria_ids = array_map(function($criterion) { return $criterion['id']; }, $current_criteria_details); foreach ($suggested_criteria as &$suggestion) { $suggestion['is_existing_criterion'] = false; $suggestion['existing_criterion_title'] = ''; if (isset($suggestion['id']) && $suggestion['id'] !== -1 && in_array($suggestion['id'], $current_category_criteria_ids)) { $suggestion['is_existing_criterion'] = true; $existing_criterion = array_filter($current_criteria_details, function($criterion) use ($suggestion) { return $criterion['id'] == $suggestion['id']; }); if (!empty($existing_criterion)) { $existing_criterion = array_shift($existing_criterion); $suggestion['existing_criterion_title'] = $existing_criterion['title'] ?? ''; } } } unset($suggestion); return ['status' => 'ok', 'suggestions' => $suggested_criteria]; } public static function create_or_update_criterion_from_suggestion_for_category(int $category_id, array $suggested_criterion): array { AFFL_LoggerService::debug('create_or_update_criterion_from_suggestion_for_category - Suggestion: ' . esc_html(print_r($suggested_criterion, true)), 'DEBUG'); $criterion_id = isset($suggested_criterion['id']) && $suggested_criterion['id'] !== -1 ? intval($suggested_criterion['id']) : null; $criterion_data = [ 'id' => $criterion_id, 'title' => sanitize_text_field($suggested_criterion['criteria_name'] ?? ''), 'description' => $suggested_criterion['description'] ?? '', 'fields' => $suggested_criterion['fields'] ?? [], 'linked_categories' => [$category_id], ]; AFFL_LoggerService::debug('create_or_update_criterion_from_suggestion_for_category - Criterion Data: ' . esc_html(print_r($criterion_data, true)), 'DEBUG'); $criterion_id = self::save($criterion_data); $updated_criteria_groups = self::get_criterions_by_categories([$category_id]); return [ 'status' => 'ok', 'message' => 'Criterion saved successfully.', 'criterion_id' => $criterion_id, 'criteria_groups' => $updated_criteria_groups ]; } /** * Renders the HTML for all criteria and their fields for a given product. * * @param array $all_criterion_values The array of criterion values for the product. * @return string The HTML output for the product criteria. */ public static function render_product_criteria_html(array $all_criterion_values): string { if (empty($all_criterion_values) || !is_array($all_criterion_values)) { return ''; } $output = '