From a160fe396463f74626ab2486547a1a215e1e7beb Mon Sep 17 00:00:00 2001 From: jknapp Date: Tue, 9 Sep 2025 19:16:57 -0700 Subject: [PATCH] Major improvements: Fix download limits, enhance license display, fix software filenames MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🔧 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 --- .gitea/workflows/release.yml | 84 +++ .gitea/workflows/update-version.yml | 57 ++ .gitignore | 28 + admin/class-wpdd-admin-payouts.php | 83 ++- admin/class-wpdd-admin.php | 72 ++ admin/class-wpdd-order-manager.php | 470 +++++++++++++ admin/class-wpdd-settings.php | 223 ++++-- admin/class-wpdd-transaction-history.php | 781 +++++++++++++++++++++ admin/wpdd-db-migrate.php | 54 ++ admin/wpdd-tools.php | 756 ++++++++++++++++++++ assets/css/frontend.css | 59 ++ assets/js/admin-order-manager.js | 31 + assets/js/admin-payouts.js | 35 + assets/js/admin.js | 46 ++ assets/js/frontend.js | 58 ++ includes/class-wpdd-api.php | 163 ++++- includes/class-wpdd-creator.php | 78 +- includes/class-wpdd-download-handler.php | 124 +++- includes/class-wpdd-earnings-processor.php | 189 +++++ includes/class-wpdd-email.php | 211 ++++++ includes/class-wpdd-install.php | 24 +- includes/class-wpdd-license-manager.php | 29 +- includes/class-wpdd-metaboxes.php | 2 +- includes/class-wpdd-orders.php | 22 +- includes/class-wpdd-paypal-payouts.php | 8 +- includes/class-wpdd-paypal.php | 63 +- includes/class-wpdd-shortcodes.php | 30 +- wp-digital-download.php | 85 ++- 28 files changed, 3709 insertions(+), 156 deletions(-) create mode 100644 .gitea/workflows/release.yml create mode 100644 .gitea/workflows/update-version.yml create mode 100644 .gitignore create mode 100644 admin/class-wpdd-order-manager.php create mode 100644 admin/class-wpdd-transaction-history.php create mode 100644 admin/wpdd-db-migrate.php create mode 100644 admin/wpdd-tools.php create mode 100644 assets/js/admin-order-manager.js create mode 100644 includes/class-wpdd-earnings-processor.php create mode 100644 includes/class-wpdd-email.php diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml new file mode 100644 index 0000000..1130910 --- /dev/null +++ b/.gitea/workflows/release.yml @@ -0,0 +1,84 @@ +name: Create Release + +on: + push: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + username: ${{ secrets.CI_USER }} + password: ${{ secrets.CI_TOKEN }} + fetch-depth: 0 # Important: Fetch all history for commit messages + + - name: Get version + id: get_version + run: | + if [ "${{ github.ref_type }}" = "tag" ]; then + echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT + else + echo "version=$(date +'%Y.%m.%d-%H%M')" >> $GITHUB_OUTPUT + fi + + - name: Generate release notes + id: release_notes + run: | + # Find the most recent tag + LATEST_TAG=$(git describe --tags --abbrev=0 --always 2>/dev/null || echo "none") + + if [ "$LATEST_TAG" = "none" ]; then + # If no previous tag exists, get all commits + COMMITS=$(git log --pretty=format:"* %s (%h)" --no-merges) + else + # Get commits since the last tag + COMMITS=$(git log --pretty=format:"* %s (%h)" ${LATEST_TAG}..HEAD --no-merges) + fi + + # Create release notes with header (without encoding newlines) + echo "notes<> $GITHUB_OUTPUT + echo "## What's New in ${{ steps.get_version.outputs.version }}" >> $GITHUB_OUTPUT + echo "" >> $GITHUB_OUTPUT + echo "$COMMITS" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + - name: Update plugin version + run: | + # Replace version placeholder in plugin header + sed -i "s/Version: .*/Version: ${{ steps.get_version.outputs.version }}/" wp-digital-download.php + # Replace version placeholder in PHP constant + sed -i "s/define('WPDD_VERSION', '[^']*');/define('WPDD_VERSION', '${{ steps.get_version.outputs.version }}');/" wp-digital-download.php + # Verify the changes were made + echo "Plugin header version:" + grep "Version:" wp-digital-download.php + echo "PHP constant version:" + grep "WPDD_VERSION" wp-digital-download.php + + - name: Create ZIP archive + run: | + # Create a temp directory with the correct plugin folder name + mkdir -p /tmp/wp-digital-download + + # Copy files to the temp directory (excluding git and other unnecessary files) + cp -r * /tmp/wp-digital-download/ 2>/dev/null || true + + # Exclude .git, .gitea, and .playwright-mcp directories + rm -rf /tmp/wp-digital-download/.git /tmp/wp-digital-download/.gitea /tmp/wp-digital-download/.playwright-mcp 2>/dev/null || true + + # Create the ZIP file with the proper structure + cd /tmp + zip -r $GITHUB_WORKSPACE/wp-digital-download.zip wp-digital-download + + - name: Create Release + uses: softprops/action-gh-release@v2 + with: + token: "${{ secrets.REPO_TOKEN }}" + title: "WP Digital Download Release ${{ steps.get_version.outputs.version }}" + tag_name: ${{ steps.get_version.outputs.version }} + body: "${{ steps.release_notes.outputs.notes }}" + files: | + wp-digital-download.zip \ No newline at end of file diff --git a/.gitea/workflows/update-version.yml b/.gitea/workflows/update-version.yml new file mode 100644 index 0000000..2e22d2e --- /dev/null +++ b/.gitea/workflows/update-version.yml @@ -0,0 +1,57 @@ +name: Update Plugin Version + +on: + release: + types: [created, edited] + +jobs: + update-version: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Get release tag + id: get_tag + run: echo "TAG=${GITEA_REF#refs/tags/}" >> $GITEA_ENV + + - name: Update version in plugin file + run: | + # Replace version in plugin header + sed -i "s/Version: .*/Version: ${{ env.TAG }}/" wp-digital-download.php + + # Replace version in PHP constant + sed -i "s/define('WPDD_VERSION', '[^']*');/define('WPDD_VERSION', '${{ env.TAG }}');/" wp-digital-download.php + + # Verify changes + echo "Plugin header version:" + grep "Version:" wp-digital-download.php + echo "PHP constant version:" + grep "WPDD_VERSION" wp-digital-download.php + + - name: Commit changes + run: | + git config --local user.email "action@gitea.com" + git config --local user.name "Gitea Action" + git add wp-digital-download.php + git commit -m "Update version to ${{ env.TAG }}" + git push + + - name: Create plugin zip + run: | + mkdir -p /tmp/wp-digital-download + rsync -av --exclude=".git" --exclude=".gitea" --exclude=".playwright-mcp" --exclude="build" . /tmp/wp-digital-download/ + cd /tmp + zip -r $GITEA_WORK_DIR/wp-digital-download.zip wp-digital-download + + - name: Upload zip to release + uses: actions/upload-release-asset@v1 + env: + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + with: + upload_url: ${{ gitea.event.release.upload_url }} + asset_path: build/wp-digital-download.zip + asset_name: wp-digital-download.zip + asset_content_type: application/zip \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1fe5e52 --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +# Playwright MCP downloads and browser automation files +.playwright-mcp/ + +# WordPress specific +wp-config.php +wp-content/uploads/ +wp-content/upgrade/ +wp-content/backup-db/ +wp-content/advanced-cache.php +wp-content/wp-cache-config.php +wp-content/cache/ +wp-content/backups/ + +# Plugin specific +node_modules/ +*.log +.DS_Store +Thumbs.db + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo + +# Temporary files +*.tmp +*.temp \ No newline at end of file diff --git a/admin/class-wpdd-admin-payouts.php b/admin/class-wpdd-admin-payouts.php index 35beedc..859fe76 100644 --- a/admin/class-wpdd-admin-payouts.php +++ b/admin/class-wpdd-admin-payouts.php @@ -92,7 +92,13 @@ class WPDD_Admin_Payouts {
-

+

+ + ' . esc_html($error_detail) . '' : __('Unknown error occurred. Please try again.', 'wp-digital-download'); + ?> +

@@ -166,19 +172,26 @@ class WPDD_Admin_Payouts { - 'wpdd_creator')); foreach ($all_creators as $creator) : $paypal_email = get_user_meta($creator->ID, 'wpdd_paypal_email', true); + $balance = WPDD_Creator::get_creator_balance($creator->ID); ?> - + @@ -217,14 +230,19 @@ class WPDD_Admin_Payouts { - - - + @@ -497,10 +515,11 @@ class WPDD_Admin_Payouts { $result = self::create_payout($creator_id); - if ($result) { + if ($result['success']) { wp_redirect(admin_url('edit.php?post_type=wpdd_product&page=wpdd-payouts&message=success')); } else { - wp_redirect(admin_url('edit.php?post_type=wpdd_product&page=wpdd-payouts&message=error')); + $error_message = urlencode($result['error'] ?? 'Unknown error occurred'); + wp_redirect(admin_url('edit.php?post_type=wpdd_product&page=wpdd-payouts&message=error&error_detail=' . $error_message)); } exit; } @@ -543,11 +562,28 @@ class WPDD_Admin_Payouts { public static function create_payout($creator_id, $method = 'manual') { global $wpdb; + // Validate creator + $creator = get_userdata($creator_id); + if (!$creator) { + return array('success' => false, 'error' => 'Creator not found'); + } + $balance = WPDD_Creator::get_creator_balance($creator_id); $paypal_email = get_user_meta($creator_id, 'wpdd_paypal_email', true); - if ($balance <= 0 || empty($paypal_email)) { - return false; + if ($balance <= 0) { + return array('success' => false, 'error' => 'Creator has zero balance to payout'); + } + + if (empty($paypal_email)) { + return array('success' => false, 'error' => 'Creator has no PayPal email configured'); + } + + // Validate PayPal credentials are configured + $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 in settings'); } $currency = get_option('wpdd_currency', 'USD'); @@ -571,6 +607,10 @@ class WPDD_Admin_Payouts { $payout_id = $wpdb->insert_id; + if (!$payout_id) { + return array('success' => false, 'error' => 'Failed to create payout record in database'); + } + // Try to process via PayPal API $result = WPDD_PayPal_Payouts::process_payout($payout_id); @@ -588,10 +628,25 @@ class WPDD_Admin_Payouts { array('%d') ); + // Mark all available earnings for this creator as paid + $wpdb->update( + $wpdb->prefix . 'wpdd_creator_earnings', + array( + 'payout_id' => $payout_id, + 'payout_status' => 'paid' + ), + array( + 'creator_id' => $creator_id, + 'payout_status' => 'available' + ), + array('%d', '%s'), + array('%d', '%s') + ); + // Reset creator balance update_user_meta($creator_id, 'wpdd_creator_balance', 0); - return true; + return array('success' => true, 'message' => 'Payout processed successfully'); } else { // Update with error $wpdb->update( @@ -605,7 +660,7 @@ class WPDD_Admin_Payouts { array('%d') ); - return false; + return array('success' => false, 'error' => $result['error'] ?? 'PayPal payout processing failed'); } } @@ -813,7 +868,7 @@ class WPDD_Admin_Payouts { } // Get current balance - $current_balance = floatval(get_user_meta($creator_id, 'wpdd_balance', true)); + $current_balance = floatval(get_user_meta($creator_id, 'wpdd_creator_balance', true)); // Calculate new balance if ($adjustment_type === 'add') { @@ -823,7 +878,7 @@ class WPDD_Admin_Payouts { } // Update the balance - update_user_meta($creator_id, 'wpdd_balance', $new_balance); + update_user_meta($creator_id, 'wpdd_creator_balance', $new_balance); // Create a record of this adjustment global $wpdb; diff --git a/admin/class-wpdd-admin.php b/admin/class-wpdd-admin.php index db27ba9..9610402 100644 --- a/admin/class-wpdd-admin.php +++ b/admin/class-wpdd-admin.php @@ -14,6 +14,8 @@ class WPDD_Admin { add_action('pre_get_posts', array(__CLASS__, 'sort_products_by_column')); add_action('pre_get_posts', array(__CLASS__, 'filter_creator_products')); add_action('admin_init', array(__CLASS__, 'handle_admin_actions')); + add_action('wp_ajax_wpdd_sync_software_product', array(__CLASS__, 'handle_sync_software_product')); + add_action('wp_ajax_wpdd_regenerate_licenses', array(__CLASS__, 'handle_regenerate_licenses')); // Initialize admin payouts if (class_exists('WPDD_Admin_Payouts')) { @@ -145,7 +147,17 @@ class WPDD_Admin { case 'wpdd_files': $files = get_post_meta($post_id, '_wpdd_files', true); $count = is_array($files) ? count($files) : 0; + $product_type = get_post_meta($post_id, '_wpdd_product_type', true); + echo $count; + + // Add sync button for software license products with no files + if ($product_type === 'software_license' && $count === 0) { + $nonce = wp_create_nonce('wpdd_sync_product_' . $post_id); + echo '
'; + } break; } } @@ -1232,4 +1244,64 @@ class WPDD_Admin { wp_redirect(admin_url('edit.php?post_type=wpdd_product&page=wpdd-creator-payouts&message=payout_requested')); exit; } + + public static function handle_sync_software_product() { + if (!current_user_can('edit_wpdd_products')) { + wp_die('Unauthorized'); + } + + if (!isset($_POST['product_id']) || !wp_verify_nonce($_POST['nonce'], 'wpdd_sync_product_' . $_POST['product_id'])) { + wp_die('Invalid nonce'); + } + + $product_id = intval($_POST['product_id']); + + if (!class_exists('WPDD_API')) { + require_once WPDD_PLUGIN_PATH . 'includes/class-wpdd-api.php'; + } + + $result = WPDD_API::sync_software_product($product_id, true); + + wp_send_json($result); + } + + public static function handle_regenerate_licenses() { + if (!current_user_can('manage_options')) { + wp_die('Unauthorized'); + } + + if (!wp_verify_nonce($_POST['nonce'], 'wpdd_regenerate_licenses')) { + wp_die('Invalid nonce'); + } + + global $wpdb; + + // Find completed orders for software license products that don't have license keys + $orders = $wpdb->get_results(" + SELECT o.*, pm.meta_value as product_type + FROM {$wpdb->prefix}wpdd_orders o + LEFT JOIN {$wpdb->postmeta} pm ON o.product_id = pm.post_id AND pm.meta_key = '_wpdd_product_type' + LEFT JOIN {$wpdb->prefix}wpdd_licenses l ON o.id = l.order_id + WHERE o.status = 'completed' + AND pm.meta_value = 'software_license' + AND l.license_key IS NULL + "); + + $generated = 0; + + foreach ($orders as $order) { + if (class_exists('WPDD_License_Manager')) { + $license_key = WPDD_License_Manager::create_license($order->id); + if ($license_key) { + $generated++; + } + } + } + + wp_send_json(array( + 'success' => true, + 'message' => sprintf('Generated %d license keys for existing orders.', $generated), + 'generated' => $generated + )); + } } \ No newline at end of file diff --git a/admin/class-wpdd-order-manager.php b/admin/class-wpdd-order-manager.php new file mode 100644 index 0000000..66150f0 --- /dev/null +++ b/admin/class-wpdd-order-manager.php @@ -0,0 +1,470 @@ + admin_url('admin-ajax.php'), + 'nonce' => wp_create_nonce('wpdd_order_manager'), + 'confirm_cancel' => __('Are you sure you want to cancel this order? This action cannot be undone.', 'wp-digital-download'), + 'confirm_refund' => __('Are you sure you want to process this refund? The customer will lose access to the product.', 'wp-digital-download'), + 'confirm_release' => __('Are you sure you want to release these earnings immediately?', 'wp-digital-download') + )); + } + + public static function render_page() { + global $wpdb; + + // Get filter parameters + $status_filter = isset($_GET['status']) ? sanitize_text_field($_GET['status']) : 'all'; + $creator_filter = isset($_GET['creator']) ? intval($_GET['creator']) : 0; + $date_from = isset($_GET['date_from']) ? sanitize_text_field($_GET['date_from']) : date('Y-m-d', strtotime('-30 days')); + $date_to = isset($_GET['date_to']) ? sanitize_text_field($_GET['date_to']) : date('Y-m-d'); + + // Build query + $where_conditions = array('1=1'); + $query_params = array(); + + if ($status_filter !== 'all') { + $where_conditions[] = 'o.status = %s'; + $query_params[] = $status_filter; + } + + if ($creator_filter > 0) { + $where_conditions[] = 'p.post_author = %d'; + $query_params[] = $creator_filter; + } + + if ($date_from) { + $where_conditions[] = 'DATE(o.purchase_date) >= %s'; + $query_params[] = $date_from; + } + + if ($date_to) { + $where_conditions[] = 'DATE(o.purchase_date) <= %s'; + $query_params[] = $date_to; + } + + $where_clause = implode(' AND ', $where_conditions); + + // Get orders with earnings status + $orders = $wpdb->get_results($wpdb->prepare( + "SELECT o.*, + p.post_title as product_name, + u.display_name as creator_name, + e.payout_status, + e.available_at, + e.creator_earning, + e.id as earning_id + FROM {$wpdb->prefix}wpdd_orders o + LEFT JOIN {$wpdb->posts} p ON o.product_id = p.ID + LEFT JOIN {$wpdb->users} u ON p.post_author = u.ID + LEFT JOIN {$wpdb->prefix}wpdd_creator_earnings e ON o.id = e.order_id + WHERE $where_clause + ORDER BY o.purchase_date DESC + LIMIT 100", + $query_params + )); + + // Get creators for filter + $creators = get_users(array('role' => 'wpdd_creator')); + + ?> +
+

+ + + +
+

+
+ +
+

+
+ +
+

+
+ +
+

+
+ + + + +
+

+
+ + + + + + + + + + + + + + + + +
+ +
+ +
+ + + +
+ +

+ +

+
+
+ + +
+

()

+ + +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ order_number); ?>
+ #id); ?> +
+ customer_name); ?>
+ customer_email); ?> +
+ product_name); ?>
+ ID: product_id); ?> +
creator_name); ?>amount); ?>purchase_date))); ?> + status) { + case 'completed': + $status_class = 'notice-success'; + break; + case 'cancelled': + case 'failed': + $status_class = 'notice-error'; + break; + case 'pending': + $status_class = 'notice-warning'; + break; + } + ?> + + status)); ?> + + + payout_status) : ?> + payout_status); + switch($order->payout_status) { + case 'pending': + $earnings_class = 'notice-info'; + if ($order->available_at) { + $earnings_text .= '
Until: ' . date('M j', strtotime($order->available_at)) . ''; + } + break; + case 'available': + $earnings_class = 'notice-warning'; + break; + case 'paid': + $earnings_class = 'notice-success'; + break; + case 'cancelled': + $earnings_class = 'notice-error'; + break; + } + ?> + + + + creator_earning > 0) : ?> +
creator_earning); ?> + + + + + + +
+
+ status === 'completed') : ?> + + payout_status === 'pending') : ?> + +
+ + + + earning_id, 'wpdd_nonce'); ?> + +
+ + + payout_status !== 'paid') : ?> + +
+ + + id, 'wpdd_nonce'); ?> + +
+ + +
+ + + id, 'wpdd_nonce'); ?> + +
+ + + + + + status); ?> + +
+
+ +
+ + +
+ update( + $wpdb->prefix . 'wpdd_orders', + array('status' => 'cancelled'), + array('id' => $order_id), + array('%s'), + array('%d') + ); + + if ($result) { + // Revoke download access + $wpdb->delete( + $wpdb->prefix . 'wpdd_download_links', + array('order_id' => $order_id), + array('%d') + ); + + // Cancel associated earnings + $earning_id = $wpdb->get_var($wpdb->prepare( + "SELECT id FROM {$wpdb->prefix}wpdd_creator_earnings WHERE order_id = %d", + $order_id + )); + + if ($earning_id) { + WPDD_Earnings_Processor::cancel_earning($earning_id, 'Order cancelled by admin'); + } + } + + return $result > 0; + } + + private static function refund_order($order_id) { + global $wpdb; + + // Update order status + $result = $wpdb->update( + $wpdb->prefix . 'wpdd_orders', + array('status' => 'refunded'), + array('id' => $order_id), + array('%s'), + array('%d') + ); + + if ($result) { + // Revoke download access + $wpdb->delete( + $wpdb->prefix . 'wpdd_download_links', + array('order_id' => $order_id), + array('%d') + ); + + // Cancel associated earnings + $earning_id = $wpdb->get_var($wpdb->prepare( + "SELECT id FROM {$wpdb->prefix}wpdd_creator_earnings WHERE order_id = %d", + $order_id + )); + + if ($earning_id) { + WPDD_Earnings_Processor::cancel_earning($earning_id, 'Order refunded by admin'); + } + } + + return $result > 0; + } +} \ No newline at end of file diff --git a/admin/class-wpdd-settings.php b/admin/class-wpdd-settings.php index 6240a4d..8fcb948 100644 --- a/admin/class-wpdd-settings.php +++ b/admin/class-wpdd-settings.php @@ -9,6 +9,54 @@ class WPDD_Settings { public static function init() { add_action('admin_menu', array(__CLASS__, 'add_settings_page')); add_action('admin_init', array(__CLASS__, 'register_settings')); + + // Ensure default settings are set + self::ensure_default_settings(); + } + + public static function ensure_default_settings() { + $defaults = array( + 'wpdd_paypal_mode' => 'sandbox', + 'wpdd_currency' => 'USD', + 'wpdd_commission_rate' => 0, + 'wpdd_payout_threshold' => 0, + 'wpdd_earnings_holding_days' => 15, + 'wpdd_enable_guest_checkout' => 1, + 'wpdd_default_download_limit' => 5, + 'wpdd_default_download_expiry' => 7, + 'wpdd_enable_watermark' => 0, + 'wpdd_file_access_method' => 'direct', + 'wpdd_smtp_enabled' => 0, + 'wpdd_smtp_port' => 587, + 'wpdd_smtp_encryption' => 'tls' + ); + + foreach ($defaults as $option_name => $default_value) { + if (get_option($option_name) === false) { + add_option($option_name, $default_value); + } + } + + // Ensure critical settings exist (but don't set default values) + $critical_settings = array( + 'wpdd_paypal_client_id', + 'wpdd_paypal_secret', + 'wpdd_admin_email', + 'wpdd_from_name', + 'wpdd_from_email', + 'wpdd_smtp_host', + 'wpdd_smtp_username', + 'wpdd_smtp_password', + 'wpdd_watermark_text', + 'wpdd_terms_page', + 'wpdd_privacy_page' + ); + + foreach ($critical_settings as $setting) { + if (get_option($setting) === false) { + add_option($setting, ''); + } + } } public static function add_settings_page() { @@ -23,69 +71,80 @@ class WPDD_Settings { } public static function register_settings() { - register_setting('wpdd_settings', 'wpdd_paypal_mode'); - register_setting('wpdd_settings', 'wpdd_paypal_client_id'); - register_setting('wpdd_settings', 'wpdd_paypal_secret'); - register_setting('wpdd_settings', 'wpdd_paypal_payout_email'); - register_setting('wpdd_settings', 'wpdd_admin_email'); - register_setting('wpdd_settings', 'wpdd_from_name'); - register_setting('wpdd_settings', 'wpdd_from_email'); - register_setting('wpdd_settings', 'wpdd_smtp_enabled'); - register_setting('wpdd_settings', 'wpdd_smtp_host'); - register_setting('wpdd_settings', 'wpdd_smtp_port'); - register_setting('wpdd_settings', 'wpdd_smtp_username'); - register_setting('wpdd_settings', 'wpdd_smtp_password'); - register_setting('wpdd_settings', 'wpdd_smtp_encryption'); - register_setting('wpdd_settings', 'wpdd_currency'); - register_setting('wpdd_settings', 'wpdd_enable_guest_checkout'); - register_setting('wpdd_settings', 'wpdd_default_download_limit'); - register_setting('wpdd_settings', 'wpdd_default_download_expiry'); - register_setting('wpdd_settings', 'wpdd_enable_watermark'); - register_setting('wpdd_settings', 'wpdd_watermark_text'); - register_setting('wpdd_settings', 'wpdd_terms_page'); - register_setting('wpdd_settings', 'wpdd_privacy_page'); - register_setting('wpdd_settings', 'wpdd_commission_rate', array( + // General Settings + register_setting('wpdd_general_settings', 'wpdd_currency'); + register_setting('wpdd_general_settings', 'wpdd_enable_guest_checkout'); + register_setting('wpdd_general_settings', 'wpdd_commission_rate', array( 'sanitize_callback' => array(__CLASS__, 'sanitize_commission_rate') )); - register_setting('wpdd_settings', 'wpdd_payout_threshold', array( + register_setting('wpdd_general_settings', 'wpdd_payout_threshold', array( 'sanitize_callback' => array(__CLASS__, 'sanitize_payout_threshold') )); - register_setting('wpdd_settings', 'wpdd_file_access_method'); - register_setting('wpdd_settings', 'wpdd_disable_admin_bar'); + register_setting('wpdd_general_settings', 'wpdd_earnings_holding_days', array( + 'sanitize_callback' => array(__CLASS__, 'sanitize_holding_days') + )); + register_setting('wpdd_general_settings', 'wpdd_terms_page'); + register_setting('wpdd_general_settings', 'wpdd_privacy_page'); + + // PayPal Settings + register_setting('wpdd_paypal_settings', 'wpdd_paypal_mode'); + register_setting('wpdd_paypal_settings', 'wpdd_paypal_client_id'); + register_setting('wpdd_paypal_settings', 'wpdd_paypal_secret'); + + // Email Settings + register_setting('wpdd_email_settings', 'wpdd_admin_email'); + register_setting('wpdd_email_settings', 'wpdd_from_name'); + register_setting('wpdd_email_settings', 'wpdd_from_email'); + register_setting('wpdd_email_settings', 'wpdd_smtp_enabled'); + register_setting('wpdd_email_settings', 'wpdd_smtp_host'); + register_setting('wpdd_email_settings', 'wpdd_smtp_port'); + register_setting('wpdd_email_settings', 'wpdd_smtp_username'); + register_setting('wpdd_email_settings', 'wpdd_smtp_password'); + register_setting('wpdd_email_settings', 'wpdd_smtp_encryption'); + + // Download Settings + register_setting('wpdd_download_settings', 'wpdd_default_download_limit'); + register_setting('wpdd_download_settings', 'wpdd_default_download_expiry'); + register_setting('wpdd_download_settings', 'wpdd_file_access_method'); + register_setting('wpdd_download_settings', 'wpdd_disable_admin_bar'); + + // Watermark Settings + register_setting('wpdd_watermark_settings', 'wpdd_enable_watermark'); + register_setting('wpdd_watermark_settings', 'wpdd_watermark_text'); add_settings_section( 'wpdd_general_settings', __('General Settings', 'wp-digital-download'), array(__CLASS__, 'general_section_callback'), - 'wpdd_settings' + 'wpdd_general_settings' ); add_settings_section( 'wpdd_paypal_settings', __('PayPal Settings', 'wp-digital-download'), array(__CLASS__, 'paypal_section_callback'), - 'wpdd_settings' + 'wpdd_paypal_settings' ); add_settings_section( 'wpdd_email_settings', __('Email Settings', 'wp-digital-download'), array(__CLASS__, 'email_section_callback'), - 'wpdd_settings' + 'wpdd_email_settings' ); add_settings_section( 'wpdd_download_settings', __('Download Settings', 'wp-digital-download'), array(__CLASS__, 'download_section_callback'), - 'wpdd_settings' + 'wpdd_download_settings' ); add_settings_section( 'wpdd_watermark_settings', __('Watermark Settings', 'wp-digital-download'), array(__CLASS__, 'watermark_section_callback'), - 'wpdd_settings' + 'wpdd_watermark_settings' ); self::add_general_fields(); @@ -100,7 +159,7 @@ class WPDD_Settings { 'wpdd_currency', __('Currency', 'wp-digital-download'), array(__CLASS__, 'currency_field'), - 'wpdd_settings', + 'wpdd_general_settings', 'wpdd_general_settings', array( 'name' => 'wpdd_currency' @@ -111,7 +170,7 @@ class WPDD_Settings { 'wpdd_enable_guest_checkout', __('Guest Checkout', 'wp-digital-download'), array(__CLASS__, 'checkbox_field'), - 'wpdd_settings', + 'wpdd_general_settings', 'wpdd_general_settings', array( 'name' => 'wpdd_enable_guest_checkout', @@ -123,7 +182,7 @@ class WPDD_Settings { 'wpdd_commission_rate', __('Platform Commission Rate (%)', 'wp-digital-download'), array(__CLASS__, 'number_field'), - 'wpdd_settings', + 'wpdd_general_settings', 'wpdd_general_settings', array( 'name' => 'wpdd_commission_rate', @@ -138,7 +197,7 @@ class WPDD_Settings { 'wpdd_payout_threshold', __('Automatic Payout Threshold ($)', 'wp-digital-download'), array(__CLASS__, 'number_field'), - 'wpdd_settings', + 'wpdd_general_settings', 'wpdd_general_settings', array( 'name' => 'wpdd_payout_threshold', @@ -148,11 +207,27 @@ class WPDD_Settings { ) ); + add_settings_field( + 'wpdd_earnings_holding_days', + __('Earnings Holding Period (Days)', 'wp-digital-download'), + array(__CLASS__, 'number_field'), + 'wpdd_general_settings', + 'wpdd_general_settings', + array( + 'name' => 'wpdd_earnings_holding_days', + 'description' => __('Number of days to hold earnings before making them available for payout (0 for immediate, 15 recommended for fraud protection)', 'wp-digital-download'), + 'min' => 0, + 'max' => 365, + 'step' => 1, + 'default' => 15 + ) + ); + add_settings_field( 'wpdd_terms_page', __('Terms & Conditions Page', 'wp-digital-download'), array(__CLASS__, 'page_dropdown_field'), - 'wpdd_settings', + 'wpdd_general_settings', 'wpdd_general_settings', array('name' => 'wpdd_terms_page') ); @@ -161,7 +236,7 @@ class WPDD_Settings { 'wpdd_privacy_page', __('Privacy Policy Page', 'wp-digital-download'), array(__CLASS__, 'page_dropdown_field'), - 'wpdd_settings', + 'wpdd_general_settings', 'wpdd_general_settings', array('name' => 'wpdd_privacy_page') ); @@ -172,7 +247,7 @@ class WPDD_Settings { 'wpdd_paypal_mode', __('PayPal Mode', 'wp-digital-download'), array(__CLASS__, 'select_field'), - 'wpdd_settings', + 'wpdd_paypal_settings', 'wpdd_paypal_settings', array( 'name' => 'wpdd_paypal_mode', @@ -187,7 +262,7 @@ class WPDD_Settings { 'wpdd_paypal_client_id', __('PayPal Client ID', 'wp-digital-download'), array(__CLASS__, 'text_field'), - 'wpdd_settings', + 'wpdd_paypal_settings', 'wpdd_paypal_settings', array('name' => 'wpdd_paypal_client_id') ); @@ -196,22 +271,11 @@ class WPDD_Settings { 'wpdd_paypal_secret', __('PayPal Secret', 'wp-digital-download'), array(__CLASS__, 'password_field'), - 'wpdd_settings', + 'wpdd_paypal_settings', 'wpdd_paypal_settings', array('name' => 'wpdd_paypal_secret') ); - add_settings_field( - 'wpdd_paypal_payout_email', - __('PayPal Payout Account Email', 'wp-digital-download'), - array(__CLASS__, 'email_field'), - 'wpdd_settings', - 'wpdd_paypal_settings', - array( - 'name' => 'wpdd_paypal_payout_email', - 'description' => __('PayPal account email that will send payouts to creators', 'wp-digital-download') - ) - ); } private static function add_email_fields() { @@ -219,7 +283,7 @@ class WPDD_Settings { 'wpdd_admin_email', __('Admin Email', 'wp-digital-download'), array(__CLASS__, 'email_field'), - 'wpdd_settings', + 'wpdd_email_settings', 'wpdd_email_settings', array( 'name' => 'wpdd_admin_email', @@ -231,7 +295,7 @@ class WPDD_Settings { 'wpdd_from_name', __('From Name', 'wp-digital-download'), array(__CLASS__, 'text_field'), - 'wpdd_settings', + 'wpdd_email_settings', 'wpdd_email_settings', array( 'name' => 'wpdd_from_name', @@ -243,7 +307,7 @@ class WPDD_Settings { 'wpdd_from_email', __('From Email', 'wp-digital-download'), array(__CLASS__, 'email_field'), - 'wpdd_settings', + 'wpdd_email_settings', 'wpdd_email_settings', array( 'name' => 'wpdd_from_email', @@ -255,7 +319,7 @@ class WPDD_Settings { 'wpdd_smtp_enabled', __('Enable SMTP', 'wp-digital-download'), array(__CLASS__, 'checkbox_field'), - 'wpdd_settings', + 'wpdd_email_settings', 'wpdd_email_settings', array( 'name' => 'wpdd_smtp_enabled', @@ -267,7 +331,7 @@ class WPDD_Settings { 'wpdd_smtp_host', __('SMTP Host', 'wp-digital-download'), array(__CLASS__, 'text_field'), - 'wpdd_settings', + 'wpdd_email_settings', 'wpdd_email_settings', array( 'name' => 'wpdd_smtp_host', @@ -279,7 +343,7 @@ class WPDD_Settings { 'wpdd_smtp_port', __('SMTP Port', 'wp-digital-download'), array(__CLASS__, 'number_field'), - 'wpdd_settings', + 'wpdd_email_settings', 'wpdd_email_settings', array( 'name' => 'wpdd_smtp_port', @@ -293,7 +357,7 @@ class WPDD_Settings { 'wpdd_smtp_encryption', __('SMTP Encryption', 'wp-digital-download'), array(__CLASS__, 'select_field'), - 'wpdd_settings', + 'wpdd_email_settings', 'wpdd_email_settings', array( 'name' => 'wpdd_smtp_encryption', @@ -310,7 +374,7 @@ class WPDD_Settings { 'wpdd_smtp_autodetect', __('Auto-Detect Settings', 'wp-digital-download'), array(__CLASS__, 'smtp_autodetect_field'), - 'wpdd_settings', + 'wpdd_email_settings', 'wpdd_email_settings', array() ); @@ -319,7 +383,7 @@ class WPDD_Settings { 'wpdd_smtp_username', __('SMTP Username', 'wp-digital-download'), array(__CLASS__, 'text_field'), - 'wpdd_settings', + 'wpdd_email_settings', 'wpdd_email_settings', array( 'name' => 'wpdd_smtp_username', @@ -331,7 +395,7 @@ class WPDD_Settings { 'wpdd_smtp_password', __('SMTP Password', 'wp-digital-download'), array(__CLASS__, 'password_field'), - 'wpdd_settings', + 'wpdd_email_settings', 'wpdd_email_settings', array( 'name' => 'wpdd_smtp_password', @@ -345,7 +409,7 @@ class WPDD_Settings { 'wpdd_default_download_limit', __('Default Download Limit', 'wp-digital-download'), array(__CLASS__, 'number_field'), - 'wpdd_settings', + 'wpdd_download_settings', 'wpdd_download_settings', array( 'name' => 'wpdd_default_download_limit', @@ -358,7 +422,7 @@ class WPDD_Settings { 'wpdd_default_download_expiry', __('Default Download Expiry (days)', 'wp-digital-download'), array(__CLASS__, 'number_field'), - 'wpdd_settings', + 'wpdd_download_settings', 'wpdd_download_settings', array( 'name' => 'wpdd_default_download_expiry', @@ -371,7 +435,7 @@ class WPDD_Settings { 'wpdd_file_access_method', __('File Access Method', 'wp-digital-download'), array(__CLASS__, 'select_field'), - 'wpdd_settings', + 'wpdd_download_settings', 'wpdd_download_settings', array( 'name' => 'wpdd_file_access_method', @@ -390,7 +454,7 @@ class WPDD_Settings { 'wpdd_enable_watermark', __('Enable Watermarking', 'wp-digital-download'), array(__CLASS__, 'checkbox_field'), - 'wpdd_settings', + 'wpdd_watermark_settings', 'wpdd_watermark_settings', array( 'name' => 'wpdd_enable_watermark', @@ -402,7 +466,7 @@ class WPDD_Settings { 'wpdd_watermark_text', __('Default Watermark Text', 'wp-digital-download'), array(__CLASS__, 'text_field'), - 'wpdd_settings', + 'wpdd_watermark_settings', 'wpdd_watermark_settings', array( 'name' => 'wpdd_watermark_text', @@ -434,25 +498,30 @@ class WPDD_Settings {
'; - do_settings_fields('wpdd_settings', $section_id); + do_settings_fields($section_id, $section_id); echo ''; } @@ -990,4 +1059,16 @@ class WPDD_Settings { } return $value; } + + public static function sanitize_holding_days($input) { + $value = intval($input); + if ($value < 0) { + $value = 15; + add_settings_error('wpdd_earnings_holding_days', 'invalid_holding_days', __('Holding days cannot be negative. Set to 15 (recommended).', 'wp-digital-download')); + } elseif ($value > 365) { + $value = 365; + add_settings_error('wpdd_earnings_holding_days', 'invalid_holding_days', __('Holding days cannot exceed 365. Set to maximum (365).', 'wp-digital-download')); + } + return $value; + } } \ No newline at end of file diff --git a/admin/class-wpdd-transaction-history.php b/admin/class-wpdd-transaction-history.php new file mode 100644 index 0000000..b51b6d5 --- /dev/null +++ b/admin/class-wpdd-transaction-history.php @@ -0,0 +1,781 @@ + 'wpdd_creator')); + + // If creator is selected, show their transactions + if ($creator_id) { + self::render_transactions($creator_id, $date_from, $date_to, $type_filter, true); + } else { + ?> +
+

+ +
+ + + + + + + + + + + + + + + + + +
+ +
+ + + +
+ +
+ +

+ +

+ +
+
+

' . __('Creator not found.', 'wp-digital-download') . '

'; + return; + } + + // Fetch all transactions for this creator + $transactions = array(); + + // Get earnings (sales) + if ($type_filter === 'all' || $type_filter === 'earnings') { + $earnings = $wpdb->get_results($wpdb->prepare( + "SELECT e.*, o.order_number, o.customer_name, o.customer_email, p.post_title as product_name + FROM {$wpdb->prefix}wpdd_creator_earnings e + LEFT JOIN {$wpdb->prefix}wpdd_orders o ON e.order_id = o.id + LEFT JOIN {$wpdb->posts} p ON e.product_id = p.ID + WHERE e.creator_id = %d + AND DATE(e.created_at) BETWEEN %s AND %s + ORDER BY e.created_at DESC", + $creator_id, $date_from, $date_to + )); + + foreach ($earnings as $earning) { + $transactions[] = array( + 'date' => $earning->created_at, + 'type' => 'earning', + 'description' => sprintf(__('Sale: %s to %s', 'wp-digital-download'), + $earning->product_name, $earning->customer_name), + 'order_number' => $earning->order_number, + 'gross_amount' => $earning->sale_amount, + 'commission' => $earning->sale_amount - $earning->creator_earning, + 'net_amount' => $earning->creator_earning, + 'balance_change' => '+' . $earning->creator_earning, + 'status' => $earning->payout_status, + 'payout_id' => $earning->payout_id + ); + } + } + + // Get payouts + if ($type_filter === 'all' || $type_filter === 'payouts') { + $payouts = $wpdb->get_results($wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}wpdd_payouts + WHERE creator_id = %d + AND DATE(created_at) BETWEEN %s AND %s + ORDER BY created_at DESC", + $creator_id, $date_from, $date_to + )); + + foreach ($payouts as $payout) { + $transactions[] = array( + 'date' => $payout->created_at, + 'type' => 'payout', + 'description' => sprintf(__('Payout to %s', 'wp-digital-download'), $payout->paypal_email), + 'order_number' => $payout->transaction_id ?: 'N/A', + 'gross_amount' => 0, + 'commission' => 0, + 'net_amount' => -$payout->amount, + 'balance_change' => '-' . $payout->amount, + 'status' => $payout->status, + 'payout_id' => $payout->id + ); + } + } + + // Get adjustments + if ($type_filter === 'all' || $type_filter === 'adjustments') { + $adjustments = $wpdb->get_results($wpdb->prepare( + "SELECT a.*, u.display_name as adjusted_by_name + FROM {$wpdb->prefix}wpdd_balance_adjustments a + LEFT JOIN {$wpdb->users} u ON a.adjusted_by = u.ID + WHERE a.creator_id = %d + AND DATE(a.created_at) BETWEEN %s AND %s + ORDER BY a.created_at DESC", + $creator_id, $date_from, $date_to + )); + + foreach ($adjustments as $adjustment) { + $amount_change = $adjustment->adjustment_type === 'add' ? $adjustment->amount : -$adjustment->amount; + $transactions[] = array( + 'date' => $adjustment->created_at, + 'type' => 'adjustment', + 'description' => sprintf(__('Manual adjustment: %s (by %s)', 'wp-digital-download'), + $adjustment->reason, $adjustment->adjusted_by_name), + 'order_number' => 'ADJ-' . $adjustment->id, + 'gross_amount' => 0, + 'commission' => 0, + 'net_amount' => $amount_change, + 'balance_change' => ($amount_change >= 0 ? '+' : '') . $amount_change, + 'status' => 'completed', + 'payout_id' => null + ); + } + } + + // Sort transactions by date + usort($transactions, function($a, $b) { + return strtotime($b['date']) - strtotime($a['date']); + }); + + // Calculate running balance + $current_balance = WPDD_Creator::get_creator_balance($creator_id); + $running_balance = $current_balance; + + // Calculate balance by going backwards from current + for ($i = 0; $i < count($transactions); $i++) { + $transactions[$i]['running_balance'] = $running_balance; + if ($i < count($transactions) - 1) { + $running_balance -= floatval(str_replace('+', '', $transactions[$i]['balance_change'])); + } + } + + ?> +
+

+ display_name); + } else { + _e('My Transaction History', 'wp-digital-download'); + } + ?> +

+ +
+

+ + + + + + + + + + + + + + + + + + + + + + +
to
+
+ + +
+
+ + + + + + + + + + + + + + + + + +
+ + + +
+ +
+ +

+ +

+
+ + +
+
+ + + + + + + + +
+ +
+ + + + + + + + +
+
+
+ + +
+

()

+ + +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ' . __('Sale', 'wp-digital-download'); + break; + case 'payout': + $type_badge = ' ' . __('Payout', 'wp-digital-download'); + break; + case 'adjustment': + $type_badge = ' ' . __('Adjustment', 'wp-digital-download'); + break; + } + echo $type_badge; + ?> + 0 ? wpdd_format_price($transaction['gross_amount']) : '-'; ?> 0 ? wpdd_format_price($transaction['commission']) : '-'; ?> + + = 0 ? '+' : '') . wpdd_format_price(abs($transaction['net_amount'])); ?> + + + + + + +
+ +
+
+ get_results($wpdb->prepare( + "SELECT e.*, o.order_number, o.customer_name, o.customer_email, p.post_title as product_name + FROM {$wpdb->prefix}wpdd_creator_earnings e + LEFT JOIN {$wpdb->prefix}wpdd_orders o ON e.order_id = o.id + LEFT JOIN {$wpdb->posts} p ON e.product_id = p.ID + WHERE e.creator_id = %d + AND DATE(e.created_at) BETWEEN %s AND %s + ORDER BY e.created_at DESC", + $creator_id, $date_from, $date_to + )); + + foreach ($earnings as $earning) { + $transactions[] = array( + 'date' => $earning->created_at, + 'type' => 'earning', + 'description' => sprintf(__('Sale: %s to %s', 'wp-digital-download'), + $earning->product_name, $earning->customer_name), + 'order_number' => $earning->order_number, + 'gross_amount' => $earning->sale_amount, + 'commission' => $earning->sale_amount - $earning->creator_earning, + 'net_amount' => $earning->creator_earning, + 'balance_change' => '+' . $earning->creator_earning, + 'status' => $earning->payout_status, + 'payout_id' => $earning->payout_id + ); + } + } + + // Get payouts + if ($type_filter === 'all' || $type_filter === 'payouts') { + $payouts = $wpdb->get_results($wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}wpdd_payouts + WHERE creator_id = %d + AND DATE(created_at) BETWEEN %s AND %s + ORDER BY created_at DESC", + $creator_id, $date_from, $date_to + )); + + foreach ($payouts as $payout) { + $transactions[] = array( + 'date' => $payout->created_at, + 'type' => 'payout', + 'description' => sprintf(__('Payout to %s', 'wp-digital-download'), $payout->paypal_email), + 'order_number' => $payout->transaction_id ?: 'N/A', + 'gross_amount' => 0, + 'commission' => 0, + 'net_amount' => -$payout->amount, + 'balance_change' => '-' . $payout->amount, + 'status' => $payout->status, + 'payout_id' => $payout->id + ); + } + } + + // Get adjustments + if ($type_filter === 'all' || $type_filter === 'adjustments') { + $adjustments = $wpdb->get_results($wpdb->prepare( + "SELECT a.*, u.display_name as adjusted_by_name + FROM {$wpdb->prefix}wpdd_balance_adjustments a + LEFT JOIN {$wpdb->users} u ON a.adjusted_by = u.ID + WHERE a.creator_id = %d + AND DATE(a.created_at) BETWEEN %s AND %s + ORDER BY a.created_at DESC", + $creator_id, $date_from, $date_to + )); + + foreach ($adjustments as $adjustment) { + $amount_change = $adjustment->adjustment_type === 'add' ? $adjustment->amount : -$adjustment->amount; + $transactions[] = array( + 'date' => $adjustment->created_at, + 'type' => 'adjustment', + 'description' => sprintf(__('Manual adjustment: %s (by %s)', 'wp-digital-download'), + $adjustment->reason, $adjustment->adjusted_by_name), + 'order_number' => 'ADJ-' . $adjustment->id, + 'gross_amount' => 0, + 'commission' => 0, + 'net_amount' => $amount_change, + 'balance_change' => ($amount_change >= 0 ? '+' : '') . $amount_change, + 'status' => 'completed', + 'payout_id' => null + ); + } + } + + // Sort transactions by date + usort($transactions, function($a, $b) { + return strtotime($b['date']) - strtotime($a['date']); + }); + + // Calculate running balance + $current_balance = WPDD_Creator::get_creator_balance($creator_id); + $running_balance = $current_balance; + + // Calculate balance by going backwards from current + for ($i = 0; $i < count($transactions); $i++) { + $transactions[$i]['running_balance'] = $running_balance; + if ($i < count($transactions) - 1) { + $running_balance -= floatval(str_replace('+', '', $transactions[$i]['balance_change'])); + } + } + + return $transactions; + } + + private static function handle_export_request($creator_id, $transactions, $date_from, $date_to, $format) { + $creator = get_userdata($creator_id); + + if ($format === 'csv') { + self::export_csv($transactions, $creator, $date_from, $date_to); + } elseif ($format === 'pdf') { + self::export_pdf($transactions, $creator, $date_from, $date_to); + } + } + + private static function export_csv($transactions, $creator, $date_from, $date_to) { + $filename = sprintf('transactions_%s_%s_to_%s.csv', + sanitize_title($creator->display_name), + $date_from, + $date_to + ); + + header('Content-Type: text/csv; charset=utf-8'); + header('Content-Disposition: attachment; filename=' . $filename); + + $output = fopen('php://output', 'w'); + + // Add BOM for Excel UTF-8 compatibility + fprintf($output, chr(0xEF).chr(0xBB).chr(0xBF)); + + // Header row + fputcsv($output, array( + 'Date', + 'Type', + 'Description', + 'Order/Reference', + 'Gross Amount', + 'Commission', + 'Net Amount', + 'Running Balance', + 'Status' + )); + + // Data rows + foreach ($transactions as $transaction) { + fputcsv($output, array( + date('Y-m-d H:i:s', strtotime($transaction['date'])), + ucfirst($transaction['type']), + $transaction['description'], + $transaction['order_number'], + $transaction['gross_amount'] > 0 ? number_format($transaction['gross_amount'], 2) : '', + $transaction['commission'] > 0 ? number_format($transaction['commission'], 2) : '', + number_format(abs($transaction['net_amount']), 2) . ($transaction['net_amount'] < 0 ? ' (Debit)' : ''), + number_format($transaction['running_balance'], 2), + ucfirst($transaction['status']) + )); + } + + fclose($output); + exit; + } + + private static function export_pdf($transactions, $creator, $date_from, $date_to) { + // Create a clean PDF-ready HTML document + $filename = sprintf('transactions_%s_%s_to_%s.pdf', + sanitize_title($creator->display_name), + $date_from, + $date_to + ); + + // Set headers for PDF display/download + header('Content-Type: text/html; charset=utf-8'); + header('Content-Disposition: inline; filename=' . $filename); + header('X-Robots-Tag: noindex, nofollow'); + + ?> + + + + + <?php printf(__('Transaction History - %s', 'wp-digital-download'), $creator->display_name); ?> + + + +

display_name); ?>

+

+ +
+

+ + + + + + + + + + + + + +
ID)); ?>
+
+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ = 0 ? '+' : '') . wpdd_format_price(abs($transaction['net_amount'])); ?> +
+ +

+ +

+ + + + + get_results( + "SHOW COLUMNS FROM {$wpdb->prefix}wpdd_creator_earnings LIKE 'available_at'" + ); + + if (empty($column_exists)) { + // Add the missing available_at column + $result = $wpdb->query( + "ALTER TABLE {$wpdb->prefix}wpdd_creator_earnings + ADD COLUMN available_at datetime DEFAULT NULL AFTER payout_status, + ADD INDEX available_at (available_at)" + ); + + if ($result !== false) { + error_log('WPDD Migration: Successfully added available_at column to wpdd_creator_earnings table'); + + // Update existing pending earnings to have an available_at date + $holding_days = intval(get_option('wpdd_earnings_holding_days', 15)); + $wpdb->query( + $wpdb->prepare( + "UPDATE {$wpdb->prefix}wpdd_creator_earnings + SET available_at = DATE_ADD(created_at, INTERVAL %d DAY) + WHERE payout_status = 'pending' AND available_at IS NULL", + $holding_days + ) + ); + + return true; + } else { + error_log('WPDD Migration Error: Failed to add available_at column - ' . $wpdb->last_error); + return false; + } + } + + return true; // Column already exists +} + +// Run migration if accessed directly (for manual execution) +if (basename($_SERVER['SCRIPT_FILENAME']) === 'wpdd-db-migrate.php') { + wpdd_migrate_database(); +} \ No newline at end of file diff --git a/admin/wpdd-tools.php b/admin/wpdd-tools.php new file mode 100644 index 0000000..75641c7 --- /dev/null +++ b/admin/wpdd-tools.php @@ -0,0 +1,756 @@ +get_charset_collate(); + $results = array(); + + // Check if creator_earnings table exists and has correct structure + $table_name = $wpdb->prefix . 'wpdd_creator_earnings'; + $table_exists = $wpdb->get_var("SHOW TABLES LIKE '$table_name'") == $table_name; + + if (!$table_exists) { + $sql = "CREATE TABLE IF NOT EXISTS $table_name ( + id bigint(20) NOT NULL AUTO_INCREMENT, + creator_id bigint(20) NOT NULL, + order_id bigint(20) NOT NULL, + product_id bigint(20) NOT NULL, + sale_amount decimal(10,2) NOT NULL, + commission_rate decimal(5,2) NOT NULL, + creator_earning decimal(10,2) NOT NULL, + payout_id bigint(20) DEFAULT NULL, + payout_status varchar(20) DEFAULT 'pending', + available_at datetime DEFAULT NULL, + created_at datetime DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id), + KEY creator_id (creator_id), + KEY order_id (order_id), + KEY product_id (product_id), + KEY payout_id (payout_id), + KEY payout_status (payout_status) + ) $charset_collate;"; + + require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); + dbDelta($sql); + $results[] = "Created table: $table_name"; + } else { + // Check if payout_id and payout_status columns exist + $columns = $wpdb->get_results("SHOW COLUMNS FROM $table_name"); + $has_payout_id = false; + $has_payout_status = false; + + $has_available_at = false; + + foreach ($columns as $column) { + if ($column->Field == 'payout_id') $has_payout_id = true; + if ($column->Field == 'payout_status') $has_payout_status = true; + if ($column->Field == 'available_at') $has_available_at = true; + } + + if (!$has_payout_id) { + $wpdb->query("ALTER TABLE $table_name ADD COLUMN payout_id bigint(20) DEFAULT NULL"); + $wpdb->query("ALTER TABLE $table_name ADD KEY payout_id (payout_id)"); + $results[] = "Added payout_id column to $table_name"; + } + + if (!$has_payout_status) { + $wpdb->query("ALTER TABLE $table_name ADD COLUMN payout_status varchar(20) DEFAULT 'pending'"); + $wpdb->query("ALTER TABLE $table_name ADD KEY payout_status (payout_status)"); + $results[] = "Added payout_status column to $table_name"; + } + + if (!$has_available_at) { + $wpdb->query("ALTER TABLE $table_name ADD COLUMN available_at datetime DEFAULT NULL"); + $wpdb->query("ALTER TABLE $table_name ADD KEY available_at (available_at)"); + $results[] = "Added available_at column to $table_name"; + + // Update existing pending earnings with available_at dates + $holding_days = intval(get_option('wpdd_earnings_holding_days', 15)); + $wpdb->query( + $wpdb->prepare( + "UPDATE $table_name + SET available_at = DATE_ADD(created_at, INTERVAL %d DAY) + WHERE payout_status = 'pending' AND available_at IS NULL", + $holding_days + ) + ); + $results[] = "Updated pending earnings with available_at dates"; + } + + $results[] = "Table exists: $table_name"; + } + + // Check if balance_adjustments table exists + $table_name = $wpdb->prefix . 'wpdd_balance_adjustments'; + if ($wpdb->get_var("SHOW TABLES LIKE '$table_name'") != $table_name) { + $sql = "CREATE TABLE IF NOT EXISTS $table_name ( + id bigint(20) NOT NULL AUTO_INCREMENT, + creator_id bigint(20) NOT NULL, + adjustment_type varchar(20) NOT NULL, + amount decimal(10,2) NOT NULL, + previous_balance decimal(10,2) NOT NULL, + new_balance decimal(10,2) NOT NULL, + reason text NOT NULL, + adjusted_by bigint(20) NOT NULL, + created_at datetime DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id), + KEY creator_id (creator_id), + KEY adjusted_by (adjusted_by) + ) $charset_collate;"; + + require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); + dbDelta($sql); + $results[] = "Created table: $table_name"; + } else { + $results[] = "Table exists: $table_name"; + } + + // Check existing orders and add missing earnings records + $completed_orders = $wpdb->get_results( + "SELECT o.*, p.post_author as creator_id + FROM {$wpdb->prefix}wpdd_orders o + INNER JOIN {$wpdb->posts} p ON o.product_id = p.ID + WHERE o.status = 'completed'" + ); + + $results[] = "Found " . count($completed_orders) . " completed orders"; + + // Process each order to ensure earnings are recorded + $added_earnings = 0; + foreach ($completed_orders as $order) { + // Check if earning already recorded + $existing = $wpdb->get_var($wpdb->prepare( + "SELECT id FROM {$wpdb->prefix}wpdd_creator_earnings + WHERE order_id = %d", + $order->id + )); + + if (!$existing && $order->amount > 0) { + $commission_rate = floatval(get_option('wpdd_commission_rate', 0)); + $creator_share = $order->amount * (1 - ($commission_rate / 100)); + + // Insert earning record + $wpdb->insert( + $wpdb->prefix . 'wpdd_creator_earnings', + array( + 'creator_id' => $order->creator_id, + 'order_id' => $order->id, + 'product_id' => $order->product_id, + 'sale_amount' => $order->amount, + 'commission_rate' => $commission_rate, + 'creator_earning' => $creator_share, + 'payout_status' => 'pending', + 'created_at' => $order->purchase_date + ), + array('%d', '%d', '%d', '%f', '%f', '%f', '%s', '%s') + ); + + $added_earnings++; + } + } + + if ($added_earnings > 0) { + $results[] = "Added $added_earnings missing earning records"; + } + + // Recalculate all creator balances based on UNPAID earnings only + $creators = get_users(array('role' => 'wpdd_creator')); + + foreach ($creators as $creator) { + // Calculate available earnings only (not pending or already paid) + $available_earnings = $wpdb->get_var($wpdb->prepare( + "SELECT SUM(creator_earning) + FROM {$wpdb->prefix}wpdd_creator_earnings + WHERE creator_id = %d + AND payout_status = 'available'", + $creator->ID + )); + + // Calculate balance adjustments + $total_adjustments = $wpdb->get_var($wpdb->prepare( + "SELECT SUM(CASE + WHEN adjustment_type = 'add' THEN amount + WHEN adjustment_type = 'subtract' THEN -amount + ELSE 0 + END) + FROM {$wpdb->prefix}wpdd_balance_adjustments + WHERE creator_id = %d", + $creator->ID + )); + + $available_earnings = floatval($available_earnings); + $total_adjustments = floatval($total_adjustments); + + $balance = $available_earnings + $total_adjustments; + + // Update creator balance + update_user_meta($creator->ID, 'wpdd_creator_balance', $balance); + + // Get total earnings for display + $total_earnings = $wpdb->get_var($wpdb->prepare( + "SELECT SUM(creator_earning) + FROM {$wpdb->prefix}wpdd_creator_earnings + WHERE creator_id = %d", + $creator->ID + )); + + // Get paid out amount + $paid_out = $wpdb->get_var($wpdb->prepare( + "SELECT SUM(creator_earning) + FROM {$wpdb->prefix}wpdd_creator_earnings + WHERE creator_id = %d + AND payout_status = 'paid'", + $creator->ID + )); + + $results[] = sprintf( + "Updated balance for %s: Total Earnings: $%.2f, Paid Out: $%.2f, Adjustments: $%.2f, Current Balance: $%.2f", + $creator->display_name, + floatval($total_earnings), + floatval($paid_out), + $total_adjustments, + $balance + ); + } + + return $results; +} + +// Add admin page to run this check +add_action('admin_menu', function() { + add_submenu_page( + 'edit.php?post_type=wpdd_product', + 'Tools', + 'Tools', + 'manage_options', + 'wpdd-tools', + function() { + $active_tab = isset($_GET['tab']) ? sanitize_text_field($_GET['tab']) : 'system'; + ?> +
+

WP Digital Download - Tools

+ + + + +
+ + +
+

đź”§ Database Check & Repair

+ +

Database check completed!

'; + echo '

Results:

'; + echo '
    '; + foreach ($results as $result) { + echo '
  • ' . esc_html($result) . '
  • '; + } + echo '
'; + } + ?> + +

This tool will:

+
    +
  • Check and create missing database tables
  • +
  • Add payout tracking columns if missing
  • +
  • Add earning records for completed orders
  • +
  • Recalculate creator balances
  • +
  • Update pending earnings with proper dates
  • +
+ +
+ +

+ +

+
+
+ + +
+

📊 System Status

+

Quick overview of your plugin status:

+ + PayPal: ' . ($paypal_configured ? 'âś“ Configured' : 'âś— Not Configured') . '

'; + + if ($paypal_configured) { + echo '

Client ID: ' . substr($paypal_client_id, 0, 10) . '...

'; + } + + // PayPal Mode + $paypal_mode = get_option('wpdd_paypal_mode', 'sandbox'); + echo '

PayPal Mode: ' . ucfirst(esc_html($paypal_mode)) . '

'; + + // Currency + $currency = get_option('wpdd_currency', 'USD'); + echo '

Currency: ' . esc_html($currency) . '

'; + + // Commission Rate + $commission = get_option('wpdd_commission_rate', 0); + echo '

Platform Commission: ' . floatval($commission) . '%

'; + + // Holding Period + $holding = get_option('wpdd_earnings_holding_days', 15); + echo '

Earnings Hold: ' . intval($holding) . ' days

'; + + // Product Count + $products = wp_count_posts('wpdd_product'); + echo '

Products: ' . intval($products->publish) . ' published

'; + + // Creator Count + $creators = count(get_users(array('role' => 'wpdd_creator'))); + echo '

Creators: ' . $creators . '

'; + ?> +
+ +
+ + +
+

🔍 Settings Debug Info

+

If settings are resetting, check these values:

+
+ ' . esc_html($setting) . ':
' . esc_html($display_value ?: '(empty)') . '
'; + } + ?> +
+ +
+ + + +
+

🔑 License Tools

+

Manage and regenerate license keys for completed orders.

+ +

âś… License generated successfully for order #' . $order_id . ': ' . esc_html($result) . '

'; + } else { + echo '

⚠️ License generation completed for order #' . $order_id . '. Check error logs if no license was created.

'; + } + } + + // Handle bulk license generation + if (isset($_POST['generate_all_missing']) && wp_verify_nonce($_POST['_wpnonce'], 'wpdd_generate_all_licenses')) { + $orders_without_licenses = $wpdb->get_results(" + SELECT o.id + FROM {$wpdb->prefix}wpdd_orders o + LEFT JOIN {$wpdb->posts} p ON o.product_id = p.ID + LEFT JOIN {$wpdb->prefix}wpdd_licenses l ON o.id = l.order_id + WHERE o.status = 'completed' + AND l.id IS NULL + AND p.post_type = 'wpdd_product' + "); + + $generated = 0; + foreach ($orders_without_licenses as $order) { + $result = WPDD_License_Manager::generate_license_for_order($order->id); + if ($result) { + $generated++; + } + } + + echo '

âś… Generated ' . $generated . ' license keys out of ' . count($orders_without_licenses) . ' eligible orders.

'; + } + + // Get orders without license keys for software products + $orders_without_licenses = $wpdb->get_results(" + SELECT o.*, p.post_title as product_name, + pm_type.meta_value as product_type, + pm_git.meta_value as git_repository + FROM {$wpdb->prefix}wpdd_orders o + LEFT JOIN {$wpdb->posts} p ON o.product_id = p.ID + LEFT JOIN {$wpdb->prefix}wpdd_licenses l ON o.id = l.order_id + LEFT JOIN {$wpdb->postmeta} pm_type ON (p.ID = pm_type.post_id AND pm_type.meta_key = '_wpdd_product_type') + LEFT JOIN {$wpdb->postmeta} pm_git ON (p.ID = pm_git.post_id AND pm_git.meta_key = '_wpdd_git_repository') + WHERE o.status = 'completed' + AND l.id IS NULL + AND p.post_type = 'wpdd_product' + AND (pm_type.meta_value = 'software_license' OR pm_git.meta_value IS NOT NULL) + ORDER BY o.id DESC + "); + ?> + + +
+
+ + +
+
+ +

Orders Missing License Keys

+ + + + + + + + + + + + + + + + + + + + + + + + + +
Order IDOrder NumberProductProduct TypeCustomer EmailDateAction
id; ?>order_number); ?> + product_name); ?> + git_repository)): ?> +
Git: git_repository); ?> + +
product_type ?: 'digital_download'); ?>customer_email); ?>purchase_date)); ?> +
+ + + +
+
+ +
+

âś… All eligible software orders have license keys assigned.

+
+ + +
+ +

🔍 License Statistics

+
+ get_var("SELECT COUNT(*) FROM {$wpdb->prefix}wpdd_licenses"); + $active_licenses = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}wpdd_licenses WHERE status = 'active'"); + $expired_licenses = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}wpdd_licenses WHERE expires_at IS NOT NULL AND expires_at < NOW()"); + + $software_orders = $wpdb->get_var(" + SELECT COUNT(*) + FROM {$wpdb->prefix}wpdd_orders o + LEFT JOIN {$wpdb->posts} p ON o.product_id = p.ID + LEFT JOIN {$wpdb->postmeta} pm_type ON (p.ID = pm_type.post_id AND pm_type.meta_key = '_wpdd_product_type') + LEFT JOIN {$wpdb->postmeta} pm_git ON (p.ID = pm_git.post_id AND pm_git.meta_key = '_wpdd_git_repository') + WHERE o.status = 'completed' + AND p.post_type = 'wpdd_product' + AND (pm_type.meta_value = 'software_license' OR pm_git.meta_value IS NOT NULL) + "); + + $missing_licenses = count($orders_without_licenses); + ?> + +
+

Total Licenses

+
+
+ +
+

Active Licenses

+
+
+ +
+

Missing Licenses

+
+
+
+ + 0): ?> +
+ ⚠️ Notice: license(s) have expired. +
+ + + + + +
+

đź“§ Email Test

+

Test your email configuration by sending a test email.

+ + ', + 'Content-Type: text/html; charset=UTF-8' + ); + + // Configure SMTP if enabled + if ($smtp_enabled) { + add_action('phpmailer_init', 'wpdd_configure_smtp'); + } + + // Send email + $sent = wp_mail($to_email, $subject, nl2br($message), $headers); + + if ($sent) { + echo '

âś… Test email sent successfully to ' . esc_html($to_email) . '

'; + } else { + global $phpmailer; + $error_info = ''; + if (isset($phpmailer) && is_object($phpmailer) && !empty($phpmailer->ErrorInfo)) { + $error_info = $phpmailer->ErrorInfo; + } + echo '

❌ Failed to send test email. ' . esc_html($error_info) . '

'; + } + } else { + echo '

Please enter a valid email address.

'; + } + } + ?> + +
+ + + + + + + + + + + + + + + +
+ +

Email address to send the test email to.

+
+ +
+ +
+ +

+ +

+
+ +
+ +

đź“‹ Current Email Configuration

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SMTP Enabled:
From Email:
From Name:
SMTP Host:
SMTP Port:
SMTP Encryption:
SMTP Username:
+
+ + + +
+

📨 Email Logs

+

View the last 100 emails sent by the plugin.

+ + prefix . 'wpdd_email_logs'; + + // Check if table exists + if ($wpdb->get_var("SHOW TABLES LIKE '$table_name'") != $table_name) { + echo '

Email logs table not found. It will be created when the first email is sent.

'; + } else { + // Get email logs + $logs = $wpdb->get_results( + "SELECT * FROM $table_name ORDER BY sent_at DESC LIMIT 100" + ); + + if (empty($logs)) { + echo '

No email logs found.

'; + } else { + ?> + + + + + + + + + + + + + + + + + + + + + + + + + + +
Date/TimeToSubjectStatusTypeActions
sent_at); ?>to_email); ?>subject); ?> + status == 'sent') : ?> + ✅ Sent + + ❌ Failed + + email_type); ?> + +
+ + + +
+ + + + + isSMTP(); + $phpmailer->Host = get_option('wpdd_smtp_host'); + $phpmailer->Port = get_option('wpdd_smtp_port', 587); + $phpmailer->SMTPAuth = true; + $phpmailer->Username = get_option('wpdd_smtp_username'); + $phpmailer->Password = get_option('wpdd_smtp_password'); + $phpmailer->SMTPSecure = get_option('wpdd_smtp_encryption', 'tls'); + $phpmailer->From = get_option('wpdd_from_email', get_option('admin_email')); + $phpmailer->FromName = get_option('wpdd_from_name', get_bloginfo('name')); +} \ No newline at end of file diff --git a/assets/css/frontend.css b/assets/css/frontend.css index 173a1ea..5447116 100644 --- a/assets/css/frontend.css +++ b/assets/css/frontend.css @@ -466,4 +466,63 @@ .wpdd-login-required a { color: #856404; font-weight: bold; +} + +/* License Key Display */ +.wpdd-license-row { + background: #f8f9fa; +} + +.wpdd-license-cell { + padding: 8px 12px !important; + border-top: 1px solid #ddd; +} + +.wpdd-license-info { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + margin: 0; +} + +.wpdd-license-info small { + color: #666; + font-weight: 500; + white-space: nowrap; +} + +.wpdd-license-key { + background: #f8f9fa; + padding: 4px 8px; + border-radius: 4px; + font-family: monospace; + font-size: 12px; + border: 1px solid #ddd; + color: #333; +} + +.wpdd-copy-license { + background: #0073aa; + color: white; + border: none; + padding: 4px 8px; + border-radius: 4px; + cursor: pointer; + font-size: 12px; + line-height: 1; + transition: background 0.3s; + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 24px; + height: 24px; +} + +.wpdd-copy-license:hover { + background: #005a87; +} + +.wpdd-copy-license i { + font-size: 11px; } \ No newline at end of file diff --git a/assets/js/admin-order-manager.js b/assets/js/admin-order-manager.js new file mode 100644 index 0000000..827a849 --- /dev/null +++ b/assets/js/admin-order-manager.js @@ -0,0 +1,31 @@ +jQuery(document).ready(function($) { + // Handle cancel order button clicks + $('.wpdd-cancel-btn').on('click', function(e) { + if (!confirm(wpdd_order_manager.confirm_cancel)) { + e.preventDefault(); + return false; + } + }); + + // Handle refund order button clicks + $('.wpdd-refund-btn').on('click', function(e) { + if (!confirm(wpdd_order_manager.confirm_refund)) { + e.preventDefault(); + return false; + } + }); + + // Handle release earnings button clicks + $('.wpdd-release-btn').on('click', function(e) { + if (!confirm(wpdd_order_manager.confirm_release)) { + e.preventDefault(); + return false; + } + }); + + // Auto-submit form when filter values change (optional convenience feature) + $('#status, #creator').on('change', function() { + // Uncomment the next line if you want auto-submit on filter change + // $(this).closest('form').submit(); + }); +}); \ No newline at end of file diff --git a/assets/js/admin-payouts.js b/assets/js/admin-payouts.js index a5447b3..90f2917 100644 --- a/assets/js/admin-payouts.js +++ b/assets/js/admin-payouts.js @@ -1,4 +1,39 @@ jQuery(document).ready(function($) { + // Format currency + function formatCurrency(amount) { + return '$' + parseFloat(amount).toFixed(2); + } + + // Handle creator selection for manual payout + $('#creator_id').on('change', function() { + var selectedOption = $(this).find('option:selected'); + var balance = selectedOption.data('balance'); + var balanceDisplay = $('#creator_balance_display'); + var balanceAmount = $('#creator_balance_amount'); + + if ($(this).val() && balance !== undefined) { + balanceAmount.text(formatCurrency(balance)); + balanceDisplay.show(); + } else { + balanceDisplay.hide(); + } + }); + + // Handle creator selection for balance adjustment + $('#adj_creator_id').on('change', function() { + var selectedOption = $(this).find('option:selected'); + var balance = selectedOption.data('balance'); + var balanceDisplay = $('#adj_creator_balance_display'); + var balanceAmount = $('#adj_creator_balance_amount'); + + if ($(this).val() && balance !== undefined) { + balanceAmount.text(formatCurrency(balance)); + balanceDisplay.show(); + } else { + balanceDisplay.hide(); + } + }); + // Handle individual payout processing $('.wpdd-process-payout').on('click', function(e) { e.preventDefault(); diff --git a/assets/js/admin.js b/assets/js/admin.js index f7a610f..c4165ad 100644 --- a/assets/js/admin.js +++ b/assets/js/admin.js @@ -10,6 +10,7 @@ jQuery(document).ready(function($) { this.initFileManager(); this.initPriceToggle(); this.initFormValidation(); + this.initProductSync(); }, initFileManager: function() { @@ -324,6 +325,51 @@ jQuery(document).ready(function($) { $('.wpdd-settings-content').hide(); $(target).show(); }); + }, + + initProductSync: function() { + // Handle software product sync + $(document).on('click', '.wpdd-sync-product', function(e) { + e.preventDefault(); + + var $button = $(this); + var productId = $button.data('product-id'); + var nonce = $button.data('nonce'); + + if (!productId || !nonce) { + WPDD_Admin.showAdminNotice('Invalid sync request', 'error'); + return; + } + + // Show loading state + $button.prop('disabled', true).text('Syncing...'); + + $.ajax({ + url: ajaxurl, + type: 'POST', + data: { + action: 'wpdd_sync_software_product', + product_id: productId, + nonce: nonce + }, + success: function(response) { + if (response.success) { + WPDD_Admin.showAdminNotice(response.message, 'success'); + // Reload the page to show updated file count + setTimeout(function() { + window.location.reload(); + }, 1500); + } else { + WPDD_Admin.showAdminNotice(response.message || 'Sync failed', 'error'); + $button.prop('disabled', false).text('Sync Files'); + } + }, + error: function() { + WPDD_Admin.showAdminNotice('Sync request failed', 'error'); + $button.prop('disabled', false).text('Sync Files'); + } + }); + }); } }; diff --git a/assets/js/frontend.js b/assets/js/frontend.js index a084b4f..6bc1c3e 100644 --- a/assets/js/frontend.js +++ b/assets/js/frontend.js @@ -27,6 +27,9 @@ jQuery(document).ready(function($) { // Download status check $(document).on('click', '.wpdd-check-download', this.checkDownloadStatus); + + // Copy license key + $(document).on('click', '.wpdd-copy-license', this.copyLicenseKey); }, initProductCards: function() { @@ -358,6 +361,61 @@ jQuery(document).ready(function($) { formatPrice: function(price) { return '$' + parseFloat(price).toFixed(2); + }, + + copyLicenseKey: function(e) { + e.preventDefault(); + + var $button = $(this); + var licenseKey = $button.data('license'); + + if (!licenseKey) { + WPDD.showNotice('No license key to copy', 'error'); + return; + } + + // Try to use the modern clipboard API + if (navigator.clipboard && window.isSecureContext) { + navigator.clipboard.writeText(licenseKey).then(function() { + WPDD.showNotice('License key copied to clipboard!', 'success'); + $button.text('Copied!'); + setTimeout(function() { + $button.text('Copy'); + }, 2000); + }).catch(function() { + WPDD.fallbackCopyTextToClipboard(licenseKey, $button); + }); + } else { + WPDD.fallbackCopyTextToClipboard(licenseKey, $button); + } + }, + + fallbackCopyTextToClipboard: function(text, $button) { + var textArea = document.createElement('textarea'); + textArea.value = text; + textArea.style.position = 'fixed'; + textArea.style.left = '-999999px'; + textArea.style.top = '-999999px'; + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + + try { + var successful = document.execCommand('copy'); + if (successful) { + WPDD.showNotice('License key copied to clipboard!', 'success'); + $button.text('Copied!'); + setTimeout(function() { + $button.text('Copy'); + }, 2000); + } else { + WPDD.showNotice('Failed to copy license key', 'error'); + } + } catch (err) { + WPDD.showNotice('Failed to copy license key', 'error'); + } + + document.body.removeChild(textArea); } }; diff --git a/includes/class-wpdd-api.php b/includes/class-wpdd-api.php index 818d088..7fac882 100644 --- a/includes/class-wpdd-api.php +++ b/includes/class-wpdd-api.php @@ -371,8 +371,22 @@ class WPDD_API { array('%d', '%d', '%d', '%s', '%s', '%s', '%s') ); - // Serve file - $filename = basename($package_path); + // Serve file with proper filename + $original_filename = basename($package_path); + $original_extension = pathinfo($original_filename, PATHINFO_EXTENSION); + + // If no extension, assume it's a zip file + if (empty($original_extension)) { + $original_extension = 'zip'; + } + + // Create sanitized filename using product name and version + $product_name = $product->post_title; + $version = $latest_version->version; + $safe_name = str_replace([' ', '.'], ['-', '_'], $product_name . ' v' . $version); + $safe_name = sanitize_file_name($safe_name); + $filename = $safe_name . '.' . $original_extension; + header('Content-Type: application/zip'); header('Content-Disposition: attachment; filename="' . $filename . '"'); header('Content-Length: ' . filesize($package_path)); @@ -632,6 +646,27 @@ class WPDD_API { // Update product version meta update_post_meta($product_id, '_wpdd_current_version', $version); + // Update product files to include the new package + $files = get_post_meta($product_id, '_wpdd_files', true); + if (!is_array($files)) { + $files = array(); + } + + // Add or update the package file in the files list + $package_file = array( + 'id' => 'package_' . $version, + 'name' => get_the_title($product_id) . ' v' . $version, + 'url' => $package_url + ); + + // Remove any existing package entries and add the new one as the first file + $files = array_filter($files, function($file) { + return !isset($file['id']) || strpos($file['id'], 'package_') !== 0; + }); + array_unshift($files, $package_file); + + update_post_meta($product_id, '_wpdd_files', $files); + // Notify customers about update (optional) self::notify_customers_about_update($product_id, $version); @@ -782,4 +817,128 @@ class WPDD_API { // Optional: Send email notifications to customers with active licenses // This could be a separate scheduled job to avoid timeout issues } + + /** + * Manually sync a software product with its latest Git release + * This can be used to fix products that don't have files or need updates + */ + public static function sync_software_product($product_id, $force_rebuild = false) { + global $wpdb; + + // Check if product is software license type + $product_type = get_post_meta($product_id, '_wpdd_product_type', true); + if ($product_type !== 'software_license') { + return array( + 'success' => false, + 'error' => 'not_software_license', + 'message' => __('Product is not a software license product.', 'wp-digital-download') + ); + } + + // Get Git repository settings + $git_url = get_post_meta($product_id, '_wpdd_git_repository', true); + $git_username = get_post_meta($product_id, '_wpdd_git_username', true); + $git_token = get_post_meta($product_id, '_wpdd_git_token', true); + $current_version = get_post_meta($product_id, '_wpdd_current_version', true); + + if (!$git_url) { + return array( + 'success' => false, + 'error' => 'no_git_url', + 'message' => __('No Git repository URL configured.', 'wp-digital-download') + ); + } + + if (!$current_version) { + return array( + 'success' => false, + 'error' => 'no_version', + 'message' => __('No current version specified. Please set a version in the product settings.', 'wp-digital-download') + ); + } + + // Check if we already have this version + $existing_version = $wpdb->get_row($wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}wpdd_software_versions + WHERE product_id = %d AND version = %s", + $product_id, + $current_version + )); + + $package_url = null; + + if (!$existing_version || $force_rebuild) { + // Build package from current version + $package_url = self::build_package_from_git($product_id, $git_url, 'v' . $current_version, $git_username, $git_token); + + if (!$package_url) { + // Try without 'v' prefix + $package_url = self::build_package_from_git($product_id, $git_url, $current_version, $git_username, $git_token); + } + + if (!$package_url) { + return array( + 'success' => false, + 'error' => 'build_failed', + 'message' => __('Failed to build package from repository.', 'wp-digital-download') + ); + } + + // Insert or update version record + if ($existing_version) { + $wpdb->update( + $wpdb->prefix . 'wpdd_software_versions', + array( + 'package_url' => $package_url, + 'release_date' => current_time('mysql') + ), + array('id' => $existing_version->id), + array('%s', '%s'), + array('%d') + ); + } else { + $wpdb->insert( + $wpdb->prefix . 'wpdd_software_versions', + array( + 'product_id' => $product_id, + 'version' => $current_version, + 'package_url' => $package_url, + 'git_tag' => 'v' . $current_version, + 'release_date' => current_time('mysql') + ), + array('%d', '%s', '%s', '%s', '%s') + ); + } + } else { + $package_url = $existing_version->package_url; + } + + // Update product files to include the package + $files = get_post_meta($product_id, '_wpdd_files', true); + if (!is_array($files)) { + $files = array(); + } + + // Add or update the package file in the files list + $package_file = array( + 'id' => 'package_' . $current_version, + 'name' => get_the_title($product_id) . ' v' . $current_version, + 'url' => $package_url + ); + + // Remove any existing package entries and add the new one as the first file + $files = array_filter($files, function($file) { + return !isset($file['id']) || strpos($file['id'], 'package_') !== 0; + }); + array_unshift($files, $package_file); + + update_post_meta($product_id, '_wpdd_files', $files); + + return array( + 'success' => true, + 'message' => __('Product synced successfully.', 'wp-digital-download'), + 'version' => $current_version, + 'package_url' => $package_url + ); + } } \ No newline at end of file diff --git a/includes/class-wpdd-creator.php b/includes/class-wpdd-creator.php index ae7ebc2..50627d7 100644 --- a/includes/class-wpdd-creator.php +++ b/includes/class-wpdd-creator.php @@ -86,14 +86,72 @@ class WPDD_Creator { } public static function get_creator_balance($user_id) { - return floatval(get_user_meta($user_id, 'wpdd_creator_balance', true)); + global $wpdb; + + // Get balance from user meta (for backward compatibility and manual adjustments) + $meta_balance = floatval(get_user_meta($user_id, 'wpdd_creator_balance', true)); + + // If we have the creator_earnings table, calculate from there + $table_exists = $wpdb->get_var("SHOW TABLES LIKE '{$wpdb->prefix}wpdd_creator_earnings'") == $wpdb->prefix . 'wpdd_creator_earnings'; + + if ($table_exists) { + // Check if payout_status column exists + $columns = $wpdb->get_results("SHOW COLUMNS FROM {$wpdb->prefix}wpdd_creator_earnings"); + $has_payout_status = false; + foreach ($columns as $column) { + if ($column->Field == 'payout_status') { + $has_payout_status = true; + break; + } + } + + if ($has_payout_status) { + // Calculate available earnings (not pending, not paid) + $available_earnings = $wpdb->get_var($wpdb->prepare( + "SELECT SUM(creator_earning) + FROM {$wpdb->prefix}wpdd_creator_earnings + WHERE creator_id = %d + AND payout_status = 'available'", + $user_id + )); + + // Calculate balance adjustments + $adjustments_table_exists = $wpdb->get_var("SHOW TABLES LIKE '{$wpdb->prefix}wpdd_balance_adjustments'") == $wpdb->prefix . 'wpdd_balance_adjustments'; + $total_adjustments = 0; + + if ($adjustments_table_exists) { + $total_adjustments = $wpdb->get_var($wpdb->prepare( + "SELECT SUM(CASE + WHEN adjustment_type = 'add' THEN amount + WHEN adjustment_type = 'subtract' THEN -amount + ELSE 0 + END) + FROM {$wpdb->prefix}wpdd_balance_adjustments + WHERE creator_id = %d", + $user_id + )); + } + + $calculated_balance = floatval($available_earnings) + floatval($total_adjustments); + + // Update the meta if different + if (abs($calculated_balance - $meta_balance) > 0.01) { + update_user_meta($user_id, 'wpdd_creator_balance', $calculated_balance); + } + + return $calculated_balance; + } + } + + // Fall back to meta balance + return $meta_balance; } public static function get_creator_total_earnings($user_id) { global $wpdb; $total = $wpdb->get_var($wpdb->prepare( - "SELECT SUM(o.total) + "SELECT SUM(o.amount) FROM {$wpdb->prefix}wpdd_orders o INNER JOIN {$wpdb->posts} p ON o.product_id = p.ID WHERE p.post_author = %d @@ -130,12 +188,20 @@ class WPDD_Creator { $creator_id = $product->post_author; $commission_rate = floatval(get_option('wpdd_commission_rate', 0)); - $creator_share = $order->total * (1 - ($commission_rate / 100)); + $creator_share = $order->amount * (1 - ($commission_rate / 100)); // Update creator balance $current_balance = self::get_creator_balance($creator_id); update_user_meta($creator_id, 'wpdd_creator_balance', $current_balance + $creator_share); + // Calculate when earnings will be available (holding period) + $holding_days = intval(get_option('wpdd_earnings_holding_days', 15)); + $available_at = ($holding_days > 0) ? + date('Y-m-d H:i:s', strtotime('+' . $holding_days . ' days')) : + current_time('mysql'); + + $initial_status = ($holding_days > 0) ? 'pending' : 'available'; + // Log the earning $wpdb->insert( $wpdb->prefix . 'wpdd_creator_earnings', @@ -143,12 +209,14 @@ class WPDD_Creator { 'creator_id' => $creator_id, 'order_id' => $order_id, 'product_id' => $order->product_id, - 'sale_amount' => $order->total, + 'sale_amount' => $order->amount, 'commission_rate' => $commission_rate, 'creator_earning' => $creator_share, + 'payout_status' => $initial_status, + 'available_at' => $available_at, 'created_at' => current_time('mysql') ), - array('%d', '%d', '%d', '%f', '%f', '%f', '%s') + array('%d', '%d', '%d', '%f', '%f', '%f', '%s', '%s', '%s') ); } diff --git a/includes/class-wpdd-download-handler.php b/includes/class-wpdd-download-handler.php index bffc838..1ccc873 100644 --- a/includes/class-wpdd-download-handler.php +++ b/includes/class-wpdd-download-handler.php @@ -87,16 +87,16 @@ class WPDD_Download_Handler { } private static function process_download_by_order() { - $download_link_id = intval($_GET['wpdd_download']); + $order_id = intval($_GET['wpdd_download']); // Debug nonce verification if (current_user_can('manage_options')) { - error_log('WPDD Debug: Download Link ID: ' . $download_link_id); + error_log('WPDD Debug: Order ID: ' . $order_id); error_log('WPDD Debug: Nonce received: ' . ($_GET['_wpnonce'] ?? 'none')); - error_log('WPDD Debug: Expected nonce action: wpdd_download_' . $download_link_id); + error_log('WPDD Debug: Expected nonce action: wpdd_download_' . $order_id); } - if (!wp_verify_nonce($_GET['_wpnonce'] ?? '', 'wpdd_download_' . $download_link_id)) { + if (!wp_verify_nonce($_GET['_wpnonce'] ?? '', 'wpdd_download_' . $order_id)) { if (current_user_can('manage_options')) { error_log('WPDD Debug: Nonce verification failed!'); } @@ -105,18 +105,6 @@ class WPDD_Download_Handler { global $wpdb; - // First get the download link to find the associated order - $download_link = $wpdb->get_row($wpdb->prepare( - "SELECT * FROM {$wpdb->prefix}wpdd_download_links WHERE id = %d", - $download_link_id - )); - - if (!$download_link) { - wp_die(__('Invalid download link.', 'wp-digital-download')); - } - - $order_id = $download_link->order_id; - // Check by email if guest if (isset($_GET['customer_email']) && isset($_GET['key'])) { $email = sanitize_email($_GET['customer_email']); @@ -436,16 +424,17 @@ class WPDD_Download_Handler { if ($file_meta && file_exists($file_meta['file_path'])) { $enable_watermark = get_post_meta($product_id, '_wpdd_enable_watermark', true); + $product_name = get_the_title($product_id); if ($enable_watermark && self::is_watermarkable($file_meta['file_path'])) { $watermarked_file = WPDD_Watermark::apply_watermark($file_meta['file_path'], $order); if ($watermarked_file) { - self::deliver_protected_file($watermarked_file, true); + self::deliver_protected_file($watermarked_file, true, $product_name); } else { - self::deliver_protected_file($file_meta['file_path']); + self::deliver_protected_file($file_meta['file_path'], false, $product_name); } } else { - self::deliver_protected_file($file_meta['file_path']); + self::deliver_protected_file($file_meta['file_path'], false, $product_name); } return; } @@ -464,16 +453,17 @@ class WPDD_Download_Handler { if ($file_meta && file_exists($file_meta['file_path'])) { $enable_watermark = get_post_meta($product_id, '_wpdd_enable_watermark', true); + $product_name = get_the_title($product_id); if ($enable_watermark && self::is_watermarkable($file_meta['file_path'])) { $watermarked_file = WPDD_Watermark::apply_watermark($file_meta['file_path'], $order); if ($watermarked_file) { - self::deliver_protected_file($watermarked_file, true); + self::deliver_protected_file($watermarked_file, true, $product_name); } else { - self::deliver_protected_file($file_meta['file_path']); + self::deliver_protected_file($file_meta['file_path'], false, $product_name); } } else { - self::deliver_protected_file($file_meta['file_path']); + self::deliver_protected_file($file_meta['file_path'], false, $product_name); } return; } @@ -483,15 +473,32 @@ class WPDD_Download_Handler { // Regular file handling (backward compatibility) $enable_watermark = get_post_meta($product_id, '_wpdd_enable_watermark', true); + // Generate proper filename for software license products + $product_type = get_post_meta($product_id, '_wpdd_product_type', true); + $final_filename = $file['name']; + + if ($product_type === 'software_license') { + // Check if this is a package file (contains version info) + if (strpos($file['url'], 'wpdd-packages/') !== false && preg_match('/package-v([^\/]+)\.zip$/', $file['url'], $matches)) { + $version = $matches[1]; + $product_name = get_the_title($product_id); + + // Create sanitized filename using product name and version + $safe_name = str_replace([' ', '.'], ['-', '_'], $product_name . ' v' . $version); + $safe_name = sanitize_file_name($safe_name); + $final_filename = $safe_name . '.zip'; + } + } + if ($enable_watermark && self::is_watermarkable($file['url'])) { $watermarked_file = WPDD_Watermark::apply_watermark($file['url'], $order); if ($watermarked_file) { - self::deliver_file($watermarked_file, $file['name'], true); + self::deliver_file($watermarked_file, $final_filename, true); } else { - self::deliver_file($file['url'], $file['name']); + self::deliver_file($file['url'], $final_filename); } } else { - self::deliver_file($file['url'], $file['name']); + self::deliver_file($file['url'], $final_filename); } } @@ -606,18 +613,77 @@ class WPDD_Download_Handler { self::deliver_protected_file($protected_path); } - private static function deliver_protected_file($file_path, $is_temp = false) { + private static function deliver_protected_file($file_path, $is_temp = false, $product_name = null) { if (!file_exists($file_path)) { wp_die(__('File not found.', 'wp-digital-download')); } - $file_name = basename($file_path); $file_size = filesize($file_path); - $file_type = wp_check_filetype($file_path); + $file_info = wp_check_filetype($file_path); + + // If product name is provided, use it with the original extension + if ($product_name) { + $original_file_name = basename($file_path); + $original_extension = pathinfo($original_file_name, PATHINFO_EXTENSION); + + // If no extension found, try to detect it + if (empty($original_extension)) { + // Check if it's a zip file by reading the first few bytes + $handle = fopen($file_path, 'rb'); + if ($handle) { + $header = fread($handle, 4); + fclose($handle); + + // ZIP files start with PK (0x504B) + if (substr($header, 0, 2) === 'PK') { + $original_extension = 'zip'; + $file_info['type'] = 'application/zip'; + $file_info['ext'] = 'zip'; + } + } + + // If still no extension, try wp_check_filetype result + if (empty($original_extension) && !empty($file_info['ext'])) { + $original_extension = $file_info['ext']; + } + } + + // Sanitize product name for filename use + // Convert spaces to dashes and dots to underscores, then use standard sanitization + $safe_product_name = str_replace([' ', '.'], ['-', '_'], $product_name); + $safe_product_name = sanitize_file_name($safe_product_name); + $file_name = $safe_product_name . ($original_extension ? '.' . $original_extension : ''); + } else { + // Fallback to original logic if no product name provided + $file_name = basename($file_path); + $has_extension = strpos($file_name, '.') !== false; + + // If no extension in filename, try to determine from content + if (!$has_extension) { + // Check if it's a zip file by reading the first few bytes + $handle = fopen($file_path, 'rb'); + if ($handle) { + $header = fread($handle, 4); + fclose($handle); + + // ZIP files start with PK (0x504B) + if (substr($header, 0, 2) === 'PK') { + $file_info['type'] = 'application/zip'; + $file_info['ext'] = 'zip'; + $file_name .= '.zip'; + } + } + } + + // If we still don't have an extension but wp_check_filetype detected one, add it + if (!$has_extension && !empty($file_info['ext']) && strpos($file_name, '.' . $file_info['ext']) === false) { + $file_name .= '.' . $file_info['ext']; + } + } nocache_headers(); - header('Content-Type: ' . ($file_type['type'] ?: 'application/octet-stream')); + header('Content-Type: ' . ($file_info['type'] ?: 'application/octet-stream')); header('Content-Disposition: attachment; filename="' . $file_name . '"'); header('Content-Length: ' . $file_size); header('Content-Transfer-Encoding: binary'); diff --git a/includes/class-wpdd-earnings-processor.php b/includes/class-wpdd-earnings-processor.php new file mode 100644 index 0000000..e4b79aa --- /dev/null +++ b/includes/class-wpdd-earnings-processor.php @@ -0,0 +1,189 @@ +get_results( + "SELECT * FROM {$wpdb->prefix}wpdd_creator_earnings + WHERE payout_status = 'pending' + AND available_at <= NOW() + AND available_at IS NOT NULL" + ); + + if (empty($pending_earnings)) { + return; // Nothing to process + } + + // Update all pending earnings to available + $updated = $wpdb->query( + "UPDATE {$wpdb->prefix}wpdd_creator_earnings + SET payout_status = 'available' + WHERE payout_status = 'pending' + AND available_at <= NOW() + AND available_at IS NOT NULL" + ); + + if ($updated > 0) { + // Log the processing + error_log("WPDD: Processed $updated pending earnings to available status"); + + // Update creator balances for affected creators + $affected_creators = $wpdb->get_col( + "SELECT DISTINCT creator_id + FROM {$wpdb->prefix}wpdd_creator_earnings + WHERE payout_status = 'available' + AND available_at <= NOW()" + ); + + foreach ($affected_creators as $creator_id) { + // Trigger balance recalculation + $current_balance = WPDD_Creator::get_creator_balance($creator_id); + update_user_meta($creator_id, 'wpdd_creator_balance', $current_balance); + } + } + } + + /** + * Manually process a specific earning (for admin override) + */ + public static function release_earning_immediately($earning_id) { + global $wpdb; + + $result = $wpdb->update( + $wpdb->prefix . 'wpdd_creator_earnings', + array( + 'payout_status' => 'available', + 'available_at' => current_time('mysql') + ), + array( + 'id' => $earning_id, + 'payout_status' => 'pending' + ), + array('%s', '%s'), + array('%d', '%s') + ); + + if ($result) { + // Get the creator and update their balance + $earning = $wpdb->get_row($wpdb->prepare( + "SELECT creator_id FROM {$wpdb->prefix}wpdd_creator_earnings WHERE id = %d", + $earning_id + )); + + if ($earning) { + $current_balance = WPDD_Creator::get_creator_balance($earning->creator_id); + update_user_meta($earning->creator_id, 'wpdd_creator_balance', $current_balance); + } + } + + return $result > 0; + } + + /** + * Cancel/refund a specific earning (for order cancellations) + */ + public static function cancel_earning($earning_id, $reason = 'Order cancelled/refunded') { + global $wpdb; + + // Get the earning details + $earning = $wpdb->get_row($wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}wpdd_creator_earnings WHERE id = %d", + $earning_id + )); + + if (!$earning) { + return false; + } + + // Only cancel if not already paid out + if ($earning->payout_status === 'paid') { + return false; // Cannot cancel paid earnings + } + + // Update to cancelled status + $result = $wpdb->update( + $wpdb->prefix . 'wpdd_creator_earnings', + array( + 'payout_status' => 'cancelled', + 'available_at' => null + ), + array('id' => $earning_id), + array('%s', '%s'), + array('%d') + ); + + if ($result) { + // Log the cancellation + $wpdb->insert( + $wpdb->prefix . 'wpdd_balance_adjustments', + array( + 'creator_id' => $earning->creator_id, + 'adjustment_type' => 'subtract', + 'amount' => $earning->creator_earning, + 'previous_balance' => WPDD_Creator::get_creator_balance($earning->creator_id), + 'new_balance' => WPDD_Creator::get_creator_balance($earning->creator_id) - $earning->creator_earning, + 'reason' => $reason . ' (Order #' . $earning->order_id . ')', + 'adjusted_by' => get_current_user_id(), + 'created_at' => current_time('mysql') + ), + array('%d', '%s', '%f', '%f', '%f', '%s', '%d', '%s') + ); + + // Update creator balance + $current_balance = WPDD_Creator::get_creator_balance($earning->creator_id); + update_user_meta($earning->creator_id, 'wpdd_creator_balance', $current_balance - $earning->creator_earning); + } + + return $result > 0; + } + + /** + * Get earnings summary for a creator + */ + public static function get_earnings_summary($creator_id) { + global $wpdb; + + $summary = $wpdb->get_row($wpdb->prepare( + "SELECT + COUNT(*) as total_earnings, + SUM(CASE WHEN payout_status = 'pending' THEN creator_earning ELSE 0 END) as pending_amount, + SUM(CASE WHEN payout_status = 'available' THEN creator_earning ELSE 0 END) as available_amount, + SUM(CASE WHEN payout_status = 'paid' THEN creator_earning ELSE 0 END) as paid_amount, + SUM(CASE WHEN payout_status = 'cancelled' THEN creator_earning ELSE 0 END) as cancelled_amount, + SUM(creator_earning) as total_amount + FROM {$wpdb->prefix}wpdd_creator_earnings + WHERE creator_id = %d", + $creator_id + )); + + return $summary; + } +} \ No newline at end of file diff --git a/includes/class-wpdd-email.php b/includes/class-wpdd-email.php new file mode 100644 index 0000000..f4fa186 --- /dev/null +++ b/includes/class-wpdd-email.php @@ -0,0 +1,211 @@ +', + 'Content-Type: text/html; charset=UTF-8' + ); + } + + // Send the email + $sent = wp_mail($to, $subject, $message, $headers); + + // Log the email + $table_name = $wpdb->prefix . 'wpdd_email_logs'; + + // Create table if it doesn't exist + if ($wpdb->get_var("SHOW TABLES LIKE '$table_name'") != $table_name) { + self::create_email_logs_table(); + } + + // Get error message if failed + $error_message = ''; + if (!$sent) { + global $phpmailer; + if (isset($phpmailer) && is_object($phpmailer) && !empty($phpmailer->ErrorInfo)) { + $error_message = $phpmailer->ErrorInfo; + } + } + + // Insert log entry + $wpdb->insert( + $table_name, + array( + 'to_email' => $to, + 'subject' => $subject, + 'message' => $message, + 'status' => $sent ? 'sent' : 'failed', + 'email_type' => $email_type, + 'error_message' => $error_message, + 'sent_at' => current_time('mysql') + ), + array('%s', '%s', '%s', '%s', '%s', '%s', '%s') + ); + + return $sent; + } + + /** + * Configure SMTP settings for PHPMailer + */ + public static function configure_smtp($phpmailer) { + $phpmailer->isSMTP(); + $phpmailer->Host = get_option('wpdd_smtp_host'); + $phpmailer->Port = get_option('wpdd_smtp_port', 587); + $phpmailer->SMTPAuth = true; + $phpmailer->Username = get_option('wpdd_smtp_username'); + $phpmailer->Password = get_option('wpdd_smtp_password'); + $phpmailer->SMTPSecure = get_option('wpdd_smtp_encryption', 'tls'); + $phpmailer->From = get_option('wpdd_from_email', get_option('admin_email')); + $phpmailer->FromName = get_option('wpdd_from_name', get_bloginfo('name')); + + // Enable debugging for failed sends + if (defined('WP_DEBUG') && WP_DEBUG) { + $phpmailer->SMTPDebug = 2; + $phpmailer->Debugoutput = 'error_log'; + } + } + + /** + * Create email logs table if it doesn't exist + */ + private static function create_email_logs_table() { + global $wpdb; + + $table_name = $wpdb->prefix . 'wpdd_email_logs'; + $charset_collate = $wpdb->get_charset_collate(); + + $sql = "CREATE TABLE IF NOT EXISTS $table_name ( + id bigint(20) NOT NULL AUTO_INCREMENT, + to_email varchar(100) NOT NULL, + subject varchar(255) NOT NULL, + message longtext NOT NULL, + status varchar(20) NOT NULL DEFAULT 'sent', + email_type varchar(50) DEFAULT 'general', + error_message text DEFAULT NULL, + sent_at datetime DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id), + KEY to_email (to_email), + KEY status (status), + KEY email_type (email_type), + KEY sent_at (sent_at) + ) $charset_collate;"; + + require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); + dbDelta($sql); + } + + /** + * Send order confirmation email + */ + public static function send_order_confirmation($order_id) { + global $wpdb; + + $order = $wpdb->get_row($wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}wpdd_orders WHERE id = %d", + $order_id + )); + + if (!$order) { + return false; + } + + $product = get_post($order->product_id); + if (!$product) { + return false; + } + + $subject = sprintf(__('Order Confirmation - %s', 'wp-digital-download'), $product->post_title); + + $message = sprintf( + __('

Thank you for your purchase!

+

Your order has been confirmed.

+

Order Details:

+
    +
  • Order ID: #%d
  • +
  • Product: %s
  • +
  • Amount: %s
  • +
  • Date: %s
  • +
+

You can download your purchase from your account page.

+

Thank you for your business!

', 'wp-digital-download'), + $order->id, + esc_html($product->post_title), + wpdd_format_price($order->amount, $order->currency), + $order->purchase_date + ); + + return self::send($order->customer_email, $subject, $message, 'order_confirmation'); + } + + /** + * Send download link email + */ + public static function send_download_link($order_id, $download_link) { + global $wpdb; + + $order = $wpdb->get_row($wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}wpdd_orders WHERE id = %d", + $order_id + )); + + if (!$order) { + return false; + } + + $product = get_post($order->product_id); + if (!$product) { + return false; + } + + $subject = sprintf(__('Your Download Link - %s', 'wp-digital-download'), $product->post_title); + + $message = sprintf( + __('

Your Download is Ready!

+

Click the link below to download your purchase:

+

Download Now

+

Product:

+

%s

+

Note: This download link will expire. Please download your file as soon as possible.

+

Thank you!

', 'wp-digital-download'), + esc_url($download_link), + esc_html($product->post_title) + ); + + return self::send($order->customer_email, $subject, $message, 'download_link'); + } +} \ No newline at end of file diff --git a/includes/class-wpdd-install.php b/includes/class-wpdd-install.php index 15abe82..004f477 100644 --- a/includes/class-wpdd-install.php +++ b/includes/class-wpdd-install.php @@ -104,11 +104,17 @@ class WPDD_Install { sale_amount decimal(10,2) NOT NULL, commission_rate decimal(5,2) NOT NULL, creator_earning decimal(10,2) NOT NULL, + payout_id bigint(20) DEFAULT NULL, + payout_status varchar(20) DEFAULT 'pending', + available_at datetime DEFAULT NULL, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY creator_id (creator_id), KEY order_id (order_id), - KEY product_id (product_id) + KEY product_id (product_id), + KEY payout_id (payout_id), + KEY payout_status (payout_status), + KEY available_at (available_at) ) $charset_collate;"; $sql[] = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}wpdd_payouts ( @@ -146,6 +152,22 @@ class WPDD_Install { ) $charset_collate;"; // Software Licensing Tables + $sql[] = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}wpdd_email_logs ( + id bigint(20) NOT NULL AUTO_INCREMENT, + to_email varchar(100) NOT NULL, + subject varchar(255) NOT NULL, + message longtext NOT NULL, + status varchar(20) NOT NULL DEFAULT 'sent', + email_type varchar(50) DEFAULT 'general', + error_message text DEFAULT NULL, + sent_at datetime DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id), + KEY to_email (to_email), + KEY status (status), + KEY email_type (email_type), + KEY sent_at (sent_at) + ) $charset_collate;"; + $sql[] = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}wpdd_licenses ( id bigint(20) NOT NULL AUTO_INCREMENT, license_key varchar(64) NOT NULL, diff --git a/includes/class-wpdd-license-manager.php b/includes/class-wpdd-license-manager.php index 1ed1a31..9afd6bb 100644 --- a/includes/class-wpdd-license-manager.php +++ b/includes/class-wpdd-license-manager.php @@ -10,7 +10,7 @@ class WPDD_License_Manager { * Initialize the license manager */ public static function init() { - add_action('wpdd_order_completed', array(__CLASS__, 'generate_license_for_order'), 10, 2); + add_action('wpdd_order_completed', array(__CLASS__, 'generate_license_for_order'), 10, 1); } /** @@ -28,12 +28,28 @@ class WPDD_License_Manager { /** * Generate license for completed order */ - public static function generate_license_for_order($order_id, $order) { + public static function generate_license_for_order($order_id) { global $wpdb; + error_log('WPDD License Debug: generate_license_for_order called for order ID: ' . $order_id); + + // Get the order details + $order = $wpdb->get_row($wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}wpdd_orders WHERE id = %d", + $order_id + )); + + if (!$order) { + error_log('WPDD License Debug: Order not found for ID: ' . $order_id); + return; + } + // Check if product is software license type $product_type = get_post_meta($order->product_id, '_wpdd_product_type', true); + error_log('WPDD License Debug: Product type for product ' . $order->product_id . ': ' . $product_type); + if ($product_type !== 'software_license') { + error_log('WPDD License Debug: Product type is not software_license, skipping license generation'); return; } @@ -44,9 +60,12 @@ class WPDD_License_Manager { )); if ($existing) { + error_log('WPDD License Debug: License already exists for order ' . $order_id . ', license ID: ' . $existing); return; } + error_log('WPDD License Debug: Generating new license for order ' . $order_id); + // Generate unique license key $license_key = self::generate_license_key(); @@ -81,6 +100,12 @@ class WPDD_License_Manager { array('%s', '%d', '%d', '%d', '%s', '%s', '%d', '%s', '%s') ); + if ($wpdb->insert_id) { + error_log('WPDD License Debug: License created successfully with ID: ' . $wpdb->insert_id . ', license key: ' . $license_key); + } else { + error_log('WPDD License Debug: Failed to create license. Last error: ' . $wpdb->last_error); + } + // Send license key to customer self::send_license_email($order, $license_key); diff --git a/includes/class-wpdd-metaboxes.php b/includes/class-wpdd-metaboxes.php index 13513b2..878273f 100644 --- a/includes/class-wpdd-metaboxes.php +++ b/includes/class-wpdd-metaboxes.php @@ -316,7 +316,7 @@ class WPDD_Metaboxes { + value="" min="0" /> diff --git a/includes/class-wpdd-orders.php b/includes/class-wpdd-orders.php index 12ff13a..dfb50ba 100644 --- a/includes/class-wpdd-orders.php +++ b/includes/class-wpdd-orders.php @@ -180,8 +180,26 @@ class WPDD_Orders { private static function generate_download_link($order_id) { global $wpdb; + // Get the order to find the product + $order = $wpdb->get_row($wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}wpdd_orders WHERE id = %d", + $order_id + )); + + if (!$order) { + return false; + } + + // Get download limit from product settings + $download_limit = get_post_meta($order->product_id, '_wpdd_download_limit', true); + $download_limit = $download_limit ?: 0; // Default to 0 (unlimited) if not set + + // Get download expiry from product settings + $download_expiry = get_post_meta($order->product_id, '_wpdd_download_expiry', true); + $download_expiry = $download_expiry ?: 30; // Default to 30 days if not set + $token = wp_hash(uniqid() . $order_id . time()); - $expires_at = date('Y-m-d H:i:s', strtotime('+7 days')); + $expires_at = date('Y-m-d H:i:s', strtotime('+' . $download_expiry . ' days')); $wpdb->insert( $wpdb->prefix . 'wpdd_download_links', @@ -189,7 +207,7 @@ class WPDD_Orders { 'order_id' => $order_id, 'token' => $token, 'expires_at' => $expires_at, - 'max_downloads' => 5, + 'max_downloads' => $download_limit, 'created_at' => current_time('mysql') ), array('%d', '%s', '%s', '%d', '%s') diff --git a/includes/class-wpdd-paypal-payouts.php b/includes/class-wpdd-paypal-payouts.php index 68eba2f..4ddcd0e 100644 --- a/includes/class-wpdd-paypal-payouts.php +++ b/includes/class-wpdd-paypal-payouts.php @@ -116,6 +116,9 @@ class WPDD_PayPal_Payouts { $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, @@ -127,7 +130,7 @@ class WPDD_PayPal_Payouts { 'recipient_type' => 'EMAIL', 'amount' => array( 'value' => number_format($payout->amount, 2, '.', ''), - 'currency' => $payout->currency + 'currency' => $currency ), 'receiver' => $payout->paypal_email, 'note' => 'Payout for your sales on ' . get_bloginfo('name'), @@ -136,6 +139,9 @@ class WPDD_PayPal_Payouts { ) ); + // 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( diff --git a/includes/class-wpdd-paypal.php b/includes/class-wpdd-paypal.php index deff6c8..7ef80a6 100644 --- a/includes/class-wpdd-paypal.php +++ b/includes/class-wpdd-paypal.php @@ -109,12 +109,23 @@ class WPDD_PayPal { $paypal_order = self::api_request('/v2/checkout/orders', $order_data, 'POST'); if (isset($paypal_order['id'])) { - $_SESSION['wpdd_paypal_order_' . $paypal_order['id']] = array( - 'product_id' => $product_id, - 'amount' => $final_price, - 'customer_email' => sanitize_email($_POST['customer_email'] ?? ''), - 'customer_name' => sanitize_text_field($_POST['customer_name'] ?? '') - ); + // Ensure session is available before storing data + if (WP_Digital_Download::ensure_session()) { + $_SESSION['wpdd_paypal_order_' . $paypal_order['id']] = array( + 'product_id' => $product_id, + 'amount' => $final_price, + 'customer_email' => sanitize_email($_POST['customer_email'] ?? ''), + 'customer_name' => sanitize_text_field($_POST['customer_name'] ?? '') + ); + } else { + // Fallback: use WordPress options table with short expiry + set_transient('wpdd_paypal_order_' . $paypal_order['id'], array( + 'product_id' => $product_id, + 'amount' => $final_price, + 'customer_email' => sanitize_email($_POST['customer_email'] ?? ''), + 'customer_name' => sanitize_text_field($_POST['customer_name'] ?? '') + ), 600); // 10 minutes + } wp_send_json_success(array('orderID' => $paypal_order['id'])); } else { @@ -134,7 +145,17 @@ class WPDD_PayPal { $capture_response = self::api_request('/v2/checkout/orders/' . $paypal_order_id . '/capture', array(), 'POST'); if (isset($capture_response['status']) && $capture_response['status'] === 'COMPLETED') { - $session_data = $_SESSION['wpdd_paypal_order_' . $paypal_order_id] ?? array(); + // Try to get data from session first, then fallback to transient + $session_data = array(); + if (WP_Digital_Download::ensure_session() && isset($_SESSION['wpdd_paypal_order_' . $paypal_order_id])) { + $session_data = $_SESSION['wpdd_paypal_order_' . $paypal_order_id]; + } else { + // Try transient fallback + $session_data = get_transient('wpdd_paypal_order_' . $paypal_order_id); + if ($session_data === false) { + $session_data = array(); + } + } // Add error logging for debugging session issues if (empty($session_data)) { @@ -212,7 +233,11 @@ class WPDD_PayPal { update_post_meta($session_data['product_id'], '_wpdd_sales_count', intval(get_post_meta($session_data['product_id'], '_wpdd_sales_count', true)) + 1); - unset($_SESSION['wpdd_paypal_order_' . $paypal_order_id]); + // Clean up stored data (both session and transient) + if (WP_Digital_Download::ensure_session() && isset($_SESSION['wpdd_paypal_order_' . $paypal_order_id])) { + unset($_SESSION['wpdd_paypal_order_' . $paypal_order_id]); + } + delete_transient('wpdd_paypal_order_' . $paypal_order_id); wp_send_json_success(array( 'redirect_url' => add_query_arg( @@ -267,8 +292,26 @@ class WPDD_PayPal { private static function generate_download_link($order_id) { global $wpdb; + // Get the order to find the product + $order = $wpdb->get_row($wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}wpdd_orders WHERE id = %d", + $order_id + )); + + if (!$order) { + return false; + } + + // Get download limit from product settings + $download_limit = get_post_meta($order->product_id, '_wpdd_download_limit', true); + $download_limit = $download_limit ?: 0; // Default to 0 (unlimited) if not set + + // Get download expiry from product settings + $download_expiry = get_post_meta($order->product_id, '_wpdd_download_expiry', true); + $download_expiry = $download_expiry ?: 30; // Default to 30 days if not set + $token = wp_hash(uniqid() . $order_id . time()); - $expires_at = date('Y-m-d H:i:s', strtotime('+7 days')); + $expires_at = date('Y-m-d H:i:s', strtotime('+' . $download_expiry . ' days')); $wpdb->insert( $wpdb->prefix . 'wpdd_download_links', @@ -276,7 +319,7 @@ class WPDD_PayPal { 'order_id' => $order_id, 'token' => $token, 'expires_at' => $expires_at, - 'max_downloads' => 5, + 'max_downloads' => $download_limit, 'created_at' => current_time('mysql') ), array('%d', '%s', '%s', '%d', '%s') diff --git a/includes/class-wpdd-shortcodes.php b/includes/class-wpdd-shortcodes.php index a6f559c..43039fa 100644 --- a/includes/class-wpdd-shortcodes.php +++ b/includes/class-wpdd-shortcodes.php @@ -356,9 +356,11 @@ class WPDD_Shortcodes { } $orders = $wpdb->get_results($wpdb->prepare( - "SELECT o.*, p.post_title as product_name + "SELECT o.*, p.post_title as product_name, l.license_key, + (SELECT COUNT(*) FROM {$wpdb->prefix}wpdd_downloads d WHERE d.order_id = o.id) as download_count FROM {$wpdb->prefix}wpdd_orders o LEFT JOIN {$wpdb->posts} p ON o.product_id = p.ID + LEFT JOIN {$wpdb->prefix}wpdd_licenses l ON o.id = l.order_id WHERE o.customer_email = %s AND o.status = 'completed' ORDER BY o.purchase_date DESC", @@ -370,9 +372,11 @@ class WPDD_Shortcodes { // Get orders by user ID or email (to include guest purchases before account creation) $orders = $wpdb->get_results($wpdb->prepare( - "SELECT o.*, p.post_title as product_name + "SELECT o.*, p.post_title as product_name, l.license_key, + (SELECT COUNT(*) FROM {$wpdb->prefix}wpdd_downloads d WHERE d.order_id = o.id) as download_count FROM {$wpdb->prefix}wpdd_orders o LEFT JOIN {$wpdb->posts} p ON o.product_id = p.ID + LEFT JOIN {$wpdb->prefix}wpdd_licenses l ON o.id = l.order_id WHERE (o.customer_id = %d OR o.customer_email = %s) AND o.status = 'completed' ORDER BY o.purchase_date DESC", @@ -409,6 +413,7 @@ class WPDD_Shortcodes { product_id, '_wpdd_download_limit', true); + $download_limit = $download_limit ?: 0; // Convert empty string to 0 (unlimited) $download_expiry = get_post_meta($order->product_id, '_wpdd_download_expiry', true); $is_expired = false; @@ -417,7 +422,9 @@ class WPDD_Shortcodes { $is_expired = current_time('mysql') > $expiry_date; } - $can_download = !$is_expired && ($download_limit == 0 || $order->download_count < $download_limit); + // Ensure download_count is a number + $current_downloads = (int) $order->download_count; + $can_download = !$is_expired && ($download_limit == 0 || $current_downloads < $download_limit); ?> order_number); ?> @@ -431,9 +438,9 @@ class WPDD_Shortcodes { 0) { - echo sprintf('%d / %d', $order->download_count, $download_limit); + echo sprintf('%d / %d', $current_downloads, $download_limit); } else { - echo $order->download_count; + echo sprintf('%d / %s', $current_downloads, __('unlimited', 'wp-digital-download')); } ?> @@ -470,6 +477,19 @@ class WPDD_Shortcodes { + license_key)) : ?> + + +
+ + license_key); ?> + +
+ + + diff --git a/wp-digital-download.php b/wp-digital-download.php index 2108d57..97c0342 100644 --- a/wp-digital-download.php +++ b/wp-digital-download.php @@ -3,7 +3,7 @@ * Plugin Name: WP Digital Download * Plugin URI: https://example.com/wp-digital-download * Description: A comprehensive digital download marketplace plugin allowing creators to sell digital products with PayPal integration - * Version: 1.0.0 + * Version: {auto_update_value_on_deploy} * Author: Josh Knapp * License: GPL v2 or later * Text Domain: wp-digital-download @@ -14,7 +14,7 @@ if (!defined('ABSPATH')) { exit; } -define('WPDD_VERSION', '1.0.2'); +define('WPDD_VERSION', '{auto_update_value_on_deploy}'); define('WPDD_PLUGIN_URL', plugin_dir_url(__FILE__)); define('WPDD_PLUGIN_PATH', plugin_dir_path(__FILE__)); define('WPDD_PLUGIN_BASENAME', plugin_basename(__FILE__)); @@ -54,7 +54,9 @@ final class WP_Digital_Download { 'includes/class-wpdd-watermark.php', 'includes/class-wpdd-ajax.php', 'includes/class-wpdd-creator.php', - 'includes/class-wpdd-paypal-payouts.php' + 'includes/class-wpdd-earnings-processor.php', + 'includes/class-wpdd-paypal-payouts.php', + 'includes/class-wpdd-email.php' ); foreach ($files as $file) { @@ -70,7 +72,8 @@ final class WP_Digital_Download { $admin_files = array( 'admin/class-wpdd-admin.php', 'admin/class-wpdd-settings.php', - 'admin/class-wpdd-admin-payouts.php' + 'admin/class-wpdd-admin-payouts.php', + 'admin/class-wpdd-order-manager.php' ); foreach ($admin_files as $file) { @@ -88,8 +91,7 @@ final class WP_Digital_Download { register_activation_hook(__FILE__, array('WPDD_Install', 'activate')); register_deactivation_hook(__FILE__, array('WPDD_Install', 'deactivate')); - // Initialize sessions for PayPal integration (must be early priority) - add_action('init', array($this, 'start_session'), 1); + // Sessions will be started only when needed (in PayPal class) add_action('init', array($this, 'init')); add_action('wp_enqueue_scripts', array($this, 'enqueue_scripts')); @@ -183,6 +185,18 @@ final class WP_Digital_Download { error_log('WPDD Error: WPDD_API class not found'); } + if (class_exists('WPDD_Earnings_Processor')) { + WPDD_Earnings_Processor::init(); + } else { + error_log('WPDD Error: WPDD_Earnings_Processor class not found'); + } + + if (class_exists('WPDD_Email')) { + WPDD_Email::init(); + } else { + error_log('WPDD Error: WPDD_Email class not found'); + } + if (is_admin()) { if (class_exists('WPDD_Setup')) { WPDD_Setup::init(); @@ -201,17 +215,53 @@ final class WP_Digital_Download { } else { error_log('WPDD Error: WPDD_Settings class not found'); } + + if (class_exists('WPDD_Order_Manager')) { + WPDD_Order_Manager::init(); + } else { + error_log('WPDD Error: WPDD_Order_Manager class not found'); + } + + // Include tools page + require_once WPDD_PLUGIN_PATH . 'admin/wpdd-tools.php'; + + // Run database migration if needed + if (file_exists(WPDD_PLUGIN_PATH . 'admin/wpdd-db-migrate.php')) { + require_once WPDD_PLUGIN_PATH . 'admin/wpdd-db-migrate.php'; + wpdd_migrate_database(); + } + + // Initialize transaction history + if (file_exists(WPDD_PLUGIN_PATH . 'admin/class-wpdd-transaction-history.php')) { + require_once WPDD_PLUGIN_PATH . 'admin/class-wpdd-transaction-history.php'; + if (class_exists('WPDD_Transaction_History')) { + WPDD_Transaction_History::init(); + } + } } } /** - * Initialize PHP sessions for PayPal order data persistence - * Called early in WordPress init to ensure sessions work properly + * Safely start session if needed + * Returns true if session is available, false otherwise */ - public function start_session() { - if (!session_id()) { - session_start(); + public static function ensure_session() { + // Don't start sessions in admin or CLI + if (is_admin() || defined('WP_CLI')) { + return false; } + + // Don't start if headers already sent + if (headers_sent()) { + return false; + } + + // Start session if we don't have one + if (session_status() === PHP_SESSION_NONE) { + return session_start(); + } + + return true; } public function enqueue_scripts() { @@ -231,6 +281,14 @@ final class WP_Digital_Download { $frontend_css_ver ); + // Enqueue FontAwesome for copy icons + wp_enqueue_style( + 'font-awesome', + 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css', + array(), + '6.0.0' + ); + wp_enqueue_script( 'wpdd-frontend', WPDD_PLUGIN_URL . 'assets/js/frontend.js', @@ -248,8 +306,9 @@ final class WP_Digital_Download { public function admin_enqueue_scripts($hook) { global $post; - if (($hook == 'post.php' || $hook == 'post-new.php') && - isset($post) && $post->post_type == 'wpdd_product') { + if ((($hook == 'post.php' || $hook == 'post-new.php') && + isset($post) && $post->post_type == 'wpdd_product') || + ($hook == 'edit.php' && isset($_GET['post_type']) && $_GET['post_type'] == 'wpdd_product')) { // Use file modification time for cache busting $admin_css_ver = file_exists(WPDD_PLUGIN_PATH . 'assets/css/admin.css')