$value) {
$key = sanitize_key($key);
if (is_array($value)) {
$sanitized[$key] = activetrail_wc_sanitize_array($value);
} elseif (is_string($value)) {
// Use sanitize_text_field for strings, but preserve email format if it's an email field.
if (filter_var($value, FILTER_VALIDATE_EMAIL)) {
$sanitized[$key] = sanitize_email($value);
} elseif (filter_var($value, FILTER_VALIDATE_URL)) {
$sanitized[$key] = esc_url_raw($value);
} else {
$sanitized[$key] = sanitize_text_field($value);
}
} elseif (is_numeric($value)) {
$sanitized[$key] = is_float($value) ? floatval($value) : intval($value);
} elseif (is_bool($value)) {
$sanitized[$key] = (bool) $value;
} else {
$sanitized[$key] = sanitize_text_field(strval($value));
}
}
return $sanitized;
}
return $data;
}
/**
* Generate external ID for ActiveTrail
*
* @param string $type Entity type (contact, order, cart)
* @param string|int $id Entity ID
* @return string
*/
function activetrail_generate_external_id($type, $id) {
$type = strtoupper((string) $type);
$id = (string) $id;
return "WC-{$type}-{$id}";
}
/**
* Generate external ID for contact
*
* @param int $user_id User ID (0 for guest)
* @param string $email Email address (for guest)
* @return string
*/
function activetrail_external_id_for_contact($user_id = 0, $email = '') {
if ($user_id) return activetrail_generate_external_id('CONTACT', $user_id);
if ($email) return activetrail_generate_external_id('CONTACT', 'GUEST-' . md5(strtolower(trim($email))));
return activetrail_generate_external_id('CONTACT', 'GUEST-UNKNOWN');
}
/**
* Generate external ID for order
*
* @param int $order_id Order ID
* @return string
*/
function activetrail_external_id_for_order($order_id) {
return activetrail_generate_external_id('ORDER', $order_id);
}
/**
* Generate external ID for cart
*
* @param string|int $key_or_user Cart key or user ID
* @return string
*/
function activetrail_external_id_for_cart($key_or_user) {
return activetrail_generate_external_id('CART', $key_or_user);
}
// Define external name constant.
if (!defined('ACTIVETRAIL_EXTERNAL_NAME')) {
define('ACTIVETRAIL_EXTERNAL_NAME', 'WooCommerce');
}
/**
* Check if WooCommerce is active
*/
function activetrail_wc_check_woocommerce() {
if (!class_exists('WooCommerce')) {
add_action('admin_notices', 'activetrail_wc_missing_woocommerce_notice');
return false;
}
return true;
}
/**
* Display notice if WooCommerce is not active
*/
function activetrail_wc_missing_woocommerce_notice() {
?>
'percent',
'fixed_price' => 'fixed_cart',
'free_shipping' => 'fixed_cart',
'percent' => 'percent',
'fixed_cart' => 'fixed_cart',
'fixed_product' => 'fixed_product',
);
return $type_map[$type] ?? $type;
}
/**
* Normalize coupon payloads coming from ActiveTrail (camelCase โ snake_case)
*
* @param array $payload
* @return array
*/
function activetrail_wc_normalize_coupon_payload($payload) {
if (!is_array($payload) || empty($payload)) {
return $payload;
}
$field_map = array(
'couponCode' => 'coupon_code',
'discountType' => 'discount_type',
'amount' => 'amount',
'description' => 'description',
'expiryDate' => 'date_expires',
'minCartValue' => 'minimum_amount',
'maxCartValue' => 'maximum_amount',
'individualUse' => 'individual_use',
'usageLimit' => 'usage_limit',
'usageLimitPerUser' => 'usage_limit_per_user',
'freeShipping' => 'free_shipping',
'excludeSaleItems' => 'exclude_sale_items',
'productIds' => 'product_ids',
'excludedProductIds' => 'excluded_product_ids',
'productCategories' => 'product_categories',
'excludedProductCategories' => 'excluded_product_categories',
'emailRestrictions' => 'email_restrictions',
'externalId' => 'external_id',
'accountId' => 'account_id',
);
$normalized = $payload;
$converted = false;
foreach ($field_map as $camel => $snake) {
if (array_key_exists($camel, $payload) && (!array_key_exists($snake, $normalized) || $normalized[$snake] === '')) {
$normalized[$snake] = $payload[$camel];
$converted = true;
}
}
if ($converted && isset($normalized['discount_type'])) {
$normalized['discount_type'] = activetrail_wc_map_discount_type($normalized['discount_type']);
}
$incoming_discount_type = $payload['discountType'] ?? '';
if ($incoming_discount_type === 'free_shipping') {
$normalized['free_shipping'] = true;
$normalized['discount_type'] = 'fixed_cart';
if (!isset($normalized['amount']) || $normalized['amount'] === '') {
$normalized['amount'] = '0';
}
}
return $normalized;
}
/**
* Handle ActiveTrail coupon creation requests
*/
function activetrail_wc_handle_coupon_request() {
// Check if this is a coupon creation request.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External webhook endpoint, authentication handled separately
if (!isset($_GET['activetrail_coupon']) || sanitize_text_field(wp_unslash($_GET['activetrail_coupon'])) !== 'create') {
return;
}
// Ensure WooCommerce is fully loaded.
if (!function_exists('wc_get_product')) {
if (class_exists('WooCommerce')) {
WC();
} else {
wp_die('WooCommerce is not available', 'ActiveTrail Coupon Error', array('response' => 500));
}
}
// Initialize logger if available.
$logger = function_exists('activetrail_logger') ? activetrail_logger() : null;
if ($logger) {
$logger->product_feed('๐ ActiveTrail coupon creation request received', ActiveTrail_Logger::LEVEL_INFO, array(
'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : 'unknown',
'ip' => isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : 'unknown',
'request_uri' => isset($_SERVER['REQUEST_URI']) ? esc_url_raw(wp_unslash($_SERVER['REQUEST_URI'])) : 'unknown'
));
}
// Check if request method is POST.
$request_method = isset($_SERVER['REQUEST_METHOD']) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_METHOD'])) : '';
if ($request_method !== 'POST') {
if ($logger) {
$logger->product_feed('โ Invalid request method for coupon creation', ActiveTrail_Logger::LEVEL_ERROR, array(
'method' => $request_method
));
}
wp_die('Method not allowed', 'ActiveTrail Coupon Error', array('response' => 405));
}
// Get the request body.
$input = file_get_contents('php://input');
$data = json_decode($input, true);
if (!$data) {
if ($logger) {
$logger->product_feed('โ Invalid JSON data received', ActiveTrail_Logger::LEVEL_ERROR);
}
wp_die('Invalid JSON data', 'ActiveTrail Coupon Error', array('response' => 400));
}
// Sanitize decoded data.
$data = activetrail_wc_sanitize_array($data);
$data = activetrail_wc_normalize_coupon_payload($data);
// Validate required fields.
$requires_amount = empty($data['free_shipping']);
$required_fields = array('coupon_code', 'discount_type');
foreach ($required_fields as $field) {
if (empty($data[$field])) {
if ($logger) {
$logger->product_feed('โ Missing required field: ' . $field, ActiveTrail_Logger::LEVEL_ERROR);
}
wp_die(esc_html('Missing required field: ' . $field), 'ActiveTrail Coupon Error', array('response' => 400));
}
}
if ($requires_amount && empty($data['amount'])) {
if ($logger) {
$logger->product_feed('โ Missing required field: amount', ActiveTrail_Logger::LEVEL_ERROR);
}
wp_die('Missing required field: amount', 'ActiveTrail Coupon Error', array('response' => 400));
} elseif (!$requires_amount && !isset($data['amount'])) {
$data['amount'] = '0';
}
// Validate discount type.
$valid_types = array('percent', 'fixed_cart', 'fixed_product');
if (!in_array($data['discount_type'], $valid_types)) {
if ($logger) {
$logger->product_feed('โ Invalid discount type: ' . $data['discount_type'], ActiveTrail_Logger::LEVEL_ERROR);
}
wp_die(esc_html('Invalid discount_type. Must be one of: ' . implode(', ', $valid_types)), 'ActiveTrail Coupon Error', array('response' => 400));
}
if ($requires_amount) {
$amount = floatval($data['amount']);
if ($amount <= 0) {
if ($logger) {
$logger->product_feed('โ Invalid amount: ' . $data['amount'], ActiveTrail_Logger::LEVEL_ERROR);
}
wp_die('Amount must be greater than 0', 'ActiveTrail Coupon Error', array('response' => 400));
}
if ($data['discount_type'] === 'percent' && $amount > 100) {
if ($logger) {
$logger->product_feed('โ Percent amount cannot exceed 100: ' . $data['amount'], ActiveTrail_Logger::LEVEL_ERROR);
}
wp_die('Percent amount cannot exceed 100', 'ActiveTrail Coupon Error', array('response' => 400));
}
} else {
$data['amount'] = '0';
}
// Check for existing coupon by external_id (idempotency).
if (!empty($data['external_id'])) {
$existing_coupon_id = activetrail_wc_find_coupon_by_external_id($data['external_id']);
if ($existing_coupon_id) {
// Update existing coupon.
$result = activetrail_wc_update_coupon($existing_coupon_id, $data);
if ($result['success']) {
if ($logger) {
$logger->product_feed('โ
Coupon updated successfully: ' . $data['coupon_code'] . ' (ID: ' . $existing_coupon_id . ')', ActiveTrail_Logger::LEVEL_INFO);
}
wp_send_json($result, 200);
} else {
if ($logger) {
$logger->product_feed('โ Failed to update coupon: ' . $result['message'], ActiveTrail_Logger::LEVEL_ERROR);
}
wp_send_json($result, 500);
}
}
}
// Check if coupon code already exists (without external_id).
if (empty($data['external_id'])) {
$existing_coupon_id = wc_get_coupon_id_by_code($data['coupon_code']);
if ($existing_coupon_id) {
if ($logger) {
$logger->product_feed('โ Coupon code already exists without external_id: ' . $data['coupon_code'], ActiveTrail_Logger::LEVEL_ERROR);
}
wp_send_json(array(
'success' => false,
'message' => 'Coupon code already exists. Use external_id for idempotency.'
), 409);
}
}
// Create new coupon.
$result = activetrail_wc_create_coupon($data);
if ($result['success']) {
if ($logger) {
$logger->product_feed('โ
Coupon created successfully: ' . $result['coupon_code'] . ' (ID: ' . $result['coupon_id'] . ')', ActiveTrail_Logger::LEVEL_INFO);
}
wp_send_json($result, 201);
} else {
if ($logger) {
$logger->product_feed('โ Failed to create coupon: ' . $result['message'], ActiveTrail_Logger::LEVEL_ERROR);
}
wp_send_json($result, 500);
}
}
/**
* Create WooCommerce coupon
*/
function activetrail_wc_create_coupon($coupon_data) {
try {
// Generate unique code if not provided.
if (empty($coupon_data['coupon_code'])) {
$prefix = $coupon_data['prefix'] ?? 'AT';
$coupon_data['coupon_code'] = activetrail_wc_generate_unique_code($prefix);
}
// Create new coupon.
$coupon = new WC_Coupon();
// Set basic properties.
$coupon->set_code($coupon_data['coupon_code']);
$coupon->set_discount_type($coupon_data['discount_type']);
$coupon->set_amount(floatval($coupon_data['amount']));
// Set description.
if (!empty($coupon_data['description'])) {
$coupon->set_description($coupon_data['description']);
}
// Set expiry date.
if (!empty($coupon_data['date_expires'])) {
$coupon->set_date_expires($coupon_data['date_expires']);
}
// Set usage limits.
if (isset($coupon_data['usage_limit']) && $coupon_data['usage_limit'] > 0) {
$coupon->set_usage_limit($coupon_data['usage_limit']);
}
if (isset($coupon_data['usage_limit_per_user']) && $coupon_data['usage_limit_per_user'] > 0) {
$coupon->set_usage_limit_per_user($coupon_data['usage_limit_per_user']);
}
// Set amount limits.
if (!empty($coupon_data['minimum_amount'])) {
$coupon->set_minimum_amount(floatval($coupon_data['minimum_amount']));
}
if (!empty($coupon_data['maximum_amount'])) {
$coupon->set_maximum_amount(floatval($coupon_data['maximum_amount']));
}
// Set boolean properties.
if (isset($coupon_data['individual_use'])) {
$coupon->set_individual_use($coupon_data['individual_use']);
}
if (isset($coupon_data['free_shipping'])) {
$coupon->set_free_shipping($coupon_data['free_shipping']);
}
// Set product restrictions.
if (!empty($coupon_data['product_ids'])) {
$coupon->set_product_ids($coupon_data['product_ids']);
}
if (!empty($coupon_data['excluded_product_ids'])) {
$coupon->set_excluded_product_ids($coupon_data['excluded_product_ids']);
}
if (!empty($coupon_data['product_categories'])) {
$coupon->set_product_categories($coupon_data['product_categories']);
}
if (!empty($coupon_data['excluded_product_categories'])) {
$coupon->set_excluded_product_categories($coupon_data['excluded_product_categories']);
}
// Set email restrictions.
if (!empty($coupon_data['email_restrictions'])) {
$coupon->set_email_restrictions($coupon_data['email_restrictions']);
}
// Save coupon.
$coupon_id = $coupon->save();
if (!$coupon_id) {
return array(
'success' => false,
'message' => 'Failed to save coupon'
);
}
// Mark as ActiveTrail coupon.
update_post_meta($coupon_id, '_activetrail_coupon', 'yes');
update_post_meta($coupon_id, '_activetrail_created_date', current_time('mysql'));
// Store external_id if provided.
if (!empty($coupon_data['external_id'])) {
update_post_meta($coupon_id, '_activetrail_external_id', $coupon_data['external_id']);
}
// Store account_id if provided.
if (!empty($coupon_data['account_id'])) {
update_post_meta($coupon_id, '_activetrail_account_id', $coupon_data['account_id']);
}
return array(
'success' => true,
'message' => 'Coupon created successfully',
'coupon_id' => $coupon_id,
'coupon_code' => $coupon_data['coupon_code'],
'external_id' => $coupon_data['external_id'] ?? null
);
} catch (Exception $e) {
return array(
'success' => false,
'message' => 'Failed to create coupon: ' . $e->getMessage()
);
}
}
/**
* Update existing WooCommerce coupon
*/
function activetrail_wc_update_coupon($coupon_id, $coupon_data) {
try {
$coupon = new WC_Coupon($coupon_id);
// Update basic properties.
$coupon->set_discount_type($coupon_data['discount_type']);
$coupon->set_amount(floatval($coupon_data['amount']));
// Update description.
if (!empty($coupon_data['description'])) {
$coupon->set_description($coupon_data['description']);
}
// Update expiry date.
if (!empty($coupon_data['date_expires'])) {
$coupon->set_date_expires($coupon_data['date_expires']);
}
// Update usage limits.
if (isset($coupon_data['usage_limit'])) {
$coupon->set_usage_limit($coupon_data['usage_limit']);
}
if (isset($coupon_data['usage_limit_per_user'])) {
$coupon->set_usage_limit_per_user($coupon_data['usage_limit_per_user']);
}
// Update amount limits.
if (isset($coupon_data['minimum_amount'])) {
$coupon->set_minimum_amount(floatval($coupon_data['minimum_amount']));
}
if (isset($coupon_data['maximum_amount'])) {
$coupon->set_maximum_amount(floatval($coupon_data['maximum_amount']));
}
// Update boolean properties.
if (isset($coupon_data['individual_use'])) {
$coupon->set_individual_use($coupon_data['individual_use']);
}
if (isset($coupon_data['free_shipping'])) {
$coupon->set_free_shipping($coupon_data['free_shipping']);
}
// Update product restrictions.
if (isset($coupon_data['product_ids'])) {
$coupon->set_product_ids($coupon_data['product_ids']);
}
if (isset($coupon_data['excluded_product_ids'])) {
$coupon->set_excluded_product_ids($coupon_data['excluded_product_ids']);
}
if (isset($coupon_data['product_categories'])) {
$coupon->set_product_categories($coupon_data['product_categories']);
}
if (isset($coupon_data['excluded_product_categories'])) {
$coupon->set_excluded_product_categories($coupon_data['excluded_product_categories']);
}
// Update email restrictions.
if (isset($coupon_data['email_restrictions'])) {
$coupon->set_email_restrictions($coupon_data['email_restrictions']);
}
// Save coupon.
$coupon->save();
// Update metadata.
update_post_meta($coupon_id, '_activetrail_updated_date', current_time('mysql'));
return array(
'success' => true,
'message' => 'Coupon updated successfully',
'coupon_id' => $coupon_id,
'coupon_code' => $coupon_data['coupon_code'],
'external_id' => $coupon_data['external_id'] ?? null
);
} catch (Exception $e) {
return array(
'success' => false,
'message' => 'Failed to update coupon: ' . $e->getMessage()
);
}
}
/**
* Find coupon by external_id
*/
function activetrail_wc_find_coupon_by_external_id($external_id) {
global $wpdb;
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Direct query needed for coupon lookup
$coupon_id = $wpdb->get_var($wpdb->prepare(
"SELECT post_id FROM {$wpdb->postmeta}
WHERE meta_key = '_activetrail_external_id'
AND meta_value = %s",
$external_id
));
return $coupon_id ? intval($coupon_id) : false;
}
// Hook the coupon creation handler.
add_action('init', 'activetrail_wc_handle_coupon_request');
/**
* Handle ActiveTrail coupon creation via GET request (alternative method)
*/
function activetrail_wc_handle_coupon_request_get() {
// Check if this is a GET coupon creation request.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External webhook endpoint, authentication handled separately
if (!isset($_GET['activetrail_coupon']) || sanitize_text_field(wp_unslash($_GET['activetrail_coupon'])) !== 'createCoupon') {
return;
}
// Ensure WooCommerce is fully loaded.
if (!function_exists('wc_get_product')) {
if (class_exists('WooCommerce')) {
WC();
} else {
wp_die('WooCommerce is not available', 'ActiveTrail Coupon Error', array('response' => 500));
}
}
// Initialize logger if available.
$logger = function_exists('activetrail_logger') ? activetrail_logger() : null;
if ($logger) {
$logger->product_feed('๐ ActiveTrail GET coupon creation request received', ActiveTrail_Logger::LEVEL_INFO, array(
'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : 'unknown',
'ip' => isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : 'unknown',
'request_uri' => isset($_SERVER['REQUEST_URI']) ? esc_url_raw(wp_unslash($_SERVER['REQUEST_URI'])) : 'unknown'
));
}
// Check if args parameter exists.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External feed endpoint, authentication handled separately
if (!isset($_GET['args'])) {
if ($logger) {
$logger->product_feed('โ Missing args parameter', ActiveTrail_Logger::LEVEL_ERROR);
}
wp_die('Missing args parameter', 'ActiveTrail Coupon Error', array('response' => 400));
}
// Decode the args parameter.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- External feed endpoint, args are base64 encoded and decoded
$args = isset($_GET['args']) ? wp_unslash($_GET['args']) : '';
$args = json_decode(base64_decode($args), true);
if (!$args) {
if ($logger) {
$logger->product_feed('โ Invalid args parameter - failed to decode', ActiveTrail_Logger::LEVEL_ERROR);
}
wp_die('Invalid args parameter', 'ActiveTrail Coupon Error', array('response' => 400));
}
// Sanitize decoded data.
if (is_array($args)) {
$args = activetrail_wc_sanitize_array($args);
}
$args = activetrail_wc_normalize_coupon_payload($args);
$requires_amount = empty($args['free_shipping']);
$required_fields = array('coupon_code', 'discount_type');
foreach ($required_fields as $field) {
if (empty($args[$field])) {
if ($logger) {
$logger->product_feed('โ Missing required field: ' . $field, ActiveTrail_Logger::LEVEL_ERROR);
}
wp_die(esc_html('Missing required field: ' . $field), 'ActiveTrail Coupon Error', array('response' => 400));
}
}
if ($requires_amount && empty($args['amount'])) {
if ($logger) {
$logger->product_feed('โ Missing required field: amount', ActiveTrail_Logger::LEVEL_ERROR);
}
wp_die('Missing required field: amount', 'ActiveTrail Coupon Error', array('response' => 400));
} elseif (!$requires_amount && !isset($args['amount'])) {
$args['amount'] = '0';
}
// Validate discount type.
$valid_types = array('percent', 'fixed_cart', 'fixed_product');
if (!in_array($args['discount_type'], $valid_types)) {
if ($logger) {
$logger->product_feed('โ Invalid discount type: ' . $args['discount_type'], ActiveTrail_Logger::LEVEL_ERROR);
}
wp_die(esc_html('Invalid discount_type. Must be one of: ' . implode(', ', $valid_types)), 'ActiveTrail Coupon Error', array('response' => 400));
}
if ($requires_amount) {
$amount = floatval($args['amount']);
if ($amount <= 0) {
if ($logger) {
$logger->product_feed('โ Invalid amount: ' . $args['amount'], ActiveTrail_Logger::LEVEL_ERROR);
}
wp_die('Amount must be greater than 0', 'ActiveTrail Coupon Error', array('response' => 400));
}
if ($args['discount_type'] === 'percent' && $amount > 100) {
if ($logger) {
$logger->product_feed('โ Percent amount cannot exceed 100: ' . $args['amount'], ActiveTrail_Logger::LEVEL_ERROR);
}
wp_die('Percent amount cannot exceed 100', 'ActiveTrail Coupon Error', array('response' => 400));
}
} else {
$args['amount'] = '0';
}
// Check for existing coupon by external_id (idempotency).
if (!empty($args['external_id'])) {
$existing_coupon_id = activetrail_wc_find_coupon_by_external_id($args['external_id']);
if ($existing_coupon_id) {
// Update existing coupon.
$result = activetrail_wc_update_coupon($existing_coupon_id, $args);
if ($result['success']) {
if ($logger) {
$logger->product_feed('โ
Coupon updated successfully via GET: ' . $args['coupon_code'] . ' (ID: ' . $existing_coupon_id . ')', ActiveTrail_Logger::LEVEL_INFO);
}
wp_send_json($result, 200);
} else {
if ($logger) {
$logger->product_feed('โ Failed to update coupon via GET: ' . $result['message'], ActiveTrail_Logger::LEVEL_ERROR);
}
wp_send_json($result, 500);
}
}
}
// Check if coupon code already exists (without external_id).
if (empty($args['external_id'])) {
$existing_coupon_id = wc_get_coupon_id_by_code($args['coupon_code']);
if ($existing_coupon_id) {
if ($logger) {
$logger->product_feed('โ Coupon code already exists without external_id via GET: ' . $args['coupon_code'], ActiveTrail_Logger::LEVEL_ERROR);
}
wp_send_json(array(
'success' => false,
'message' => 'Coupon code already exists. Use external_id for idempotency.'
), 409);
}
}
// Create new coupon.
$result = activetrail_wc_create_coupon($args);
if ($result['success']) {
if ($logger) {
$logger->product_feed('โ
Coupon created successfully via GET: ' . $result['coupon_code'] . ' (ID: ' . $result['coupon_id'] . ')', ActiveTrail_Logger::LEVEL_INFO);
}
wp_send_json($result, 201);
} else {
if ($logger) {
$logger->product_feed('โ Failed to create coupon via GET: ' . $result['message'], ActiveTrail_Logger::LEVEL_ERROR);
}
wp_send_json($result, 500);
}
}
// Hook the GET coupon creation handler.
add_action('init', 'activetrail_wc_handle_coupon_request_get');
/**
* Handle ActiveTrail coupon creation with defaults via GET request
*/
function activetrail_wc_handle_coupon_request_defaults() {
// Check if this is a default coupon creation request.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External webhook endpoint, authentication handled separately
$activetrail_coupon = isset($_GET['activetrail_coupon']) ? sanitize_text_field(wp_unslash($_GET['activetrail_coupon'])) : '';
if ($activetrail_coupon !== 'createDefault') {
return;
}
// Ensure WooCommerce is fully loaded.
if (!function_exists('wc_get_product')) {
if (class_exists('WooCommerce')) {
WC();
} else {
wp_die('WooCommerce is not available', 'ActiveTrail Coupon Error', array('response' => 500));
}
}
// Initialize logger if available.
$logger = function_exists('activetrail_logger') ? activetrail_logger() : null;
if ($logger) {
$logger->product_feed('๐ ActiveTrail default coupon creation request received', ActiveTrail_Logger::LEVEL_INFO, array(
'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : 'unknown',
'ip' => isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : 'unknown',
'request_uri' => isset($_SERVER['REQUEST_URI']) ? esc_url_raw(wp_unslash($_SERVER['REQUEST_URI'])) : 'unknown'
));
}
// Check if args parameter exists.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External feed endpoint, authentication handled separately
if (!isset($_GET['args'])) {
if ($logger) {
$logger->product_feed('โ Missing args parameter for default coupon', ActiveTrail_Logger::LEVEL_ERROR);
}
wp_die('Missing args parameter', 'ActiveTrail Coupon Error', array('response' => 400));
}
// Decode the args parameter.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- External feed endpoint, args are base64 encoded and decoded
$args = isset($_GET['args']) ? wp_unslash($_GET['args']) : '';
$args = json_decode(base64_decode($args), true);
if (!$args) {
if ($logger) {
$logger->product_feed('โ Invalid args parameter for default coupon - failed to decode', ActiveTrail_Logger::LEVEL_ERROR);
}
wp_die('Invalid args parameter', 'ActiveTrail Coupon Error', array('response' => 400));
}
// Sanitize decoded data.
if (is_array($args)) {
$args = activetrail_wc_sanitize_array($args);
}
$args = activetrail_wc_normalize_coupon_payload($args);
// Validate required fields (only amount is required for default creation).
if (empty($args['amount'])) {
if ($logger) {
$logger->product_feed('โ Missing required field: amount for default coupon', ActiveTrail_Logger::LEVEL_ERROR);
}
wp_die('Missing required field: amount', 'ActiveTrail Coupon Error', array('response' => 400));
}
// Validate amount.
$amount = floatval($args['amount']);
if ($amount <= 0) {
if ($logger) {
$logger->product_feed('โ Invalid amount for default coupon: ' . $args['amount'], ActiveTrail_Logger::LEVEL_ERROR);
}
wp_die('Amount must be greater than 0', 'ActiveTrail Coupon Error', array('response' => 400));
}
// Check for existing coupon by external_id (idempotency).
if (!empty($args['external_id'])) {
$existing_coupon_id = activetrail_wc_find_coupon_by_external_id($args['external_id']);
if ($existing_coupon_id) {
// Update existing coupon.
$result = activetrail_wc_update_coupon($existing_coupon_id, $args);
if ($result['success']) {
if ($logger) {
$logger->product_feed('โ
Default coupon updated successfully: ' . $result['coupon_code'] . ' (ID: ' . $existing_coupon_id . ')', ActiveTrail_Logger::LEVEL_INFO);
}
wp_send_json($result, 200);
} else {
if ($logger) {
$logger->product_feed('โ Failed to update default coupon: ' . $result['message'], ActiveTrail_Logger::LEVEL_ERROR);
}
wp_send_json($result, 500);
}
}
}
// Create new coupon with defaults.
$result = activetrail_wc_create_coupon_with_defaults($args);
if ($result['success']) {
if ($logger) {
$logger->product_feed('โ
Default coupon created successfully: ' . $result['coupon_code'] . ' (ID: ' . $result['coupon_id'] . ')', ActiveTrail_Logger::LEVEL_INFO);
}
wp_send_json($result, 201);
} else {
if ($logger) {
$logger->product_feed('โ Failed to create default coupon: ' . $result['message'], ActiveTrail_Logger::LEVEL_ERROR);
}
wp_send_json($result, 500);
}
}
// Hook the default coupon creation handler.
add_action('init', 'activetrail_wc_handle_coupon_request_defaults');
/**
* Handle ActiveTrail coupon creation with custom prefix via GET request
*/
function activetrail_wc_handle_coupon_request_prefix() {
// Check if this is a prefix coupon creation request.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External webhook endpoint, authentication handled separately
$activetrail_coupon = isset($_GET['activetrail_coupon']) ? sanitize_text_field(wp_unslash($_GET['activetrail_coupon'])) : '';
if ($activetrail_coupon !== 'createPrefix') {
return;
}
// Ensure WooCommerce is fully loaded.
if (!function_exists('wc_get_product')) {
if (class_exists('WooCommerce')) {
WC();
} else {
wp_die('WooCommerce is not available', 'ActiveTrail Coupon Error', array('response' => 500));
}
}
// Initialize logger if available.
$logger = function_exists('activetrail_logger') ? activetrail_logger() : null;
if ($logger) {
$logger->product_feed('๐ ActiveTrail prefix coupon creation request received', ActiveTrail_Logger::LEVEL_INFO, array(
'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : 'unknown',
'ip' => isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : 'unknown',
'request_uri' => isset($_SERVER['REQUEST_URI']) ? esc_url_raw(wp_unslash($_SERVER['REQUEST_URI'])) : 'unknown'
));
}
// Check if args parameter exists.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External feed endpoint, authentication handled separately
if (!isset($_GET['args'])) {
if ($logger) {
$logger->product_feed('โ Missing args parameter for prefix coupon', ActiveTrail_Logger::LEVEL_ERROR);
}
wp_die('Missing args parameter', 'ActiveTrail Coupon Error', array('response' => 400));
}
// Decode the args parameter.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- External feed endpoint, args are base64 encoded and decoded
$args = isset($_GET['args']) ? wp_unslash($_GET['args']) : '';
$args = json_decode(base64_decode($args), true);
if (!$args) {
if ($logger) {
$logger->product_feed('โ Invalid args parameter for prefix coupon - failed to decode', ActiveTrail_Logger::LEVEL_ERROR);
}
wp_die('Invalid args parameter', 'ActiveTrail Coupon Error', array('response' => 400));
}
// Sanitize decoded data.
if (is_array($args)) {
$args = activetrail_wc_sanitize_array($args);
}
$args = activetrail_wc_normalize_coupon_payload($args);
// Check if prefix is provided.
if (empty($args['prefix'])) {
if ($logger) {
$logger->product_feed('โ Missing prefix parameter for prefix coupon', ActiveTrail_Logger::LEVEL_ERROR);
}
wp_die('Missing prefix parameter', 'ActiveTrail Coupon Error', array('response' => 400));
}
// Validate required fields (only amount is required for prefix creation).
if (empty($args['amount'])) {
if ($logger) {
$logger->product_feed('โ Missing required field: amount for prefix coupon', ActiveTrail_Logger::LEVEL_ERROR);
}
wp_die('Missing required field: amount', 'ActiveTrail Coupon Error', array('response' => 400));
}
// Validate amount.
$amount = floatval($args['amount']);
if ($amount <= 0) {
if ($logger) {
$logger->product_feed('โ Invalid amount for prefix coupon: ' . $args['amount'], ActiveTrail_Logger::LEVEL_ERROR);
}
wp_die('Amount must be greater than 0', 'ActiveTrail Coupon Error', array('response' => 400));
}
// Check for existing coupon by external_id (idempotency).
if (!empty($args['external_id'])) {
$existing_coupon_id = activetrail_wc_find_coupon_by_external_id($args['external_id']);
if ($existing_coupon_id) {
// Update existing coupon.
$result = activetrail_wc_update_coupon($existing_coupon_id, $args);
if ($result['success']) {
if ($logger) {
$logger->product_feed('โ
Prefix coupon updated successfully: ' . $result['coupon_code'] . ' (ID: ' . $existing_coupon_id . ')', ActiveTrail_Logger::LEVEL_INFO);
}
wp_send_json($result, 200);
} else {
if ($logger) {
$logger->product_feed('โ Failed to update prefix coupon: ' . $result['message'], ActiveTrail_Logger::LEVEL_ERROR);
}
wp_send_json($result, 500);
}
}
}
// Create new coupon with custom prefix.
$result = activetrail_wc_create_coupon_with_prefix($args['prefix'], $args);
if ($result['success']) {
if ($logger) {
$logger->product_feed('โ
Prefix coupon created successfully: ' . $result['coupon_code'] . ' (ID: ' . $result['coupon_id'] . ')', ActiveTrail_Logger::LEVEL_INFO);
}
wp_send_json($result, 201);
} else {
if ($logger) {
$logger->product_feed('โ Failed to create prefix coupon: ' . $result['message'], ActiveTrail_Logger::LEVEL_ERROR);
}
wp_send_json($result, 500);
}
}
// Hook the prefix coupon creation handler.
add_action('init', 'activetrail_wc_handle_coupon_request_prefix');
/**
* Handle ActiveTrail coupon creation with automatic code via GET request
*/
function activetrail_wc_handle_coupon_request_auto() {
// Check if this is an auto code coupon creation request.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External webhook endpoint, authentication handled separately
$activetrail_coupon = isset($_GET['activetrail_coupon']) ? sanitize_text_field(wp_unslash($_GET['activetrail_coupon'])) : '';
if ($activetrail_coupon !== 'createAuto') {
return;
}
// Ensure WooCommerce is fully loaded.
if (!function_exists('wc_get_product')) {
if (class_exists('WooCommerce')) {
WC();
} else {
wp_die('WooCommerce is not available', 'ActiveTrail Coupon Error', array('response' => 500));
}
}
// Initialize logger if available.
$logger = function_exists('activetrail_logger') ? activetrail_logger() : null;
if ($logger) {
$logger->product_feed('๐ ActiveTrail auto code coupon creation request received', ActiveTrail_Logger::LEVEL_INFO, array(
'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : 'unknown',
'ip' => isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : 'unknown',
'request_uri' => isset($_SERVER['REQUEST_URI']) ? esc_url_raw(wp_unslash($_SERVER['REQUEST_URI'])) : 'unknown'
));
}
// Check if args parameter exists.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External feed endpoint, authentication handled separately
if (!isset($_GET['args'])) {
if ($logger) {
$logger->product_feed('โ Missing args parameter for auto code coupon', ActiveTrail_Logger::LEVEL_ERROR);
}
wp_die('Missing args parameter', 'ActiveTrail Coupon Error', array('response' => 400));
}
// Decode the args parameter.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- External feed endpoint, args are base64 encoded and decoded
$args = isset($_GET['args']) ? wp_unslash($_GET['args']) : '';
$args = json_decode(base64_decode($args), true);
if (!$args) {
if ($logger) {
$logger->product_feed('โ Invalid args parameter for auto code coupon - failed to decode', ActiveTrail_Logger::LEVEL_ERROR);
}
wp_die('Invalid args parameter', 'ActiveTrail Coupon Error', array('response' => 400));
}
// Sanitize decoded data.
if (is_array($args)) {
$args = activetrail_wc_sanitize_array($args);
}
$args = activetrail_wc_normalize_coupon_payload($args);
// Validate required fields (only amount is required for auto code creation).
if (empty($args['amount'])) {
if ($logger) {
$logger->product_feed('โ Missing required field: amount for auto code coupon', ActiveTrail_Logger::LEVEL_ERROR);
}
wp_die('Missing required field: amount', 'ActiveTrail Coupon Error', array('response' => 400));
}
// Validate amount.
$amount = floatval($args['amount']);
if ($amount <= 0) {
if ($logger) {
$logger->product_feed('โ Invalid amount for auto code coupon: ' . $args['amount'], ActiveTrail_Logger::LEVEL_ERROR);
}
wp_die('Amount must be greater than 0', 'ActiveTrail Coupon Error', array('response' => 400));
}
// Check for existing coupon by external_id (idempotency).
if (!empty($args['external_id'])) {
$existing_coupon_id = activetrail_wc_find_coupon_by_external_id($args['external_id']);
if ($existing_coupon_id) {
// Update existing coupon.
$result = activetrail_wc_update_coupon($existing_coupon_id, $args);
if ($result['success']) {
if ($logger) {
$logger->product_feed('โ
Auto code coupon updated successfully: ' . $result['coupon_code'] . ' (ID: ' . $existing_coupon_id . ')', ActiveTrail_Logger::LEVEL_INFO);
}
wp_send_json($result, 200);
} else {
if ($logger) {
$logger->product_feed('โ Failed to update auto code coupon: ' . $result['message'], ActiveTrail_Logger::LEVEL_ERROR);
}
wp_send_json($result, 500);
}
}
}
// Create new coupon with automatic code generation.
$result = activetrail_wc_create_coupon_auto_code($args);
if ($result['success']) {
if ($logger) {
$logger->product_feed('โ
Auto code coupon created successfully: ' . $result['coupon_code'] . ' (ID: ' . $result['coupon_id'] . ')', ActiveTrail_Logger::LEVEL_INFO);
}
wp_send_json($result, 201);
} else {
if ($logger) {
$logger->product_feed('โ Failed to create auto code coupon: ' . $result['message'], ActiveTrail_Logger::LEVEL_ERROR);
}
wp_send_json($result, 500);
}
}
// Hook the auto code coupon creation handler.
add_action('init', 'activetrail_wc_handle_coupon_request_auto');
/**
* Handle ActiveTrail coupon creation with custom defaults via GET request
*/
function activetrail_wc_handle_coupon_request_custom_defaults() {
// Check if this is a custom defaults coupon creation request.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External webhook endpoint, authentication handled separately
$activetrail_coupon = isset($_GET['activetrail_coupon']) ? sanitize_text_field(wp_unslash($_GET['activetrail_coupon'])) : '';
if ($activetrail_coupon !== 'createCustom') {
return;
}
// Ensure WooCommerce is fully loaded.
if (!function_exists('wc_get_product')) {
if (class_exists('WooCommerce')) {
WC();
} else {
wp_die('WooCommerce is not available', 'ActiveTrail Coupon Error', array('response' => 500));
}
}
// Initialize logger if available.
$logger = function_exists('activetrail_logger') ? activetrail_logger() : null;
if ($logger) {
$logger->product_feed('๐ ActiveTrail custom defaults coupon creation request received', ActiveTrail_Logger::LEVEL_INFO, array(
'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : 'unknown',
'ip' => isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : 'unknown',
'request_uri' => isset($_SERVER['REQUEST_URI']) ? esc_url_raw(wp_unslash($_SERVER['REQUEST_URI'])) : 'unknown'
));
}
// Check if args parameter exists.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External feed endpoint, authentication handled separately
if (!isset($_GET['args'])) {
if ($logger) {
$logger->product_feed('โ Missing args parameter for custom defaults coupon', ActiveTrail_Logger::LEVEL_ERROR);
}
wp_die('Missing args parameter', 'ActiveTrail Coupon Error', array('response' => 400));
}
// Decode the args parameter.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- External feed endpoint, args are base64 encoded and decoded
$args = isset($_GET['args']) ? wp_unslash($_GET['args']) : '';
$args = json_decode(base64_decode($args), true);
if (!$args) {
if ($logger) {
$logger->product_feed('โ Invalid args parameter for custom defaults coupon - failed to decode', ActiveTrail_Logger::LEVEL_ERROR);
}
wp_die('Invalid args parameter', 'ActiveTrail Coupon Error', array('response' => 400));
}
// Sanitize decoded data.
if (is_array($args)) {
$args = activetrail_wc_sanitize_array($args);
}
$args = activetrail_wc_normalize_coupon_payload($args);
// Check if custom_defaults parameter exists.
if (!isset($args['custom_defaults'])) {
if ($logger) {
$logger->product_feed('โ Missing custom_defaults parameter for custom defaults coupon', ActiveTrail_Logger::LEVEL_ERROR);
}
wp_die('Missing custom_defaults parameter', 'ActiveTrail Coupon Error', array('response' => 400));
}
// Validate required fields (only amount is required for custom defaults creation).
if (empty($args['amount'])) {
if ($logger) {
$logger->product_feed('โ Missing required field: amount for custom defaults coupon', ActiveTrail_Logger::LEVEL_ERROR);
}
wp_die('Missing required field: amount', 'ActiveTrail Coupon Error', array('response' => 400));
}
// Validate amount.
$amount = floatval($args['amount']);
if ($amount <= 0) {
if ($logger) {
$logger->product_feed('โ Invalid amount for custom defaults coupon: ' . $args['amount'], ActiveTrail_Logger::LEVEL_ERROR);
}
wp_die('Amount must be greater than 0', 'ActiveTrail Coupon Error', array('response' => 400));
}
// Check for existing coupon by external_id (idempotency).
if (!empty($args['external_id'])) {
$existing_coupon_id = activetrail_wc_find_coupon_by_external_id($args['external_id']);
if ($existing_coupon_id) {
// Update existing coupon.
$result = activetrail_wc_update_coupon($existing_coupon_id, $args);
if ($result['success']) {
if ($logger) {
$logger->product_feed('โ
Custom defaults coupon updated successfully: ' . $result['coupon_code'] . ' (ID: ' . $existing_coupon_id . ')', ActiveTrail_Logger::LEVEL_INFO);
}
wp_send_json($result, 200);
} else {
if ($logger) {
$logger->product_feed('โ Failed to update custom defaults coupon: ' . $result['message'], ActiveTrail_Logger::LEVEL_ERROR);
}
wp_send_json($result, 500);
}
}
}
// Extract custom defaults.
$custom_defaults = $args['custom_defaults'];
unset($args['custom_defaults']); // Remove from main data
// Create new coupon with custom defaults.
$result = activetrail_wc_create_coupon_custom_defaults($args, $custom_defaults);
if ($result['success']) {
if ($logger) {
$logger->product_feed('โ
Custom defaults coupon created successfully: ' . $result['coupon_code'] . ' (ID: ' . $result['coupon_id'] . ')', ActiveTrail_Logger::LEVEL_INFO);
}
wp_send_json($result, 201);
} else {
if ($logger) {
$logger->product_feed('โ Failed to create custom defaults coupon: ' . $result['message'], ActiveTrail_Logger::LEVEL_ERROR);
}
wp_send_json($result, 500);
}
}
// Hook the custom defaults coupon creation handler.
add_action('init', 'activetrail_wc_handle_coupon_request_custom_defaults');
/**
* Generate unique coupon code
*
* @param string $prefix Prefix for the coupon code
* @return string Unique coupon code
*/
function activetrail_wc_generate_unique_code($prefix = 'AT') {
$code = $prefix . wp_generate_password(6, false);
// Check if code already exists.
if (!empty(wc_get_coupon_id_by_code($code))) {
return activetrail_wc_generate_unique_code($prefix); // Recursive call until unique code found
}
return $code;
}
/**
* Create coupon with default values
*
* @param array $data Coupon data
* @return array Result
*/
function activetrail_wc_create_coupon_with_defaults($data) {
// Set default values.
$defaults = array(
'discount_type' => 'fixed_cart',
'amount' => 0,
'usage_limit' => 1,
'usage_limit_per_user' => 1,
'expiry_date' => gmdate('Y-m-d', strtotime('+371 days')),
'free_shipping' => false,
'product_ids' => null,
'individual_use' => false,
'exclude_product_ids' => '',
'minimum_amount' => '',
'maximum_amount' => '',
'product_categories' => '',
'exclude_product_categories' => '',
'exclude_sale_items' => true,
'description' => ''
);
// Merge with provided data.
$merged = array_merge($defaults, $data);
// Generate unique code if not provided.
if (empty($merged['coupon_code'])) {
$prefix = $merged['prefix'] ?? 'AT';
$merged['coupon_code'] = activetrail_wc_generate_unique_code($prefix);
}
// Create the coupon.
return activetrail_wc_create_coupon($merged);
}
/**
* Create coupon with custom prefix
*
* @param string $prefix Custom prefix for coupon code
* @param array $data Coupon data
* @return array Result
*/
function activetrail_wc_create_coupon_with_prefix($prefix, $data) {
// Set the prefix.
$data['prefix'] = $prefix;
// Create coupon with defaults.
return activetrail_wc_create_coupon_with_defaults($data);
}
/**
* Create coupon with automatic unique code
*
* @param array $data Coupon data
* @return array Result
*/
function activetrail_wc_create_coupon_auto_code($data) {
// Generate unique code automatically.
$prefix = $data['prefix'] ?? 'AT';
$data['coupon_code'] = activetrail_wc_generate_unique_code($prefix);
// Create the coupon.
return activetrail_wc_create_coupon($data);
}
/**
* Create coupon with custom defaults
*
* @param array $data Coupon data
* @param array $custom_defaults Custom default values
* @return array Result
*/
function activetrail_wc_create_coupon_custom_defaults($data, $custom_defaults = array()) {
// Set custom default values.
$defaults = array(
'discount_type' => 'fixed_cart',
'amount' => 0,
'usage_limit' => 1,
'usage_limit_per_user' => 1,
'expiry_date' => gmdate('Y-m-d', strtotime('+371 days')),
'free_shipping' => false,
'product_ids' => null,
'individual_use' => false,
'exclude_product_ids' => '',
'minimum_amount' => '',
'maximum_amount' => '',
'product_categories' => '',
'exclude_product_categories' => '',
'exclude_sale_items' => true,
'description' => ''
);
// Merge with custom defaults.
$defaults = array_merge($defaults, $custom_defaults);
// Merge with provided data.
$merged = array_merge($defaults, $data);
// Generate unique code if not provided.
if (empty($merged['coupon_code'])) {
$prefix = $merged['prefix'] ?? 'AT';
$merged['coupon_code'] = activetrail_wc_generate_unique_code($prefix);
}
// Create the coupon.
return activetrail_wc_create_coupon($merged);
}
/**
* Handle product feed export requests
*/
function activetrail_wc_handle_export_request() {
// Check if this is an export request.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External feed endpoint, authentication handled separately
if (!isset($_GET['activetrail_export']) || !isset($_GET['key'])) {
return;
}
// Ensure WooCommerce is fully loaded.
if (!function_exists('wc_get_product')) {
// Force load WooCommerce if not loaded.
if (class_exists('WooCommerce')) {
WC();
} else {
wp_die('WooCommerce is not available', 'ActiveTrail Export Error', array('response' => 500));
}
}
// Initialize logger if available.
$logger = function_exists('activetrail_logger') ? activetrail_logger() : null;
if ($logger) {
$logger->product_feed('๐ง WooCommerce initialization check', ActiveTrail_Logger::LEVEL_DEBUG, array(
'wc_get_product_exists' => function_exists('wc_get_product'),
'woocommerce_class_exists' => class_exists('WooCommerce'),
'wc_function_exists' => function_exists('WC')
));
}
if ($logger) {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External feed endpoint, authentication handled separately
$logger->product_feed('๐ Product feed export request received', ActiveTrail_Logger::LEVEL_INFO, array(
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External feed endpoint, authentication handled separately
'export_type' => isset($_GET['activetrail_export']) ? sanitize_text_field(wp_unslash($_GET['activetrail_export'])) : 'unknown',
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External feed endpoint, authentication handled separately
'key_provided' => !empty($_GET['key']),
'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : 'unknown',
'ip' => isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : 'unknown',
'request_uri' => isset($_SERVER['REQUEST_URI']) ? esc_url_raw(wp_unslash($_SERVER['REQUEST_URI'])) : 'unknown'
));
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External feed endpoint, authentication handled separately
$export_type = isset($_GET['activetrail_export']) ? sanitize_text_field(wp_unslash($_GET['activetrail_export'])) : '';
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External feed endpoint, authentication handled separately
$provided_key = isset($_GET['key']) ? sanitize_text_field(wp_unslash($_GET['key'])) : '';
if ($logger) {
$logger->product_feed('๐ Request parameters validated', ActiveTrail_Logger::LEVEL_DEBUG, array(
'export_type' => $export_type,
'key_length' => strlen($provided_key)
));
}
// Validate export type.
if (!in_array($export_type, array('google', 'facebook'))) {
if ($logger) {
$logger->product_feed('โ Invalid export type provided', ActiveTrail_Logger::LEVEL_ERROR, array(
'provided_type' => $export_type,
'allowed_types' => array('google', 'facebook')
));
}
wp_die('Invalid export type', 'ActiveTrail Export Error', array('response' => 400));
}
// Get the stored key for this export type.
$stored_key = get_option('activetrail_' . $export_type . '_feed_key', '');
if ($logger) {
$logger->product_feed('๐ Checking stored key', ActiveTrail_Logger::LEVEL_DEBUG, array(
'export_type' => $export_type,
'stored_key_exists' => !empty($stored_key),
'stored_key_length' => strlen($stored_key),
'keys_match' => ($provided_key === $stored_key)
));
}
// Validate the key.
if (empty($stored_key) || $provided_key !== $stored_key) {
if ($logger) {
$logger->product_feed('โ Key validation failed', ActiveTrail_Logger::LEVEL_ERROR, array(
'stored_key_empty' => empty($stored_key),
'keys_match' => ($provided_key === $stored_key),
'export_type' => $export_type
));
}
wp_die('Invalid or expired key', 'ActiveTrail Export Error', array('response' => 403));
}
// Check if product feed is enabled.
$settings = get_option('activetrail_wc_settings', array());
$enable_product_feed = true; // Always enabled
if ($logger) {
$logger->product_feed('โ๏ธ Checking product feed settings', ActiveTrail_Logger::LEVEL_DEBUG, array(
'enable_product_feed' => $enable_product_feed,
'settings_exist' => !empty($settings),
'settings_keys' => array_keys($settings)
));
}
if (!$enable_product_feed) {
if ($logger) {
$logger->product_feed('โ Product feed is disabled in settings', ActiveTrail_Logger::LEVEL_WARNING);
}
wp_die('Product feed is disabled', 'ActiveTrail Export Error', array('response' => 403));
}
// Load the product feed template.
$template_file = ACTIVETRAIL_WC_PLUGIN_DIR . 'templates/product-feed.php';
if ($logger) {
$logger->product_feed('๐ Checking template file', ActiveTrail_Logger::LEVEL_DEBUG, array(
'template_path' => $template_file,
'file_exists' => file_exists($template_file),
'file_readable' => is_readable($template_file)
));
}
if (!file_exists($template_file)) {
if ($logger) {
$logger->product_feed('โ Template file not found', ActiveTrail_Logger::LEVEL_ERROR, array(
'expected_path' => $template_file
));
}
wp_die('Feed template not found', 'ActiveTrail Export Error', array('response' => 500));
}
if ($logger) {
$logger->product_feed('โ
All validations passed, generating feed', ActiveTrail_Logger::LEVEL_INFO, array(
'export_type' => $export_type
));
}
// Check for format parameter (allows XML with Facebook or JSON with Google).
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External feed endpoint, authentication handled separately
$format = isset($_GET['format']) ? sanitize_text_field(wp_unslash($_GET['format'])) : '';
// Determine feed format based on format parameter or default behavior.
if ($format === 'xml') {
$feed_format = 'xml';
} elseif ($format === 'json') {
$feed_format = 'json';
} else {
// Default behavior: both google and facebook = XML.
$feed_format = 'xml';
}
// Set appropriate headers based on feed format.
if ($feed_format === 'xml') {
header('Content-Type: application/xml; charset=utf-8');
$filename = ($export_type === 'google') ? 'google-products.xml' : 'facebook-products.xml';
header('Content-Disposition: inline; filename="' . $filename . '"');
if ($logger) {
$logger->product_feed('๐ Set XML headers', ActiveTrail_Logger::LEVEL_DEBUG, array(
'export_type' => $export_type,
'format' => $format
));
}
} else {
header('Content-Type: application/json; charset=utf-8');
$filename = ($export_type === 'facebook') ? 'facebook-products.json' : 'google-products.json';
header('Content-Disposition: inline; filename="' . $filename . '"');
if ($logger) {
$logger->product_feed('๐ Set JSON headers', ActiveTrail_Logger::LEVEL_DEBUG, array(
'export_type' => $export_type,
'format' => $format
));
}
}
// Prevent caching.
header('Cache-Control: no-cache, must-revalidate');
header('Expires: Sat, 26 Jul 1997 05:00:00 GMT');
// Update last export time.
update_option('activetrail_feed_last_update', current_time('mysql'));
if ($logger) {
$logger->product_feed('๐ Updated last export time', ActiveTrail_Logger::LEVEL_DEBUG, array(
'timestamp' => current_time('mysql')
));
}
// Include the template and pass the export type and format.
$GLOBALS['activetrail_export_type'] = $export_type;
$GLOBALS['activetrail_feed_format'] = $feed_format;
if ($logger) {
$logger->product_feed('๐ฏ Including template file to generate feed', ActiveTrail_Logger::LEVEL_INFO, array(
'template_file' => $template_file,
'export_type' => $export_type
));
}
// Include template to generate feed content.
// Template stores content in $GLOBALS['activetrail_feed_output'] instead of echoing.
// Use output buffering to catch any accidental output.
ob_start();
include $template_file;
$ob_content = ob_get_clean();
// Get the generated content from global variable (preferred method).
$feed_content = isset( $GLOBALS['activetrail_feed_output'] ) ? $GLOBALS['activetrail_feed_output'] : '';
// If global variable is empty but output buffer has content, use that (fallback).
if ( empty( $feed_content ) && ! empty( $ob_content ) ) {
$feed_content = $ob_content;
}
if ($logger) {
$logger->product_feed('โ
Feed generation completed successfully', ActiveTrail_Logger::LEVEL_INFO, array(
'export_type' => $export_type,
'content_length' => strlen($feed_content),
'content_preview' => substr($feed_content, 0, 500) . '...',
'content_empty' => empty($feed_content),
'starts_with_xml' => strpos($feed_content, ' strpos($feed_content, ' isset($GLOBALS['activetrail_feed_output']),
'from_buffer' => !empty($ob_content),
'first_100_chars' => substr($feed_content, 0, 100)
));
}
// Also log to logger for immediate debugging.
if (function_exists('activetrail_logger')) {
$logger = activetrail_logger();
$logger->product_feed('Feed generated with length: ' . strlen($feed_content), 'DEBUG');
$logger->product_feed('Content preview: ' . substr($feed_content, 0, 200), 'DEBUG');
}
// Output the feed content.
// Check if there was an error (stored in global variable).
$is_error = isset( $GLOBALS['activetrail_feed_error'] ) && $GLOBALS['activetrail_feed_error'];
if ( empty( $feed_content ) ) {
// No content generated - output error.
if ( $feed_format === 'json' ) {
status_header( 500 );
wp_send_json_error( array( 'error' => 'Feed generation failed - no content generated' ) );
} else {
header( 'Content-Type: application/xml; charset=utf-8' );
status_header( 500 );
echo 'Feed generation failed - no content generated';
wp_die();
}
}
// For JSON feeds: output JSON string directly with proper headers (feed is pre-encoded with specific flags).
// For XML feeds: output XML directly with proper headers and exit.
if ( $feed_format === 'json' ) {
// JSON feed is pre-encoded by generate_facebook_json() with JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE.
// Output directly to preserve formatting and encoding flags.
if ( $is_error ) {
status_header( 500 );
}
header( 'Content-Type: application/json; charset=utf-8' );
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- JSON feed content is generated server-side using wp_json_encode() and must be output as-is for external feed parsers. Escaping would break JSON structure.
echo $feed_content;
// Use exit instead of wp_die() for JSON feeds - wp_die() outputs HTML which breaks JSON structure.
exit;
} else {
// XML feed - output directly with proper headers.
// Headers are already set above, but ensure Content-Type is correct.
if ( $is_error ) {
status_header( 500 );
}
header( 'Content-Type: application/xml; charset=utf-8' );
// Ensure feed content starts with XML declaration (safety check).
if ( ! empty( $feed_content ) && strpos( $feed_content, 'product_feed( 'โ ๏ธ WARNING: Feed content does not start with XML declaration', ActiveTrail_Logger::LEVEL_WARNING, array(
'first_100_chars' => substr( $feed_content, 0, 100 ),
'content_length' => strlen( $feed_content )
) );
}
// If content doesn't start with XML, something is wrong - don't output it.
wp_die( 'Error: Feed content is invalid', 'Feed Error', array( 'response' => 500 ) );
}
// Clear any output that might have been sent before.
if ( ob_get_level() > 0 ) {
ob_clean();
}
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- XML feed content is generated server-side and must be output as-is for external feed parsers. Escaping would break XML structure.
echo $feed_content;
// Use exit instead of wp_die() for XML feeds - wp_die() outputs HTML which breaks XML structure.
exit;
}
}
// Hook after WooCommerce registers its post types and taxonomies (priority 5) so wc_get_product() works correctly.
add_action('init', 'activetrail_wc_handle_export_request', 10);
/**
* Alternative hook for export requests - even earlier
*/
function activetrail_wc_handle_export_request_early() {
// Check if this is an export request.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External feed endpoint, authentication handled separately
if (!isset($_GET['activetrail_export']) || !isset($_GET['key'])) {
return;
}
// Force load WooCommerce early.
if (!function_exists('wc_get_product')) {
// Try to load WooCommerce.
if (file_exists(WP_PLUGIN_DIR . '/woocommerce/woocommerce.php')) {
include_once WP_PLUGIN_DIR . '/woocommerce/woocommerce.php';
}
// Initialize WooCommerce if class exists.
if (class_exists('WooCommerce') && !function_exists('wc_get_product')) {
// Force initialize WooCommerce.
// Note: $woocommerce is a core WooCommerce global variable, not our plugin's variable.
if (!isset($GLOBALS['woocommerce']) || !$GLOBALS['woocommerce']) {
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound -- Core WooCommerce global variable
$GLOBALS['woocommerce'] = new WooCommerce();
}
WC();
}
}
}
// Hook even earlier to ensure WooCommerce is loaded.
add_action('plugins_loaded', 'activetrail_wc_handle_export_request_early', 5);
/**
* Add custom cron schedules
*
* Register cron schedules early, before any event scheduling
*/
function activetrail_wc_add_cron_schedules($schedules) {
$schedules['every_minute'] = array(
'interval' => 60, // 1 minute in seconds
// phpcs:ignore WordPress.WP.I18n.TextDomainMismatch -- Text domain 'activetrail-for-woocommerce' is correct per WordPress review requirements
'display' => esc_html__('Every Minute', 'activetrail-for-woocommerce')
);
$schedules['every_five_minutes'] = array(
'interval' => 300, // 5 minutes in seconds
// phpcs:ignore WordPress.WP.I18n.TextDomainMismatch -- Text domain 'activetrail-for-woocommerce' is correct per WordPress review requirements
'display' => esc_html__('Every 5 Minutes', 'activetrail-for-woocommerce')
);
$schedules['every_fifteen_minutes'] = array(
'interval' => 900, // 15 minutes in seconds
// phpcs:ignore WordPress.WP.I18n.TextDomainMismatch -- Text domain 'activetrail-for-woocommerce' is correct per WordPress review requirements
'display' => esc_html__('Every 15 Minutes', 'activetrail-for-woocommerce')
);
return $schedules;
}
// Use priority 5 to ensure schedules are registered before any events are scheduled.
add_filter('cron_schedules', 'activetrail_wc_add_cron_schedules', 5);
/**
* Plugin activation hook
*/
function activetrail_wc_activate() {
// Check if WooCommerce is active.
if (!activetrail_wc_check_woocommerce()) {
deactivate_plugins(plugin_basename(__FILE__));
wp_die(
// phpcs:ignore WordPress.WP.I18n.TextDomainMismatch -- Text domain 'activetrail-for-woocommerce' is correct per WordPress review requirements
esc_html__('ActiveTrail for WooCommerce requires WooCommerce to be installed and active.', 'activetrail-for-woocommerce'),
// phpcs:ignore WordPress.WP.I18n.TextDomainMismatch -- Text domain 'activetrail-for-woocommerce' is correct per WordPress review requirements
esc_html__('Plugin Activation Error', 'activetrail-for-woocommerce'),
array('back_link' => true)
);
}
// Set default options.
$default_options = array(
'api_key' => '',
'group_id' => '',
'order_statuses' => array( 'completed' ),
'enable_checkout_consent' => true,
'consent_text' => 'I agree to receive marketing emails', // Will be translated when displayed
'consent_checked_by_default' => false,
'consent_required' => false,
'enable_registration_consent' => false,
'sync_without_consent' => false,
);
add_option( 'activetrail_wc_settings', $default_options );
// Create or update custom database tables and store DB schema version.
activetrail_wc_create_tables();
update_option( 'activetrail_wc_db_version', ACTIVETRAIL_WC_VERSION );
// Flush rewrite rules so /activetrail-feed.xml resolves immediately after activation.
add_rewrite_rule( '^activetrail-feed\.xml$', 'index.php?activetrail_feed=xml', 'top' );
add_rewrite_rule( '^activetrail-feed\.json$', 'index.php?activetrail_feed=json', 'top' );
flush_rewrite_rules();
}
/**
* Ensure database tables are up to date on plugin load.
*
* This runs on existing installations as well (not ืจืง ืืืคืขืื ืจืืฉืื ื),
* ืืืคืขืื ืฉืื ืืช dbDelta ืื ืืจืกืช ืืกืืื ืืฉืชื ืชื โ ืื ืืชืืืกืคืืช ืขืืืืืช ืืืฉืืช
* ืืื sync_attempts, ืืื ืืืื ื ืชืื ืื, ืืื ืกืืืื ืฉืื ืืชืงืื ืื ืื ืืขืืื ืื ืืช ืืชืืกืฃ.
*/
function activetrail_wc_maybe_update_tables() {
$installed_version = get_option( 'activetrail_wc_db_version' );
if ( $installed_version === ACTIVETRAIL_WC_VERSION ) {
return;
}
// (Re)create/upgrade tables โ dbDelta ืืืกืืฃ ืจืง ืื ืฉืืกืจ.
activetrail_wc_create_tables();
update_option( 'activetrail_wc_db_version', ACTIVETRAIL_WC_VERSION );
}
// Run after plugins_loaded so ืฉืึพ$wpdb ืืฉ ืืช ืื ืืืืืข, ืืื ืืคื ื ืฉืื ืื ื ืืชืืืืื ืืืฉืชืืฉ ืืืืืืืช.
add_action( 'plugins_loaded', 'activetrail_wc_maybe_update_tables', 20 );
/**
* One-time clear of old 2-arg debounced cart sync cron events.
* New code schedules events with 3 args (session_id, snapshot_time, cart_data) so sync works without session.
* Run once per site so old scheduled events are removed and only 3-arg events run.
*/
function activetrail_wc_clear_legacy_debounced_cron() {
if ( get_option( 'activetrail_wc_debounced_cron_v2', '' ) === 'yes' ) {
return;
}
wp_clear_scheduled_hook( 'activetrail_debounced_cart_sync' );
update_option( 'activetrail_wc_debounced_cron_v2', 'yes' );
}
add_action( 'plugins_loaded', 'activetrail_wc_clear_legacy_debounced_cron', 21 );
/**
* Schedule plugin cron events
*
* This function is called after cron schedules are registered
* to ensure all events use valid schedules
*/
function activetrail_wc_schedule_events() {
// Schedule abandoned cart sync if not already scheduled.
if (!wp_next_scheduled('activetrail_sync_abandoned_carts')) {
wp_schedule_event(time(), 'every_minute', 'activetrail_sync_abandoned_carts');
}
// Schedule event queue processing if not already scheduled.
if (!wp_next_scheduled('activetrail_process_event_queue')) {
wp_schedule_event(time(), 'every_five_minutes', 'activetrail_process_event_queue');
}
// Schedule expired coupons cleanup if not already scheduled.
if (!wp_next_scheduled('activetrail_clean_expired_coupons')) {
wp_schedule_event(time(), 'daily', 'activetrail_clean_expired_coupons');
}
}
// Schedule events after cron schedules are registered.
add_action('init', 'activetrail_wc_schedule_events', 20);
/**
* Send disconnect API call to ActiveTrail
* Helper function for deactivation and uninstall hooks
*/
function activetrail_wc_send_disconnect() {
// Get API key from settings.
require_once ACTIVETRAIL_WC_PLUGIN_DIR . 'includes/settings/class-settings.php';
$settings = ActiveTrail_Settings::get_all();
$api_key = isset( $settings['api_key'] ) ? $settings['api_key'] : '';
if ( empty( $api_key ) ) {
// No API key, nothing to disconnect.
if ( function_exists( 'activetrail_logger' ) ) {
// Log explicitly so ืฉื ืืื ืืจืืืช ืืืื ืืื ืื ื ืฉืื disconnect.
activetrail_logger()->info( 'Skip store disconnect: no API key configured', 'SETTINGS' );
}
return;
}
// Load API client.
require_once ACTIVETRAIL_WC_PLUGIN_DIR . 'includes/api/class-activetrail-api-client.php';
$api_client = new ActiveTrail_API_Client( $api_key );
// Log ืืคื ื ืืงืจืืื ืืคืืขื ืืื ืฉื ืืข ืฉโdeactivate/uninstall ื ืืกื ืืฉืืื disconnect.
if ( function_exists( 'activetrail_logger' ) ) {
activetrail_logger()->info( 'Attempting store disconnect on plugin deactivation/uninstall', 'SETTINGS' );
}
// Send disconnect call with short timeout (fail silently if it fails).
$result = $api_client->disconnect_store();
// Log ืชืืฆืืช ืืงืจืืื (ืืื ืืืฉืืฃ ืคืจืืื ืจืืืฉืื).
if ( function_exists( 'activetrail_logger' ) ) {
$success_flag = ( is_array( $result ) && isset( $result['success'] ) )
? ( $result['success'] ? 'success' : 'failure' )
: 'unknown';
$extra_message = ( is_array( $result ) && ! empty( $result['message'] ) )
? ' | message: ' . $result['message']
: '';
activetrail_logger()->info(
'Store disconnect finished with status: ' . $success_flag . $extra_message,
'SETTINGS'
);
}
// Result is logged internally, we don't need to do anything else here.
// Failures are handled silently to avoid blocking admin UX.
}
/**
* Plugin deactivation hook
*/
function activetrail_wc_deactivate() {
global $wpdb;
// Send disconnect API call.
activetrail_wc_send_disconnect();
// Clear all scheduled events.
wp_clear_scheduled_hook('activetrail_sync_abandoned_carts');
wp_clear_scheduled_hook('activetrail_process_event_queue');
wp_clear_scheduled_hook('activetrail_clean_expired_coupons');
wp_clear_scheduled_hook('activetrail_debounced_cart_sync');
// Delete all plugin options.
$options = array(
'activetrail_wc_settings',
'activetrail_api_key',
'activetrail_group_id',
'activetrail_store_status',
'activetrail_language',
'activetrail_order_statuses',
'activetrail_enable_checkout_consent',
'activetrail_consent_text',
'activetrail_consent_checked_by_default',
'activetrail_consent_required',
'activetrail_sync_without_consent',
'activetrail_enable_cart_tracking',
'activetrail_enable_product_feed',
'activetrail_feed_title',
'activetrail_feed_description',
'activetrail_debug_mode',
'activetrail_historical_sync_status',
'activetrail_status_mapping',
);
foreach ( $options as $option ) {
delete_option( $option );
}
// Delete any remaining options with the activetrail prefix.
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
$wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE 'activetrail_%'" );
// Remove transients.
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
$wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_activetrail_%'" );
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
$wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_timeout_activetrail_%'" );
}
/**
* Create custom database tables
*/
function activetrail_wc_create_tables() {
global $wpdb;
$charset_collate = $wpdb->get_charset_collate();
// Cart tracking table (enhanced).
$cart_table = $wpdb->prefix . 'activetrail_cart_tracking';
$cart_sql = "CREATE TABLE $cart_table (
id mediumint(9) NOT NULL AUTO_INCREMENT,
session_id varchar(255) NOT NULL,
user_id bigint(20) DEFAULT NULL,
cart_data longtext NOT NULL,
email varchar(100) DEFAULT NULL,
created_at datetime DEFAULT CURRENT_TIMESTAMP,
updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
synced tinyint(1) DEFAULT 0,
sync_attempts int(11) DEFAULT 0,
last_sync_attempt datetime DEFAULT NULL,
abandonment_sent tinyint(1) DEFAULT 0,
recovery_email_sent tinyint(1) DEFAULT 0,
PRIMARY KEY (id),
KEY session_id (session_id),
KEY user_id (user_id),
KEY email (email),
KEY synced (synced),
KEY sync_attempts (sync_attempts),
KEY abandonment_sent (abandonment_sent)
) $charset_collate;";
// Events tracking table.
$events_table = $wpdb->prefix . 'activetrail_events';
$events_sql = "CREATE TABLE $events_table (
id bigint(20) NOT NULL AUTO_INCREMENT,
event_type varchar(50) NOT NULL,
customer_email varchar(100) DEFAULT NULL,
customer_id bigint(20) DEFAULT NULL,
session_id varchar(255) DEFAULT NULL,
product_id bigint(20) DEFAULT NULL,
order_id bigint(20) DEFAULT NULL,
event_data longtext DEFAULT NULL,
event_value decimal(10,2) DEFAULT NULL,
currency varchar(3) DEFAULT NULL,
created_at datetime DEFAULT CURRENT_TIMESTAMP,
synced tinyint(1) DEFAULT 0,
sync_attempts int(11) DEFAULT 0,
last_sync_attempt datetime DEFAULT NULL,
PRIMARY KEY (id),
KEY event_type (event_type),
KEY customer_email (customer_email),
KEY customer_id (customer_id),
KEY product_id (product_id),
KEY synced (synced),
KEY created_at (created_at)
) $charset_collate;";
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
dbDelta($cart_sql);
dbDelta($events_sql);
}
// Register activation and deactivation hooks.
register_activation_hook(__FILE__, 'activetrail_wc_activate');
register_deactivation_hook(__FILE__, 'activetrail_wc_deactivate');
/**
* Add plugin action links
*/
function activetrail_wc_plugin_action_links($links) {
// phpcs:ignore WordPress.WP.I18n.TextDomainMismatch -- Text domain 'activetrail-for-woocommerce' is correct per WordPress review requirements
$settings_link = '' . esc_html__('Settings', 'activetrail-for-woocommerce') . '';
array_unshift($links, $settings_link);
return $links;
}
add_filter('plugin_action_links_' . plugin_basename(__FILE__), 'activetrail_wc_plugin_action_links');