$attributes */ public static function merge_cart_attributes( string $session_key, array $attributes ): void { $session_key = Agentmatica_WooCommerce_Cart::sanitize_session_key( $session_key ); if ( $session_key === '' || empty( $attributes ) ) { return; } $cart = Agentmatica_WooCommerce_Cart::get_cart_state_public( $session_key ); $existing = isset( $cart['attributes'] ) && is_array( $cart['attributes'] ) ? $cart['attributes'] : array(); foreach ( $attributes as $entry ) { if ( ! is_array( $entry ) ) { continue; } $key = isset( $entry['key'] ) ? (string) $entry['key'] : ''; if ( $key === '' ) { continue; } $existing[ $key ] = isset( $entry['value'] ) ? (string) $entry['value'] : ''; } $cart['attributes'] = $existing; Agentmatica_WooCommerce_Cart::save_cart_state_public( $session_key, $cart ); } /** * @param WC_Order $order * @param array $data */ public static function copy_cart_attribution_to_order( $order, $data ): void { unset( $data ); if ( ! $order instanceof WC_Order ) { return; } $session_key = Agentmatica_WooCommerce_Cart::sanitize_session_key( (string) WC()->session?->get( 'agentmatica_checkout_session_key', '' ) ); if ( $session_key === '' ) { return; } $cart = Agentmatica_WooCommerce_Cart::get_cart_state_public( $session_key ); $attributes = isset( $cart['attributes'] ) && is_array( $cart['attributes'] ) ? $cart['attributes'] : array(); foreach ( $attributes as $key => $value ) { $meta_key = self::normalize_meta_key( (string) $key ); if ( $meta_key !== '' ) { $order->update_meta_data( $meta_key, (string) $value ); } } } /** * @param int $order_id */ public static function maybe_push_order_attribution( $order_id ): void { $order = wc_get_order( $order_id ); if ( ! $order instanceof WC_Order ) { return; } $attribution = self::derive_attribution( $order ); if ( $attribution === null ) { return; } $token = Agentmatica_Auth::get_integration_token_for_outbound(); if ( $token === '' ) { return; } $platform_url = self::resolve_platform_api_url(); if ( $platform_url === '' ) { return; } $payload = array( 'order_id' => (int) $order->get_id(), 'order_number' => (string) $order->get_order_number(), 'total' => (string) $attribution['total'], 'currency' => (string) $order->get_currency(), 'shop_url' => home_url( '/' ), 'conversation_id' => self::sanitize_conversation_id( $attribution['conversation_id'] ), 'attribution_type' => $attribution['type'], 'attribution_reason' => self::sanitize_reason( $attribution['reason'], $attribution['type'] ), 'order_total' => (string) $order->get_total(), 'item_count' => (int) $order->get_item_count(), 'line_items' => self::summarize_line_items( $order, $attribution['type'] ), ); wp_remote_post( trailingslashit( $platform_url ) . 'api/webhooks/woocommerce/order-attribution', array( 'timeout' => 15, 'headers' => array( 'Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json', ), 'body' => wp_json_encode( $payload ), ) ); } /** * Product summary for Insights ("top products sold through the agent"). * Product data only: no customer names, emails or addresses. * * @return array */ private static function summarize_line_items( WC_Order $order, string $type ): array { $lines = array(); foreach ( $order->get_items() as $item ) { if ( ! $item instanceof WC_Order_Item_Product ) { continue; } if ( count( $lines ) >= 50 ) { break; } $quantity = max( 1, (int) $item->get_quantity() ); $assisted = (string) $item->get_meta( '_agentmatica_assisted', true ); if ( $assisted === '' ) { $assisted = (string) $item->get_meta( '_ai_assisted', true ); } $lines[] = array( 'product_id' => (int) $item->get_product_id(), 'variant_id' => (int) $item->get_variation_id(), 'title' => wp_strip_all_tags( (string) $item->get_name() ), 'quantity' => $quantity, 'price' => round( (float) $item->get_total() / $quantity, 2 ), 'agent' => $type === 'DIRECT' || $assisted === 'true' || $assisted === '1', ); } return $lines; } /** * @return array{type: string, total: float, reason: string, conversation_id: string}|null */ private static function derive_attribution( WC_Order $order ): ?array { $source = (string) $order->get_meta( '_agentmatica_source', true ); if ( $source === '' ) { $source = (string) $order->get_meta( '__agentmatica_source', true ); } if ( $source === '' ) { $source = (string) $order->get_meta( '__ai_source', true ); } if ( $source === 'true' || $source === '1' ) { $conversation_id = (string) $order->get_meta( '_agentmatica_session_id', true ); if ( $conversation_id === '' ) { $conversation_id = (string) $order->get_meta( '__agentmatica_session_id', true ); } if ( $conversation_id === '' ) { $conversation_id = (string) $order->get_meta( '__ai_session_id', true ); } $reason = (string) $order->get_meta( '__agentmatica_reason', true ); if ( $reason === '' ) { $reason = (string) $order->get_meta( '__ai_reason', true ); } if ( $reason === '' ) { $reason = 'agent_checkout'; } return array( 'type' => 'DIRECT', 'total' => (float) $order->get_total(), 'reason' => $reason, 'conversation_id' => $conversation_id, ); } $total = 0.0; $reason = ''; $conversation_id = ''; $matched = false; foreach ( $order->get_items() as $item ) { if ( ! $item instanceof WC_Order_Item_Product ) { continue; } $assisted = (string) $item->get_meta( '_agentmatica_assisted', true ); if ( $assisted === '' ) { $assisted = (string) $item->get_meta( '_ai_assisted', true ); } if ( $assisted !== 'true' && $assisted !== '1' ) { continue; } $matched = true; $total += (float) $item->get_total(); if ( $reason === '' ) { $reason = (string) $item->get_meta( '_agentmatica_reason', true ); } if ( $reason === '' ) { $reason = (string) $item->get_meta( '_ai_reason', true ); } if ( $conversation_id === '' ) { $conversation_id = (string) $item->get_meta( '_agentmatica_session', true ); } if ( $conversation_id === '' ) { $conversation_id = (string) $item->get_meta( '_ai_session', true ); } } if ( ! $matched ) { return null; } return array( 'type' => 'ASSISTED', 'total' => $total, 'reason' => $reason, 'conversation_id' => $conversation_id, ); } /** * Read an attribution field from the add-to-cart POST, falling back to its * legacy name. No nonce: WooCommerce's own add-to-cart form has none, and a * nonce would go stale on cached product pages. The value only tags the cart * line as agent-suggested; it grants nothing and is sanitized here. */ private static function posted_field( string $name, string $legacy ): string { foreach ( array( $name, $legacy ) as $key ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- see docblock. if ( isset( $_POST[ $key ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- see docblock. return sanitize_text_field( wp_unslash( (string) $_POST[ $key ] ) ); } } return ''; } /** * @param array $cart_item_data * @return array */ public static function capture_posted_line_attribution( array $cart_item_data ): array { $assisted = self::posted_field( '_agentmatica_assisted', '_ai_assisted' ); if ( $assisted !== 'true' && $assisted !== '1' ) { return $cart_item_data; } $cart_item_data['_agentmatica_assisted'] = 'true'; $session = self::posted_field( '_agentmatica_session', '_ai_session' ); if ( $session !== '' ) { $cart_item_data['_agentmatica_session'] = $session; } $reason = self::posted_field( '_agentmatica_reason', '_ai_reason' ); if ( $reason === 'agent_card' || $reason === 'recommended_product' ) { $cart_item_data['_agentmatica_reason'] = $reason; } return $cart_item_data; } /** * @param array $cart_item_data * @param mixed $request * @return array */ public static function capture_store_api_line_attribution( $cart_item_data, $request ): array { if ( ! is_array( $cart_item_data ) ) { $cart_item_data = array(); } $extensions = array(); if ( $request instanceof WP_REST_Request ) { $param = $request->get_param( 'extensions' ); if ( is_array( $param ) ) { $extensions = $param; } } $assisted = isset( $extensions['_agentmatica_assisted'] ) ? (string) $extensions['_agentmatica_assisted'] : ''; if ( $assisted === '' && isset( $extensions['_ai_assisted'] ) ) { $assisted = (string) $extensions['_ai_assisted']; } if ( $assisted !== 'true' && $assisted !== '1' ) { return $cart_item_data; } $cart_item_data['_agentmatica_assisted'] = 'true'; $session = isset( $extensions['_agentmatica_session'] ) ? (string) $extensions['_agentmatica_session'] : ''; if ( $session === '' && isset( $extensions['_ai_session'] ) ) { $session = (string) $extensions['_ai_session']; } if ( $session !== '' ) { $cart_item_data['_agentmatica_session'] = sanitize_text_field( $session ); } $reason = isset( $extensions['_agentmatica_reason'] ) ? (string) $extensions['_agentmatica_reason'] : ''; if ( $reason === '' && isset( $extensions['_ai_reason'] ) ) { $reason = (string) $extensions['_ai_reason']; } if ( $reason === 'agent_card' || $reason === 'recommended_product' ) { $cart_item_data['_agentmatica_reason'] = $reason; } return $cart_item_data; } /** * @param WC_Order_Item_Product $item * @param string $cart_item_key * @param array $values */ public static function copy_line_attribution_to_item( $item, $cart_item_key, $values ): void { unset( $cart_item_key ); if ( ! $item instanceof WC_Order_Item_Product || ! is_array( $values ) ) { return; } if ( ( $values['_agentmatica_assisted'] ?? '' ) !== 'true' && ( $values['_ai_assisted'] ?? '' ) !== 'true' ) { return; } $item->add_meta_data( '_agentmatica_assisted', 'true', true ); $session = (string) ( $values['_agentmatica_session'] ?? $values['_ai_session'] ?? '' ); if ( $session !== '' ) { $item->add_meta_data( '_agentmatica_session', $session, true ); } $reason = (string) ( $values['_agentmatica_reason'] ?? $values['_ai_reason'] ?? '' ); if ( $reason !== '' ) { $item->add_meta_data( '_agentmatica_reason', $reason, true ); } } private static function sanitize_reason( string $reason, string $type ): ?string { if ( $reason === 'agent_card' || $reason === 'recommended_product' || $reason === 'agent_checkout' ) { return $reason; } if ( $type === 'DIRECT' ) { return 'agent_checkout'; } return null; } private static function normalize_meta_key( string $key ): string { $key = trim( $key ); if ( $key === '' ) { return ''; } if ( strpos( $key, '_agentmatica_' ) === 0 || strpos( $key, '__agentmatica_' ) === 0 || strpos( $key, '__ai_' ) === 0 ) { if ( $key === '__agentmatica_session_id' || $key === '__ai_session_id' ) { return '_agentmatica_session_id'; } if ( $key === '__agentmatica_bot_id' || $key === '__ai_bot_id' ) { return '_agentmatica_bot_id'; } if ( $key === '__agentmatica_source' || $key === '__ai_source' ) { return '_agentmatica_source'; } if ( $key === '__agentmatica_reason' || $key === '__ai_reason' ) { return '_agentmatica_reason'; } return $key; } return '_agentmatica_' . ltrim( $key, '_' ); } private static function sanitize_conversation_id( string $value ): ?string { $value = trim( $value ); if ( $value === '' ) { return null; } if ( preg_match( '/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $value ) ) { return strtolower( $value ); } if ( preg_match( '/^c_[a-z0-9]+$/i', $value ) ) { return $value; } return null; } public static function resolve_platform_api_url(): string { $stored = trim( (string) get_option( self::PLATFORM_URL_OPTION, '' ) ); if ( $stored !== '' ) { return untrailingslashit( $stored ); } /** * Filter the Agentmatica platform API origin used for order attribution webhooks. * * @param string $url Default platform URL. */ $filtered = trim( (string) apply_filters( 'agentmatica_platform_api_url', 'https://agentmatica.com' ) ); return untrailingslashit( $filtered ); } }