File manager - Edit - /home/verseaumee/lauvia/wp-content/plugins/woocommerce-gateway-stripe/includes/compat/trait-wc-stripe-subscriptions.php
Back
<?php use Automattic\WooCommerce\Enums\OrderStatus; use Automattic\WooCommerce\Enums\PaymentGatewayFeature; if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Trait for Subscriptions compatibility. */ trait WC_Stripe_Subscriptions_Trait { use WC_Stripe_Subscriptions_Utilities_Trait; /** * Stores a flag to indicate if the subscription integration hooks have been attached. * * The callbacks attached as part of maybe_init_subscriptions() only need to be attached once to avoid duplication. * * @var bool False by default, true once the callbacks have been attached. */ private static $has_attached_integration_hooks = false; /** * Initialize subscription support and hooks. * * @return void * @since 5.6.0 */ public function maybe_init_subscriptions() { if ( ! WC_Stripe_Subscriptions_Helper::is_subscriptions_enabled() ) { return; } $this->supports = array_merge( $this->supports, [ PaymentGatewayFeature::SUBSCRIPTIONS, PaymentGatewayFeature::SUBSCRIPTION_CANCELLATION, PaymentGatewayFeature::SUBSCRIPTION_SUSPENSION, PaymentGatewayFeature::SUBSCRIPTION_REACTIVATION, PaymentGatewayFeature::SUBSCRIPTION_AMOUNT_CHANGES, PaymentGatewayFeature::SUBSCRIPTION_DATE_CHANGES, PaymentGatewayFeature::SUBSCRIPTION_PAYMENT_METHOD_CHANGE, PaymentGatewayFeature::SUBSCRIPTION_PAYMENT_METHOD_CHANGE_CUSTOMER, PaymentGatewayFeature::SUBSCRIPTION_PAYMENT_METHOD_CHANGE_ADMIN, PaymentGatewayFeature::MULTIPLE_SUBSCRIPTIONS, ] ); /** * We need to attach the callbacks below once per Gateway (CC, SEPA, etc.), but only once. * Therefore, we use a static flag at class level to indicate that they have been attached. */ if ( self::$has_attached_integration_hooks ) { return; } add_action( 'woocommerce_scheduled_subscription_payment_' . $this->id, [ $this, 'scheduled_subscription_payment' ], 10, 2 ); add_action( 'woocommerce_subscription_failing_payment_method_updated_' . $this->id, [ $this, 'update_failing_payment_method' ], 10, 2 ); add_action( 'wc_stripe_payment_fields_' . $this->id, [ $this, 'display_update_subs_payment_checkout' ] ); add_action( 'wc_stripe_add_payment_method_' . $this->id . '_success', [ $this, 'handle_add_payment_method_success' ], 10, 2 ); add_action( 'woocommerce_stripe_add_payment_method', [ $this, 'handle_upe_add_payment_method_success' ], 10, 2 ); // Display the payment method used for a subscription in the "My Subscriptions" table. add_filter( 'woocommerce_my_subscriptions_payment_method', [ $this, 'maybe_render_subscription_payment_method' ], 10, 2 ); // Allow store managers to manually set Stripe as the payment method on a subscription. add_filter( 'woocommerce_subscription_payment_meta', [ $this, 'add_subscription_payment_meta' ], 10, 2 ); // Validate the payment method meta data set on a subscription. add_action( 'woocommerce_subscription_validate_payment_meta', [ $this, 'validate_subscription_payment_meta' ], 10, 2 ); self::$has_attached_integration_hooks = true; /** * The callbacks attached below only need to be attached once. We don't need each gateway instance to have its own callback. * Therefore we only attach them once on the main `stripe` gateway and store a flag to indicate that they have been attached. */ if ( WC_Stripe_UPE_Payment_Gateway::ID !== $this->id ) { return; } // Secondary check to skip registration for the OC payment method, which mimics the Stripe ID. $current_class = get_class( $this ); if ( WC_Stripe_UPE_Payment_Gateway::class !== $current_class ) { return; } add_action( 'woocommerce_subscriptions_change_payment_before_submit', [ $this, 'differentiate_change_payment_method_form' ] ); add_action( 'wcs_resubscribe_order_created', [ $this, 'delete_resubscribe_meta' ], 10 ); // @phpstan-ignore return.void (Callers may be relying on the return value from delete_renewal_meta(), so we are keeping that in place.) add_action( 'wcs_renewal_order_created', [ $this, 'delete_renewal_meta' ], 10 ); add_filter( 'wc_stripe_display_save_payment_method_checkbox', [ $this, 'display_save_payment_method_checkbox' ] ); // Add the necessary information to create a mandate to the payment intent. add_filter( 'wc_stripe_generate_create_intent_request', [ $this, 'add_subscription_information_to_intent' ], 10, 4 ); /* * WC subscriptions hooks into the "template_redirect" hook with priority 100. * If the screen is "Pay for order" and the order is a subscription renewal, it redirects to the plain checkout. * See: https://github.com/woocommerce/woocommerce-subscriptions/blob/99a75687e109b64cbc07af6e5518458a6305f366/includes/class-wcs-cart-renewal.php#L165 * If we are in the "You just need to authorize SCA" flow, we don't want that redirection to happen. */ add_action( 'template_redirect', [ $this, 'remove_order_pay_var' ], 99 ); add_action( 'template_redirect', [ $this, 'restore_order_pay_var' ], 101 ); // Disable editing for Indian subscriptions with mandates. Those need to be recreated as mandates does not support upgrades (due fixed amounts). add_filter( 'wc_order_is_editable', [ $this, 'disable_subscription_edit_for_india' ], 10, 2 ); add_filter( 'woocommerce_subscriptions_update_payment_via_pay_shortcode', [ $this, 'update_payment_after_deferred_intent' ], 10, 3 ); } /** * Displays a checkbox to allow users to update all subs payments with new * payment. * * @return void * @since 4.1.11 */ public function display_update_subs_payment_checkout() { $statuses = apply_filters( 'wc_stripe_update_subs_payment_method_card_statuses', [ 'active' ] ); if ( ! apply_filters( 'wc_stripe_display_update_subs_payment_method_card_checkbox', true ) ) { return; } if ( ! is_add_payment_method_page() ) { return; } if ( function_exists( 'wcs_user_has_subscription' ) && wcs_user_has_subscription( get_current_user_id(), '', $statuses ) ) { $label = esc_html( apply_filters( 'wc_stripe_save_to_subs_text', __( 'Update the payment method for all of my current subscriptions', 'woocommerce-gateway-stripe' ) ) ); $id = sprintf( 'wc-%1$s-update-subs-payment-method-card', $this->id ); woocommerce_form_field( $id, [ 'type' => 'checkbox', 'label' => $label, 'default' => apply_filters( 'wc_stripe_save_to_subs_checked', false ), 'input_class' => [ 'wc-stripe-update-all-subscriptions-payment-method' ], ] ); } } /** * Updates all active subscriptions payment method. * * Note: This is the Legacy checkout experience method for updating subscriptions payment method. * * @since 4.1.11 * * @see handle_upe_add_payment_method_success() for the new UPE checkout method. * * @param string $source_id The source ID. * @param object $source_object The source object. * @return void */ public function handle_add_payment_method_success( $source_id, $source_object ) { $this->handle_upe_add_payment_method_success( get_current_user_id(), $source_object ); } /** * Updates all the user's active subscriptions payment method with the new payment method. * * @since 8.8.0 * * @param int $user_id The user ID. * @param stdClass $payment_method_object The newly added payment method object. * @return void */ public function handle_upe_add_payment_method_success( $user_id, $payment_method_object ) { // To avoid errors, exit early if there is no WC_Subscriptions_Change_Payment_Gateway class or the payment method object is not complete. if ( ! class_exists( 'WC_Subscriptions_Change_Payment_Gateway' ) || ! isset( $payment_method_object->id ) ) { return; } // Check if the customer has requested to update all subscriptions via a direct request or after returning from the UPE redirect. $should_update_subscriptions = isset( $_POST[ 'wc-' . $this->id . '-update-subs-payment-method-card' ] ); $should_update_subscriptions = $should_update_subscriptions || isset( $this->stripe_id, $_GET[ "wc-stripe-{$this->stripe_id}-update-all-subscription-payment-methods" ] ); if ( ! $should_update_subscriptions ) { return; } $statuses = apply_filters( 'wc_stripe_update_subs_payment_method_card_statuses', [ 'active' ] ); $subscriptions = function_exists( 'wcs_get_users_subscriptions' ) ? wcs_get_users_subscriptions( $user_id ) : []; $stripe_customer = new WC_Stripe_Customer( $user_id ); foreach ( $subscriptions as $subscription ) { if ( ! $subscription->has_status( $statuses ) ) { continue; } if ( ! current_user_can( 'edit_shop_subscription_payment_method', $subscription->get_id() ) ) { continue; } WC_Subscriptions_Change_Payment_Gateway::update_payment_method( $subscription, $this->id, [ 'post_meta' => [ '_stripe_source_id' => [ 'value' => $payment_method_object->id ], '_stripe_customer_id' => [ 'value' => $stripe_customer->get_id() ], ], ] ); } } /** * Render a dummy element in the "Change payment method" form (that does not appear in the "Pay for order" form) * which can be checked to determine proper SCA handling to apply for each form. * * @return void * @since 4.6.1 */ public function differentiate_change_payment_method_form() { echo '<input type="hidden" id="wc-stripe-change-payment-method" />'; } /** * Maybe process payment method change for subscriptions. * * @since 5.6.0 * * @param int $order_id * @return bool */ public function maybe_change_subscription_payment_method( $order_id ) { return ( WC_Stripe_Subscriptions_Helper::is_subscriptions_enabled() && $this->has_subscription( $order_id ) && $this->is_changing_payment_method_for_subscription() ); } /** * Process the payment method change for subscriptions. * * @since 5.6.0 * * @param int $order_id * @return array|null */ public function process_change_subscription_payment_method( $order_id ) { try { $subscription = wc_get_order( $order_id ); $prepared_source = $this->prepare_source( get_current_user_id(), true ); $this->maybe_disallow_prepaid_card( $prepared_source->source_object ); $this->check_source( $prepared_source ); $this->save_source_to_order( $subscription, $prepared_source ); do_action( 'wc_stripe_change_subs_payment_method_success', $prepared_source->source, $prepared_source ); return [ 'result' => 'success', 'redirect' => $this->get_return_url( $subscription ), ]; } catch ( WC_Stripe_Exception $e ) { wc_add_notice( $e->getLocalizedMessage(), 'error' ); WC_Stripe_Logger::error( 'Error processing change subscription payment method for subscription: ' . $order_id, [ 'error_message' => $e->getMessage() ] ); } } /** * Process the payment method change with deferred intent. * * @param int $subscription_id * * @return array */ public function process_change_subscription_payment_with_deferred_intent( $subscription_id ) { $subscription = wcs_get_subscription( $subscription_id ); if ( ! $subscription ) { return [ 'result' => 'failure', 'redirect' => '', ]; } try { $payment_information = $this->prepare_payment_information_from_request( $subscription ); $this->validate_selected_payment_method_type( $payment_information, $subscription->get_billing_country() ); $payment_method_id = $payment_information['payment_method']; $selected_payment_type = $payment_information['selected_payment_type']; $upe_payment_method = $this->payment_methods[ $selected_payment_type ] ?? null; // Retrieve the payment method object from Stripe. $payment_method = $this->stripe_request( 'payment_methods/' . $payment_method_id ); // Throw an exception when the payment method is a prepaid card and it's disallowed. $this->maybe_disallow_prepaid_card( $payment_method ); // Create a setup intent, or update an existing one associated with the order. $payment_intent = $this->process_setup_intent_for_order( $subscription, $payment_information ); // Handle saving the payment method in the store. if ( $payment_information['save_payment_method_to_store'] && $upe_payment_method && $upe_payment_method->get_id() === $upe_payment_method->get_retrievable_type() ) { $this->handle_saving_payment_method( $subscription, $payment_information['payment_method_details'], $selected_payment_type ); } $redirect = $this->get_return_url( $subscription ); $new_payment_method = $this->get_upe_gateway_id_for_order( $upe_payment_method ); // If the payment intent requires confirmation or action, redirect the customer to confirm the intent. if ( in_array( $payment_intent->status, WC_Stripe_Intent_Status::REQUIRES_CONFIRMATION_OR_ACTION_STATUSES, true ) ) { // Because we're filtering woocommerce_subscriptions_update_payment_via_pay_shortcode, we need to manually set this delayed update all flag here. if ( isset( $_POST['update_all_subscriptions_payment_method'] ) && wc_clean( wp_unslash( $_POST['update_all_subscriptions_payment_method'] ) ) ) { $subscription->update_meta_data( '_delayed_update_payment_method_all', $new_payment_method ); $subscription->save(); } wp_safe_redirect( $this->get_redirect_url( $redirect, $payment_intent, $payment_information, $subscription, false ) ); exit; } else { // Update the payment method for the subscription. WC_Subscriptions_Change_Payment_Gateway::update_payment_method( $subscription, $new_payment_method ); // Attach the new payment method ID and the customer ID to the subscription on success. $this->set_payment_method_id_for_subscription( $subscription, $payment_method_id ); $this->set_customer_id_for_subscription( $subscription, $payment_information['customer'] ); // Trigger wc_stripe_change_subs_payment_method_success action hook to preserve backwards compatibility, see process_change_subscription_payment_method(). do_action( 'wc_stripe_change_subs_payment_method_success', $payment_information['payment_method'], (object) [ 'token_id' => false !== $payment_information['token'] ? $payment_information['token']->get_id() : false, 'customer' => $payment_information['customer'], 'source' => null, 'source_object' => $payment_method, 'payment_method' => $payment_information['payment_method'], ] ); // Because this new payment does not require action/confirmation, remove this filter so that WC_Subscriptions_Change_Payment_Gateway proceeds to update all subscriptions if flagged. remove_filter( 'woocommerce_subscriptions_update_payment_via_pay_shortcode', [ $this, 'update_payment_after_deferred_intent' ], 10 ); } return [ 'result' => 'success', 'redirect' => $redirect, ]; } catch ( WC_Stripe_Exception $e ) { wc_add_notice( $e->getLocalizedMessage(), 'error' ); WC_Stripe_Logger::error( 'Error processing change subscription payment method with deferred intent for subscription: ' . $subscription_id, [ 'error_message' => $e->getMessage() ] ); return [ 'result' => 'failure', 'redirect' => '', ]; } } /** * Process a scheduled subscription payment, * most commonly via the 'woocommerce_scheduled_subscription_payment_<payment_method>' action. * * @param float $amount_to_charge The amount to charge. * @param WC_Order $renewal_order A WC_Order object created to record the renewal payment. * @return void */ public function scheduled_subscription_payment( $amount_to_charge, $renewal_order ) { $this->process_subscription_payment( $amount_to_charge, $renewal_order, true, false ); } /** * Determines whether a failed payment was blocked by Stripe Radar. * * When Stripe Radar blocks a charge it still creates a charge object (for audit * purposes) with outcome.type === 'blocked'. The charge ID is surfaced in the * error response so we can fetch it and inspect the outcome. * * @param object $response The Stripe API response containing the error. * @return string|false The Radar block reason (e.g. 'highest_risk_level', 'rule') when the * charge is blocked, false otherwise. */ protected function is_charge_blocked_by_radar( $response ) { $charge_id = null; // For both the Charges API (e.g. SEPA) and the Payment Intents API, the // charge ID appears at error.charge when a charge was actually attempted. if ( ! empty( $response->error->charge ) && is_string( $response->error->charge ) ) { $charge_id = $response->error->charge; } elseif ( ! empty( $response->error->payment_intent->latest_charge ) && is_string( $response->error->payment_intent->latest_charge ) ) { // Fallback: Payment Intents API may surface the charge ID via latest_charge. $charge_id = $response->error->payment_intent->latest_charge; } if ( empty( $charge_id ) ) { return false; } $charge = WC_Stripe_API::retrieve( "charges/{$charge_id}" ); if ( is_wp_error( $charge ) || empty( $charge ) || ! empty( $charge->error ) ) { return false; } if ( isset( $charge->outcome->type ) && 'blocked' === $charge->outcome->type ) { return isset( $charge->outcome->reason ) ? (string) $charge->outcome->reason : 'unknown'; } return false; } /** * Process a payment for a subscription renewal. * * @since 3.0 * @since 4.0.4 Add third parameter flag to retry. * @since 4.1.0 Add fourth parameter to log previous errors. * @since 5.6.0 Process renewal payments for SEPA and UPE. * * @param float $amount The amount to charge. * @param WC_Order $renewal_order The renewal order. * @param bool $retry Should we retry the process? * @param object|false $previous_error Previous error object. * @return void */ public function process_subscription_payment( $amount, $renewal_order, $retry = true, $previous_error = false ) { $order_locked = false; $radar_reason = false; try { $order_id = $renewal_order->get_id(); // Check for an existing intent, which is associated with the order. if ( $this->has_authentication_already_failed( $renewal_order ) ) { return; } // Get source from order $prepared_source = $this->prepare_order_source( $renewal_order ); $source_object = $prepared_source->source_object; if ( ! $prepared_source->customer ) { throw new WC_Stripe_Exception( 'Failed to process renewal for order ' . $renewal_order->get_id() . '. Stripe customer id is missing in the order', __( 'Customer not found', 'woocommerce-gateway-stripe' ) ); } WC_Stripe_Logger::debug( "Begin processing subscription payment for order {$order_id} for the amount of {$amount}", [ 'order_id' => $order_id, 'amount' => $amount, ] ); /* * If we're doing a retry and source is chargeable, we need to pass * a different idempotency key and retry for success. */ if ( is_object( $source_object ) && empty( $source_object->error ) && $this->need_update_idempotency_key( $source_object, $previous_error ) ) { add_filter( 'wc_stripe_idempotency_key', [ $this, 'change_idempotency_key' ], 10, 2 ); } if ( ( $this->is_no_such_source_error( $previous_error ) || $this->is_no_linked_source_error( $previous_error ) ) && apply_filters( 'wc_stripe_use_default_customer_source', true ) ) { // Passing empty source will charge customer default. $prepared_source->source = ''; } $order_helper = WC_Stripe_Order_Helper::get_instance(); // If the payment gateway is SEPA, use the charges API. // TODO: Remove when SEPA is migrated to payment intents. if ( 'stripe_sepa' === $this->id ) { $request = $this->generate_payment_request( $renewal_order, $prepared_source ); $request['capture'] = 'true'; $request['amount'] = WC_Stripe_Helper::get_stripe_amount( $amount, $request['currency'] ); $response = WC_Stripe_API::request( $request ); $is_authentication_required = false; } else { $order_helper->lock_order_payment( $renewal_order ); $order_locked = true; $response = $this->create_and_confirm_intent_for_off_session( $renewal_order, $prepared_source, $amount ); $is_authentication_required = $this->is_authentication_required_for_payment( $response ); } // It's only a failed payment if it's an error and it's not of the type 'authentication_required'. // If it's 'authentication_required', then we should email the user and ask them to authenticate. if ( ! empty( $response->error ) && ! $is_authentication_required ) { // Compute once here so the catch block can reuse the result without a second API call. $radar_reason = $this->is_charge_blocked_by_radar( $response ); // We want to retry — unless Stripe Radar blocked the charge, in which case retrying // would just create another blocked charge and inflate the block rate. if ( $this->is_retryable_error( $response->error ) && false === $radar_reason ) { if ( $retry ) { // Don't do anymore retries after this. if ( 5 <= $this->retry_interval ) { // @phpstan-ignore-line (retry_interval is defined in classes using this class) $this->process_subscription_payment( $amount, $renewal_order, false, $response->error ); return; } sleep( $this->retry_interval ); ++$this->retry_interval; $this->process_subscription_payment( $amount, $renewal_order, true, $response->error ); return; } else { $localized_message = sprintf( /* translators: 1) error message from Stripe; 2) request log URL */ __( 'Sorry, we are unable to process the payment at this time. Reason: %1$s %2$s', 'woocommerce-gateway-stripe' ), $response->error->message, isset( $response->error->request_log_url ) ? make_clickable( $response->error->request_log_url ) : '' ); $renewal_order->add_order_note( $localized_message ); throw new WC_Stripe_Exception( print_r( $response, true ), $localized_message ); } } if ( 'payment_intent_mandate_invalid' === $response->error->type ) { $localized_message = __( 'The mandate used for this renewal payment is invalid. You may need to bring the customer back to your store and ask them to resubmit their payment information.', 'woocommerce-gateway-stripe' ); } else { $localized_message = WC_Stripe_Helper::get_localized_error_message_from_response( $response ); } if ( isset( $response->error->request_log_url ) ) { $localized_message .= ' ' . make_clickable( $response->error->request_log_url ); } $renewal_order->add_order_note( $localized_message ); throw new WC_Stripe_Exception( print_r( $response, true ), $localized_message ); } } catch ( WC_Stripe_Exception $e ) { WC_Stripe_Logger::error( 'Error processing subscription renewal payment: ' . $e->getMessage(), [ 'order_id' => $renewal_order->get_id(), 'amount' => $amount, 'error_message' => $e->getMessage(), 'localized_message' => $e->getLocalizedMessage(), ] ); do_action( 'wc_gateway_stripe_process_payment_error', $e, $renewal_order ); $renewal_order->update_status( OrderStatus::FAILED ); // If the payment was blocked by Stripe Radar, cancel any scheduled // retry attempt. Without this, WC Subscriptions schedules a retry // that would create another charge for Radar to block, inflating // the block rate. if ( false !== $radar_reason ) { $radar_cause = ''; switch ( $radar_reason ) { case 'rule': $radar_cause = __( 'Stripe Radar blocked payment for the saved payment method due to a custom Radar rule.', 'woocommerce-gateway-stripe' ); break; case 'low_probability_of_authorization': $radar_cause = __( 'Stripe blocked payment for the saved payment method due to low probability of authorization.', 'woocommerce-gateway-stripe' ); break; case 'highest_risk_level': $radar_cause = __( 'Stripe Radar blocked payment for the saved payment method as high risk.', 'woocommerce-gateway-stripe' ); break; default: $radar_cause = sprintf( /* translators: %s is the Stripe Radar reason code returned by the API. */ __( 'Stripe Radar blocked payment for the saved payment method (reason: %s).', 'woocommerce-gateway-stripe' ), $radar_reason ); break; } $retry_cancelled_suffix = __( 'The automatic retry has been cancelled to prevent further blocked payment attempts.', 'woocommerce-gateway-stripe' ); try { $subscriptions = function_exists( 'wcs_get_subscriptions_for_renewal_order' ) ? wcs_get_subscriptions_for_renewal_order( $renewal_order ) : []; $retry_cancelled = false; if ( class_exists( 'WCS_Retry_Manager' ) && method_exists( 'WCS_Retry_Manager', 'is_retry_enabled' ) && WCS_Retry_Manager::is_retry_enabled() && method_exists( 'WCS_Retry_Manager', 'store' ) ) { $retry_store = WCS_Retry_Manager::store(); $last_retry = method_exists( $retry_store, 'get_last_retry_for_order' ) ? $retry_store->get_last_retry_for_order( $renewal_order->get_id() ) : null; if ( $last_retry && 'pending' === $last_retry->get_status() ) { $last_retry->update_status( 'cancelled' ); $retry_cancelled = true; } foreach ( $subscriptions as $subscription ) { if ( $subscription->get_date( 'payment_retry' ) > 0 ) { $subscription->delete_date( 'payment_retry' ); $retry_cancelled = true; } } } $radar_note = $retry_cancelled ? $radar_cause . ' ' . $retry_cancelled_suffix : $radar_cause; foreach ( $subscriptions as $subscription ) { $subscription->add_order_note( $radar_note ); } $renewal_order->add_order_note( $radar_note ); } catch ( Exception $radar_e ) { WC_Stripe_Logger::error( 'Failed to cancel scheduled retry after Stripe Radar blocked subscription renewal: ' . $radar_e->getMessage(), [ 'order_id' => $renewal_order->get_id() ] ); } } if ( $order_locked && isset( $order_helper ) ) { $order_helper->unlock_order_payment( $renewal_order ); $order_locked = false; } return; } try { // Either the charge was successfully captured, or it requires further authentication. if ( $is_authentication_required ) { do_action( 'wc_gateway_stripe_process_payment_authentication_required', $renewal_order, $response ); $error_message = __( 'This transaction requires authentication.', 'woocommerce-gateway-stripe' ); $renewal_order->add_order_note( $error_message ); $charge = $this->get_latest_charge_from_intent( $response->error->payment_intent ); $id = $charge->id; $renewal_order->set_transaction_id( $id ); /* translators: %s is the charge Id */ $renewal_order->update_status( OrderStatus::FAILED, sprintf( __( 'Stripe charge awaiting authentication by user: %s.', 'woocommerce-gateway-stripe' ), $id ) ); if ( is_callable( [ $renewal_order, 'save' ] ) ) { $renewal_order->save(); } } elseif ( $this->is_charge_attempt_delayed( $response ) ) { $charge_attempt_at = $response->processing->card->customer_notification->completes_at; $attempt_date = wp_date( get_option( 'date_format', 'F j, Y' ), $charge_attempt_at, wp_timezone() ); $attempt_time = wp_date( get_option( 'time_format', 'g:i a' ), $charge_attempt_at, wp_timezone() ); $message = ''; if ( ! empty( $response->processing->card->customer_notification->approval_requested ) ) { $message = sprintf( /* translators: 1) a date in the format yyyy-mm-dd, e.g. 2021-09-21; 2) time in the 24-hour format HH:mm, e.g. 23:04 */ __( 'The customer must authorize this payment via the pre-debit notification sent to them by their card issuing bank, before %1$s at %2$s, when the charge will be attempted.', 'woocommerce-gateway-stripe' ), $attempt_date, $attempt_time ); } else { $message = sprintf( /* translators: 1) a date in the format yyyy-mm-dd, e.g. 2021-09-21; 2) time in the 24-hour format HH:mm, e.g. 23:04 */ __( 'The charge will be attempted on %1$s at %2$s.', 'woocommerce-gateway-stripe' ), $attempt_date, $attempt_time ); } $renewal_order->add_order_note( $message ); $renewal_order->update_status( OrderStatus::PENDING ); if ( is_callable( [ $renewal_order, 'save' ] ) ) { $renewal_order->save(); } do_action( 'wc_gateway_stripe_process_payment_subscription_charge_attempt_delayed', $response, $renewal_order ); } else { // The charge was successfully captured do_action_deprecated( 'wc_gateway_stripe_process_payment', [ $response, $renewal_order ], '9.7.0', 'wc_gateway_stripe_process_payment_charge', 'The wc_gateway_stripe_process_payment action is deprecated. Use wc_gateway_stripe_process_payment_charge instead.' ); // Use the last charge within the intent or the full response body in case of SEPA. $latest_charge = $this->get_latest_charge_from_intent( $response ); $this->process_response( ( ! empty( $latest_charge ) ) ? $latest_charge : $response, $renewal_order ); } } catch ( WC_Stripe_Exception $e ) { WC_Stripe_Logger::error( 'Error processing subscription renewal payment: ' . $e->getMessage(), [ 'order_id' => $renewal_order->get_id(), 'amount' => $amount, 'error_message' => $e->getMessage(), 'localized_message' => $e->getLocalizedMessage(), ] ); do_action( 'wc_gateway_stripe_process_payment_error', $e, $renewal_order ); } if ( $order_locked && isset( $order_helper ) ) { $order_helper->unlock_order_payment( $renewal_order ); } } /** * Updates other subscription sources. * * @since 5.6.0 * * @param WC_Order $order The order object. * @param stdClass $source The source object. * @param string $payment_gateway_id The payment method ID. eg 'stripe'. * @return void */ public function maybe_update_source_on_subscription_order( $order, $source, $payment_gateway_id = '' ) { if ( ! WC_Stripe_Subscriptions_Helper::is_subscriptions_enabled() ) { return; } $order_id = $order->get_id(); // Also store it on the subscriptions being purchased or paid for in the order if ( function_exists( 'wcs_order_contains_subscription' ) && wcs_order_contains_subscription( $order_id ) ) { $subscriptions = function_exists( 'wcs_get_subscriptions_for_order' ) ? wcs_get_subscriptions_for_order( $order_id ) : []; } elseif ( function_exists( 'wcs_order_contains_renewal' ) && wcs_order_contains_renewal( $order_id ) ) { $subscriptions = function_exists( 'wcs_get_subscriptions_for_renewal_order' ) ? wcs_get_subscriptions_for_renewal_order( $order_id ) : []; } else { $subscriptions = []; } $order_helper = WC_Stripe_Order_Helper::get_instance(); foreach ( $subscriptions as $subscription ) { $order_helper->update_stripe_customer_id( $subscription, $source->customer ); if ( ! empty( $source->payment_method ) ) { $order_helper->update_stripe_source_id( $subscription, $source->payment_method ); } else { $order_helper->update_stripe_source_id( $subscription, $source->source ); } // Update the payment method. if ( ! empty( $payment_gateway_id ) ) { $subscription->set_payment_method( $payment_gateway_id ); } $subscription->save(); } } /** * Don't transfer Stripe customer/token meta to resubscribe orders. * * @param WC_Order $resubscribe_order The order created for the customer to resubscribe to the old expired/cancelled subscription * @return void */ public function delete_resubscribe_meta( $resubscribe_order ) { if ( ! $resubscribe_order instanceof WC_Order ) { return; } $order_helper = WC_Stripe_Order_Helper::get_instance(); $order_helper->delete_stripe_source_id( $resubscribe_order ); $order_helper->delete_stripe_customer_id( $resubscribe_order ); // For BW compat will remove in future. $order_helper->delete_stripe_card_id( $resubscribe_order ); // Delete payment intent ID. $order_helper->delete_stripe_intent_id( $resubscribe_order ); $this->delete_renewal_meta( $resubscribe_order ); $resubscribe_order->save(); } /** * Don't transfer Stripe fee/ID meta to renewal orders. * * @param WC_Order|null $renewal_order The renewal order. * @return WC_Order|null The renewal order. */ public function delete_renewal_meta( $renewal_order ) { if ( ! $renewal_order instanceof WC_Order ) { return $renewal_order; } $order_helper = WC_Stripe_Order_Helper::get_instance(); $order_helper->delete_stripe_fee( $renewal_order ); $order_helper->delete_stripe_net( $renewal_order ); // Delete payment intent ID. $order_helper->delete_stripe_intent_id( $renewal_order ); return $renewal_order; } /** * Update the customer_id for a subscription after using Stripe to complete a payment to make up for * an automatic renewal payment which previously failed. * * @param WC_Subscription $subscription The subscription for which the failing payment method relates. * @param WC_Order $renewal_order The order which recorded the successful payment (to make up for the failed automatic payment). * @return void */ public function update_failing_payment_method( $subscription, $renewal_order ) { if ( ! $subscription instanceof WC_Subscription ) { return; } if ( ! $renewal_order instanceof WC_Order ) { return; } $order_helper = WC_Stripe_Order_Helper::get_instance(); $stripe_customer_id = $order_helper->get_stripe_customer_id( $renewal_order ); $stripe_source_id = $order_helper->get_stripe_source_id( $renewal_order ); $order_helper->update_stripe_customer_id( $subscription, $stripe_customer_id ? $stripe_customer_id : '' ); $order_helper->update_stripe_source_id( $subscription, $stripe_source_id ? $stripe_source_id : '' ); $subscription->save(); } /** * Include the payment meta data required to process automatic recurring payments so that store managers can * manually set up automatic recurring payments for a customer via the Edit Subscriptions screen in 2.0+. * * @since 2.5 * * @param array $payment_meta associative array of meta data required for automatic payments * @param WC_Subscription $subscription An instance of a subscription object * @return array */ public function add_subscription_payment_meta( $payment_meta, $subscription ) { $subscription_id = $subscription->get_id(); $order_helper = WC_Stripe_Order_Helper::get_instance(); $source_id = $order_helper->get_stripe_source_id( $subscription ); // For BW compat will remove in future. if ( empty( $source_id ) ) { $source_id = $order_helper->get_stripe_card_id( $subscription ); // Take this opportunity to update the key name. $order_helper->update_stripe_source_id( $subscription, $source_id ? $source_id : '' ); $order_helper->delete_stripe_card_id( $subscription ); $subscription->save(); } $payment_meta[ $this->id ] = [ 'post_meta' => [ '_stripe_customer_id' => [ 'value' => $order_helper->get_stripe_customer_id( $subscription ), 'label' => 'Stripe Customer ID', ], '_stripe_source_id' => [ 'value' => $source_id, 'label' => 'Stripe Payment Method ID', ], ], ]; return $payment_meta; } /** * Validate the payment meta data required to process automatic recurring payments so that store managers can * manually set up automatic recurring payments for a customer via the Edit Subscriptions screen in 2.0+. * * @since 2.5 * @since 4.0.4 Stripe sourd id field no longer needs to be required. * * @param string $payment_method_id The ID of the payment method to validate * @param array $payment_meta associative array of meta data required for automatic payments * @return void */ public function validate_subscription_payment_meta( $payment_method_id, $payment_meta ) { if ( $this->id === $payment_method_id ) { if ( ! isset( $payment_meta['post_meta']['_stripe_customer_id']['value'] ) || empty( $payment_meta['post_meta']['_stripe_customer_id']['value'] ) ) { // Allow empty stripe customer id during subscription renewal. It will be added when processing payment if required. if ( ! isset( $_POST['wc_order_action'] ) || 'wcs_process_renewal' !== $_POST['wc_order_action'] ) { throw new Exception( __( 'A "Stripe Customer ID" value is required.', 'woocommerce-gateway-stripe' ) ); } } elseif ( 0 !== strpos( $payment_meta['post_meta']['_stripe_customer_id']['value'], 'cus_' ) ) { throw new Exception( __( 'Invalid customer ID. A valid "Stripe Customer ID" must begin with "cus_".', 'woocommerce-gateway-stripe' ) ); } if ( ! empty( $payment_meta['post_meta']['_stripe_source_id']['value'] ) && ( 0 !== strpos( $payment_meta['post_meta']['_stripe_source_id']['value'], 'card_' ) && 0 !== strpos( $payment_meta['post_meta']['_stripe_source_id']['value'], 'src_' ) && 0 !== strpos( $payment_meta['post_meta']['_stripe_source_id']['value'], 'pm_' ) ) ) { throw new Exception( __( 'Invalid payment method ID. A valid "Stripe Payment Method ID" must begin with "src_", "pm_", or "card_".', 'woocommerce-gateway-stripe' ) ); } } } /** * Add the necessary information to payment intents for subscriptions to allow Stripe to create * mandates for 3DS payments in India. It's ok to apply this across the board; Stripe will * take care of handling any authorizations. * * @param array $request The HTTP request that will be sent to Stripe to create the payment intent. * @param WC_Order $order The renewal order. * @param object $prepared_source The source object. * @param bool $is_setup_intent Whether this is a setup intent. * @return array */ public function add_subscription_information_to_intent( $request, $order, $prepared_source, $is_setup_intent = false ) { // Just in case the order doesn't contain a subscription we return the base request. if ( ! $this->has_subscription( $order->get_id() ) ) { return $request; } $subscriptions_for_renewal_order = []; // Check if this is not a subscription switch. When switching we will force the creation of mandates to update the amount if ( ! WC_Subscriptions_Switcher::cart_contains_switches() ) { // TODO: maybe this isn't necessary since this function should really only be called // when creating the intent? It's called in process_subscription_payment though // so it's probably needed here too? // If we've already created a mandate for this order; use that. $mandate = WC_Stripe_Order_Helper::get_instance()->get_stripe_mandate_id( $order ); if ( isset( $request['confirm'] ) && filter_var( $request['confirm'], FILTER_VALIDATE_BOOLEAN ) && ! empty( $mandate ) ) { $request['mandate'] = $mandate; // We already have a mandate -- unset mandate_data and setup_future_usage, if set. unset( $request['mandate_data'] ); unset( $request['setup_future_usage'] ); return $request; } $subscriptions_for_renewal_order = function_exists( 'wcs_get_subscriptions_for_renewal_order' ) ? wcs_get_subscriptions_for_renewal_order( $order ) : []; // Check if mandate already exists. if ( 1 === count( $subscriptions_for_renewal_order ) ) { $subscription_order = reset( $subscriptions_for_renewal_order ); $mandate = $this->get_mandate_for_subscription( $subscription_order, isset( $request['payment_method'] ) ? $request['payment_method'] : '' ); if ( ! empty( $mandate ) ) { $request['confirm'] = 'true'; $request['mandate'] = $mandate; // We already have a mandate -- unset mandate_data and setup_future_usage, if set. unset( $request['mandate_data'] ); unset( $request['setup_future_usage'] ); return $request; } } } // Add mandate options to request to create new mandate if mandate id does not already exist in a previous renewal or parent order. // Note: This is for backwards compatibility if `_stripe_mandate_id` is not set. $mandate_options = $this->create_mandate_options_for_order( $order, $subscriptions_for_renewal_order, $is_setup_intent ); if ( ! empty( $mandate_options ) ) { if ( ! isset( $request['payment_method_options']['card']['mandate_options'] ) ) { $request['payment_method_options']['card']['mandate_options'] = []; } $request['payment_method_options']['card']['mandate_options'] = array_merge( $request['payment_method_options']['card']['mandate_options'], $mandate_options ); } return $request; } /** * Find the mandate id for a subscription renewal from a previous renewal order. Return the mandate id * if it exists and the amount matches the renewal order amount, return empty otherwise to indicate that a * new mandate should be created. * * @param WC_Order $order The subscription order. * @param string $payment_method The payment method ID. * @return string The mandate id or empty string if no valid mandate id is found. */ private function get_mandate_for_subscription( $order, $payment_method ) { $renewal_order_ids = $order->get_related_orders( 'ids' ); foreach ( $renewal_order_ids as $renewal_order_id ) { $renewal_order = wc_get_order( $renewal_order_id ); if ( ! $renewal_order instanceof WC_Order ) { continue; } $order_helper = WC_Stripe_Order_Helper::get_instance(); $mandate = $order_helper->get_stripe_mandate_id( $renewal_order ); $renewal_order_payment_method = $order_helper->get_stripe_source_id( $renewal_order ); // Return from the most recent renewal order with a valid mandate. Mandate is created against a payment method // in Stripe so the payment method should also match to reuse the mandate. if ( ! empty( $mandate ) && $renewal_order_payment_method === $payment_method ) { return $mandate; } } return ''; } /** * Create mandate options for a subscription order to be added to the payment intent request. * * @param WC_Order $order The renewal order. * @param WC_Subscription[] $subscriptions Subscriptions for the renewal order. * @param bool $is_setup_intent Whether the intent is a setup intent. * @return array the mandate_options for the subscription order. */ private function create_mandate_options_for_order( $order, $subscriptions, $is_setup_intent = false ) { $mandate_options = []; $currency = strtolower( $order->get_currency() ); // We don't need to add mandate options if the currency is not supported for Indian recurring payment mandates. if ( ! WC_Stripe_Helper::is_currency_supported_for_indian_recurring_payment_mandate( $currency ) ) { return []; } $sub_amount = 0; $cart_contain_switches = WC_Subscriptions_Switcher::cart_contains_switches(); $is_changing_payment_method = $this->is_changing_payment_method_for_subscription(); // If this is a switch order we set the mandate options based on the new subscription. if ( $cart_contain_switches ) { foreach ( WC()->cart->cart_contents as $cart_item ) { $subscription_price = WC_Subscriptions_Product::get_price( $cart_item['data'] ); $sub_amount += (int) WC_Stripe_Helper::get_stripe_amount( $subscription_price, $currency ); } // Get the first cart item associated with this order. $cart_item = reset( WC()->cart->cart_contents ); $sub_billing_period = WC_Subscriptions_Product::get_period( $cart_item['data'] ); $sub_billing_interval = absint( WC_Subscriptions_Product::get_interval( $cart_item['data'] ) ); } elseif ( $is_changing_payment_method ) { // On the change payment method page, the $order object sent in this function is actually a subscription. $subscription = $order; $sub_amount = WC_Stripe_Helper::get_stripe_amount( $subscription->get_subtotal(), $currency ); $sub_billing_period = strtolower( $subscription->get_billing_period() ); $sub_billing_interval = $subscription->get_billing_interval(); } else { // If this is the first order, not a renewal, then get the subscriptions for the parent order. if ( empty( $subscriptions ) ) { $subscriptions = function_exists( 'wcs_get_subscriptions_for_order' ) ? wcs_get_subscriptions_for_order( $order ) : []; } // If there are no subscriptions we just return since mandates aren't required. if ( 0 === count( $subscriptions ) ) { return []; } foreach ( $subscriptions as $sub ) { $sub_amount += WC_Stripe_Helper::get_stripe_amount( $sub->get_total(), $currency ); } // If the total amount is 0 and it's a setup intent, then calculate the amount from the subscription subtotal. // The total could be 0 if the subscription has a free trial or a coupon is used. if ( 0 === $sub_amount && $is_setup_intent ) { foreach ( $subscriptions as $sub ) { $sub_amount += WC_Stripe_Helper::get_stripe_amount( $sub->get_subtotal(), $currency ); } } // Get the first subscription associated with this order. $sub = reset( $subscriptions ); $sub_billing_period = strtolower( $sub->get_billing_period() ); $sub_billing_interval = $sub->get_billing_interval(); } // If the amount is 0 we don't need to create a mandate since we won't be charging anything. // And there won't be any renewal for this free subscription. if ( 0 === $sub_amount ) { return []; } $has_interval = $sub_billing_period && $sub_billing_interval > 0; if ( $has_interval && ( 1 === count( $subscriptions ) || $cart_contain_switches || $is_changing_payment_method ) ) { $mandate_options['amount_type'] = 'fixed'; $mandate_options['interval'] = $sub_billing_period; $mandate_options['interval_count'] = $sub_billing_interval; } else { // If there are multiple subscriptions the amount_type becomes 'maximum' so we can charge anything // less than the order total, and the interval is sporadic so we don't have to follow a set interval. $mandate_options['amount_type'] = 'maximum'; $mandate_options['interval'] = 'sporadic'; } // Currency is required for mandate options when creating a setup intent for card payment methods. if ( $is_setup_intent ) { $mandate_options['currency'] = $currency; } $mandate_options['amount'] = $sub_amount; $mandate_options['reference'] = $order->get_id(); $mandate_options['start_date'] = time(); $mandate_options['supported_types'] = [ 'india' ]; return $mandate_options; } /** * Render the payment method used for a subscription in the "My Subscriptions" table * * @since 1.7.5 * @version 5.6.0 * * @param string $payment_method_to_display the default payment method text to display * @param WC_Subscription $subscription the subscription details * @return string the subscription payment method */ public function maybe_render_subscription_payment_method( $payment_method_to_display, $subscription ) { $customer_user = $subscription->get_customer_id(); // bail for other payment methods if ( $subscription->get_payment_method() !== $this->id || ! $customer_user ) { return $payment_method_to_display; } $order_helper = WC_Stripe_Order_Helper::get_instance(); $stripe_source_id = $order_helper->get_stripe_source_id( $subscription ); // For BW compat will remove in future. if ( empty( $stripe_source_id ) ) { $stripe_source_id = $order_helper->get_stripe_card_id( $subscription ); // Take this opportunity to update the key name. $order_helper->update_stripe_source_id( $subscription, $stripe_source_id ? $stripe_source_id : '' ); $subscription->save(); } $stripe_customer_id = $order_helper->get_stripe_customer_id( $subscription ); // If we couldn't find a Stripe customer linked to the subscription, fallback to the user meta data. if ( ! $stripe_customer_id || ! is_string( $stripe_customer_id ) ) { $user_id = $customer_user; $stripe_customer_id = get_user_option( '_stripe_customer_id', $user_id ); $stripe_source_id = get_user_option( '_stripe_source_id', $user_id ); // For BW compat will remove in future. if ( empty( $stripe_source_id ) ) { $stripe_source_id = get_user_option( '_stripe_card_id', $user_id ); // Take this opportunity to update the key name. update_user_option( $user_id, '_stripe_source_id', $stripe_source_id, false ); } } // If we couldn't find a Stripe customer linked to the account, fallback to the order meta data. if ( ( ! $stripe_customer_id || ! is_string( $stripe_customer_id ) ) && false !== $subscription->get_parent() ) { $order_helper = WC_Stripe_Order_Helper::get_instance(); $parent_order = wc_get_order( $subscription->get_parent_id() ); $stripe_customer_id = $order_helper->get_stripe_customer_id( $parent_order ); $stripe_source_id = $order_helper->get_stripe_source_id( $parent_order ); // For BW compat will remove in future. if ( empty( $stripe_source_id ) ) { $stripe_source_id = $order_helper->get_stripe_card_id( $parent_order ); // Take this opportunity to update the key name. $order_helper->update_stripe_source_id( $parent_order, $stripe_source_id ); $parent_order->save(); } } try { $saved_payment_method = WC_Stripe_Subscriptions_Helper::get_subscription_payment_method_details( $stripe_customer_id, $stripe_source_id ); if ( null !== $saved_payment_method ) { return $this->get_payment_method_to_display_for_payment_method( $saved_payment_method ); } } catch ( WC_Stripe_Exception $e ) { wc_add_notice( $e->getLocalizedMessage(), 'error' ); WC_Stripe_Logger::error( 'Error rendering subscription payment method for subscription: ' . $subscription->get_id(), [ 'error_message' => $e->getMessage() ] ); } return __( 'N/A', 'woocommerce-gateway-stripe' ); } /** * Helper function to get the descriptive text for a payment method or source. * * @param object $payment_method The payment method or source object. * @return string The descriptive text for the payment method or source. */ protected function get_payment_method_to_display_for_payment_method( object $payment_method ): string { // Legacy handling for Stripe Card objects. ref: https://docs.stripe.com/api/cards/object if ( isset( $payment_method->object ) && WC_Stripe_Payment_Methods::CARD === $payment_method->object ) { return sprintf( /* translators: 1) card brand 2) last 4 digits */ __( 'Via %1$s card ending in %2$s', 'woocommerce-gateway-stripe' ), ( isset( $payment_method->brand ) ? wc_get_credit_card_type_label( $payment_method->brand ) : __( 'N/A', 'woocommerce-gateway-stripe' ) ), $payment_method->last4 ); } switch ( $payment_method->type ) { case WC_Stripe_Payment_Methods::CARD: return sprintf( /* translators: 1) card brand 2) last 4 digits */ __( 'Via %1$s card ending in %2$s', 'woocommerce-gateway-stripe' ), ( isset( $payment_method->card->brand ) ? wc_get_credit_card_type_label( $payment_method->card->brand ) : __( 'N/A', 'woocommerce-gateway-stripe' ) ), $payment_method->card->last4 ); case WC_Stripe_Payment_Methods::SEPA_DEBIT: /* translators: 1) last 4 digits of SEPA Direct Debit */ return sprintf( __( 'Via SEPA Direct Debit ending in %1$s', 'woocommerce-gateway-stripe' ), $payment_method->sepa_debit->last4 ); case WC_Stripe_Payment_Methods::CASHAPP_PAY: /* translators: 1) Cash App Cashtag */ return sprintf( __( 'Via Cash App Pay (%1$s)', 'woocommerce-gateway-stripe' ), $payment_method->cashapp->cashtag ); case WC_Stripe_Payment_Methods::LINK: /* translators: 1) email address associated with the Stripe Link payment method */ return sprintf( __( 'Via Stripe Link (%1$s)', 'woocommerce-gateway-stripe' ), $payment_method->link->email ); case WC_Stripe_Payment_Methods::ACH: return sprintf( /* translators: 1) account type (checking, savings), 2) last 4 digits of account. */ __( 'Via %1$s Account ending in %2$s', 'woocommerce-gateway-stripe' ), ucfirst( $payment_method->us_bank_account->account_type ), $payment_method->us_bank_account->last4 ); case WC_Stripe_Payment_Methods::BECS_DEBIT: return sprintf( /* translators: last 4 digits of account. */ __( 'BECS Direct Debit ending in %s', 'woocommerce-gateway-stripe' ), $payment_method->au_becs_debit->last4 ); case WC_Stripe_Payment_Methods::ACSS_DEBIT: return sprintf( /* translators: 1) bank name, 2) last 4 digits of account. */ __( 'Via %1$s ending in %2$s', 'woocommerce-gateway-stripe' ), $payment_method->acss_debit->bank_name, $payment_method->acss_debit->last4 ); case WC_Stripe_Payment_Methods::BACS_DEBIT: /* translators: 1) the Bacs Direct Debit payment method's last 4 numbers */ return sprintf( __( 'Via Bacs Direct Debit ending in (%1$s)', 'woocommerce-gateway-stripe' ), $payment_method->bacs_debit->last4 ); case WC_Stripe_Payment_Methods::AMAZON_PAY: /* translators: 1) the Amazon Pay payment method's email */ return sprintf( __( 'Via Amazon Pay (%1$s)', 'woocommerce-gateway-stripe' ), $payment_method->billing_details->email ?? '' ); } return __( 'N/A', 'woocommerce-gateway-stripe' ); } /** * If this is the "Pass the SCA challenge" flow, remove a variable that is checked by WC Subscriptions * so WC Subscriptions doesn't redirect to the checkout * * @return void */ public function remove_order_pay_var() { global $wp; if ( isset( $_GET['wc-stripe-confirmation'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification $this->order_pay_var = $wp->query_vars['order-pay']; // @phpstan-ignore-line (order_pay_var is defined in classes using this class) $wp->query_vars['order-pay'] = null; } } /** * Restore the variable that was removed in remove_order_pay_var() * * @return void */ public function restore_order_pay_var() { global $wp; if ( isset( $this->order_pay_var ) ) { $wp->query_vars['order-pay'] = $this->order_pay_var; } } /** * Checks if a renewal already failed because a manual authentication is required. * * @param WC_Order $renewal_order The renewal order. * @return boolean */ public function has_authentication_already_failed( $renewal_order ) { $existing_intent = $this->get_intent_from_order( $renewal_order ); if ( ! $existing_intent || WC_Stripe_Intent_Status::REQUIRES_PAYMENT_METHOD !== $existing_intent->status || empty( $existing_intent->last_payment_error ) || 'authentication_required' !== $existing_intent->last_payment_error->code ) { return false; } // Make sure all emails are instantiated. WC_Emails::instance(); /** * A payment attempt failed because SCA authentication is required. * * @param WC_Order $renewal_order The order that is being renewed. */ do_action( 'wc_gateway_stripe_process_payment_authentication_required', $renewal_order ); // Fail the payment attempt (order would be currently pending because of retry rules). $charge = $this->get_latest_charge_from_intent( $existing_intent ); $charge_id = $charge->id; /* translators: %s is the stripe charge Id */ $renewal_order->update_status( OrderStatus::FAILED, sprintf( __( 'Stripe charge awaiting authentication by user: %s.', 'woocommerce-gateway-stripe' ), $charge_id ) ); return true; } /** * Hijacks `wp_redirect` in order to generate a JS-friendly object with the URL. * * @param string $url The URL that Subscriptions attempts a redirect to. * @return void */ public function redirect_after_early_renewal( $url ) { echo wp_json_encode( [ 'stripe_sca_required' => false, 'redirect_url' => $url, ] ); exit; } /** * Once an intent has been verified, perform some final actions for early renewals. * * @param WC_Order $order The renewal order. * @param stdClass $intent The Payment Intent object. * @return void */ protected function maybe_process_subscription_early_renewal_success( $order, $intent ) { if ( WC_Stripe_Subscriptions_Helper::is_subscriptions_enabled() && isset( $_GET['early_renewal'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification if ( function_exists( 'wcs_update_dates_after_early_renewal' ) && function_exists( 'wcs_get_subscription' ) ) { wcs_update_dates_after_early_renewal( wcs_get_subscription( $order->get_meta( '_subscription_renewal' ) ), $order ); } if ( function_exists( 'wc_add_notice' ) ) { wc_add_notice( __( 'Your early renewal order was successful.', 'woocommerce-gateway-stripe' ), 'success' ); } } } /** * During early renewals, instead of failing the renewal order, delete it and let Subs redirect to the checkout. * * @param WC_Order $order The renewal order. * @param stdClass $intent The Payment Intent object (unused). * @return void */ protected function maybe_process_subscription_early_renewal_failure( $order, $intent ) { if ( WC_Stripe_Subscriptions_Helper::is_subscriptions_enabled() && isset( $_GET['early_renewal'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification $order->delete( true ); wc_add_notice( __( 'Payment authorization for the renewal order was unsuccessful, please try again.', 'woocommerce-gateway-stripe' ), 'error' ); $renewal_url = ( function_exists( 'wcs_get_early_renewal_url' ) && function_exists( 'wcs_get_subscription' ) ) ? wcs_get_early_renewal_url( wcs_get_subscription( $order->get_meta( '_subscription_renewal' ) ) ) : ''; wp_safe_redirect( $renewal_url ); exit; } } /** * Returns true if Stripe will attempt the charges at a future date. * * This applies to subscription payments and Indian 3DS regulations. * https://docs.stripe.com/india-recurring-payments?integration=subscriptions#predebit-notification * * @param StdClass $payment_intent the Payment Intent to be evaluated. * @return bool true if the charge attempt is delayed, false otherwise. */ protected function is_charge_attempt_delayed( $payment_intent ) { return ! empty( $payment_intent->status ) && WC_Stripe_Intent_Status::PROCESSING === $payment_intent->status && ! empty( $payment_intent->processing->card->customer_notification->completes_at ); } /** * Updates the payment method for all subscriptions related to an order. * * @param WC_Order $order The order to update the related subscriptions for. * @param string $payment_method_type The payment method ID. eg 'stripe', 'stripe_sepa'. * @return void */ public function update_subscription_payment_method_from_order( $order, $payment_method_type ) { if ( ! WC_Stripe_Subscriptions_Helper::is_subscriptions_enabled() || ! function_exists( 'wcs_get_subscriptions_for_order' ) ) { return; } foreach ( wcs_get_subscriptions_for_order( $order, [ 'order_type' => 'any' ] ) as $subscription ) { $subscription->set_payment_method( $payment_method_type ); $subscription->save(); } } /** * Disable edits for subscriptions that have a mandate and a card payment method from India. * * @param bool $editable The current editability of the subscription. * @param WC_Order $order The order object. * @return bool Returns true if the subscription can be edited, false otherwise. */ public function disable_subscription_edit_for_india( $editable, $order ) { if ( ! WC_Stripe_Subscriptions_Helper::is_subscriptions_enabled() || ! $this->is_subscription( $order ) ) { return $editable; } // Only disable editing if we're on the subscription edit page. if ( ! WC_Stripe_Subscriptions_Helper::is_subscription_edit_page() ) { return $editable; } $parent_order = wc_get_order( $order->get_parent_id() ); if ( ! $parent_order ) { return $editable; } // Bail if subscription's parent order does not have a mandate ID if ( empty( WC_Stripe_Order_Helper::get_instance()->get_stripe_mandate_id( $parent_order ) ) ) { return $editable; } $source_id = WC_Stripe_Order_Helper::get_instance()->get_stripe_source_id( $order ); if ( empty( $source_id ) ) { return $editable; } // Retrieve the payment method object from Stripe. $cache_key = 'payment_method_for_source_' . $source_id; $payment_method = WC_Stripe_Database_Cache::get( $cache_key ); if ( ! $payment_method ) { $payment_method = $this->stripe_request( 'payment_methods/' . $source_id ); WC_Stripe_Database_Cache::set( $cache_key, $payment_method, HOUR_IN_SECONDS ); } // If the payment method is a card and the card's country is India, disable subscription editing. if ( $payment_method && WC_Stripe_Payment_Methods::CARD === $payment_method->type && WC_Stripe_Country_Code::INDIA === ( $payment_method->card->country ?? '' ) ) { return false; } // Fallback to the default behavior. return $editable; } /** * When handling a subscription change payment method request with deferred intents, * don't immediately update the subscription's payment method to Stripe until we've created and confirmed the setup intent. * * For purchases with a 3DS card specifically, we don't want to update the payment method on the subscription until after the customer has authenticated. * * @param bool $update_payment_method Whether to update the payment method. * @param string $new_payment_method The new payment method. * @param WC_Subscription $subscription The subscription. * * @return bool */ public function update_payment_after_deferred_intent( $update_payment_method, $new_payment_method, $subscription ) { if ( ! $this->is_changing_payment_method_for_subscription() || $new_payment_method !== $this->id ) { return $update_payment_method; } return false; } }
| ver. 1.4 |
Github
|
.
| PHP 8.5.7 | Generation time: 0 |
proxy
|
phpinfo
|
Settings