PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.2.0
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.2.0
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.2.0, at includes/Controllers/PaymentController.php

1,619 lines 63.6 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 (!current_user_can('manage_options') && $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 (!current_user_can('manage_options')) {
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
813 if (!current_user_can('manage_options')) {
814 wp_send_json_error(['message' => __('You do not have permission to perform this action', 'easy-invoice')]);
815 return;
816 }
817
818 // Verify nonce
819 check_ajax_referer('easy_invoice_admin', 'nonce');
820
821 $invoice_id = isset($_POST['invoice_id']) ? intval($_POST['invoice_id']) : 0;
822 $reason = isset($_POST['reason']) ? sanitize_textarea_field($_POST['reason']) : '';
823
824 if (!$invoice_id) {
825 wp_send_json_error(['message' => __('Invoice ID is required', 'easy-invoice')]);
826 return;
827 }
828
829 // Get the invoice
830 $post = get_post($invoice_id);
831 if (!$post || $post->post_type !== \EasyInvoice\Constants\PostTypes::EASY_INVOICE_POST_TYPE) {
832 wp_send_json_error(['message' => __('Invalid invoice', 'easy-invoice')]);
833 return;
834 }
835
836 $invoice = new Invoice($post);
837
838 // Update invoice status
839 update_post_meta($invoice_id, '_payment_status', 'rejected');
840
841 // Add rejection reason
842 update_post_meta($invoice_id, '_payment_rejection_reason', $reason);
843 update_post_meta($invoice_id, '_payment_rejection_date', current_time('mysql'));
844 update_post_meta($invoice_id, '_payment_rejection_user', get_current_user_id());
845
846 // Send rejection email to customer
847 $this->sendPaymentRejectionEmail($invoice_id, $reason);
848
849 wp_send_json_success([
850 'message' => __('Payment rejected successfully', 'easy-invoice')
851 ]);
852 }
853
854
855
856 /**
857 * Send payment confirmation email to customer
858 *
859 * @param int $invoice_id
860 * @param int $payment_id
861 */
862 private function sendPaymentConfirmationEmail($invoice_id, $payment_id): void {
863 $invoice = new Invoice(get_post($invoice_id));
864
865 if (!$invoice || !$invoice->getId()) {
866 return;
867 }
868
869 // Use EmailManager to send payment confirmation using proper template system
870 // This will check if payment email is enabled in settings
871 $email_manager = \EasyInvoice\Services\EmailManager::getInstance();
872 $email_manager->sendInvoiceEmail($invoice, 'paid', [
873 'payment_id' => $payment_id,
874 'skip_bcc' => true // Skip BCC to admin since this is a direct call
875 ]);
876 }
877
878 /**
879 * Send payment rejection email to customer
880 *
881 * @param int $invoice_id
882 * @param string $reason
883 */
884 private function sendPaymentRejectionEmail($invoice_id, $reason): void {
885 $invoice = new Invoice(get_post($invoice_id));
886
887 if (!$invoice || !$invoice->getId()) {
888 return;
889 }
890
891 // Use EmailManager to send payment rejection
892 $email_manager = \EasyInvoice\Services\EmailManager::getInstance();
893 $email_manager->sendPaymentRejectionEmail($invoice, $reason);
894 }
895
896 /**
897 * Add pending payment statuses to admin filters
898 *
899 * @param array $statuses
900 * @return array
901 */
902 public function addPendingPaymentStatuses($statuses): array {
903 $statuses['pending-bank'] = __('Pending Bank Transfer', 'easy-invoice');
904 $statuses['pending-cheque'] = __('Pending Cheque', 'easy-invoice');
905 return $statuses;
906 }
907
908 /**
909 * Add payment method column to payments list
910 *
911 * @param array $columns
912 * @return array
913 */
914 public function addPaymentMethodColumn($columns): array {
915 $new_columns = [];
916
917 foreach ($columns as $key => $value) {
918 $new_columns[$key] = $value;
919
920 if ($key === 'title') {
921 $new_columns['payment_method'] = __('Payment Method', 'easy-invoice');
922 }
923 }
924
925 return $new_columns;
926 }
927
928 /**
929 * Render payment method column
930 *
931 * @param string $column
932 * @param int $post_id
933 */
934 public function renderPaymentMethodColumn($column, $post_id): void {
935 if ($column === 'payment_method') {
936 $payment_method = get_post_meta($post_id, '_payment_method', true);
937 $payment_methods = [
938 'paypal' => __('PayPal', 'easy-invoice')
939 ];
940
941 echo isset($payment_methods[$payment_method]) ? esc_html($payment_methods[$payment_method]) : esc_html($payment_method);
942 }
943 }
944
945 /**
946 * Send payment reminders for pending manual payments
947 */
948 public function sendPaymentReminders(): void {
949 // Get invoices with pending manual payments
950 $pending_invoices = get_posts([
951 'post_type' => \EasyInvoice\Constants\PostTypes::EASY_INVOICE_POST_TYPE,
952 'posts_per_page' => -1,
953 'meta_query' => [
954 'relation' => 'AND',
955 [
956 'key' => '_payment_status',
957 'value' => ['pending-bank', 'pending-cheque'],
958 'compare' => 'IN'
959 ],
960 [
961 'key' => '_payment_reminder_sent',
962 'compare' => 'NOT EXISTS'
963 ]
964 ]
965 ]);
966
967 if (!empty($pending_invoices)) {
968 // Get currency settings
969 $settings_controller = new \EasyInvoice\Controllers\SettingsController();
970 $settings = $settings_controller->getSettings();
971 $currency_code = $settings['easy_invoice_currency_code'] ?? 'USD';
972 $currency_symbol = \EasyInvoice\Helpers\CurrencyHelper::getCurrencySymbol($currency_code);
973
974 foreach ($pending_invoices as $post) {
975 $invoice = new Invoice($post);
976
977 if (!$invoice || !$invoice->getId()) {
978 continue;
979 }
980
981 // Use EmailManager to send payment reminder
982 $email_manager = \EasyInvoice\Services\EmailManager::getInstance();
983 $result = $email_manager->sendInvoiceEmail($invoice, 'reminder', [
984 'payment_method' => get_post_meta($invoice->getId(), '_payment_method', true)
985 ]);
986
987 // Mark reminder as sent if email was sent successfully
988 if ($result['success']) {
989 update_post_meta($invoice->getId(), '_payment_reminder_sent', current_time('mysql'));
990 }
991 }
992
993 wp_reset_postdata();
994 }
995 }
996
997 /**
998 * Submit manual payment
999 */
1000 public function submitManualPayment(): void {
1001 // Verify nonce
1002 if (!wp_verify_nonce($_POST['nonce'], 'easy_invoice_payment')) {
1003 wp_send_json_error(['message' => __('Security check failed', 'easy-invoice')]);
1004 return;
1005 }
1006
1007 $invoice_id = isset($_POST['invoice_id']) ? intval($_POST['invoice_id']) : 0;
1008 $payment_type = isset($_POST['payment_type']) ? sanitize_text_field($_POST['payment_type']) : '';
1009 $payment_notes = isset($_POST['payment_notes']) ? sanitize_textarea_field($_POST['payment_notes']) : '';
1010
1011 if (!$invoice_id || !$payment_type) {
1012 wp_send_json_error(['message' => __('Missing required fields', 'easy-invoice')]);
1013 return;
1014 }
1015
1016 // Get invoice
1017 $invoice_post = get_post($invoice_id);
1018 if (!$invoice_post || $invoice_post->post_type !== \EasyInvoice\Constants\PostTypes::EASY_INVOICE_POST_TYPE) {
1019 wp_send_json_error(['message' => __('Invalid invoice', 'easy-invoice')]);
1020 return;
1021 }
1022
1023 $invoice = new \EasyInvoice\Models\Invoice($invoice_post);
1024 $currency_code = $invoice->getCurrencyCode() ?: 'USD';
1025 if ($currency_code === 'global') {
1026 $currency_code = get_option('easy_invoice_currency_code', 'USD');
1027 }
1028 $currency_symbol = \EasyInvoice\Helpers\CurrencyHelper::getCurrencySymbol($currency_code);
1029
1030 // Handle file upload (never trust client MIME or filename extension — use WordPress filetype APIs)
1031 $proof_url = '';
1032 if (isset($_FILES['payment_proof']) && $_FILES['payment_proof']['error'] === UPLOAD_ERR_OK) {
1033 $file = $_FILES['payment_proof'];
1034
1035 if (empty($file['tmp_name']) || !is_uploaded_file($file['tmp_name'])) {
1036 wp_send_json_error(['message' => __('Invalid upload.', 'easy-invoice')]);
1037 return;
1038 }
1039
1040 $max_size = 5 * 1024 * 1024; // 5MB
1041 if ($file['size'] > $max_size) {
1042 wp_send_json_error(['message' => __('File size must be less than 5MB.', 'easy-invoice')]);
1043 return;
1044 }
1045
1046 $allowed_mimes = [
1047 'jpg|jpeg|jpe' => 'image/jpeg',
1048 'png' => 'image/png',
1049 'gif' => 'image/gif',
1050 'pdf' => 'application/pdf',
1051 ];
1052
1053 $checked = wp_check_filetype_and_ext($file['tmp_name'], $file['name'], $allowed_mimes);
1054 if (empty($checked['ext']) || empty($checked['type'])) {
1055 wp_send_json_error(['message' => __('Invalid file type. Only JPG, PNG, GIF, and PDF files are allowed.', 'easy-invoice')]);
1056 return;
1057 }
1058
1059 $allowed_types = array_values($allowed_mimes);
1060 if (!in_array($checked['type'], $allowed_types, true)) {
1061 wp_send_json_error(['message' => __('Invalid file type. Only JPG, PNG, GIF, and PDF files are allowed.', 'easy-invoice')]);
1062 return;
1063 }
1064
1065 $upload_dir = wp_upload_dir();
1066 $proof_dir = $upload_dir['basedir'] . '/easy-invoice/payment-proofs/';
1067
1068 if (!wp_mkdir_p($proof_dir)) {
1069 wp_send_json_error(['message' => __('Could not create upload directory.', 'easy-invoice')]);
1070 return;
1071 }
1072
1073 $filename = uniqid('payment_proof_', true) . '.' . $checked['ext'];
1074 $filepath = $proof_dir . $filename;
1075
1076 if (!move_uploaded_file($file['tmp_name'], $filepath)) {
1077 wp_send_json_error(['message' => __('Failed to save payment proof file.', 'easy-invoice')]);
1078 return;
1079 }
1080
1081 chmod($filepath, 0644);
1082 $proof_url = $upload_dir['baseurl'] . '/easy-invoice/payment-proofs/' . $filename;
1083 }
1084
1085 // Create payment record
1086 $payment_data = [
1087 'post_title' => sprintf('Manual Payment (%s) for Invoice #%s', ucfirst($payment_type), $invoice->getNumber()),
1088 'post_type' => 'easy_invoice_payment',
1089 'post_status' => 'publish',
1090 'post_author' => get_current_user_id(),
1091 ];
1092
1093 $payment_id = wp_insert_post($payment_data);
1094
1095 if (is_wp_error($payment_id)) {
1096 wp_send_json_error(['message' => __('Failed to create payment record', 'easy-invoice')]);
1097 return;
1098 }
1099
1100 // Save payment metadata
1101 update_post_meta($payment_id, '_invoice_id', $invoice_id);
1102 update_post_meta($payment_id, '_amount', $invoice->getTotal());
1103 update_post_meta($payment_id, '_payment_method', 'manual');
1104 update_post_meta($payment_id, '_payment_type', $payment_type);
1105 update_post_meta($payment_id, '_status', 'pending');
1106 update_post_meta($payment_id, '_transaction_id', 'MANUAL-' . $invoice_id . '-' . time());
1107 update_post_meta($payment_id, '_payment_date', current_time('mysql'));
1108 update_post_meta($payment_id, '_notes', $payment_notes);
1109 update_post_meta($payment_id, '_currency', $currency_code);
1110 update_post_meta($payment_id, '_currency_symbol', $currency_symbol);
1111 update_post_meta($payment_id, '_payment_proof', $proof_url);
1112
1113 // Update invoice status to pending verification
1114 $invoice->setStatus('pending_verification');
1115 $invoice->save();
1116
1117 // Store payment details on invoice
1118 $invoice->setMeta('_payment_method', 'manual');
1119 $invoice->setMeta('_payment_type', $payment_type);
1120 $invoice->setMeta('_payment_status', 'pending');
1121 $invoice->setMeta('_manual_payment_id', $payment_id);
1122 $invoice->setMeta('_manual_payment_proof', $proof_url);
1123 $invoice->setMeta('_manual_payment_notes', $payment_notes);
1124
1125 // Send admin notification
1126 do_action('easy_invoice_manual_payment_submitted', $invoice_id, $payment_type);
1127
1128 wp_send_json_success([
1129 'message' => __('Payment submitted successfully! Your payment will be verified by the administrator.', 'easy-invoice'),
1130 'payment_id' => $payment_id
1131 ]);
1132 }
1133
1134 /**
1135 * Handle submission of payment proof for manual gateways (Bank Transfer, Cheque)
1136 */
1137 public function submitPaymentProof(): void {
1138 $gateway_name = isset($_POST['gateway']) ? sanitize_text_field($_POST['gateway']) : '';
1139 $invoice_id = isset($_POST['invoice_id']) ? intval($_POST['invoice_id']) : 0;
1140
1141 if (empty($gateway_name) || empty($invoice_id)) {
1142 wp_send_json_error(['message' => __('Invalid request. Missing gateway or invoice ID.', 'easy-invoice')]);
1143 return;
1144 }
1145
1146 // Nonce verification (make nonce name consistent or check based on gateway)
1147 $nonce_action = 'easy_invoice_payment_proof_' . $invoice_id; // Bank transfer nonce
1148 $nonce_value = isset($_POST['payment_proof_nonce']) ? sanitize_text_field($_POST['payment_proof_nonce']) : '';
1149 if ($gateway_name === 'cheque') {
1150 $nonce_action = 'easy_invoice_cheque_notification_' . $invoice_id; // Cheque nonce
1151 $nonce_value = isset($_POST['cheque_notification_nonce']) ? sanitize_text_field($_POST['cheque_notification_nonce']) : '';
1152 }
1153
1154 if (!wp_verify_nonce($nonce_value, $nonce_action)) {
1155 wp_send_json_error(['message' => __('Nonce verification failed. Please try again.', 'easy-invoice')]);
1156 return;
1157 }
1158
1159 // Optional: Add capability check if this can be submitted by logged-in users only from frontend
1160 // if (is_user_logged_in() && !current_user_can('read_invoice', $invoice_id)) { // Example capability
1161 // wp_send_json_error(['message' => __('You do not have permission to submit proof for this invoice.', 'easy-invoice')]);
1162 // return;
1163 // }
1164
1165 $gateway = $this->gatewayManager->getGateway($gateway_name);
1166
1167 if (!$gateway || !method_exists($gateway, 'handleProofSubmission')) {
1168 wp_send_json_error(['message' => __('Invalid payment gateway or submission handler not found.', 'easy-invoice')]);
1169 return;
1170 }
1171
1172 // Prepare data for the gateway handler
1173 $post_data = stripslashes_deep($_POST);
1174 $files_data = $_FILES;
1175
1176 $result = $gateway->handleProofSubmission($post_data, $files_data);
1177
1178 if ($result['success']) {
1179 wp_send_json_success(['message' => $result['message']]);
1180 } else {
1181 wp_send_json_error(['message' => $result['message']]);
1182 }
1183 }
1184
1185 /**
1186 * AJAX handler for admin to mark an invoice as paid.
1187 */
1188 public function mark_invoice_paid_ajax(): void {
1189 $invoice_id = isset($_POST['invoice_id']) ? intval($_POST['invoice_id']) : 0;
1190 $nonce = isset($_POST['nonce']) ? sanitize_text_field($_POST['nonce']) : '';
1191 $notes = isset($_POST['notes']) ? sanitize_textarea_field($_POST['notes']) : '';
1192
1193 if (empty($invoice_id) || !wp_verify_nonce($nonce, 'easy_invoice_approve_payment')) {
1194 easy_invoice_toast_error(__('Invalid request or security check failed.', 'easy-invoice'));
1195 return;
1196 }
1197
1198 // Use manage_options capability which administrators have
1199 if (!current_user_can('manage_options')) {
1200 easy_invoice_toast_error(__('You do not have permission to perform this action.', 'easy-invoice'));
1201 return;
1202 }
1203
1204 $invoice_post = get_post($invoice_id);
1205 if (!$invoice_post || $invoice_post->post_type !== \EasyInvoice\Constants\PostTypes::EASY_INVOICE_POST_TYPE) {
1206 wp_send_json_error(['message' => __('Invalid invoice.', 'easy-invoice')]);
1207 return;
1208 }
1209
1210 $invoice = new Invoice($invoice_post);
1211 // For manual approval, always use 'manual' as payment method
1212 $payment_method = 'manual';
1213
1214 // Update invoice post status to 'publish' (or your primary paid status)
1215 wp_update_post(['ID' => $invoice_id, 'post_status' => 'publish']);
1216 update_post_meta($invoice_id, '_payment_status', 'completed'); // General completed status for payments
1217
1218 // Allow plugins to control invoice status update
1219 $should_update_invoice_status = apply_filters('easy_invoice_should_update_invoice_status', true, $invoice_id);
1220 if ($should_update_invoice_status) {
1221 update_post_meta($invoice_id, InvoiceFields::STATUS, 'paid'); // Specific invoice status field if used by model
1222 }
1223
1224 // Use submitted notes or default note
1225 $payment_notes = !empty($notes)
1226 ? $notes
1227 : __('Payment manually verified by admin.', 'easy-invoice');
1228
1229 // Find existing pending payment records for this invoice
1230 $existing_payment_args = [
1231 'post_type' => 'easy_invoice_payment',
1232 'posts_per_page' => 1,
1233 'meta_query' => [
1234 'relation' => 'AND',
1235 [
1236 'key' => '_invoice_id',
1237 'value' => $invoice_id,
1238 ],
1239 [
1240 'key' => '_status',
1241 'value' => ['pending-bank', 'pending-cheque', 'pending'], // Check against pending statuses
1242 'compare' => 'IN'
1243 ]
1244 ]
1245 ];
1246 $existing_payments = get_posts($existing_payment_args);
1247 $payment_id = null;
1248
1249 if (!empty($existing_payments)) {
1250 // Update existing pending payment instead of creating new one
1251 $payment_id = $existing_payments[0]->ID;
1252 update_post_meta($payment_id, '_status', 'completed'); // Update status to completed
1253 update_post_meta($payment_id, '_payment_method', 'manual'); // Set payment method to manual
1254 update_post_meta($payment_id, '_transaction_id', 'MANUAL-' . $invoice_id . '-' . time());
1255 update_post_meta($payment_id, '_payment_date', current_time('mysql'));
1256 update_post_meta($payment_id, '_notes', $payment_notes); // Update notes on existing payment
1257 } else {
1258 // Only create a new payment if no pending payments exist
1259 // This prevents creating duplicate payment records
1260 $existing_payments = get_posts([
1261 'post_type' => 'easy_invoice_payment',
1262 'posts_per_page' => -1,
1263 'meta_query' => [
1264 [
1265 'key' => '_invoice_id',
1266 'value' => $invoice_id,
1267 ]
1268 ]
1269 ]);
1270
1271 if (!empty($existing_payments)) {
1272 // If payments exist but none are pending, don't create a new one
1273 // Just update the invoice status
1274 easy_invoice_toast_success(__('Invoice marked as paid successfully.', 'easy-invoice'));
1275 return;
1276 }
1277
1278 // Get currency from invoice
1279 $currency_code = get_post_meta($invoice_id, '_easy_invoice_currency_code', true);
1280 if (empty($currency_code) || $currency_code === 'global') {
1281 $currency_code = get_option('easy_invoice_currency_code', 'USD');
1282 }
1283 $currency_symbol = \EasyInvoice\Helpers\CurrencyHelper::getCurrencySymbol($currency_code);
1284
1285 $payment_data = [
1286 'invoice_id' => $invoice_id,
1287 'amount' => $invoice->getTotal(), // Or get amount from proof submission if it varies
1288 'payment_method' => $payment_method,
1289 'status' => 'completed',
1290 'transaction_id' => get_post_meta($invoice_id, '_' . $payment_method . '_transaction_id', true) ?: 'MANUAL-' . $invoice_id,
1291 'payment_date' => current_time('mysql'),
1292 'notes' => $payment_notes, // Use provided notes
1293 'payment_type' => 'manual',
1294 'currency' => $currency_code,
1295 'currency_symbol' => $currency_symbol,
1296 'gateway_response' => json_encode([
1297 'admin_verified' => true,
1298 'user' => get_current_user_id(),
1299 'verification_date' => current_time('mysql'),
1300 'notes' => $payment_notes // Store notes in response JSON as well
1301 ])
1302 ];
1303 try {
1304 // Create payment record using WordPress post creation
1305 $payment_post_data = [
1306 'post_title' => sprintf('Manual Payment for Invoice #%s', $invoice->getNumber()),
1307 'post_type' => 'easy_invoice_payment',
1308 'post_status' => 'publish',
1309 'post_author' => get_current_user_id(),
1310 'meta_input' => [
1311 '_invoice_id' => $invoice_id,
1312 '_amount' => $invoice->getTotal(),
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,
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
1326 ])
1327 ]
1328 ];
1329
1330 $payment_id = wp_insert_post($payment_post_data);
1331 if (is_wp_error($payment_id)) {
1332 easy_invoice_toast_error(__('Error creating payment record:', 'easy-invoice') . ' ' . $payment_id->get_error_message());
1333 return;
1334 }
1335 } catch (\Exception $e) {
1336 easy_invoice_toast_error(__('Error creating payment record:', 'easy-invoice') . ' ' . $e->getMessage());
1337 return;
1338 }
1339 }
1340
1341 // Store payment details before updating status (for the hook)
1342 $transaction_id = get_post_meta($invoice_id, '_' . $payment_method . '_transaction_id', true) ?: 'MANUAL-' . $invoice_id;
1343 $invoice->setMeta('_payment_method', $payment_method);
1344 $invoice->setMeta('_transaction_id', $transaction_id);
1345
1346 // Update invoice status to paid
1347 // This will trigger 'easy_invoice_payment_completed' hook which sends admin notification
1348 $invoice->setStatus('paid');
1349 $invoice->save();
1350
1351 // Trigger the payment completed hook manually since we're updating status directly
1352 do_action('easy_invoice_payment_completed', $invoice_id, $invoice, [
1353 'payment_method' => $payment_method,
1354 'gateway_name' => 'manual',
1355 'transaction_id' => $transaction_id,
1356 'amount' => $invoice->getTotal()
1357 ]);
1358
1359 // Trigger email confirmation and actions only if we have a payment_id
1360 if ($payment_id) {
1361 // Send confirmation email to customer
1362 $this->sendPaymentConfirmationEmail($invoice_id, $payment_id);
1363 do_action('easy_invoice_manual_payment_confirmed', $invoice_id, $payment_id, $payment_method);
1364 }
1365
1366 easy_invoice_toast_success(__('Invoice marked as paid successfully.', 'easy-invoice'));
1367 }
1368
1369 /**
1370 * Handle bulk actions for payments
1371 */
1372 public function handleBulkActions() {
1373 // Check if we're processing a bulk action
1374 if (!isset($_POST['action']) || $_POST['action'] !== 'easy_invoice_payment_bulk_action') {
1375 return;
1376 }
1377
1378 // Check nonce and capability
1379 if (!wp_verify_nonce($_POST['easy_invoice_payment_bulk_nonce'], 'easy_invoice_payment_bulk_action')) {
1380 wp_die(__('Security check failed.', 'easy-invoice'));
1381 }
1382
1383 if (!current_user_can('manage_options')) {
1384 wp_die(__('You do not have permission to perform this action.', 'easy-invoice'));
1385 }
1386
1387 // Check if we have payment IDs
1388 if (!isset($_POST['payment_ids']) || !is_array($_POST['payment_ids']) || empty($_POST['payment_ids'])) {
1389 wp_redirect(admin_url('admin.php?page=easy-invoice-payments&bulk_error=no_selection'));
1390 exit;
1391 }
1392
1393 // Get bulk action and payment IDs
1394 $bulk_action = isset($_POST['bulk_action']) ? sanitize_text_field($_POST['bulk_action']) : '';
1395 $payment_ids = array_map('intval', $_POST['payment_ids']);
1396
1397 // Process based on action
1398 $processed = 0;
1399 $invoice_updates = array(); // Track invoice updates needed
1400
1401 switch ($bulk_action) {
1402 case 'trash':
1403 foreach ($payment_ids as $id) {
1404 // Get payment info before trashing for invoice status update
1405 $payment_post = get_post($id);
1406 if (!$payment_post || $payment_post->post_type !== 'easy_invoice_payment') {
1407 continue;
1408 }
1409 $payment = new Payment($payment_post);
1410 $payment_status = $payment->getStatus();
1411 $invoice_id = $payment->getInvoiceId();
1412 $payment_amount = $payment->getAmount();
1413
1414 if (wp_trash_post($id)) {
1415 $processed++;
1416
1417 // Track invoice updates needed for completed payments
1418 if ($payment_status === 'completed' && $invoice_id) {
1419 if (!isset($invoice_updates[$invoice_id])) {
1420 $invoice_updates[$invoice_id] = 0;
1421 }
1422 $invoice_updates[$invoice_id] += $payment_amount;
1423 }
1424 }
1425 }
1426
1427 // Update invoice statuses for completed payments that were trashed
1428 foreach ($invoice_updates as $invoice_id => $deleted_amount) {
1429 $this->updateInvoiceStatusAfterPaymentDeletion($invoice_id, $deleted_amount);
1430 }
1431
1432 wp_redirect(admin_url('admin.php?page=easy-invoice-payments&bulk_trashed=' . $processed));
1433 break;
1434
1435 case 'restore':
1436 foreach ($payment_ids as $id) {
1437 // Get payment info before restoring for invoice status update
1438 $payment_post = get_post($id);
1439 if (!$payment_post || $payment_post->post_type !== 'easy_invoice_payment') {
1440 continue;
1441 }
1442 $payment = new Payment($payment_post);
1443 $payment_status = $payment->getStatus();
1444 $invoice_id = $payment->getInvoiceId();
1445 $payment_amount = $payment->getAmount();
1446
1447 if (wp_untrash_post($id)) {
1448 // Also set status to publish (since WordPress sets it to draft by default)
1449 wp_update_post(array(
1450 'ID' => $id,
1451 'post_status' => 'publish'
1452 ));
1453 $processed++;
1454
1455 // Track invoice updates needed for completed payments
1456 if ($payment_status === 'completed' && $invoice_id) {
1457 if (!isset($invoice_updates[$invoice_id])) {
1458 $invoice_updates[$invoice_id] = 0;
1459 }
1460 $invoice_updates[$invoice_id] += $payment_amount;
1461 }
1462 }
1463 }
1464
1465 // Update invoice statuses for completed payments that were restored
1466 foreach ($invoice_updates as $invoice_id => $restored_amount) {
1467 $this->updateInvoiceStatusAfterPaymentRestoration($invoice_id, $restored_amount);
1468 }
1469
1470 wp_redirect(admin_url('admin.php?page=easy-invoice-payments&bulk_restored=' . $processed));
1471 break;
1472
1473 case 'delete':
1474 foreach ($payment_ids as $id) {
1475 // Get payment info before deletion for invoice status update
1476 $payment_post = get_post($id);
1477 if (!$payment_post || $payment_post->post_type !== 'easy_invoice_payment') {
1478 continue;
1479 }
1480 $payment = new Payment($payment_post);
1481 $payment_status = $payment->getStatus();
1482 $invoice_id = $payment->getInvoiceId();
1483 $payment_amount = $payment->getAmount();
1484
1485 if (wp_delete_post($id, true)) {
1486 $processed++;
1487
1488 // Track invoice updates needed for completed payments
1489 if ($payment_status === 'completed' && $invoice_id) {
1490 if (!isset($invoice_updates[$invoice_id])) {
1491 $invoice_updates[$invoice_id] = 0;
1492 }
1493 $invoice_updates[$invoice_id] += $payment_amount;
1494 }
1495 }
1496 }
1497
1498 // Update invoice statuses for completed payments that were deleted
1499 foreach ($invoice_updates as $invoice_id => $deleted_amount) {
1500 $this->updateInvoiceStatusAfterPaymentDeletion($invoice_id, $deleted_amount);
1501 }
1502
1503 wp_redirect(admin_url('admin.php?page=easy-invoice-payments&bulk_deleted=' . $processed));
1504 break;
1505
1506 default:
1507 wp_redirect(admin_url('admin.php?page=easy-invoice-payments&bulk_error=invalid_action'));
1508 }
1509
1510 exit;
1511 }
1512
1513 /**
1514 * Update invoice status after payment deletion
1515 */
1516 private function updateInvoiceStatusAfterPaymentDeletion($invoice_id, $deleted_amount) {
1517 $invoice = new Invoice($invoice_id);
1518
1519 if (!$invoice->getId()) {
1520 return;
1521 }
1522
1523 // Get all remaining payments for this invoice
1524 $remaining_payments = get_posts(array(
1525 'post_type' => 'easy_invoice_payment',
1526 'post_status' => 'publish',
1527 'meta_query' => array(
1528 array(
1529 'key' => '_invoice_id',
1530 'value' => $invoice_id,
1531 'compare' => '='
1532 ),
1533 array(
1534 'key' => '_status',
1535 'value' => 'completed',
1536 'compare' => '='
1537 )
1538 ),
1539 'posts_per_page' => -1
1540 ));
1541
1542 // Calculate total remaining payments
1543 $total_remaining = 0;
1544 foreach ($remaining_payments as $payment_post) {
1545 $payment = new Payment($payment_post);
1546 $total_remaining += floatval($payment->getAmount());
1547 }
1548
1549 $invoice_total = floatval($invoice->getTotal());
1550
1551 // Update invoice status based on remaining payments
1552 if ($total_remaining >= $invoice_total) {
1553 // Still fully paid
1554 update_post_meta($invoice_id, '_status', 'paid');
1555 } elseif ($total_remaining > 0) {
1556 // Partially paid
1557 update_post_meta($invoice_id, '_status', 'partial');
1558 } else {
1559 // No payments remaining
1560 update_post_meta($invoice_id, '_status', 'unpaid');
1561 }
1562 }
1563
1564 /**
1565 * Update invoice status after payment restoration
1566 */
1567 private function updateInvoiceStatusAfterPaymentRestoration($invoice_id, $restored_amount) {
1568 $invoice = new Invoice($invoice_id);
1569
1570 if (!$invoice->getId()) {
1571 return;
1572 }
1573
1574 // Get all payments for this invoice (including the restored one)
1575 $all_payments = get_posts(array(
1576 'post_type' => 'easy_invoice_payment',
1577 'post_status' => 'publish',
1578 'meta_query' => array(
1579 array(
1580 'key' => '_invoice_id',
1581 'value' => $invoice_id,
1582 'compare' => '='
1583 ),
1584 array(
1585 'key' => '_status',
1586 'value' => 'completed',
1587 'compare' => '='
1588 )
1589 ),
1590 'posts_per_page' => -1
1591 ));
1592
1593 // Calculate total payments (including restored ones)
1594 $total_payments = 0;
1595 foreach ($all_payments as $payment_post) {
1596 $payment = new Payment($payment_post);
1597 $total_payments += floatval($payment->getAmount());
1598 }
1599
1600 $invoice_total = floatval($invoice->getTotal());
1601
1602 // Update invoice status based on total payments
1603 if ($total_payments >= $invoice_total) {
1604 // Fully paid
1605 update_post_meta($invoice_id, '_status', 'paid');
1606 } elseif ($total_payments > 0) {
1607 // Partially paid
1608 update_post_meta($invoice_id, '_status', 'partial');
1609 } else {
1610 // No payments
1611 update_post_meta($invoice_id, '_status', 'unpaid');
1612 }
1613 }
1614
1615 // Stripe payment recording moved to Pro plugin
1616
1617
1618 }
1619