gatewayManager = EasyInvoice::getInstance()->getGatewayManager(); } /** * Initialize the controller */ public function init() { add_action('admin_enqueue_scripts', [$this, 'enqueueAssets']); add_action('wp_ajax_easy_invoice_process_payment', [$this, 'processPayment']); add_action('wp_ajax_nopriv_easy_invoice_process_payment', [$this, 'processPayment']); add_action('wp_ajax_easy_invoice_update_payment', [$this, 'updatePayment']); add_action('wp_ajax_easy_invoice_payment_callback', [$this, 'handleCallback']); add_action('wp_ajax_nopriv_easy_invoice_payment_callback', [$this, 'handleCallback']); add_action('wp_ajax_easy_invoice_verify_manual_payment', [$this, 'verifyManualPayment']); add_action('wp_ajax_easy_invoice_reject_manual_payment', [$this, 'rejectManualPayment']); // Handler for submitting payment proof for manual gateways add_action('wp_ajax_easy_invoice_submit_payment_proof', [$this, 'submitPaymentProof']); add_action('wp_ajax_nopriv_easy_invoice_submit_payment_proof', [$this, 'submitPaymentProof']); // Handler for manual payment submission add_action('wp_ajax_easy_invoice_submit_manual_payment', [$this, 'submitManualPayment']); add_action('wp_ajax_nopriv_easy_invoice_submit_manual_payment', [$this, 'submitManualPayment']); // Handler for getting payment instructions for manual gateways add_action('wp_ajax_easy_invoice_get_payment_instructions', [$this, 'getPaymentInstructions']); add_action('wp_ajax_nopriv_easy_invoice_get_payment_instructions', [$this, 'getPaymentInstructions']); // Enqueue frontend scripts add_action('wp_enqueue_scripts', [$this, 'enqueueFrontendAssets']); // Handler for admin to mark an invoice as paid add_action('wp_ajax_easy_invoice_approve_payment', [$this, 'mark_invoice_paid_ajax']); // Stripe payment handlers moved to Pro plugin add_action('wp_enqueue_scripts', [$this, 'enqueueScripts']); // Add filter to show pending payments in admin add_filter('easy_invoice_admin_payment_statuses', [$this, 'addPendingPaymentStatuses']); // Add custom columns to payments list add_filter('manage_easy-payment_posts_columns', [$this, 'addPaymentMethodColumn']); add_action('manage_easy-payment_posts_custom_column', [$this, 'renderPaymentMethodColumn'], 10, 2); // Add reminder CRON job for pending payments add_action('easy_invoice_payment_reminder', [$this, 'sendPaymentReminders']); if (!wp_next_scheduled('easy_invoice_payment_reminder')) { wp_schedule_event(time(), 'daily', 'easy_invoice_payment_reminder'); } // Handle bulk actions add_action('admin_init', [$this, 'handleBulkActions']); } /** * Get payment instructions for manual gateways */ public function getPaymentInstructions() { // Verify nonce if (!wp_verify_nonce($_POST['nonce'], 'easy_invoice_payment')) { wp_send_json_error(['message' => 'Security check failed']); return; } $gateway = sanitize_text_field($_POST['gateway']); $invoice_id = intval($_POST['invoice_id']); if (!$gateway || !$invoice_id) { wp_send_json_error(['message' => 'Missing required parameters']); return; } // Get invoice $invoice_post = get_post($invoice_id); if (!$invoice_post || $invoice_post->post_type !== 'easy_invoice') { wp_send_json_error(['message' => 'Invalid invoice']); return; } // Guests may only load instructions for published invoices (avoid leaking draft/private details). if (!easy_invoice_user_can('ei_view_invoices') && $invoice_post->post_status !== 'publish') { wp_send_json_error(['message' => __('Invoice not found', 'easy-invoice')]); return; } $invoice = new \EasyInvoice\Models\Invoice($invoice_post); // Get gateway instance $gateway_instance = $this->gatewayManager->getGateway($gateway); if (!$gateway_instance) { wp_send_json_error(['message' => 'Gateway not found']); return; } // Get instructions using the hook system ob_start(); do_action('easy_invoice_payment_gateways_after', $invoice, $gateway); $instructions = ob_get_clean(); if ($instructions) { wp_send_json_success(['instructions' => $instructions]); } else { wp_send_json_error(['message' => 'No instructions available']); } } /** * Enqueue admin assets */ public function enqueueAssets() { $screen = get_current_screen(); if (!$screen || !property_exists($screen, 'id') || strpos($screen->id, 'easy-invoice') === false) { return; } // Enqueue manual payment script wp_enqueue_script( 'easy-invoice-manual-payment', EASY_INVOICE_PLUGIN_URL . 'assets/js/manual-payment.js', ['jquery'], '1.0.0', true ); // Localize script wp_localize_script('easy-invoice-manual-payment', 'easy_invoice_ajax', [ 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('easy_invoice_payment') ]); } /** * Enqueue frontend assets */ public function enqueueFrontendAssets() { // Only load on invoice pages if (is_singular('easy_invoice')) { wp_enqueue_script( 'easy-invoice-manual-payment', EASY_INVOICE_PLUGIN_URL . 'assets/js/manual-payment.js', ['jquery'], '1.0.0', true ); // Forward the per-invoice access token from the URL to the JS // so the manual-payment AJAX request can present it back to // canSubmitPaymentForInvoice. Without this the legitimate // email-link recipient flow would break — they'd hit the gate. $access_token = isset($_GET['ik']) ? sanitize_text_field(wp_unslash($_GET['ik'])) : ''; wp_localize_script('easy-invoice-manual-payment', 'easy_invoice_ajax', [ 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('easy_invoice_payment'), 'access_token' => $access_token, ]); } } /** * Display method implementation * * @param array $args Display arguments */ public function display(array $args = []) { $page = isset($args['page']) ? $args['page'] : ''; switch ($page) { case PagesSlugs::PAYMENTS: $this->displayPaymentsPage(); break; case PagesSlugs::PAYMENT_NEW: $this->displayTemplate(EASY_INVOICE_PLUGIN_DIR . 'templates/payments/new.php'); break; case 'view': $payment_id = isset($_GET['id']) ? intval($_GET['id']) : 0; if ($payment_id) { $payment_post = get_post($payment_id); if ($payment_post && $payment_post->post_type === 'easy_invoice_payment') { try { $payment = new Payment($payment_post); $this->displayTemplate(EASY_INVOICE_PLUGIN_DIR . 'templates/payments/view.php', ['payment' => $payment]); } catch (\Exception $e) { wp_die(__('Invalid payment ID', 'easy-invoice')); } } else { wp_die(__('Invalid payment ID', 'easy-invoice')); } } else { wp_die(__('Payment ID is required', 'easy-invoice')); } break; case 'edit': $payment_id = isset($_GET['id']) ? intval($_GET['id']) : 0; if ($payment_id) { $payment_post = get_post($payment_id); if ($payment_post && $payment_post->post_type === 'easy_invoice_payment') { try { $payment = new Payment($payment_post); $this->displayTemplate(EASY_INVOICE_PLUGIN_DIR . 'templates/payments/edit.php', ['payment' => $payment]); } catch (\Exception $e) { wp_die(__('Invalid payment ID', 'easy-invoice')); } } else { wp_die(__('Invalid payment ID', 'easy-invoice')); } } else { wp_die(__('Payment ID is required', 'easy-invoice')); } break; default: $this->displayPaymentsPage(); break; } } /** * Display payments page with pagination */ protected function displayPaymentsPage() { // Get current view (all, trash) $current_view = isset($_GET['view']) ? sanitize_text_field($_GET['view']) : 'all'; // Get status filter $status_filter = isset($_GET['status']) ? sanitize_text_field($_GET['status']) : ''; // Pagination settings $per_page = 20; $current_page = isset($_GET['paged']) ? max(1, intval($_GET['paged'])) : 1; // Build query arguments $args = array( 'post_type' => 'easy_invoice_payment', 'posts_per_page' => $per_page, 'paged' => $current_page, 'orderby' => 'ID', 'order' => 'DESC', 'no_found_rows' => false, // We need this for pagination ); // Set post status based on current view if ($current_view === 'trash') { $args['post_status'] = 'trash'; } else { $args['post_status'] = 'publish'; } // Add status filter if set if (!empty($status_filter)) { $args['meta_query'] = array( array( 'key' => '_status', 'value' => $status_filter, ), ); } // Allow plugins to modify query arguments $args = apply_filters('easy_invoice_payment_controller_query_args', $args, $current_view, $status_filter); // Get paginated payments using WordPress query $wp_query = new \WP_Query($args); $payments = []; if ($wp_query->have_posts()) { while ($wp_query->have_posts()) { $wp_query->the_post(); $post = get_post(); $payment = new Payment($post); $payments[] = $payment; } } wp_reset_postdata(); // Allow plugins to modify the payments array $payments = apply_filters('easy_invoice_payment_controller_payments_list', $payments, $wp_query); // Get pagination info from WordPress query $total_payments = $wp_query->found_posts; $total_pages = $wp_query->max_num_pages; // Calculate statistics from ALL payments (not just current page) $stats_args = array( 'post_type' => 'easy_invoice_payment', 'posts_per_page' => -1, // Get all payments 'meta_query' => array( array( 'key' => '_status', 'compare' => 'EXISTS', ), ), ); // Set post status for stats based on current view if ($current_view === 'trash') { $stats_args['post_status'] = 'trash'; } else { $stats_args['post_status'] = 'publish'; } $stats_query = new \WP_Query($stats_args); $stats = [ 'total_payments' => $stats_query->found_posts, 'total_amount' => 0, 'completed_payments' => 0, 'pending_payments' => 0, 'failed_payments' => 0 ]; // Calculate stats from the query results if ($stats_query->have_posts()) { while ($stats_query->have_posts()) { $stats_query->the_post(); $payment = new Payment(get_post()); $amount = floatval($payment->getAmount()); $status = $payment->getStatus(); $stats['total_amount'] += $amount; switch ($status) { case 'completed': $stats['completed_payments']++; break; case 'pending': $stats['pending_payments']++; break; case 'failed': $stats['failed_payments']++; break; } } } wp_reset_postdata(); // Ensure all required keys exist with default values $stats = array_merge([ 'total_payments' => 0, 'total_amount' => 0, 'completed_payments' => 0, 'pending_payments' => 0, 'failed_payments' => 0 ], $stats); // Get trash count for tab display $trash_args = array( 'post_type' => 'easy_invoice_payment', 'post_status' => 'trash', 'posts_per_page' => -1 ); $trash_query = new \WP_Query($trash_args); $trash_count = $trash_query->found_posts; // Define available status filters $status_filters = array( 'completed' => 'Completed', 'pending' => 'Pending', 'failed' => 'Failed' ); // Prepare template data $template_data = [ 'payments' => $payments, 'current_view' => $current_view, 'status_filter' => $status_filter, 'status_filters' => $status_filters, 'trash_count' => $trash_count, 'stats' => $stats, 'current_page' => $current_page, 'per_page' => $per_page, 'total_payments' => $total_payments, 'total_pages' => $total_pages, 'wp_query' => $wp_query ]; // Allow plugins to modify template data $template_data = apply_filters('easy_invoice_payment_controller_template_data', $template_data); // Display the template $this->displayTemplate( EASY_INVOICE_PLUGIN_DIR . 'templates/payments/list.php', $template_data ); // Allow plugins to perform actions after displaying payments page do_action('easy_invoice_payment_controller_after_display_payments_page', $template_data); } /** * Enqueue required scripts and styles */ public function enqueueScripts(): void { // Check if scripts are already enqueued if (wp_script_is('easy-invoice-payment', 'enqueued')) { return; } if(!is_singular(PostTypes::EASY_INVOICE_POST_TYPE)){ //return; } // Enqueue our custom scripts wp_enqueue_script( 'easy-invoice-payment', EASY_INVOICE_URL . 'assets/js/payment.js', ['jquery'], EASY_INVOICE_VERSION, true ); // Get currency settings $settings_controller = new \EasyInvoice\Controllers\SettingsController(); $settings = $settings_controller->getSettings(); $currency_code = $settings['easy_invoice_currency_code'] ?? 'USD'; $currency_symbol = \EasyInvoice\Helpers\CurrencyHelper::getCurrencySymbol($currency_code); // Localize script variables for payment form wp_localize_script('easy-invoice-payment', 'easy_invoice_vars', [ 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('easy_invoice_payment'), 'currency_symbol' => $currency_symbol, 'currency_code' => $currency_code ]); } // Stripe methods moved to Pro plugin /** * Process payment via AJAX */ public function processPayment() { check_ajax_referer('easy_invoice_payment', 'payment_nonce'); $invoice_id = isset($_POST['invoice_id']) ? intval($_POST['invoice_id']) : 0; $payment_method_slug = isset($_POST['payment_method']) ? sanitize_text_field($_POST['payment_method']) : ''; // Add filter for extensions to handle custom payment logic (e.g., partial payments) $custom_result = apply_filters('easy_invoice_before_process_payment', null, $invoice_id, $_POST); if (is_array($custom_result) && isset($custom_result['handled']) && $custom_result['handled']) { if ($custom_result['success']) { wp_send_json_success($custom_result); } else { wp_send_json_error(['message' => $custom_result['message'] ?? __('Payment failed.', 'easy-invoice')]); } return; } if (!$invoice_id || !$payment_method_slug) { wp_send_json_error(['message' => __('Missing required fields.', 'easy-invoice')]); return; } $invoice_post = get_post($invoice_id); if (!$invoice_post || $invoice_post->post_type !== \EasyInvoice\Constants\PostTypes::EASY_INVOICE_POST_TYPE) { wp_send_json_error(['message' => __('Invalid invoice.', 'easy-invoice')]); return; } $invoice = new \EasyInvoice\Models\Invoice($invoice_post); $amount = $invoice->total ?? 0; // Log the payment processing details $gateway_instance = $this->gatewayManager->getGateway($payment_method_slug); if (!$gateway_instance || !$gateway_instance->isEnabled() || !$gateway_instance->isAvailable()) { wp_send_json_error(['message' => __('Selected payment gateway is not available or configured correctly.', 'easy-invoice')]); return; } try { // Pass the entire $_POST array to the gateway $result = $gateway_instance->processPayment($amount, $_POST); if (isset($result['success']) && $result['success']) { wp_send_json_success($result); } else { wp_send_json_error(['message' => $result['message'] ?? __('Payment processing failed with the gateway.', 'easy-invoice')]); } } catch (\Exception $e) { error_log('Easy Invoice Payment Error: ' . $e->getMessage() . ' in ' . $e->getFile() . ' on line ' . $e->getLine()); wp_send_json_error(['message' => __('An unexpected error occurred during payment processing. Please check plugin logs or contact support.', 'easy-invoice')]); } } /** * Handle payment callback/webhook */ public function handleCallback(): void { check_ajax_referer('easy_invoice_payment', 'payment_nonce'); $invoice_id = isset($_POST['invoice_id']) ? intval($_POST['invoice_id']) : 0; $gateway = isset($_POST['gateway']) ? sanitize_text_field($_POST['gateway']) : ''; if (!$invoice_id || !$gateway) { wp_send_json_error(['message' => __('Invalid request', 'easy-invoice')]); } $gateway_instance = $this->gatewayManager->getGateway($gateway); if (!$gateway_instance) { wp_send_json_error(['message' => __('Invalid payment gateway', 'easy-invoice')]); } $result = $gateway_instance->handleCallback($_POST); // Send admin notification for manual payments if ($result['success'] && in_array($gateway, ['bank', 'cheque'])) { do_action('easy_invoice_manual_payment_submitted', $invoice_id, $gateway); } if ($result['success']) { wp_send_json_success($result); } else { wp_send_json_error($result); } } /** * Get available payment gateways for an invoice * * @param int $invoice_id * @return array */ public function getAvailableGateways(int $invoice_id): array { $post = get_post($invoice_id); if (!$post || $post->post_type !== \EasyInvoice\Constants\PostTypes::EASY_INVOICE_POST_TYPE) { return []; } $invoice = new \EasyInvoice\Models\Invoice($post); $invoice_status = $invoice->getStatus(); if (!in_array($invoice_status, [ 'unpaid', 'available'])) { return []; } $enabled_gateways = $this->gatewayManager->getEnabledGateways(); if (empty($enabled_gateways)) { return []; } // Get invoice-specific gateways (comma-separated string or empty) $invoice_gateways = $invoice->getPaymentGateways(); $selected_gateways = []; // Handle both string and array formats if (!empty($invoice_gateways)) { if (is_string($invoice_gateways)) { // If it's a string, split by comma $selected_gateways = array_filter(array_map('trim', explode(',', $invoice_gateways))); } elseif (is_array($invoice_gateways)) { // If it's already an array, use it directly $selected_gateways = array_filter($invoice_gateways); } } $available_gateways = []; $gateway_manager = \EasyInvoice\EasyInvoice::getInstance()->getGatewayManager(); // $enabled_gateways is an associative array with gateway_id as key and gateway object as value foreach ($enabled_gateways as $gateway_id => $gateway) { // If invoice has custom gateways selected, only show those // If no custom gateways are selected (empty array), show all enabled gateways if (!empty($selected_gateways) && !in_array($gateway_id, $selected_gateways, true)) { continue; } $is_available = $gateway->isAvailable(); if ($is_available) { $available_gateways[] = [ 'id' => $gateway_id, 'title' => $gateway_manager->getGatewayDisplayName($gateway_id), 'icon' => $gateway->getIcon(), 'description' => $gateway->getDescription() ]; } } return $available_gateways; } /** * Update payment via AJAX */ public function updatePayment() { check_ajax_referer('easy_invoice_payment', 'payment_nonce'); // Authorisation: this handler mutates payment-record fields // (amount, method, status, notes) and on status=completed it // can flip the linked invoice to paid via // updateInvoiceStatusIfPaid(). The shared `easy_invoice_payment` // nonce is rendered on every public invoice page so any // authenticated visitor can obtain a valid one — the nonce is // CSRF defense, NOT authorisation. Gate on the same payment- // management capability as the sibling verifyManualPayment / // rejectManualPayment / mark_invoice_paid_ajax handlers. if (!easy_invoice_user_can('ei_record_payment')) { wp_send_json_error(['message' => __('You do not have permission to update payments.', 'easy-invoice')]); return; } $payment_id = isset($_POST['payment_id']) ? intval($_POST['payment_id']) : 0; $invoice_id = isset($_POST['invoice_id']) ? intval($_POST['invoice_id']) : 0; $amount = isset($_POST['amount']) ? floatval($_POST['amount']) : 0; $payment_method = isset($_POST['payment_method']) ? sanitize_text_field($_POST['payment_method']) : ''; $payment_date = isset($_POST['payment_date']) ? sanitize_text_field($_POST['payment_date']) : date('Y-m-d'); $status = isset($_POST['status']) ? sanitize_text_field($_POST['status']) : 'pending'; $notes = isset($_POST['notes']) ? sanitize_textarea_field($_POST['notes']) : ''; if (!$payment_id || !$invoice_id || !$amount || !$payment_method) { wp_send_json_error(['message' => __('Missing required fields', 'easy-invoice')]); return; } try { // Check if payment post exists before instantiating $payment_post = get_post($payment_id); if (!$payment_post || $payment_post->post_type !== 'easy_invoice_payment') { wp_send_json_error(['message' => __('Invalid payment', 'easy-invoice')]); return; } $payment = new Payment($payment_post); // Get the old payment status before updating $old_status = $payment->getStatus(); $post = get_post($invoice_id); if (!$post || $post->post_type !== \EasyInvoice\Constants\PostTypes::EASY_INVOICE_POST_TYPE) { wp_send_json_error(['message' => __('Invalid invoice', 'easy-invoice')]); return; } $invoice = new Invoice($post); $payment_data = [ 'invoice_id' => $invoice_id, 'amount' => $amount, 'payment_method' => $payment_method, 'payment_date' => $payment_date, 'status' => $status, 'notes' => $notes, 'gateway_response' => [ 'method' => $payment_method, 'date' => $payment_date, 'notes' => $notes ] ]; $result = $payment->update($payment_data); if ($result) { // Update invoice status based on payment status change if ($status === 'completed' && $old_status !== 'completed') { // Payment changed TO completed - check if invoice should be marked as paid $invoice->setMeta('_payment_method', $payment_method); $this->updateInvoiceStatusIfPaid($invoice_id, $invoice, 'manual'); } elseif ($status !== 'completed' && $old_status === 'completed') { // Payment changed FROM completed to another status (failed, pending, etc.) // Recalculate total payments and update invoice status accordingly $total_payments = $this->calculateTotalPaymentsForInvoice($invoice_id); $invoice_total = $invoice->getTotal(); if ($total_payments < $invoice_total) { // Not enough payments anymore, revert invoice to draft/pending $invoice->setStatus('draft'); $invoice->save(); error_log("Easy Invoice: Invoice #$invoice_id status reverted to 'draft' - payment marked as $status"); } else { // Still enough payments from other completed payments $this->updateInvoiceStatusIfPaid($invoice_id, $invoice, 'manual'); } } wp_send_json_success([ 'message' => __('Payment updated successfully', 'easy-invoice') ]); } else { wp_send_json_error(['message' => __('Failed to update payment', 'easy-invoice')]); } } catch (\Exception $e) { wp_send_json_error(['message' => $e->getMessage()]); } } /** * Verify manual payment */ public function verifyManualPayment(): void { // Check permissions if (!easy_invoice_user_can('ei_record_payment')) { wp_send_json_error(['message' => __('You do not have permission to perform this action', 'easy-invoice')]); return; } // Verify nonce check_ajax_referer('easy_invoice_admin', 'nonce'); $invoice_id = isset($_POST['invoice_id']) ? intval($_POST['invoice_id']) : 0; $amount = isset($_POST['amount']) ? floatval($_POST['amount']) : 0; $payment_method = isset($_POST['payment_method']) ? sanitize_text_field($_POST['payment_method']) : ''; $notes = isset($_POST['notes']) ? sanitize_textarea_field($_POST['notes']) : ''; $transaction_id = isset($_POST['transaction_id']) ? sanitize_text_field($_POST['transaction_id']) : ''; if (!$invoice_id || !$amount || !$payment_method) { wp_send_json_error(['message' => __('Missing required fields', 'easy-invoice')]); return; } // Get the invoice $post = get_post($invoice_id); if (!$post || $post->post_type !== \EasyInvoice\Constants\PostTypes::EASY_INVOICE_POST_TYPE) { wp_send_json_error(['message' => __('Invalid invoice', 'easy-invoice')]); return; } $invoice = new Invoice($post); // Get currency settings $settings_controller = new \EasyInvoice\Controllers\SettingsController(); $settings = $settings_controller->getSettings(); $currency_code = $settings['easy_invoice_currency_code'] ?? 'USD'; $currency_symbol = \EasyInvoice\Helpers\CurrencyHelper::getCurrencySymbol($currency_code); $payment_data = [ 'invoice_id' => $invoice_id, 'amount' => $amount, 'payment_method' => $payment_method, 'payment_date' => current_time('mysql'), 'notes' => $notes, 'status' => 'completed', 'payment_type' => 'full', 'transaction_id' => $transaction_id, 'recurring_id' => '', 'parent_payment_id' => '', 'currency' => $currency_code, 'currency_symbol' => $currency_symbol, 'gateway_response' => [ 'admin_verified' => true, 'verification_date' => current_time('mysql'), 'verification_user' => get_current_user_id() ] ]; try { $payment = Payment::create($payment_data); // Store payment details before updating status (for the hook) $invoice->setMeta('_payment_method', $payment_method); if ($transaction_id) { $invoice->setMeta('_transaction_id', $transaction_id); } // Update invoice status to paid only if total payments are sufficient // This will trigger 'easy_invoice_payment_completed' hook which sends admin notification $this->updateInvoiceStatusIfPaid($invoice_id, $invoice, 'manual'); // Send confirmation email to customer $this->sendPaymentConfirmationEmail($invoice_id, $payment->getId()); wp_send_json_success([ 'message' => __('Payment verified successfully', 'easy-invoice'), 'payment_id' => $payment->getId() ]); } catch (\Exception $e) { wp_send_json_error(['message' => $e->getMessage()]); } } /** * Reject manual payment */ public function rejectManualPayment(): void { // Check permissions — rejecting a manual payment is a record-payment // operation (it transitions state, doesn't refund money). if (!easy_invoice_user_can('ei_record_payment')) { wp_send_json_error(['message' => __('You do not have permission to perform this action', 'easy-invoice')]); return; } // Verify nonce check_ajax_referer('easy_invoice_admin', 'nonce'); $invoice_id = isset($_POST['invoice_id']) ? intval($_POST['invoice_id']) : 0; $reason = isset($_POST['reason']) ? sanitize_textarea_field($_POST['reason']) : ''; if (!$invoice_id) { wp_send_json_error(['message' => __('Invoice ID is required', 'easy-invoice')]); return; } // Get the invoice $post = get_post($invoice_id); if (!$post || $post->post_type !== \EasyInvoice\Constants\PostTypes::EASY_INVOICE_POST_TYPE) { wp_send_json_error(['message' => __('Invalid invoice', 'easy-invoice')]); return; } $invoice = new Invoice($post); // Update invoice status update_post_meta($invoice_id, '_payment_status', 'rejected'); // Add rejection reason update_post_meta($invoice_id, '_payment_rejection_reason', $reason); update_post_meta($invoice_id, '_payment_rejection_date', current_time('mysql')); update_post_meta($invoice_id, '_payment_rejection_user', get_current_user_id()); // Send rejection email to customer $this->sendPaymentRejectionEmail($invoice_id, $reason); wp_send_json_success([ 'message' => __('Payment rejected successfully', 'easy-invoice') ]); } /** * Send payment confirmation email to customer * * @param int $invoice_id * @param int $payment_id */ private function sendPaymentConfirmationEmail($invoice_id, $payment_id): void { $invoice = new Invoice(get_post($invoice_id)); if (!$invoice || !$invoice->getId()) { return; } // Use EmailManager to send payment confirmation using proper template system // This will check if payment email is enabled in settings $email_manager = \EasyInvoice\Services\EmailManager::getInstance(); $email_manager->sendInvoiceEmail($invoice, 'paid', [ 'payment_id' => $payment_id, 'skip_bcc' => true // Skip BCC to admin since this is a direct call ]); } /** * Send payment rejection email to customer * * @param int $invoice_id * @param string $reason */ private function sendPaymentRejectionEmail($invoice_id, $reason): void { $invoice = new Invoice(get_post($invoice_id)); if (!$invoice || !$invoice->getId()) { return; } // Use EmailManager to send payment rejection $email_manager = \EasyInvoice\Services\EmailManager::getInstance(); $email_manager->sendPaymentRejectionEmail($invoice, $reason); } /** * Add pending payment statuses to admin filters * * @param array $statuses * @return array */ public function addPendingPaymentStatuses($statuses): array { $statuses['pending-bank'] = __('Pending Bank Transfer', 'easy-invoice'); $statuses['pending-cheque'] = __('Pending Cheque', 'easy-invoice'); return $statuses; } /** * Add payment method column to payments list * * @param array $columns * @return array */ public function addPaymentMethodColumn($columns): array { $new_columns = []; foreach ($columns as $key => $value) { $new_columns[$key] = $value; if ($key === 'title') { $new_columns['payment_method'] = __('Payment Method', 'easy-invoice'); } } return $new_columns; } /** * Render payment method column * * @param string $column * @param int $post_id */ public function renderPaymentMethodColumn($column, $post_id): void { if ($column === 'payment_method') { $payment_method = get_post_meta($post_id, '_payment_method', true); $payment_methods = [ 'paypal' => __('PayPal', 'easy-invoice') ]; echo isset($payment_methods[$payment_method]) ? esc_html($payment_methods[$payment_method]) : esc_html($payment_method); } } /** * Send payment reminders for pending manual payments */ public function sendPaymentReminders(): void { // Get invoices with pending manual payments $pending_invoices = get_posts([ 'post_type' => \EasyInvoice\Constants\PostTypes::EASY_INVOICE_POST_TYPE, 'posts_per_page' => -1, 'meta_query' => [ 'relation' => 'AND', [ 'key' => '_payment_status', 'value' => ['pending-bank', 'pending-cheque'], 'compare' => 'IN' ], [ 'key' => '_payment_reminder_sent', 'compare' => 'NOT EXISTS' ] ] ]); if (!empty($pending_invoices)) { // Get currency settings $settings_controller = new \EasyInvoice\Controllers\SettingsController(); $settings = $settings_controller->getSettings(); $currency_code = $settings['easy_invoice_currency_code'] ?? 'USD'; $currency_symbol = \EasyInvoice\Helpers\CurrencyHelper::getCurrencySymbol($currency_code); foreach ($pending_invoices as $post) { $invoice = new Invoice($post); if (!$invoice || !$invoice->getId()) { continue; } // Use EmailManager to send payment reminder $email_manager = \EasyInvoice\Services\EmailManager::getInstance(); $result = $email_manager->sendInvoiceEmail($invoice, 'reminder', [ 'payment_method' => get_post_meta($invoice->getId(), '_payment_method', true) ]); // Mark reminder as sent if email was sent successfully if ($result['success']) { update_post_meta($invoice->getId(), '_payment_reminder_sent', current_time('mysql')); } } wp_reset_postdata(); } } /** * Submit manual payment */ public function submitManualPayment(): void { // CSRF defense — keep the existing nonce check. The nonce is // global (`easy_invoice_payment`) so any public invoice page leaks // a valid value; the REAL authorisation gate is the ownership // check below. if (!wp_verify_nonce($_POST['nonce'] ?? '', 'easy_invoice_payment')) { wp_send_json_error(['message' => __('Security check failed', 'easy-invoice')]); return; } $invoice_id = isset($_POST['invoice_id']) ? intval($_POST['invoice_id']) : 0; $payment_type = isset($_POST['payment_type']) ? sanitize_text_field($_POST['payment_type']) : ''; $payment_notes = isset($_POST['payment_notes']) ? sanitize_textarea_field($_POST['payment_notes']) : ''; if (!$invoice_id || !$payment_type) { wp_send_json_error(['message' => __('Missing required fields', 'easy-invoice')]); return; } // Get invoice $invoice_post = get_post($invoice_id); if (!$invoice_post || $invoice_post->post_type !== \EasyInvoice\Constants\PostTypes::EASY_INVOICE_POST_TYPE) { wp_send_json_error(['message' => __('Invalid invoice', 'easy-invoice')]); return; } $invoice = new \EasyInvoice\Models\Invoice($invoice_post); // Authorisation: reject unless the caller is the legitimate email // recipient (per-invoice access token), an admin, or the // logged-in client bound to this invoice. Without this gate the // public AJAX endpoint allowed any visitor with a harvested // global nonce to flood arbitrary invoices into // `pending_verification` and attach payment-proof uploads. if (!\EasyInvoice\Controllers\InvoiceController::canSubmitPaymentForInvoice($invoice_id, $invoice)) { wp_send_json_error([ 'message' => __('You do not have permission to submit a payment for this invoice.', 'easy-invoice'), ]); return; } $currency_code = $invoice->getCurrencyCode() ?: 'USD'; if ($currency_code === 'global') { $currency_code = get_option('easy_invoice_currency_code', 'USD'); } $currency_symbol = \EasyInvoice\Helpers\CurrencyHelper::getCurrencySymbol($currency_code); // Handle file upload (never trust client MIME or filename extension — use WordPress filetype APIs) $proof_url = ''; if (isset($_FILES['payment_proof']) && $_FILES['payment_proof']['error'] === UPLOAD_ERR_OK) { $file = $_FILES['payment_proof']; if (empty($file['tmp_name']) || !is_uploaded_file($file['tmp_name'])) { wp_send_json_error(['message' => __('Invalid upload.', 'easy-invoice')]); return; } $max_size = 5 * 1024 * 1024; // 5MB if ($file['size'] > $max_size) { wp_send_json_error(['message' => __('File size must be less than 5MB.', 'easy-invoice')]); return; } $allowed_mimes = [ 'jpg|jpeg|jpe' => 'image/jpeg', 'png' => 'image/png', 'gif' => 'image/gif', 'pdf' => 'application/pdf', ]; $checked = wp_check_filetype_and_ext($file['tmp_name'], $file['name'], $allowed_mimes); if (empty($checked['ext']) || empty($checked['type'])) { wp_send_json_error(['message' => __('Invalid file type. Only JPG, PNG, GIF, and PDF files are allowed.', 'easy-invoice')]); return; } $allowed_types = array_values($allowed_mimes); if (!in_array($checked['type'], $allowed_types, true)) { wp_send_json_error(['message' => __('Invalid file type. Only JPG, PNG, GIF, and PDF files are allowed.', 'easy-invoice')]); return; } $upload_dir = wp_upload_dir(); $proof_dir = $upload_dir['basedir'] . '/easy-invoice/payment-proofs/'; if (!wp_mkdir_p($proof_dir)) { wp_send_json_error(['message' => __('Could not create upload directory.', 'easy-invoice')]); return; } $filename = uniqid('payment_proof_', true) . '.' . $checked['ext']; $filepath = $proof_dir . $filename; if (!move_uploaded_file($file['tmp_name'], $filepath)) { wp_send_json_error(['message' => __('Failed to save payment proof file.', 'easy-invoice')]); return; } chmod($filepath, 0644); $proof_url = $upload_dir['baseurl'] . '/easy-invoice/payment-proofs/' . $filename; } // Create payment record $payment_data = [ 'post_title' => sprintf('Manual Payment (%s) for Invoice #%s', ucfirst($payment_type), $invoice->getNumber()), 'post_type' => 'easy_invoice_payment', 'post_status' => 'publish', 'post_author' => get_current_user_id(), ]; $payment_id = wp_insert_post($payment_data); if (is_wp_error($payment_id)) { wp_send_json_error(['message' => __('Failed to create payment record', 'easy-invoice')]); return; } // Save payment metadata update_post_meta($payment_id, '_invoice_id', $invoice_id); update_post_meta($payment_id, '_amount', $invoice->getTotal()); update_post_meta($payment_id, '_payment_method', 'manual'); update_post_meta($payment_id, '_payment_type', $payment_type); update_post_meta($payment_id, '_status', 'pending'); update_post_meta($payment_id, '_transaction_id', 'MANUAL-' . $invoice_id . '-' . time()); update_post_meta($payment_id, '_payment_date', current_time('mysql')); update_post_meta($payment_id, '_notes', $payment_notes); update_post_meta($payment_id, '_currency', $currency_code); update_post_meta($payment_id, '_currency_symbol', $currency_symbol); update_post_meta($payment_id, '_payment_proof', $proof_url); // Update invoice status to pending verification $invoice->setStatus('pending_verification'); $invoice->save(); // Store payment details on invoice $invoice->setMeta('_payment_method', 'manual'); $invoice->setMeta('_payment_type', $payment_type); $invoice->setMeta('_payment_status', 'pending'); $invoice->setMeta('_manual_payment_id', $payment_id); $invoice->setMeta('_manual_payment_proof', $proof_url); $invoice->setMeta('_manual_payment_notes', $payment_notes); // Send admin notification do_action('easy_invoice_manual_payment_submitted', $invoice_id, $payment_type); wp_send_json_success([ 'message' => __('Payment submitted successfully! Your payment will be verified by the administrator.', 'easy-invoice'), 'payment_id' => $payment_id ]); } /** * Handle submission of payment proof for manual gateways (Bank Transfer, Cheque) */ public function submitPaymentProof(): void { $gateway_name = isset($_POST['gateway']) ? sanitize_text_field($_POST['gateway']) : ''; $invoice_id = isset($_POST['invoice_id']) ? intval($_POST['invoice_id']) : 0; if (empty($gateway_name) || empty($invoice_id)) { wp_send_json_error(['message' => __('Invalid request. Missing gateway or invoice ID.', 'easy-invoice')]); return; } // Nonce verification (make nonce name consistent or check based on gateway) $nonce_action = 'easy_invoice_payment_proof_' . $invoice_id; // Bank transfer nonce $nonce_value = isset($_POST['payment_proof_nonce']) ? sanitize_text_field($_POST['payment_proof_nonce']) : ''; if ($gateway_name === 'cheque') { $nonce_action = 'easy_invoice_cheque_notification_' . $invoice_id; // Cheque nonce $nonce_value = isset($_POST['cheque_notification_nonce']) ? sanitize_text_field($_POST['cheque_notification_nonce']) : ''; } if (!wp_verify_nonce($nonce_value, $nonce_action)) { wp_send_json_error(['message' => __('Nonce verification failed. Please try again.', 'easy-invoice')]); return; } // Optional: Add capability check if this can be submitted by logged-in users only from frontend // if (is_user_logged_in() && !current_user_can('read_invoice', $invoice_id)) { // Example capability // wp_send_json_error(['message' => __('You do not have permission to submit proof for this invoice.', 'easy-invoice')]); // return; // } $gateway = $this->gatewayManager->getGateway($gateway_name); if (!$gateway || !method_exists($gateway, 'handleProofSubmission')) { wp_send_json_error(['message' => __('Invalid payment gateway or submission handler not found.', 'easy-invoice')]); return; } // Prepare data for the gateway handler $post_data = stripslashes_deep($_POST); $files_data = $_FILES; $result = $gateway->handleProofSubmission($post_data, $files_data); if ($result['success']) { wp_send_json_success(['message' => $result['message']]); } else { wp_send_json_error(['message' => $result['message']]); } } /** * AJAX handler for admin to mark an invoice as paid. */ public function mark_invoice_paid_ajax(): void { $invoice_id = isset($_POST['invoice_id']) ? intval($_POST['invoice_id']) : 0; $nonce = isset($_POST['nonce']) ? sanitize_text_field($_POST['nonce']) : ''; $notes = isset($_POST['notes']) ? sanitize_textarea_field($_POST['notes']) : ''; if (empty($invoice_id) || !wp_verify_nonce($nonce, 'easy_invoice_approve_payment')) { easy_invoice_toast_error(__('Invalid request or security check failed.', 'easy-invoice')); return; } // Mark-as-paid is a record-payment action — gated by the matching cap. if (!easy_invoice_user_can('ei_record_payment')) { easy_invoice_toast_error(__('You do not have permission to perform this action.', 'easy-invoice')); return; } $invoice_post = get_post($invoice_id); if (!$invoice_post || $invoice_post->post_type !== \EasyInvoice\Constants\PostTypes::EASY_INVOICE_POST_TYPE) { wp_send_json_error(['message' => __('Invalid invoice.', 'easy-invoice')]); return; } $invoice = new Invoice($invoice_post); // For manual approval, always use 'manual' as payment method $payment_method = 'manual'; // Update invoice post status to 'publish' (or your primary paid status) wp_update_post(['ID' => $invoice_id, 'post_status' => 'publish']); update_post_meta($invoice_id, '_payment_status', 'completed'); // General completed status for payments // Allow plugins to control invoice status update $should_update_invoice_status = apply_filters('easy_invoice_should_update_invoice_status', true, $invoice_id); if ($should_update_invoice_status) { update_post_meta($invoice_id, InvoiceFields::STATUS, 'paid'); // Specific invoice status field if used by model } // Use submitted notes or default note $payment_notes = !empty($notes) ? $notes : __('Payment manually verified by admin.', 'easy-invoice'); // Find existing pending payment records for this invoice $existing_payment_args = [ 'post_type' => 'easy_invoice_payment', 'posts_per_page' => 1, 'meta_query' => [ 'relation' => 'AND', [ 'key' => '_invoice_id', 'value' => $invoice_id, ], [ 'key' => '_status', 'value' => ['pending-bank', 'pending-cheque', 'pending'], // Check against pending statuses 'compare' => 'IN' ] ] ]; $existing_payments = get_posts($existing_payment_args); $payment_id = null; if (!empty($existing_payments)) { // Update existing pending payment instead of creating new one $payment_id = $existing_payments[0]->ID; update_post_meta($payment_id, '_status', 'completed'); // Update status to completed update_post_meta($payment_id, '_payment_method', 'manual'); // Set payment method to manual update_post_meta($payment_id, '_transaction_id', 'MANUAL-' . $invoice_id . '-' . time()); update_post_meta($payment_id, '_payment_date', current_time('mysql')); update_post_meta($payment_id, '_notes', $payment_notes); // Update notes on existing payment } else { // Only create a new payment if no pending payments exist // This prevents creating duplicate payment records $existing_payments = get_posts([ 'post_type' => 'easy_invoice_payment', 'posts_per_page' => -1, 'meta_query' => [ [ 'key' => '_invoice_id', 'value' => $invoice_id, ] ] ]); if (!empty($existing_payments)) { // If payments exist but none are pending, don't create a new one // Just update the invoice status easy_invoice_toast_success(__('Invoice marked as paid successfully.', 'easy-invoice')); return; } // Get currency from invoice $currency_code = get_post_meta($invoice_id, '_easy_invoice_currency_code', true); if (empty($currency_code) || $currency_code === 'global') { $currency_code = get_option('easy_invoice_currency_code', 'USD'); } $currency_symbol = \EasyInvoice\Helpers\CurrencyHelper::getCurrencySymbol($currency_code); $payment_data = [ 'invoice_id' => $invoice_id, 'amount' => $invoice->getTotal(), // Or get amount from proof submission if it varies 'payment_method' => $payment_method, 'status' => 'completed', 'transaction_id' => get_post_meta($invoice_id, '_' . $payment_method . '_transaction_id', true) ?: 'MANUAL-' . $invoice_id, 'payment_date' => current_time('mysql'), 'notes' => $payment_notes, // Use provided notes 'payment_type' => 'manual', 'currency' => $currency_code, 'currency_symbol' => $currency_symbol, 'gateway_response' => json_encode([ 'admin_verified' => true, 'user' => get_current_user_id(), 'verification_date' => current_time('mysql'), 'notes' => $payment_notes // Store notes in response JSON as well ]) ]; try { // Create payment record using WordPress post creation $payment_post_data = [ 'post_title' => sprintf('Manual Payment for Invoice #%s', $invoice->getNumber()), 'post_type' => 'easy_invoice_payment', 'post_status' => 'publish', 'post_author' => get_current_user_id(), 'meta_input' => [ '_invoice_id' => $invoice_id, '_amount' => $invoice->getTotal(), '_payment_method' => $payment_method, '_status' => 'completed', '_transaction_id' => get_post_meta($invoice_id, '_' . $payment_method . '_transaction_id', true) ?: 'MANUAL-' . $invoice_id, '_payment_date' => current_time('mysql'), '_notes' => $payment_notes, '_payment_type' => 'manual', '_currency' => $currency_code, '_currency_symbol' => $currency_symbol, '_gateway_response' => json_encode([ 'admin_verified' => true, 'user' => get_current_user_id(), 'verification_date' => current_time('mysql'), 'notes' => $payment_notes ]) ] ]; $payment_id = wp_insert_post($payment_post_data); if (is_wp_error($payment_id)) { easy_invoice_toast_error(__('Error creating payment record:', 'easy-invoice') . ' ' . $payment_id->get_error_message()); return; } } catch (\Exception $e) { easy_invoice_toast_error(__('Error creating payment record:', 'easy-invoice') . ' ' . $e->getMessage()); return; } } // Store payment details before updating status (for the hook) $transaction_id = get_post_meta($invoice_id, '_' . $payment_method . '_transaction_id', true) ?: 'MANUAL-' . $invoice_id; $invoice->setMeta('_payment_method', $payment_method); $invoice->setMeta('_transaction_id', $transaction_id); // Update invoice status to paid // This will trigger 'easy_invoice_payment_completed' hook which sends admin notification $invoice->setStatus('paid'); $invoice->save(); // Trigger the payment completed hook manually since we're updating status directly do_action('easy_invoice_payment_completed', $invoice_id, $invoice, [ 'payment_method' => $payment_method, 'gateway_name' => 'manual', 'transaction_id' => $transaction_id, 'amount' => $invoice->getTotal() ]); // Trigger email confirmation and actions only if we have a payment_id if ($payment_id) { // Send confirmation email to customer $this->sendPaymentConfirmationEmail($invoice_id, $payment_id); do_action('easy_invoice_manual_payment_confirmed', $invoice_id, $payment_id, $payment_method); } easy_invoice_toast_success(__('Invoice marked as paid successfully.', 'easy-invoice')); } /** * Handle bulk actions for payments */ public function handleBulkActions() { // Check if we're processing a bulk action if (!isset($_POST['action']) || $_POST['action'] !== 'easy_invoice_payment_bulk_action') { return; } // Check nonce and capability if (!wp_verify_nonce($_POST['easy_invoice_payment_bulk_nonce'], 'easy_invoice_payment_bulk_action')) { wp_die(__('Security check failed.', 'easy-invoice')); } // Bulk action on payments — record-payment cap is the right gate // (covers trash/restore/delete which all change payment state). if (!easy_invoice_user_can('ei_record_payment')) { wp_die(__('You do not have permission to perform this action.', 'easy-invoice')); } // Check if we have payment IDs if (!isset($_POST['payment_ids']) || !is_array($_POST['payment_ids']) || empty($_POST['payment_ids'])) { wp_redirect(admin_url('admin.php?page=easy-invoice-payments&bulk_error=no_selection')); exit; } // Get bulk action and payment IDs $bulk_action = isset($_POST['bulk_action']) ? sanitize_text_field($_POST['bulk_action']) : ''; $payment_ids = array_map('intval', $_POST['payment_ids']); // Process based on action $processed = 0; $invoice_updates = array(); // Track invoice updates needed switch ($bulk_action) { case 'trash': foreach ($payment_ids as $id) { // Get payment info before trashing for invoice status update $payment_post = get_post($id); if (!$payment_post || $payment_post->post_type !== 'easy_invoice_payment') { continue; } $payment = new Payment($payment_post); $payment_status = $payment->getStatus(); $invoice_id = $payment->getInvoiceId(); $payment_amount = $payment->getAmount(); if (wp_trash_post($id)) { $processed++; // Track invoice updates needed for completed payments if ($payment_status === 'completed' && $invoice_id) { if (!isset($invoice_updates[$invoice_id])) { $invoice_updates[$invoice_id] = 0; } $invoice_updates[$invoice_id] += $payment_amount; } } } // Update invoice statuses for completed payments that were trashed foreach ($invoice_updates as $invoice_id => $deleted_amount) { $this->updateInvoiceStatusAfterPaymentDeletion($invoice_id, $deleted_amount); } wp_redirect(admin_url('admin.php?page=easy-invoice-payments&bulk_trashed=' . $processed)); break; case 'restore': foreach ($payment_ids as $id) { // Get payment info before restoring for invoice status update $payment_post = get_post($id); if (!$payment_post || $payment_post->post_type !== 'easy_invoice_payment') { continue; } $payment = new Payment($payment_post); $payment_status = $payment->getStatus(); $invoice_id = $payment->getInvoiceId(); $payment_amount = $payment->getAmount(); if (wp_untrash_post($id)) { // Also set status to publish (since WordPress sets it to draft by default) wp_update_post(array( 'ID' => $id, 'post_status' => 'publish' )); $processed++; // Track invoice updates needed for completed payments if ($payment_status === 'completed' && $invoice_id) { if (!isset($invoice_updates[$invoice_id])) { $invoice_updates[$invoice_id] = 0; } $invoice_updates[$invoice_id] += $payment_amount; } } } // Update invoice statuses for completed payments that were restored foreach ($invoice_updates as $invoice_id => $restored_amount) { $this->updateInvoiceStatusAfterPaymentRestoration($invoice_id, $restored_amount); } wp_redirect(admin_url('admin.php?page=easy-invoice-payments&bulk_restored=' . $processed)); break; case 'delete': foreach ($payment_ids as $id) { // Get payment info before deletion for invoice status update $payment_post = get_post($id); if (!$payment_post || $payment_post->post_type !== 'easy_invoice_payment') { continue; } $payment = new Payment($payment_post); $payment_status = $payment->getStatus(); $invoice_id = $payment->getInvoiceId(); $payment_amount = $payment->getAmount(); if (wp_delete_post($id, true)) { $processed++; // Track invoice updates needed for completed payments if ($payment_status === 'completed' && $invoice_id) { if (!isset($invoice_updates[$invoice_id])) { $invoice_updates[$invoice_id] = 0; } $invoice_updates[$invoice_id] += $payment_amount; } } } // Update invoice statuses for completed payments that were deleted foreach ($invoice_updates as $invoice_id => $deleted_amount) { $this->updateInvoiceStatusAfterPaymentDeletion($invoice_id, $deleted_amount); } wp_redirect(admin_url('admin.php?page=easy-invoice-payments&bulk_deleted=' . $processed)); break; default: wp_redirect(admin_url('admin.php?page=easy-invoice-payments&bulk_error=invalid_action')); } exit; } /** * Update invoice status after payment deletion */ private function updateInvoiceStatusAfterPaymentDeletion($invoice_id, $deleted_amount) { $invoice = new Invoice($invoice_id); if (!$invoice->getId()) { return; } // Get all remaining payments for this invoice $remaining_payments = get_posts(array( 'post_type' => 'easy_invoice_payment', 'post_status' => 'publish', 'meta_query' => array( array( 'key' => '_invoice_id', 'value' => $invoice_id, 'compare' => '=' ), array( 'key' => '_status', 'value' => 'completed', 'compare' => '=' ) ), 'posts_per_page' => -1 )); // Calculate total remaining payments $total_remaining = 0; foreach ($remaining_payments as $payment_post) { $payment = new Payment($payment_post); $total_remaining += floatval($payment->getAmount()); } $invoice_total = floatval($invoice->getTotal()); // Update invoice status based on remaining payments if ($total_remaining >= $invoice_total) { // Still fully paid update_post_meta($invoice_id, '_status', 'paid'); } elseif ($total_remaining > 0) { // Partially paid update_post_meta($invoice_id, '_status', 'partial'); } else { // No payments remaining update_post_meta($invoice_id, '_status', 'unpaid'); } } /** * Update invoice status after payment restoration */ private function updateInvoiceStatusAfterPaymentRestoration($invoice_id, $restored_amount) { $invoice = new Invoice($invoice_id); if (!$invoice->getId()) { return; } // Get all payments for this invoice (including the restored one) $all_payments = get_posts(array( 'post_type' => 'easy_invoice_payment', 'post_status' => 'publish', 'meta_query' => array( array( 'key' => '_invoice_id', 'value' => $invoice_id, 'compare' => '=' ), array( 'key' => '_status', 'value' => 'completed', 'compare' => '=' ) ), 'posts_per_page' => -1 )); // Calculate total payments (including restored ones) $total_payments = 0; foreach ($all_payments as $payment_post) { $payment = new Payment($payment_post); $total_payments += floatval($payment->getAmount()); } $invoice_total = floatval($invoice->getTotal()); // Update invoice status based on total payments if ($total_payments >= $invoice_total) { // Fully paid update_post_meta($invoice_id, '_status', 'paid'); } elseif ($total_payments > 0) { // Partially paid update_post_meta($invoice_id, '_status', 'partial'); } else { // No payments update_post_meta($invoice_id, '_status', 'unpaid'); } } // Stripe payment recording moved to Pro plugin }