Major improvements: Fix download limits, enhance license display, fix software filenames
🔧 Bug Fixes: - Fixed download limits defaulting to 5 instead of 0 for unlimited downloads - Fixed software license filename sanitization (spaces→dashes, dots→underscores, proper .zip extension) - Software downloads now show as "Test-Plugin-v2-2-0.zip" instead of "Test Plugin v2.2.0" ✨ UI/UX Enhancements: - Redesigned license key display to span full table width with FontAwesome copy icons - Added responsive CSS styling for license key rows - Integrated FontAwesome CDN for modern copy icons 🏗️ Architecture Improvements: - Added comprehensive filename sanitization in both download handler and API paths - Enhanced software license product handling for local package files - Improved error handling and logging throughout download processes 📦 Infrastructure: - Added Gitea workflows for automated releases on push to main - Created comprehensive .gitignore excluding test files and browser automation - Updated documentation with all recent improvements and technical insights 🔍 Technical Details: - Software license products served from wp-content/uploads/wpdd-packages/ - Download flow: token → process_download_by_token() → process_download() → deliver_file() - Dual path coverage for both API downloads and regular file delivery - Version placeholder system for automated deployment 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
470
admin/class-wpdd-order-manager.php
Normal file
470
admin/class-wpdd-order-manager.php
Normal file
@@ -0,0 +1,470 @@
|
||||
<?php
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class WPDD_Order_Manager {
|
||||
|
||||
public static function init() {
|
||||
add_action('admin_menu', array(__CLASS__, 'add_menu_page'));
|
||||
add_action('admin_post_wpdd_cancel_order', array(__CLASS__, 'handle_cancel_order'));
|
||||
add_action('admin_post_wpdd_refund_order', array(__CLASS__, 'handle_refund_order'));
|
||||
add_action('admin_post_wpdd_release_earnings', array(__CLASS__, 'handle_release_earnings'));
|
||||
add_action('admin_enqueue_scripts', array(__CLASS__, 'enqueue_scripts'));
|
||||
}
|
||||
|
||||
public static function add_menu_page() {
|
||||
if (!current_user_can('manage_options')) {
|
||||
return;
|
||||
}
|
||||
|
||||
add_submenu_page(
|
||||
'edit.php?post_type=wpdd_product',
|
||||
__('Order Management', 'wp-digital-download'),
|
||||
__('Order Manager', 'wp-digital-download'),
|
||||
'manage_options',
|
||||
'wpdd-order-manager',
|
||||
array(__CLASS__, 'render_page')
|
||||
);
|
||||
}
|
||||
|
||||
public static function enqueue_scripts($hook) {
|
||||
if ($hook !== 'wpdd_product_page_wpdd-order-manager') {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_enqueue_script('wpdd-order-manager', WPDD_PLUGIN_URL . 'assets/js/admin-order-manager.js', array('jquery'), WPDD_VERSION, true);
|
||||
wp_localize_script('wpdd-order-manager', 'wpdd_order_manager', array(
|
||||
'ajax_url' => 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'));
|
||||
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1><?php _e('Order Management', 'wp-digital-download'); ?></h1>
|
||||
|
||||
<?php if (isset($_GET['message'])) : ?>
|
||||
<?php if ($_GET['message'] === 'cancelled') : ?>
|
||||
<div class="notice notice-success is-dismissible">
|
||||
<p><?php _e('Order cancelled successfully.', 'wp-digital-download'); ?></p>
|
||||
</div>
|
||||
<?php elseif ($_GET['message'] === 'refunded') : ?>
|
||||
<div class="notice notice-success is-dismissible">
|
||||
<p><?php _e('Refund processed successfully.', 'wp-digital-download'); ?></p>
|
||||
</div>
|
||||
<?php elseif ($_GET['message'] === 'released') : ?>
|
||||
<div class="notice notice-success is-dismissible">
|
||||
<p><?php _e('Earnings released successfully.', 'wp-digital-download'); ?></p>
|
||||
</div>
|
||||
<?php elseif ($_GET['message'] === 'error') : ?>
|
||||
<div class="notice notice-error is-dismissible">
|
||||
<p><?php _e('Error processing request. Please try again.', 'wp-digital-download'); ?></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Filter Form -->
|
||||
<div class="wpdd-filter-box" style="background: white; padding: 20px; margin: 20px 0; border: 1px solid #ccd0d4;">
|
||||
<h3><?php _e('Filters', 'wp-digital-download'); ?></h3>
|
||||
<form method="get" action="">
|
||||
<input type="hidden" name="post_type" value="wpdd_product">
|
||||
<input type="hidden" name="page" value="wpdd-order-manager">
|
||||
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th><label for="status"><?php _e('Order Status', 'wp-digital-download'); ?></label></th>
|
||||
<td>
|
||||
<select name="status" id="status">
|
||||
<option value="all"><?php _e('All Statuses', 'wp-digital-download'); ?></option>
|
||||
<option value="completed" <?php selected($status_filter, 'completed'); ?>><?php _e('Completed', 'wp-digital-download'); ?></option>
|
||||
<option value="pending" <?php selected($status_filter, 'pending'); ?>><?php _e('Pending', 'wp-digital-download'); ?></option>
|
||||
<option value="failed" <?php selected($status_filter, 'failed'); ?>><?php _e('Failed', 'wp-digital-download'); ?></option>
|
||||
<option value="cancelled" <?php selected($status_filter, 'cancelled'); ?>><?php _e('Cancelled', 'wp-digital-download'); ?></option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="creator"><?php _e('Creator', 'wp-digital-download'); ?></label></th>
|
||||
<td>
|
||||
<select name="creator" id="creator">
|
||||
<option value="0"><?php _e('All Creators', 'wp-digital-download'); ?></option>
|
||||
<?php foreach ($creators as $creator) : ?>
|
||||
<option value="<?php echo esc_attr($creator->ID); ?>" <?php selected($creator_filter, $creator->ID); ?>>
|
||||
<?php echo esc_html($creator->display_name); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="date_from"><?php _e('Date Range', 'wp-digital-download'); ?></label></th>
|
||||
<td>
|
||||
<input type="date" name="date_from" id="date_from" value="<?php echo esc_attr($date_from); ?>">
|
||||
<?php _e('to', 'wp-digital-download'); ?>
|
||||
<input type="date" name="date_to" id="date_to" value="<?php echo esc_attr($date_to); ?>">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<p class="submit">
|
||||
<input type="submit" class="button button-primary" value="<?php _e('Filter Orders', 'wp-digital-download'); ?>">
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Orders Table -->
|
||||
<div class="wpdd-orders-table" style="background: white; padding: 20px; margin: 20px 0; border: 1px solid #ccd0d4;">
|
||||
<h3><?php _e('Orders', 'wp-digital-download'); ?> (<?php echo count($orders); ?>)</h3>
|
||||
|
||||
<?php if (empty($orders)) : ?>
|
||||
<p><?php _e('No orders found for the selected criteria.', 'wp-digital-download'); ?></p>
|
||||
<?php else : ?>
|
||||
<table class="wp-list-table widefat fixed striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?php _e('Order', 'wp-digital-download'); ?></th>
|
||||
<th><?php _e('Customer', 'wp-digital-download'); ?></th>
|
||||
<th><?php _e('Product', 'wp-digital-download'); ?></th>
|
||||
<th><?php _e('Creator', 'wp-digital-download'); ?></th>
|
||||
<th><?php _e('Amount', 'wp-digital-download'); ?></th>
|
||||
<th><?php _e('Date', 'wp-digital-download'); ?></th>
|
||||
<th><?php _e('Status', 'wp-digital-download'); ?></th>
|
||||
<th><?php _e('Earnings Status', 'wp-digital-download'); ?></th>
|
||||
<th><?php _e('Actions', 'wp-digital-download'); ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($orders as $order) : ?>
|
||||
<tr>
|
||||
<td>
|
||||
<strong><?php echo esc_html($order->order_number); ?></strong><br>
|
||||
<small>#<?php echo esc_html($order->id); ?></small>
|
||||
</td>
|
||||
<td>
|
||||
<strong><?php echo esc_html($order->customer_name); ?></strong><br>
|
||||
<small><?php echo esc_html($order->customer_email); ?></small>
|
||||
</td>
|
||||
<td>
|
||||
<strong><?php echo esc_html($order->product_name); ?></strong><br>
|
||||
<small>ID: <?php echo esc_html($order->product_id); ?></small>
|
||||
</td>
|
||||
<td><?php echo esc_html($order->creator_name); ?></td>
|
||||
<td><strong><?php echo wpdd_format_price($order->amount); ?></strong></td>
|
||||
<td><?php echo esc_html(date_i18n(get_option('date_format'), strtotime($order->purchase_date))); ?></td>
|
||||
<td>
|
||||
<?php
|
||||
$status_class = '';
|
||||
switch($order->status) {
|
||||
case 'completed':
|
||||
$status_class = 'notice-success';
|
||||
break;
|
||||
case 'cancelled':
|
||||
case 'failed':
|
||||
$status_class = 'notice-error';
|
||||
break;
|
||||
case 'pending':
|
||||
$status_class = 'notice-warning';
|
||||
break;
|
||||
}
|
||||
?>
|
||||
<span class="notice <?php echo esc_attr($status_class); ?>" style="padding: 2px 8px; display: inline-block;">
|
||||
<?php echo esc_html(ucfirst($order->status)); ?>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<?php if ($order->payout_status) : ?>
|
||||
<?php
|
||||
$earnings_class = '';
|
||||
$earnings_text = ucfirst($order->payout_status);
|
||||
switch($order->payout_status) {
|
||||
case 'pending':
|
||||
$earnings_class = 'notice-info';
|
||||
if ($order->available_at) {
|
||||
$earnings_text .= '<br><small>Until: ' . date('M j', strtotime($order->available_at)) . '</small>';
|
||||
}
|
||||
break;
|
||||
case 'available':
|
||||
$earnings_class = 'notice-warning';
|
||||
break;
|
||||
case 'paid':
|
||||
$earnings_class = 'notice-success';
|
||||
break;
|
||||
case 'cancelled':
|
||||
$earnings_class = 'notice-error';
|
||||
break;
|
||||
}
|
||||
?>
|
||||
<span class="notice <?php echo esc_attr($earnings_class); ?>" style="padding: 2px 8px; display: inline-block;">
|
||||
<?php echo $earnings_text; ?>
|
||||
</span>
|
||||
<?php if ($order->creator_earning > 0) : ?>
|
||||
<br><small><?php echo wpdd_format_price($order->creator_earning); ?></small>
|
||||
<?php endif; ?>
|
||||
<?php else : ?>
|
||||
<span class="notice notice-error" style="padding: 2px 8px; display: inline-block;">
|
||||
<?php _e('No Earnings', 'wp-digital-download'); ?>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<div class="button-group">
|
||||
<?php if ($order->status === 'completed') : ?>
|
||||
|
||||
<?php if ($order->payout_status === 'pending') : ?>
|
||||
<!-- Release Earnings Button -->
|
||||
<form method="post" action="<?php echo admin_url('admin-post.php'); ?>" style="display: inline;">
|
||||
<input type="hidden" name="action" value="wpdd_release_earnings">
|
||||
<input type="hidden" name="earning_id" value="<?php echo esc_attr($order->earning_id); ?>">
|
||||
<input type="hidden" name="order_id" value="<?php echo esc_attr($order->id); ?>">
|
||||
<?php wp_nonce_field('wpdd_release_earnings_' . $order->earning_id, 'wpdd_nonce'); ?>
|
||||
<button type="submit" class="button button-secondary wpdd-release-btn" title="<?php _e('Release earnings immediately', 'wp-digital-download'); ?>">
|
||||
<span class="dashicons dashicons-unlock"></span> <?php _e('Release', 'wp-digital-download'); ?>
|
||||
</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($order->payout_status !== 'paid') : ?>
|
||||
<!-- Cancel Order Button -->
|
||||
<form method="post" action="<?php echo admin_url('admin-post.php'); ?>" style="display: inline;">
|
||||
<input type="hidden" name="action" value="wpdd_cancel_order">
|
||||
<input type="hidden" name="order_id" value="<?php echo esc_attr($order->id); ?>">
|
||||
<?php wp_nonce_field('wpdd_cancel_order_' . $order->id, 'wpdd_nonce'); ?>
|
||||
<button type="submit" class="button button-link-delete wpdd-cancel-btn" title="<?php _e('Cancel order and revoke access', 'wp-digital-download'); ?>">
|
||||
<span class="dashicons dashicons-no"></span> <?php _e('Cancel', 'wp-digital-download'); ?>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- Refund Button -->
|
||||
<form method="post" action="<?php echo admin_url('admin-post.php'); ?>" style="display: inline;">
|
||||
<input type="hidden" name="action" value="wpdd_refund_order">
|
||||
<input type="hidden" name="order_id" value="<?php echo esc_attr($order->id); ?>">
|
||||
<?php wp_nonce_field('wpdd_refund_order_' . $order->id, 'wpdd_nonce'); ?>
|
||||
<button type="submit" class="button button-link-delete wpdd-refund-btn" title="<?php _e('Process refund (manual PayPal refund required)', 'wp-digital-download'); ?>">
|
||||
<span class="dashicons dashicons-undo"></span> <?php _e('Refund', 'wp-digital-download'); ?>
|
||||
</button>
|
||||
</form>
|
||||
<?php else : ?>
|
||||
<span class="description"><?php _e('Earnings already paid out', 'wp-digital-download'); ?></span>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php else : ?>
|
||||
<span class="description"><?php printf(__('Order is %s', 'wp-digital-download'), $order->status); ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.button-group {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.button-group .button {
|
||||
margin: 2px;
|
||||
font-size: 11px;
|
||||
padding: 3px 8px;
|
||||
height: auto;
|
||||
}
|
||||
.button-group .dashicons {
|
||||
font-size: 12px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
</style>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
public static function handle_cancel_order() {
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_die(__('Permission denied', 'wp-digital-download'));
|
||||
}
|
||||
|
||||
$order_id = intval($_POST['order_id']);
|
||||
|
||||
if (!wp_verify_nonce($_POST['wpdd_nonce'], 'wpdd_cancel_order_' . $order_id)) {
|
||||
wp_redirect(admin_url('edit.php?post_type=wpdd_product&page=wpdd-order-manager&message=error'));
|
||||
exit;
|
||||
}
|
||||
|
||||
$success = self::cancel_order($order_id);
|
||||
|
||||
$message = $success ? 'cancelled' : 'error';
|
||||
wp_redirect(admin_url('edit.php?post_type=wpdd_product&page=wpdd-order-manager&message=' . $message));
|
||||
exit;
|
||||
}
|
||||
|
||||
public static function handle_refund_order() {
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_die(__('Permission denied', 'wp-digital-download'));
|
||||
}
|
||||
|
||||
$order_id = intval($_POST['order_id']);
|
||||
|
||||
if (!wp_verify_nonce($_POST['wpdd_nonce'], 'wpdd_refund_order_' . $order_id)) {
|
||||
wp_redirect(admin_url('edit.php?post_type=wpdd_product&page=wpdd-order-manager&message=error'));
|
||||
exit;
|
||||
}
|
||||
|
||||
$success = self::refund_order($order_id);
|
||||
|
||||
$message = $success ? 'refunded' : 'error';
|
||||
wp_redirect(admin_url('edit.php?post_type=wpdd_product&page=wpdd-order-manager&message=' . $message));
|
||||
exit;
|
||||
}
|
||||
|
||||
public static function handle_release_earnings() {
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_die(__('Permission denied', 'wp-digital-download'));
|
||||
}
|
||||
|
||||
$earning_id = intval($_POST['earning_id']);
|
||||
$order_id = intval($_POST['order_id']);
|
||||
|
||||
if (!wp_verify_nonce($_POST['wpdd_nonce'], 'wpdd_release_earnings_' . $earning_id)) {
|
||||
wp_redirect(admin_url('edit.php?post_type=wpdd_product&page=wpdd-order-manager&message=error'));
|
||||
exit;
|
||||
}
|
||||
|
||||
$success = WPDD_Earnings_Processor::release_earning_immediately($earning_id);
|
||||
|
||||
$message = $success ? 'released' : 'error';
|
||||
wp_redirect(admin_url('edit.php?post_type=wpdd_product&page=wpdd-order-manager&message=' . $message));
|
||||
exit;
|
||||
}
|
||||
|
||||
private static function cancel_order($order_id) {
|
||||
global $wpdb;
|
||||
|
||||
// Update order status
|
||||
$result = $wpdb->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;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user