-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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
[RFC] Cancel payments refunds correctly #2111
Conversation
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 just left a comment about naming a class but I'm fine with what has been done as well!
# @return [Class] a class instance that responds to `cancel!(payment)` | ||
attr_writer :payment_canceller | ||
def payment_canceller | ||
@payment_canceller ||= Spree::Payment::Cancellation.new( |
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 just have some doubt about the naming:
I expected this class to be named Spree::Payment::Canceller
because it is the entity that performs the cancellation. Spree::Payment::Cancellation
also makes sense but it's more an action, maybe I'd expect the cancellation to happen right when it is initialized, instead this will not perform anything until the cancel!
method is called.
As discussed in private with @tvdeyen it is just a personal preference, both make sense, I just wanted to express mine 😸 .
|
||
# @param reason [String] (DEFAULT_REASON) - | ||
# The reason used to create the Spree::RefundReason | ||
def initialize(reason: DEFAULT_REASON) |
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.
Maybe this should not be an optional argument since we are passing it on initialization.
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 want to be able to initialize without the reason given
if response = payment.payment_method.try_void(payment) | ||
payment.send(:handle_void_response, response) | ||
else | ||
payment.refunds.create!(amount: payment.amount, reason: refund_reason) |
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.
Might need to use the credit_allowed
method here instead of amount
?
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.
Thanks, Jordan. Fixed
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 like that there's a test for canceling a partially refunded payment, and that that works well now. I understand this is a pretty sensitive area of Solidus, but I do think this should get in.
It is worth mentioning that a payment method that does not implement |
@jordan-brough I fixed the refund amount. It now uses |
I am very much for this, but would like a dedicated changelog entry detailing that this is happening and what it means. |
# | ||
# @param payment [Spree::Payment] - the payment that should be canceled | ||
# | ||
def cancel!(payment) |
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.
This should probably be just cancel
(without the !
)
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.
Fixed
@@ -55,7 +55,7 @@ def void(_response_code, _credit_card, _options = {}) | |||
ActiveMerchant::Billing::Response.new(true, 'Bogus Gateway: Forced success', {}, test: true, authorization: '12345') | |||
end | |||
|
|||
def cancel(_response_code) | |||
def try_void(_payment) |
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.
A YARDoc comment would be helpful here, the return values are a little confusing:
ActiveMerchant::Billing::Response
withtrue
if the void succeededActiveMerchant::Billing::Response
withfalse
if the void failed- Just
false
if it can't be voided at this time.
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.
added
It means nothing to existing stores as all existing payment methods do not implement But I will add a note about the deprecation |
71b4b84
to
aa3ee8d
Compare
@mamhoff @jhawthorn addressed your feedback |
This new class handles cancellations of payments called by order cancel. This new class changes the way we cancel payments in that it asks the payment method to try a void. If that succeeds it handles the void as always. But if that fails - because the transaction is in a un-voidable state - it creates a refund about the amount instead.
In order to get a proper payment state - with refunds created when void is not possible - I needed to update the store credit spec in that it now tests that the payment handles the refund, in contrare to the former directly credit approach. Latter is not supported anymore.
The problem
When canceling payments Solidus handles all response as a void response.
That assumption is unfortunate. Lots of payment providers only allow voiding payments if they are not settled yet. Braintree is such a provider. After a transaction has settled you cannot void anymore but have to refund instead.
The
solidus_braintree
andsolidus_paypal_braintree
integrations handle this by checking the status of the transaction first and refund if the transaction cannot be voided anymore. Although this seems correct gateway wise, it is not for Solidus.For a refunded payment we need to have a
Spree::Refund
object. Handling every response as a void and not as refund - if it should - is wrong and leads to incorrect payment state.Solution
A new method
Spree::PaymentMethod#try_void
Spree::Payment#cancel!
has changed to tell the payment method to try a void. If that succeeds we handle the response as a void - like we do currently -, but if that fails we create a refund with the full amount and a "Order canceled" reason.Spree::Refund
already talks with the gateway and credits accordingly.Fixes #2104