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

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