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

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

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