'No request data provided' ); } $endpoint = ( 'category' === $type ) ? Endpoints::get_cat_opt_req_url() : Endpoints::get_prod_opt_req_url(); $result = self::request( 'POST', $endpoint, $request_data ); if ( $result['is_wp_error'] ) { return array( 'error' => $result['error_message'] ); } $code = $result['code']; $msg = $result['message']; $body = $result['body']; if ( $code < 200 || $code >= 300 ) { Logger::write_log( 'error', "API request failed: HTTP {$code} {$msg}", array( 'endpoint' => $endpoint, 'status' => $code, 'body' => substr( (string) $body, 0, 2048 ), ) ); return array( 'error' => "HTTP $code $msg", 'body' => $body, 'endpoint' => $endpoint, 'status_code' => $code, ); } $decoded = json_decode( $body, true ); if ( json_last_error() === JSON_ERROR_NONE && is_array( $decoded ) ) { return $decoded; } $body = str_replace( '"', '', $body ); return array( 'batch_id' => $body ); } /** * Get the batch request status for a given batch request ID. * * @param string $id The batch request ID. * @return string The raw response from the API. */ public static function get_batch_request_status( $id ) { if ( null === $id || '' === $id ) { return 'Invalid batch request ID'; } $api_url = Endpoints::get_batch_opt_status_url( $id ) . '/status'; $result = self::request( 'GET', $api_url ); if ( $result['is_wp_error'] ) { return 'API request failed: ' . $result['error_message']; } $status_code = (int) $result['code']; if ( 404 === $status_code || 410 === $status_code ) { Logger::write_log( 'warning', "Batch request {$id} returned HTTP {$status_code} from middleware, marking as orphaned." ); BatchRequest::mark_orphaned_by_guid( (string) $id ); return wp_json_encode( array( 'status' => BatchRequestStatus::ORPHANED->value ) ); } if ( $status_code >= 400 ) { $body_preview = substr( $result['body'], 0, 256 ); Logger::write_log( 'error', "Batch request {$id} returned unexpected HTTP {$status_code} from middleware status endpoint. Body: {$body_preview}" ); return wp_json_encode( array( 'status' => BatchRequestStatus::ERROR->value ) ); } return $result['body']; } /** * Get a human-readable label for the batch request status. * * @param string $id The batch request ID. * @return string The status label. */ public static function get_batch_request_status_label( $id ) { if ( null === $id || '' === $id ) { return Messages::get( 'enums.null_id' ); } $status = self::get_batch_request_status_decoded( $id )['status'] ?? null; if ( isset( $status ) ) { // API v1 returns numeric status codes. Treat any numeric value (including numeric strings) as a v1 code. if ( is_numeric( $status ) ) { $enum_case = BatchRequestStatus::from_api_code( (int) $status ); return $enum_case ? BatchRequestStatus::to_human_readable( $enum_case ) : Messages::get( 'enums.status_error_label' ); } // API v2 returns string status labels, so if it's already a non-numeric string, map it directly. if ( is_string( $status ) ) { $enum_case = BatchRequestStatus::from_api_status( $status ); return $enum_case ? BatchRequestStatus::to_human_readable( $enum_case ) : Messages::get( 'enums.status_error_label' ); } return Messages::get( 'enums.status_unrecognized' ); } return Messages::get( 'enums.api_response_error' ); } /** * Get both the human-readable label and raw status for a given batch request ID. * * @param string $id The batch request ID. * @return array { * An associative array containing the human-readable label and raw status. * * @type string $label The human-readable status label. * @type string $status The raw status value. * } */ public static function get_batch_request_status_info( $id ): array { return array( 'label' => self::get_batch_request_status_label( $id ), 'status' => self::get_batch_request_status( $id ), ); } /** * Get and decode the batch request status for a given batch request ID. * * @param string $id The batch request ID. * @return array The decoded batch request status or an error message. */ public static function get_batch_request_status_decoded( $id ) { if ( null === $id || '' === $id ) { return array( 'error' => 'Invalid batch request ID' ); } $status_response = self::get_batch_request_status( $id ); $decoded = json_decode( $status_response, true ); if ( json_last_error() === JSON_ERROR_NONE && is_array( $decoded ) ) { return $decoded; } return array( 'error' => 'Failed to decode API response', 'body' => $status_response, ); } /** * Get the count of failed items for a given batch request ID. * * @param string $id The batch request ID. * @return int The count of failed items. */ public static function get_failed_count( $id ) { if ( null === $id || '' === $id ) { return 0; } $decoded = self::get_batch_request_status_decoded( $id ); $failed_products = isset( $decoded['failedProductItems'] ) ? (int) $decoded['failedProductItems'] : 0; $failed_categories = isset( $decoded['failedCategoryItems'] ) ? (int) $decoded['failedCategoryItems'] : 0; return $failed_products + $failed_categories; } /** * Get the count of processed items for a given batch request ID. * * @param string $id Batch request ID. * @return int */ public static function get_processed_count( $id ) { if ( null === $id || '' === $id ) { return 0; } $decoded = self::get_batch_request_status_decoded( $id ); $processed_products = isset( $decoded['processedProductItems'] ) ? (int) $decoded['processedProductItems'] : 0; $processed_categories = isset( $decoded['processedCategoryItems'] ) ? (int) $decoded['processedCategoryItems'] : 0; return $processed_products + $processed_categories; } /** * Get the total item count for a batch request ID. * * @param string $id Batch request ID. * @return int */ public static function get_total_count( $id ) { if ( null === $id || '' === $id ) { return 0; } return self::get_batch_request_status_decoded( $id )['totalItems'] ?? 0; } /** * Get the count of remaining items to be processed for a given batch request ID. * * @param string $id The batch request ID. * @return int The count of remaining items. */ public static function get_remaining_count( $id ) { if ( null === $id || '' === $id ) { return 0; } return self::get_total_count( $id ) - ( self::get_failed_count( $id ) + self::get_processed_count( $id ) ); } /** * Get a paged list of items for a given batch request ID and type. * * @param string $id The batch request ID. * @param string $type The type of items ('product' or 'category'). * @param int $page The page number to retrieve. * @return string The raw response from the API. */ public static function get_batch_request_paged( $id, $type, $page = 1 ) { if ( null === $id || '' === $id ) { return 'Invalid batch request ID'; } $type_plural = ( 'category' === $type ) ? 'categories' : $type . 's'; $api_url = Endpoints::get_batch_opt_status_url( $id ) . '/paged-' . $type_plural . '?Page=' . intval( $page ); $result = self::request( 'GET', $api_url ); if ( $result['is_wp_error'] ) { return 'API request failed: ' . $result['error_message']; } return $result['body']; } /** * Notify the Parallax API of an item status change (approved, rejected, or reverted). * * Fires a POST to the batch item status endpoint with the action taken and an * optional comment (used for rejections/reverts to capture the user's reason). * * @param string $batch_id Remote batch identifier (UUID stored in batch_request_id column). * @param string $item_type Item type: 'product' or 'category'. * @param string $status Action status: 'approved', 'rejected', or 'reverted'. * @param array $item_ids WooCommerce item IDs affected by this action. * @param string $comment Optional rejection/revert comment from the user. * @return array Decoded response or error payload. */ public static function send_item_status( string $batch_id, string $item_type, string $status, array $item_ids = array(), string $comment = '' ): array { if ( '' === $batch_id ) { return array( 'error' => 'Missing batch ID' ); } $allowed_statuses = array( 'approved', 'rejected', 'reverted' ); if ( ! in_array( $status, $allowed_statuses, true ) ) { return array( 'error' => 'Invalid status: ' . $status ); } $allowed_types = array( 'product', 'category' ); if ( ! in_array( $item_type, $allowed_types, true ) ) { return array( 'error' => 'Invalid item type: ' . $item_type ); } $endpoint = 'category' === $item_type ? Endpoints::get_batch_category_status_url( $batch_id ) : Endpoints::get_batch_product_status_url( $batch_id ); $body = array( 'status' => $status, 'itemIds' => array_values( array_map( 'intval', $item_ids ) ), ); if ( '' !== $comment ) { $body['comment'] = $comment; } $result = self::request( 'POST', $endpoint, $body, array( 'timeout' => 5 ) ); if ( $result['is_wp_error'] ) { Logger::write_log( 'error', 'send_item_status: WP_Error — ' . $result['error_message'], array( 'batch_id' => $batch_id, 'item_type' => $item_type, 'status' => $status, ) ); return array( 'error' => $result['error_message'] ); } $code = $result['code']; $response_msg = $result['message']; $response_body = $result['body']; if ( $code < 200 || $code >= 300 ) { Logger::write_log( 'error', "send_item_status: HTTP {$code} {$response_msg}", array( 'endpoint' => $endpoint, 'status' => $status, 'body' => substr( (string) $response_body, 0, 2048 ), ) ); return array( 'error' => "HTTP $code $response_msg", 'body' => $response_body, 'status_code' => $code, ); } $decoded = json_decode( $response_body, true ); if ( json_last_error() === JSON_ERROR_NONE && is_array( $decoded ) ) { return $decoded; } return array( 'success' => true ); } /** * Fetch client information from the Parallax API. * * @return array The client information or an error message. */ public static function get_client_info() { $client_id = Client::load()->get_client_id(); if ( ! $client_id ) { return array( 'error' => 'Client ID not found' ); } $api_url = Endpoints::get_admin_client_url() . '/' . rawurlencode( $client_id ); // If define is added (likely in wp-config.php or plugin) use that key instead of the logged in user's key to query the client admin endpoint. $opts = defined( 'EPARALLAX_ADMIN_API_KEY' ) ? array( 'key' => EPARALLAX_ADMIN_API_KEY ) : array(); $result = self::request( 'GET', $api_url, null, $opts ); if ( $result['is_wp_error'] ) { return array( 'error' => 'API request failed: ' . $result['error_message'] ); } $body = $result['body']; $decoded = json_decode( $body, true ); if ( json_last_error() === JSON_ERROR_NONE && is_array( $decoded ) ) { return $decoded; } return array( 'error' => 'Failed to decode API response', 'body' => $body, ); } /** * Execute an HTTP request and return a normalized result array. * * Callers receive either: * ['is_wp_error' => true, 'error_message' => string] * ['is_wp_error' => false, 'code' => int, 'message' => string, 'body' => string] * * @param string $method HTTP method ('GET' or 'POST'). * @param string $url Full request URL. * @param array|null $body Request body for POST requests. * @param array $opts Optional overrides: 'timeout' (int), 'key' (string API key). * @return array Normalized response. */ private static function request( string $method, string $url, ?array $body = null, array $opts = array() ): array { $upper = strtoupper( $method ); if ( 'GET' !== $upper && 'POST' !== $upper ) { return array( 'is_wp_error' => true, 'error_message' => "Unsupported HTTP method: $method", ); } $args = self::get_request_args( $body, $opts['key'] ?? null ); $args['method'] = $upper; if ( isset( $opts['timeout'] ) ) { $args['timeout'] = (int) $opts['timeout']; } $response = wp_remote_request( $url, $args ); if ( is_wp_error( $response ) ) { return array( 'is_wp_error' => true, 'error_message' => $response->get_error_message(), ); } return array( 'is_wp_error' => false, 'code' => wp_remote_retrieve_response_code( $response ), 'message' => wp_remote_retrieve_response_message( $response ), 'body' => wp_remote_retrieve_body( $response ), ); } /** * Helper to construct request arguments for wp_remote_get/post. * * @param mixed $body The request body, if any. * @param string|null $key Optional API key to use instead of the logged-in user's key. * @return array The request arguments. */ private static function get_request_args( $body = null, $key = null ): array { $key = $key ?? Client::load()->get_api_key(); return array( 'headers' => array( 'accept' => 'text/plain', 'Content-Type' => 'application/json', 'X-API-Key' => $key, 'Expect' => '', 'Accept-Encoding' => 'identity', 'X-Client-Application' => 'WooCommerce', 'X-Client-Website' => esc_url_raw( home_url( '/' ) ), ), 'body' => ( $body ? wp_json_encode( $body, JSON_UNESCAPED_UNICODE ) : null ), 'timeout' => 30, 'httpversion' => '1.1', 'data_format' => 'body', ); } }