get_meta(Affilibee_Config::META_KEY_AFFILIATE_SLUG); $should_enqueue = (bool) $affiliate_slug; /** * Allow custom logic to enqueue even when no slug is present. * Example: attribution added later or stored elsewhere. * * @param bool $should_enqueue * @param WC_Order $order */ $should_enqueue = apply_filters('affilibee_should_enqueue_on_complete', $should_enqueue, $order); if (!$should_enqueue) { return; } // Avoid stacking identical jobs if completion fires multiple times quickly. if (function_exists('as_has_scheduled_action') && as_has_scheduled_action('affilibee_send_order', array('order_id' => $order_id), 'affilibee')) { return; } if (function_exists('as_enqueue_async_action')) { as_enqueue_async_action('affilibee_send_order', array('order_id' => $order_id), 'affilibee'); } else { // Fallback (keeps dev env working without AS). self::handle_order_completion($order_id); } } /** * Send completed order to the Affilibee API. * * Retries on 429/5xx (when Action Scheduler handles the call), * treats known duplicate responses as success to stop retries. * * @throws Exception To trigger AS retries when present. */ public static function handle_order_completion(int $order_id): void { /** @var \WC_Logger|null $logger */ $logger = function_exists('wc_get_logger') ? wc_get_logger() : null; $log_ctx = array('source' => 'affilibee'); if (!$order_id) { if ($logger) { $logger->warning('Missing $order_id in handle_order_completion', $log_ctx); } return; } $order = wc_get_order($order_id); if (!$order) { if ($logger) { $logger->warning('Invalid order_id ' . $order_id . ' in handle_order_completion', $log_ctx); } return; } $affiliate_slug = $order->get_meta(Affilibee_Config::META_KEY_AFFILIATE_SLUG); if (!$affiliate_slug) { if ($logger) { $logger->info('No affiliate slug on order ' . $order_id . ', skipping', $log_ctx); } return; } $currency = $order->get_currency(); $order_rows = []; foreach ($order->get_items('line_item') as $item) { /** @var WC_Order_Item_Product $item */ $qty = max(1, (int) $item->get_quantity()); $line_total = (float) $item->get_total(); // major units (post-discount), excl. tax $line_tax = (float) $item->get_total_tax(); // major units $line_total_minor = Affilibee_Helpers::to_minor_units($line_total, $currency); $line_tax_minor = Affilibee_Helpers::to_minor_units($line_tax, $currency); $unit_price_minor = (int) round($line_total_minor / $qty); $unit_tax_minor = (int) round($line_tax_minor / $qty); $product = $item->get_product(); // WC_Product|false $sku = ($product && $product->get_sku()) ? $product->get_sku() : 'empty'; $order_rows[] = array( 'product_sku' => $sku, 'product_name' => $item->get_name(), 'segment_slug' => 'default', 'price_value' => $unit_price_minor, // unit price in minor units 'tax_value' => $unit_tax_minor, // unit tax in minor units 'quantity' => $qty, ); } $body = array( 'affiliate_slug' => $affiliate_slug, 'order_no' => (string) $order_id, 'total_shipping_value' => Affilibee_Helpers::to_minor_units($order->get_shipping_total(), $currency), 'total_discount_value' => Affilibee_Helpers::to_minor_units($order->get_discount_total(), $currency), 'currency_code' => $currency, 'order_rows' => $order_rows, ); /** * Allow last-minute adjustments to the request body. * * @param array $body * @param WC_Order $order */ $body = apply_filters('affilibee_request_body', $body, $order); $url = Affilibee_Helpers::get_effective_api_url(); $token = Affilibee_Helpers::get_effective_api_token(); // Helpful for diagnostics on the API side. $ua = 'Affilibee-Woo/' . (defined('AFFILIBEE_VERSION') ? AFFILIBEE_VERSION : 'dev') . ' WP/' . get_bloginfo('version') . ' WC/' . (defined('WC_VERSION') ? WC_VERSION : 'unknown'); $headers = array( 'Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json', 'Accept' => 'application/json', 'X-Affilibee-Client' => $ua, ); /** * Allow sites to adjust headers (e.g., add an idempotency key). * * @param array $headers * @param WC_Order $order */ $headers = apply_filters('affilibee_request_headers', $headers, $order); $response = wp_remote_post($url, array( 'headers' => $headers, 'body' => wp_json_encode($body), 'timeout' => 15, 'sslverify' => false, )); if (is_wp_error($response)) { if ($logger) { $logger->error('Affilibee API network error: ' . $response->get_error_message(), $log_ctx); } if (function_exists('as_has_scheduled_action')) { throw new Exception('retry'); } return; } $code = (int) wp_remote_retrieve_response_code($response); $body_text = (string) wp_remote_retrieve_body($response); $json = json_decode($body_text, true); // Known duplicate signal (your API returns 422 + "already been taken"). $duplicate_422 = ( $code === 422 && is_array($json) && isset($json['data']['errors']['order_no']) && preg_match('/already been taken/i', implode(' ', (array) $json['data']['errors']['order_no'])) ); /** * Decide if the response should count as "success" (to stop retries). * * Defaults: 2xx, 409, or your duplicate 422 case. * * @param bool $is_success * @param int $code * @param array $json * @param WC_Order $order * @param string $raw_body */ $is_success = apply_filters( 'affilibee_is_success_response', (($code >= 200 && $code < 300) || $code === 409 || $duplicate_422), $code, is_array($json) ? $json : array(), $order, $body_text ); if ($is_success) { // Consider promoting this magic string into a config constant later. $order->update_meta_data('_affilibee_sent', time()); $order->save(); if ($logger) { $logger->info('Affilibee API success for order ' . $order_id . ' (HTTP ' . $code . ')', $log_ctx); } return; } // Retry only for transient conditions. $retryable = ($code >= 500 || $code === 429); /** * Allow sites to mark additional codes as retryable (or not). * * @param bool $retryable * @param int $code * @param array $json * @param WC_Order $order */ $retryable = apply_filters('affilibee_is_retryable_response', $retryable, $code, is_array($json) ? $json : array(), $order); if ($retryable) { if ($logger) { $logger->error('Affilibee API transient error ' . $code . ' for order ' . $order_id . '; body: ' . wp_strip_all_tags($body_text), $log_ctx); } if (function_exists('as_has_scheduled_action')) { throw new Exception('retry'); } return; } // Non-retryable client error (e.g., other 4xx). if ($logger) { $logger->error('Affilibee API HTTP ' . $code . ' for order ' . $order_id . '; body: ' . wp_strip_all_tags($body_text), $log_ctx); } } } /** * Register async handler for 'affilibee_send_order'. */ add_action('affilibee_send_order', static function ($order_id) { if (class_exists('Affilibee_Order_Completion')) { try { Affilibee_Order_Completion::handle_order_completion((int) $order_id); } catch (Exception $e) { if (function_exists('wc_get_logger')) { wc_get_logger()->error( 'Affilibee async send failed for order ' . $order_id . ': ' . $e->getMessage(), array('source' => 'affilibee') ); } throw $e; // Let AS retry. } } });