Files
wp-digital-download/admin/class-wpdd-order-manager.php
jknapp 4731637f33
Some checks failed
Create Release / build (push) Failing after 3s
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>
2025-09-09 19:16:57 -07:00

470 lines
24 KiB
PHP

<?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;
}
}