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';
}
}