prefix . 'aieo_dmm_user_behavior_metrics'; } public static function get($user_id) { $user_id = (int) $user_id; if ($user_id <= 0) { return self::defaults($user_id); } $cache_key = 'aieo_ubm:' . $user_id; $cached = wp_cache_get($cache_key, self::CACHE_GROUP); if (is_array($cached)) { return $cached; } global $wpdb; $row = $wpdb->get_row($wpdb->prepare( 'SELECT * FROM ' . self::table() . ' WHERE user_id = %d LIMIT 1', $user_id ), ARRAY_A); if (!is_array($row)) { $row = self::defaults($user_id); } else { $row['user_id'] = (int) $row['user_id']; $row['total_orders'] = (int) $row['total_orders']; $row['total_cancellations'] = (int) $row['total_cancellations']; $row['cancellation_rate'] = (float) $row['cancellation_rate']; $row['risk_band'] = (int) $row['risk_band']; } wp_cache_set($cache_key, $row, self::CACHE_GROUP, self::CACHE_TTL); return $row; } public static function risk_band($user_id) { $row = self::get($user_id); $band = (int) $row['risk_band']; return (int) apply_filters('aieo_dmm_user_risk_band', $band, $user_id, $row); } public static function compute_band($cancellation_rate, $total_orders) { $thresholds = (array) apply_filters('aieo_dmm_user_risk_thresholds', array( 'min_orders' => 3, 'elevated_rate' => 0.05, 'high_rate' => 0.15, )); $min_orders = max(1, (int) ($thresholds['min_orders'] ?? 3)); $elevated_rate = (float) ($thresholds['elevated_rate'] ?? 0.05); $high_rate = (float) ($thresholds['high_rate'] ?? 0.15); if ((int) $total_orders < $min_orders) { return 0; } if ($cancellation_rate >= $high_rate) { return 2; } if ($cancellation_rate >= $elevated_rate) { return 1; } return 0; } public static function recompute($user_id) { $user_id = (int) $user_id; if ($user_id <= 0) { return; } global $wpdb; $oc = $wpdb->prefix . 'aieo_dmm_order_cancellations'; $weights = (array) apply_filters('aieo_dmm_ubm_kind_weights', array(1 => 0.7, 2 => 1.0, 3 => 1.5)); $w_returned = (float) ($weights[1] ?? 0.7); $w_kept = (float) ($weights[2] ?? 1.0); $w_refused = (float) ($weights[3] ?? 1.5); $total_cancellations = (int) $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM {$oc} WHERE user_id = %d AND event_kind > 0", $user_id )); $weighted_score = (float) $wpdb->get_var($wpdb->prepare( "SELECT SUM(CASE event_kind WHEN 1 THEN %f WHEN 2 THEN %f WHEN 3 THEN %f ELSE 0 END) FROM {$oc} WHERE user_id = %d AND event_kind > 0", $w_returned, $w_kept, $w_refused, $user_id )); $last_cancelled_at = $wpdb->get_var($wpdb->prepare( "SELECT MAX(cancelled_at) FROM {$oc} WHERE user_id = %d AND event_kind > 0", $user_id )); $top_reason_slug = $wpdb->get_var($wpdb->prepare( "SELECT reason_slug FROM {$oc} WHERE user_id = %d AND event_kind > 0 GROUP BY reason_slug ORDER BY COUNT(*) DESC LIMIT 1", $user_id )); $total_orders = self::count_user_orders($user_id); $cancellation_rate = $total_orders > 0 ? round((float) $weighted_score / (float) $total_orders, 4) : 0.0; $risk_band = self::compute_band($cancellation_rate, $total_orders); $row = array( 'user_id' => $user_id, 'total_orders' => $total_orders, 'total_cancellations' => $total_cancellations, 'cancellation_rate' => $cancellation_rate, 'last_cancelled_at' => $last_cancelled_at ?: null, 'top_reason_slug' => $top_reason_slug ?: null, 'risk_band' => $risk_band, 'computed_at' => current_time('mysql', true), ); $tbl = self::table(); $exists = (int) $wpdb->get_var($wpdb->prepare( "SELECT 1 FROM {$tbl} WHERE user_id = %d", $user_id )); if ($exists) { $wpdb->update($tbl, $row, array('user_id' => $user_id)); } else { $wpdb->insert($tbl, $row); } wp_cache_delete('aieo_ubm:' . $user_id, self::CACHE_GROUP); return $row; } private static function count_user_orders($user_id) { global $wpdb; $hpos = $wpdb->prefix . 'wc_orders'; if (AIEO_Order_Store::uses_hpos() && $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $hpos)) === $hpos) { return (int) $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM {$hpos} WHERE customer_id = %d", $user_id )); } return (int) $wpdb->get_var($wpdb->prepare( "SELECT COUNT(DISTINCT pm.post_id) FROM {$wpdb->postmeta} pm JOIN {$wpdb->posts} p ON p.ID = pm.post_id AND p.post_type = 'shop_order' WHERE pm.meta_key = '_customer_user' AND pm.meta_value = %d", $user_id )); } public static function defaults($user_id) { return array( 'user_id' => (int) $user_id, 'total_orders' => 0, 'total_cancellations' => 0, 'cancellation_rate' => 0.0, 'last_cancelled_at' => null, 'top_reason_slug' => null, 'risk_band' => 0, 'computed_at' => null, ); } public static function backfill_all() { global $wpdb; $oc = $wpdb->prefix . 'aieo_dmm_order_cancellations'; $lr = $wpdb->prefix . 'aieo_dmm_order_line_refunds'; $user_ids = array(); $rows = $wpdb->get_col("SELECT DISTINCT user_id FROM {$oc} WHERE user_id > 0"); foreach ((array) $rows as $uid) { $uid = (int) $uid; if ($uid > 0) { $user_ids[$uid] = true; } } $rows = $wpdb->get_col("SELECT DISTINCT user_id FROM {$lr} WHERE user_id > 0"); foreach ((array) $rows as $uid) { $uid = (int) $uid; if ($uid > 0) { $user_ids[$uid] = true; } } $count = 0; foreach (array_keys($user_ids) as $uid) { self::recompute($uid); $count++; } return $count; } public static function recompute_stale($stale_hours = 24, $limit = 200) { global $wpdb; $tbl = self::table(); $ids = $wpdb->get_col($wpdb->prepare( "SELECT user_id FROM {$tbl} WHERE computed_at < DATE_SUB(UTC_TIMESTAMP(), INTERVAL %d HOUR) ORDER BY computed_at ASC LIMIT %d", (int) $stale_hours, (int) $limit )); $count = 0; foreach ((array) $ids as $uid) { self::recompute((int) $uid); $count++; } return $count; } public static function bootstrap() { static $done = false; if ($done) { return; } $done = true; add_action('aieo_dmm_recompute_behavior_metrics_for_user', array(__CLASS__, 'recompute'), 10, 1); } }