outbox = $outbox instanceof Akkedli_V2_Outbox ? $outbox : null; $this->init_hooks(); } private function init_hooks() { // Hook into new order creation (HPOS compatible) add_action('woocommerce_new_order', array($this, 'handle_new_order'), 10, 2); // HPOS Store API hook add_action('woocommerce_store_api_checkout_order_processed', array($this, 'handle_new_order_hpos'), 10, 1); // Hook for status changes to processing add_action('woocommerce_order_status_processing', array($this, 'handle_order_processing'), 10, 2); // Hook for ALL status changes — sends status sync to Akkedli add_action('woocommerce_order_status_changed', array($this, 'handle_status_change'), 10, 4); } /** * Handle new order creation */ public function handle_new_order($order_id, $order = null) { $this->dispatch_new_order($order_id, $order, false); } private function dispatch_new_order($order_id, $order, $store_api) { if (Akkedli_API_Handler::is_callback_order($order_id)) return; $selection = Akkedli_Outbound_Selector::order($order_id); if (Akkedli_Outbound_Selector::is_v2($selection)) { if (is_object($this->outbox)) { if ($store_api) { $this->outbox->signal_store_api_order($order, $selection); } else { $this->outbox->signal_new_order($order_id, $order, $selection); } } return; } $legacy = Akkedli_Outbound_Selector::legacy($selection); if (!is_array($legacy)) return; if (!$order) { $order = wc_get_order($order_id); } if (!$order) { return; } // Check if plugin is configured if (empty($legacy['api_key']) || empty($legacy['webhook_url'])) { return; } // Check if we should send pending orders $send_pending = get_option('akkedli_send_pending', true); if (!$send_pending) { return; } // Prevent duplicate sends $transient_key = 'akkedli_order_sent_' . $order_id; if (get_transient($transient_key)) { return; } // Set transient to prevent duplicates (5 minutes) set_transient($transient_key, true, 5 * MINUTE_IN_SECONDS); // Send to webhook $this->send_order_to_webhook($order, $legacy); } /** * Handle HPOS Store API order */ public function handle_new_order_hpos($order) { if ($order instanceof WC_Order) { $this->dispatch_new_order($order->get_id(), $order, true); } } /** * Handle order status change to processing */ public function handle_order_processing($order_id, $order = null) { if (Akkedli_API_Handler::is_callback_order($order_id)) return; $legacy = Akkedli_Outbound_Selector::legacy( Akkedli_Outbound_Selector::processing($order_id) ); if (!is_array($legacy)) return; if (!$order) { $order = wc_get_order($order_id); } if (!$order) { return; } // Check if plugin is configured if (empty($legacy['api_key']) || empty($legacy['webhook_url'])) { return; } // Check if we should send processing orders $send_processing = get_option('akkedli_send_processing', true); if (!$send_processing) { return; } // Prevent duplicate sends $transient_key = 'akkedli_order_sent_' . $order_id; if (get_transient($transient_key)) { return; } set_transient($transient_key, true, 5 * MINUTE_IN_SECONDS); $this->send_order_to_webhook($order, $legacy); } /** * Handle ANY order status change — sends status sync to Akkedli backend * * Hook: woocommerce_order_status_changed * * @param int $order_id WooCommerce order ID * @param string $old_status Previous status (without wc- prefix) * @param string $new_status New status (without wc- prefix) * @param WC_Order $order WooCommerce order object */ public function handle_status_change($order_id, $old_status, $new_status, $order) { if (Akkedli_API_Handler::is_callback_order($order_id)) return; $selection = Akkedli_Outbound_Selector::status($order_id, $old_status, $new_status); if (Akkedli_Outbound_Selector::is_v2($selection)) { if (is_object($this->outbox)) { $this->outbox->signal_status( $order_id, $old_status, $new_status, $order, $selection ); } return; } $legacy = Akkedli_Outbound_Selector::legacy($selection); if (!is_array($legacy)) return; // Skip if not configured if (empty($legacy['api_key']) || empty($legacy['webhook_url'])) { return; } // Idempotency: skip if status hasn't actually changed if ($old_status === $new_status) { return; } // Prevent duplicate status syncs (2-minute cooldown per order+status) $transient_key = 'akkedli_status_sync_' . $order_id . '_' . $new_status; if (get_transient($transient_key)) { return; } set_transient($transient_key, true, 2 * MINUTE_IN_SECONDS); // Send status change to Akkedli $this->send_status_to_webhook($order, $old_status, $new_status, $legacy); } /** * Send order status change to Akkedli webhook with HMAC-SHA256 signature * * Endpoint: https://api.akkedli.com/webhook/woocommerce/status * Security: X-Akkedli-Signature header (HMAC-SHA256 using api_key) * * @param WC_Order $order WooCommerce order object * @param string $old_status Previous status * @param string $new_status New status */ private function send_status_to_webhook($order, $old_status, $new_status, array $legacy) { $webhook_url = $legacy['webhook_url']; $api_key = $legacy['api_key']; if (empty($webhook_url) || empty($api_key)) { return; } // Build status-change payload $payload = array( 'event' => 'order.status_changed', 'api_key' => $api_key, 'domain' => home_url(), 'timestamp' => current_time('c'), 'order_id' => $order->get_id(), 'order_number' => $order->get_order_number(), 'old_status' => $old_status, 'new_status' => $new_status, 'customer_name' => trim($order->get_billing_first_name() . ' ' . $order->get_billing_last_name()), 'customer_phone' => $order->get_billing_phone(), 'total_amount' => (float) $order->get_total(), 'shipping_total' => (float) $order->get_shipping_total(), ); $body = wp_json_encode($payload); // HMAC-SHA256 signature using api_key as secret $signature = 'sha256=' . hash_hmac('sha256', $body, $api_key); $status_url = self::LEGACY_STATUS_URL; $response = wp_remote_post($status_url, array( 'body' => $body, 'headers' => array( 'Content-Type' => 'application/json', 'User-Agent' => 'Akkedli-Connector/' . AKKEDLI_VERSION, 'X-API-Key' => $api_key, 'X-Akkedli-Signature' => $signature, ), 'timeout' => 30, )); // Log result as order note if (is_wp_error($response)) { $order->add_order_note( sprintf( /* translators: %1$s: old status, %2$s: new status, %3$s: error message */ __('Akkedli status sync failed (%1$s → %2$s): %3$s', 'akkedli-connector'), $old_status, $new_status, $response->get_error_message() ) ); } else { $response_code = wp_remote_retrieve_response_code($response); if ($response_code >= 200 && $response_code < 300) { $order->add_order_note( sprintf( /* translators: %1$s: old status, %2$s: new status */ __('Akkedli status synced: %1$s → %2$s', 'akkedli-connector'), $old_status, $new_status ) ); } else { $response_body = wp_remote_retrieve_body($response); $order->add_order_note( sprintf( /* translators: %1$s: old status, %2$s: new status, %3$d: HTTP status code, %4$s: response body */ __('Akkedli status sync returned %3$d (%1$s → %2$s): %4$s', 'akkedli-connector'), $old_status, $new_status, $response_code, substr($response_body, 0, 200) ) ); } } } /** * Send order data to webhook */ private function send_order_to_webhook($order, array $legacy) { $webhook_url = $legacy['webhook_url']; $api_key = $legacy['api_key']; if (empty($webhook_url)) { return; } // Build order data payload $order_data = array( 'event' => 'order.created', 'api_key' => $api_key, 'domain' => home_url(), 'timestamp' => current_time('c'), 'order' => array( 'id' => $order->get_id(), 'number' => $order->get_order_number(), 'status' => $order->get_status(), 'currency' => $order->get_currency(), 'total' => $order->get_total(), 'subtotal' => $order->get_subtotal(), 'shipping_total' => $order->get_shipping_total(), 'payment_method' => $order->get_payment_method(), 'payment_title' => $order->get_payment_method_title(), 'date_created' => $order->get_date_created() ? $order->get_date_created()->format('c') : '', 'customer_note' => $order->get_customer_note(), 'billing' => array( 'first_name' => $order->get_billing_first_name(), 'last_name' => $order->get_billing_last_name(), 'company' => $order->get_billing_company(), 'address_1' => $order->get_billing_address_1(), 'address_2' => $order->get_billing_address_2(), 'city' => $order->get_billing_city(), 'state' => $order->get_billing_state(), 'postcode' => $order->get_billing_postcode(), 'country' => $order->get_billing_country(), 'email' => $order->get_billing_email(), 'phone' => $order->get_billing_phone() ), 'shipping' => array( 'first_name' => $order->get_shipping_first_name(), 'last_name' => $order->get_shipping_last_name(), 'address_1' => $order->get_shipping_address_1(), 'address_2' => $order->get_shipping_address_2(), 'city' => $order->get_shipping_city(), 'state' => $order->get_shipping_state(), 'postcode' => $order->get_shipping_postcode(), 'country' => $order->get_shipping_country(), 'phone' => $order->get_shipping_phone() ), 'line_items' => array() ) ); // Add line items foreach ($order->get_items() as $item_id => $item) { $product = $item->get_product(); $item_data = array( 'id' => $item_id, 'name' => $item->get_name(), 'product_id' => $item->get_product_id(), 'variation_id' => $item->get_variation_id(), 'quantity' => $item->get_quantity(), 'subtotal' => $item->get_subtotal(), 'total' => $item->get_total(), 'sku' => $product ? $product->get_sku() : '', 'price' => $product ? $product->get_price() : '' ); $order_data['order']['line_items'][] = $item_data; } // Send webhook request $response = wp_remote_post($webhook_url, array( 'body' => wp_json_encode($order_data), 'headers' => array( 'Content-Type' => 'application/json', 'User-Agent' => 'Akkedli-Connector/' . AKKEDLI_VERSION, 'X-API-Key' => $api_key ), 'timeout' => 30 )); // Log result if (is_wp_error($response)) { $order->add_order_note( sprintf( /* translators: %1$s: error message from webhook */ __('Akkedli webhook failed: %1$s', 'akkedli-connector'), $response->get_error_message() ) ); } else { $response_code = wp_remote_retrieve_response_code($response); if ($response_code >= 200 && $response_code < 300) { $order->add_order_note(__('Order sent to Akkedli successfully.', 'akkedli-connector')); } else { $order->add_order_note( sprintf( /* translators: %1$d: HTTP response status code */ __('Akkedli webhook returned status: %1$d', 'akkedli-connector'), $response_code ) ); } } } }