feat(affiliate): add core module, controllers, and route registration
This commit is contained in:
364
includes/Api/Controllers/AffiliateAdminController.php
Normal file
364
includes/Api/Controllers/AffiliateAdminController.php
Normal file
@@ -0,0 +1,364 @@
|
||||
<?php
|
||||
|
||||
namespace WooNooW\Api\Controllers;
|
||||
|
||||
use WP_REST_Request;
|
||||
use WP_REST_Response;
|
||||
use WP_REST_Server;
|
||||
use WooNooW\Core\ModuleRegistry;
|
||||
|
||||
class AffiliateAdminController
|
||||
{
|
||||
private $namespace = 'woonoow/v1';
|
||||
|
||||
public function register_routes()
|
||||
{
|
||||
// List Affiliates
|
||||
register_rest_route($this->namespace, '/admin/affiliates', [
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [$this, 'get_affiliates'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Get Affiliate Balance (payable amount)
|
||||
register_rest_route($this->namespace, '/admin/affiliates/(?P<id>\d+)/balance', [
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [$this, 'get_affiliate_balance'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Approve Affiliate
|
||||
register_rest_route($this->namespace, '/admin/affiliates/(?P<id>\d+)/approve', [
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => [$this, 'approve_affiliate'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Update Affiliate (commission rate)
|
||||
register_rest_route($this->namespace, '/admin/affiliates/(?P<id>\d+)/update', [
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => [$this, 'update_affiliate'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// List Referrals
|
||||
register_rest_route($this->namespace, '/admin/affiliates/referrals', [
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [$this, 'get_referrals'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// List Payouts (for all affiliates)
|
||||
register_rest_route($this->namespace, '/admin/affiliates/payouts', [
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [$this, 'get_payouts'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Create Payout
|
||||
register_rest_route($this->namespace, '/admin/affiliates/payouts', [
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => [$this, 'create_payout'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function check_permission()
|
||||
{
|
||||
return current_user_can('manage_woocommerce');
|
||||
}
|
||||
|
||||
public function get_affiliates(WP_REST_Request $request)
|
||||
{
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . 'woonoow_affiliates';
|
||||
$affiliates = $wpdb->get_results("SELECT * FROM $table ORDER BY created_at DESC", ARRAY_A);
|
||||
|
||||
// Add payable_balance to each affiliate
|
||||
foreach ($affiliates as &$affiliate) {
|
||||
$affiliate['payable_balance'] = (float) ($affiliate['total_earnings'] ?? 0) - (float) ($affiliate['paid_earnings'] ?? 0);
|
||||
// Get user info
|
||||
$user = get_userdata($affiliate['user_id']);
|
||||
if ($user) {
|
||||
$affiliate['user_email'] = $user->user_email;
|
||||
$affiliate['user_name'] = $user->display_name;
|
||||
}
|
||||
}
|
||||
|
||||
return rest_ensure_response($affiliates);
|
||||
}
|
||||
|
||||
public function get_affiliate_balance(WP_REST_Request $request)
|
||||
{
|
||||
global $wpdb;
|
||||
$id = $request->get_param('id');
|
||||
$table = $wpdb->prefix . 'woonoow_affiliates';
|
||||
|
||||
$affiliate = $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT id, user_id, referral_code, total_earnings, paid_earnings, total_referrals
|
||||
FROM $table WHERE id = %d",
|
||||
$id
|
||||
));
|
||||
|
||||
if (!$affiliate) {
|
||||
return new WP_REST_Response(['error' => 'Affiliate not found'], 404);
|
||||
}
|
||||
|
||||
$user = get_userdata($affiliate->user_id);
|
||||
|
||||
return rest_ensure_response([
|
||||
'id' => (int) $affiliate->id,
|
||||
'user_id' => (int) $affiliate->user_id,
|
||||
'user_name' => $user ? $user->display_name : 'Unknown',
|
||||
'user_email' => $user ? $user->user_email : '',
|
||||
'referral_code' => $affiliate->referral_code,
|
||||
'total_earnings' => (float) $affiliate->total_earnings,
|
||||
'paid_earnings' => (float) $affiliate->paid_earnings,
|
||||
'payable_balance' => (float) $affiliate->total_earnings - (float) $affiliate->paid_earnings,
|
||||
'total_referrals' => (int) $affiliate->total_referrals,
|
||||
'approved_referrals' => $this->get_approved_referral_count($affiliate->id),
|
||||
'pending_referrals' => $this->get_pending_referral_count($affiliate->id),
|
||||
]);
|
||||
}
|
||||
|
||||
private function get_approved_referral_count($affiliate_id)
|
||||
{
|
||||
global $wpdb;
|
||||
return (int) $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$wpdb->prefix}woonoow_referrals WHERE affiliate_id = %d AND status = 'approved'",
|
||||
$affiliate_id
|
||||
));
|
||||
}
|
||||
|
||||
private function get_pending_referral_count($affiliate_id)
|
||||
{
|
||||
global $wpdb;
|
||||
return (int) $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$wpdb->prefix}woonoow_referrals WHERE affiliate_id = %d AND status = 'pending'",
|
||||
$affiliate_id
|
||||
));
|
||||
}
|
||||
|
||||
public function approve_affiliate(WP_REST_Request $request)
|
||||
{
|
||||
global $wpdb;
|
||||
$id = $request->get_param('id');
|
||||
$table = $wpdb->prefix . 'woonoow_affiliates';
|
||||
|
||||
$wpdb->update(
|
||||
$table,
|
||||
['status' => 'active'],
|
||||
['id' => $id]
|
||||
);
|
||||
|
||||
// Trigger email notification for approval
|
||||
$affiliate = $wpdb->get_row($wpdb->prepare("SELECT * FROM $table WHERE id = %d", $id));
|
||||
if ($affiliate) {
|
||||
$user = get_userdata($affiliate->user_id);
|
||||
if ($user) {
|
||||
do_action('woonoow/email/trigger', 'affiliate_application_approved', $user->user_email, [
|
||||
'affiliate_name' => $user->display_name,
|
||||
'referral_code' => $affiliate->referral_code
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return rest_ensure_response(['success' => true]);
|
||||
}
|
||||
|
||||
public function update_affiliate(WP_REST_Request $request)
|
||||
{
|
||||
global $wpdb;
|
||||
$id = $request->get_param('id');
|
||||
$table = $wpdb->prefix . 'woonoow_affiliates';
|
||||
|
||||
$custom_rate = $request->get_param('custom_commission_rate');
|
||||
|
||||
// If rate is empty string or not provided, clear custom rate
|
||||
if ($custom_rate === '' || $custom_rate === null || $custom_rate === false) {
|
||||
$data = ['custom_commission_rate' => null];
|
||||
} else {
|
||||
$custom_rate = floatval($custom_rate);
|
||||
if ($custom_rate < 0 || $custom_rate > 100) {
|
||||
return new WP_REST_Response(['error' => 'Commission rate must be between 0 and 100'], 400);
|
||||
}
|
||||
$data = ['custom_commission_rate' => $custom_rate];
|
||||
}
|
||||
|
||||
$result = $wpdb->update(
|
||||
$table,
|
||||
$data,
|
||||
['id' => $id]
|
||||
);
|
||||
|
||||
if ($result === false) {
|
||||
return new WP_REST_Response(['error' => 'Failed to update affiliate'], 500);
|
||||
}
|
||||
|
||||
return rest_ensure_response(['success' => true, 'custom_commission_rate' => $data['custom_commission_rate']]);
|
||||
}
|
||||
|
||||
public function get_referrals(WP_REST_Request $request)
|
||||
{
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . 'woonoow_referrals';
|
||||
|
||||
// Add filter support
|
||||
$where = "1=1";
|
||||
|
||||
$affiliate_id = $request->get_param('affiliate_id');
|
||||
if ($affiliate_id) {
|
||||
$where .= $wpdb->prepare(" AND affiliate_id = %d", $affiliate_id);
|
||||
}
|
||||
|
||||
$status = $request->get_param('status');
|
||||
if ($status) {
|
||||
$where .= $wpdb->prepare(" AND status = %s", $status);
|
||||
}
|
||||
|
||||
$date_start = $request->get_param('date_start');
|
||||
if ($date_start) {
|
||||
$where .= $wpdb->prepare(" AND created_at >= %s", $date_start . ' 00:00:00');
|
||||
}
|
||||
|
||||
$date_end = $request->get_param('date_end');
|
||||
if ($date_end) {
|
||||
$where .= $wpdb->prepare(" AND created_at <= %s", $date_end . ' 23:59:59');
|
||||
}
|
||||
|
||||
$order_id = $request->get_param('order_id');
|
||||
if ($order_id) {
|
||||
$where .= $wpdb->prepare(" AND order_id = %d", $order_id);
|
||||
}
|
||||
|
||||
$referrals = $wpdb->get_results("SELECT * FROM $table WHERE $where ORDER BY created_at DESC", ARRAY_A);
|
||||
|
||||
// Enrich with affiliate info
|
||||
$affiliates_table = $wpdb->prefix . 'woonoow_affiliates';
|
||||
foreach ($referrals as &$referral) {
|
||||
$affiliate = $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT a.*, u.display_name as affiliate_name, u.user_email as affiliate_email
|
||||
FROM $affiliates_table a
|
||||
LEFT JOIN $wpdb->users u ON a.user_id = u.ID
|
||||
WHERE a.id = %d",
|
||||
$referral['affiliate_id']
|
||||
));
|
||||
if ($affiliate) {
|
||||
$referral['affiliate_name'] = $affiliate->affiliate_name;
|
||||
$referral['affiliate_email'] = $affiliate->affiliate_email;
|
||||
}
|
||||
}
|
||||
|
||||
return rest_ensure_response($referrals);
|
||||
}
|
||||
|
||||
public function get_payouts(WP_REST_Request $request)
|
||||
{
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . 'woonoow_affiliate_payouts';
|
||||
|
||||
$affiliate_id = $request->get_param('affiliate_id');
|
||||
$where = $affiliate_id ? $wpdb->prepare("WHERE affiliate_id = %d", $affiliate_id) : "";
|
||||
|
||||
$payouts = $wpdb->get_results("SELECT * FROM $table $where ORDER BY created_at DESC", ARRAY_A);
|
||||
|
||||
// Enrich with affiliate info
|
||||
$affiliates_table = $wpdb->prefix . 'woonoow_affiliates';
|
||||
foreach ($payouts as &$payout) {
|
||||
$affiliate = $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT a.*, u.display_name as affiliate_name, u.user_email as affiliate_email
|
||||
FROM $affiliates_table a
|
||||
LEFT JOIN $wpdb->users u ON a.user_id = u.ID
|
||||
WHERE a.id = %d",
|
||||
$payout['affiliate_id']
|
||||
));
|
||||
if ($affiliate) {
|
||||
$payout['affiliate_name'] = $affiliate->affiliate_name;
|
||||
$payout['affiliate_email'] = $affiliate->affiliate_email;
|
||||
}
|
||||
}
|
||||
|
||||
return rest_ensure_response($payouts);
|
||||
}
|
||||
|
||||
public function create_payout(WP_REST_Request $request)
|
||||
{
|
||||
global $wpdb;
|
||||
$payouts_table = $wpdb->prefix . 'woonoow_affiliate_payouts';
|
||||
$affiliates_table = $wpdb->prefix . 'woonoow_affiliates';
|
||||
|
||||
$affiliate_id = absint($request->get_param('affiliate_id'));
|
||||
$amount = floatval($request->get_param('amount'));
|
||||
$method = sanitize_text_field($request->get_param('method') ?: 'bank_transfer');
|
||||
$notes = '';
|
||||
|
||||
// Validate affiliate exists and get balance
|
||||
$affiliate = $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT * FROM $affiliates_table WHERE id = %d",
|
||||
$affiliate_id
|
||||
));
|
||||
|
||||
if (!$affiliate) {
|
||||
return new WP_REST_Response(['error' => 'Affiliate not found'], 404);
|
||||
}
|
||||
|
||||
$payable_balance = (float) $affiliate->total_earnings - (float) $affiliate->paid_earnings;
|
||||
|
||||
if ($amount <= 0) {
|
||||
return new WP_REST_Response(['error' => 'Amount must be greater than 0'], 400);
|
||||
}
|
||||
|
||||
if ($amount > $payable_balance) {
|
||||
return new WP_REST_Response([
|
||||
'error' => 'Amount exceeds payable balance',
|
||||
'payable_balance' => $payable_balance
|
||||
], 400);
|
||||
}
|
||||
|
||||
// Generate coupon for store_credit method
|
||||
if ($method === 'store_credit') {
|
||||
$user = get_userdata($affiliate->user_id);
|
||||
if ($user) {
|
||||
$coupon_code = 'CREDIT-' . strtoupper(wp_generate_password(8, false));
|
||||
$coupon = new \WC_Coupon();
|
||||
$coupon->set_code($coupon_code);
|
||||
$coupon->set_discount_type('fixed_cart');
|
||||
$coupon->set_amount($amount);
|
||||
$coupon->set_email_restrictions([$user->user_email]);
|
||||
$coupon->set_usage_limit(1);
|
||||
$coupon->set_description('Store Credit for Affiliate Payout');
|
||||
$coupon->save();
|
||||
|
||||
$notes = 'Generated Store Credit Coupon: ' . $coupon_code;
|
||||
}
|
||||
}
|
||||
|
||||
// Create payout record
|
||||
$wpdb->insert($payouts_table, [
|
||||
'affiliate_id' => $affiliate_id,
|
||||
'amount' => $amount,
|
||||
'currency' => get_woocommerce_currency(),
|
||||
'method' => $method,
|
||||
'status' => 'completed',
|
||||
'notes' => $notes,
|
||||
'completed_at' => current_time('mysql')
|
||||
]);
|
||||
|
||||
$payout_id = $wpdb->insert_id;
|
||||
|
||||
// Update affiliate's paid_earnings
|
||||
$wpdb->query($wpdb->prepare(
|
||||
"UPDATE $affiliates_table SET paid_earnings = paid_earnings + %f WHERE id = %d",
|
||||
$amount,
|
||||
$affiliate_id
|
||||
));
|
||||
|
||||
return rest_ensure_response([
|
||||
'success' => true,
|
||||
'id' => $payout_id,
|
||||
'new_paid_earnings' => (float) $affiliate->paid_earnings + $amount,
|
||||
'new_payable_balance' => $payable_balance - $amount,
|
||||
'coupon_code' => $method === 'store_credit' ? $coupon_code : null
|
||||
]);
|
||||
}
|
||||
}
|
||||
273
includes/Api/Controllers/AffiliateCustomerController.php
Normal file
273
includes/Api/Controllers/AffiliateCustomerController.php
Normal file
@@ -0,0 +1,273 @@
|
||||
<?php
|
||||
|
||||
namespace WooNooW\Api\Controllers;
|
||||
|
||||
use WP_REST_Request;
|
||||
use WP_REST_Response;
|
||||
use WP_REST_Server;
|
||||
|
||||
class AffiliateCustomerController
|
||||
{
|
||||
private $namespace = 'woonoow/v1';
|
||||
|
||||
public function register_routes()
|
||||
{
|
||||
register_rest_route($this->namespace, '/account/affiliate', [
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [$this, 'get_dashboard'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route($this->namespace, '/account/affiliate/apply', [
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => [$this, 'apply_affiliate'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route($this->namespace, '/account/affiliate/referrals', [
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [$this, 'get_referrals'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route($this->namespace, '/account/affiliate/payouts', [
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [$this, 'get_payouts'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route($this->namespace, '/account/affiliate/payment-details', [
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [$this, 'get_payment_details'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route($this->namespace, '/account/affiliate/payment-details', [
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => [$this, 'update_payment_details'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function check_permission()
|
||||
{
|
||||
return is_user_logged_in();
|
||||
}
|
||||
|
||||
public function get_dashboard(WP_REST_Request $request)
|
||||
{
|
||||
global $wpdb;
|
||||
$user_id = get_current_user_id();
|
||||
$table = $wpdb->prefix . 'woonoow_affiliates';
|
||||
|
||||
$affiliate = $wpdb->get_row($wpdb->prepare("SELECT * FROM $table WHERE user_id = %d", $user_id), ARRAY_A);
|
||||
|
||||
if (!$affiliate) {
|
||||
return new \WP_Error('not_found', 'Affiliate profile not found', ['status' => 404]);
|
||||
}
|
||||
|
||||
// Get global default rate
|
||||
$global_rate = (float) get_option('woonoow_affiliate_default_rate', 10);
|
||||
|
||||
// Use custom rate if set, otherwise global
|
||||
$effective_rate = !empty($affiliate['custom_commission_rate'])
|
||||
? (float) $affiliate['custom_commission_rate']
|
||||
: $global_rate;
|
||||
|
||||
$affiliate['global_commission_rate'] = $global_rate;
|
||||
$affiliate['commission_rate'] = $effective_rate;
|
||||
|
||||
return rest_ensure_response($affiliate);
|
||||
}
|
||||
|
||||
public function apply_affiliate(WP_REST_Request $request)
|
||||
{
|
||||
global $wpdb;
|
||||
$user_id = get_current_user_id();
|
||||
$table = $wpdb->prefix . 'woonoow_affiliates';
|
||||
|
||||
// Check if already applied
|
||||
$exists = $wpdb->get_var($wpdb->prepare("SELECT id FROM $table WHERE user_id = %d", $user_id));
|
||||
if ($exists) {
|
||||
return new \WP_Error('exists', 'You have already applied.', ['status' => 400]);
|
||||
}
|
||||
|
||||
// Generate simple code
|
||||
$user = wp_get_current_user();
|
||||
$referral_code = strtolower(preg_replace('/[^a-zA-Z0-9]/', '', $user->user_login)) . wp_generate_password(4, false);
|
||||
|
||||
$auto_approve = get_option('woonoow_affiliate_auto_approve', false);
|
||||
$status = $auto_approve ? 'active' : 'pending';
|
||||
|
||||
$wpdb->insert($table, [
|
||||
'user_id' => $user_id,
|
||||
'referral_code' => $referral_code,
|
||||
'commission_rate' => get_option('woonoow_affiliate_default_rate', 10), // 10% default
|
||||
'status' => $status
|
||||
]);
|
||||
|
||||
// Trigger email notification for admin
|
||||
$admin_email = get_option('admin_email');
|
||||
do_action('woonoow/email/trigger', 'affiliate_application_received', $admin_email, [
|
||||
'affiliate_name' => $user->display_name,
|
||||
'customer_email' => $user->user_email
|
||||
]);
|
||||
|
||||
if ($auto_approve) {
|
||||
do_action('woonoow/email/trigger', 'affiliate_application_approved', $user->user_email, [
|
||||
'affiliate_name' => $user->display_name,
|
||||
'customer_email' => $user->user_email,
|
||||
'referral_code' => $referral_code
|
||||
]);
|
||||
}
|
||||
|
||||
return rest_ensure_response(['success' => true, 'status' => $status, 'referral_code' => $referral_code]);
|
||||
}
|
||||
|
||||
public function get_referrals(WP_REST_Request $request)
|
||||
{
|
||||
global $wpdb;
|
||||
$user_id = get_current_user_id();
|
||||
$affiliates_table = $wpdb->prefix . 'woonoow_affiliates';
|
||||
$referrals_table = $wpdb->prefix . 'woonoow_referrals';
|
||||
|
||||
$affiliate = $wpdb->get_row($wpdb->prepare("SELECT id FROM $affiliates_table WHERE user_id = %d", $user_id));
|
||||
if (!$affiliate) {
|
||||
return rest_ensure_response([]);
|
||||
}
|
||||
|
||||
$referrals = $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT r.*,
|
||||
COALESCE(NULLIF(r.cancelled_reason, ''), NULL) as cancelled_reason,
|
||||
COALESCE(r.approved_at, r.created_at) as approved_at
|
||||
FROM $referrals_table r
|
||||
WHERE r.affiliate_id = %d
|
||||
ORDER BY r.created_at DESC",
|
||||
$affiliate->id
|
||||
), ARRAY_A);
|
||||
return rest_ensure_response($referrals);
|
||||
}
|
||||
|
||||
public function get_payouts(WP_REST_Request $request)
|
||||
{
|
||||
global $wpdb;
|
||||
$user_id = get_current_user_id();
|
||||
$affiliates_table = $wpdb->prefix . 'woonoow_affiliates';
|
||||
$payouts_table = $wpdb->prefix . 'woonoow_affiliate_payouts';
|
||||
|
||||
$affiliate = $wpdb->get_row($wpdb->prepare("SELECT id FROM $affiliates_table WHERE user_id = %d", $user_id));
|
||||
if (!$affiliate) {
|
||||
return rest_ensure_response([]);
|
||||
}
|
||||
|
||||
$payouts = $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT id, amount, currency, method, status, notes, created_at, completed_at
|
||||
FROM $payouts_table
|
||||
WHERE affiliate_id = %d
|
||||
ORDER BY created_at DESC",
|
||||
$affiliate->id
|
||||
), ARRAY_A);
|
||||
|
||||
return rest_ensure_response($payouts);
|
||||
}
|
||||
|
||||
public function get_payment_details(WP_REST_Request $request)
|
||||
{
|
||||
global $wpdb;
|
||||
$user_id = get_current_user_id();
|
||||
$table = $wpdb->prefix . 'woonoow_affiliates';
|
||||
|
||||
$affiliate = $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT payment_method, payment_details FROM $table WHERE user_id = %d",
|
||||
$user_id
|
||||
));
|
||||
|
||||
if (!$affiliate) {
|
||||
return new \WP_Error('not_found', 'Affiliate not found', ['status' => 404]);
|
||||
}
|
||||
|
||||
$payment_details = $affiliate->payment_details ? json_decode($affiliate->payment_details, true) : [];
|
||||
|
||||
return rest_ensure_response([
|
||||
'payment_method' => $affiliate->payment_method ?: '',
|
||||
'payment_details' => $payment_details ?: new \stdClass()
|
||||
]);
|
||||
}
|
||||
|
||||
public function update_payment_details(WP_REST_Request $request)
|
||||
{
|
||||
global $wpdb;
|
||||
$user_id = get_current_user_id();
|
||||
$table = $wpdb->prefix . 'woonoow_affiliates';
|
||||
|
||||
// Get allowed payment methods from settings
|
||||
$settings = get_option('woonoow_module_affiliate_settings', []);
|
||||
$allowed_methods = $settings['woonoow_affiliate_payment_methods'] ?? ['bank_transfer'];
|
||||
|
||||
$payment_method = sanitize_text_field($request->get_param('payment_method') ?: '');
|
||||
$payment_details_raw = $request->get_param('payment_details') ?: [];
|
||||
|
||||
// Validate payment method is allowed
|
||||
if (!in_array($payment_method, $allowed_methods)) {
|
||||
return new \WP_Error(
|
||||
'invalid_payment_method',
|
||||
'This payment method is not available. Please contact admin.',
|
||||
['status' => 400]
|
||||
);
|
||||
}
|
||||
|
||||
// Sanitize payment details based on method
|
||||
$sanitized_details = self::sanitize_payment_details($payment_method, $payment_details_raw);
|
||||
|
||||
$result = $wpdb->update(
|
||||
$table,
|
||||
[
|
||||
'payment_method' => $payment_method,
|
||||
'payment_details' => json_encode($sanitized_details)
|
||||
],
|
||||
['user_id' => $user_id]
|
||||
);
|
||||
|
||||
if ($result === false) {
|
||||
return new \WP_Error('update_failed', 'Failed to update payment details', ['status' => 500]);
|
||||
}
|
||||
|
||||
return rest_ensure_response([
|
||||
'success' => true,
|
||||
'payment_method' => $payment_method,
|
||||
'payment_details' => $sanitized_details
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize payment details based on payment method
|
||||
*/
|
||||
private static function sanitize_payment_details($method, $details)
|
||||
{
|
||||
$sanitized = [];
|
||||
|
||||
switch ($method) {
|
||||
case 'bank_transfer':
|
||||
$sanitized['bank_name'] = sanitize_text_field($details['bank_name'] ?? '');
|
||||
$sanitized['account_number'] = sanitize_text_field($details['account_number'] ?? '');
|
||||
$sanitized['account_holder'] = sanitize_text_field($details['account_holder'] ?? '');
|
||||
$sanitized['swift_code'] = sanitize_text_field($details['swift_code'] ?? '');
|
||||
$sanitized['bank_address'] = sanitize_text_field($details['bank_address'] ?? '');
|
||||
break;
|
||||
|
||||
case 'paypal':
|
||||
case 'wise':
|
||||
case 'skrill':
|
||||
case 'payoneer':
|
||||
$sanitized['email'] = sanitize_email($details['email'] ?? '');
|
||||
$sanitized['name'] = sanitize_text_field($details['name'] ?? '');
|
||||
break;
|
||||
|
||||
case 'custom':
|
||||
$sanitized['notes'] = sanitize_textarea_field($details['notes'] ?? '');
|
||||
break;
|
||||
}
|
||||
|
||||
return $sanitized;
|
||||
}
|
||||
}
|
||||
@@ -191,6 +191,14 @@ class Routes
|
||||
$onboarding_controller = new OnboardingController();
|
||||
$onboarding_controller->register_routes();
|
||||
|
||||
// Affiliate Admin controller
|
||||
$affiliate_admin_controller = new \WooNooW\Api\Controllers\AffiliateAdminController();
|
||||
$affiliate_admin_controller->register_routes();
|
||||
|
||||
// Affiliate Customer controller
|
||||
$affiliate_customer_controller = new \WooNooW\Api\Controllers\AffiliateCustomerController();
|
||||
$affiliate_customer_controller->register_routes();
|
||||
|
||||
// Frontend controllers (customer-facing)
|
||||
ShopController::register_routes();
|
||||
FrontendCartController::register_routes();
|
||||
|
||||
@@ -57,6 +57,7 @@ class ModuleRegistry
|
||||
'category' => 'marketing',
|
||||
'icon' => 'users',
|
||||
'default_enabled' => false,
|
||||
'has_settings' => true,
|
||||
'features' => [
|
||||
__('Referral tracking', 'woonoow'),
|
||||
__('Commission management', 'woonoow'),
|
||||
|
||||
252
includes/Modules/Affiliate/AffiliateLifecycle.php
Normal file
252
includes/Modules/Affiliate/AffiliateLifecycle.php
Normal file
@@ -0,0 +1,252 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Affiliate Lifecycle
|
||||
*
|
||||
* Handles order status changes (refunds, cancellations) and auto-approvals.
|
||||
*
|
||||
* @package WooNooW\Modules\Affiliate
|
||||
*/
|
||||
|
||||
namespace WooNooW\Modules\Affiliate;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
class AffiliateLifecycle
|
||||
{
|
||||
/**
|
||||
* Initialize lifecycle hooks
|
||||
*/
|
||||
public static function init()
|
||||
{
|
||||
// Cancel/Revert on order refund or cancellation
|
||||
add_action('woocommerce_order_status_refunded', [__CLASS__, 'handle_order_cancelled']);
|
||||
add_action('woocommerce_order_status_cancelled', [__CLASS__, 'handle_order_cancelled']);
|
||||
add_action('woocommerce_order_status_failed', [__CLASS__, 'handle_order_cancelled']);
|
||||
|
||||
// Handle order completion - immediate approval
|
||||
add_action('woocommerce_order_status_completed', [__CLASS__, 'handle_order_completed']);
|
||||
|
||||
// HPOS compatible hooks
|
||||
add_action('woocommerce_order_status_changed', [__CLASS__, 'handle_hpos_status_changed'], 10, 4);
|
||||
add_action('woocommerce_update_order', [__CLASS__, 'handle_order_updated'], 10, 2);
|
||||
|
||||
// Handle order deletion (trash + permanent delete)
|
||||
add_action('before_delete_post', [__CLASS__, 'handle_order_deleted'], 10, 1);
|
||||
add_action('woocommerce_delete_order', [__CLASS__, 'handle_order_deleted'], 10, 1);
|
||||
|
||||
// Action Scheduler Hook for auto-approval
|
||||
add_action('woonoow_approve_referral', [__CLASS__, 'auto_approve_referral']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle cancelled, refunded, or failed orders
|
||||
*/
|
||||
public static function handle_order_cancelled($order_id)
|
||||
{
|
||||
global $wpdb;
|
||||
$referrals_table = $wpdb->prefix . 'woonoow_referrals';
|
||||
|
||||
// Get the order to determine the reason
|
||||
$order = wc_get_order($order_id);
|
||||
$reason = 'order_cancelled';
|
||||
if ($order) {
|
||||
$status = $order->get_status();
|
||||
$reason = 'order_' . $status;
|
||||
}
|
||||
|
||||
// Find pending or approved referral
|
||||
$referral = $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT * FROM $referrals_table WHERE order_id = %d AND status IN ('pending', 'approved')",
|
||||
$order_id
|
||||
));
|
||||
|
||||
if ($referral) {
|
||||
// If was already approved, this is a clawback - decrease affiliate earnings
|
||||
if ($referral->status === 'approved') {
|
||||
$affiliates_table = $wpdb->prefix . 'woonoow_affiliates';
|
||||
$wpdb->query($wpdb->prepare(
|
||||
"UPDATE $affiliates_table SET total_earnings = total_earnings - %f WHERE id = %d",
|
||||
$referral->commission_amount,
|
||||
$referral->affiliate_id
|
||||
));
|
||||
}
|
||||
|
||||
// Update status to rejected with reason
|
||||
$wpdb->update(
|
||||
$referrals_table,
|
||||
[
|
||||
'status' => 'rejected',
|
||||
'cancelled_reason' => $reason,
|
||||
'cancelled_at' => current_time('mysql')
|
||||
],
|
||||
['id' => $referral->id]
|
||||
);
|
||||
|
||||
// Unschedule action if action scheduler exists
|
||||
if (function_exists('as_unschedule_all_actions')) {
|
||||
as_unschedule_all_actions('woonoow_approve_referral', ['referral_id' => $referral->id], 'woonoow_affiliate');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle order completion - check if referral should be approved
|
||||
*/
|
||||
public static function handle_order_completed($order_id)
|
||||
{
|
||||
global $wpdb;
|
||||
$referrals_table = $wpdb->prefix . 'woonoow_referrals';
|
||||
|
||||
// Find pending referral for this order
|
||||
$referral = $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT * FROM $referrals_table WHERE order_id = %d AND status = 'pending'",
|
||||
$order_id
|
||||
));
|
||||
|
||||
if (!$referral) return;
|
||||
|
||||
// Check if holding period is 0 (immediate approval on completion)
|
||||
$holding_period = (int) get_option('woonoow_affiliate_holding_period', 14);
|
||||
|
||||
if ($holding_period === 0) {
|
||||
// Immediate approval
|
||||
self::auto_approve_referral($referral->id);
|
||||
} else {
|
||||
// If order was completed BEFORE the scheduled action time, approve now
|
||||
// Otherwise, the scheduled action will approve later
|
||||
// Check if the scheduled action time has already passed
|
||||
$approval_time = strtotime($referral->created_at) + ($holding_period * DAY_IN_SECONDS);
|
||||
|
||||
if (time() >= $approval_time) {
|
||||
self::auto_approve_referral($referral->id);
|
||||
}
|
||||
// If not, the scheduled Action Scheduler job will handle it
|
||||
}
|
||||
|
||||
// Cancel the scheduled auto-approval since we're handling it now
|
||||
if (function_exists('as_unschedule_all_actions')) {
|
||||
as_unschedule_all_actions('woonoow_approve_referral', ['referral_id' => $referral->id], 'woonoow_affiliate');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle HPOS order status change
|
||||
*/
|
||||
public static function handle_hpos_status_changed($order_id, $from_status, $to_status, $order)
|
||||
{
|
||||
if ($to_status === 'completed') {
|
||||
self::handle_order_completed($order_id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle order update (HPOS compatible)
|
||||
*/
|
||||
public static function handle_order_updated($order_id, $order)
|
||||
{
|
||||
if (!$order) return;
|
||||
|
||||
// Check if order was just completed
|
||||
$status = $order->get_status();
|
||||
if ($status === 'completed') {
|
||||
self::handle_order_completed($order_id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle order deletion (permanent delete or trash)
|
||||
*/
|
||||
public static function handle_order_deleted($order_id)
|
||||
{
|
||||
// Check if this is a WooCommerce order
|
||||
$order = wc_get_order($order_id);
|
||||
if (!$order) return;
|
||||
|
||||
// Only process shop orders
|
||||
$post_type = get_post_type($order_id);
|
||||
if ($post_type !== 'shop_order') return;
|
||||
|
||||
global $wpdb;
|
||||
$referrals_table = $wpdb->prefix . 'woonoow_referrals';
|
||||
|
||||
// Find any referral for this order
|
||||
$referral = $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT * FROM $referrals_table WHERE order_id = %d",
|
||||
$order_id
|
||||
));
|
||||
|
||||
if ($referral) {
|
||||
// If was already approved, this is a clawback - decrease affiliate earnings
|
||||
if ($referral->status === 'approved') {
|
||||
$affiliates_table = $wpdb->prefix . 'woonoow_affiliates';
|
||||
$wpdb->query($wpdb->prepare(
|
||||
"UPDATE $affiliates_table SET total_earnings = total_earnings - %f WHERE id = %d",
|
||||
$referral->commission_amount,
|
||||
$referral->affiliate_id
|
||||
));
|
||||
}
|
||||
|
||||
// Mark as rejected with "order_deleted" reason
|
||||
$wpdb->update(
|
||||
$referrals_table,
|
||||
[
|
||||
'status' => 'rejected',
|
||||
'cancelled_reason' => 'order_deleted',
|
||||
'cancelled_at' => current_time('mysql')
|
||||
],
|
||||
['id' => $referral->id]
|
||||
);
|
||||
|
||||
// Unschedule any pending approval action
|
||||
if (function_exists('as_unschedule_all_actions')) {
|
||||
as_unschedule_all_actions('woonoow_approve_referral', ['referral_id' => $referral->id], 'woonoow_affiliate');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Action Scheduler callback for auto-approving a referral after the holding period
|
||||
*/
|
||||
public static function auto_approve_referral($referral_id)
|
||||
{
|
||||
global $wpdb;
|
||||
$referrals_table = $wpdb->prefix . 'woonoow_referrals';
|
||||
$affiliates_table = $wpdb->prefix . 'woonoow_affiliates';
|
||||
|
||||
// Find pending referral
|
||||
$referral = $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT * FROM $referrals_table WHERE id = %d AND status = 'pending'",
|
||||
$referral_id
|
||||
));
|
||||
|
||||
if (!$referral) return; // Already processed or deleted
|
||||
|
||||
// Double check order status
|
||||
$order = wc_get_order($referral->order_id);
|
||||
if (!$order || in_array($order->get_status(), ['refunded', 'cancelled', 'failed'])) {
|
||||
self::handle_order_cancelled($referral->order_id);
|
||||
return;
|
||||
}
|
||||
|
||||
// Approve referral
|
||||
$wpdb->update(
|
||||
$referrals_table,
|
||||
[
|
||||
'status' => 'approved',
|
||||
'approved_at' => current_time('mysql')
|
||||
],
|
||||
['id' => $referral_id]
|
||||
);
|
||||
|
||||
// Update Affiliate totals
|
||||
$wpdb->query($wpdb->prepare(
|
||||
"UPDATE $affiliates_table SET
|
||||
total_referrals = total_referrals + 1,
|
||||
total_earnings = total_earnings + %f
|
||||
WHERE id = %d",
|
||||
$referral->commission_amount,
|
||||
$referral->affiliate_id
|
||||
));
|
||||
}
|
||||
}
|
||||
156
includes/Modules/Affiliate/AffiliateManager.php
Normal file
156
includes/Modules/Affiliate/AffiliateManager.php
Normal file
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Affiliate Manager
|
||||
*
|
||||
* Handles database table creation and core management.
|
||||
*
|
||||
* @package WooNooW\Modules\Affiliate
|
||||
*/
|
||||
|
||||
namespace WooNooW\Modules\Affiliate;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
class AffiliateManager
|
||||
{
|
||||
private static $affiliates_table = 'woonoow_affiliates';
|
||||
private static $referrals_table = 'woonoow_referrals';
|
||||
private static $payouts_table = 'woonoow_affiliate_payouts';
|
||||
|
||||
/**
|
||||
* Initialize
|
||||
*/
|
||||
public static function init()
|
||||
{
|
||||
// Initialization logic for the manager if needed
|
||||
}
|
||||
|
||||
/**
|
||||
* Create database tables
|
||||
*/
|
||||
public static function create_tables()
|
||||
{
|
||||
global $wpdb;
|
||||
$charset_collate = $wpdb->get_charset_collate();
|
||||
|
||||
$affiliates_table = $wpdb->prefix . self::$affiliates_table;
|
||||
$referrals_table = $wpdb->prefix . self::$referrals_table;
|
||||
$payouts_table = $wpdb->prefix . self::$payouts_table;
|
||||
|
||||
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
|
||||
|
||||
// Affiliates Table
|
||||
$sql_affiliates = "CREATE TABLE $affiliates_table (
|
||||
id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
user_id bigint(20) UNSIGNED NOT NULL,
|
||||
referral_code varchar(50) NOT NULL,
|
||||
coupon_id bigint(20) UNSIGNED DEFAULT NULL,
|
||||
commission_rate decimal(10,2) NOT NULL DEFAULT '0.00',
|
||||
custom_commission_rate decimal(10,2) DEFAULT NULL,
|
||||
status varchar(20) NOT NULL DEFAULT 'pending',
|
||||
total_referrals int(11) NOT NULL DEFAULT 0,
|
||||
total_earnings decimal(19,4) NOT NULL DEFAULT '0.0000',
|
||||
paid_earnings decimal(19,4) NOT NULL DEFAULT '0.0000',
|
||||
payment_bank_name varchar(100) DEFAULT NULL,
|
||||
payment_bank_account varchar(100) DEFAULT NULL,
|
||||
payment_email varchar(100) DEFAULT NULL,
|
||||
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY user_id (user_id),
|
||||
UNIQUE KEY referral_code (referral_code),
|
||||
KEY status (status)
|
||||
) $charset_collate;";
|
||||
|
||||
dbDelta($sql_affiliates);
|
||||
|
||||
// Add custom_commission_rate column if it doesn't exist (for existing installations)
|
||||
if ($wpdb->get_var("SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = '$affiliates_table' AND COLUMN_NAME = 'custom_commission_rate'") == 0) {
|
||||
$wpdb->query("ALTER TABLE $affiliates_table ADD COLUMN custom_commission_rate decimal(10,2) DEFAULT NULL AFTER commission_rate");
|
||||
}
|
||||
|
||||
// Add payment details columns if they don't exist (for existing installations)
|
||||
$payment_columns = ['payment_bank_name', 'payment_bank_account', 'payment_email'];
|
||||
foreach ($payment_columns as $col) {
|
||||
if ($wpdb->get_var("SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = '$affiliates_table' AND COLUMN_NAME = '$col'") == 0) {
|
||||
$col_def = $col === 'payment_email' ? "varchar(100) DEFAULT NULL" : "varchar(100) DEFAULT NULL";
|
||||
$wpdb->query("ALTER TABLE $affiliates_table ADD COLUMN $col $col_def AFTER paid_earnings");
|
||||
}
|
||||
}
|
||||
|
||||
// Add flexible payment_details JSON column
|
||||
if ($wpdb->get_var("SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = '$affiliates_table' AND COLUMN_NAME = 'payment_details'") == 0) {
|
||||
$wpdb->query("ALTER TABLE $affiliates_table ADD COLUMN payment_details longtext DEFAULT NULL AFTER payment_email");
|
||||
}
|
||||
|
||||
// Add payment_method column
|
||||
if ($wpdb->get_var("SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = '$affiliates_table' AND COLUMN_NAME = 'payment_method'") == 0) {
|
||||
$wpdb->query("ALTER TABLE $affiliates_table ADD COLUMN payment_method varchar(50) DEFAULT NULL AFTER payment_details");
|
||||
}
|
||||
|
||||
// Referrals Table
|
||||
$sql_referrals = "CREATE TABLE $referrals_table (
|
||||
id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
affiliate_id bigint(20) UNSIGNED NOT NULL,
|
||||
order_id bigint(20) UNSIGNED NOT NULL,
|
||||
customer_id bigint(20) UNSIGNED DEFAULT NULL,
|
||||
commission_amount decimal(19,4) NOT NULL DEFAULT '0.0000',
|
||||
currency varchar(10) NOT NULL DEFAULT 'USD',
|
||||
status varchar(20) NOT NULL DEFAULT 'pending',
|
||||
cancelled_reason varchar(100) DEFAULT NULL,
|
||||
cancelled_at datetime DEFAULT NULL,
|
||||
utm_source varchar(100) DEFAULT NULL,
|
||||
utm_medium varchar(100) DEFAULT NULL,
|
||||
utm_campaign varchar(255) DEFAULT NULL,
|
||||
utm_content varchar(255) DEFAULT NULL,
|
||||
utm_term varchar(255) DEFAULT NULL,
|
||||
referrer_url varchar(500) DEFAULT NULL,
|
||||
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
approved_at datetime DEFAULT NULL,
|
||||
paid_at datetime DEFAULT NULL,
|
||||
PRIMARY KEY (id),
|
||||
KEY affiliate_id (affiliate_id),
|
||||
KEY order_id (order_id),
|
||||
KEY status (status)
|
||||
) $charset_collate;";
|
||||
|
||||
dbDelta($sql_referrals);
|
||||
|
||||
// Add cancelled columns if they don't exist (for existing installations)
|
||||
if ($wpdb->get_var("SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = '$referrals_table' AND COLUMN_NAME = 'cancelled_reason'") == 0) {
|
||||
$wpdb->query("ALTER TABLE $referrals_table ADD COLUMN cancelled_reason varchar(100) DEFAULT NULL AFTER status");
|
||||
}
|
||||
if ($wpdb->get_var("SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = '$referrals_table' AND COLUMN_NAME = 'cancelled_at'") == 0) {
|
||||
$wpdb->query("ALTER TABLE $referrals_table ADD COLUMN cancelled_at datetime DEFAULT NULL AFTER cancelled_reason");
|
||||
}
|
||||
|
||||
// Add UTM columns if they don't exist (for existing installations)
|
||||
$utm_columns = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term', 'referrer_url'];
|
||||
foreach ($utm_columns as $col) {
|
||||
if ($wpdb->get_var("SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = '$referrals_table' AND COLUMN_NAME = '$col'") == 0) {
|
||||
$col_def = $col === 'referrer_url' ? "varchar(500) DEFAULT NULL" : ($col === 'utm_campaign' || $col === 'utm_content' || $col === 'utm_term' ? "varchar(255) DEFAULT NULL" : "varchar(100) DEFAULT NULL");
|
||||
$after_col = $col === 'utm_source' ? 'cancelled_at' : ($col === 'utm_medium' ? 'utm_source' : ($col === 'utm_campaign' ? 'utm_medium' : ($col === 'utm_content' ? 'utm_campaign' : ($col === 'utm_term' ? 'utm_content' : 'utm_term'))));
|
||||
$wpdb->query("ALTER TABLE $referrals_table ADD COLUMN $col $col_def AFTER cancelled_at");
|
||||
}
|
||||
}
|
||||
|
||||
// Payouts Table
|
||||
$sql_payouts = "CREATE TABLE $payouts_table (
|
||||
id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
affiliate_id bigint(20) UNSIGNED NOT NULL,
|
||||
amount decimal(19,4) NOT NULL,
|
||||
currency varchar(10) NOT NULL DEFAULT 'USD',
|
||||
method varchar(50) NOT NULL,
|
||||
status varchar(20) NOT NULL DEFAULT 'pending',
|
||||
notes text DEFAULT NULL,
|
||||
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
completed_at datetime DEFAULT NULL,
|
||||
PRIMARY KEY (id),
|
||||
KEY affiliate_id (affiliate_id),
|
||||
KEY status (status)
|
||||
) $charset_collate;";
|
||||
|
||||
dbDelta($sql_payouts);
|
||||
}
|
||||
}
|
||||
136
includes/Modules/Affiliate/AffiliateModule.php
Normal file
136
includes/Modules/Affiliate/AffiliateModule.php
Normal file
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Affiliate Module Bootstrap
|
||||
*
|
||||
* @package WooNooW\Modules\Affiliate
|
||||
*/
|
||||
|
||||
namespace WooNooW\Modules\Affiliate;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
use WooNooW\Core\ModuleRegistry;
|
||||
|
||||
class AffiliateModule
|
||||
{
|
||||
/**
|
||||
* Initialize the affiliate module
|
||||
*/
|
||||
public static function init()
|
||||
{
|
||||
self::maybe_init_manager();
|
||||
|
||||
// Install tables on module enable
|
||||
add_action('woonoow/module/enabled', [__CLASS__, 'on_module_enabled']);
|
||||
|
||||
// Run migrations on admin init
|
||||
add_action('admin_init', [__CLASS__, 'run_migrations']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize manager if module is enabled
|
||||
*/
|
||||
public static function maybe_init_manager()
|
||||
{
|
||||
if (ModuleRegistry::is_enabled('affiliate')) {
|
||||
self::ensure_tables();
|
||||
AffiliateManager::init();
|
||||
AffiliateTracker::init();
|
||||
AffiliateLifecycle::init();
|
||||
AffiliateSettings::init();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure database tables exist and have all required columns
|
||||
*/
|
||||
private static function ensure_tables()
|
||||
{
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . 'woonoow_affiliates';
|
||||
|
||||
// Check if table exists
|
||||
if ($wpdb->get_var("SHOW TABLES LIKE '$table'") !== $table) {
|
||||
AffiliateManager::create_tables();
|
||||
} else {
|
||||
// Run migrations for existing tables
|
||||
self::migrate_existing_tables();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate existing tables to add missing columns
|
||||
*/
|
||||
private static function migrate_existing_tables()
|
||||
{
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . 'woonoow_affiliates';
|
||||
$referrals_table = $wpdb->prefix . 'woonoow_referrals';
|
||||
|
||||
// Add custom_commission_rate column if missing
|
||||
if ($wpdb->get_var("SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = '$table' AND COLUMN_NAME = 'custom_commission_rate'") == 0) {
|
||||
$wpdb->query("ALTER TABLE $table ADD COLUMN custom_commission_rate decimal(10,2) DEFAULT NULL AFTER commission_rate");
|
||||
}
|
||||
|
||||
// Add payment detail columns if missing
|
||||
$payment_columns = ['payment_bank_name', 'payment_bank_account', 'payment_email'];
|
||||
foreach ($payment_columns as $col) {
|
||||
if ($wpdb->get_var("SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = '$table' AND COLUMN_NAME = '$col'") == 0) {
|
||||
$wpdb->query("ALTER TABLE $table ADD COLUMN $col varchar(100) DEFAULT NULL AFTER paid_earnings");
|
||||
}
|
||||
}
|
||||
|
||||
// Add flexible payment_details and payment_method columns
|
||||
if ($wpdb->get_var("SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = '$table' AND COLUMN_NAME = 'payment_details'") == 0) {
|
||||
$wpdb->query("ALTER TABLE $table ADD COLUMN payment_details longtext DEFAULT NULL AFTER payment_email");
|
||||
}
|
||||
if ($wpdb->get_var("SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = '$table' AND COLUMN_NAME = 'payment_method'") == 0) {
|
||||
$wpdb->query("ALTER TABLE $table ADD COLUMN payment_method varchar(50) DEFAULT NULL AFTER payment_details");
|
||||
}
|
||||
|
||||
// Add referral attribution columns if missing.
|
||||
if ($wpdb->get_var("SHOW TABLES LIKE '$referrals_table'") === $referrals_table) {
|
||||
$referral_columns = [
|
||||
'cancelled_reason' => ['definition' => 'varchar(100) DEFAULT NULL', 'after' => 'status'],
|
||||
'cancelled_at' => ['definition' => 'datetime DEFAULT NULL', 'after' => 'cancelled_reason'],
|
||||
'utm_source' => ['definition' => 'varchar(100) DEFAULT NULL', 'after' => 'cancelled_at'],
|
||||
'utm_medium' => ['definition' => 'varchar(100) DEFAULT NULL', 'after' => 'utm_source'],
|
||||
'utm_campaign' => ['definition' => 'varchar(255) DEFAULT NULL', 'after' => 'utm_medium'],
|
||||
'utm_content' => ['definition' => 'varchar(255) DEFAULT NULL', 'after' => 'utm_campaign'],
|
||||
'utm_term' => ['definition' => 'varchar(255) DEFAULT NULL', 'after' => 'utm_content'],
|
||||
'referrer_url' => ['definition' => 'varchar(500) DEFAULT NULL', 'after' => 'utm_term'],
|
||||
];
|
||||
|
||||
foreach ($referral_columns as $column => $schema) {
|
||||
if ($wpdb->get_var("SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = '$referrals_table' AND COLUMN_NAME = '$column'") == 0) {
|
||||
$wpdb->query("ALTER TABLE $referrals_table ADD COLUMN $column {$schema['definition']} AFTER {$schema['after']}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle module enable
|
||||
*/
|
||||
public static function on_module_enabled($module_id)
|
||||
{
|
||||
if ($module_id === 'affiliate') {
|
||||
AffiliateManager::create_tables();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run migrations (called on admin_init)
|
||||
*/
|
||||
public static function run_migrations()
|
||||
{
|
||||
// Only run once per session (check transient)
|
||||
if (get_transient('woonoow_affiliate_migrated')) {
|
||||
return;
|
||||
}
|
||||
|
||||
self::migrate_existing_tables();
|
||||
set_transient('woonoow_affiliate_migrated', true, DAY_IN_SECONDS);
|
||||
}
|
||||
}
|
||||
71
includes/Modules/Affiliate/AffiliateSettings.php
Normal file
71
includes/Modules/Affiliate/AffiliateSettings.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
/**
|
||||
* Affiliate Module Settings Schema
|
||||
*
|
||||
* Defines the settings schema for the Affiliate module.
|
||||
*
|
||||
* @package WooNooW\Modules\Affiliate
|
||||
*/
|
||||
|
||||
namespace WooNooW\Modules\Affiliate;
|
||||
|
||||
class AffiliateSettings {
|
||||
|
||||
public static function init() {
|
||||
// Register settings schema
|
||||
add_filter('woonoow/module_settings_schema', [__CLASS__, 'register_schema']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register affiliate settings schema
|
||||
*/
|
||||
public static function register_schema($schemas) {
|
||||
$schemas['affiliate'] = [
|
||||
'woonoow_affiliate_default_rate' => [
|
||||
'type' => 'number',
|
||||
'label' => __('Default Commission Rate (%)', 'woonoow'),
|
||||
'description' => __('The default commission rate percentage for affiliates.', 'woonoow'),
|
||||
'placeholder' => '10',
|
||||
'default' => 10,
|
||||
'min' => 0,
|
||||
'max' => 100,
|
||||
],
|
||||
'woonoow_affiliate_holding_period' => [
|
||||
'type' => 'number',
|
||||
'label' => __('Holding Period (Days)', 'woonoow'),
|
||||
'description' => __('Number of days before a referral becomes eligible for payout (e.g. to account for refunds). Set to 0 for immediate approval on order completion.', 'woonoow'),
|
||||
'placeholder' => '14',
|
||||
'default' => 14,
|
||||
'min' => 0,
|
||||
],
|
||||
'woonoow_affiliate_payment_methods' => [
|
||||
'type' => 'multiselect',
|
||||
'label' => __('Available Payment Methods', 'woonoow'),
|
||||
'description' => __('Select which payment methods affiliates can use to receive payouts.', 'woonoow'),
|
||||
'options' => [
|
||||
'bank_transfer' => __('Bank Transfer', 'woonoow'),
|
||||
'paypal' => __('PayPal', 'woonoow'),
|
||||
'wise' => __('Wise', 'woonoow'),
|
||||
'skrill' => __('Skrill', 'woonoow'),
|
||||
'payoneer' => __('Payoneer', 'woonoow'),
|
||||
'custom' => __('Custom (Notes)', 'woonoow'),
|
||||
],
|
||||
'default' => ['bank_transfer'],
|
||||
],
|
||||
'woonoow_affiliate_auto_approve' => [
|
||||
'type' => 'toggle',
|
||||
'label' => __('Auto-Approve Affiliates', 'woonoow'),
|
||||
'description' => __('Automatically approve new affiliate applications.', 'woonoow'),
|
||||
'default' => false,
|
||||
],
|
||||
'woonoow_affiliate_allow_self_referral' => [
|
||||
'type' => 'toggle',
|
||||
'label' => __('Allow Self-Referrals', 'woonoow'),
|
||||
'description' => __('Allow affiliates to earn commission when their own user account places an order.', 'woonoow'),
|
||||
'default' => false,
|
||||
],
|
||||
];
|
||||
|
||||
return $schemas;
|
||||
}
|
||||
}
|
||||
416
includes/Modules/Affiliate/AffiliateTracker.php
Normal file
416
includes/Modules/Affiliate/AffiliateTracker.php
Normal file
@@ -0,0 +1,416 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Affiliate Tracker
|
||||
*
|
||||
* Handles referral tracking via cookies and order creation.
|
||||
*
|
||||
* @package WooNooW\Modules\Affiliate
|
||||
*/
|
||||
|
||||
namespace WooNooW\Modules\Affiliate;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
class AffiliateTracker
|
||||
{
|
||||
const COOKIE_NAME = 'woonoow_ref';
|
||||
const COOKIE_EXPIRES = 30; // days
|
||||
|
||||
/**
|
||||
* Initialize tracking hooks
|
||||
*/
|
||||
public static function init()
|
||||
{
|
||||
error_log('[AffiliateTracker] init() called');
|
||||
|
||||
// Intercept referral links on init
|
||||
add_action('init', [__CLASS__, 'capture_referral_link']);
|
||||
|
||||
// Hook into WooCommerce order creation (legacy checkout)
|
||||
add_action('woocommerce_checkout_order_processed', [__CLASS__, 'record_referral'], 10, 3);
|
||||
|
||||
// Hook into WooCommerce 8.3+ block checkout
|
||||
add_action('woocommerce_store_api_checkout_order_processed', [__CLASS__, 'record_referral_block'], 10, 1);
|
||||
|
||||
// Hook into new order creation (universal)
|
||||
add_action('woocommerce_new_order', [__CLASS__, 'record_referral_new_order'], 10, 2);
|
||||
|
||||
// Hook for all order status changes
|
||||
add_action('woocommerce_order_status_changed', [__CLASS__, 'handle_order_status_changed'], 10, 4);
|
||||
|
||||
// Hook for REST API order creation
|
||||
add_action('rest_after_insert_shop_order', [__CLASS__, 'handle_rest_order_created'], 10, 3);
|
||||
|
||||
error_log('[AffiliateTracker] All hooks registered');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle order status change - track completed orders
|
||||
*/
|
||||
public static function handle_order_status_changed($order_id, $from_status, $to_status, $order)
|
||||
{
|
||||
if ($to_status === 'completed') {
|
||||
self::process_order_for_referral($order_id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle REST API order creation
|
||||
*/
|
||||
public static function handle_rest_order_created($order, $request, $creating)
|
||||
{
|
||||
if ($creating) {
|
||||
self::process_order_for_referral($order->get_id());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Record referral from block checkout (Store API)
|
||||
*/
|
||||
public static function record_referral_block($order)
|
||||
{
|
||||
if (is_numeric($order)) {
|
||||
$order = wc_get_order($order);
|
||||
}
|
||||
if ($order) {
|
||||
self::process_order_for_referral($order->get_id());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Record referral from woocommerce_new_order hook
|
||||
*/
|
||||
public static function record_referral_new_order($order_id, $order)
|
||||
{
|
||||
error_log('[AffiliateTracker] woocommerce_new_order hook fired for order: ' . $order_id);
|
||||
self::process_order_for_referral($order_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Capture ?ref=CODE from URL and set cookie
|
||||
* Also captures UTM parameters for referral attribution
|
||||
*/
|
||||
public static function capture_referral_link()
|
||||
{
|
||||
error_log('[AffiliateTracker] capture_referral_link called, ref=' . ($_GET['ref'] ?? 'none'));
|
||||
|
||||
$options = [
|
||||
'expires' => time() + (self::COOKIE_EXPIRES * DAY_IN_SECONDS),
|
||||
'path' => '/',
|
||||
'secure' => is_ssl(),
|
||||
'samesite' => 'Lax'
|
||||
];
|
||||
|
||||
// Capture referral code
|
||||
if (isset($_GET['ref']) && !empty($_GET['ref'])) {
|
||||
$referral_code = sanitize_text_field($_GET['ref']);
|
||||
$result = setcookie(self::COOKIE_NAME, $referral_code, $options);
|
||||
$_COOKIE[self::COOKIE_NAME] = $referral_code;
|
||||
error_log('[AffiliateTracker] Set woonoow_ref cookie: ' . $referral_code . ', result=' . ($result ? 'true' : 'false'));
|
||||
error_log('[AffiliateTracker] Cookie options: ' . json_encode($options));
|
||||
} else {
|
||||
// Check if cookie already exists from previous visit
|
||||
if (isset($_COOKIE[self::COOKIE_NAME])) {
|
||||
error_log('[AffiliateTracker] No ref param, but existing cookie: ' . $_COOKIE[self::COOKIE_NAME]);
|
||||
} else {
|
||||
error_log('[AffiliateTracker] No ref param and no existing cookie');
|
||||
}
|
||||
}
|
||||
|
||||
// Capture UTM parameters
|
||||
$utm_params = [];
|
||||
$utm_keys = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term'];
|
||||
|
||||
foreach ($utm_keys as $key) {
|
||||
if (isset($_GET[$key]) && !empty($_GET[$key])) {
|
||||
$utm_params[$key] = sanitize_text_field($_GET[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
// Capture referrer URL
|
||||
if (isset($_SERVER['HTTP_REFERER']) && !empty($_SERVER['HTTP_REFERER'])) {
|
||||
$utm_params['referrer_url'] = esc_url_raw($_SERVER['HTTP_REFERER']);
|
||||
}
|
||||
|
||||
// Store UTM params in cookie if any captured
|
||||
if (!empty($utm_params)) {
|
||||
$utm_json = json_encode($utm_params);
|
||||
setcookie(self::COOKIE_NAME . '_utm', $utm_json, $options);
|
||||
$_COOKIE[self::COOKIE_NAME . '_utm'] = $utm_json;
|
||||
error_log('[AffiliateTracker] Set woonoow_ref_utm cookie');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get active affiliate for an order
|
||||
* Checks both coupons and cookies.
|
||||
*/
|
||||
public static function get_affiliate_for_order($order)
|
||||
{
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . 'woonoow_affiliates';
|
||||
|
||||
// 1. Check if a coupon is used that maps to an affiliate
|
||||
$used_coupons = $order->get_coupon_codes();
|
||||
if (!empty($used_coupons)) {
|
||||
foreach ($used_coupons as $code) {
|
||||
$coupon = new \WC_Coupon($code);
|
||||
if ($coupon->get_id()) {
|
||||
$affiliate = $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT * FROM $table WHERE coupon_id = %d AND status = 'active'",
|
||||
$coupon->get_id()
|
||||
));
|
||||
if ($affiliate) return $affiliate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Check referral code stored on the order by SPA checkout
|
||||
$order_referral_code = $order->get_meta('_woonoow_referral_code');
|
||||
if (!empty($order_referral_code)) {
|
||||
$affiliate = $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT * FROM $table WHERE referral_code = %s AND status = 'active'",
|
||||
sanitize_text_field($order_referral_code)
|
||||
));
|
||||
if ($affiliate) return $affiliate;
|
||||
}
|
||||
|
||||
// 3. Fallback to cookie
|
||||
if (isset($_COOKIE[self::COOKIE_NAME])) {
|
||||
$referral_code = sanitize_text_field($_COOKIE[self::COOKIE_NAME]);
|
||||
$affiliate = $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT * FROM $table WHERE referral_code = %s AND status = 'active'",
|
||||
$referral_code
|
||||
));
|
||||
if ($affiliate) return $affiliate;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get referral record for a specific order
|
||||
* Used by admin API to display affiliate info on order details
|
||||
*/
|
||||
public static function get_referral_for_order($order_id)
|
||||
{
|
||||
global $wpdb;
|
||||
$referrals_table = $wpdb->prefix . 'woonoow_referrals';
|
||||
|
||||
return $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT * FROM $referrals_table WHERE order_id = %d",
|
||||
$order_id
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Record referral when order is processed (Checkout)
|
||||
*/
|
||||
public static function record_referral($order_id, $posted_data, $order)
|
||||
{
|
||||
self::process_order_for_referral($order_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Record referral when order is created via Admin
|
||||
*/
|
||||
public static function record_referral_admin($post_id)
|
||||
{
|
||||
$order = wc_get_order($post_id);
|
||||
if ($order) {
|
||||
self::process_order_for_referral($post_id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process order and create pending referral record
|
||||
*/
|
||||
private static function process_order_for_referral($order_id)
|
||||
{
|
||||
error_log('[AffiliateTracker] process_order_for_referral() called for order: ' . $order_id);
|
||||
|
||||
global $wpdb;
|
||||
$order = wc_get_order($order_id);
|
||||
if (!$order) {
|
||||
error_log('[AffiliateTracker] Order not found: ' . $order_id);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if referral already exists for this order
|
||||
$referrals_table = $wpdb->prefix . 'woonoow_referrals';
|
||||
$exists = $wpdb->get_var($wpdb->prepare("SELECT id FROM $referrals_table WHERE order_id = %d", $order_id));
|
||||
if ($exists) {
|
||||
error_log('[AffiliateTracker] Referral already exists for order: ' . $order_id);
|
||||
return;
|
||||
}
|
||||
|
||||
$affiliate = self::get_affiliate_for_order($order);
|
||||
if (!$affiliate) {
|
||||
error_log('[AffiliateTracker] No affiliate found for order: ' . $order_id);
|
||||
return;
|
||||
}
|
||||
|
||||
error_log('[AffiliateTracker] Found affiliate: ' . $affiliate->referral_code . ' (rate: ' . ($affiliate->custom_commission_rate ?? 'null') . ')');
|
||||
|
||||
// Prevent self-referrals (unless admin override is enabled)
|
||||
$order_user_id = $order->get_user_id();
|
||||
if ((int)$order_user_id === (int)$affiliate->user_id) {
|
||||
error_log('[AffiliateTracker] Self-referral detected, blocking');
|
||||
$allow_self_referral = get_option('woonoow_affiliate_allow_self_referral', false);
|
||||
if (!$allow_self_referral) return;
|
||||
}
|
||||
|
||||
// Calculate commission
|
||||
// Priority: Product rate > Affiliate custom rate > Global default
|
||||
$global_default_rate = (float) get_option('woonoow_affiliate_default_rate', 10);
|
||||
$affiliate_custom_rate = !empty($affiliate->custom_commission_rate) ? (float) $affiliate->custom_commission_rate : null;
|
||||
|
||||
// Get order items - try WC method first, fallback to direct DB query
|
||||
$items = self::get_order_items($order_id);
|
||||
error_log('[AffiliateTracker] Order items: ' . count($items));
|
||||
$total_commission = 0;
|
||||
|
||||
if (!empty($items)) {
|
||||
foreach ($items as $item) {
|
||||
$product_id = (int) $item['product_id'];
|
||||
$line_total = (float) $item['line_total'];
|
||||
|
||||
// Check for product-specific affiliate rate
|
||||
$product_rate = get_post_meta($product_id, '_woonoow_affiliate_commission_rate', true);
|
||||
$product_enabled = get_post_meta($product_id, '_woonoow_affiliate_enabled', true) === 'yes';
|
||||
error_log('[AffiliateTracker] Product ' . $product_id . ': enabled=' . ($product_enabled ? 'yes' : 'no') . ', rate=' . $product_rate . ', line_total=' . $line_total);
|
||||
|
||||
// Determine rate for this item (only if product affiliate is enabled)
|
||||
$item_rate = null;
|
||||
if ($product_enabled && $product_rate !== '') {
|
||||
$item_rate = (float) $product_rate;
|
||||
error_log('[AffiliateTracker] Using product rate: ' . $item_rate);
|
||||
} elseif ($affiliate_custom_rate !== null) {
|
||||
$item_rate = $affiliate_custom_rate;
|
||||
error_log('[AffiliateTracker] Using affiliate rate: ' . $item_rate);
|
||||
} else {
|
||||
$item_rate = $global_default_rate;
|
||||
error_log('[AffiliateTracker] Using global rate: ' . $item_rate);
|
||||
}
|
||||
|
||||
if ($item_rate > 0 && $line_total > 0) {
|
||||
$total_commission += ($line_total * $item_rate) / 100;
|
||||
error_log('[AffiliateTracker] Added: ' . ($line_total * $item_rate / 100));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fallback to subtotal if no items (legacy)
|
||||
$subtotal = (float)$order->get_subtotal();
|
||||
$commission_rate = $affiliate_custom_rate ?? $global_default_rate;
|
||||
if ($commission_rate <= 0) return;
|
||||
$total_commission = ($subtotal * $commission_rate) / 100;
|
||||
}
|
||||
|
||||
if ($total_commission <= 0) {
|
||||
error_log('[AffiliateTracker] Commission is 0, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
error_log('[AffiliateTracker] Total commission: ' . $total_commission);
|
||||
|
||||
// Get UTM parameters from cookie
|
||||
$utm_data = [];
|
||||
if (isset($_COOKIE[self::COOKIE_NAME . '_utm'])) {
|
||||
$utm_data = json_decode($_COOKIE[self::COOKIE_NAME . '_utm'], true) ?: [];
|
||||
}
|
||||
|
||||
// Insert pending referral
|
||||
$insert_data = [
|
||||
'affiliate_id' => $affiliate->id,
|
||||
'order_id' => $order_id,
|
||||
'customer_id' => $order->get_user_id() ?: null,
|
||||
'commission_amount' => $total_commission,
|
||||
'currency' => $order->get_currency(),
|
||||
'status' => 'pending'
|
||||
];
|
||||
|
||||
// Add UTM data if available
|
||||
if (!empty($utm_data)) {
|
||||
foreach (['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term', 'referrer_url'] as $utm_key) {
|
||||
if (isset($utm_data[$utm_key])) {
|
||||
$insert_data[$utm_key] = $utm_data[$utm_key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$inserted = $wpdb->insert($referrals_table, $insert_data);
|
||||
if (!$inserted) {
|
||||
error_log('[AffiliateTracker] Failed to insert referral for order ' . $order_id . ': ' . $wpdb->last_error);
|
||||
return;
|
||||
}
|
||||
|
||||
$referral_id = $wpdb->insert_id;
|
||||
|
||||
// Fire event for notifications
|
||||
do_action('woonoow/affiliate/referral_received', $referral_id, $affiliate, $order);
|
||||
|
||||
// Trigger email notification to affiliate
|
||||
$user = get_userdata($affiliate->user_id);
|
||||
if ($user) {
|
||||
do_action('woonoow/email/trigger', 'affiliate_new_referral', $user->user_email, [
|
||||
'affiliate_name' => $user->display_name,
|
||||
'commission_amount' => $total_commission,
|
||||
'currency' => $order->get_currency(),
|
||||
'order_number' => $order->get_order_number()
|
||||
]);
|
||||
}
|
||||
|
||||
// Schedule auto-approval (e.g., 14 days) via Action Scheduler
|
||||
if (function_exists('as_schedule_single_action')) {
|
||||
$approval_days = get_option('woonoow_affiliate_holding_period', 14);
|
||||
$timestamp = time() + ($approval_days * DAY_IN_SECONDS);
|
||||
as_schedule_single_action($timestamp, 'woonoow_approve_referral', ['referral_id' => $referral_id], 'woonoow_affiliate');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get order items - direct DB query for HPOS compatibility
|
||||
*/
|
||||
private static function get_order_items($order_id)
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
// First try HPOS order_product_lookup table (more reliable with HPOS)
|
||||
// Force fresh query by using wpdb::query with direct table
|
||||
$order_id_int = (int) $order_id;
|
||||
$items = $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT product_id, product_qty, product_net_revenue as line_total
|
||||
FROM {$wpdb->prefix}wc_order_product_lookup
|
||||
WHERE order_id = %d",
|
||||
$order_id_int
|
||||
), ARRAY_A);
|
||||
|
||||
error_log('[AffiliateTracker] get_order_items() HPOS query returned: ' . count($items) . ' items for order ' . $order_id_int);
|
||||
|
||||
// If no results from HPOS table, try legacy order_items table
|
||||
if (empty($items)) {
|
||||
$items = $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT oim_product.meta_value as product_id, oim_total.meta_value as line_total
|
||||
FROM {$wpdb->prefix}woocommerce_order_items oi
|
||||
LEFT JOIN {$wpdb->prefix}woocommerce_order_itemmeta oim_product ON oi.order_item_id = oim_product.order_item_id AND oim_product.meta_key = '_product_id'
|
||||
LEFT JOIN {$wpdb->prefix}woocommerce_order_itemmeta oim_total ON oi.order_item_id = oim_total.order_item_id AND oim_total.meta_key = '_line_total'
|
||||
WHERE oi.order_id = %d AND oi.order_item_type = 'line_item'",
|
||||
$order_id
|
||||
), ARRAY_A);
|
||||
}
|
||||
|
||||
// Convert to standard format
|
||||
$result = [];
|
||||
foreach ($items as $item) {
|
||||
$product_id = isset($item['product_id']) ? (int) $item['product_id'] : 0;
|
||||
if ($product_id > 0) {
|
||||
$result[] = [
|
||||
'product_id' => $product_id,
|
||||
'line_total' => (float) ($item['line_total'] ?? 0)
|
||||
];
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user