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

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

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