From 7aed55d0e5716d840e9cfe01433b66bf0ddfc1c6 Mon Sep 17 00:00:00 2001 From: David Stone Date: Tue, 27 Oct 2026 20:17:27 -0600 Subject: [PATCH] wip: send payment_received after manual confirmation and email verification (GH#2055) --- .../class-payment-edit-admin-page.php | 2 + inc/managers/class-customer-manager.php | 27 +++++++++++++ inc/managers/class-payment-manager.php | 40 +++++++++++++++++++ 3 files changed, 69 insertions(+) diff --git a/inc/admin-pages/class-payment-edit-admin-page.php b/inc/admin-pages/class-payment-edit-admin-page.php index 5f3a9b712..b2fc6eb1b 100644 --- a/inc/admin-pages/class-payment-edit-admin-page.php +++ b/inc/admin-pages/class-payment-edit-admin-page.php @@ -1502,6 +1502,8 @@ public function handle_save(): bool { return false; } } + + \WP_Ultimo\Managers\Payment_Manager::get_instance()->maybe_trigger_payment_received($payment, $membership); } return $saved; diff --git a/inc/managers/class-customer-manager.php b/inc/managers/class-customer-manager.php index a8496441b..54cb9e828 100644 --- a/inc/managers/class-customer-manager.php +++ b/inc/managers/class-customer-manager.php @@ -14,6 +14,7 @@ use WP_Ultimo\Checkout\Checkout; use WP_Ultimo\Models\Customer; use WP_Ultimo\Database\Memberships\Membership_Status; +use WP_Ultimo\Database\Payments\Payment_Status; // Exit if accessed directly defined('ABSPATH') || exit; @@ -232,6 +233,30 @@ protected function maybe_send_verification_email(int $customer_id): void { $customer->send_verification_email(); } + /** + * Sends the deferred receipt for a free signup once the customer is verified. + * + * Paid and manual payments are skipped: they are confirmed by their own flows. + * + * @since 2.18.1 + * + * @param \WP_Ultimo\Models\Payment $payment The payment. + * @param \WP_Ultimo\Models\Membership $membership The membership. + * @return void + */ + protected function maybe_send_payment_received($payment, $membership): void { + + if (Payment_Status::COMPLETED !== $payment->get_status() || 0 < (float) $payment->get_total()) { + return; + } + + if ( ! in_array($membership->get_status(), [Membership_Status::ACTIVE, Membership_Status::TRIALING], true)) { + return; + } + + Payment_Manager::get_instance()->maybe_trigger_payment_received($payment, $membership); + } + /** * Verifies a customer by checking the email key. * @@ -317,6 +342,8 @@ public function maybe_verify_email_address(): void { $payments = $membership->get_payments(); if ($payments) { + $this->maybe_send_payment_received($payments[0], $membership); + $redirect_url = add_query_arg( [ 'payment' => $payments[0]->get_hash(), diff --git a/inc/managers/class-payment-manager.php b/inc/managers/class-payment-manager.php index b805b42f6..9da9ebcfa 100644 --- a/inc/managers/class-payment-manager.php +++ b/inc/managers/class-payment-manager.php @@ -217,6 +217,46 @@ public function handle_payment_success($payment, $membership, $gateway): void { } wu_do_event('payment_received', $payload); + + if ($payment->get_id()) { + $payment->update_meta('wu_payment_received_sent', 1); + } + } + + /** + * Sends the payment received notifications once per payment. + * + * Used by flows that confirm a payment outside a gateway callback, such as + * manual payment confirmation and deferred free signups after email verification. + * + * @since 2.18.1 + * + * @param \WP_Ultimo\Models\Payment $payment The payment. + * @param \WP_Ultimo\Models\Membership|null $membership The membership, if known. + * @return bool True when the notification was triggered now. + */ + public function maybe_trigger_payment_received($payment, $membership = null): bool { + + if ( ! $payment || ! $payment->get_id() || $payment->get_meta('wu_payment_received_sent')) { + return false; + } + + $gateway = wu_get_gateway($payment->get_gateway() ?: 'manual'); + + if ( ! $gateway) { + $gateway = wu_get_gateway('free'); + } + + if ( ! $gateway) { + return false; + } + + // Mark first so a re-entrant or repeated call cannot send twice. + $payment->update_meta('wu_payment_received_sent', 1); + + $gateway->trigger_payment_processed($payment, $membership); + + return true; } /**