PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.4.1
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.4.1
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
← All changes | includes/Gateways/PayPalGateway.php +125 -1 2.3.42.4.1 View file →
@@ -529,8 +529,25 @@
529 529 $paypal_payment_date = $payload['payment_date'] ?? current_time('mysql');
530 530 $paypal_txn_id = $payload['txn_id'] ?? '';
531 531 $payment_amount = floatval($payload['mc_gross'] ?? 0);
532 532
533 + // PayPal retries IPNs it thinks were not acknowledged. The txn_id is
534 + // the one thing that identifies a payment, so a second delivery of
535 + // one we have already booked changes nothing.
536 + if ('' !== $paypal_txn_id && $this->completedPaymentForTransaction($invoice_id, $paypal_txn_id) > 0) {
537 + $this->log('IPN for txn ' . $paypal_txn_id . ' already booked; nothing to do.');
538 + return [
539 + 'success' => true,
540 + 'message' => 'Payment already recorded'
541 + ];
542 + }
543 +
544 + // "Is the invoice paid?" is answered by summing *completed* payment
545 + // records. The record created when the customer was sent to PayPal is
546 + // still pending, so it has to be completed here -- otherwise a verified
547 + // PayPal payment left the invoice unpaid until someone marked it by hand.
548 + $this->settlePaymentRecord($invoice_id, $paypal_txn_id, $payment_amount, (string) ($payload['mc_currency'] ?? ''), $payload);
549 +
533 550 $invoice->setMeta('_easy_invoice_payment_method', 'paypal');
534 551 $invoice->setMeta('_easy_invoice_payment_date', $paypal_payment_date);
535 552 $invoice->setMeta('_easy_invoice_transaction_id', $paypal_txn_id);
536 553
@@ -555,7 +572,114 @@
555 572 'success' => true,
556 573 'message' => 'Payment processed successfully'
557 574 ];
558 575 }
559 -
576 +
577 + /**
578 + * Completed payment record already holding this PayPal transaction, if any.
579 + *
580 + * @param int $invoice_id Invoice.
581 + * @param string $txn_id PayPal txn_id.
582 + * @return int Payment post ID, 0 when none.
583 + */
584 + private function completedPaymentForTransaction(int $invoice_id, string $txn_id): int {
585 + $ids = get_posts([
586 + 'post_type' => 'easy_invoice_payment',
587 + 'post_status' => 'publish',
588 + 'fields' => 'ids',
589 + 'posts_per_page' => 1,
590 + 'no_found_rows' => true,
591 + 'meta_query' => [
592 + ['key' => '_invoice_id', 'value' => $invoice_id],
593 + ['key' => '_transaction_id', 'value' => $txn_id],
594 + ['key' => '_status', 'value' => 'completed'],
595 + ],
596 + ]);
597 +
598 + return empty($ids) ? 0 : (int) $ids[0];
599 + }
600 +
601 + /**
602 + * Turn the pending record from checkout into a completed one, or book a
603 + * fresh record when there is none to complete.
604 + *
605 + * The amount written is what PayPal says was paid, not what the invoice
606 + * asked for. A short payment then shows as partial rather than paid.
607 + *
608 + * @param int $invoice_id Invoice.
609 + * @param string $txn_id PayPal txn_id.
610 + * @param float $amount Gross amount from the IPN.
611 + * @param string $currency Currency code from the IPN.
612 + * @param array $payload Verified IPN payload, kept for the audit trail.
613 + * @return int Payment post ID, 0 on failure.
614 + */
615 + private function settlePaymentRecord(int $invoice_id, string $txn_id, float $amount, string $currency, array $payload): int {
616 + $pending = get_posts([
617 + 'post_type' => 'easy_invoice_payment',
618 + 'post_status' => 'publish',
619 + 'fields' => 'ids',
620 + 'posts_per_page' => 1,
621 + 'no_found_rows' => true,
622 + 'orderby' => 'date',
623 + 'order' => 'DESC',
624 + 'meta_query' => [
625 + ['key' => '_invoice_id', 'value' => $invoice_id],
626 + ['key' => '_payment_method', 'value' => 'paypal'],
627 + ['key' => '_status', 'value' => 'pending'],
628 + ],
629 + ]);
630 +
631 + $gateway_response = [
632 + 'gateway' => 'paypal',
633 + 'txn_id' => $txn_id,
634 + 'payment_status' => $payload['payment_status'] ?? '',
635 + 'mc_gross' => $payload['mc_gross'] ?? '',
636 + 'mc_currency' => $currency,
637 + 'payer_email' => $payload['payer_email'] ?? '',
638 + ];
639 +
640 + if (!empty($pending)) {
641 + $payment_id = (int) $pending[0];
642 + update_post_meta($payment_id, '_status', 'completed');
643 + update_post_meta($payment_id, '_transaction_id', '' !== $txn_id ? $txn_id : get_post_meta($payment_id, '_transaction_id', true));
644 + if ($amount > 0) {
645 + update_post_meta($payment_id, '_amount', $amount);
646 + }
647 + update_post_meta($payment_id, '_payment_date', current_time('mysql'));
648 + update_post_meta($payment_id, '_notes', __('Payment confirmed by PayPal', 'easy-invoice'));
649 + update_post_meta($payment_id, '_gateway_response', wp_json_encode($gateway_response));
650 + $this->log('Pending PayPal record #' . $payment_id . ' completed for txn ' . $txn_id);
651 +
652 + return $payment_id;
653 + }
654 +
655 + $currency = '' !== $currency ? strtoupper($currency) : get_option('easy_invoice_currency_code', 'USD');
656 + $payment_id = wp_insert_post([
657 + 'post_title' => sprintf('PayPal Payment for Invoice #%s', $invoice_id),
658 + 'post_type' => 'easy_invoice_payment',
659 + 'post_status' => 'publish',
660 + 'post_author' => 1,
661 + 'meta_input' => [
662 + '_invoice_id' => $invoice_id,
663 + '_amount' => $amount,
664 + '_payment_method' => 'paypal',
665 + '_status' => 'completed',
666 + '_transaction_id' => $txn_id,
667 + '_payment_date' => current_time('mysql'),
668 + '_notes' => __('Payment confirmed by PayPal', 'easy-invoice'),
669 + '_payment_type' => 'gateway',
670 + '_currency' => $currency,
671 + '_currency_symbol' => \EasyInvoice\Helpers\CurrencyHelper::getCurrencySymbol($currency),
672 + '_gateway_response' => wp_json_encode($gateway_response),
673 + ],
674 + ]);
675 +
676 + if (is_wp_error($payment_id) || !$payment_id) {
677 + $this->log('Failed to book PayPal payment for txn ' . $txn_id, 'error');
678 + return 0;
679 + }
680 + $this->log('PayPal payment record #' . $payment_id . ' booked for txn ' . $txn_id);
681 +
682 + return (int) $payment_id;
683 + }
560 684
561 685 }