Skip to content

Commit

Permalink
Add a default implementation for PaymentMethod#try_void
Browse files Browse the repository at this point in the history
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.

[1]: https://github.com/solidusio/solidus/blob/16d1e3704fbbe630a584c919de1a640a6ab6c6f8/core/app/models/spree/payment/processing.rb#L74-L81
  • Loading branch information
kennyadsl committed Jan 9, 2023
1 parent 16d1e37 commit 40eacea
Show file tree
Hide file tree
Showing 2 changed files with 102 additions and 10 deletions.
27 changes: 17 additions & 10 deletions core/app/models/spree/payment_method.rb
Original file line number Diff line number Diff line change
Expand Up @@ -173,16 +173,23 @@ def supports?(_source)
# Return +false+ or +nil+ if the void is not possible anymore - because it was already processed by the bank.
# Solidus will refund the amount of the payment in this case.
#
# @return [ActiveMerchant::Billing::Response] with +true+ if the void succeeded
# @return [ActiveMerchant::Billing::Response] with +false+ if the void failed
# @return [false] if it can't be voided at this time
#
def try_void(_payment)
raise ::NotImplementedError,
"You need to implement `try_void` for #{self.class.name}. In that " \
'return a ActiveMerchant::Billing::Response object if the void succeeds '\
'or `false|nil` if the void is not possible anymore. ' \
'Solidus will refund the amount of the payment then.'
# This default implementation will void the payment if void succeed,
# otherwise it returns false.
#
# @api public
# @param payment [Spree::Payment] the payment to void
# @return [ActiveMerchant::Billing::Response|FalseClass]
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

return void_attempt if void_attempt.success?

false
end

def store_credit?
Expand Down
85 changes: 85 additions & 0 deletions core/spec/models/spree/payment_method_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,91 @@ def gateway_class
end
end

describe "#try_void" do
let(:payment_method) { TestPaymentMethodWithoutTryVoid.new }
let(:payment) { create(:payment, payment_method: payment_method) }

context "when the payment gateway void has arity = -3" do
class PaymentGatewayVoidable
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

class TestPaymentMethodWithoutTryVoid < Spree::PaymentMethod
# We are intentionally not defining try_void on this payment method.
# In this way the following specs will use the default implementation
# on Spree::PaymentMethod.

def gateway_class
PaymentGatewayVoidable
end
end

context "when the payment is already captured" do
it "returns false" do
allow(payment).to receive(:completed?).and_return true
expect(payment.payment_method.try_void(payment)).to be_falsey
end
end

context "when the payment is not yet captured" do
it "returns the success response" do
expect(payment.payment_method.try_void(payment)).to be_success
end
end
end

context "when the payment gateway void has arity = -2" do
class PaymentGatewayVoidableWithoutSource
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 TestPaymentMethodWithoutTryVoid < Spree::PaymentMethod
# We are intentionally not defining try_void on this payment method.
# In this way the following specs will use the default implementation
# on Spree::PaymentMethod.

def gateway_class
PaymentGatewayVoidableWithoutSource
end
end

context "when the payment is already captured" do
it "returns false" do
allow(payment).to receive(:completed?).and_return true
expect(payment.payment_method.try_void(payment)).to be_falsey
end
end

context "when the payment is not yet captured" do
it "returns the success response" do
expect(payment.payment_method.try_void(payment)).to be_success
end
end
end
end

describe 'model_name.human' do
context 'PaymentMethod itself' do
it "returns i18n value" do
Expand Down

0 comments on commit 40eacea

Please sign in to comment.