Some checks failed
Create Release / build (push) Failing after 3s
🔧 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>
470 lines
24 KiB
PHP
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;
|
|
}
|
|
}
|