🔧 Bug Fixes: - Fixed download limits defaulting to 5 instead of 0 for unlimited downloads - Fixed software license filename sanitization (spaces→dashes, dots→underscores, proper .zip extension) - Software downloads now show as "Test-Plugin-v2-2-0.zip" instead of "Test Plugin v2.2.0" ✨ UI/UX Enhancements: - Redesigned license key display to span full table width with FontAwesome copy icons - Added responsive CSS styling for license key rows - Integrated FontAwesome CDN for modern copy icons 🏗️ Architecture Improvements: - Added comprehensive filename sanitization in both download handler and API paths - Enhanced software license product handling for local package files - Improved error handling and logging throughout download processes 📦 Infrastructure: - Added Gitea workflows for automated releases on push to main - Created comprehensive .gitignore excluding test files and browser automation - Updated documentation with all recent improvements and technical insights 🔍 Technical Details: - Software license products served from wp-content/uploads/wpdd-packages/ - Download flow: token → process_download_by_token() → process_download() → deliver_file() - Dual path coverage for both API downloads and regular file delivery - Version placeholder system for automated deployment 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
		
			
				
	
	
		
			237 lines
		
	
	
		
			8.2 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			237 lines
		
	
	
		
			8.2 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
<?php
 | 
						|
 | 
						|
if (!defined('ABSPATH')) {
 | 
						|
    exit;
 | 
						|
}
 | 
						|
 | 
						|
class WPDD_PayPal_Payouts {
 | 
						|
    
 | 
						|
    public static function init() {
 | 
						|
        // Schedule cron event for automatic payouts
 | 
						|
        if (!wp_next_scheduled('wpdd_process_automatic_payouts')) {
 | 
						|
            wp_schedule_event(time(), 'daily', 'wpdd_process_automatic_payouts');
 | 
						|
        }
 | 
						|
        
 | 
						|
        add_action('wpdd_process_automatic_payouts', array(__CLASS__, 'process_automatic_payouts'));
 | 
						|
    }
 | 
						|
    
 | 
						|
    public static function process_automatic_payouts() {
 | 
						|
        $threshold = floatval(get_option('wpdd_payout_threshold', 0));
 | 
						|
        
 | 
						|
        if ($threshold <= 0) {
 | 
						|
            return;
 | 
						|
        }
 | 
						|
        
 | 
						|
        $creators = WPDD_Creator::get_creators_with_balance();
 | 
						|
        
 | 
						|
        foreach ($creators as $creator) {
 | 
						|
            if (floatval($creator->balance) >= $threshold && !empty($creator->paypal_email)) {
 | 
						|
                WPDD_Admin_Payouts::create_payout($creator->ID, 'automatic');
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    public static function process_payout($payout_id) {
 | 
						|
        global $wpdb;
 | 
						|
        
 | 
						|
        // Get payout details
 | 
						|
        $payout = $wpdb->get_row($wpdb->prepare(
 | 
						|
            "SELECT * FROM {$wpdb->prefix}wpdd_payouts WHERE id = %d",
 | 
						|
            $payout_id
 | 
						|
        ));
 | 
						|
        
 | 
						|
        if (!$payout) {
 | 
						|
            return array('success' => false, 'error' => 'Payout not found');
 | 
						|
        }
 | 
						|
        
 | 
						|
        // Get PayPal credentials
 | 
						|
        $mode = get_option('wpdd_paypal_mode', 'sandbox');
 | 
						|
        $client_id = get_option('wpdd_paypal_client_id');
 | 
						|
        $secret = get_option('wpdd_paypal_secret');
 | 
						|
        
 | 
						|
        if (empty($client_id) || empty($secret)) {
 | 
						|
            return array('success' => false, 'error' => 'PayPal credentials not configured');
 | 
						|
        }
 | 
						|
        
 | 
						|
        // Get access token
 | 
						|
        $token_result = self::get_access_token($client_id, $secret, $mode);
 | 
						|
        
 | 
						|
        if (!$token_result['success']) {
 | 
						|
            return array('success' => false, 'error' => $token_result['error']);
 | 
						|
        }
 | 
						|
        
 | 
						|
        $access_token = $token_result['token'];
 | 
						|
        
 | 
						|
        // Create payout batch
 | 
						|
        $batch_result = self::create_payout_batch($payout, $access_token, $mode);
 | 
						|
        
 | 
						|
        if ($batch_result['success']) {
 | 
						|
            return array(
 | 
						|
                'success' => true,
 | 
						|
                'transaction_id' => $batch_result['batch_id']
 | 
						|
            );
 | 
						|
        } else {
 | 
						|
            return array(
 | 
						|
                'success' => false,
 | 
						|
                'error' => $batch_result['error']
 | 
						|
            );
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    private static function get_access_token($client_id, $secret, $mode) {
 | 
						|
        $base_url = $mode === 'sandbox' 
 | 
						|
            ? 'https://api-m.sandbox.paypal.com' 
 | 
						|
            : 'https://api-m.paypal.com';
 | 
						|
        
 | 
						|
        $response = wp_remote_post(
 | 
						|
            $base_url . '/v1/oauth2/token',
 | 
						|
            array(
 | 
						|
                'headers' => array(
 | 
						|
                    'Authorization' => 'Basic ' . base64_encode($client_id . ':' . $secret),
 | 
						|
                    'Content-Type' => 'application/x-www-form-urlencoded'
 | 
						|
                ),
 | 
						|
                'body' => 'grant_type=client_credentials',
 | 
						|
                'timeout' => 30
 | 
						|
            )
 | 
						|
        );
 | 
						|
        
 | 
						|
        if (is_wp_error($response)) {
 | 
						|
            return array('success' => false, 'error' => $response->get_error_message());
 | 
						|
        }
 | 
						|
        
 | 
						|
        $body = json_decode(wp_remote_retrieve_body($response), true);
 | 
						|
        
 | 
						|
        if (isset($body['access_token'])) {
 | 
						|
            return array('success' => true, 'token' => $body['access_token']);
 | 
						|
        } else {
 | 
						|
            $error = isset($body['error_description']) ? $body['error_description'] : 'Failed to get access token';
 | 
						|
            return array('success' => false, 'error' => $error);
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    private static function create_payout_batch($payout, $access_token, $mode) {
 | 
						|
        $base_url = $mode === 'sandbox' 
 | 
						|
            ? 'https://api-m.sandbox.paypal.com' 
 | 
						|
            : 'https://api-m.paypal.com';
 | 
						|
        
 | 
						|
        $batch_id = 'WPDD_' . $payout->id . '_' . time();
 | 
						|
        
 | 
						|
        // Ensure currency is set, fallback to USD if empty
 | 
						|
        $currency = !empty($payout->currency) ? $payout->currency : 'USD';
 | 
						|
        
 | 
						|
        $payout_data = array(
 | 
						|
            'sender_batch_header' => array(
 | 
						|
                'sender_batch_id' => $batch_id,
 | 
						|
                'email_subject' => 'You have received a payout!',
 | 
						|
                'email_message' => 'You have received a payout from ' . get_bloginfo('name')
 | 
						|
            ),
 | 
						|
            'items' => array(
 | 
						|
                array(
 | 
						|
                    'recipient_type' => 'EMAIL',
 | 
						|
                    'amount' => array(
 | 
						|
                        'value' => number_format($payout->amount, 2, '.', ''),
 | 
						|
                        'currency' => $currency
 | 
						|
                    ),
 | 
						|
                    'receiver' => $payout->paypal_email,
 | 
						|
                    'note' => 'Payout for your sales on ' . get_bloginfo('name'),
 | 
						|
                    'sender_item_id' => 'payout_' . $payout->id
 | 
						|
                )
 | 
						|
            )
 | 
						|
        );
 | 
						|
        
 | 
						|
        // Log the payout data for debugging
 | 
						|
        error_log('WPDD PayPal Payout Data: ' . json_encode($payout_data));
 | 
						|
        
 | 
						|
        $response = wp_remote_post(
 | 
						|
            $base_url . '/v1/payments/payouts',
 | 
						|
            array(
 | 
						|
                'headers' => array(
 | 
						|
                    'Authorization' => 'Bearer ' . $access_token,
 | 
						|
                    'Content-Type' => 'application/json'
 | 
						|
                ),
 | 
						|
                'body' => json_encode($payout_data),
 | 
						|
                'timeout' => 30
 | 
						|
            )
 | 
						|
        );
 | 
						|
        
 | 
						|
        if (is_wp_error($response)) {
 | 
						|
            return array('success' => false, 'error' => $response->get_error_message());
 | 
						|
        }
 | 
						|
        
 | 
						|
        $response_code = wp_remote_retrieve_response_code($response);
 | 
						|
        $body = json_decode(wp_remote_retrieve_body($response), true);
 | 
						|
        
 | 
						|
        if ($response_code === 201 && isset($body['batch_header']['payout_batch_id'])) {
 | 
						|
            return array(
 | 
						|
                'success' => true,
 | 
						|
                'batch_id' => $body['batch_header']['payout_batch_id']
 | 
						|
            );
 | 
						|
        } else {
 | 
						|
            $error = 'Failed to create payout batch';
 | 
						|
            if (isset($body['message'])) {
 | 
						|
                $error = $body['message'];
 | 
						|
            } elseif (isset($body['error_description'])) {
 | 
						|
                $error = $body['error_description'];
 | 
						|
            }
 | 
						|
            
 | 
						|
            return array('success' => false, 'error' => $error);
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    public static function check_batch_status($batch_id, $mode = null) {
 | 
						|
        if (!$mode) {
 | 
						|
            $mode = get_option('wpdd_paypal_mode', 'sandbox');
 | 
						|
        }
 | 
						|
        
 | 
						|
        $client_id = get_option('wpdd_paypal_client_id');
 | 
						|
        $secret = get_option('wpdd_paypal_secret');
 | 
						|
        
 | 
						|
        if (empty($client_id) || empty($secret)) {
 | 
						|
            return array('success' => false, 'error' => 'PayPal credentials not configured');
 | 
						|
        }
 | 
						|
        
 | 
						|
        // Get access token
 | 
						|
        $token_result = self::get_access_token($client_id, $secret, $mode);
 | 
						|
        
 | 
						|
        if (!$token_result['success']) {
 | 
						|
            return array('success' => false, 'error' => $token_result['error']);
 | 
						|
        }
 | 
						|
        
 | 
						|
        $access_token = $token_result['token'];
 | 
						|
        
 | 
						|
        $base_url = $mode === 'sandbox' 
 | 
						|
            ? 'https://api-m.sandbox.paypal.com' 
 | 
						|
            : 'https://api-m.paypal.com';
 | 
						|
        
 | 
						|
        $response = wp_remote_get(
 | 
						|
            $base_url . '/v1/payments/payouts/' . $batch_id,
 | 
						|
            array(
 | 
						|
                'headers' => array(
 | 
						|
                    'Authorization' => 'Bearer ' . $access_token,
 | 
						|
                    'Content-Type' => 'application/json'
 | 
						|
                ),
 | 
						|
                'timeout' => 30
 | 
						|
            )
 | 
						|
        );
 | 
						|
        
 | 
						|
        if (is_wp_error($response)) {
 | 
						|
            return array('success' => false, 'error' => $response->get_error_message());
 | 
						|
        }
 | 
						|
        
 | 
						|
        $body = json_decode(wp_remote_retrieve_body($response), true);
 | 
						|
        
 | 
						|
        if (isset($body['batch_header'])) {
 | 
						|
            return array(
 | 
						|
                'success' => true,
 | 
						|
                'status' => $body['batch_header']['batch_status'],
 | 
						|
                'data' => $body
 | 
						|
            );
 | 
						|
        } else {
 | 
						|
            return array('success' => false, 'error' => 'Failed to get batch status');
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    public static function deactivate() {
 | 
						|
        wp_clear_scheduled_hook('wpdd_process_automatic_payouts');
 | 
						|
    }
 | 
						|
} |