Skip to content
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 payment method and refactor payment processing with deferred intent #3805

Open
wants to merge 27 commits into
base: develop
Choose a base branch
from

Conversation

ricardo
Copy link
Member

@ricardo ricardo commented Jan 30, 2025

Fixes #3804

For context: #2935 introduced deferred intents, which included major changes to how the checkout flow is handled.

After #2935, a Payment Intent is only created after the UPE is mounted and the payment is about to be confirmed.

This makes it incompatible for ACSS and BLIK, which are payment methods that don't support deferred intent creation (ref).

So, to fix this issue, I introduced a supports_deferred_intent property to the UPE Payment method, which should be used to determine how the intent creation and checkout should be handled.

Changes proposed in this Pull Request:

  • Add ACSS Debit / Canadian Pre-Authorized Debit as a payment method.
  • Refactored intent creation to include payment method and mandate information.
  • Mount PE in the front-end based on deferred intent support, and only mount it when the PM is selected.
  • Refactor WC_Stripe_UPE_Payment_Gateway->process_payment.
  • Handle payment_intent.processing webhooks to fix delayed notifications.

Known issues

  • This PR still only implements ACSS for the classic checkout, not the blocks checkout yet.
  • Due to level3 retrials, the error message when the payment fails is still misleading, this should be fixed in ACSS: Handle Errors and Edge Cases #3830.
  • There's some visual jank before the PE loads (p1738600641942219-slack-C055WHLA98D).

Testing instructions

Setup:

  1. Enable the ACSS feature flag:
npm run wp option update _wcstripe_feature_lpm_acss 'yes'
  1. Enable "Canadian Pre-Authorized Debits" from your Stripe dashboard.
  2. Enable "Pre-Authorized Debit" from WooCommerce > Settings > Payments > Stripe.
  3. Enable Stripe Webhook listening:
npm run listen
  1. Change the store currency to CAD.

Test 1: Successful checkout

  1. Add a simple product to your cart and proceed to the shortcode checkout.
  2. Select Canada, Vancouver, British Columbia, K1A 0B1 (zip code).
  3. Select "Pre-Authorized Debit" as the payment method.
  4. Click Place order > Agree > Simulate successful verification > Agree.
  5. Inspect the order details page and ensure the total and payment method are correct.
  6. As the merchant, navigate to WooCommerce > Orders and click the order you just created.
  7. Inspect the order, order notes and click to see the Stripe charge.
  8. The Stripe charge should contain correct information about the order, metadata, customer information and mandate details.

Test 2: Checkout with micro deposits verification + delayed notification

  1. Add a simple product to your cart and proceed to the shortcode checkout.
  2. Type a valid email where you can receive a micro deposit notification in the following format: {A8C_VALID_EMAIL}[email protected].
  3. Select Canada, Vancouver, British Columbia, K1A 0B1 (zip code).
  4. Select "Pre-Authorized Debit" as the payment method.
  5. Click Place order > Manually verify.
  6. Type in 000, 11000, 900123456789, click Confirm > Agree.
  7. Inspect the order details page and ensure the total and payment method are correct.
  8. As a merchant, navigate to the order you just created and ensure the status is "Pending payment".
  9. Check your email and click "Verify now".
  10. Type 32 and 45 to the verification page and the payment should be confirmed with a 3 minute delay once the micro deposits are verified.
  11. As the merchant, navigate to WooCommerce > Orders and click the order you just created.
  12. Wait for the payment_intent.processing webhook and the order status should be set to "On-hold" followed by "Processing" a few moments later.
  13. Inspect the order, order notes and click to see the Stripe charge.
  14. The Stripe charge should contain correct information about the order, metadata, customer information and mandate details.

Test 3: Disable feature flag

  1. Disable the feature flag:
npm run wp option delete _wcstripe_feature_lpm_acss
  1. Navigate to the merchant's payment methods page and shopper's checkout, and ensure the payment method "Pre-Authorized Debit" is no longer visible.

  • Covered with tests (or have a good reason not to test in description ☝️)
  • Added changelog entry in both changelog.txt and readme.txt (or does not apply)
  • Tested on mobile (or does not apply)

Post merge

@ricardo ricardo marked this pull request as ready for review February 3, 2025 18:15
@ricardo ricardo changed the title Refactor payment processing with deferred intent for ACSS Add ACSS payment method and refactor payment processing with deferred intent Feb 4, 2025
@ricardo ricardo requested a review from cesarcosta99 February 5, 2025 18:24
// If the payment method doesn't support deferred intent, the intent must be created here.
if ( ! supportsDeferredIntent ) {
const intent = await api.createIntent( null, paymentMethodType );
gatewayUPEComponents[ paymentMethodType ].intentId = intent.id;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is similar to how it used to work before #2935, but instead of saving the PI ID to a local variable, we save the intent ID to each specific UPE component object (gatewayUPEComponents), so we can render the PE only when the PM is selected, and have a different PI for each split UPE that doesn't support deferred intents (In this case it will be only BLIK and ACSS).

Comment on lines 607 to 616
// Also uncheck the radio button if it's selected.
const radioButton = document.querySelector(
'input[name="payment_method"][value="stripe_' +
paymentMethodType +
'"]'
);

if ( radioButton ) {
radioButton.checked = false;
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a small UX improvement. Before this, the payment method would be hidden, but remain selected, causing an error at checkout.

'acss_debit' => [
'mandate_options' => [
'payment_schedule' => 'interval',
'interval_description' => __( 'One-time payment', 'woocommerce-gateway-stripe' ), // TODO: Change to cadence if purchasing a subscription.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This TODO should be handled in #3831.

Comment on lines +678 to +687
$payment_intent_id = isset( $_POST['wc_payment_intent_id'] ) ? wc_clean( wp_unslash( $_POST['wc_payment_intent_id'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing
$order = wc_get_order( $order_id );
$selected_payment_type = $this->get_selected_payment_method_type_from_request();

if ( $payment_intent_id && ! $this->payment_methods[ $selected_payment_type ]->supports_deferred_intent() ) {
// Adds customer and metadata to PaymentIntent.
// These parameters cannot be added upon updating the intent via the `/confirm` API.
$this->intent_controller->update_payment_intent( $payment_intent_id, $order_id );
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally we should refactor process_payment_with_deferred_intent below and possibly the entire process_payment method, but since there's a comment "To be removed." for $_POST['wc-stripe-is-deferred-intent'], I decided to keep this change at a minimum.

Basically, if an unsupported deferred intent payment method was used, we need to first update the PI , and then process the payment as normal with process_payment_with_deferred_intent.

This PI update will add customer and mandate data to the PI, as well as attach the PI to the order.

Even though the ACSS method doesn't use a "deferred intent", this is more of a naming problem than a problem with the code and should be refactored when "To be removed." actually happens. I'll find or create an issue for this and link it in the code as a comment.

Comment on lines 994 to 1001
// Asynchronous payment methods such as bank debits will only provide a charge ID at `payment_intent.processing`, once the required actions are taken by the customer.
// We need to update the order transaction ID, so that the `payment_intent.succeeded` webhook is able to process the order.
case 'payment_intent.processing':
$charge = $this->get_latest_charge_from_intent( $intent );
$order->set_transaction_id( $charge->id );
/* translators: transaction id */
$order->update_status( 'on-hold', sprintf( __( 'Stripe charge awaiting payment: %s.', 'woocommerce-gateway-stripe' ), $charge->id ) );
break;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@asumaran, @rafaelzaleski – I wanted to invite you to do some inspection of this part of the code, because it touches other direct debit methods.

This change implements handling for payment_intent.processing, which is a status that is specific to bank debits, so 'd like to get both your 👍/👎 on this change.

  1. When we select a manual deposit verification for ACH or ACSS and click "place order", the payment intent is set to a "requires_action" status, but at this point it doesn't add any charge information to it. It's like the SCA / 3DS card flow, but the order has already been processed.
  2. You can check the wp_wc_orders.transaction_id column and notice there's no charge ID in there.
  3. Once the customer completes the required action, i.e submits the manual verification of micro deposits, it switches the payment intent status to processing – from what I've tested, this is only new to delayed notification methods.
  4. Please validate that the payment_intent.processing is the first event that Stripe sends us that contains a charge ID we can use to update the order. Some other events happen in between, e.g payment_intent.requires_action, mandate.updated, but they don't have a charge ID we can use yet.
  5. By updating the transaction_id, the subsequent status updates can be captured for this order accordingly.

Let me know if you have any questions about this fix.

You should be able to replicate this from the "Test 2: Checkout with micro deposits verification + delayed notification" testing instructions using BACS or ACH.

From what I tested in develop, this should fix it for both ACSS and ACH, but I haven't tried with BACS yet, so it would be nice if you can try this as well @asumaran. I'm not sure if BACS requires any action or supports delayed notifications.

@ricardo
Copy link
Member Author

ricardo commented Feb 5, 2025

Assigned @cesarcosta99 as he will be working on the next PM spike, and @rafaelzaleski and @asumaran can do a quicker spot check of the aforementioned payment_intent.processing issue.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should also rename this file to something more generic since it's now handling non-deferred intents as well?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another option would be to add a common file that imports the regular deferred-intent.js file and a new one for non-deferred.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think classic/upe/deferred-intent.js and classic/upe/index.js serve similar purposes right now both adding event listeners to the checkout and having some repeat variables (1, 2), so my question was more if deferred-intent is a good naming?

Also, handleUPECheckout from index.js apparently is not being used anymore after the split UPE work.

Perhaps we should unify all these event listeners into a new index.js or checkout-handler.js, and keep functions in other files like payment-processing.js.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps we should unify all these event listeners into a new index.js or checkout-handler.js, and keep functions in other files like payment-processing.js.

I think that's a good idea 👍

@Mayisha Mayisha requested review from a team and diegocurbelo and removed request for a team February 6, 2025 09:58
WC_Stripe_Payment_Methods::ALIPAY => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/alipay.svg" class="stripe-alipay-icon stripe-icon" alt="Alipay" />',
WC_Stripe_Payment_Methods::WECHAT_PAY => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/wechat.svg" class="stripe-wechat-icon stripe-icon" alt="Wechat Pay" />',
WC_Stripe_Payment_Methods::BANCONTACT => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/bancontact.svg" class="stripe-bancontact-icon stripe-icon" alt="Bancontact" />',
WC_Stripe_Payment_Methods::IDEAL => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/ideal.svg" class="stripe-ideal-icon stripe-icon" alt="iDEAL" />',
WC_Stripe_Payment_Methods::P24 => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/p24.svg" class="stripe-p24-icon stripe-icon" alt="P24" />',
WC_Stripe_Payment_Methods::GIROPAY => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/giropay.svg" class="stripe-giropay-icon stripe-icon" alt="giropay" />',
WC_Stripe_Payment_Methods::KLARNA => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/klarna.svg" class="stripe-klarna-icon stripe-icon" alt="klarna" />',
WC_Stripe_Payment_Methods::AFFIRM => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/affirm.svg" class="stripe-affirm-icon stripe-icon" alt="affirm" />',
WC_Stripe_Payment_Methods::KLARNA => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/klarna.svg" class="stripe-klarna-icon stripe-icon" alt="Klarna" />',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! Thanks for fixing this

Copy link
Contributor

@cesarcosta99 cesarcosta99 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work with ACSS 🚀 I left a couple nitpicks and have some questions before approving, but overall this is looking great!

Due to level3 retrials, the error message when the payment fails is still misleading, this should be fixed in #3830.

Can we add this to that issue description or as a comment just so we have this tracked?

All tests passed, but I wanted to confirm this:

Last step of test 1 and 2 states we should find correct information in the Stripe transaction. I found the correct amount, mandate and customer information, however, I didn't find anything about the order such as ID, or metadata. Are we expecting that, at this point, to be set in the charge on Stripe?

client/stripe-utils/utils.js Outdated Show resolved Hide resolved
@@ -972,7 +1001,7 @@ private function build_base_payment_intent_request_params( $payment_information
* Determines if mandate data is required for deferred intent UPE payment.
*
* A mandate must be provided before a deferred intent UPE payment can be processed.
* This applies to SEPA, Bancontact, iDeal, Sofort, Cash App and Link payment methods.
* This applies to SEPA, Bancontact, iDeal, Sofort, Cash App, Link payment methods and ACSS Debit.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add the missing payment methods here too?

Suggested change
* This applies to SEPA, Bancontact, iDeal, Sofort, Cash App, Link payment methods and ACSS Debit.
* This applies to SEPA, Bancontact, iDeal, Sofort, Cash App, Link payment methods,
* ACH, ACSS Debit and BACS.

@ricardo
Copy link
Member Author

ricardo commented Feb 7, 2025

Can we add this to that issue description or as a comment just so we have this tracked?

Added it ✔️.

I didn't find anything about the order such as ID, or metadata.

When you click the charge ID from the WC edit order page and go to the Stripe dashboard, you should be able to see metadata information on the right side of the page right below the customer information, like this:

image

@cesarcosta99 Are you able to find it? If not, can you try with other payment methods such as cards? Also, are you using the custom orders table / HPOS in WooCommerce?

@cesarcosta99
Copy link
Contributor

When you click the charge ID from the WC edit order page and go to the Stripe dashboard

@ricardo, actually, none of the Stripe IDs in the order edit page is clickable for me. What I did to check the transaction details was to search for the value directly in the Stripe dashboard. And to me, metadata came empty for an ACSS payment:

image

Also, are you using the custom orders table / HPOS in WooCommerce?

Yes, HPOS is enabled. Is this known limitation?

can you try with other payment methods such as cards?

In a card transaction metadata has been correctly set.

Screenshot 2025-02-07 at 16 42 08

@cesarcosta99
Copy link
Contributor

@ricardo, similar to ACH, should we hide the ACSS payment option from the checkout page if billing country is not supported?

@cesarcosta99
Copy link
Contributor

@ricardo, actually, none of the Stripe IDs in the order edit page is clickable for me. What I did to check the transaction details was to search for the value directly in the Stripe dashboard. And to me, metadata came empty for an ACSS payment:

The Stripe ID was not clickable because I had disabled the ACSS feature flag after test 3. Re-enabling it made it render as expected.

I did another attempt on test 1 while being logged out from WP Admin and metadata came in correctly ✅ I saw the dev logs on the Stripe dashboard and can confirm there were failures for the requests containing the metadata with error 400 resource_missing - customer - No such customer: 'cus_...'. So either was some punctual failures on the Stripe API, or it's related to being logged as Admin in WordPress. Might worth taking a look at this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

ACSS: Refactor deferred intent approach for ACSS
3 participants