container = $container; } /** * Register all hooks. * * @return void */ public function register(): void { $has_woocommerce = function_exists( 'aicommerce_has_woocommerce' ) && aicommerce_has_woocommerce(); // Website-centric features — run on any site, with or without WooCommerce. $this->register_core_hooks(); $this->register_schema_hooks(); $this->register_manifest_hooks(); $this->register_crawler_hooks(); $this->register_llms_txt_hooks(); $this->register_rest_hooks(); $this->register_admin_hooks(); $this->register_multilingual_hooks(); $this->register_whitelabel_hooks(); $this->register_addon_hooks(); $this->register_metrics_hooks(); // Translations are auto-loaded by WordPress.org since WP 4.6; no // load_plugin_textdomain() call is needed. // Ecommerce features — only when WooCommerce is active. if ( $has_woocommerce ) { $this->register_feed_hooks(); $this->register_gmc_hooks(); $this->register_bing_api_hooks(); $this->register_builder_hooks(); $this->register_content_hooks(); $this->register_bulk_hooks(); $this->register_image_alt_hooks(); $this->register_attribution_hooks(); $this->register_digest_hooks(); $this->register_mcp_hooks(); $this->register_soa_hooks(); } } /** * Register core (front-end + back-end) hooks. * * @return void */ private function register_core_hooks(): void { /** @var Database $database */ $database = $this->container->get( Database::class ); // Run migrations on admin_init so dbDelta has access to upgrade.php. add_action( 'admin_init', array( $database, 'migrate' ) ); // Plugin action links on Plugins page. add_filter( 'plugin_action_links_' . AICOMMERCE_PLUGIN_BASENAME, array( $this, 'add_action_links' ) ); } /** * Register Schema Engine hooks. * * Injects JSON-LD at priority 5 so it's among the first things in . * Also registers conflict detection and suppression, and cache invalidation. * * @return void */ private function register_schema_hooks(): void { if ( ! $this->container->has( SchemaEngine::class ) ) { return; } /** @var SchemaEngine $schema_engine */ $schema_engine = $this->container->get( SchemaEngine::class ); $has_woocommerce = function_exists( 'aicommerce_has_woocommerce' ) && aicommerce_has_woocommerce(); // Inject site-level schema (WebSite + Organization/OnlineStore/LocalBusiness) // on the home/shop page for every website type. if ( $this->container->has( \AICommerce\Schema\SiteSchema::class ) ) { /** @var \AICommerce\Schema\SiteSchema $site_schema */ $site_schema = $this->container->get( \AICommerce\Schema\SiteSchema::class ); add_action( 'wp_head', array( $site_schema, 'inject' ), 5 ); } // Inject content schema (Article for posts, WebPage for pages, breadcrumbs) // on singular non-product content — this works on any site. if ( $this->container->has( \AICommerce\Schema\ContentSchemaEngine::class ) ) { /** @var \AICommerce\Schema\ContentSchemaEngine $content_schema */ $content_schema = $this->container->get( \AICommerce\Schema\ContentSchemaEngine::class ); add_action( 'wp_head', array( $content_schema, 'inject' ), 5 ); } // The remaining product-specific wiring requires WooCommerce. if ( ! $has_woocommerce ) { return; } // Inject product JSON-LD on product pages — priority 5, before most themes. add_action( 'wp_head', array( $schema_engine, 'inject' ), 5 ); /** @var ConflictDetector $conflict_detector */ $conflict_detector = $schema_engine->get_conflict_detector(); // Detect and suppress conflicting SEO plugin schema on init. add_action( 'wp', static function () use ( $conflict_detector ): void { if ( function_exists( 'is_product' ) && is_product() ) { $conflict_detector->detect(); $conflict_detector->suppress(); } } ); // Clear validation + schema cache when a product is saved. if ( $this->container->has( Validator::class ) ) { /** @var Validator $validator */ $validator = $this->container->get( Validator::class ); add_action( 'woocommerce_update_product', static function ( int $product_id ) use ( $validator, $schema_engine ): void { $validator->clear_cache( $product_id ); $schema_engine->clear_schema_cache( $product_id ); } ); } } /** * Register feed generation hooks. * * Sets up Action Scheduler chunk processing, product update triggers * for dirty marking, and recurring generation schedules. * * @return void */ private function register_feed_hooks(): void { if ( ! $this->container->has( FeedGenerator::class ) ) { return; } /** @var FeedGenerator $generator */ $generator = $this->container->get( FeedGenerator::class ); /** @var ChangeDetector $change_detector */ $change_detector = $this->container->get( ChangeDetector::class ); // Register Action Scheduler chunk handler. add_action( FeedGenerator::CHUNK_ACTION, array( $generator, 'process_chunk' ), 10, 1 ); // Handle the recurring generation trigger. add_action( FeedGenerator::GENERATE_ACTION, static function () use ( $generator ): void { $generator->schedule_full_generation(); } ); // Schedule recurring feed generation on admin_init. add_action( 'admin_init', static function () use ( $generator ): void { $generator->schedule_recurring(); } ); // Mark products dirty when updated via WooCommerce. $mark_dirty = static function ( int $product_id ) use ( $change_detector ): void { $change_detector->mark_dirty( $product_id ); }; add_action( 'woocommerce_update_product', $mark_dirty ); add_action( 'woocommerce_new_product', $mark_dirty ); add_action( 'woocommerce_variation_saved', $mark_dirty ); // This hook passes a WC_Product object, not an int. add_action( 'woocommerce_product_object_updated_props', static function ( $product ) use ( $change_detector ): void { if ( $product instanceof \WC_Product ) { $change_detector->mark_dirty( $product->get_id() ); } } ); // Stock status and quantity changes. add_action( 'woocommerce_product_set_stock_status', static function ( int $product_id ) use ( $change_detector ): void { $change_detector->mark_dirty( $product_id ); } ); add_action( 'woocommerce_product_set_stock', static function ( $product ) use ( $change_detector ): void { if ( $product instanceof \WC_Product ) { $change_detector->mark_dirty( $product->get_id() ); } } ); // Register custom cron interval as fallback. add_filter( 'cron_schedules', array( FeedGenerator::class, 'add_cron_interval' ) ); } /** * Register /.well-known/ai-commerce.json manifest hooks. * * @return void */ private function register_manifest_hooks(): void { if ( ! $this->container->has( ManifestEndpoint::class ) ) { return; } /** @var ManifestEndpoint $manifest */ $manifest = $this->container->get( ManifestEndpoint::class ); add_action( 'init', array( $manifest, 'register_rewrite_rule' ) ); add_action( 'template_redirect', array( $manifest, 'maybe_serve' ) ); add_filter( 'query_vars', array( $manifest, 'add_query_var' ) ); } /** * Register Google Merchant Center hooks. * * @return void */ private function register_gmc_hooks(): void { if ( ! $this->container->has( GMCApiClient::class ) ) { return; } /** @var GMCApiClient $gmc_client */ $gmc_client = $this->container->get( GMCApiClient::class ); // Daily feed submission to GMC. add_action( 'aicommerce_gmc_submit', static function () use ( $gmc_client ): void { $file_manager = new \AICommerce\Feed\FeedFileManager(); $feed_url = $file_manager->get_feed_url( 'google-feed.xml' ); $gmc_client->submit_feed( $feed_url ); } ); // Schedule daily submission on admin_init. add_action( 'admin_init', static function () use ( $gmc_client ): void { $gmc_client->schedule_daily_submission(); } ); } /** * Register crawler monitoring hooks. * * @return void */ private function register_crawler_hooks(): void { if ( ! $this->container->has( CrawlerMonitor::class ) ) { return; } /** @var CrawlerMonitor $crawler */ $crawler = $this->container->get( CrawlerMonitor::class ); // Detect and log AI bot visits on front-end requests. add_action( 'template_redirect', array( $crawler, 'handle_request' ), 1 ); // Async log writer. add_action( 'aicommerce_log_crawler_visit', array( $crawler, 'log_visit' ), 10, 1 ); // Robots.txt filter. if ( $this->container->has( RobotsManager::class ) ) { /** @var RobotsManager $robots */ $robots = $this->container->get( RobotsManager::class ); add_filter( 'robots_txt', array( $robots, 'filter_robots_txt' ), 100, 2 ); } } /** * Register /llms.txt endpoint hooks. * * @return void */ private function register_llms_txt_hooks(): void { if ( ! $this->container->has( LlmsTxtGenerator::class ) ) { return; } /** @var LlmsTxtGenerator $llms */ $llms = $this->container->get( LlmsTxtGenerator::class ); add_action( 'init', array( $llms, 'register_rewrite_rule' ) ); add_action( 'template_redirect', array( $llms, 'maybe_serve' ) ); add_filter( 'query_vars', array( $llms, 'add_query_var' ) ); } /** * Register Bing IndexNow API hooks. * * @return void */ private function register_bing_api_hooks(): void { if ( ! $this->container->has( BingApiClient::class ) ) { return; } /** @var BingApiClient $bing_client */ $bing_client = $this->container->get( BingApiClient::class ); // Serve IndexNow key verification file. add_action( 'template_redirect', array( $bing_client, 'maybe_serve_key_file' ) ); // Schedule daily IndexNow ping. add_action( 'admin_init', static function () use ( $bing_client ): void { $bing_client->schedule_daily_ping(); } ); // IndexNow ping action. add_action( 'aicommerce_indexnow_ping', static function () use ( $bing_client ): void { $file_manager = new \AICommerce\Feed\FeedFileManager(); $feed_url = $file_manager->get_feed_url( 'bing-feed.xml' ); $bing_client->notify_feed_updated( $feed_url ); } ); } /** * Register REST API hooks. * * @return void */ private function register_rest_hooks(): void { if ( ! $this->container->has( RestController::class ) ) { return; } /** @var RestController $rest */ $rest = $this->container->get( RestController::class ); add_action( 'rest_api_init', array( $rest, 'register_routes' ) ); // Clean any stray output (e.g. PHP notices from other plugins) before // the REST API sends its JSON response. Without this, WP_DEBUG_DISPLAY // can prepend HTML that corrupts the JSON body. add_filter( 'rest_pre_serve_request', static function ( bool $served ): bool { $level = ob_get_level(); while ( $level > 0 ) { ob_end_clean(); --$level; } return $served; }, 1 ); // Rate limiting. if ( $this->container->has( RateLimiter::class ) ) { /** @var RateLimiter $rate_limiter */ $rate_limiter = $this->container->get( RateLimiter::class ); add_filter( 'rest_pre_dispatch', array( $rate_limiter, 'maybe_limit' ), 10, 3 ); } } /** * Register admin-only hooks. * * @return void */ private function register_admin_hooks(): void { if ( ! is_admin() ) { return; } add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_assets' ) ); // Admin menu. if ( $this->container->has( AdminMenu::class ) ) { /** @var AdminMenu $admin_menu */ $admin_menu = $this->container->get( AdminMenu::class ); $admin_menu->register(); } // Classic Editor meta box. if ( $this->container->has( MetaBox::class ) ) { /** @var MetaBox $meta_box */ $meta_box = $this->container->get( MetaBox::class ); $meta_box->register(); } // Gutenberg sidebar panel. if ( $this->container->has( GutenbergPanel::class ) ) { /** @var GutenbergPanel $gutenberg_panel */ $gutenberg_panel = $this->container->get( GutenbergPanel::class ); $gutenberg_panel->register(); } } /** * Register page-builder and shortcode integrations. * * Wires Elementor widget + dynamic tag, Divi module, Bricks element, * the [aicommerce_score] shortcode, and front-end badge CSS. * * @return void */ private function register_builder_hooks(): void { // Front-end badge CSS. add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_badge_styles' ) ); // [aicommerce_score] shortcode. add_shortcode( 'aicommerce_score', array( $this, 'render_score_shortcode' ) ); // Elementor integration. add_action( 'elementor/widgets/register', static function ( $widgets_manager ): void { $widgets_manager->register( new \AICommerce\Integrations\ElementorWidget() ); } ); add_action( 'elementor/elements/categories_registered', static function ( $elements_manager ): void { $elements_manager->add_category( 'aicommerce', array( 'title' => __( 'Tyralix AI Discoverability', 'ai-discoverability-kit' ), 'icon' => 'eicon-star-o', ) ); } ); add_action( 'elementor/dynamic_tags/register', static function ( $dynamic_tags_manager ): void { $dynamic_tags_manager->register( new \AICommerce\Integrations\ElementorDynamicTag() ); } ); // Divi integration. add_action( 'et_builder_ready', static function (): void { if ( class_exists( '\ET_Builder_Module' ) ) { new \AICommerce\Integrations\DiviModule(); } } ); // Bricks integration. add_action( 'init', static function (): void { if ( defined( 'BRICKS_VERSION' ) && class_exists( '\Bricks\Elements' ) ) { \Bricks\Elements::register_element( AICOMMERCE_PLUGIN_DIR . 'src/Integrations/BricksElement.php' ); } }, 11 ); } /** * Render the [aicommerce_score] shortcode. * * @param array|string $atts Shortcode attributes. * * @return string HTML output. */ public function render_score_shortcode( $atts ): string { $atts = shortcode_atts( array( 'style' => 'badge', 'show_label' => 'yes', 'color' => '#00e58a', 'font_size' => '24px', ), $atts, 'aicommerce_score' ); $product_id = get_the_ID(); if ( ! function_exists( 'wc_get_product' ) ) { return ''; } $product = wc_get_product( $product_id ); if ( ! $product ) { return ''; } if ( ! $this->container->has( \AICommerce\Analytics\ReadinessScore::class ) ) { return ''; } $readiness = $this->container->get( \AICommerce\Analytics\ReadinessScore::class ); $result = $readiness->calculate_and_cache( $product ); $score = (int) ( $result['overall'] ?? 0 ); $show_label = 'yes' === $atts['show_label']; if ( ! function_exists( 'aicommerce_render_score_badge' ) ) { return ''; } return aicommerce_render_score_badge( $score, $atts['style'], $show_label, $atts['color'], $atts['font_size'] ); } /** * Enqueue front-end score badge CSS. * * @return void */ public function enqueue_badge_styles(): void { if ( ! function_exists( 'is_product' ) || ! is_product() ) { return; } wp_register_style( 'aicommerce-badge', false, array(), AICOMMERCE_VERSION ); wp_enqueue_style( 'aicommerce-badge' ); $css = ' .aicommerce-score-badge { display:inline-block; text-align:center; } .aicommerce-badge-circle { display:inline-flex; align-items:center; justify-content:center; width:80px; height:80px; border-radius:50%; border:4px solid currentColor; font-weight:700; } .aicommerce-score-bar-wrap { width:100%; height:12px; background:#e0e0e0; border-radius:6px; overflow:hidden; margin-bottom:4px; } .aicommerce-score-bar { height:100%; border-radius:6px; transition:width .4s ease; } '; wp_add_inline_style( 'aicommerce-badge', $css ); } /** * Register content optimizer hooks (front-end toggle system). * * Injects AI-optimized descriptions and short descriptions when * the merchant has toggled them on for a specific product. * * @return void */ private function register_content_hooks(): void { if ( ! $this->container->has( ContentOptimizer::class ) ) { return; } /** @var ContentOptimizer $optimizer */ $optimizer = $this->container->get( ContentOptimizer::class ); // Inject optimized short description on product pages. add_filter( 'woocommerce_short_description', array( $optimizer, 'maybe_inject_short_description' ) ); // Inject optimized description on product pages. add_filter( 'the_content', array( $optimizer, 'maybe_inject_description' ) ); } /** * Register bulk optimization hooks. * * Wires Action Scheduler chunk processing for bulk content optimization. * * @return void */ private function register_bulk_hooks(): void { if ( ! $this->container->has( BulkQueue::class ) ) { return; } /** @var BulkQueue $bulk_queue */ $bulk_queue = $this->container->get( BulkQueue::class ); // Action Scheduler chunk handler. add_action( BulkQueue::CHUNK_ACTION, array( $bulk_queue, 'process_chunk' ), 10, 2 ); } /** * Register image alt text injection hooks. * * Injects AI-generated alt text on product images via the * wp_get_attachment_image_attributes filter. * * @return void */ private function register_image_alt_hooks(): void { if ( ! $this->container->has( ImageAltTextOptimizer::class ) ) { return; } /** @var ImageAltTextOptimizer $alt_optimizer */ $alt_optimizer = $this->container->get( ImageAltTextOptimizer::class ); add_filter( 'wp_get_attachment_image_attributes', array( $alt_optimizer, 'inject_alt_text' ), 10, 3 ); } /** * Register AI attribution tracking hooks. * * Detects AI referral sessions and records AI-attributed orders. * * @return void */ private function register_attribution_hooks(): void { if ( ! $this->container->has( AttributionTracker::class ) ) { return; } /** @var AttributionTracker $tracker */ $tracker = $this->container->get( AttributionTracker::class ); // Detect AI referral sessions on front-end page loads. add_action( 'wp', array( $tracker, 'detect_ai_session' ) ); // Record AI-attributed orders on checkout completion. add_action( 'woocommerce_thankyou', array( $tracker, 'record_ai_order' ), 10, 1 ); } /** * Register multilingual integration hooks (WPML + Polylang). * * @return void */ private function register_multilingual_hooks(): void { // WPML integration. if ( $this->container->has( WPMLIntegration::class ) && WPMLIntegration::is_available() ) { /** @var WPMLIntegration $wpml */ $wpml = $this->container->get( WPMLIntegration::class ); $wpml->register(); } // Polylang integration. if ( $this->container->has( PolylangIntegration::class ) && PolylangIntegration::is_available() ) { /** @var PolylangIntegration $polylang */ $polylang = $this->container->get( PolylangIntegration::class ); $polylang->register(); } } /** * Register white-label hooks (Agency tier only). * * @return void */ private function register_whitelabel_hooks(): void { if ( ! $this->container->has( WhiteLabel::class ) ) { return; } /** @var WhiteLabel $whitelabel */ $whitelabel = $this->container->get( WhiteLabel::class ); $whitelabel->apply(); } /** * Register weekly digest cron hooks. * * @return void */ private function register_digest_hooks(): void { if ( ! $this->container->has( WeeklyDigest::class ) ) { return; } /** @var WeeklyDigest $digest */ $digest = $this->container->get( WeeklyDigest::class ); // Schedule weekly cron. add_action( 'admin_init', array( $digest, 'schedule' ) ); // Digest send action. add_action( WeeklyDigest::CRON_HOOK, array( $digest, 'send' ) ); } /** * Register MCP (Model Context Protocol) hooks. * * Registers the MCP REST routes, discovery endpoint, and log writer. * * @return void */ private function register_mcp_hooks(): void { if ( ! $this->container->has( MCPServer::class ) ) { return; } /** @var MCPServer $mcp_server */ $mcp_server = $this->container->get( MCPServer::class ); /** @var AgentAuthenticator $agent_auth */ $agent_auth = $this->container->get( AgentAuthenticator::class ); $container = $this->container; // Register MCP REST routes. add_action( 'rest_api_init', static function () use ( $mcp_server, $agent_auth, $container ): void { // Main MCP endpoint. register_rest_route( 'aicommerce/v1', '/mcp', array( 'methods' => \WP_REST_Server::CREATABLE, 'callback' => array( $mcp_server, 'handle_request' ), 'permission_callback' => array( $agent_auth, 'verify_request' ), ) ); // MCP schema/discovery endpoint (public). register_rest_route( 'aicommerce/v1', '/mcp/schema', array( 'methods' => \WP_REST_Server::READABLE, 'callback' => array( $mcp_server, 'get_schema' ), 'permission_callback' => '__return_true', ) ); // MCP API key management. register_rest_route( 'aicommerce/v1', '/mcp/keys', array( array( 'methods' => \WP_REST_Server::CREATABLE, 'callback' => array( $agent_auth, 'handle_generate_key' ), 'permission_callback' => static function () { return \AICommerce\API\TierGate::require_business( 'MCP API keys' ); }, ), array( 'methods' => \WP_REST_Server::READABLE, 'callback' => array( $agent_auth, 'handle_list_keys' ), 'permission_callback' => static function () { return \AICommerce\API\TierGate::require_business( 'MCP API keys' ); }, ), ) ); register_rest_route( 'aicommerce/v1', '/mcp/keys/(?P[a-f0-9]{64})', array( 'methods' => \WP_REST_Server::DELETABLE, 'callback' => array( $agent_auth, 'handle_revoke_key' ), 'permission_callback' => static function () { return \AICommerce\API\TierGate::require_business( 'MCP API keys' ); }, ) ); // Agent analytics endpoints. register_rest_route( 'aicommerce/v1', '/analytics/agent-stats', array( 'methods' => \WP_REST_Server::READABLE, 'callback' => array( $mcp_server, 'get_agent_stats' ), 'permission_callback' => static function () { return \AICommerce\API\TierGate::require_business( 'Agent analytics' ); }, ) ); register_rest_route( 'aicommerce/v1', '/analytics/agent-log', array( 'methods' => \WP_REST_Server::READABLE, 'callback' => array( $mcp_server, 'get_agent_log' ), 'permission_callback' => static function () { return \AICommerce\API\TierGate::require_business( 'Agent log' ); }, 'args' => array( 'per_page' => array( 'type' => 'integer', 'default' => 50, ), 'page' => array( 'type' => 'integer', 'default' => 1, ), 'tool_name' => array( 'type' => 'string', 'default' => '', ), ), ) ); // Stripe webhook endpoint (public — authenticated via Stripe signature). $stripe_acp = $container->has( StripeACP::class ) ? $container->get( StripeACP::class ) : null; $order_creator = $container->has( OrderCreator::class ) ? $container->get( OrderCreator::class ) : null; if ( $stripe_acp && $order_creator ) { register_rest_route( 'aicommerce/v1', '/agent/webhook/stripe', array( 'methods' => \WP_REST_Server::CREATABLE, 'callback' => static function ( \WP_REST_Request $request ) use ( $stripe_acp, $order_creator ): \WP_REST_Response { $payload = $request->get_body(); $sig_header = $request->get_header( 'stripe-signature' ); if ( ! $sig_header ) { return new \WP_REST_Response( array( 'error' => 'Missing Stripe-Signature header' ), 400 ); } try { $event = $stripe_acp->verify_webhook( $payload, $sig_header ); } catch ( \Exception $e ) { return new \WP_REST_Response( array( 'error' => $e->getMessage() ), 400 ); } if ( 'checkout.session.completed' === ( $event['type'] ?? '' ) ) { $session_id = $event['data']['object']['metadata']['aicommerce_session'] ?? null; if ( $session_id ) { try { $order_creator->create_from_session( $session_id ); } catch ( \Exception $e ) { return new \WP_REST_Response( array( 'error' => $e->getMessage() ), 500 ); } } } return new \WP_REST_Response( array( 'received' => true ), 200 ); }, 'permission_callback' => '__return_true', ) ); // Agent sessions list endpoint (for dashboard). register_rest_route( 'aicommerce/v1', '/analytics/agent-sessions', array( 'methods' => \WP_REST_Server::READABLE, 'callback' => array( $mcp_server, 'get_agent_sessions' ), 'permission_callback' => static function () { return \AICommerce\API\TierGate::require_business( 'Agent sessions' ); }, ) ); } } ); // Async MCP log writer. add_action( 'aicommerce_log_mcp_request', array( $mcp_server, 'write_log_entry' ), 10, 1 ); // Load agent cart on WC checkout (for WC-native fallback). add_action( 'woocommerce_before_checkout_form', static function (): void { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $token = sanitize_text_field( wp_unslash( $_GET['aicommerce_agent_cart'] ?? '' ) ); if ( empty( $token ) ) { return; } $cart_items = get_transient( 'aicommerce_agent_cart_' . $token ); if ( ! $cart_items || ! is_array( $cart_items ) ) { return; } if ( ! function_exists( 'WC' ) || ! WC()->cart ) { return; } WC()->cart->empty_cart(); foreach ( $cart_items as $item ) { $product_id = $item['product_id'] ?? 0; $variation_id = $item['variation_id'] ?? 0; $quantity = $item['quantity'] ?? 1; WC()->cart->add_to_cart( $product_id, $quantity, $variation_id ); } delete_transient( 'aicommerce_agent_cart_' . $token ); } ); // Inventory reservation cleanup cron. if ( $this->container->has( InventoryReservation::class ) ) { /** @var InventoryReservation $reservation */ $reservation = $this->container->get( InventoryReservation::class ); // Register 15-minute cron interval. add_filter( 'cron_schedules', static function ( array $schedules ): array { $schedules['aicommerce_15min'] = array( 'interval' => 900, 'display' => __( 'Every 15 minutes', 'ai-discoverability-kit' ), ); return $schedules; } ); // Schedule cleanup cron. add_action( 'admin_init', static function (): void { if ( ! wp_next_scheduled( InventoryReservation::CLEANUP_HOOK ) ) { wp_schedule_event( time(), 'aicommerce_15min', InventoryReservation::CLEANUP_HOOK ); } } ); // Cleanup handler. add_action( InventoryReservation::CLEANUP_HOOK, array( $reservation, 'cleanup_expired_reservations' ) ); // Weekly data pruning. add_action( 'admin_init', static function (): void { if ( ! wp_next_scheduled( 'aicommerce_prune_old_records' ) ) { wp_schedule_event( time(), 'weekly', 'aicommerce_prune_old_records' ); } } ); add_action( 'aicommerce_prune_old_records', array( $reservation, 'prune_old_records' ) ); } // /.well-known/mcp.json discovery endpoint. add_action( 'init', static function (): void { add_rewrite_rule( '^\.well-known/mcp\.json$', 'index.php?aicommerce_mcp_discovery=1', 'top' ); } ); add_filter( 'query_vars', static function ( array $vars ): array { $vars[] = 'aicommerce_mcp_discovery'; return $vars; } ); add_action( 'template_redirect', static function () use ( $mcp_server ): void { if ( ! get_query_var( 'aicommerce_mcp_discovery' ) ) { return; } // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged @header( 'Content-Type: application/json; charset=utf-8' ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged @header( 'Cache-Control: public, max-age=3600' ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged @header( 'Access-Control-Allow-Origin: *' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo wp_json_encode( $mcp_server->get_discovery_data(), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT ); exit; } ); } /** * Register Share-of-Answer tracking hooks. * * @return void */ private function register_soa_hooks(): void { if ( ! $this->container->has( SOATracker::class ) ) { return; } /** @var SOATracker $soa */ $soa = $this->container->get( SOATracker::class ); /** @var CompetitorTracker $competitors */ $competitors = $this->container->get( CompetitorTracker::class ); // Daily SOA check cron. add_action( SOATracker::CRON_HOOK, array( $soa, 'run_daily_check' ) ); add_action( 'admin_init', static function (): void { if ( ! wp_next_scheduled( SOATracker::CRON_HOOK ) ) { $tomorrow_3am = strtotime( 'tomorrow 03:00:00' ); wp_schedule_event( $tomorrow_3am, 'daily', SOATracker::CRON_HOOK ); } } ); // SOA REST endpoints. add_action( 'rest_api_init', static function () use ( $soa, $competitors ): void { // GET /analytics/soa. register_rest_route( 'aicommerce/v1', '/analytics/soa', array( 'methods' => \WP_REST_Server::READABLE, 'callback' => static function ( \WP_REST_Request $request ) use ( $soa ): \WP_REST_Response { $days = (int) ( $request->get_param( 'days' ) ?: 30 ); return new \WP_REST_Response( $soa->get_soa_summary( $days ), 200 ); }, 'permission_callback' => static function () { return \AICommerce\API\TierGate::require_business( 'Share of Answer' ); }, 'args' => array( 'days' => array( 'type' => 'integer', 'default' => 30, ), ), ) ); // GET /analytics/competitors. register_rest_route( 'aicommerce/v1', '/analytics/competitors', array( 'methods' => \WP_REST_Server::READABLE, 'callback' => static function ( \WP_REST_Request $request ) use ( $competitors ): \WP_REST_Response { $days = (int) ( $request->get_param( 'days' ) ?: 30 ); return new \WP_REST_Response( $competitors->get_competitor_soa_comparison( $days ), 200 ); }, 'permission_callback' => static function () { return \AICommerce\API\TierGate::require_business( 'Competitor tracking' ); }, ) ); // POST /analytics/competitors. register_rest_route( 'aicommerce/v1', '/analytics/competitors', array( 'methods' => \WP_REST_Server::CREATABLE, 'callback' => static function ( \WP_REST_Request $request ) use ( $competitors ): \WP_REST_Response { $params = $request->get_json_params(); $domain = sanitize_text_field( $params['domain'] ?? '' ); $name = sanitize_text_field( $params['name'] ?? '' ); if ( empty( $domain ) ) { return new \WP_REST_Response( array( 'error' => 'Domain is required.' ), 400 ); } $id = $competitors->add_competitor( $domain, $name ); if ( ! $id ) { return new \WP_REST_Response( array( 'error' => 'Competitor already exists or invalid domain.' ), 409 ); } return new \WP_REST_Response( array( 'id' => $id, 'domain' => $domain, ), 201 ); }, 'permission_callback' => static function () { return \AICommerce\API\TierGate::require_business( 'Competitor tracking' ); }, ) ); // DELETE /analytics/competitors/{id}. register_rest_route( 'aicommerce/v1', '/analytics/competitors/(?P\d+)', array( 'methods' => \WP_REST_Server::DELETABLE, 'callback' => static function ( \WP_REST_Request $request ) use ( $competitors ): \WP_REST_Response { $id = (int) $request->get_param( 'id' ); $deleted = $competitors->remove_competitor( $id ); if ( ! $deleted ) { return new \WP_REST_Response( array( 'error' => 'Not found.' ), 404 ); } return new \WP_REST_Response( array( 'deleted' => true ), 200 ); }, 'permission_callback' => static function () { return \AICommerce\API\TierGate::require_business( 'Competitor tracking' ); }, ) ); // POST /analytics/soa/run-now. register_rest_route( 'aicommerce/v1', '/analytics/soa/run-now', array( 'methods' => \WP_REST_Server::CREATABLE, 'callback' => static function () use ( $soa ): \WP_REST_Response { $last_run = get_option( 'aicommerce_soa_last_run', '' ); if ( $last_run && ( time() - strtotime( $last_run ) ) < 21600 ) { return new \WP_REST_Response( array( 'error' => 'SOA check can only run once every 6 hours.' ), 429 ); } $platforms = $soa->get_active_platforms(); if ( empty( $platforms ) ) { return new \WP_REST_Response( array( 'error' => 'No AI API keys configured.' ), 400 ); } wp_schedule_single_event( time(), SOATracker::CRON_HOOK ); return new \WP_REST_Response( array( 'scheduled' => true, 'estimated_minutes' => count( $platforms ) * 2, ), 200 ); }, 'permission_callback' => static function () { return \AICommerce\API\TierGate::require_business( 'SOA run' ); }, ) ); } ); } /** * Register metrics export hooks. * * @return void */ private function register_metrics_hooks(): void { add_action( 'rest_api_init', static function (): void { register_rest_route( 'aicommerce/v1', '/admin/metrics', array( 'methods' => \WP_REST_Server::READABLE, 'callback' => static function (): \WP_REST_Response { $export = new \AICommerce\Admin\MetricsExport(); return $export->handle_export(); }, 'permission_callback' => static function (): bool { return current_user_can( 'manage_options' ); }, ) ); } ); } /** * Register add-on marketplace hooks. * * @return void */ private function register_addon_hooks(): void { // Initialize add-on registry on plugins_loaded. add_action( 'plugins_loaded', static function (): void { AddonRegistry::get_registered_addons(); }, 20 ); // REST endpoint for listing add-ons. add_action( 'rest_api_init', static function (): void { register_rest_route( 'aicommerce/v1', '/addons', array( 'methods' => \WP_REST_Server::READABLE, 'callback' => array( AddonRegistry::class, 'handle_list_addons' ), 'permission_callback' => static function (): bool { return current_user_can( 'manage_options' ); }, ) ); } ); } /** * Add action links to the Plugins list table. * * @param array $links Existing action links. * * @return array */ public function add_action_links( array $links ): array { $settings_link = sprintf( '%s', esc_url( admin_url( 'admin.php?page=tyralix-aicommerce' ) ), esc_html__( 'Settings', 'ai-discoverability-kit' ) ); array_unshift( $links, $settings_link ); return $links; } /** * Enqueue admin CSS and JavaScript. * * Only loads on AICommerce admin pages. * * @param string $hook_suffix The current admin page hook suffix. * * @return void */ public function enqueue_admin_assets( string $hook_suffix ): void { // Only load on our own admin pages. if ( ! str_contains( $hook_suffix, 'aicommerce' ) ) { return; } $asset_file = AICOMMERCE_PLUGIN_DIR . 'assets/build/index.asset.php'; if ( ! file_exists( $asset_file ) ) { return; } $asset = require $asset_file; wp_enqueue_script( 'aicommerce-admin', AICOMMERCE_PLUGIN_URL . 'assets/build/index.js', $asset['dependencies'], $asset['version'], true ); wp_enqueue_style( 'aicommerce-admin', AICOMMERCE_PLUGIN_URL . 'assets/build/index.css', array( 'wp-components' ), $asset['version'] ); $localize_data = array( 'restUrl' => esc_url_raw( rest_url( 'aicommerce/v1/' ) ), 'nonce' => wp_create_nonce( 'wp_rest' ), 'adminUrl' => esc_url( admin_url() ), 'version' => AICOMMERCE_VERSION, 'is_pro' => function_exists( 'aicommerce_is_pro' ) && aicommerce_is_pro(), 'is_business' => function_exists( 'aicommerce_is_business' ) && aicommerce_is_business(), 'is_agency' => function_exists( 'aicommerce_is_agency' ) && aicommerce_is_agency(), 'onboarding_complete' => (bool) get_option( 'aicommerce_onboarding_complete', false ), ); // Free edition: no upgrade URL. $localize_data['upgrade_url'] = ''; wp_localize_script( 'aicommerce-admin', 'aicommerceAdmin', $localize_data ); } }