PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.3.5
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.3.5
2.4.3 2.4.4 2.4.2 2.4.0 2.4.1 2.3.8 2.3.7 2.3.6 2.3.5 2.3.4 2.3.3 2.3.2 2.3.1 2.2.0 2.1.21 2.1.20 2.1.19 2.1.18 2.1.0 2.1.1 2.1.10 2.1.11 2.1.12 2.1.13 2.1.14 All 60 releases
easy-invoice / includes / Controllers / PaymentController.php

PaymentController.php in Easy Invoice – Invoice Generator, PDF Quotes & Payments 2.3.5, at includes/Controllers/PaymentController.php

1,646 lines 65.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Payment Controller
4 *
5 * @package Easy_Invoice
6 */
7
8 namespace EasyInvoice\Controllers;
9
10 use EasyInvoice\Constants\PostTypes;
11 use EasyInvoice\PaymentGatewayManager;
12 use EasyInvoice\EasyInvoice;
13 use EasyInvoice\Models\Invoice;
14 use EasyInvoice\Models\Payment;
15 use EasyInvoice\Traits\TemplateTrait;
16 use EasyInvoice\Traits\PaymentCalculationTrait;
17 use EasyInvoice\Constants\PagesSlugs;
18 use EasyInvoice\Constants\InvoiceFields;
19 use EasyInvoice\Constants\InvoiceMetaKeys;
20 use EasyInvoice\Helpers\Sanitization;
21 use EasyInvoice\Providers\InvoiceServiceProvider; // Assuming this is used elsewhere or for future
22
23 /**
24 * Class PaymentController
25 *
26 * @package EasyInvoice\Controllers
27 */
28 class PaymentController extends BaseController {
29 use TemplateTrait;
30 use PaymentCalculationTrait;
31
32 /**
33 * Payment gateway manager instance
34 *
35 * @var PaymentGatewayManager
36 */
37 private $gatewayManager;
38
39 /**
40 * Constructor
41 */
42 public function __construct() {
43 $this->gatewayManager = EasyInvoice::getInstance()->getGatewayManager();
44 }
45
46 /**
47 * Initialize the controller
48 */
49 public function init() {
50 add_action('admin_enqueue_scripts', [$this, 'enqueueAssets']);
51 add_action('wp_ajax_easy_invoice_process_payment', [$this, 'processPayment']);
52 add_action('wp_ajax_nopriv_easy_invoice_process_payment', [$this, 'processPayment']);
53 add_action('wp_ajax_easy_invoice_update_payment', [$this, 'updatePayment']);
54 add_action('wp_ajax_easy_invoice_payment_callback', [$this, 'handleCallback']);
55 add_action('wp_ajax_nopriv_easy_invoice_payment_callback', [$this, 'handleCallback']);
56 add_action('wp_ajax_easy_invoice_verify_manual_payment', [$this, 'verifyManualPayment']);
57 add_action('wp_ajax_easy_invoice_reject_manual_payment', [$this, 'rejectManualPayment']);
58
59
60
61 // Handler for submitting payment proof for manual gateways
62 add_action('wp_ajax_easy_invoice_submit_payment_proof', [$this, 'submitPaymentProof']);
63 add_action('wp_ajax_nopriv_easy_invoice_submit_payment_proof', [$this, 'submitPaymentProof']);
64
65 // Handler for manual payment submission
66 add_action('wp_ajax_easy_invoice_submit_manual_payment', [$this, 'submitManualPayment']);
67 add_action('wp_ajax_nopriv_easy_invoice_submit_manual_payment', [$this, 'submitManualPayment']);
68
69 // Handler for getting payment instructions for manual gateways
70 add_action('wp_ajax_easy_invoice_get_payment_instructions', [$this, 'getPaymentInstructions']);
71 add_action('wp_ajax_nopriv_easy_invoice_get_payment_instructions', [$this, 'getPaymentInstructions']);
72
73 // Enqueue frontend scripts
74 add_action('wp_enqueue_scripts', [$this, 'enqueueFrontendAssets']);
75
76 // Handler for admin to mark an invoice as paid
77 add_action('wp_ajax_easy_invoice_approve_payment', [$this, 'mark_invoice_paid_ajax']);
78
79 // Stripe payment handlers moved to Pro plugin
80
81 add_action('wp_enqueue_scripts', [$this, 'enqueueScripts']);
82
83 // Add filter to show pending payments in admin
84 add_filter('easy_invoice_admin_payment_statuses', [$this, 'addPendingPaymentStatuses']);
85
86 // Add custom columns to payments list
87 add_filter('manage_easy-payment_posts_columns', [$this, 'addPaymentMethodColumn']);
88 add_action('manage_easy-payment_posts_custom_column', [$this, 'renderPaymentMethodColumn'], 10, 2);
89
90 // Add reminder CRON job for pending payments
91 add_action('easy_invoice_payment_reminder', [$this, 'sendPaymentReminders']);
92 if (!wp_next_scheduled('easy_invoice_payment_reminder')) {
93 wp_schedule_event(time(), 'daily', 'easy_invoice_payment_reminder');
94 }
95
96 // Handle bulk actions
97 add_action('admin_init', [$this, 'handleBulkActions']);
98 }
99
100 /**
101 * Get payment instructions for manual gateways
102 */
103 public function getPaymentInstructions() {
104 // Verify nonce
105 if (!wp_verify_nonce($_POST['nonce'], 'easy_invoice_payment')) {
106 wp_send_json_error(['message' => 'Security check failed']);
107 return;
108 }
109
110 $gateway = sanitize_text_field($_POST['gateway']);
111 $invoice_id = intval($_POST['invoice_id']);
112
113 if (!$gateway || !$invoice_id) {
114 wp_send_json_error(['message' => 'Missing required parameters']);
115 return;
116 }
117
118 // Get invoice
119 $invoice_post = get_post($invoice_id);
120 if (!$invoice_post || $invoice_post->post_type !== 'easy_invoice') {
121 wp_send_json_error(['message' => 'Invalid invoice']);
122 return;
123 }
124
125 // Guests may only load instructions for published invoices (avoid leaking draft/private details).
126 if (!easy_invoice_user_can('ei_view_invoices') && $invoice_post->post_status !== 'publish') {
127 wp_send_json_error(['message' => __('Invoice not found', 'easy-invoice')]);
128 return;
129 }
130
131 $invoice = new \EasyInvoice\Models\Invoice($invoice_post);
132
133 // Get gateway instance
134 $gateway_instance = $this->gatewayManager->getGateway($gateway);
135
136 if (!$gateway_instance) {
137 wp_send_json_error(['message' => 'Gateway not found']);
138 return;
139 }
140
141 // Get instructions using the hook system
142 ob_start();
143 do_action('easy_invoice_payment_gateways_after', $invoice, $gateway);
144 $instructions = ob_get_clean();
145
146 if ($instructions) {
147 wp_send_json_success(['instructions' => $instructions]);
148 } else {
149 wp_send_json_error(['message' => 'No instructions available']);
150 }
151 }
152
153 /**
154 * Enqueue admin assets
155 */
156 public function enqueueAssets() {
157 $screen = get_current_screen();
158 if (!$screen || !property_exists($screen, 'id') || strpos($screen->id, 'easy-invoice') === false) {
159 return;
160 }
161
162 // Enqueue manual payment script
163 wp_enqueue_script(
164 'easy-invoice-manual-payment',
165 EASY_INVOICE_PLUGIN_URL . 'assets/js/manual-payment.js',
166 ['jquery'],
167 '1.0.0',
168 true
169 );
170
171 // Localize script
172 wp_localize_script('easy-invoice-manual-payment', 'easy_invoice_ajax', [
173 'ajax_url' => admin_url('admin-ajax.php'),
174 'nonce' => wp_create_nonce('easy_invoice_payment')
175 ]);
176 }
177
178 /**
179 * Enqueue frontend assets
180 */
181 public function enqueueFrontendAssets() {
182 // Only load on invoice pages
183 if (is_singular('easy_invoice')) {
184 wp_enqueue_script(
185 'easy-invoice-manual-payment',
186 EASY_INVOICE_PLUGIN_URL . 'assets/js/manual-payment.js',
187 ['jquery'],
188 '1.0.0',
189 true
190 );
191
192 // Forward the per-invoice access token from the URL to the JS
193 // so the manual-payment AJAX request can present it back to
194 // canSubmitPaymentForInvoice. Without this the legitimate
195 // email-link recipient flow would break — they'd hit the gate.
196 $access_token = isset($_GET['ik'])
197 ? sanitize_text_field(wp_unslash($_GET['ik']))
198 : '';
199
200 wp_localize_script('easy-invoice-manual-payment', 'easy_invoice_ajax', [
201 'ajax_url' => admin_url('admin-ajax.php'),
202 'nonce' => wp_create_nonce('easy_invoice_payment'),
203 'access_token' => $access_token,
204 ]);
205 }
206 }
207
208 /**
209 * Display method implementation
210 *
211 * @param array $args Display arguments
212 */
213 public function display(array $args = []) {
214 $page = isset($args['page']) ? $args['page'] : '';
215
216 switch ($page) {
217 case PagesSlugs::PAYMENTS:
218 $this->displayPaymentsPage();
219 break;
220
221 case PagesSlugs::PAYMENT_NEW:
222 $this->displayTemplate(EASY_INVOICE_PLUGIN_DIR . 'templates/payments/new.php');
223 break;
224
225 case 'view':
226 $payment_id = isset($_GET['id']) ? intval($_GET['id']) : 0;
227 if ($payment_id) {
228 $payment_post = get_post($payment_id);
229 if ($payment_post && $payment_post->post_type === 'easy_invoice_payment') {
230 try {
231 $payment = new Payment($payment_post);
232 $this->displayTemplate(EASY_INVOICE_PLUGIN_DIR . 'templates/payments/view.php', ['payment' => $payment]);
233 } catch (\Exception $e) {
234 wp_die(__('Invalid payment ID', 'easy-invoice'));
235 }
236 } else {
237 wp_die(__('Invalid payment ID', 'easy-invoice'));
238 }
239 } else {
240 wp_die(__('Payment ID is required', 'easy-invoice'));
241 }
242 break;
243
244 case 'edit':
245 $payment_id = isset($_GET['id']) ? intval($_GET['id']) : 0;
246 if ($payment_id) {
247 $payment_post = get_post($payment_id);
248 if ($payment_post && $payment_post->post_type === 'easy_invoice_payment') {
249 try {
250 $payment = new Payment($payment_post);
251 $this->displayTemplate(EASY_INVOICE_PLUGIN_DIR . 'templates/payments/edit.php', ['payment' => $payment]);
252 } catch (\Exception $e) {
253 wp_die(__('Invalid payment ID', 'easy-invoice'));
254 }
255 } else {
256 wp_die(__('Invalid payment ID', 'easy-invoice'));
257 }
258 } else {
259 wp_die(__('Payment ID is required', 'easy-invoice'));
260 }
261 break;
262
263 default:
264 $this->displayPaymentsPage();
265 break;
266 }
267 }
268
269 /**
270 * Display payments page with pagination
271 */
272 protected function displayPaymentsPage() {
273 // Get current view (all, trash)
274 $current_view = isset($_GET['view']) ? sanitize_text_field($_GET['view']) : 'all';
275
276 // Get status filter
277 $status_filter = isset($_GET['status']) ? sanitize_text_field($_GET['status']) : '';
278
279 // Pagination settings
280 $per_page = 20;
281 $current_page = isset($_GET['paged']) ? max(1, intval($_GET['paged'])) : 1;
282
283 // Build query arguments
284 $args = array(
285 'post_type' => 'easy_invoice_payment',
286 'posts_per_page' => $per_page,
287 'paged' => $current_page,
288 'orderby' => 'ID',
289 'order' => 'DESC',
290 'no_found_rows' => false, // We need this for pagination
291 );
292
293 // Set post status based on current view
294 if ($current_view === 'trash') {
295 $args['post_status'] = 'trash';
296 } else {
297 $args['post_status'] = 'publish';
298 }
299
300 // Add status filter if set
301 if (!empty($status_filter)) {
302 $args['meta_query'] = array(
303 array(
304 'key' => '_status',
305 'value' => $status_filter,
306 ),
307 );
308 }
309
310 // Allow plugins to modify query arguments
311 $args = apply_filters('easy_invoice_payment_controller_query_args', $args, $current_view, $status_filter);
312
313
314 // Get paginated payments using WordPress query
315 $wp_query = new \WP_Query($args);
316
317
318 $payments = [];
319
320 if ($wp_query->have_posts()) {
321 while ($wp_query->have_posts()) {
322 $wp_query->the_post();
323 $post = get_post();
324 $payment = new Payment($post);
325 $payments[] = $payment;
326 }
327 }
328
329 wp_reset_postdata();
330
331 // Allow plugins to modify the payments array
332 $payments = apply_filters('easy_invoice_payment_controller_payments_list', $payments, $wp_query);
333
334 // Get pagination info from WordPress query
335 $total_payments = $wp_query->found_posts;
336 $total_pages = $wp_query->max_num_pages;
337
338 // Calculate statistics from ALL payments (not just current page)
339 $stats_args = array(
340 'post_type' => 'easy_invoice_payment',
341 'posts_per_page' => -1, // Get all payments
342 'meta_query' => array(
343 array(
344 'key' => '_status',
345 'compare' => 'EXISTS',
346 ),
347 ),
348 );
349
350 // Set post status for stats based on current view
351 if ($current_view === 'trash') {
352 $stats_args['post_status'] = 'trash';
353 } else {
354 $stats_args['post_status'] = 'publish';
355 }
356
357 $stats_query = new \WP_Query($stats_args);
358
359 $stats = [
360 'total_payments' => $stats_query->found_posts,
361 'total_amount' => 0,
362 'completed_payments' => 0,
363 'pending_payments' => 0,
364 'failed_payments' => 0
365 ];
366
367 // Calculate stats from the query results
368 if ($stats_query->have_posts()) {
369 while ($stats_query->have_posts()) {
370 $stats_query->the_post();
371 $payment = new Payment(get_post());
372
373 $amount = floatval($payment->getAmount());
374 $status = $payment->getStatus();
375
376 $stats['total_amount'] += $amount;
377
378 switch ($status) {
379 case 'completed':
380 $stats['completed_payments']++;
381 break;
382 case 'pending':
383 $stats['pending_payments']++;
384 break;
385 case 'failed':
386 $stats['failed_payments']++;
387 break;
388 }
389 }
390 }
391 wp_reset_postdata();
392
393 // Ensure all required keys exist with default values
394 $stats = array_merge([
395 'total_payments' => 0,
396 'total_amount' => 0,
397 'completed_payments' => 0,
398 'pending_payments' => 0,
399 'failed_payments' => 0
400 ], $stats);
401
402 // Get trash count for tab display
403 $trash_args = array(
404 'post_type' => 'easy_invoice_payment',
405 'post_status' => 'trash',
406 'posts_per_page' => -1
407 );
408 $trash_query = new \WP_Query($trash_args);
409 $trash_count = $trash_query->found_posts;
410
411 // Define available status filters
412 $status_filters = array(
413 'completed' => 'Completed',
414 'pending' => 'Pending',
415 'failed' => 'Failed'
416 );
417
418 // Prepare template data
419 $template_data = [
420 'payments' => $payments,
421 'current_view' => $current_view,
422 'status_filter' => $status_filter,
423 'status_filters' => $status_filters,
424 'trash_count' => $trash_count,
425 'stats' => $stats,
426 'current_page' => $current_page,
427 'per_page' => $per_page,
428 'total_payments' => $total_payments,
429 'total_pages' => $total_pages,
430 'wp_query' => $wp_query
431 ];
432
433 // Allow plugins to modify template data
434 $template_data = apply_filters('easy_invoice_payment_controller_template_data', $template_data);
435
436 // Display the template
437 $this->displayTemplate(
438 EASY_INVOICE_PLUGIN_DIR . 'templates/payments/list.php',
439 $template_data
440 );
441
442 // Allow plugins to perform actions after displaying payments page
443 do_action('easy_invoice_payment_controller_after_display_payments_page', $template_data);
444 }
445
446 /**
447 * Enqueue required scripts and styles
448 */
449 public function enqueueScripts(): void {
450 // Check if scripts are already enqueued
451 if (wp_script_is('easy-invoice-payment', 'enqueued')) {
452 return;
453 }
454 if(!is_singular(PostTypes::EASY_INVOICE_POST_TYPE)){
455 //return;
456 }
457
458 // Enqueue our custom scripts
459 wp_enqueue_script(
460 'easy-invoice-payment',
461 EASY_INVOICE_URL . 'assets/js/payment.js',
462 ['jquery'],
463 EASY_INVOICE_VERSION,
464 true
465 );
466
467 // Get currency settings
468 $settings_controller = new \EasyInvoice\Controllers\SettingsController();
469 $settings = $settings_controller->getSettings();
470 $currency_code = $settings['easy_invoice_currency_code'] ?? 'USD';
471 $currency_symbol = \EasyInvoice\Helpers\CurrencyHelper::getCurrencySymbol($currency_code);
472
473 // Localize script variables for payment form
474 wp_localize_script('easy-invoice-payment', 'easy_invoice_vars', [
475 'ajax_url' => admin_url('admin-ajax.php'),
476 'nonce' => wp_create_nonce('easy_invoice_payment'),
477 'currency_symbol' => $currency_symbol,
478 'currency_code' => $currency_code
479 ]);
480 }
481
482 // Stripe methods moved to Pro plugin
483
484 /**
485 * Process payment via AJAX
486 */
487 public function processPayment() {
488 check_ajax_referer('easy_invoice_payment', 'payment_nonce');
489
490 $invoice_id = isset($_POST['invoice_id']) ? intval($_POST['invoice_id']) : 0;
491 $payment_method_slug = isset($_POST['payment_method']) ? sanitize_text_field($_POST['payment_method']) : '';
492
493 // Add filter for extensions to handle custom payment logic (e.g., partial payments)
494 $custom_result = apply_filters('easy_invoice_before_process_payment', null, $invoice_id, $_POST);
495
496 if (is_array($custom_result) && isset($custom_result['handled']) && $custom_result['handled']) {
497 if ($custom_result['success']) {
498 wp_send_json_success($custom_result);
499 } else {
500 wp_send_json_error(['message' => $custom_result['message'] ?? __('Payment failed.', 'easy-invoice')]);
501 }
502 return;
503 }
504
505 if (!$invoice_id || !$payment_method_slug) {
506 wp_send_json_error(['message' => __('Missing required fields.', 'easy-invoice')]);
507 return;
508 }
509
510 $invoice_post = get_post($invoice_id);
511 if (!$invoice_post || $invoice_post->post_type !== \EasyInvoice\Constants\PostTypes::EASY_INVOICE_POST_TYPE) {
512 wp_send_json_error(['message' => __('Invalid invoice.', 'easy-invoice')]);
513 return;
514 }
515
516 $invoice = new \EasyInvoice\Models\Invoice($invoice_post);
517 $amount = $invoice->total ?? 0;
518
519 // Log the payment processing details
520
521 $gateway_instance = $this->gatewayManager->getGateway($payment_method_slug);
522
523 if (!$gateway_instance || !$gateway_instance->isEnabled() || !$gateway_instance->isAvailable()) {
524 wp_send_json_error(['message' => __('Selected payment gateway is not available or configured correctly.', 'easy-invoice')]);
525 return;
526 }
527
528 try {
529 // Pass the entire $_POST array to the gateway
530 $result = $gateway_instance->processPayment($amount, $_POST);
531
532 if (isset($result['success']) && $result['success']) {
533 wp_send_json_success($result);
534 } else {
535 wp_send_json_error(['message' => $result['message'] ?? __('Payment processing failed with the gateway.', 'easy-invoice')]);
536 }
537
538 } catch (\Exception $e) {
539 error_log('Easy Invoice Payment Error: ' . $e->getMessage() . ' in ' . $e->getFile() . ' on line ' . $e->getLine());
540 wp_send_json_error(['message' => __('An unexpected error occurred during payment processing. Please check plugin logs or contact support.', 'easy-invoice')]);
541 }
542 }
543
544 /**
545 * Handle payment callback/webhook
546 */
547 public function handleCallback(): void {
548 check_ajax_referer('easy_invoice_payment', 'payment_nonce');
549
550 $invoice_id = isset($_POST['invoice_id']) ? intval($_POST['invoice_id']) : 0;
551 $gateway = isset($_POST['gateway']) ? sanitize_text_field($_POST['gateway']) : '';
552
553 if (!$invoice_id || !$gateway) {
554 wp_send_json_error(['message' => __('Invalid request', 'easy-invoice')]);
555 }
556
557 $gateway_instance = $this->gatewayManager->getGateway($gateway);
558 if (!$gateway_instance) {
559 wp_send_json_error(['message' => __('Invalid payment gateway', 'easy-invoice')]);
560 }
561
562 $result = $gateway_instance->handleCallback($_POST);
563
564 // Send admin notification for manual payments
565 if ($result['success'] && in_array($gateway, ['bank', 'cheque'])) {
566 do_action('easy_invoice_manual_payment_submitted', $invoice_id, $gateway);
567 }
568
569 if ($result['success']) {
570 wp_send_json_success($result);
571 } else {
572 wp_send_json_error($result);
573 }
574 }
575
576 /**
577 * Get available payment gateways for an invoice
578 *
579 * @param int $invoice_id
580 * @return array
581 */
582 public function getAvailableGateways(int $invoice_id): array {
583 $post = get_post($invoice_id);
584 if (!$post || $post->post_type !== \EasyInvoice\Constants\PostTypes::EASY_INVOICE_POST_TYPE) {
585 return [];
586 }
587
588 $invoice = new \EasyInvoice\Models\Invoice($post);
589 $invoice_status = $invoice->getStatus();
590
591 if (!in_array($invoice_status, [ 'unpaid', 'available'])) {
592 return [];
593 }
594
595 $enabled_gateways = $this->gatewayManager->getEnabledGateways();
596
597 if (empty($enabled_gateways)) {
598 return [];
599 }
600
601 // Get invoice-specific gateways (comma-separated string or empty)
602 $invoice_gateways = $invoice->getPaymentGateways();
603 $selected_gateways = [];
604
605 // Handle both string and array formats
606 if (!empty($invoice_gateways)) {
607 if (is_string($invoice_gateways)) {
608 // If it's a string, split by comma
609 $selected_gateways = array_filter(array_map('trim', explode(',', $invoice_gateways)));
610 } elseif (is_array($invoice_gateways)) {
611 // If it's already an array, use it directly
612 $selected_gateways = array_filter($invoice_gateways);
613 }
614 }
615
616 $available_gateways = [];
617 $gateway_manager = \EasyInvoice\EasyInvoice::getInstance()->getGatewayManager();
618
619 // $enabled_gateways is an associative array with gateway_id as key and gateway object as value
620 foreach ($enabled_gateways as $gateway_id => $gateway) {
621 // If invoice has custom gateways selected, only show those
622 // If no custom gateways are selected (empty array), show all enabled gateways
623 if (!empty($selected_gateways) && !in_array($gateway_id, $selected_gateways, true)) {
624 continue;
625 }
626
627 $is_available = $gateway->isAvailable();
628
629 if ($is_available) {
630 $available_gateways[] = [
631 'id' => $gateway_id,
632 'title' => $gateway_manager->getGatewayDisplayName($gateway_id),
633 'icon' => $gateway->getIcon(),
634 'description' => $gateway->getDescription()
635 ];
636 }
637 }
638
639 return $available_gateways;
640 }
641
642 /**
643 * Update payment via AJAX
644 */
645 public function updatePayment() {
646 check_ajax_referer('easy_invoice_payment', 'payment_nonce');
647
648 $payment_id = isset($_POST['payment_id']) ? intval($_POST['payment_id']) : 0;
649 $invoice_id = isset($_POST['invoice_id']) ? intval($_POST['invoice_id']) : 0;
650 $amount = isset($_POST['amount']) ? floatval($_POST['amount']) : 0;
651 $payment_method = isset($_POST['payment_method']) ? sanitize_text_field($_POST['payment_method']) : '';
652 $payment_date = isset($_POST['payment_date']) ? sanitize_text_field($_POST['payment_date']) : date('Y-m-d');
653 $status = isset($_POST['status']) ? sanitize_text_field($_POST['status']) : 'pending';
654 $notes = isset($_POST['notes']) ? sanitize_textarea_field($_POST['notes']) : '';
655
656 if (!$payment_id || !$invoice_id || !$amount || !$payment_method) {
657 wp_send_json_error(['message' => __('Missing required fields', 'easy-invoice')]);
658 return;
659 }
660
661 try {
662 // Check if payment post exists before instantiating
663 $payment_post = get_post($payment_id);
664 if (!$payment_post || $payment_post->post_type !== 'easy_invoice_payment') {
665 wp_send_json_error(['message' => __('Invalid payment', 'easy-invoice')]);
666 return;
667 }
668
669 $payment = new Payment($payment_post);
670
671 // Get the old payment status before updating
672 $old_status = $payment->getStatus();
673
674 $post = get_post($invoice_id);
675 if (!$post || $post->post_type !== \EasyInvoice\Constants\PostTypes::EASY_INVOICE_POST_TYPE) {
676 wp_send_json_error(['message' => __('Invalid invoice', 'easy-invoice')]);
677 return;
678 }
679 $invoice = new Invoice($post);
680
681 $payment_data = [
682 'invoice_id' => $invoice_id,
683 'amount' => $amount,
684 'payment_method' => $payment_method,
685 'payment_date' => $payment_date,
686 'status' => $status,
687 'notes' => $notes,
688 'gateway_response' => [
689 'method' => $payment_method,
690 'date' => $payment_date,
691 'notes' => $notes
692 ]
693 ];
694
695 $result = $payment->update($payment_data);
696
697 if ($result) {
698 // Update invoice status based on payment status change
699 if ($status === 'completed' && $old_status !== 'completed') {
700 // Payment changed TO completed - check if invoice should be marked as paid
701 $invoice->setMeta('_payment_method', $payment_method);
702 $this->updateInvoiceStatusIfPaid($invoice_id, $invoice, 'manual');
703 } elseif ($status !== 'completed' && $old_status === 'completed') {
704 // Payment changed FROM completed to another status (failed, pending, etc.)
705 // Recalculate total payments and update invoice status accordingly
706 $total_payments = $this->calculateTotalPaymentsForInvoice($invoice_id);
707 $invoice_total = $invoice->getTotal();
708
709 if ($total_payments < $invoice_total) {
710 // Not enough payments anymore, revert invoice to draft/pending
711 $invoice->setStatus('draft');
712 $invoice->save();
713
714 error_log("Easy Invoice: Invoice #$invoice_id status reverted to 'draft' - payment marked as $status");
715 } else {
716 // Still enough payments from other completed payments
717 $this->updateInvoiceStatusIfPaid($invoice_id, $invoice, 'manual');
718 }
719 }
720
721 wp_send_json_success([
722 'message' => __('Payment updated successfully', 'easy-invoice')
723 ]);
724 } else {
725 wp_send_json_error(['message' => __('Failed to update payment', 'easy-invoice')]);
726 }
727 } catch (\Exception $e) {
728 wp_send_json_error(['message' => $e->getMessage()]);
729 }
730 }
731
732 /**
733 * Verify manual payment
734 */
735 public function verifyManualPayment(): void {
736 // Check permissions
737 if (!easy_invoice_user_can('ei_record_payment')) {
738 wp_send_json_error(['message' => __('You do not have permission to perform this action', 'easy-invoice')]);
739 return;
740 }
741
742 // Verify nonce
743 check_ajax_referer('easy_invoice_admin', 'nonce');
744
745 $invoice_id = isset($_POST['invoice_id']) ? intval($_POST['invoice_id']) : 0;
746 $amount = isset($_POST['amount']) ? floatval($_POST['amount']) : 0;
747 $payment_method = isset($_POST['payment_method']) ? sanitize_text_field($_POST['payment_method']) : '';
748 $notes = isset($_POST['notes']) ? sanitize_textarea_field($_POST['notes']) : '';
749 $transaction_id = isset($_POST['transaction_id']) ? sanitize_text_field($_POST['transaction_id']) : '';
750
751 if (!$invoice_id || !$amount || !$payment_method) {
752 wp_send_json_error(['message' => __('Missing required fields', 'easy-invoice')]);
753 return;
754 }
755
756 // Get the invoice
757 $post = get_post($invoice_id);
758 if (!$post || $post->post_type !== \EasyInvoice\Constants\PostTypes::EASY_INVOICE_POST_TYPE) {
759 wp_send_json_error(['message' => __('Invalid invoice', 'easy-invoice')]);
760 return;
761 }
762
763 $invoice = new Invoice($post);
764
765 // Get currency settings
766 $settings_controller = new \EasyInvoice\Controllers\SettingsController();
767 $settings = $settings_controller->getSettings();
768 $currency_code = $settings['easy_invoice_currency_code'] ?? 'USD';
769 $currency_symbol = \EasyInvoice\Helpers\CurrencyHelper::getCurrencySymbol($currency_code);
770
771 $payment_data = [
772 'invoice_id' => $invoice_id,
773 'amount' => $amount,
774 'payment_method' => $payment_method,
775 'payment_date' => current_time('mysql'),
776 'notes' => $notes,
777 'status' => 'completed',
778 'payment_type' => 'full',
779 'transaction_id' => $transaction_id,
780 'recurring_id' => '',
781 'parent_payment_id' => '',
782 'currency' => $currency_code,
783 'currency_symbol' => $currency_symbol,
784 'gateway_response' => [
785 'admin_verified' => true,
786 'verification_date' => current_time('mysql'),
787 'verification_user' => get_current_user_id()
788 ]
789 ];
790
791 try {
792 $payment = Payment::create($payment_data);
793
794 // Store payment details before updating status (for the hook)
795 $invoice->setMeta('_payment_method', $payment_method);
796 if ($transaction_id) {
797 $invoice->setMeta('_transaction_id', $transaction_id);
798 }
799
800 // Update invoice status to paid only if total payments are sufficient
801 // This will trigger 'easy_invoice_payment_completed' hook which sends admin notification
802 $this->updateInvoiceStatusIfPaid($invoice_id, $invoice, 'manual');
803
804 // Send confirmation email to customer
805 $this->sendPaymentConfirmationEmail($invoice_id, $payment->getId());
806
807 wp_send_json_success([
808 'message' => __('Payment verified successfully', 'easy-invoice'),
809 'payment_id' => $payment->getId()
810 ]);
811 } catch (\Exception $e) {
812 wp_send_json_error(['message' => $e->getMessage()]);
813 }
814 }
815
816 /**
817 * Reject manual payment
818 */
819 public function rejectManualPayment(): void {
820 // Check permissions — rejecting a manual payment is a record-payment
821 // operation (it transitions state, doesn't refund money).
822 if (!easy_invoice_user_can('ei_record_payment')) {
823 wp_send_json_error(['message' => __('You do not have permission to perform this action', 'easy-invoice')]);
824 return;
825 }
826
827 // Verify nonce
828 check_ajax_referer('easy_invoice_admin', 'nonce');
829
830 $invoice_id = isset($_POST['invoice_id']) ? intval($_POST['invoice_id']) : 0;
831 $reason = isset($_POST['reason']) ? sanitize_textarea_field($_POST['reason']) : '';
832
833 if (!$invoice_id) {
834 wp_send_json_error(['message' => __('Invoice ID is required', 'easy-invoice')]);
835 return;
836 }
837
838 // Get the invoice
839 $post = get_post($invoice_id);
840 if (!$post || $post->post_type !== \EasyInvoice\Constants\PostTypes::EASY_INVOICE_POST_TYPE) {
841 wp_send_json_error(['message' => __('Invalid invoice', 'easy-invoice')]);
842 return;
843 }
844
845 $invoice = new Invoice($post);
846
847 // Update invoice status
848 update_post_meta($invoice_id, '_payment_status', 'rejected');
849
850 // Add rejection reason
851 update_post_meta($invoice_id, '_payment_rejection_reason', $reason);
852 update_post_meta($invoice_id, '_payment_rejection_date', current_time('mysql'));
853 update_post_meta($invoice_id, '_payment_rejection_user', get_current_user_id());
854
855 // Send rejection email to customer
856 $this->sendPaymentRejectionEmail($invoice_id, $reason);
857
858 wp_send_json_success([
859 'message' => __('Payment rejected successfully', 'easy-invoice')
860 ]);
861 }
862
863
864
865 /**
866 * Send payment confirmation email to customer
867 *
868 * @param int $invoice_id
869 * @param int $payment_id
870 */
871 private function sendPaymentConfirmationEmail($invoice_id, $payment_id): void {
872 $invoice = new Invoice(get_post($invoice_id));
873
874 if (!$invoice || !$invoice->getId()) {
875 return;
876 }
877
878 // Use EmailManager to send payment confirmation using proper template system
879 // This will check if payment email is enabled in settings
880 $email_manager = \EasyInvoice\Services\EmailManager::getInstance();
881 $email_manager->sendInvoiceEmail($invoice, 'paid', [
882 'payment_id' => $payment_id,
883 'skip_bcc' => true // Skip BCC to admin since this is a direct call
884 ]);
885 }
886
887 /**
888 * Send payment rejection email to customer
889 *
890 * @param int $invoice_id
891 * @param string $reason
892 */
893 private function sendPaymentRejectionEmail($invoice_id, $reason): void {
894 $invoice = new Invoice(get_post($invoice_id));
895
896 if (!$invoice || !$invoice->getId()) {
897 return;
898 }
899
900 // Use EmailManager to send payment rejection
901 $email_manager = \EasyInvoice\Services\EmailManager::getInstance();
902 $email_manager->sendPaymentRejectionEmail($invoice, $reason);
903 }
904
905 /**
906 * Add pending payment statuses to admin filters
907 *
908 * @param array $statuses
909 * @return array
910 */
911 public function addPendingPaymentStatuses($statuses): array {
912 $statuses['pending-bank'] = __('Pending Bank Transfer', 'easy-invoice');
913 $statuses['pending-cheque'] = __('Pending Cheque', 'easy-invoice');
914 return $statuses;
915 }
916
917 /**
918 * Add payment method column to payments list
919 *
920 * @param array $columns
921 * @return array
922 */
923 public function addPaymentMethodColumn($columns): array {
924 $new_columns = [];
925
926 foreach ($columns as $key => $value) {
927 $new_columns[$key] = $value;
928
929 if ($key === 'title') {
930 $new_columns['payment_method'] = __('Payment Method', 'easy-invoice');
931 }
932 }
933
934 return $new_columns;
935 }
936
937 /**
938 * Render payment method column
939 *
940 * @param string $column
941 * @param int $post_id
942 */
943 public function renderPaymentMethodColumn($column, $post_id): void {
944 if ($column === 'payment_method') {
945 $payment_method = get_post_meta($post_id, '_payment_method', true);
946 $payment_methods = [
947 'paypal' => __('PayPal', 'easy-invoice')
948 ];
949
950 echo isset($payment_methods[$payment_method]) ? esc_html($payment_methods[$payment_method]) : esc_html($payment_method);
951 }
952 }
953
954 /**
955 * Send payment reminders for pending manual payments
956 */
957 public function sendPaymentReminders(): void {
958 // Get invoices with pending manual payments
959 $pending_invoices = get_posts([
960 'post_type' => \EasyInvoice\Constants\PostTypes::EASY_INVOICE_POST_TYPE,
961 'posts_per_page' => -1,
962 'meta_query' => [
963 'relation' => 'AND',
964 [
965 'key' => '_payment_status',
966 'value' => ['pending-bank', 'pending-cheque'],
967 'compare' => 'IN'
968 ],
969 [
970 'key' => '_payment_reminder_sent',
971 'compare' => 'NOT EXISTS'
972 ]
973 ]
974 ]);
975
976 if (!empty($pending_invoices)) {
977 // Get currency settings
978 $settings_controller = new \EasyInvoice\Controllers\SettingsController();
979 $settings = $settings_controller->getSettings();
980 $currency_code = $settings['easy_invoice_currency_code'] ?? 'USD';
981 $currency_symbol = \EasyInvoice\Helpers\CurrencyHelper::getCurrencySymbol($currency_code);
982
983 foreach ($pending_invoices as $post) {
984 $invoice = new Invoice($post);
985
986 if (!$invoice || !$invoice->getId()) {
987 continue;
988 }
989
990 // Use EmailManager to send payment reminder
991 $email_manager = \EasyInvoice\Services\EmailManager::getInstance();
992 $result = $email_manager->sendInvoiceEmail($invoice, 'reminder', [
993 'payment_method' => get_post_meta($invoice->getId(), '_payment_method', true)
994 ]);
995
996 // Mark reminder as sent if email was sent successfully
997 if ($result['success']) {
998 update_post_meta($invoice->getId(), '_payment_reminder_sent', current_time('mysql'));
999 }
1000 }
1001
1002 wp_reset_postdata();
1003 }
1004 }
1005
1006 /**
1007 * Submit manual payment
1008 */
1009 public function submitManualPayment(): void {
1010 // CSRF defense — keep the existing nonce check. The nonce is
1011 // global (`easy_invoice_payment`) so any public invoice page leaks
1012 // a valid value; the REAL authorisation gate is the ownership
1013 // check below.
1014 if (!wp_verify_nonce($_POST['nonce'] ?? '', 'easy_invoice_payment')) {
1015 wp_send_json_error(['message' => __('Security check failed', 'easy-invoice')]);
1016 return;
1017 }
1018
1019 $invoice_id = isset($_POST['invoice_id']) ? intval($_POST['invoice_id']) : 0;
1020 $payment_type = isset($_POST['payment_type']) ? sanitize_text_field($_POST['payment_type']) : '';
1021 $payment_notes = isset($_POST['payment_notes']) ? sanitize_textarea_field($_POST['payment_notes']) : '';
1022
1023 if (!$invoice_id || !$payment_type) {
1024 wp_send_json_error(['message' => __('Missing required fields', 'easy-invoice')]);
1025 return;
1026 }
1027
1028 // Get invoice
1029 $invoice_post = get_post($invoice_id);
1030 if (!$invoice_post || $invoice_post->post_type !== \EasyInvoice\Constants\PostTypes::EASY_INVOICE_POST_TYPE) {
1031 wp_send_json_error(['message' => __('Invalid invoice', 'easy-invoice')]);
1032 return;
1033 }
1034
1035 $invoice = new \EasyInvoice\Models\Invoice($invoice_post);
1036
1037 // Authorisation: reject unless the caller is the legitimate email
1038 // recipient (per-invoice access token), an admin, or the
1039 // logged-in client bound to this invoice. Without this gate the
1040 // public AJAX endpoint allowed any visitor with a harvested
1041 // global nonce to flood arbitrary invoices into
1042 // `pending_verification` and attach payment-proof uploads.
1043 if (!\EasyInvoice\Controllers\InvoiceController::canSubmitPaymentForInvoice($invoice_id, $invoice)) {
1044 wp_send_json_error([
1045 'message' => __('You do not have permission to submit a payment for this invoice.', 'easy-invoice'),
1046 ]);
1047 return;
1048 }
1049 $currency_code = $invoice->getCurrencyCode() ?: 'USD';
1050 if ($currency_code === 'global') {
1051 $currency_code = get_option('easy_invoice_currency_code', 'USD');
1052 }
1053 $currency_symbol = \EasyInvoice\Helpers\CurrencyHelper::getCurrencySymbol($currency_code);
1054
1055 // Handle file upload (never trust client MIME or filename extension — use WordPress filetype APIs)
1056 $proof_url = '';
1057 if (isset($_FILES['payment_proof']) && $_FILES['payment_proof']['error'] === UPLOAD_ERR_OK) {
1058 $file = $_FILES['payment_proof'];
1059
1060 if (empty($file['tmp_name']) || !is_uploaded_file($file['tmp_name'])) {
1061 wp_send_json_error(['message' => __('Invalid upload.', 'easy-invoice')]);
1062 return;
1063 }
1064
1065 $max_size = 5 * 1024 * 1024; // 5MB
1066 if ($file['size'] > $max_size) {
1067 wp_send_json_error(['message' => __('File size must be less than 5MB.', 'easy-invoice')]);
1068 return;
1069 }
1070
1071 $allowed_mimes = [
1072 'jpg|jpeg|jpe' => 'image/jpeg',
1073 'png' => 'image/png',
1074 'gif' => 'image/gif',
1075 'pdf' => 'application/pdf',
1076 ];
1077
1078 $checked = wp_check_filetype_and_ext($file['tmp_name'], $file['name'], $allowed_mimes);
1079 if (empty($checked['ext']) || empty($checked['type'])) {
1080 wp_send_json_error(['message' => __('Invalid file type. Only JPG, PNG, GIF, and PDF files are allowed.', 'easy-invoice')]);
1081 return;
1082 }
1083
1084 $allowed_types = array_values($allowed_mimes);
1085 if (!in_array($checked['type'], $allowed_types, true)) {
1086 wp_send_json_error(['message' => __('Invalid file type. Only JPG, PNG, GIF, and PDF files are allowed.', 'easy-invoice')]);
1087 return;
1088 }
1089
1090 $upload_dir = wp_upload_dir();
1091 $proof_dir = $upload_dir['basedir'] . '/easy-invoice/payment-proofs/';
1092
1093 if (!wp_mkdir_p($proof_dir)) {
1094 wp_send_json_error(['message' => __('Could not create upload directory.', 'easy-invoice')]);
1095 return;
1096 }
1097
1098 $filename = uniqid('payment_proof_', true) . '.' . $checked['ext'];
1099 $filepath = $proof_dir . $filename;
1100
1101 if (!move_uploaded_file($file['tmp_name'], $filepath)) {
1102 wp_send_json_error(['message' => __('Failed to save payment proof file.', 'easy-invoice')]);
1103 return;
1104 }
1105
1106 chmod($filepath, 0644);
1107 $proof_url = $upload_dir['baseurl'] . '/easy-invoice/payment-proofs/' . $filename;
1108 }
1109
1110 // Create payment record
1111 $payment_data = [
1112 'post_title' => sprintf('Manual Payment (%s) for Invoice #%s', ucfirst($payment_type), $invoice->getNumber()),
1113 'post_type' => 'easy_invoice_payment',
1114 'post_status' => 'publish',
1115 'post_author' => get_current_user_id(),
1116 ];
1117
1118 $payment_id = wp_insert_post($payment_data);
1119
1120 if (is_wp_error($payment_id)) {
1121 wp_send_json_error(['message' => __('Failed to create payment record', 'easy-invoice')]);
1122 return;
1123 }
1124
1125 // Save payment metadata
1126 update_post_meta($payment_id, '_invoice_id', $invoice_id);
1127 update_post_meta($payment_id, '_amount', $invoice->getTotal());
1128 update_post_meta($payment_id, '_payment_method', 'manual');
1129 update_post_meta($payment_id, '_payment_type', $payment_type);
1130 update_post_meta($payment_id, '_status', 'pending');
1131 update_post_meta($payment_id, '_transaction_id', 'MANUAL-' . $invoice_id . '-' . time());
1132 update_post_meta($payment_id, '_payment_date', current_time('mysql'));
1133 update_post_meta($payment_id, '_notes', $payment_notes);
1134 update_post_meta($payment_id, '_currency', $currency_code);
1135 update_post_meta($payment_id, '_currency_symbol', $currency_symbol);
1136 update_post_meta($payment_id, '_payment_proof', $proof_url);
1137
1138 // Update invoice status to pending verification
1139 $invoice->setStatus('pending_verification');
1140 $invoice->save();
1141
1142 // Store payment details on invoice
1143 $invoice->setMeta('_payment_method', 'manual');
1144 $invoice->setMeta('_payment_type', $payment_type);
1145 $invoice->setMeta('_payment_status', 'pending');
1146 $invoice->setMeta('_manual_payment_id', $payment_id);
1147 $invoice->setMeta('_manual_payment_proof', $proof_url);
1148 $invoice->setMeta('_manual_payment_notes', $payment_notes);
1149
1150 // Send admin notification
1151 do_action('easy_invoice_manual_payment_submitted', $invoice_id, $payment_type);
1152
1153 wp_send_json_success([
1154 'message' => __('Payment submitted successfully! Your payment will be verified by the administrator.', 'easy-invoice'),
1155 'payment_id' => $payment_id
1156 ]);
1157 }
1158
1159 /**
1160 * Handle submission of payment proof for manual gateways (Bank Transfer, Cheque)
1161 */
1162 public function submitPaymentProof(): void {
1163 $gateway_name = isset($_POST['gateway']) ? sanitize_text_field($_POST['gateway']) : '';
1164 $invoice_id = isset($_POST['invoice_id']) ? intval($_POST['invoice_id']) : 0;
1165
1166 if (empty($gateway_name) || empty($invoice_id)) {
1167 wp_send_json_error(['message' => __('Invalid request. Missing gateway or invoice ID.', 'easy-invoice')]);
1168 return;
1169 }
1170
1171 // Nonce verification (make nonce name consistent or check based on gateway)
1172 $nonce_action = 'easy_invoice_payment_proof_' . $invoice_id; // Bank transfer nonce
1173 $nonce_value = isset($_POST['payment_proof_nonce']) ? sanitize_text_field($_POST['payment_proof_nonce']) : '';
1174 if ($gateway_name === 'cheque') {
1175 $nonce_action = 'easy_invoice_cheque_notification_' . $invoice_id; // Cheque nonce
1176 $nonce_value = isset($_POST['cheque_notification_nonce']) ? sanitize_text_field($_POST['cheque_notification_nonce']) : '';
1177 }
1178
1179 if (!wp_verify_nonce($nonce_value, $nonce_action)) {
1180 wp_send_json_error(['message' => __('Nonce verification failed. Please try again.', 'easy-invoice')]);
1181 return;
1182 }
1183
1184 // Optional: Add capability check if this can be submitted by logged-in users only from frontend
1185 // if (is_user_logged_in() && !current_user_can('read_invoice', $invoice_id)) { // Example capability
1186 // wp_send_json_error(['message' => __('You do not have permission to submit proof for this invoice.', 'easy-invoice')]);
1187 // return;
1188 // }
1189
1190 $gateway = $this->gatewayManager->getGateway($gateway_name);
1191
1192 if (!$gateway || !method_exists($gateway, 'handleProofSubmission')) {
1193 wp_send_json_error(['message' => __('Invalid payment gateway or submission handler not found.', 'easy-invoice')]);
1194 return;
1195 }
1196
1197 // Prepare data for the gateway handler
1198 $post_data = stripslashes_deep($_POST);
1199 $files_data = $_FILES;
1200
1201 $result = $gateway->handleProofSubmission($post_data, $files_data);
1202
1203 if ($result['success']) {
1204 wp_send_json_success(['message' => $result['message']]);
1205 } else {
1206 wp_send_json_error(['message' => $result['message']]);
1207 }
1208 }
1209
1210 /**
1211 * AJAX handler for admin to mark an invoice as paid.
1212 */
1213 public function mark_invoice_paid_ajax(): void {
1214 $invoice_id = isset($_POST['invoice_id']) ? intval($_POST['invoice_id']) : 0;
1215 $nonce = isset($_POST['nonce']) ? sanitize_text_field($_POST['nonce']) : '';
1216 $notes = isset($_POST['notes']) ? sanitize_textarea_field($_POST['notes']) : '';
1217
1218 if (empty($invoice_id) || !wp_verify_nonce($nonce, 'easy_invoice_approve_payment')) {
1219 easy_invoice_toast_error(__('Invalid request or security check failed.', 'easy-invoice'));
1220 return;
1221 }
1222
1223 // Mark-as-paid is a record-payment action — gated by the matching cap.
1224 if (!easy_invoice_user_can('ei_record_payment')) {
1225 easy_invoice_toast_error(__('You do not have permission to perform this action.', 'easy-invoice'));
1226 return;
1227 }
1228
1229 $invoice_post = get_post($invoice_id);
1230 if (!$invoice_post || $invoice_post->post_type !== \EasyInvoice\Constants\PostTypes::EASY_INVOICE_POST_TYPE) {
1231 wp_send_json_error(['message' => __('Invalid invoice.', 'easy-invoice')]);
1232 return;
1233 }
1234
1235 $invoice = new Invoice($invoice_post);
1236 // For manual approval, always use 'manual' as payment method
1237 $payment_method = 'manual';
1238
1239 // Update invoice post status to 'publish' (or your primary paid status)
1240 wp_update_post(['ID' => $invoice_id, 'post_status' => 'publish']);
1241 update_post_meta($invoice_id, '_payment_status', 'completed'); // General completed status for payments
1242
1243 // Allow plugins to control invoice status update
1244 $should_update_invoice_status = apply_filters('easy_invoice_should_update_invoice_status', true, $invoice_id);
1245 if ($should_update_invoice_status) {
1246 update_post_meta($invoice_id, InvoiceFields::STATUS, 'paid'); // Specific invoice status field if used by model
1247 }
1248
1249 // Use submitted notes or default note
1250 $payment_notes = !empty($notes)
1251 ? $notes
1252 : __('Payment manually verified by admin.', 'easy-invoice');
1253
1254 // Find existing pending payment records for this invoice
1255 $existing_payment_args = [
1256 'post_type' => 'easy_invoice_payment',
1257 'posts_per_page' => 1,
1258 'meta_query' => [
1259 'relation' => 'AND',
1260 [
1261 'key' => '_invoice_id',
1262 'value' => $invoice_id,
1263 ],
1264 [
1265 'key' => '_status',
1266 'value' => ['pending-bank', 'pending-cheque', 'pending'], // Check against pending statuses
1267 'compare' => 'IN'
1268 ]
1269 ]
1270 ];
1271 $existing_payments = get_posts($existing_payment_args);
1272 $payment_id = null;
1273
1274 if (!empty($existing_payments)) {
1275 // Update existing pending payment instead of creating new one
1276 $payment_id = $existing_payments[0]->ID;
1277 update_post_meta($payment_id, '_status', 'completed'); // Update status to completed
1278 update_post_meta($payment_id, '_payment_method', 'manual'); // Set payment method to manual
1279 update_post_meta($payment_id, '_transaction_id', 'MANUAL-' . $invoice_id . '-' . time());
1280 update_post_meta($payment_id, '_payment_date', current_time('mysql'));
1281 update_post_meta($payment_id, '_notes', $payment_notes); // Update notes on existing payment
1282 } else {
1283 // Only create a new payment if no pending payments exist
1284 // This prevents creating duplicate payment records
1285 $existing_payments = get_posts([
1286 'post_type' => 'easy_invoice_payment',
1287 'posts_per_page' => -1,
1288 'meta_query' => [
1289 [
1290 'key' => '_invoice_id',
1291 'value' => $invoice_id,
1292 ]
1293 ]
1294 ]);
1295
1296 if (!empty($existing_payments)) {
1297 // If payments exist but none are pending, don't create a new one
1298 // Just update the invoice status
1299 easy_invoice_toast_success(__('Invoice marked as paid successfully.', 'easy-invoice'));
1300 return;
1301 }
1302
1303 // Get currency from invoice
1304 $currency_code = get_post_meta($invoice_id, '_easy_invoice_currency_code', true);
1305 if (empty($currency_code) || $currency_code === 'global') {
1306 $currency_code = get_option('easy_invoice_currency_code', 'USD');
1307 }
1308 $currency_symbol = \EasyInvoice\Helpers\CurrencyHelper::getCurrencySymbol($currency_code);
1309
1310 $payment_data = [
1311 'invoice_id' => $invoice_id,
1312 'amount' => $invoice->getTotal(), // Or get amount from proof submission if it varies
1313 'payment_method' => $payment_method,
1314 'status' => 'completed',
1315 'transaction_id' => get_post_meta($invoice_id, '_' . $payment_method . '_transaction_id', true) ?: 'MANUAL-' . $invoice_id,
1316 'payment_date' => current_time('mysql'),
1317 'notes' => $payment_notes, // Use provided notes
1318 'payment_type' => 'manual',
1319 'currency' => $currency_code,
1320 'currency_symbol' => $currency_symbol,
1321 'gateway_response' => json_encode([
1322 'admin_verified' => true,
1323 'user' => get_current_user_id(),
1324 'verification_date' => current_time('mysql'),
1325 'notes' => $payment_notes // Store notes in response JSON as well
1326 ])
1327 ];
1328 try {
1329 // Create payment record using WordPress post creation
1330 $payment_post_data = [
1331 'post_title' => sprintf('Manual Payment for Invoice #%s', $invoice->getNumber()),
1332 'post_type' => 'easy_invoice_payment',
1333 'post_status' => 'publish',
1334 'post_author' => get_current_user_id(),
1335 'meta_input' => [
1336 '_invoice_id' => $invoice_id,
1337 '_amount' => $invoice->getTotal(),
1338 '_payment_method' => $payment_method,
1339 '_status' => 'completed',
1340 '_transaction_id' => get_post_meta($invoice_id, '_' . $payment_method . '_transaction_id', true) ?: 'MANUAL-' . $invoice_id,
1341 '_payment_date' => current_time('mysql'),
1342 '_notes' => $payment_notes,
1343 '_payment_type' => 'manual',
1344 '_currency' => $currency_code,
1345 '_currency_symbol' => $currency_symbol,
1346 '_gateway_response' => json_encode([
1347 'admin_verified' => true,
1348 'user' => get_current_user_id(),
1349 'verification_date' => current_time('mysql'),
1350 'notes' => $payment_notes
1351 ])
1352 ]
1353 ];
1354
1355 $payment_id = wp_insert_post($payment_post_data);
1356 if (is_wp_error($payment_id)) {
1357 easy_invoice_toast_error(__('Error creating payment record:', 'easy-invoice') . ' ' . $payment_id->get_error_message());
1358 return;
1359 }
1360 } catch (\Exception $e) {
1361 easy_invoice_toast_error(__('Error creating payment record:', 'easy-invoice') . ' ' . $e->getMessage());
1362 return;
1363 }
1364 }
1365
1366 // Store payment details before updating status (for the hook)
1367 $transaction_id = get_post_meta($invoice_id, '_' . $payment_method . '_transaction_id', true) ?: 'MANUAL-' . $invoice_id;
1368 $invoice->setMeta('_payment_method', $payment_method);
1369 $invoice->setMeta('_transaction_id', $transaction_id);
1370
1371 // Update invoice status to paid
1372 // This will trigger 'easy_invoice_payment_completed' hook which sends admin notification
1373 $invoice->setStatus('paid');
1374 $invoice->save();
1375
1376 // Trigger the payment completed hook manually since we're updating status directly
1377 do_action('easy_invoice_payment_completed', $invoice_id, $invoice, [
1378 'payment_method' => $payment_method,
1379 'gateway_name' => 'manual',
1380 'transaction_id' => $transaction_id,
1381 'amount' => $invoice->getTotal()
1382 ]);
1383
1384 // Trigger email confirmation and actions only if we have a payment_id
1385 if ($payment_id) {
1386 // Send confirmation email to customer
1387 $this->sendPaymentConfirmationEmail($invoice_id, $payment_id);
1388 do_action('easy_invoice_manual_payment_confirmed', $invoice_id, $payment_id, $payment_method);
1389 }
1390
1391 easy_invoice_toast_success(__('Invoice marked as paid successfully.', 'easy-invoice'));
1392 }
1393
1394 /**
1395 * Handle bulk actions for payments
1396 */
1397 public function handleBulkActions() {
1398 // Check if we're processing a bulk action
1399 if (!isset($_POST['action']) || $_POST['action'] !== 'easy_invoice_payment_bulk_action') {
1400 return;
1401 }
1402
1403 // Check nonce and capability
1404 if (!wp_verify_nonce($_POST['easy_invoice_payment_bulk_nonce'], 'easy_invoice_payment_bulk_action')) {
1405 wp_die(__('Security check failed.', 'easy-invoice'));
1406 }
1407
1408 // Bulk action on payments — record-payment cap is the right gate
1409 // (covers trash/restore/delete which all change payment state).
1410 if (!easy_invoice_user_can('ei_record_payment')) {
1411 wp_die(__('You do not have permission to perform this action.', 'easy-invoice'));
1412 }
1413
1414 // Check if we have payment IDs
1415 if (!isset($_POST['payment_ids']) || !is_array($_POST['payment_ids']) || empty($_POST['payment_ids'])) {
1416 wp_redirect(admin_url('admin.php?page=easy-invoice-payments&bulk_error=no_selection'));
1417 exit;
1418 }
1419
1420 // Get bulk action and payment IDs
1421 $bulk_action = isset($_POST['bulk_action']) ? sanitize_text_field($_POST['bulk_action']) : '';
1422 $payment_ids = array_map('intval', $_POST['payment_ids']);
1423
1424 // Process based on action
1425 $processed = 0;
1426 $invoice_updates = array(); // Track invoice updates needed
1427
1428 switch ($bulk_action) {
1429 case 'trash':
1430 foreach ($payment_ids as $id) {
1431 // Get payment info before trashing for invoice status update
1432 $payment_post = get_post($id);
1433 if (!$payment_post || $payment_post->post_type !== 'easy_invoice_payment') {
1434 continue;
1435 }
1436 $payment = new Payment($payment_post);
1437 $payment_status = $payment->getStatus();
1438 $invoice_id = $payment->getInvoiceId();
1439 $payment_amount = $payment->getAmount();
1440
1441 if (wp_trash_post($id)) {
1442 $processed++;
1443
1444 // Track invoice updates needed for completed payments
1445 if ($payment_status === 'completed' && $invoice_id) {
1446 if (!isset($invoice_updates[$invoice_id])) {
1447 $invoice_updates[$invoice_id] = 0;
1448 }
1449 $invoice_updates[$invoice_id] += $payment_amount;
1450 }
1451 }
1452 }
1453
1454 // Update invoice statuses for completed payments that were trashed
1455 foreach ($invoice_updates as $invoice_id => $deleted_amount) {
1456 $this->updateInvoiceStatusAfterPaymentDeletion($invoice_id, $deleted_amount);
1457 }
1458
1459 wp_redirect(admin_url('admin.php?page=easy-invoice-payments&bulk_trashed=' . $processed));
1460 break;
1461
1462 case 'restore':
1463 foreach ($payment_ids as $id) {
1464 // Get payment info before restoring for invoice status update
1465 $payment_post = get_post($id);
1466 if (!$payment_post || $payment_post->post_type !== 'easy_invoice_payment') {
1467 continue;
1468 }
1469 $payment = new Payment($payment_post);
1470 $payment_status = $payment->getStatus();
1471 $invoice_id = $payment->getInvoiceId();
1472 $payment_amount = $payment->getAmount();
1473
1474 if (wp_untrash_post($id)) {
1475 // Also set status to publish (since WordPress sets it to draft by default)
1476 wp_update_post(array(
1477 'ID' => $id,
1478 'post_status' => 'publish'
1479 ));
1480 $processed++;
1481
1482 // Track invoice updates needed for completed payments
1483 if ($payment_status === 'completed' && $invoice_id) {
1484 if (!isset($invoice_updates[$invoice_id])) {
1485 $invoice_updates[$invoice_id] = 0;
1486 }
1487 $invoice_updates[$invoice_id] += $payment_amount;
1488 }
1489 }
1490 }
1491
1492 // Update invoice statuses for completed payments that were restored
1493 foreach ($invoice_updates as $invoice_id => $restored_amount) {
1494 $this->updateInvoiceStatusAfterPaymentRestoration($invoice_id, $restored_amount);
1495 }
1496
1497 wp_redirect(admin_url('admin.php?page=easy-invoice-payments&bulk_restored=' . $processed));
1498 break;
1499
1500 case 'delete':
1501 foreach ($payment_ids as $id) {
1502 // Get payment info before deletion for invoice status update
1503 $payment_post = get_post($id);
1504 if (!$payment_post || $payment_post->post_type !== 'easy_invoice_payment') {
1505 continue;
1506 }
1507 $payment = new Payment($payment_post);
1508 $payment_status = $payment->getStatus();
1509 $invoice_id = $payment->getInvoiceId();
1510 $payment_amount = $payment->getAmount();
1511
1512 if (wp_delete_post($id, true)) {
1513 $processed++;
1514
1515 // Track invoice updates needed for completed payments
1516 if ($payment_status === 'completed' && $invoice_id) {
1517 if (!isset($invoice_updates[$invoice_id])) {
1518 $invoice_updates[$invoice_id] = 0;
1519 }
1520 $invoice_updates[$invoice_id] += $payment_amount;
1521 }
1522 }
1523 }
1524
1525 // Update invoice statuses for completed payments that were deleted
1526 foreach ($invoice_updates as $invoice_id => $deleted_amount) {
1527 $this->updateInvoiceStatusAfterPaymentDeletion($invoice_id, $deleted_amount);
1528 }
1529
1530 wp_redirect(admin_url('admin.php?page=easy-invoice-payments&bulk_deleted=' . $processed));
1531 break;
1532
1533 default:
1534 wp_redirect(admin_url('admin.php?page=easy-invoice-payments&bulk_error=invalid_action'));
1535 }
1536
1537 exit;
1538 }
1539
1540 /**
1541 * Update invoice status after payment deletion
1542 */
1543 private function updateInvoiceStatusAfterPaymentDeletion($invoice_id, $deleted_amount) {
1544 $invoice = new Invoice($invoice_id);
1545
1546 if (!$invoice->getId()) {
1547 return;
1548 }
1549
1550 // Get all remaining payments for this invoice
1551 $remaining_payments = get_posts(array(
1552 'post_type' => 'easy_invoice_payment',
1553 'post_status' => 'publish',
1554 'meta_query' => array(
1555 array(
1556 'key' => '_invoice_id',
1557 'value' => $invoice_id,
1558 'compare' => '='
1559 ),
1560 array(
1561 'key' => '_status',
1562 'value' => 'completed',
1563 'compare' => '='
1564 )
1565 ),
1566 'posts_per_page' => -1
1567 ));
1568
1569 // Calculate total remaining payments
1570 $total_remaining = 0;
1571 foreach ($remaining_payments as $payment_post) {
1572 $payment = new Payment($payment_post);
1573 $total_remaining += floatval($payment->getAmount());
1574 }
1575
1576 $invoice_total = floatval($invoice->getTotal());
1577
1578 // Update invoice status based on remaining payments
1579 if ($total_remaining >= $invoice_total) {
1580 // Still fully paid
1581 update_post_meta($invoice_id, '_status', 'paid');
1582 } elseif ($total_remaining > 0) {
1583 // Partially paid
1584 update_post_meta($invoice_id, '_status', 'partial');
1585 } else {
1586 // No payments remaining
1587 update_post_meta($invoice_id, '_status', 'unpaid');
1588 }
1589 }
1590
1591 /**
1592 * Update invoice status after payment restoration
1593 */
1594 private function updateInvoiceStatusAfterPaymentRestoration($invoice_id, $restored_amount) {
1595 $invoice = new Invoice($invoice_id);
1596
1597 if (!$invoice->getId()) {
1598 return;
1599 }
1600
1601 // Get all payments for this invoice (including the restored one)
1602 $all_payments = get_posts(array(
1603 'post_type' => 'easy_invoice_payment',
1604 'post_status' => 'publish',
1605 'meta_query' => array(
1606 array(
1607 'key' => '_invoice_id',
1608 'value' => $invoice_id,
1609 'compare' => '='
1610 ),
1611 array(
1612 'key' => '_status',
1613 'value' => 'completed',
1614 'compare' => '='
1615 )
1616 ),
1617 'posts_per_page' => -1
1618 ));
1619
1620 // Calculate total payments (including restored ones)
1621 $total_payments = 0;
1622 foreach ($all_payments as $payment_post) {
1623 $payment = new Payment($payment_post);
1624 $total_payments += floatval($payment->getAmount());
1625 }
1626
1627 $invoice_total = floatval($invoice->getTotal());
1628
1629 // Update invoice status based on total payments
1630 if ($total_payments >= $invoice_total) {
1631 // Fully paid
1632 update_post_meta($invoice_id, '_status', 'paid');
1633 } elseif ($total_payments > 0) {
1634 // Partially paid
1635 update_post_meta($invoice_id, '_status', 'partial');
1636 } else {
1637 // No payments
1638 update_post_meta($invoice_id, '_status', 'unpaid');
1639 }
1640 }
1641
1642 // Stripe payment recording moved to Pro plugin
1643
1644
1645 }
1646