get_id()); } } /** * Record referral from block checkout (Store API) */ public static function record_referral_block($order) { if (is_numeric($order)) { $order = wc_get_order($order); } if ($order) { self::process_order_for_referral($order->get_id()); } } /** * Record referral from woocommerce_new_order hook */ public static function record_referral_new_order($order_id, $order) { error_log('[AffiliateTracker] woocommerce_new_order hook fired for order: ' . $order_id); self::process_order_for_referral($order_id); } /** * Capture ?ref=CODE from URL and set cookie * Also captures UTM parameters for referral attribution */ public static function capture_referral_link() { error_log('[AffiliateTracker] capture_referral_link called, ref=' . ($_GET['ref'] ?? 'none')); $options = [ 'expires' => time() + (self::COOKIE_EXPIRES * DAY_IN_SECONDS), 'path' => '/', 'secure' => is_ssl(), 'samesite' => 'Lax' ]; $referral_code = ''; // 1. Capture from ?ref= parameter if (isset($_GET['ref']) && !empty($_GET['ref'])) { $referral_code = sanitize_text_field($_GET['ref']); } // 2. Or capture from collection slug in URL (e.g., /collection/my-slug) if (empty($referral_code)) { $request_uri = $_SERVER['REQUEST_URI'] ?? '/'; $path = parse_url($request_uri, PHP_URL_PATH); // Extract collection slug, accounting for possible subdirectories (e.g. /store/collection/slug) if (preg_match('#/collection/([^/]+)#', $path, $matches)) { $collection_slug = sanitize_text_field($matches[1]); global $wpdb; $collections_table = $wpdb->prefix . 'woonoow_affiliate_collections'; $affiliates_table = $wpdb->prefix . 'woonoow_affiliates'; $referral_code = $wpdb->get_var($wpdb->prepare(" SELECT a.referral_code FROM $collections_table c JOIN $affiliates_table a ON c.affiliate_id = a.id WHERE c.slug = %s ", $collection_slug)); } } // Set the cookie if we found a referral code if (!empty($referral_code)) { $result = setcookie(self::COOKIE_NAME, $referral_code, $options); $_COOKIE[self::COOKIE_NAME] = $referral_code; error_log('[AffiliateTracker] Set woonoow_ref cookie: ' . $referral_code . ', result=' . ($result ? 'true' : 'false')); error_log('[AffiliateTracker] Cookie options: ' . json_encode($options)); } else { // Check if cookie already exists from previous visit if (isset($_COOKIE[self::COOKIE_NAME])) { error_log('[AffiliateTracker] No ref param, but existing cookie: ' . $_COOKIE[self::COOKIE_NAME]); } else { error_log('[AffiliateTracker] No ref param and no existing cookie'); } } // Capture UTM parameters $utm_params = []; $utm_keys = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term']; foreach ($utm_keys as $key) { if (isset($_GET[$key]) && !empty($_GET[$key])) { $utm_params[$key] = sanitize_text_field($_GET[$key]); } } // Capture referrer URL if (isset($_SERVER['HTTP_REFERER']) && !empty($_SERVER['HTTP_REFERER'])) { $utm_params['referrer_url'] = esc_url_raw($_SERVER['HTTP_REFERER']); } // Store UTM params in cookie if any captured if (!empty($utm_params)) { $utm_json = json_encode($utm_params); setcookie(self::COOKIE_NAME . '_utm', $utm_json, $options); $_COOKIE[self::COOKIE_NAME . '_utm'] = $utm_json; error_log('[AffiliateTracker] Set woonoow_ref_utm cookie'); } } /** * Get active affiliate for an order * Checks both coupons and cookies. */ public static function get_affiliate_for_order($order) { global $wpdb; $table = $wpdb->prefix . 'woonoow_affiliates'; // 1. Check if a coupon is used that maps to an affiliate $used_coupons = $order->get_coupon_codes(); if (!empty($used_coupons)) { foreach ($used_coupons as $code) { $coupon = new \WC_Coupon($code); if ($coupon->get_id()) { $affiliate = $wpdb->get_row($wpdb->prepare( "SELECT * FROM $table WHERE coupon_id = %d AND status = 'active'", $coupon->get_id() )); if ($affiliate) return $affiliate; } } } // 2. Check referral code stored on the order by SPA checkout $order_referral_code = $order->get_meta('_woonoow_referral_code'); if (!empty($order_referral_code)) { $affiliate = $wpdb->get_row($wpdb->prepare( "SELECT * FROM $table WHERE referral_code = %s AND status = 'active'", sanitize_text_field($order_referral_code) )); if ($affiliate) return $affiliate; } // 3. Fallback to cookie if (isset($_COOKIE[self::COOKIE_NAME])) { $referral_code = sanitize_text_field($_COOKIE[self::COOKIE_NAME]); $affiliate = $wpdb->get_row($wpdb->prepare( "SELECT * FROM $table WHERE referral_code = %s AND status = 'active'", $referral_code )); if ($affiliate) return $affiliate; } return null; } /** * Get referral record for a specific order * Used by admin API to display affiliate info on order details */ public static function get_referral_for_order($order_id) { global $wpdb; $referrals_table = $wpdb->prefix . 'woonoow_referrals'; return $wpdb->get_row($wpdb->prepare( "SELECT * FROM $referrals_table WHERE order_id = %d", $order_id )); } /** * Record referral when order is processed (Checkout) */ public static function record_referral($order_id, $posted_data, $order) { self::process_order_for_referral($order_id); } /** * Record referral when order is created via Admin */ public static function record_referral_admin($post_id) { $order = wc_get_order($post_id); if ($order) { self::process_order_for_referral($post_id); } } /** * Process order and create pending referral record */ private static function process_order_for_referral($order_id) { error_log('[AffiliateTracker] process_order_for_referral() called for order: ' . $order_id); global $wpdb; $order = wc_get_order($order_id); if (!$order) { error_log('[AffiliateTracker] Order not found: ' . $order_id); return; } // Check if referral already exists for this order $referrals_table = $wpdb->prefix . 'woonoow_referrals'; $exists = $wpdb->get_var($wpdb->prepare("SELECT id FROM $referrals_table WHERE order_id = %d", $order_id)); if ($exists) { error_log('[AffiliateTracker] Referral already exists for order: ' . $order_id); return; } $affiliate = self::get_affiliate_for_order($order); if (!$affiliate) { error_log('[AffiliateTracker] No affiliate found for order: ' . $order_id); return; } error_log('[AffiliateTracker] Found affiliate: ' . $affiliate->referral_code . ' (rate: ' . ($affiliate->custom_commission_rate ?? 'null') . ')'); // Prevent self-referrals (unless admin override is enabled) $order_user_id = $order->get_user_id(); if ((int)$order_user_id === (int)$affiliate->user_id) { error_log('[AffiliateTracker] Self-referral detected, blocking'); $allow_self_referral = get_option('woonoow_affiliate_allow_self_referral', false); if (!$allow_self_referral) return; } // Calculate commission // Priority: Product rate > Affiliate custom rate > Global default $global_default_rate = (float) get_option('woonoow_affiliate_default_rate', 10); $affiliate_custom_rate = !empty($affiliate->custom_commission_rate) ? (float) $affiliate->custom_commission_rate : null; // Get order items - try WC method first, fallback to direct DB query $items = self::get_order_items($order_id); error_log('[AffiliateTracker] Order items: ' . count($items)); $total_commission = 0; if (!empty($items)) { foreach ($items as $item) { $product_id = (int) $item['product_id']; $line_total = (float) $item['line_total']; // Check for product-specific affiliate rate $product_rate = get_post_meta($product_id, '_woonoow_affiliate_commission_rate', true); $product_enabled = get_post_meta($product_id, '_woonoow_affiliate_enabled', true) === 'yes'; error_log('[AffiliateTracker] Product ' . $product_id . ': enabled=' . ($product_enabled ? 'yes' : 'no') . ', rate=' . $product_rate . ', line_total=' . $line_total); // Determine rate for this item (only if product affiliate is enabled) $item_rate = null; if ($product_enabled && $product_rate !== '') { $item_rate = (float) $product_rate; error_log('[AffiliateTracker] Using product rate: ' . $item_rate); } elseif ($affiliate_custom_rate !== null) { $item_rate = $affiliate_custom_rate; error_log('[AffiliateTracker] Using affiliate rate: ' . $item_rate); } else { $item_rate = $global_default_rate; error_log('[AffiliateTracker] Using global rate: ' . $item_rate); } if ($item_rate > 0 && $line_total > 0) { $total_commission += ($line_total * $item_rate) / 100; error_log('[AffiliateTracker] Added: ' . ($line_total * $item_rate / 100)); } } } else { // Fallback to subtotal if no items (legacy) $subtotal = (float)$order->get_subtotal(); $commission_rate = $affiliate_custom_rate ?? $global_default_rate; if ($commission_rate <= 0) return; $total_commission = ($subtotal * $commission_rate) / 100; } if ($total_commission <= 0) { error_log('[AffiliateTracker] Commission is 0, skipping'); return; } error_log('[AffiliateTracker] Total commission: ' . $total_commission); // Get UTM parameters from cookie $utm_data = []; if (isset($_COOKIE[self::COOKIE_NAME . '_utm'])) { $utm_data = json_decode($_COOKIE[self::COOKIE_NAME . '_utm'], true) ?: []; } // Insert pending referral $insert_data = [ 'affiliate_id' => $affiliate->id, 'order_id' => $order_id, 'customer_id' => $order->get_user_id() ?: null, 'commission_amount' => $total_commission, 'currency' => $order->get_currency(), 'status' => 'pending' ]; // Add UTM data if available if (!empty($utm_data)) { foreach (['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term', 'referrer_url'] as $utm_key) { if (isset($utm_data[$utm_key])) { $insert_data[$utm_key] = $utm_data[$utm_key]; } } } $inserted = $wpdb->insert($referrals_table, $insert_data); if (!$inserted) { error_log('[AffiliateTracker] Failed to insert referral for order ' . $order_id . ': ' . $wpdb->last_error); return; } $referral_id = $wpdb->insert_id; // Fire event for notifications do_action('woonoow/affiliate/referral_received', $referral_id, $affiliate, $order); // Trigger email notification to affiliate $user = get_userdata($affiliate->user_id); if ($user) { do_action('woonoow/email/trigger', 'affiliate_new_referral', $user->user_email, [ 'affiliate_name' => $user->display_name, 'commission_amount' => $total_commission, 'currency' => $order->get_currency(), 'order_number' => $order->get_order_number() ]); } // Schedule auto-approval (e.g., 14 days) via Action Scheduler if (function_exists('as_schedule_single_action')) { $approval_days = (int) AffiliateSettings::get_setting('woonoow_affiliate_holding_period', 14); $timestamp = time() + ($approval_days * DAY_IN_SECONDS); as_schedule_single_action($timestamp, 'woonoow_approve_referral', ['referral_id' => $referral_id], 'woonoow_affiliate'); } } /** * Get order items - direct DB query for HPOS compatibility */ private static function get_order_items($order_id) { global $wpdb; // First try HPOS order_product_lookup table (more reliable with HPOS) // Force fresh query by using wpdb::query with direct table $order_id_int = (int) $order_id; $items = $wpdb->get_results($wpdb->prepare( "SELECT product_id, product_qty, product_net_revenue as line_total FROM {$wpdb->prefix}wc_order_product_lookup WHERE order_id = %d", $order_id_int ), ARRAY_A); error_log('[AffiliateTracker] get_order_items() HPOS query returned: ' . count($items) . ' items for order ' . $order_id_int); // If no results from HPOS table, try legacy order_items table if (empty($items)) { $items = $wpdb->get_results($wpdb->prepare( "SELECT oim_product.meta_value as product_id, oim_total.meta_value as line_total FROM {$wpdb->prefix}woocommerce_order_items oi LEFT JOIN {$wpdb->prefix}woocommerce_order_itemmeta oim_product ON oi.order_item_id = oim_product.order_item_id AND oim_product.meta_key = '_product_id' LEFT JOIN {$wpdb->prefix}woocommerce_order_itemmeta oim_total ON oi.order_item_id = oim_total.order_item_id AND oim_total.meta_key = '_line_total' WHERE oi.order_id = %d AND oi.order_item_type = 'line_item'", $order_id ), ARRAY_A); } // Convert to standard format $result = []; foreach ($items as $item) { $product_id = isset($item['product_id']) ? (int) $item['product_id'] : 0; if ($product_id > 0) { $result[] = [ 'product_id' => $product_id, 'line_total' => (float) ($item['line_total'] ?? 0) ]; } } return $result; } }