debug_mode = (defined('WP_DEBUG') && WP_DEBUG) || $plugin_debug; } private function track_timeline(string $event, array $data = []): void { \AdvocateLoop\Core\Logger::debug($event, $data); } private function log(string $message, string $level = self::LOG_INFO, array $context = []): void { \AdvocateLoop\Core\Logger::log($message, $level, $context); } private function get_order_meta($order, string $key) { $order = $order instanceof \WC_Order ? $order : wc_get_order($order); return $order ? $order->get_meta($key, true) : ''; } private function set_order_meta($order, string $key, $value): void { $order = $order instanceof \WC_Order ? $order : wc_get_order($order); if (!$order) { return; } $order->update_meta_data($key, $value); $order->save(); } private function is_referral_coupon(string $coupon_code): bool { $upper = strtoupper($coupon_code); if (isset($this->coupon_configs[$upper])) { return true; } $referral_code = $this->extract_referral_code($coupon_code); $config = $this->get_cached_coupon_config($referral_code); if ($config !== null) { $cached_coupon_code = $config['coupon_code'] ?? null; if ($cached_coupon_code && strtoupper($cached_coupon_code) === $upper) { return true; } $cached_referral_code = $config['referral_code'] ?? null; if ($cached_referral_code && strtoupper($cached_referral_code) === strtoupper($referral_code)) { return true; } } return false; } private function get_cached_coupon_config(string $referral_code): ?array { $upper = strtoupper($referral_code); foreach ($this->coupon_configs as $code => $config) { if (isset($config['referral_code']) && strtoupper($config['referral_code']) === $upper) { return $config; } if ($this->extract_referral_code($code) === $upper) { return $config; } } return null; } private function extract_referral_code(string $coupon_code): string { $upper = strtoupper($coupon_code); if (strpos($upper, '-') !== false) { $parts = explode('-', $upper); return end($parts); } return $upper; } private function fetch_coupon_config_from_api(string $referral_code): ?array { $api_key = get_option('advocate_loop_api_key'); $brand_id = get_option('advocate_loop_brand_id'); if (empty($api_key) || empty($brand_id)) { $this->log('Cannot fetch coupon config - missing API credentials', self::LOG_ERROR); return null; } $visitor_id = $this->get_visitor_id(); $cart_total = WC()->cart ? WC()->cart->get_subtotal() : 0; $customer_email = ''; if (function_exists('WC') && WC()->customer) { $customer_email = WC()->customer->get_billing_email(); } if (empty($customer_email) && is_user_logged_in()) { $customer_email = wp_get_current_user()->user_email; } $this->log('=== FETCHING DISCOUNT CONFIG FROM API ===', self::LOG_INFO, [ 'referral_code' => $referral_code, 'visitor_id' => $visitor_id ?: 'none', 'cart_total' => $cart_total ]); $response = wp_remote_post(ADVOCATE_LOOP_API_URL . '/api/v1/validate/discount-eligibility', [ 'headers' => [ 'Content-Type' => 'application/json', 'X-API-Key' => $api_key, 'X-Brand-ID' => $brand_id ], 'body' => wp_json_encode([ 'referral_code' => $referral_code, 'visitor_id' => $visitor_id, 'cart_total' => $cart_total, 'customer_email' => $customer_email ]), 'timeout' => 10 ]); if (is_wp_error($response)) { $this->log('Failed to fetch discount config', self::LOG_ERROR, [ 'error' => $response->get_error_message(), 'referral_code' => $referral_code ]); return null; } $body = wp_remote_retrieve_body($response); $http_code = wp_remote_retrieve_response_code($response); $result = json_decode($body, true); $this->track_timeline('API Response received', [ 'http_code' => $http_code, 'success' => $result['success'] ?? 'missing', 'eligible' => $result['eligible'] ?? 'missing', 'reason' => $result['reason'] ?? 'none', 'has_discount' => isset($result['discount']) ? 'yes' : 'no' ]); if (!empty($result['cookie_actions'])) { $this->execute_cookie_actions($result['cookie_actions']); } if (!isset($result['success']) || !$result['success']) { $this->log('API returned unsuccessful response', self::LOG_WARNING, [ 'referral_code' => $referral_code, 'http_code' => $http_code, 'response' => $result ]); $this->track_timeline('API Error: success=false', [ 'response' => $result ]); return null; } if (!isset($result['eligible']) || !$result['eligible']) { $reason = $result['reason'] ?? 'unknown'; $message = $result['message'] ?? ''; $this->log('Not eligible for discount', self::LOG_WARNING, [ 'referral_code' => $referral_code, 'reason' => $reason, 'message' => $message ]); $this->track_timeline('Not eligible for discount', [ 'reason' => $reason, 'message' => $message ]); return null; } if (!isset($result['discount'])) { $this->log('No discount data in response', self::LOG_WARNING); return null; } $discount = $result['discount']; $config = [ 'coupon_code' => $discount['coupon_code'] ?? '', 'referral_code' => $result['advocate']['code'] ?? $referral_code, 'click_type' => $result['click_type'] ?? 'referral', 'amount' => $discount['raw_value'] ?? $discount['amount'] ?? 0, 'discount_type' => $discount['type'] === 'percent' ? 'percent' : 'fixed_cart', 'individual_use' => $discount['individual_use'] ?? false, 'minimum_amount' => $discount['minimum_purchase'] ?? null, 'maximum_amount' => $discount['maximum_discount'] ?? null, 'calculated_discount' => $discount['amount'] ?? 0, 'discount_display' => $discount['display'] ?? '' ]; $this->log('=== API DISCOUNT CONFIG RESPONSE ===', self::LOG_INFO, [ 'referral_code' => $referral_code, 'coupon_code_from_api' => $config['coupon_code'], 'click_type' => $config['click_type'], 'amount' => $config['amount'], 'discount_type' => $config['discount_type'], 'calculated_discount' => $config['calculated_discount'] ]); return $config; } private function cart_has_referral_coupon(): bool { if (!WC()->cart) { return false; } $applied_coupons = WC()->cart->get_applied_coupons(); foreach ($applied_coupons as $coupon_code) { if ($this->is_referral_coupon($coupon_code)) { return true; } } return false; } private function get_applied_referral_coupon(): ?string { if (!WC()->cart) { return null; } $applied_coupons = WC()->cart->get_applied_coupons(); foreach ($applied_coupons as $coupon_code) { if ($this->is_referral_coupon($coupon_code)) { return $coupon_code; } } return null; } private function is_first_referral_coupon(string $coupon_code): bool { if (!WC()->cart) { return true; } $upper = strtoupper($coupon_code); foreach (WC()->cart->get_applied_coupons() as $applied) { if ($this->is_referral_coupon($applied)) { return strtoupper($applied) === $upper; } } return true; } public function init(): void { if (!class_exists('WooCommerce')) { return; } $woo_settings = advocate_loop_woo_settings(); if (isset($woo_settings['enabled']) && !$woo_settings['enabled']) { return; } $this->log('WooCommerce integration initializing', self::LOG_DEBUG); $this->track_timeline('WooCommerce init started'); add_action('woocommerce_before_checkout_billing_form', [$this, 'display_referral_field_safe']); add_action('wp_loaded', [$this, 'maybe_apply_referral_discount_safe'], 20); add_action('woocommerce_cart_loaded_from_session', [$this, 'maybe_apply_referral_discount_safe'], 99); add_action('woocommerce_checkout_update_order_review', [$this, 'maybe_apply_referral_discount_safe'], 20); add_action('woocommerce_add_to_cart', [$this, 'on_cart_updated'], 99); add_action('woocommerce_cart_item_removed', [$this, 'on_cart_updated'], 99); add_action('woocommerce_after_cart_item_quantity_update', [$this, 'on_cart_updated'], 99); add_filter('woocommerce_get_shop_coupon_data', [$this, 'filter_virtual_coupon'], 10, 2); add_action('woocommerce_after_checkout_validation', [$this, 'validate_claim_eligibility_safe'], 10, 2); add_filter('woocommerce_cart_totals_coupon_label', [$this, 'customize_coupon_label'], 10, 2); add_filter('woocommerce_coupon_is_valid', [$this, 'validate_referral_coupon_safe'], 10, 3); add_filter('woocommerce_coupon_validate_minimum_amount', [$this, 'validate_minimum_amount'], 10, 3); add_filter('woocommerce_coupon_get_discount_amount', [$this, 'cap_discount_amount'], 10, 5); add_filter('woocommerce_coupon_message', [$this, 'suppress_wc_native_coupon_notice'], 10, 3); add_action('woocommerce_before_calculate_totals', [$this, 'reset_discount_tracking'], 1); add_action('woocommerce_applied_coupon', [$this, 'debug_log_coupon_applied']); add_action('woocommerce_removed_coupon', [$this, 'debug_log_coupon_removed']); add_action('woocommerce_checkout_order_created', [$this, 'track_claim_on_checkout']); add_action('woocommerce_payment_complete', [$this, 'track_conversion_on_payment']); add_action('woocommerce_order_status_processing', [$this, 'track_conversion_on_payment']); add_action('woocommerce_order_status_completed', [$this, 'track_conversion_on_payment']); add_action('woocommerce_order_refunded', [$this, 'track_refund'], 10, 2); add_action('woocommerce_order_status_refunded', [$this, 'track_order_status_refunded']); add_action('woocommerce_order_status_cancelled', [$this, 'track_order_cancelled']); add_action('woocommerce_order_status_failed', [$this, 'track_order_failed']); add_action('woocommerce_checkout_update_order_meta', [$this, 'save_referral_to_order']); add_action('woocommerce_admin_order_data_after_billing_address', [$this, 'display_referral_in_admin']); add_action('rest_api_init', [$this, 'register_woo_endpoints']); add_action('wp_ajax_advocate_loop_early_claim', [$this, 'ajax_early_claim']); add_action('wp_ajax_nopriv_advocate_loop_early_claim', [$this, 'ajax_early_claim']); add_action('woocommerce_thankyou', [$this, 'cleanup_storage_after_conversion'], 5); $this->log('WooCommerce integration initialized', self::LOG_DEBUG); } public function display_referral_field_safe($checkout): void { try { $this->log('display_referral_field called', self::LOG_DEBUG); $this->display_referral_field($checkout); $this->log('display_referral_field completed', self::LOG_DEBUG); } catch (\Throwable $e) { $this->log('display_referral_field error: ' . $e->getMessage(), self::LOG_ERROR, [ 'file' => $e->getFile(), 'line' => $e->getLine() ]); } } public function maybe_apply_referral_discount_safe($cart): void { try { $this->maybe_apply_referral_discount($cart); } catch (\Throwable $e) { $this->log('maybe_apply_referral_discount error: ' . $e->getMessage(), self::LOG_ERROR, [ 'file' => $e->getFile(), 'line' => $e->getLine() ]); } } public function validate_claim_eligibility_safe(array $data, \WP_Error $errors): void { try { $this->log('validate_claim_eligibility called', self::LOG_DEBUG); $this->validate_claim_eligibility($data, $errors); $this->log('validate_claim_eligibility completed', self::LOG_DEBUG); } catch (\Throwable $e) { $this->log('validate_claim_eligibility error: ' . $e->getMessage(), self::LOG_ERROR, [ 'file' => $e->getFile(), 'line' => $e->getLine() ]); } } public function validate_referral_coupon_safe(bool $valid, \WC_Coupon $coupon, \WC_Discounts $discounts): bool { try { return $this->validate_referral_coupon($valid, $coupon, $discounts); } catch (\Throwable $e) { $this->log('validate_referral_coupon error: ' . $e->getMessage(), self::LOG_ERROR, [ 'file' => $e->getFile(), 'line' => $e->getLine() ]); return $valid; } } public function validate_referral_coupon(bool $valid, \WC_Coupon $coupon, \WC_Discounts $discounts): bool { if (!$this->is_referral_coupon($coupon->get_code())) { return $valid; } if (!$valid) { return $valid; } static $validated_this_request = []; $coupon_code = $coupon->get_code(); if (isset($validated_this_request[$coupon_code])) { return $validated_this_request[$coupon_code]; } if (!$this->is_first_referral_coupon($coupon_code)) { $validated_this_request[$coupon_code] = false; $message = __('Only one referral discount can be used per order.', 'advocateloop-for-woocommerce'); if (!wc_has_notice($message, 'error')) { wc_add_notice($message, 'error'); } return false; } $referral_code = $this->extract_referral_code($coupon_code); $billing_email = null; if (WC()->checkout) { $billing_email = WC()->checkout->get_value('billing_email'); } if (!$billing_email && isset($_POST['billing_email'])) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- WooCommerce verifies its checkout nonce before this runs; reading already-submitted checkout data only. $billing_email = sanitize_email(wp_unslash($_POST['billing_email'])); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- WooCommerce verifies its checkout nonce before this runs; reading already-submitted checkout data only. } if (!$billing_email) { $validated_this_request[$coupon_code] = true; return $valid; } $api_key = get_option('advocate_loop_api_key'); $brand_id = get_option('advocate_loop_brand_id'); if (empty($api_key) || empty($brand_id)) { $validated_this_request[$coupon_code] = true; return $valid; } $cart_total = WC()->cart ? WC()->cart->get_subtotal() : 0; $response = wp_remote_post(ADVOCATE_LOOP_API_URL . '/api/v1/validate/discount-eligibility', [ 'headers' => [ 'Content-Type' => 'application/json', 'X-API-Key' => $api_key, 'X-Brand-ID' => $brand_id ], 'body' => wp_json_encode([ 'referral_code' => $referral_code, 'customer_email' => $billing_email, 'visitor_id' => $this->get_visitor_id(), 'cart_total' => $cart_total ]), 'timeout' => 10 ]); if (is_wp_error($response)) { $this->log('Eligibility check failed', self::LOG_ERROR, ['error' => $response->get_error_message()]); $validated_this_request[$coupon_code] = true; return $valid; } $result = json_decode(wp_remote_retrieve_body($response), true); if (!empty($result['cookie_actions'])) { $this->execute_cookie_actions($result['cookie_actions']); } if (isset($result['eligible']) && $result['eligible'] === false) { $validated_this_request[$coupon_code] = false; $message = $result['message'] ?? __('This discount is not available.', 'advocateloop-for-woocommerce'); if (!wc_has_notice($message, 'error')) { wc_add_notice($message, 'error'); } return false; } $validated_this_request[$coupon_code] = true; return $valid; } public function validate_minimum_amount(bool $should_fail, \WC_Coupon $coupon, float $subtotal): bool { if (!$this->is_referral_coupon($coupon->get_code())) { return $should_fail; } $minimum = $coupon->get_minimum_amount(); $cart_subtotal = WC()->cart ? WC()->cart->get_subtotal() : 0; $this->log('Minimum amount validation', self::LOG_DEBUG, [ 'coupon' => $coupon->get_code(), 'minimum_required' => $minimum, 'wc_reported_subtotal' => $subtotal, 'cart_get_subtotal' => $cart_subtotal, 'wc_says_should_fail' => $should_fail ? 'yes' : 'no' ]); if ($should_fail && $minimum > 0) { $actual_subtotal = 0; if (WC()->cart) { $actual_subtotal = max( floatval(WC()->cart->get_subtotal()), floatval(WC()->cart->get_cart_contents_total()), floatval(WC()->cart->get_displayed_subtotal()) ); } $this->log('Minimum check override evaluation', self::LOG_DEBUG, [ 'actual_subtotal' => $actual_subtotal, 'minimum' => $minimum, 'passes' => $actual_subtotal >= $minimum ? 'yes' : 'no' ]); if ($actual_subtotal >= $minimum) { $this->log('Overriding minimum check - cart actually meets minimum', self::LOG_INFO); return false; } } return $should_fail; } public function reset_discount_tracking(): void { $this->discount_tracking = []; } public function debug_log_coupon_applied(string $coupon_code): void { $is_ours = $this->is_referral_coupon($coupon_code); $referral_code = $this->extract_referral_code($coupon_code); $config = $this->get_cached_coupon_config($referral_code); $this->log('=== COUPON APPLIED ===', self::LOG_INFO, [ 'coupon_code_applied' => $coupon_code, 'is_advocate_loop_coupon' => $is_ours ? 'YES' : 'NO', 'extracted_referral_code' => $referral_code, 'config_in_cache' => $config ? 'YES' : 'NO', 'config_coupon_code' => $config['coupon_code'] ?? 'N/A', 'config_click_type' => $config['click_type'] ?? 'N/A', 'config_amount' => $config['amount'] ?? 'N/A', 'config_discount_type' => $config['discount_type'] ?? 'N/A', ]); if (WC()->cart) { $all_coupons = WC()->cart->get_applied_coupons(); $this->log('All applied coupons', self::LOG_INFO, [ 'coupons' => $all_coupons ]); } } public function debug_log_coupon_removed(string $coupon_code): void { $this->log('=== COUPON REMOVED ===', self::LOG_INFO, [ 'coupon_code_removed' => $coupon_code, 'is_advocate_loop_coupon' => $this->is_referral_coupon($coupon_code) ? 'YES' : 'NO' ]); } public function suppress_wc_native_coupon_notice($message, $msg_code, $coupon) { if ($msg_code !== \WC_Coupon::WC_COUPON_SUCCESS) { return $message; } if (!is_object($coupon) || !method_exists($coupon, 'get_code')) { return $message; } if ($this->is_referral_coupon($coupon->get_code())) { return ''; } return $message; } public function cap_discount_amount(float $discount, float $discounting_amount, $cart_item, bool $single, \WC_Coupon $coupon): float { $coupon_code = $coupon->get_code(); if (!$this->is_referral_coupon($coupon_code)) { return $discount; } $referral_code = $this->extract_referral_code($coupon_code); $config = $this->get_cached_coupon_config($referral_code); if (!$config) { return $discount; } $discount_cap = isset($config['maximum_amount']) ? floatval($config['maximum_amount']) : 0; if ($discount_cap <= 0) { return $discount; } $coupon_id = strtoupper($coupon_code); if (!isset($this->discount_tracking[$coupon_id])) { $this->discount_tracking[$coupon_id] = 0; } $remaining_cap = $discount_cap - $this->discount_tracking[$coupon_id]; if ($remaining_cap <= 0) { $this->log('Discount cap reached', self::LOG_DEBUG, [ 'coupon' => $coupon_code, 'cap' => $discount_cap, 'already_applied' => $this->discount_tracking[$coupon_id] ]); return 0; } $original_discount = $discount; if ($discount > $remaining_cap) { $discount = $remaining_cap; $this->log('Capping discount', self::LOG_DEBUG, [ 'coupon' => $coupon_code, 'original_discount' => $original_discount, 'capped_to' => $discount, 'cap' => $discount_cap ]); } $this->discount_tracking[$coupon_id] += $discount; return $discount; } public function get_referral_code(): ?string { $code = isset($_COOKIE[$this->cookie_name]) ? sanitize_text_field(wp_unslash($_COOKIE[$this->cookie_name])) : null; if ($this->debug_mode) { $all_cookies = array_map('sanitize_text_field', array_keys(wp_unslash($_COOKIE))); $al_cookies = array_filter($all_cookies, function($k) { return strpos($k, '__al') === 0; }); if (!$code) { $this->track_timeline('Cookie check: __alsrc NOT found', [ 'looking_for' => $this->cookie_name, 'al_cookies_in_php' => !empty($al_cookies) ? implode(', ', $al_cookies) : 'NONE', 'total_cookies_in_php' => count($all_cookies), 'wp_cookiepath' => defined('COOKIEPATH') ? COOKIEPATH : '/', 'wp_cookie_domain' => defined('COOKIE_DOMAIN') ? (COOKIE_DOMAIN ?: 'empty') : 'undefined', 'hint' => 'Cookie may be set with different path. Widget uses path=/, WP may use ' . (defined('COOKIEPATH') ? COOKIEPATH : '/') ]); } else { $this->track_timeline('Cookie check: __alsrc FOUND', [ 'code' => $code ]); } } return $code; } public function get_visitor_id(): ?string { return isset($_COOKIE[$this->visitor_id_cookie]) ? sanitize_text_field(wp_unslash($_COOKIE[$this->visitor_id_cookie])) : null; } private function is_code_refused(?string $code): bool { if (!$code || !function_exists('WC') || !WC()->session) { return false; } $refused = WC()->session->get(self::REFUSED_SESSION_KEY); return $refused && strtoupper((string) $refused) === strtoupper($code); } private function forget_refused_referral(string $code, string $reason): bool { if ($code && function_exists('WC') && WC()->session) { WC()->session->set(self::REFUSED_SESSION_KEY, strtoupper($code)); } if (in_array($reason, self::KEEP_CODE_REASONS, true)) { return false; } $paths = array_unique(['/', defined('COOKIEPATH') && COOKIEPATH ? COOKIEPATH : '/']); foreach (['__alsrc', '__alct'] as $name) { foreach ($paths as $path) { setcookie($name, '', time() - 3600, $path); if (defined('COOKIE_DOMAIN') && COOKIE_DOMAIN) { setcookie($name, '', time() - 3600, $path, COOKIE_DOMAIN); } } unset($_COOKIE[$name]); } return true; } private function execute_cookie_actions(array $cookie_actions): void { if (empty($cookie_actions)) { return; } foreach ($cookie_actions as $action) { $cookie_name = $action['name'] ?? ''; $action_type = $action['action'] ?? ''; if (empty($cookie_name)) { continue; } switch ($action_type) { case 'remove': setcookie($cookie_name, '', time() - 3600, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true); unset($_COOKIE[$cookie_name]); $this->log('Cookie removed (API instruction)', self::LOG_DEBUG, ['cookie' => $cookie_name]); break; case 'set': $value = $action['value'] ?? ''; $expires = isset($action['expires']) ? time() + intval($action['expires']) : time() + (30 * DAY_IN_SECONDS); setcookie($cookie_name, $value, $expires, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true); $_COOKIE[$cookie_name] = $value; $this->log('Cookie set (API instruction)', self::LOG_DEBUG, ['cookie' => $cookie_name]); break; } } } public function display_referral_field($checkout): void { $referral_code = $this->get_referral_code(); if (empty($referral_code) || $this->is_code_refused($referral_code)) { return; } $api_key = get_option('advocate_loop_api_key'); $brand_id = get_option('advocate_loop_brand_id'); $cache_key = 'advocate_loop_woo_reward_display_' . md5($referral_code); $reward_display = get_transient($cache_key); if ($reward_display === false) { $url = ADVOCATE_LOOP_API_URL . '/api/v1/brand/reward-display?referral_code=' . urlencode($referral_code); $response = wp_remote_get($url, [ 'headers' => [ 'X-API-Key' => $api_key, 'X-Brand-ID' => $brand_id ], 'timeout' => 10 ]); if (!is_wp_error($response)) { $result = json_decode(wp_remote_retrieve_body($response), true); if (isset($result['success']) && $result['success'] && isset($result['data'])) { $reward_display = $result['data']; set_transient($cache_key, $reward_display, 5 * MINUTE_IN_SECONDS); } } } if (!$reward_display) { $this->log('API unreachable, hiding referral section on checkout', self::LOG_WARNING); return; } $reward_text = $reward_display['referee_reward_text'] ?? 'your discount'; $applied_message = $reward_display['checkout_applied_message'] ?? ''; ?>
✓ ' . esc_html($reward_text) . '' ); } ?>
admin_url('admin-ajax.php'), 'earlyClaimNonce' => wp_create_nonce('advocate_loop_early_claim'), 'referralCodeHint' => $referral_code ?: '', ]); } private function refuse_referral_discount(array $data, string $email, string $referral_code, string $context): void { if (!empty($data['cookie_actions']) && is_array($data['cookie_actions'])) { $this->execute_cookie_actions($data['cookie_actions']); } $removed = false; $applied_coupon = $this->get_applied_referral_coupon(); if ($applied_coupon && WC()->cart) { $removed = WC()->cart->remove_coupon($applied_coupon); } $message = $data['message'] ?? __('This discount is not available.', 'advocateloop-for-woocommerce'); $reason = (string) ($data['error'] ?? ''); $code_cleared = $this->forget_refused_referral($referral_code, $reason); $this->log('=== ' . $context . ' REFUSED ===', self::LOG_INFO, [ 'email' => $email, 'referral_code' => $referral_code, 'reason' => $reason ?: 'unknown', 'coupon_removed' => $removed, 'code_cleared' => $code_cleared ]); wp_send_json_error([ 'message' => $message, 'action' => 'remove_coupon', 'reload' => (bool) $removed, 'clear_code' => $code_cleared ]); } public function ajax_early_claim(): void { if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'] ?? '')), 'advocate_loop_early_claim')) { wp_send_json_error(['message' => 'Security check failed']); } $referral_code = sanitize_text_field(wp_unslash($_POST['referral_code'] ?? '')); if (!$referral_code) { $referral_code = $this->get_referral_code(); } if (!$referral_code) { wp_send_json_error(['message' => 'No referral code found']); } $email = sanitize_email(wp_unslash($_POST['email'] ?? '')); $first_name = sanitize_text_field(wp_unslash($_POST['first_name'] ?? '')); $last_name = sanitize_text_field(wp_unslash($_POST['last_name'] ?? '')); $phone = sanitize_text_field(wp_unslash($_POST['phone'] ?? '')); $existing_claim_id = sanitize_text_field(wp_unslash($_POST['claim_id'] ?? '')); $previous_email = sanitize_email(wp_unslash($_POST['previous_email'] ?? '')); $is_update = sanitize_text_field(wp_unslash($_POST['is_update'] ?? '')) === '1'; $trigger = sanitize_text_field(wp_unslash($_POST['trigger'] ?? 'unknown')); $visitor_id = sanitize_text_field(wp_unslash($_POST['visitor_id'] ?? '')); if (!$visitor_id) { $visitor_id = $this->get_visitor_id(); } $this->log('=== EARLY CLAIM REQUEST ===', self::LOG_INFO, [ 'trigger' => $trigger, 'email' => $email, 'referral_code' => $referral_code, 'visitor_id' => $visitor_id, 'is_update' => !empty($existing_claim_id) && $previous_email !== $email ]); if (empty($email) || !is_email($email)) { wp_send_json_error(['message' => 'A valid email is required']); } $api_key = get_option('advocate_loop_api_key'); $brand_id = get_option('advocate_loop_brand_id'); if (empty($api_key) || empty($brand_id)) { wp_send_json_error(['message' => 'Configuration error']); } $cart_total = WC()->cart ? WC()->cart->get_subtotal() : 0; $eligibility_response = wp_remote_post(ADVOCATE_LOOP_API_URL . '/api/v1/validate/discount-eligibility', [ 'headers' => [ 'Content-Type' => 'application/json', 'X-API-Key' => $api_key, 'X-Brand-ID' => $brand_id ], 'body' => wp_json_encode([ 'referral_code' => $referral_code, 'customer_email' => $email, 'visitor_id' => $visitor_id, 'cart_total' => $cart_total ]), 'timeout' => 10 ]); if (is_wp_error($eligibility_response)) { $this->log('Early claim eligibility check failed', self::LOG_ERROR, [ 'error' => $eligibility_response->get_error_message() ]); } else { $eligibility_result = json_decode(wp_remote_retrieve_body($eligibility_response), true); if (!empty($eligibility_result['cookie_actions'])) { $this->execute_cookie_actions($eligibility_result['cookie_actions']); } if (!empty($eligibility_result['click_type'])) { $click_type = $eligibility_result['click_type']; } if (isset($eligibility_result['eligible']) && $eligibility_result['eligible'] === false) { $removed = false; $applied_coupon = $this->get_applied_referral_coupon(); if ($applied_coupon && WC()->cart) { $removed = WC()->cart->remove_coupon($applied_coupon); } $error_message = $eligibility_result['message'] ?? 'This discount is no longer available for your account.'; $this->log('=== EARLY CLAIM REJECTED - NOT ELIGIBLE ===', self::LOG_INFO, [ 'email' => $email, 'referral_code' => $referral_code, 'reason' => $eligibility_result['reason'] ?? $error_message, 'coupon_removed' => $removed ]); wp_send_json_error([ 'message' => $error_message, 'action' => 'remove_coupon', 'reload' => (bool) $removed ]); } } if ($existing_claim_id && $is_update) { $this->log('=== UPDATING EARLY CLAIM (customer info changed) ===', self::LOG_INFO, [ 'claim_id' => $existing_claim_id, 'previous_email' => $previous_email, 'new_email' => $email, 'first_name' => $first_name, 'last_name' => $last_name, 'phone' => $phone ]); $customer_name = trim($first_name . ' ' . $last_name); $update_body = [ 'customer_name' => $customer_name, 'customer_email' => $email, 'customer_phone' => $phone ]; $update_url = ADVOCATE_LOOP_API_URL . '/api/v1/track/claim/' . $existing_claim_id; $this->log('Sending PATCH to: ' . $update_url, self::LOG_DEBUG); $response = wp_remote_request($update_url, [ 'method' => 'PATCH', 'headers' => [ 'Content-Type' => 'application/json', 'X-API-Key' => $api_key, 'X-Brand-ID' => $brand_id ], 'body' => wp_json_encode($update_body), 'timeout' => 15 ]); if (is_wp_error($response)) { $this->log('Early claim update WP error', self::LOG_ERROR, [ 'claim_id' => $existing_claim_id, 'error' => $response->get_error_message() ]); } else { $status_code = wp_remote_retrieve_response_code($response); $body = wp_remote_retrieve_body($response); $data = json_decode($body, true); $this->log('Early claim update response', self::LOG_DEBUG, [ 'status_code' => $status_code, 'body' => $body ]); if (isset($data['success']) && $data['success']) { $old_key = 'advocate_loop_woo_early_claim_' . md5($visitor_id . '_' . $previous_email); $new_key = 'advocate_loop_woo_early_claim_' . md5($visitor_id . '_' . $email); delete_transient($old_key); set_transient($new_key, $existing_claim_id, HOUR_IN_SECONDS); if (WC()->session) { WC()->session->set('al_early_claim_email', $email); } $this->log('=== EARLY CLAIM UPDATED SUCCESSFULLY ===', self::LOG_INFO, [ 'claim_id' => $existing_claim_id, 'new_email' => $email ]); wp_send_json_success([ 'message' => 'Claim updated', 'claim_id' => $existing_claim_id, 'updated' => true ]); } else { if (in_array($data['error'] ?? '', self::CLAIM_REFUSAL_CODES, true)) { $this->refuse_referral_discount($data, $email, $referral_code, 'EARLY CLAIM UPDATE'); } $this->log('Early claim update API error', self::LOG_WARNING, [ 'claim_id' => $existing_claim_id, 'status' => $status_code, 'error' => $data['error'] ?? 'Unknown error' ]); } } $this->log('Early claim update failed, will check for existing or create new', self::LOG_WARNING, [ 'claim_id' => $existing_claim_id ]); } $early_claim_key = 'advocate_loop_woo_early_claim_' . md5($visitor_id . '_' . $email); $cached_claim_id = get_transient($early_claim_key); if ($cached_claim_id) { $this->log('Early claim already exists for this email', self::LOG_DEBUG, [ 'claim_id' => $cached_claim_id, 'email' => $email ]); wp_send_json_success([ 'message' => 'Claim already recorded', 'claim_id' => $cached_claim_id, 'already_exists' => true ]); } $customer_name = trim($first_name . ' ' . $last_name); $cart_subtotal = WC()->cart ? WC()->cart->get_subtotal() : 0; $cart_total = WC()->cart ? floatval(WC()->cart->get_total('edit')) : 0; $discount_total = WC()->cart ? floatval(WC()->cart->get_discount_total()) : 0; $coupons_applied = []; if (WC()->cart) { foreach (WC()->cart->get_applied_coupons() as $code) { $coupons_applied[] = [ 'code' => $code, 'discount_amount' => floatval(WC()->cart->get_coupon_discount_amount($code)), 'discount_tax' => floatval(WC()->cart->get_coupon_discount_tax_amount($code)), ]; } } $applied_coupon = $this->get_applied_referral_coupon(); $products = []; if (WC()->cart) { foreach (WC()->cart->get_cart() as $cart_item) { $product = $cart_item['data']; $products[] = [ 'name' => $product->get_name(), 'sku' => $product->get_sku(), 'quantity' => $cart_item['quantity'], 'price' => floatval($product->get_price()), 'total' => floatval($cart_item['line_total']) ]; } } $request_body = [ 'platform' => 'woocommerce', 'customer_name' => $customer_name, 'customer_email' => $email, 'customer_phone' => $phone, 'referral_code' => $referral_code, 'coupon_code' => $applied_coupon, 'visitor_id' => $visitor_id, 'claim_value' => $cart_total, 'coupons_applied' => $coupons_applied, 'claim_source' => 'woocommerce', 'order_id' => null, 'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : '', 'ip_address' => $this->get_client_ip(), 'claim_page' => wc_get_checkout_url(), 'products' => $products, 'status' => 'pending', 'early_claim' => true, 'metadata' => [ 'early_claim' => true, 'cart_item_count' => WC()->cart ? WC()->cart->get_cart_contents_count() : 0, 'claim_stage' => 'payment_step_reached', 'trigger' => $trigger ] ]; $response = \AdvocateLoop\Core\Signing::post('/api/v1/track/claim', $request_body); if (is_wp_error($response)) { $this->log('Early claim API error', self::LOG_ERROR, [ 'error' => $response->get_error_message() ]); wp_send_json_success([ 'message' => 'Claim recorded', 'claim_id' => null, 'api_error' => true ]); } $data = json_decode(wp_remote_retrieve_body($response), true); if (isset($data['success']) && $data['success']) { $claim_id = $data['data']['id'] ?? null; set_transient($early_claim_key, $claim_id, HOUR_IN_SECONDS); if (WC()->session) { WC()->session->set('al_early_claim_id', $claim_id); WC()->session->set('al_early_claim_email', $email); WC()->session->set('advocate_loop_referral_code', $referral_code); } $this->log('=== EARLY CLAIM CREATED ===', self::LOG_INFO, [ 'claim_id' => $claim_id, 'email' => $email, 'referral_code' => $referral_code, 'type' => $data['data']['type'] ?? $data['data']['claim_type'] ?? 'unknown', 'visitor_id' => $visitor_id ]); wp_send_json_success([ 'message' => 'Claim recorded', 'claim_id' => $claim_id ]); } else { $error = $data['error'] ?? 'Unknown error'; if (is_array($data) && in_array($error, self::CLAIM_REFUSAL_CODES, true)) { $this->refuse_referral_discount($data, $email, $referral_code, 'EARLY CLAIM'); } $this->log('Early claim failed', self::LOG_ERROR, [ 'error' => $error, 'email' => $email ]); wp_send_json_success([ 'message' => 'Claim recorded', 'claim_id' => null, 'error' => $error ]); } } public function maybe_apply_referral_discount($cart = null): void { if (is_admin() && !defined('DOING_AJAX')) { return; } if (!function_exists('WC') || !WC()->cart) { $this->track_timeline('Auto-apply skipped: Cart not available'); return; } static $already_applied = false; if ($already_applied) { $this->track_timeline('Auto-apply skipped: Already processed this request'); return; } $referral_code = $this->get_referral_code(); $visitor_id = $this->get_visitor_id(); $this->track_timeline('Auto-apply check started', [ 'referral_code' => $referral_code ?: 'NONE', 'visitor_id' => $visitor_id ?: 'NONE', 'cart_subtotal' => WC()->cart->get_subtotal(), 'cart_items' => WC()->cart->get_cart_contents_count() ]); $this->log('maybe_apply_referral_discount called', self::LOG_DEBUG, [ 'referral_code' => $referral_code ?: 'none', 'cart_subtotal' => WC()->cart->get_subtotal(), 'cart_total' => WC()->cart->get_total('edit') ]); if (!$referral_code) { $this->track_timeline('Auto-apply skipped: No referral code in cookie'); return; } if ($this->is_code_refused($referral_code)) { $this->track_timeline('Auto-apply skipped: Code refused earlier this visit', [ 'referral_code' => $referral_code ]); return; } $existing_referral_coupon = $this->get_applied_referral_coupon(); if ($existing_referral_coupon) { $this->track_timeline('Auto-apply skipped: Coupon already applied', [ 'existing_coupon' => $existing_referral_coupon ]); $this->log('Cart already has referral coupon', self::LOG_DEBUG, [ 'existing' => $existing_referral_coupon, 'attempted' => $referral_code ]); $already_applied = true; return; } $already_applied = true; $this->track_timeline('Auto-apply: Attempting to apply discount', [ 'referral_code' => $referral_code ]); $this->apply_referral_discount_to_cart($referral_code); } public function validate_claim_eligibility(array $data, \WP_Error $errors): void { $referral_code = $this->get_referral_code(); if (!$referral_code) { return; } $applied_coupon = $this->get_applied_referral_coupon(); if (!$applied_coupon || !WC()->cart) { return; } $api_key = get_option('advocate_loop_api_key'); $brand_id = get_option('advocate_loop_brand_id'); if (empty($api_key) || empty($brand_id)) { return; } $config = $this->get_cached_coupon_config($referral_code); if ($config && isset($config['minimum_amount']) && $config['minimum_amount'] > 0) { $cart_subtotal = WC()->cart->get_subtotal(); $minimum = floatval($config['minimum_amount']); if ($cart_subtotal < $minimum) { WC()->cart->remove_coupon($applied_coupon); $errors->add('advocate_loop_minimum', sprintf( /* translators: %s: formatted minimum purchase amount (e.g., "$25.00") */ __('The minimum purchase amount for this referral discount is %s.', 'advocateloop-for-woocommerce'), wc_price($minimum) )); $this->log('Minimum purchase not met at checkout', self::LOG_WARNING, [ 'minimum' => $minimum, 'cart_subtotal' => $cart_subtotal ]); return; } } $customer_email = isset($data['billing_email']) ? sanitize_email($data['billing_email']) : ''; if (empty($customer_email)) { return; } $cart_total = WC()->cart ? WC()->cart->get_subtotal() : 0; $response = wp_remote_post(ADVOCATE_LOOP_API_URL . '/api/v1/validate/discount-eligibility', [ 'headers' => [ 'Content-Type' => 'application/json', 'X-API-Key' => $api_key, 'X-Brand-ID' => $brand_id ], 'body' => wp_json_encode([ 'referral_code' => $referral_code, 'customer_email' => $customer_email, 'visitor_id' => $this->get_visitor_id(), 'cart_total' => $cart_total ]), 'timeout' => 10 ]); if (is_wp_error($response)) { $this->log('Eligibility check failed', self::LOG_ERROR, ['error' => $response->get_error_message()]); return; } $result = json_decode(wp_remote_retrieve_body($response), true); if (!empty($result['cookie_actions'])) { $this->execute_cookie_actions($result['cookie_actions']); } if (!isset($result['eligible']) || $result['eligible'] === true) { return; } WC()->cart->remove_coupon($applied_coupon); $reason = $result['reason'] ?? ''; $error_message = $result['message'] ?? __('This discount is no longer available.', 'advocateloop-for-woocommerce'); // A changed referral is a swap, not a refusal (the API set the new code). if ($reason !== 'referral_changed') { $this->forget_refused_referral($referral_code, $reason); } $soft_reasons = ['advocate_email_mismatch', 'no_redemptions_available']; if (in_array($reason, $soft_reasons, true)) { wc_add_notice($error_message, 'notice'); $this->log('Advocate discount removed (soft - checkout allowed)', self::LOG_INFO, ['reason' => $reason]); return; } $errors->add('advocate_loop_ineligible', $error_message); $this->log('Customer ineligible', self::LOG_WARNING, ['reason' => $error_message]); } private function apply_referral_discount_to_cart(string $code): void { if ($this->cart_has_referral_coupon()) { $this->track_timeline('Apply discount skipped: Referral coupon already in cart'); return; } $config = $this->get_cached_coupon_config($code); $this->log('Applying referral discount', self::LOG_DEBUG, [ 'code' => $code, 'config_cached' => $config ? 'yes (request-level)' : 'no' ]); if (!$config) { $this->track_timeline('Fetching coupon config from API', ['code' => $code]); $config = $this->fetch_coupon_config_from_api($code); if (!$config) { $this->track_timeline('No coupon config returned - not applying discount', ['code' => $code]); $this->log('No coupon config from API - not applying discount', self::LOG_DEBUG); return; } $this->track_timeline('API config received', [ 'coupon_code' => $config['coupon_code'] ?? 'NOT SET', 'click_type' => $config['click_type'] ?? 'NOT SET', 'discount' => ($config['amount'] ?? '?') . ' ' . ($config['discount_type'] ?? '?'), 'minimum' => $config['minimum_amount'] ?? 'NONE', 'maximum' => $config['maximum_amount'] ?? 'NONE' ]); $this->log('API coupon config response', self::LOG_DEBUG, [ 'coupon_code' => $config['coupon_code'] ?? 'not set', 'click_type' => $config['click_type'] ?? 'not set', 'minimum_amount' => $config['minimum_amount'] ?? 'not set', 'amount' => $config['amount'] ?? 'not set' ]); } $coupon_code = $config['coupon_code'] ?? strtoupper($code); if (isset($config['minimum_amount']) && $config['minimum_amount'] > 0) { $cart_subtotal = WC()->cart ? WC()->cart->get_subtotal() : 0; $minimum = floatval($config['minimum_amount']); if ($cart_subtotal < $minimum) { $this->track_timeline('Minimum purchase not met - coupon NOT applied', [ 'cart_subtotal' => '$' . number_format($cart_subtotal, 2), 'minimum_required' => '$' . number_format($minimum, 2), 'shortfall' => '$' . number_format($minimum - $cart_subtotal, 2) ]); $this->log('Minimum purchase not met - not showing discount yet', self::LOG_DEBUG, [ 'minimum' => $minimum, 'cart_subtotal' => $cart_subtotal ]); return; } } $this->store_coupon_config($coupon_code, $config); $this->track_timeline('Applying coupon to cart', [ 'coupon_code' => $coupon_code, 'discount' => ($config['amount'] ?? '?') . ($config['discount_type'] === 'percent' ? '%' : ' fixed') ]); $this->log('=== ATTEMPTING TO APPLY COUPON ===', self::LOG_INFO, [ 'coupon_code_to_apply' => $coupon_code, 'referral_code' => $config['referral_code'] ?? $code, 'click_type' => $config['click_type'] ?? 'unknown', 'amount' => $config['amount'] ?? 'N/A', 'discount_type' => $config['discount_type'] ?? 'N/A', 'cart_subtotal' => WC()->cart ? WC()->cart->get_subtotal() : 0, 'cart_item_count' => WC()->cart ? WC()->cart->get_cart_contents_count() : 0 ]); if (WC()->cart && !WC()->cart->has_discount($coupon_code)) { $applied = WC()->cart->apply_coupon($coupon_code); $this->track_timeline($applied ? '✓ COUPON APPLIED SUCCESSFULLY' : '✗ COUPON APPLICATION FAILED', [ 'coupon_code' => $coupon_code, 'result' => $applied ? 'SUCCESS' : 'FAILED', 'all_coupons_now' => WC()->cart->get_applied_coupons() ]); $this->log('=== COUPON APPLICATION RESULT ===', self::LOG_INFO, [ 'coupon_code' => $coupon_code, 'success' => $applied ? 'YES' : 'NO', 'all_applied_coupons' => WC()->cart->get_applied_coupons() ]); } else { $this->track_timeline('Coupon already applied or cart unavailable', [ 'coupon_code' => $coupon_code ]); $this->log('Coupon already applied or cart not available', self::LOG_DEBUG, [ 'coupon_code' => $coupon_code, 'already_has_discount' => WC()->cart ? (WC()->cart->has_discount($coupon_code) ? 'yes' : 'no') : 'cart_null' ]); } } private function store_coupon_config(string $coupon_code, array $config): void { $upper = strtoupper($coupon_code); $this->coupon_configs[$upper] = $config; if (function_exists('WC') && WC()->session) { WC()->session->set('al_coupon_config_' . $upper, $config); } } private function clear_coupon_config(string $coupon_code): void { $upper = strtoupper($coupon_code); unset($this->coupon_configs[$upper]); if (function_exists('WC') && WC()->session) { WC()->session->set('al_coupon_config_' . $upper, null); } } private function get_coupon_config(string $coupon_code): ?array { return $this->coupon_configs[strtoupper($coupon_code)] ?? null; } public function on_cart_updated(): void { $referral_code = $this->get_referral_code(); if (!$referral_code) { return; } if ($this->get_applied_referral_coupon()) { return; } $this->log('Cart updated - re-checking discount eligibility', self::LOG_DEBUG, [ 'referral_code' => $referral_code, 'cart_subtotal' => WC()->cart ? WC()->cart->get_subtotal() : 0 ]); $config = $this->fetch_coupon_config_from_api($referral_code); if ($config) { $coupon_code = $config['coupon_code'] ?? strtoupper($referral_code); $this->store_coupon_config($coupon_code, $config); if (WC()->cart && !WC()->cart->has_discount($coupon_code)) { WC()->cart->apply_coupon($coupon_code); $this->log('Coupon auto-applied after cart update', self::LOG_INFO, [ 'coupon_code' => $coupon_code ]); } } } public function filter_virtual_coupon($false, $data) { $coupon_code = strtoupper($data); $referral_code = $this->extract_referral_code($coupon_code); $config = $this->get_coupon_config($coupon_code); if (!$config && WC()->session) { $session_config = WC()->session->get('al_coupon_config_' . $coupon_code); if ($session_config) { $config = $session_config; $this->coupon_configs[strtoupper($coupon_code)] = $config; $this->log('Virtual coupon filter - restored from session', self::LOG_DEBUG, [ 'coupon_code' => $coupon_code ]); } } if ($config && WC()->cart) { $cart_subtotal = WC()->cart->get_subtotal(); $minimum = isset($config['minimum_amount']) ? floatval($config['minimum_amount']) : 0; if ($minimum > 0 && $cart_subtotal < $minimum) { $this->log('Virtual coupon filter - minimum no longer met, invalidating', self::LOG_DEBUG, [ 'coupon_code' => $coupon_code, 'cart_subtotal' => $cart_subtotal, 'minimum_required' => $minimum ]); $this->clear_coupon_config($coupon_code); return $false; } } if (!$config) { $config = $this->get_cached_coupon_config($referral_code); if (!$config) { $config = $this->fetch_coupon_config_from_api($referral_code); if ($config) { $this->store_coupon_config($config['coupon_code'], $config); } } $this->log('Virtual coupon filter - config lookup', self::LOG_DEBUG, [ 'coupon_code' => $coupon_code, 'referral_code' => $referral_code, 'config_found' => $config ? 'yes' : 'no', 'is_ajax' => defined('DOING_AJAX') && DOING_AJAX ? 'yes' : 'no' ]); } if (!$config) { return $false; } $cart_subtotal = WC()->cart ? WC()->cart->get_subtotal() : 0; $minimum_amount = ''; $maximum_amount = ''; $actual_minimum = isset($config['minimum_amount']) ? floatval($config['minimum_amount']) : 0; $discount_cap = isset($config['maximum_amount']) ? floatval($config['maximum_amount']) : 0; $this->log('=== VIRTUAL COUPON CREATED ===', self::LOG_INFO, [ 'requested_coupon_code' => $coupon_code, 'returned_coupon_code' => $config['coupon_code'], 'referral_code' => $config['referral_code'] ?? 'N/A', 'click_type' => $config['click_type'] ?? 'N/A', 'amount' => $config['amount'], 'discount_type' => $config['discount_type'], 'minimum_from_api' => $actual_minimum, 'max_discount_cap' => $discount_cap, 'cart_subtotal' => $cart_subtotal ]); return [ 'id' => 0, 'code' => $config['coupon_code'], 'amount' => floatval($config['amount']), 'discount_type' => $config['discount_type'], 'individual_use' => (bool)$config['individual_use'], 'usage_limit' => 1, 'usage_count' => 0, 'date_expires' => !empty($config['date_expires']) ? strtotime($config['date_expires']) : null, 'minimum_amount' => $minimum_amount, 'maximum_amount' => $maximum_amount, 'free_shipping' => (bool)($config['free_shipping'] ?? false), 'exclude_sale_items' => false, 'product_ids' => [], 'excluded_product_ids' => [], 'product_categories' => [], 'excluded_product_categories' => [], 'meta_data' => $config['meta_data'] ?? [] ]; } public function customize_coupon_label(string $label, \WC_Coupon $coupon): string { if ($this->is_referral_coupon($coupon->get_code())) { return __('Discount', 'advocateloop-for-woocommerce'); } return $label; } public function save_referral_to_order(int $order_id): void { $order = wc_get_order($order_id); if (!$order) { return; } $referral_code = null; $source = null; $referral_code = $this->get_referral_code(); if ($referral_code) { $source = 'cookie'; } if (!$referral_code && WC()->session) { $session_referral = WC()->session->get('advocate_loop_referral_code'); if ($session_referral) { $referral_code = $session_referral; $source = 'wc_session'; } } if (!$referral_code) { $coupon_codes = $order->get_coupon_codes(); foreach ($coupon_codes as $coupon_code) { if ($this->is_referral_coupon($coupon_code)) { $referral_code = $this->extract_referral_code($coupon_code); $source = 'order_coupon'; break; } } } if (!$referral_code && isset($_POST['advocate_loop_referral_code']) && !empty($_POST['advocate_loop_referral_code'])) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Sanitized request value read during checkout; used only as a lookup key. $referral_code = sanitize_text_field(wp_unslash($_POST['advocate_loop_referral_code'])); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Sanitized request value read during checkout; used only as a lookup key. $source = 'post_data'; } if ($referral_code) { $this->set_order_meta($order_id, '_advocate_loop_referral_code', $referral_code); $this->set_order_meta($order_id, '_advocate_loop_referral_timestamp', current_time('mysql')); $this->set_order_meta($order_id, '_advocate_loop_referral_source', $source); $this->log('Referral code saved to order', self::LOG_INFO, [ 'order_id' => $order_id, 'referral_code' => $referral_code, 'source' => $source ]); } else { $this->log('No referral code found for order', self::LOG_DEBUG, [ 'order_id' => $order_id, 'checked_sources' => ['cookie', 'wc_session', 'order_coupon', 'post_data'] ]); } } public function track_claim_on_checkout(\WC_Order $order): void { $order_id = $order->get_id(); $referral_code = $this->get_order_meta($order_id, '_advocate_loop_referral_code') ?: null; if (!$referral_code) { $referral_code = $this->get_referral_code(); } if (!$referral_code && WC()->session) { $referral_code = WC()->session->get('advocate_loop_referral_code') ?: null; } if (!$referral_code) { return; } if ($this->get_order_meta($order_id, '_advocate_loop_claim_tracked')) { return; } $api_key = get_option('advocate_loop_api_key'); $brand_id = get_option('advocate_loop_brand_id'); if (empty($api_key) || empty($brand_id)) { return; } $visitor_id = $this->get_visitor_id(); $customer_email = $order->get_billing_email(); $early_claim_key = 'advocate_loop_woo_early_claim_' . md5($visitor_id . '_' . $customer_email); $early_claim_id = get_transient($early_claim_key); if (!$early_claim_id && WC()->session) { $session_claim_id = WC()->session->get('al_early_claim_id'); $session_email = WC()->session->get('al_early_claim_email'); if ($session_claim_id && $session_email === $customer_email) { $early_claim_id = $session_claim_id; } } if ($early_claim_id) { $this->log('Early claim found - storing metadata for conversion on payment', self::LOG_INFO, [ 'order_id' => $order_id, 'early_claim_id' => $early_claim_id ]); $this->set_order_meta($order_id, '_advocate_loop_claim_tracked', true); $this->set_order_meta($order_id, '_advocate_loop_claim_id', $early_claim_id); $this->set_order_meta($order_id, '_advocate_loop_visitor_id', $visitor_id); $this->set_order_meta($order_id, '_advocate_loop_referral_code', $referral_code); $this->set_order_meta($order_id, '_advocate_loop_early_claim', true); delete_transient($early_claim_key); return; } $api_config = advocate_loop_get_brand_config(); $coupons = $api_config['coupons'] ?? array(); $advocate_coupon = $coupons['advocate_coupon_code'] ?? null; $referral_coupon = $coupons['referral_coupon_code'] ?? null; $subtotal = 0; foreach ($order->get_items() as $item) { $subtotal += floatval($item->get_total()); } $coupons_applied = $this->get_order_coupons($order); $order_total = floatval($order->get_total()); $total_discount = floatval($order->get_discount_total()); $coupon_codes = $order->get_coupon_codes(); $al_coupon = null; foreach ($coupon_codes as $code) { if ($this->is_referral_coupon($code)) { $al_coupon = $code; break; } } $this->log('=== SENDING CLAIM DATA ===', self::LOG_INFO, [ 'order_id' => $order_id, 'order_total' => $order_total, 'product_subtotal' => $subtotal, 'total_discount' => $total_discount, 'coupons_applied' => $coupons_applied, 'coupon_code' => $al_coupon, 'note' => 'API will calculate AL vs non-AL breakdown and resolve claim type' ]); $products = $this->get_order_products($order); $visitor_id = $this->get_visitor_id(); $request_body = [ 'platform' => 'woocommerce', 'customer_name' => $order->get_billing_first_name() . ' ' . $order->get_billing_last_name(), 'customer_email' => $order->get_billing_email(), 'customer_phone' => $order->get_billing_phone(), 'referral_code' => $referral_code, 'coupon_code' => $al_coupon, 'visitor_id' => $visitor_id, 'coupons_applied' => $coupons_applied, 'claim_value' => $order_total, 'claim_source' => 'woocommerce', 'order_id' => (string) $order_id, 'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : '', 'ip_address' => $this->get_client_ip(), 'claim_page' => wc_get_checkout_url(), 'products' => $products, 'status' => 'pending', 'metadata' => [ 'currency' => $order->get_currency(), 'order_status' => $order->get_status(), 'payment_method' => $order->get_payment_method(), 'shipping_total' => floatval($order->get_shipping_total()), 'tax_total' => floatval($order->get_total_tax()), 'items_count' => $order->get_item_count(), 'product_subtotal' => $subtotal ] ]; if (!$visitor_id) { $this->log('No visitor_id found for order - attribution may fail', self::LOG_WARNING, ['order_id' => $order_id]); } $this->log('========================================', self::LOG_INFO); $this->log('SENDING CLAIM REQUEST TO API', self::LOG_INFO); $this->log('========================================', self::LOG_INFO); $this->log('Endpoint: /api/v1/track/claim', self::LOG_INFO); $this->log('Request Body:', self::LOG_INFO, $request_body); $this->log('========================================', self::LOG_INFO); $response = \AdvocateLoop\Core\Signing::post('/api/v1/track/claim', $request_body); if (!is_wp_error($response)) { $data = json_decode(wp_remote_retrieve_body($response), true); $this->log('========================================', self::LOG_INFO); $this->log('RECEIVED CLAIM RESPONSE FROM API', self::LOG_INFO); $this->log('========================================', self::LOG_INFO); $this->log('HTTP Status: ' . wp_remote_retrieve_response_code($response), self::LOG_INFO); $this->log('Response Data:', self::LOG_INFO, $data); $this->log('========================================', self::LOG_INFO); if (isset($data['success']) && $data['success']) { $claim_id = $data['data']['id'] ?? ''; $claim_type = $data['data']['type'] ?? $data['data']['claim_type'] ?? 'referral'; $this->set_order_meta($order_id, '_advocate_loop_claim_tracked', true); $this->set_order_meta($order_id, '_advocate_loop_claim_id', $claim_id); $this->set_order_meta($order_id, '_advocate_loop_claim_type', $claim_type); $this->set_order_meta($order_id, '_advocate_loop_visitor_id', $visitor_id); $this->log('Claim tracked successfully', self::LOG_INFO, [ 'order_id' => $order_id, 'type' => $claim_type, 'claim_id' => $claim_id, 'visitor_id' => $visitor_id ?: 'none' ]); } else { $error = $data['error'] ?? 'unknown'; $this->log('Claim failed', self::LOG_ERROR, ['order_id' => $order_id, 'error' => $error]); $this->set_order_meta($order_id, '_advocate_loop_claim_error', $error); if (isset($data['claim_id'])) { $this->set_order_meta($order_id, '_advocate_loop_claim_id', $data['claim_id']); $this->set_order_meta($order_id, '_advocate_loop_claim_tracked', true); } } } else { $this->log('API error - queuing for retry', self::LOG_ERROR, ['order_id' => $order_id, 'error' => $response->get_error_message()]); advocate_loop_queue_request( '/api/v1/track/claim', 'POST', $request_body ); $this->set_order_meta($order_id, '_advocate_loop_claim_queued', true); $this->set_order_meta($order_id, '_advocate_loop_claim_error', 'Queued: ' . $response->get_error_message()); } } private function create_conversion_for_early_claim(string $claim_id, \WC_Order $order): bool { $api_key = get_option('advocate_loop_api_key'); $brand_id = get_option('advocate_loop_brand_id'); $order_id = $order->get_id(); $customer_email = $order->get_billing_email(); $customer_name = trim($order->get_billing_first_name() . ' ' . $order->get_billing_last_name()); $visitor_id = $this->get_order_meta($order_id, '_advocate_loop_visitor_id'); if (!$visitor_id) { $visitor_id = $this->get_visitor_id(); } $referral_code = $this->get_order_meta($order_id, '_advocate_loop_referral_code'); if (!$referral_code) { $referral_code = $this->get_referral_code(); } $order_total = floatval($order->get_total()); $discount_total = floatval($order->get_discount_total()); $coupon_codes = $order->get_coupon_codes(); $al_coupon = null; foreach ($coupon_codes as $code) { if ($this->is_referral_coupon($code)) { $al_coupon = $code; break; } } $products = $this->get_order_products($order); $coupons_applied = $this->get_order_coupons($order); $conversion_body = [ 'platform' => 'woocommerce', 'customer_email' => $customer_email, 'order_id' => (string) $order_id, 'order_total' => $order_total, 'tax' => floatval($order->get_total_tax()), 'shipping' => floatval($order->get_shipping_total()), 'discount' => floatval($order->get_total_discount()), 'visitor_id' => $visitor_id, 'claim_id' => $claim_id, 'customer_name' => $customer_name, 'referral_code' => $referral_code, 'coupons_applied' => $coupons_applied, 'products' => $products, 'source' => 'woocommerce', 'metadata' => [ 'currency' => $order->get_currency(), 'order_status' => $order->get_status(), 'payment_method' => $order->get_payment_method(), 'shipping_total' => floatval($order->get_shipping_total()), 'tax_total' => floatval($order->get_total_tax()), 'items_count' => $order->get_item_count(), 'coupon_code' => $al_coupon, 'linked_from_early_claim' => true, 'early_claim_id' => $claim_id ] ]; $this->log('Creating conversion for early claim', self::LOG_DEBUG, [ 'claim_id' => $claim_id, 'order_id' => $order_id, 'visitor_id' => $visitor_id ]); $response = \AdvocateLoop\Core\Signing::post('/api/v1/track/conversion', $conversion_body); if (is_wp_error($response)) { $this->log('Conversion creation API error', self::LOG_ERROR, [ 'claim_id' => $claim_id, 'order_id' => $order_id, 'error' => $response->get_error_message() ]); return false; } $status_code = wp_remote_retrieve_response_code($response); $data = json_decode(wp_remote_retrieve_body($response), true); if (isset($data['success']) && $data['success']) { $conversion_id = $data['data']['id'] ?? null; $this->log('=== CONVERSION CREATED FOR EARLY CLAIM ===', self::LOG_INFO, [ 'conversion_id' => $conversion_id, 'claim_id' => $claim_id, 'order_id' => $order_id, 'order_value' => $order_total, 'status' => $data['data']['status'] ?? 'unknown' ]); if ($conversion_id) { $this->set_order_meta($order_id, '_advocate_loop_conversion_id', $conversion_id); } return true; } $this->log('Conversion creation failed', self::LOG_ERROR, [ 'claim_id' => $claim_id, 'order_id' => $order_id, 'status_code' => $status_code, 'error' => $data['error'] ?? 'unknown', 'message' => $data['message'] ?? null ]); return false; } private function get_order_coupons(\WC_Order $order): array { $coupons = []; foreach ($order->get_items('coupon') as $coupon_item) { $coupons[] = [ 'code' => $coupon_item->get_code(), 'discount_amount' => abs(floatval($coupon_item->get_discount())), 'discount_tax' => abs(floatval($coupon_item->get_discount_tax())) ]; } return $coupons; } private function get_order_products(\WC_Order $order): array { $products = []; foreach ($order->get_items() as $item_id => $item) { $product = $item->get_product(); $product_data = [ 'id' => $product ? $product->get_id() : null, 'sku' => $product ? $product->get_sku() : null, 'name' => $item->get_name(), 'quantity' => $item->get_quantity(), 'price' => floatval($item->get_total()) / max(1, $item->get_quantity()), 'total' => floatval($item->get_total()), ]; if ($product && $product->is_type('variation')) { $product_data['variant_id'] = $product->get_id(); $product_data['parent_id'] = $product->get_parent_id(); $product_data['attributes'] = $product->get_attributes(); } if ($product) { $categories = wp_get_post_terms($product->get_id(), 'product_cat', ['fields' => 'names']); if (!is_wp_error($categories)) { $product_data['categories'] = $categories; } } if ($product) { $image_id = $product->get_image_id(); if ($image_id) { $product_data['image_url'] = wp_get_attachment_image_url($image_id, 'thumbnail'); } } $products[] = $product_data; } return $products; } public function track_conversion_on_payment(int $order_id): void { $order = wc_get_order($order_id); if (!$order) { return; } $order_status = $order->get_status(); if (!in_array($order_status, ['processing', 'completed'])) { return; } if (!$order->is_paid()) { return; } if ($this->get_order_meta($order_id, '_advocate_loop_conversion_tracked')) { return; } $api_key = get_option('advocate_loop_api_key'); $brand_id = get_option('advocate_loop_brand_id'); if (empty($api_key) || empty($brand_id)) { return; } $referral_code = $this->get_order_meta($order_id, '_advocate_loop_referral_code') ?: null; if (!$referral_code) { $referral_code = $this->get_referral_code(); } if (!$referral_code && WC()->session) { $referral_code = WC()->session->get('advocate_loop_referral_code') ?: null; } $claim_id = $this->get_order_meta($order_id, '_advocate_loop_claim_id') ?: null; $visitor_id = $this->get_order_meta($order_id, '_advocate_loop_visitor_id') ?: null; if (!$visitor_id) { $visitor_id = $this->get_visitor_id(); } $coupons_applied = $this->get_order_coupons($order); $has_referral_hint = $referral_code || $claim_id; $has_coupons = !empty($coupons_applied); if (!$has_referral_hint && !$has_coupons) { return; } $order_total = floatval($order->get_total()); $products = $this->get_order_products($order); $request_body = [ 'platform' => 'woocommerce', 'referral_code' => $referral_code, 'claim_id' => $claim_id ? (int) $claim_id : null, 'visitor_id' => $visitor_id, 'customer_email' => $order->get_billing_email(), 'customer_name' => trim($order->get_billing_first_name() . ' ' . $order->get_billing_last_name()), 'order_id' => (string) $order_id, 'order_total' => $order_total, 'tax' => floatval($order->get_total_tax()), 'shipping' => floatval($order->get_shipping_total()), 'discount' => floatval($order->get_total_discount()), 'coupons_applied' => $coupons_applied, 'products' => $products, 'source' => 'woocommerce', 'status' => 'completed', 'metadata' => [ 'currency' => $order->get_currency(), 'order_status' => $order_status, 'payment_method' => $order->get_payment_method(), 'payment_method_title' => $order->get_payment_method_title(), 'items_count' => $order->get_item_count(), 'shipping_total' => floatval($order->get_shipping_total()), 'tax_total' => floatval($order->get_total_tax()), 'billing_country' => $order->get_billing_country(), 'shipping_country' => $order->get_shipping_country(), 'early_claim_id' => $claim_id ] ]; $this->log('========================================', self::LOG_INFO); $this->log('SENDING CONVERSION REQUEST TO API', self::LOG_INFO); $this->log('========================================', self::LOG_INFO); $this->log('Endpoint: /api/v1/track/conversion', self::LOG_INFO); $this->log('Request Body:', self::LOG_INFO, $request_body); $this->log('========================================', self::LOG_INFO); $response = \AdvocateLoop\Core\Signing::post('/api/v1/track/conversion', $request_body); if (!is_wp_error($response)) { $data = json_decode(wp_remote_retrieve_body($response), true); $this->log('========================================', self::LOG_INFO); $this->log('RECEIVED CONVERSION RESPONSE FROM API', self::LOG_INFO); $this->log('========================================', self::LOG_INFO); $this->log('HTTP Status: ' . wp_remote_retrieve_response_code($response), self::LOG_INFO); $this->log('Response Data:', self::LOG_INFO, $data); $this->log('========================================', self::LOG_INFO); if (isset($data['success']) && $data['success']) { $this->set_order_meta($order_id, '_advocate_loop_conversion_tracked', true); $this->set_order_meta($order_id, '_advocate_loop_conversion_id', $data['data']['id'] ?? ''); if (isset($data['data']['discount_breakdown'])) { $breakdown = $data['data']['discount_breakdown']; $this->set_order_meta($order_id, '_advocate_loop_discount_applied', $breakdown['al_discount'] ?? 0); $this->set_order_meta($order_id, '_advocate_loop_total_discount', $breakdown['total_discount'] ?? 0); $this->set_order_meta($order_id, '_advocate_loop_order_subtotal', $breakdown['order_subtotal'] ?? 0); $this->log('Stored discount breakdown in order meta:', self::LOG_INFO, [ 'order_id' => $order_id, 'al_discount' => $breakdown['al_discount'] ?? 0, 'total_discount' => $breakdown['total_discount'] ?? 0, 'order_subtotal' => $breakdown['order_subtotal'] ?? 0 ]); } if (!empty($data['data']['attribution_source'])) { $this->set_order_meta($order_id, '_advocate_loop_attribution_source', $data['data']['attribution_source']); } if (!empty($data['data']['advocate_code'])) { $this->set_order_meta($order_id, '_advocate_loop_referral_code', $data['data']['advocate_code']); } if (!empty($data['data']['type'])) { $this->set_order_meta($order_id, '_advocate_loop_claim_type', $data['data']['type']); } if (!empty($data['data']['visitor_id'])) { $this->set_order_meta($order_id, '_advocate_loop_original_visitor_id', $data['data']['visitor_id']); } if (!empty($data['data']['visitor_id_restored'])) { $this->set_order_meta($order_id, '_advocate_loop_visitor_id_restored', true); } if (!empty($data['cookie_actions'])) { $this->execute_cookie_actions($data['cookie_actions']); } $this->log('Conversion tracked successfully', self::LOG_INFO, [ 'order_id' => $order_id, 'attribution_source' => $data['data']['attribution_source'] ?? 'unknown', 'visitor_id_restored' => !empty($data['data']['visitor_id_restored']), 'discount_breakdown' => $data['data']['discount_breakdown'] ?? 'not provided' ]); } else { $error = $data['error'] ?? 'unknown'; $this->log('Conversion API error', self::LOG_ERROR, ['order_id' => $order_id, 'error' => $error]); $this->set_order_meta($order_id, '_advocate_loop_conversion_error', $error); } } else { $this->log('Conversion API error - queuing for retry', self::LOG_ERROR, ['order_id' => $order_id, 'error' => $response->get_error_message()]); advocate_loop_queue_request( '/api/v1/track/conversion', 'POST', $request_body ); $this->set_order_meta($order_id, '_advocate_loop_conversion_queued', true); $this->set_order_meta($order_id, '_advocate_loop_conversion_error', 'Queued: ' . $response->get_error_message()); } } public function track_refund(int $order_id, int $refund_id): void { $order = wc_get_order($order_id); $refund = wc_get_order($refund_id); if (!$order || !$refund) { return; } $this->log('track_refund fired', self::LOG_INFO, [ 'order_id' => $order_id, 'refund_id' => $refund_id, ]); $al_referral_code = $this->get_order_meta($order_id, '_advocate_loop_referral_code'); $al_conversion_id = $this->get_order_meta($order_id, '_advocate_loop_conversion_id'); $al_claim_id = $this->get_order_meta($order_id, '_advocate_loop_claim_id'); if (!$al_referral_code && !$al_conversion_id && !$al_claim_id) { $this->log('track_refund skipped: order has no AL tracking markers', self::LOG_WARNING, [ 'order_id' => $order_id, ]); return; } $processed = $this->get_order_meta($order_id, '_advocate_loop_processed_refunds') ?: []; if (in_array((string) $refund_id, array_map('strval', $processed), true)) { return; } $refund_amount = abs(floatval($refund->get_total())); if ($refund_amount <= 0) { return; } $response = \AdvocateLoop\Core\Signing::post('/api/v1/track/refund', [ 'order_id' => (string) $order_id, 'conversion_id' => $al_conversion_id ?: null, 'claim_id' => $al_claim_id ?: null, 'refund_id' => (string) $refund_id, 'refund_amount' => $refund_amount, 'refund_reason' => $refund->get_reason() ?: '', 'platform' => 'woocommerce', ]); $this->handle_refund_response($order_id, (string) $refund_id, $response); } public function track_order_status_refunded(int $order_id): void { $order = wc_get_order($order_id); if (!$order) { return; } $this->log('track_order_status_refunded fired', self::LOG_INFO, [ 'order_id' => $order_id, ]); $al_referral_code = $this->get_order_meta($order_id, '_advocate_loop_referral_code'); $al_conversion_id = $this->get_order_meta($order_id, '_advocate_loop_conversion_id'); $al_claim_id = $this->get_order_meta($order_id, '_advocate_loop_claim_id'); if (!$al_referral_code && !$al_conversion_id && !$al_claim_id) { $this->log('track_order_status_refunded skipped: order has no AL tracking markers', self::LOG_WARNING, [ 'order_id' => $order_id, ]); return; } if ($this->get_order_meta($order_id, '_advocate_loop_claim_status') === 'refunded') { return; } $synthetic_id = 'order-' . $order_id . '-status-refunded'; $processed = $this->get_order_meta($order_id, '_advocate_loop_processed_refunds') ?: []; if (in_array($synthetic_id, array_map('strval', $processed), true)) { return; } $response = \AdvocateLoop\Core\Signing::post('/api/v1/track/refund', [ 'order_id' => (string) $order_id, 'conversion_id' => $al_conversion_id ?: null, 'claim_id' => $al_claim_id ?: null, 'refund_id' => $synthetic_id, 'refund_reason' => 'Order marked refunded', 'platform' => 'woocommerce', ]); $this->handle_refund_response($order_id, $synthetic_id, $response); } private function handle_refund_response(int $order_id, string $refund_ref, $response): void { if (is_wp_error($response)) { $this->log('Refund API error', self::LOG_ERROR, [ 'order_id' => $order_id, 'refund_id' => $refund_ref, 'error' => $response->get_error_message(), ]); return; } $http_code = wp_remote_retrieve_response_code($response); $body = json_decode(wp_remote_retrieve_body($response), true); if (!empty($body['success'])) { $refund_status = $body['data']['status'] ?? null; $this->mark_refund_processed($order_id, $refund_ref); $this->set_order_meta($order_id, '_advocate_loop_refund_tracked', true); if ($refund_status) { $this->set_order_meta($order_id, '_advocate_loop_claim_status', $refund_status); } $this->log('Refund tracked', self::LOG_INFO, [ 'order_id' => $order_id, 'refund_id' => $refund_ref, 'status' => $refund_status, 'total_refunded' => $body['data']['total_refunded'] ?? null, 'reward_reversed'=> $body['data']['reward_reversed'] ?? null, ]); return; } if (($body['error'] ?? '') === 'already_refunded') { $this->mark_refund_processed($order_id, $refund_ref); $this->set_order_meta($order_id, '_advocate_loop_claim_status', 'refunded'); $this->log('Refund already fully recorded', self::LOG_INFO, [ 'order_id' => $order_id, 'refund_id' => $refund_ref, ]); return; } if (($body['error'] ?? '') === 'conversion_not_found') { $this->log('Refund could not find conversion in AdvocateLoop', self::LOG_WARNING, [ 'order_id' => $order_id, 'refund_id' => $refund_ref, 'http_code' => $http_code, ]); return; } $this->log('Refund not tracked', self::LOG_ERROR, [ 'order_id' => $order_id, 'refund_id' => $refund_ref, 'http_code' => $http_code, 'error' => $body['error'] ?? 'unknown', ]); } private function mark_refund_processed(int $order_id, string $refund_ref): void { $processed = $this->get_order_meta($order_id, '_advocate_loop_processed_refunds') ?: []; if (!in_array($refund_ref, array_map('strval', $processed), true)) { $processed[] = $refund_ref; $this->set_order_meta($order_id, '_advocate_loop_processed_refunds', $processed); } } public function track_order_cancelled(int $order_id): void { $this->update_claim_status($order_id, 'cancelled', 'Order cancelled'); } public function track_order_failed(int $order_id): void { $this->update_claim_status($order_id, 'failed', 'Payment failed'); } private function update_claim_status(int $order_id, string $status, string $reason = ''): void { $order = wc_get_order($order_id); if (!$order) { return; } $referral_code = $this->get_order_meta($order_id, '_advocate_loop_referral_code'); if (!$referral_code) { return; } $current_status = $this->get_order_meta($order_id, '_advocate_loop_claim_status'); if ($current_status === $status) { return; } $api_key = get_option('advocate_loop_api_key'); $brand_id = get_option('advocate_loop_brand_id'); if (empty($api_key) || empty($brand_id)) { return; } $claim_id = $this->get_order_meta($order_id, '_advocate_loop_claim_id'); $conversion_id = $this->get_order_meta($order_id, '_advocate_loop_conversion_id'); $response = wp_remote_post(ADVOCATE_LOOP_API_URL . '/api/v1/track/claim-status', [ 'headers' => [ 'Content-Type' => 'application/json', 'X-API-Key' => $api_key, 'X-Brand-ID' => $brand_id ], 'body' => wp_json_encode([ 'order_id' => (string) $order_id, 'referral_code' => $referral_code, 'claim_id' => $claim_id, 'conversion_id' => $conversion_id, 'status' => $status, 'reason' => $reason, 'source' => 'woocommerce', 'metadata' => [ 'order_status' => $order->get_status(), 'updated_at' => current_time('c'), 'order_total' => $order->get_total(), 'total_refunded' => $order->get_total_refunded() ] ]), 'timeout' => 15 ]); if (!is_wp_error($response)) { $data = json_decode(wp_remote_retrieve_body($response), true); if (isset($data['success']) && $data['success']) { $this->set_order_meta($order_id, '_advocate_loop_claim_status', $status); $this->log('Claim status updated', self::LOG_INFO, ['order_id' => $order_id, 'status' => $status]); } } } public function display_referral_in_admin(\WC_Order $order): void { $referral_code = $this->get_order_meta($order->get_id(), '_advocate_loop_referral_code'); if ($referral_code) { ?>

AdvocateLoop Details

Referral Code:

get_order_meta($order_id, '_advocate_loop_referral_code'); $claim_tracked = $this->get_order_meta($order_id, '_advocate_loop_claim_tracked'); $conversion_tracked = $this->get_order_meta($order_id, '_advocate_loop_conversion_tracked'); if (!$referral_code && !$claim_tracked && !$conversion_tracked) { return; } $original_visitor_id = $this->get_order_meta($order_id, '_advocate_loop_original_visitor_id'); $visitor_id_restored = $this->get_order_meta($order_id, '_advocate_loop_visitor_id_restored'); $attribution_source = $this->get_order_meta($order_id, '_advocate_loop_attribution_source'); $js = "(function() {\n"; $js .= " console.log('[AdvocateLoop] Thank-you page - conversion confirmed');\n"; $js .= " console.log('[AdvocateLoop] Attribution source: ' + " . wp_json_encode($attribution_source ?: 'unknown') . ");\n"; if ($visitor_id_restored && $original_visitor_id) { $js .= " console.log('[AdvocateLoop] Cross-device conversion detected');\n"; $js .= " localStorage.setItem('__alvid', " . wp_json_encode($original_visitor_id) . ");\n"; $js .= " console.log('[AdvocateLoop] visitor_id restored for unified tracking');\n"; } $js .= " localStorage.removeItem('__alsrc');\n"; $js .= " localStorage.removeItem('__alsrc_exp');\n"; $js .= " console.log('[AdvocateLoop] localStorage cleanup complete');\n"; $js .= "})();"; wp_register_script('advocate-loop-thankyou', '', [], ADVOCATE_LOOP_WOO_VERSION, true); wp_enqueue_script('advocate-loop-thankyou'); wp_add_inline_script('advocate-loop-thankyou', $js); } public function register_woo_endpoints(): void { register_rest_route('advocate-loop/v1', '/woo/order-completed', [ 'methods' => 'POST', 'callback' => [$this, 'handle_order_webhook'], 'permission_callback' => [$this, 'verify_webhook_permission'] ]); } public function verify_webhook_permission(\WP_REST_Request $request): bool { $api_key = $request->get_header('X-API-Key'); $stored_key = get_option('advocate_loop_api_key'); if (empty($api_key) || !hash_equals($stored_key, $api_key)) { return new \WP_Error('unauthorized', 'Invalid API key', ['status' => 401]); } return true; } public function handle_order_webhook(\WP_REST_Request $request): \WP_REST_Response { $params = $request->get_json_params(); $order_id = $params['order_id'] ?? null; if (!$order_id) { return new \WP_REST_Response(['success' => false, 'error' => 'order_id required'], 400); } $order = wc_get_order($order_id); if (!$order) { return new \WP_REST_Response(['success' => false, 'error' => 'Order not found'], 404); } $this->track_conversion_on_payment($order_id); return new \WP_REST_Response(['success' => true, 'message' => 'Conversion tracked']); } private function get_client_ip(): string { $ip_keys = ['HTTP_CF_CONNECTING_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP', 'REMOTE_ADDR']; foreach ($ip_keys as $key) { if (!empty($_SERVER[$key])) { $ip = explode(',', sanitize_text_field(wp_unslash($_SERVER[$key])))[0]; $ip = trim($ip); if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { return $ip; } } } return isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : 'UNKNOWN'; } }