stripe = $stripe; $this->reservation = $reservation; $this->fraud_detector = $fraud; } /** * Set the current agent identity. * * @param string $identity Agent identity hash. * * @return void */ public function set_agent_identity( string $identity ): void { $this->current_agent_identity = $identity; } /** * Initiate a checkout session. * * Validates all cart items, creates a session record in the DB, * then delegates to StripeACP for checkout URL generation. * * @param array $args Tool arguments containing 'items' array. * * @return array Session data for the agent. * * @throws \InvalidArgumentException If validation fails. * @throws \RuntimeException If stock/availability check fails. */ public function initiate_checkout( array $args ): array { if ( ! aicommerce_is_business() ) { throw new \AICommerce\Core\LicenseException( 'Business', 'Agentic commerce' ); } $cart_items = $args['items'] ?? array(); if ( empty( $cart_items ) || ! is_array( $cart_items ) ) { throw new \InvalidArgumentException( 'items array is required and must not be empty' ); } $validated_items = array(); $total = 0.0; foreach ( $cart_items as $item ) { $product_id = absint( $item['product_id'] ?? 0 ); $variation_id = absint( $item['variation_id'] ?? 0 ); $quantity = max( 1, (int) ( $item['quantity'] ?? 1 ) ); $product = $variation_id ? wc_get_product( $variation_id ) : wc_get_product( $product_id ); if ( ! $product || ! $product->is_purchasable() ) { throw new \InvalidArgumentException( esc_html( sprintf( 'Product %d is not available for purchase', $product_id ) ) ); } if ( ! $product->is_in_stock() ) { throw new \RuntimeException( esc_html( sprintf( "Product '%s' is out of stock", $product->get_name() ) ) ); } $price = (float) $product->get_price() * $quantity; $total += $price; $validated_items[] = array( 'product_id' => $product_id, 'variation_id' => $variation_id, 'quantity' => $quantity, 'price' => $price, ); } // Fraud detection. $fraud_score = 0; if ( $this->fraud_detector ) { $fraud_score = $this->fraud_detector->calculate_fraud_score( array( 'ip_address' => $this->get_request_ip(), 'agent_identity' => $this->current_agent_identity, 'cart_items' => $validated_items, 'total_amount' => $total, ) ); if ( $this->fraud_detector->is_blocked( $fraud_score ) ) { throw new \RuntimeException( 'Request flagged by fraud detection. Contact store support.' ); } } // Create session record. $session_id = wp_generate_uuid4(); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date $expires_at = date( 'Y-m-d H:i:s', time() + 600 ); global $wpdb; // Safe: internal $wpdb->prefix table names; user values use placeholders. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter $wpdb->insert( $wpdb->prefix . 'aicommerce_agent_sessions', array( 'session_id' => $session_id, 'agent_identity' => $this->current_agent_identity, 'status' => 'pending', 'cart_items' => wp_json_encode( $validated_items ), 'total_amount' => $total, 'currency' => get_woocommerce_currency(), 'ip_address' => $this->get_request_ip(), 'expires_at' => $expires_at, 'created_at' => current_time( 'mysql' ), ), array( '%s', '%s', '%s', '%s', '%f', '%s', '%s', '%s', '%s' ) ); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter // Reserve inventory (reduces WC stock temporarily). if ( $this->reservation ) { $this->reservation->reserve( $session_id, $validated_items ); } // Get checkout URL from Stripe or WC fallback. $checkout = $this->stripe->create_checkout_session( $validated_items, array( 'session_id' => $session_id ) ); // Update session with checkout URL and Stripe session ID. // Safe: internal $wpdb->prefix table names; user values use placeholders. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter $wpdb->update( $wpdb->prefix . 'aicommerce_agent_sessions', array( 'checkout_url' => $checkout['checkout_url'], 'stripe_session_id' => $checkout['session_id'], ), array( 'session_id' => $session_id ), array( '%s', '%s' ), array( '%s' ) ); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter return array( 'session_id' => $session_id, 'checkout_url' => $checkout['checkout_url'], 'total' => $total, 'currency' => get_woocommerce_currency(), 'expires_in' => 600, 'provider' => $checkout['provider'], 'items_count' => count( $validated_items ), ); } /** * Get the status of a checkout session. * * @param string $session_id The session UUID. * * @return array Session status data. * * @throws \InvalidArgumentException If session not found. */ public function get_checkout_status( string $session_id ): array { global $wpdb; // Safe: internal $wpdb->prefix table names; user values use placeholders. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter $session = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}aicommerce_agent_sessions WHERE session_id = %s", $session_id ), ARRAY_A ); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter if ( ! $session ) { throw new \InvalidArgumentException( esc_html( "Session not found: {$session_id}" ) ); } $expired = strtotime( $session['expires_at'] ) < time(); // Auto-expire pending sessions. if ( $expired && 'pending' === $session['status'] ) { // Safe: internal $wpdb->prefix table names; user values use placeholders. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter $wpdb->update( $wpdb->prefix . 'aicommerce_agent_sessions', array( 'status' => 'expired' ), array( 'session_id' => $session_id ), array( '%s' ), array( '%s' ) ); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter $session['status'] = 'expired'; } return array( 'session_id' => $session_id, 'status' => $session['status'], 'total' => (float) $session['total_amount'], 'currency' => $session['currency'], 'wc_order_id' => $session['wc_order_id'] ? (int) $session['wc_order_id'] : null, 'expired' => $expired, 'checkout_url' => 'pending' === $session['status'] && ! $expired ? $session['checkout_url'] : null, ); } /** * Get client IP from the current request. * * @return string */ private function get_request_ip(): string { $ip = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '0.0.0.0'; return '' !== $ip ? $ip : '0.0.0.0'; } }