445 lines
17 KiB
PHP
445 lines
17 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Affiliate Tracker
|
|
*
|
|
* Handles referral tracking via cookies and order creation.
|
|
*
|
|
* @package WooNooW\Modules\Affiliate
|
|
*/
|
|
|
|
namespace WooNooW\Modules\Affiliate;
|
|
|
|
if (!defined('ABSPATH')) exit;
|
|
|
|
class AffiliateTracker
|
|
{
|
|
const COOKIE_NAME = 'woonoow_ref';
|
|
const COOKIE_EXPIRES = 30; // days
|
|
|
|
/**
|
|
* Initialize tracking hooks
|
|
*/
|
|
public static function init()
|
|
{
|
|
error_log('[AffiliateTracker] init() called');
|
|
|
|
// Intercept referral links on init
|
|
add_action('init', [__CLASS__, 'capture_referral_link']);
|
|
|
|
// Hook into WooCommerce order creation (legacy checkout)
|
|
add_action('woocommerce_checkout_order_processed', [__CLASS__, 'record_referral'], 10, 3);
|
|
|
|
// Hook into WooCommerce 8.3+ block checkout
|
|
add_action('woocommerce_store_api_checkout_order_processed', [__CLASS__, 'record_referral_block'], 10, 1);
|
|
|
|
// Hook into new order creation (universal)
|
|
add_action('woocommerce_new_order', [__CLASS__, 'record_referral_new_order'], 10, 2);
|
|
|
|
// Hook for all order status changes
|
|
add_action('woocommerce_order_status_changed', [__CLASS__, 'handle_order_status_changed'], 10, 4);
|
|
|
|
// Hook for REST API order creation
|
|
add_action('rest_after_insert_shop_order', [__CLASS__, 'handle_rest_order_created'], 10, 3);
|
|
|
|
error_log('[AffiliateTracker] All hooks registered');
|
|
}
|
|
|
|
/**
|
|
* Handle order status change - track completed orders
|
|
*/
|
|
public static function handle_order_status_changed($order_id, $from_status, $to_status, $order)
|
|
{
|
|
if ($to_status === 'completed') {
|
|
self::process_order_for_referral($order_id);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle REST API order creation
|
|
*/
|
|
public static function handle_rest_order_created($order, $request, $creating)
|
|
{
|
|
if ($creating) {
|
|
self::process_order_for_referral($order->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;
|
|
}
|
|
}
|