__( 'Stock recovery (false-OOS)', 'ai-eshop-optimizer' ), 'description' => sprintf( __( 'Products the ERP marks OUT OF STOCK in the eshop that the PHYSICAL stores are still selling — strong evidence the eshop OOS is wrong. Returns candidates with days_oos, physical units sold + distinct sale-days since the OOS date, suggested restock qty, and meets_threshold (whether they clear the manager\'s auto-restock confidence rules). Also returns a recovered-sales summary: eshop units + %s turnover earned after each restock, split auto vs total, and a precision %% (success vs failure of restocks). Reads wp_aieo_dmm_stock_recovery_candidates + _events, built by the Catalog Sync → "Stock recovery" job.', 'ai-eshop-optimizer' ), $sym ), 'category' => \AIEO\Claude\Bootstrap::CATEGORY, 'input_schema' => array( 'default' => array(), 'type' => 'object', 'properties' => array( 'only_meets_threshold' => array( 'type' => 'boolean', 'default' => false, 'description' => 'Only return candidates that clear the auto-restock thresholds (the ones that would be auto-applied).', ), 'limit' => array( 'type' => 'integer', 'minimum' => 1, 'maximum' => 200, 'default' => 50, ), ), ), 'output_schema' => array( 'type' => 'object' ), 'execute_callback' => array( __CLASS__, 'execute' ), 'permission_callback' => array( __CLASS__, 'permission_check' ), 'meta' => array( 'annotations' => array( 'readonly' => true, 'idempotent' => true ), 'show_in_rest' => true, ), ) ); } public static function permission_check( $input = null ) { unset( $input ); return current_user_can( 'manage_woocommerce' ); } public static function execute( $input ) { global $wpdb; $input = is_array( $input ) ? $input : array(); $only = ! empty( $input['only_meets_threshold'] ); $limit = isset( $input['limit'] ) ? max( 1, min( 200, (int) $input['limit'] ) ) : 50; $cand = $wpdb->prefix . 'aieo_dmm_stock_recovery_candidates'; $ev = $wpdb->prefix . 'aieo_dmm_stock_recovery_events'; if ( (string) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $cand ) ) !== $cand ) { return new \WP_Error( 'aieo_sr_missing', __( 'Run the "Stock recovery" job under Catalog Sync first.', 'ai-eshop-optimizer' ) ); } $c = $wpdb->get_row( "SELECT COUNT(*) total, SUM(meets_threshold=1) meets, ROUND(SUM(est_lost_sales),2) est_lost, ROUND(SUM(phys_turnover_after),2) instore FROM `{$cand}` WHERE phys_units_after > 0", ARRAY_A ); $e = array(); if ( (string) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $ev ) ) === $ev ) { $e = $wpdb->get_row( "SELECT COUNT(*) applied, SUM(mode='auto') autos, ROUND(SUM(eshop_units_recovered),0) units, ROUND(SUM(eshop_turnover_recovered),2) turnover, ROUND(SUM(CASE WHEN mode='auto' THEN eshop_turnover_recovered ELSE 0 END),2) auto_turnover, SUM(outcome='success') successes, SUM(outcome='failure') failures FROM `{$ev}`", ARRAY_A ); } $succ = (int) ( $e['successes'] ?? 0 ); $fail = (int) ( $e['failures'] ?? 0 ); $where = 'phys_units_after > 0 AND status <> \'dismissed\'' . ( $only ? ' AND meets_threshold = 1' : '' ); $rows = $wpdb->get_results( "SELECT product_id, sku, name, days_oos, oos_source, phys_units_after, phys_sale_days, last_phys_sale_date, phys_turnover_after, est_lost_sales, lost_optimistic, lost_pessimistic, lost_average, suggested_restock_qty, meets_threshold, status, blocked_reason FROM `{$cand}` WHERE {$where} ORDER BY meets_threshold DESC, phys_units_after DESC, last_phys_sale_date DESC LIMIT {$limit}", ARRAY_A ); return array( 'summary' => array( 'candidates_selling_in_stores' => (int) ( $c['total'] ?? 0 ), 'meet_threshold' => (int) ( $c['meets'] ?? 0 ), 'estimated_lost_online_sales' => (float) ( $c['est_lost'] ?? 0 ), 'in_store_turnover_same_period' => (float) ( $c['instore'] ?? 0 ), 'restocks_applied' => (int) ( $e['applied'] ?? 0 ), 'restocks_auto' => (int) ( $e['autos'] ?? 0 ), 'units_recovered' => (float) ( $e['units'] ?? 0 ), 'turnover_recovered' => (float) ( $e['turnover'] ?? 0 ), 'turnover_recovered_via_auto' => (float) ( $e['auto_turnover'] ?? 0 ), 'precision_pct' => ( $succ + $fail ) > 0 ? round( 100 * $succ / ( $succ + $fail ), 1 ) : null, ), 'note' => sprintf( __( 'days_oos = how long the eshop has shown it OOS (oos_source: history=precise, latest_sale=estimated from last online sale). meets_threshold=1 means it clears the manager\'s auto-restock rules. est_lost_sales = the sales predictor\'s AVERAGE modelled lost-online-sales cost while OOS (lost_pessimistic..lost_optimistic = the range); estimated_lost_online_sales sums it. phys_turnover_after / in_store_turnover_same_period = the ACTUAL in-store net %s for the same period.', 'ai-eshop-optimizer' ), function_exists( 'get_woocommerce_currency_symbol' ) ? html_entity_decode( get_woocommerce_currency_symbol() ) : '' ), 'count' => count( (array) $rows ), 'products' => array_values( (array) $rows ), ); } }