-
Notifications
You must be signed in to change notification settings - Fork 211
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add ACSS support for WC Subscriptions #4051
Changes from 17 commits
9b4208b
7a4bf67
593ee39
9f8f8ef
f4f0718
449e065
367ad3a
4ec297b
b2e74fc
6ea7cdf
99caaf3
54440ad
389e82d
1936888
325cf2a
f38039b
28b5871
fdef680
5ccf8f8
c6d25a6
d5bfa7d
8432941
0352277
0870c81
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -550,8 +550,13 @@ public function process_response( $response, $order ) { | |
$this->update_fees( $order, is_string( $response->balance_transaction ) ? $response->balance_transaction : $response->balance_transaction->id ); | ||
} | ||
|
||
// TODO: Refactor and add mandate ID support for other payment methods, if necessary. | ||
// The mandate ID is not available for the intent object, so we need to fetch the charge. | ||
// Mandate ID is necessary for renewal payments for certain payment methods and Indian cards. | ||
if ( isset( $response->payment_method_details->card->mandate ) ) { | ||
$order->update_meta_data( '_stripe_mandate_id', $response->payment_method_details->card->mandate ); | ||
} elseif ( isset( $response->payment_method_details->acss_debit->mandate ) ) { | ||
$order->update_meta_data( '_stripe_mandate_id', $response->payment_method_details->acss_debit->mandate ); | ||
} | ||
|
||
if ( isset( $response->payment_method, $response->payment_method_details ) ) { | ||
|
@@ -1618,14 +1623,23 @@ public function save_intent_to_order( $order, $intent ) { | |
if ( 'payment_intent' === $intent->object ) { | ||
WC_Stripe_Helper::add_payment_intent_to_order( $intent->id, $order ); | ||
|
||
// Add the mandate id necessary for renewal payments with Indian cards if it's present. | ||
// TODO: Refactor and add mandate ID support for other payment methods, if necessary. | ||
// The mandate ID is not available for the intent object, so we need to fetch the charge. | ||
// Mandate ID is necessary for renewal payments for certain payment methods and Indian cards. | ||
$charge = $this->get_latest_charge_from_intent( $intent ); | ||
|
||
if ( isset( $charge->payment_method_details->card->mandate ) ) { | ||
$order->update_meta_data( '_stripe_mandate_id', $charge->payment_method_details->card->mandate ); | ||
} elseif ( isset( $charge->payment_method_details->acss_debit->mandate ) ) { | ||
$order->update_meta_data( '_stripe_mandate_id', $charge->payment_method_details->acss_debit->mandate ); | ||
} | ||
} elseif ( 'setup_intent' === $intent->object ) { | ||
$order->update_meta_data( '_stripe_setup_intent', $intent->id ); | ||
|
||
// Add mandate for free trial subscriptions. | ||
if ( isset( $intent->mandate ) ) { | ||
$order->update_meta_data( '_stripe_mandate_id', $intent->mandate ); | ||
} | ||
Comment on lines
+1653
to
+1655
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For payment intents, the mandate ID is only available from the charge object, however for setup intents, the mandate ID can be accessed directly from the intent object. |
||
} | ||
|
||
if ( is_callable( [ $order, 'save' ] ) ) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -410,7 +410,7 @@ public function update_payment_intent_ajax() { | |
throw new Exception( __( 'Unable to verify your request. Please reload the page and try again.', 'woocommerce-gateway-stripe' ) ); | ||
} | ||
|
||
wp_send_json_success( $this->update_payment_intent( $payment_intent_id, $order_id, $save_payment_method, $selected_upe_payment_type ), 200 ); | ||
wp_send_json_success( $this->update_intent( $payment_intent_id, $order_id, $save_payment_method, $selected_upe_payment_type ), 200 ); | ||
} catch ( Exception $e ) { | ||
// Send back error so it can be displayed to the customer. | ||
wp_send_json_error( | ||
|
@@ -424,9 +424,10 @@ public function update_payment_intent_ajax() { | |
} | ||
|
||
/** | ||
* Updates payment intent to be able to save payment method. | ||
* Updates payment intent or setup intent to be able to save payment method. | ||
* | ||
* @since 5.6.0 | ||
* @version x.x.x | ||
* | ||
* @param {string} $payment_intent_id The id of the payment intent to update. | ||
cesarcosta99 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* @param {int} $order_id The id of the order if intent created from Order. | ||
|
@@ -436,7 +437,7 @@ public function update_payment_intent_ajax() { | |
* @throws Exception If the update intent call returns with an error. | ||
* @return array|null An array with result of the update, or nothing | ||
*/ | ||
public function update_payment_intent( $payment_intent_id = '', $order_id = null, $save_payment_method = false, $selected_upe_payment_type = '' ) { | ||
public function update_intent( $payment_intent_id = '', $order_id = null, $save_payment_method = false, $selected_upe_payment_type = '' ) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This method now handles setup intents as well. Looking at the Stripe logs, there was an error where it was incorrectly calling Given the similarity of a potential new method for handling only setup intents, I decided to unify both needs into the same function. I'd appreciate the reviewer double-checking this approach, but I believe it shouldn't affect existing flows, given how minimal the change is. |
||
$order = wc_get_order( $order_id ); | ||
|
||
if ( ! is_a( $order, 'WC_Order' ) ) { | ||
|
@@ -449,15 +450,19 @@ public function update_payment_intent( $payment_intent_id = '', $order_id = null | |
$customer = new WC_Stripe_Customer( wp_get_current_user()->ID ); | ||
|
||
if ( $payment_intent_id ) { | ||
|
||
$request = [ | ||
'amount' => WC_Stripe_Helper::get_stripe_amount( $amount, strtolower( $currency ) ), | ||
'currency' => strtolower( $currency ), | ||
'metadata' => $gateway->get_metadata_from_order( $order ), | ||
/* translators: 1) blog name 2) order number */ | ||
'description' => sprintf( __( '%1$s - Order %2$s', 'woocommerce-gateway-stripe' ), wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ), $order->get_order_number() ), | ||
]; | ||
|
||
$is_setup_intent = substr( $payment_intent_id, 0, 4 ) === 'seti'; | ||
if ( ! $is_setup_intent ) { | ||
// These parameters are only supported for payment intents. | ||
$request['amount'] = WC_Stripe_Helper::get_stripe_amount( $amount, strtolower( $currency ) ); | ||
$request['currency'] = strtolower( $currency ); | ||
} | ||
|
||
if ( '' !== $selected_upe_payment_type ) { | ||
// Only update the payment_method_types if we have a reference to the payment type the customer selected. | ||
$request['payment_method_types'] = [ $selected_upe_payment_type ]; | ||
|
@@ -485,9 +490,11 @@ public function update_payment_intent( $payment_intent_id = '', $order_id = null | |
|
||
$level3_data = $gateway->get_level3_data_from_order( $order ); | ||
|
||
// Use "setup_intents" endpoint if `$payment_intent_id` starts with `seti_`. | ||
$endpoint = $is_setup_intent ? 'setup_intents' : 'payment_intents'; | ||
WC_Stripe_API::request_with_level3_data( | ||
$request, | ||
"payment_intents/{$payment_intent_id}", | ||
"{$endpoint}/{$payment_intent_id}", | ||
$level3_data, | ||
$order | ||
); | ||
|
@@ -506,7 +513,7 @@ public function update_payment_intent( $payment_intent_id = '', $order_id = null | |
* Handle AJAX requests for creating a setup intent without confirmation for Stripe UPE. | ||
* | ||
* @since 5.6.0 | ||
* @version 5.6.0 | ||
* @version x.x.x | ||
*/ | ||
public function init_setup_intent_ajax() { | ||
try { | ||
|
@@ -515,7 +522,9 @@ public function init_setup_intent_ajax() { | |
throw new Exception( __( "We're not able to add this payment method. Please refresh the page and try again.", 'woocommerce-gateway-stripe' ) ); | ||
} | ||
|
||
wp_send_json_success( $this->init_setup_intent(), 200 ); | ||
$payment_method_type = isset( $_POST['payment_method_type'] ) ? wc_clean( wp_unslash( $_POST['payment_method_type'] ) ) : ''; | ||
|
||
wp_send_json_success( $this->init_setup_intent( $payment_method_type ), 200 ); | ||
} catch ( Exception $e ) { | ||
// Send back error, so it can be displayed to the customer. | ||
wp_send_json_error( | ||
|
@@ -532,11 +541,13 @@ public function init_setup_intent_ajax() { | |
* Creates a setup intent without confirmation. | ||
* | ||
* @since 5.6.0 | ||
* @version 5.6.0 | ||
* @version x.x.x | ||
* | ||
* @param string|null $payment_method_type The type of payment method to use for the intent. | ||
* @return array | ||
* @throws Exception If customer for the current user cannot be read/found. | ||
*/ | ||
public function init_setup_intent() { | ||
public function init_setup_intent( $payment_method_type = null ) { | ||
// Determine the customer managing the payment methods, create one if we don't have one already. | ||
$user = wp_get_current_user(); | ||
$customer = new WC_Stripe_Customer( $user->ID ); | ||
|
@@ -547,17 +558,18 @@ public function init_setup_intent() { | |
$customer_id = $customer->update_customer(); | ||
} | ||
|
||
$gateway = $this->get_upe_gateway(); | ||
$payment_method_types = array_filter( $gateway->get_upe_enabled_payment_method_ids(), [ $gateway, 'is_enabled_for_saved_payments' ] ); | ||
$gateway = $this->get_upe_gateway(); | ||
$enabled_payment_methods = $payment_method_type ? [ $payment_method_type ] : array_values( array_filter( $gateway->get_upe_enabled_payment_method_ids(), [ $gateway, 'is_enabled_for_saved_payments' ] ) ); | ||
|
||
$setup_intent = WC_Stripe_API::request( | ||
[ | ||
'customer' => $customer_id, | ||
'confirm' => 'false', | ||
'payment_method_types' => array_values( $payment_method_types ), | ||
], | ||
'setup_intents' | ||
); | ||
$request = [ | ||
'customer' => $customer_id, | ||
'confirm' => 'false', | ||
'payment_method_types' => $enabled_payment_methods, | ||
]; | ||
|
||
$request = $this->maybe_add_mandate_options( $request, $payment_method_type, true ); | ||
|
||
$setup_intent = WC_Stripe_API::request( $request, 'setup_intents' ); | ||
|
||
if ( ! empty( $setup_intent->error ) ) { | ||
throw new Exception( $setup_intent->error->message ); | ||
|
@@ -763,7 +775,7 @@ public function create_and_confirm_payment_intent( $payment_information ) { | |
$request['statement_descriptor_suffix'] = $payment_information['statement_descriptor_suffix']; | ||
} | ||
|
||
if ( isset( $payment_information['payment_method_options'] ) ) { | ||
if ( ! empty( $payment_information['payment_method_options'] ) ) { | ||
$request['payment_method_options'] = $payment_information['payment_method_options']; | ||
} | ||
|
||
|
@@ -802,21 +814,26 @@ public function create_and_confirm_payment_intent( $payment_information ) { | |
* | ||
* @param array $request The request array to add the mandate options to. | ||
* @param string|null $payment_method_type The type of payment method to use for the intent. | ||
* @param bool $is_setup_intent Whether the request is for a setup intent. | ||
* | ||
* @return array | ||
*/ | ||
private function maybe_add_mandate_options( $request, $payment_method_type ) { | ||
// Add required mandate options for ACSS. | ||
private function maybe_add_mandate_options( $request, $payment_method_type, $is_setup_intent = false ) { | ||
if ( WC_Stripe_UPE_Payment_Method_ACSS::STRIPE_ID === $payment_method_type ) { | ||
$request['payment_method_options'] = [ | ||
WC_Stripe_Payment_Methods::ACSS_DEBIT => [ | ||
'mandate_options' => [ | ||
'payment_schedule' => 'interval', | ||
'interval_description' => __( 'One-time payment', 'woocommerce-gateway-stripe' ), // TODO: Change to cadence if purchasing a subscription. | ||
'payment_schedule' => 'combined', | ||
'interval_description' => __( 'Payments as per agreement', 'woocommerce-gateway-stripe' ), | ||
Comment on lines
+835
to
+836
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I figured it's best to set the payment schedule to Reference: https://docs.stripe.com/payments/acss-debit#payment-schedule |
||
'transaction_type' => 'personal', | ||
], | ||
], | ||
]; | ||
|
||
// If it's a setup intent, add the CAD currency parameter. | ||
if ( $is_setup_intent ) { | ||
$request['payment_method_options'][ WC_Stripe_Payment_Methods::ACSS_DEBIT ]['currency'] = strtolower( WC_Stripe_Currency_Code::CANADIAN_DOLLAR ); | ||
} | ||
} | ||
|
||
return $request; | ||
|
@@ -961,6 +978,8 @@ private function build_base_payment_intent_request_params( $payment_information | |
$request = WC_Stripe_Helper::add_mandate_data( $request ); | ||
} | ||
|
||
$request = $this->maybe_add_mandate_options( $request, $payment_information['selected_payment_type'] ); | ||
|
||
// Does not set the return URL if Single Payment Element is enabled or if the request needs redirection. | ||
if ( $this->get_upe_gateway()->is_spe_enabled() || $this->request_needs_redirection( $payment_method_types ) ) { | ||
$request['return_url'] = $payment_information['return_url']; | ||
|
@@ -1033,6 +1052,8 @@ public function create_and_confirm_setup_intent( $payment_information ) { | |
$request = WC_Stripe_Helper::add_mandate_data( $request ); | ||
} | ||
|
||
$request = $this->maybe_add_mandate_options( $request, $payment_information['selected_payment_type'], true ); | ||
|
||
// For voucher payment methods type like Boleto, Oxxo, Multibanco, and Cash App, we shouldn't confirm the intent immediately as this is done on the front-end when displaying the voucher to the customer. | ||
// When the intent is confirmed, Stripe sends a webhook to the store which puts the order on-hold, which we only want to happen after successfully displaying the voucher. | ||
if ( $this->is_delayed_confirmation_required( $request['payment_method_types'] ) ) { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The mandate ID here is necessary for renewal orders. Without it, renewal orders will fail.
I think it's best to keep this as a TODO just to indicate that the approach should be refactored if adding new payment methods, and also to make sure we test other payment methods because I figured this is necessary for at least card and ACSS.
@asumaran I couldn't test BACS subscriptions, but this doesn't seem necessary for ACH as far as I have tested. Can you try processing a renewal order for BACS and make sure this is not needed for BACS? There's a similar check in line 1631.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ricardo, for Bacs, automatic subscription renewal worked fine, and the ‘Renew now’ option also worked without any issues. Thanks for the heads up!