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
Reset order state to cart in case the stripe SCA authorization step fails #5824
Reset order state to cart in case the stripe SCA authorization step fails #5824
Conversation
# This can also happen during the edit action | ||
# but the actions and response needed are exactly the same as when the update action fails | ||
update_failed(error) | ||
end |
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 key difference here is the call to checkout_failed in update_failed that executes PostCheckoutActions that will reset the order state to cart 👍
…re is a stripe exception anywhere is the same as when the update action fails
…) whenever there's a payment gateway exceeption raised
3fe5bd9
to
7350a28
Compare
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.
Good one 👏 ! I think it'd be a good idea to create a tech debt issue to find out about
the payment is correctly set to failed state but the order workflow state machine rolls it back to pending state causing #5818
and fix it properly. IMO this part of the codebase added callbacks on top of things there weren't properly working and this led to a lot of complexity that is making it hard to change.
looks like we had an infinite loop in the build. |
ah, yeah, I missed that. I'll have a look now,. Thanks! |
I tried to fix the build but this after_rollback callback breaks the specs because the specs always rollback the transactions... I saw that Spree also removes this callback in v2.3. So I tried to create a better solution. It was hard but I think I do have a slightly better workaround for this. |
rollbacks the transactions, with this we keep record of what went wrong.
7350a28
to
734fce5
Compare
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.
Some nice refactoring in here. You will be so happy when we finally rewrite that state machine. 😄
The specs need more work, moving back to in-dev. |
I fixed all the obvious specs but there are still some failing:
The last one passed locally and may be flaky but the first two are definitely broken by this pull request. The credit card secrets are not passed onto the payment gateway and without valid credit card number it fails.
|
Reloading the payment is a problem:
While the records look exactly the same, reloading gets rid of in-memory data like the credit card secrets.
The easy fix is: diff --git a/app/services/order_workflow.rb b/app/services/order_workflow.rb
index b7db840ef..c2a7e9dc9 100644
--- a/app/services/order_workflow.rb
+++ b/app/services/order_workflow.rb
@@ -64,8 +64,8 @@ class OrderWorkflow
def persist_all_payments
order.payments.each do |payment|
original_payment_state = payment.state
- if original_payment_state != payment.reload.state
- payment.update(state: original_payment_state)
+ if original_payment_state != Spree::Payment.find(payment.id).state
+ payment.reload.update(state: original_payment_state)
end
end
end But I'm not sure about the implications. Having rollback code and then working against it sounds broken. Maybe we need to question the rollback code? |
@luisramos0 I'm handing the keyboard back to you. Let me know what you think. I guess we can go ahead with this fix and create a tech debt issue. Or we investigate fixing the callback. You may have a better overview of that and it may make more sense to tackle that in bye-bye-spree. |
… method) when checkout_fails while handling stripe redirect
…date action has a different logic where there is a generic rescue StandardError after the GatewayError rescue
… wiped out When checkout fails and the payment states dont match (inside the if), in-memory data of the failed payment can be lost but updating the payment state is the fundamental part here so that further checkout attempts work. We may improve this update statement so that all the data of the failed payment is persisted
8e34805
to
a6cb209
Compare
hey @mkllnk nice async pair programming! Thank you! I think your fix works! I am not sure the persisted failed payments will end up with all the info but it doesnt matter for now, first checkout will now work and second, we will have spree_log_entries which is the most important. I added a few more simple commits. |
a6cb209
to
e739c51
Compare
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.
I think that this is good to go. Can't wait to clean this up one day.
We have a similar problem in Ceres Fair Food. In some very rare circumstances, a rollback happens. But it actually rolls back the creation of the payment. The result is a gap in the sequence of payment ids and in some cases the payment has gone through. Rollbacks are dangerous.
app/services/order_workflow.rb
Outdated
if original_payment_state != payment.reload.state | ||
payment.update(state: original_payment_state) | ||
if original_payment_state != Spree::Payment.find(payment.id).state | ||
payment.reload.update(state: original_payment_state) |
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.
I'm still a bit confused what's happening here. Do we need a comment explaining that? My theory is:
- During checkout the order is going to state
payment
and a payment object is created. - The order should go from state
payment
tocomplete
but on error the state machine stays inpayment
. - The payment object's normal states are:
checkout
,pending
,processing
,completed
- Error states are:
void
andinvalid
. - A failed checkout causes a database rollback which reverts the payment state from
void
orinvalid
tocheckout
orpending
(I don't know which). - Despite the rollback, our in-memory payment object still has the failed state.
- Checking for a state change in the database is a sign of a rollback and we override the database with our payment state.
- In theory, if something else updated the payment record at the same time, like a webhook, we could accidentally override that. There is a potential race condition here but we are most likely fine because of the checkout sequence and synchronisation.
Do you agree with this?
What else could happen? I guess a payment could complete but something else fails and we override the payment state to mark it as completed. That's a good thing.
I reckon that the payment should happen outside of the state machine transition to not be affected by the rollback. The state machine transition just verifies that there is enough money for the order. Out of scope here.
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.
I dont agree about you being confused :-)
I think what's happening is exactly what you have described 👌 and that was what I thought was happening 👍
I used some of your description to add code comments. I think it's now better explained, thanks!
2ba3e24
to
0359d10
Compare
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.
Ace! That is so much clearer. 🤓
Hey @luisramos0 and reviewers, Reproduced issue and re-ran the procedure, after staging - for both Stripe-SCA and Connect (which was already working before). This looks good, transaction fee is now correctly charged, after failing the payment once, with card ...0069. Amounts match in stripe-dashboard and on Payments (while editing the order): Also verified we now get a warning on Bugsnag/Slack on this type of failed payments - which is really awesome, surely helpful as well for issues such as #5785 ! This works for both stripe methods. Reproduced the issue before staging. True, it seems to break as well by manually failing the 3D authentication process. So, after this PR the process goes as expected, after the first failed attempt: No additional transaction fees are added, for each failed attempt - I tried several other invalid cards, such as 4000008260003178 (insufficient funds) or 4000008400001629 (card_declined), verifying that these errors appear in Bugsnag and in stripe's Dashboard, correctly: Both issues are solved - and we've got data on errors now. Awesome work 💪 |
This is great news. I will include this in this release 3.2.1 making it fully ready for the stripeSCA rollout 🎉 |
What? Why?
Closes #5816 and #5818
I am putting these two fixes together in one PR because they are not too big but mostly so we can test these things together, they are different but closely related.
Regarding #5816
The handler of any gateway exceptions is used for the StripeSCA authorization step. Whenever there's an error on checkout we need to reset the order to cart state.
We were not doing that before and that was causing the order to stay in the payment state after an authorization error. Because the order was in the payment state, the callback to update order totals was not executed on the second attempt and that caused not all fees to be collected #5816
This is a generic improvement so it will make the checkout code more resilient for some other cases as well.
Regarding #5818
Here we are adding an alternative to the infamous after_rollback callback called persist_invalid for the payment model.
On checkout, in the situation where we are handling a redirect from stripe and capturing the payment, the payment is correctly set to failed state but the order workflow state machine rolls it back to pending state causing #5818. Here we are adding some logic to the OrderWorkflow service to persist the payments after a failure so we get the data for debugging.
Related to this there's a good refactoring with code moved from checkout controller to the OrderWorkflow service.
Additionally, while investigating I thought I'd have to patch the checkout workflow code, in the end I didnt but I ended up bringing the checkout.rb file from spree_core to OFN. No changes to it, just rubocop and transpec.
What should we test?
We need to verify the issue is solved.
The code changed is related to any errors that happen on the stripe side so we can test any other error scenario on stripe and verify that fixing it will process the payment in stripe with the correct amount. For example, we can check if this will also work if we use a card that needs external authorization and we fail the validation.
I just verified that #5816 is also present when we use a card like 4000 0027 6000 3184 and press "fail authentication". So:
This test case should fail before this PR and pass after this PR 👍
Release notes
Changelog Category: Fixed
Fixes StripeSCA problem where the second attempt to make a payment would fail even the card and details were valid.