PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.3.2
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.3.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 2.1.15 2.1.16 2.1.2 All 57 releases
easy-invoice / includes / Controllers / PaymentController.php

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

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