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 a default implementation for PaymentMethod#try_void #4843

Merged

Conversation

kennyadsl
Copy link
Member

Summary

We use to replicate this behavior on all the payment extensions that add a payment integration. Still, the logic is pretty similar all the times.

By adding this default behavior we can save some time integrating new payment methods, unelss something special is needed.

The default try_void method added will just check if the void attempt is successful, otherwise it will return false. This will determine if Solidus will emit a refund along with an order cancellation.

The method is a bit complex because void can have different arity, based on if the payment method support payment profiles or not. See payment processing for more information.

This PR comes from this conversation on PayPal Commerce Platform.

Checklist

Check out our PR guidelines for more details.

The following are mandatory for all PRs:

  • I have written a thorough PR description.
  • I have kept my commits small and atomic.
  • I have used clear, explanatory commit messages.

The following are not always needed (cross them out if they are not):

  • I have added automated tests to cover my changes.
  • I have attached screenshots to demo visual changes.
  • I have opened a PR to update the guides.
  • I have updated the README to account for my changes.

@kennyadsl kennyadsl self-assigned this Jan 9, 2023
@kennyadsl kennyadsl requested a review from a team as a code owner January 9, 2023 14:03
@github-actions github-actions bot added the changelog:solidus_core Changes to the solidus_core gem label Jan 9, 2023
@kennyadsl kennyadsl force-pushed the kennyadsl/default-payment-try-void branch from efdc735 to 40eacea Compare January 9, 2023 14:50
core/app/models/spree/payment_method.rb Outdated Show resolved Hide resolved
Comment on lines 182 to 187
def try_void(payment)
void_attempt = case gateway.method(:void).arity
when -2
void(nil, { originator: payment })
when -3
void(nil, nil, { originator: payment })
end
Copy link
Member

Choose a reason for hiding this comment

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

For consistency with other placed deciding which arity to use I would make it dependent on the result of payment_profiles_supported? as in:

if payment_method.payment_profiles_supported?
# Gateways supporting payment profiles will need access to credit card object because this stores the payment profile information
# so supply the authorization itself as well as the credit card, rather than just the authorization code
response = payment_method.void(response_code, source, gateway_options)
else
# Standard ActiveMerchant void usage
response = payment_method.void(response_code, gateway_options)
end

Not sure there's a way to abstract it or maybe stop having two signatures, but for the time being I would suggest we stick to the same check, so it will be easier to refactor in the future.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done, looks cleaner for sure, and I think we will notice tests failing on the try_void method if that implementation changes.

let(:payment) { create(:payment, payment_method: payment_method) }

context "when the payment gateway void has arity = -3" do
class PaymentGatewayVoidable
Copy link
Member

Choose a reason for hiding this comment

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

caveat: the class / module keywords will create a nesting scope related to the top-level, while the describe blocks won't create one. This means that TestPaymentMethodWithoutTryVoid, PaymentGateway etc. are defined on the top-level (aka Object).

A possible alternative is to use anonymous classes or a file-specific namespace in which to store these fixtures:

module SpreePaymentMethodSpec
  class TestPaymentMethodWithoutTryVoid < Spree::PaymentMethod
    attr_accessor :gateway_class, :payment_profiles_supported?
  end
  
  class PaymentGatewayWithoutSource
    def initialize(_options)
    end

    def void(_response_code, gateway_options = {})
      payment = gateway_options[:originator]

      if payment.completed?
        ActiveMerchant::Billing::Response.new(false, "Can't void a completed payment", {}, test: true)
      else
        ActiveMerchant::Billing::Response.new(true, "Payment correctly voided", {}, test: true)
      end
    end
  end
  
  class PaymentGatewayWithSource
    def initialize(_options)
    end

    def void(_response_code, _source, gateway_options = {})
      payment = gateway_options[:originator]

      if payment.completed?
        ActiveMerchant::Billing::Response.new(false, "Can't void a completed payment", {}, test: true)
      else
        ActiveMerchant::Billing::Response.new(true, "Payment correctly voided", {}, test: true)
      end
    end
  end
end

RSpec.describe Spree::PaymentMethod, type: :model do
  # ...
  
  describe "#try_void" do
    let(:payment) { create(:payment, payment_method: payment_method) }
    
    context "when the payment supports profiles" do
      let(:payment_method) { SpreePaymentMethodSpec::TestPaymentMethodWithoutTryVoid.new(
        gateway_class: SpreePaymentMethodSpec::PaymentGatewayWithSource, 
        payment_profiles_supported?: true
      ) }

      # ...

Copy link
Member Author

Choose a reason for hiding this comment

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

@elia done a first pass with anonymous classes. I added a preliminary commit to refactor the rest of the file, which was using the same approach I used (that's why I went that route). I have the feeling we are losing a bit of readability in specs, at least now it seems harder to imagine what real Payment Method/Gateway classes look like. I'm open to giving it another shot if that's too cryptic.

Copy link
Member

Choose a reason for hiding this comment

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

I think it's fine, any solution in this space has trade-offs and at least this isn't messing up with top-level constants 👍

to avoid polluting the global namespace, in fact those
classes were defined on a global Object and avaiable in
all the rest of the test suite after their first run.

This will reduce the possibility of flacky specs.
@kennyadsl kennyadsl force-pushed the kennyadsl/default-payment-try-void branch from 40eacea to 0896273 Compare January 9, 2023 16:02
Copy link
Member

@elia elia left a comment

Choose a reason for hiding this comment

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

👍

Copy link
Member

@tvdeyen tvdeyen 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

We use to replicate this behavior on all the payment extensions
that add a payment integration. Still, the logic is pretty similar
all the times.

By adding this default behavior we can save some time integrating
new payment methods, unelss something special is needed.

The default try_void method added will just check if the void attempt
is successful, otherwise it will return false. This will determine if
Solidus will emit a refund along with an order cancellation.

The method is a bit complex because void can have different arity, based
on if the payment method support payment profiles or not. See [1] for
more information.

One thing worth mentioning: the self.name definition in the anonymous
payment method classes in tests is needed for Rails 5.2.

[1]: https://github.com/solidusio/solidus/blob/16d1e3704fbbe630a584c919de1a640a6ab6c6f8/core/app/models/spree/payment/processing.rb#L74-L81
@kennyadsl kennyadsl force-pushed the kennyadsl/default-payment-try-void branch from 0896273 to a52c467 Compare January 9, 2023 17:08
Copy link
Contributor

@waiting-for-dev waiting-for-dev left a comment

Choose a reason for hiding this comment

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

👍

@kennyadsl kennyadsl added the type:enhancement Proposed or newly added feature label Jan 10, 2023
@kennyadsl kennyadsl merged commit 843a9a1 into solidusio:master Jan 10, 2023
@kennyadsl kennyadsl deleted the kennyadsl/default-payment-try-void branch January 10, 2023 09:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
changelog:solidus_core Changes to the solidus_core gem type:enhancement Proposed or newly added feature
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants