Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion app/models/spree/gateway/stripe_gateway.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,19 @@ def authorize(money, creditcard, gateway_options)
end

def capture(money, response_code, gateway_options)
provider.capture(money, response_code, gateway_options)
response = provider.capture(money, response_code, gateway_options)
return response if response.success?

if response.params.is_a?(Hash) && response.params.dig('error', 'code') == 'charge_already_captured'
return ActiveMerchant::Billing::Response.new(
true,
'Charge was already captured on Stripe',
response.params,
authorization: response_code
)
end

response
end

def credit(money, creditcard, response_code, gateway_options)
Expand Down
75 changes: 73 additions & 2 deletions spec/models/gateway/stripe_gateway_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -143,11 +143,11 @@
end

it 'convert the amount to cents' do
provider.should_receive(:capture).with(1234,anything,anything)
provider.should_receive(:capture).with(1234,anything,anything).and_return(double(success?: true))
end

it 'use the response code as the authorization' do
provider.should_receive(:capture).with(anything,'response_code',anything)
provider.should_receive(:capture).with(anything,'response_code',anything).and_return(double(success?: true))
end
end

Expand Down Expand Up @@ -198,3 +198,74 @@
end
end
end

# Regression for spree/spree#11974: when a charge is captured directly on the
# Stripe dashboard and then captured again from Spree, ActiveMerchant returns
# an error with code 'charge_already_captured'. We treat this as a successful
# capture so the Spree payment transitions to completed instead of failed.
#
# This describe intentionally sits outside the block above so it does not
# inherit the legacy `.stub` syntax used in the older specs.
describe Spree::Gateway::StripeGateway, '#capture when Stripe already captured the charge' do
let(:provider) { double('provider') }

let(:gateway) do
described_class.new.tap do |g|
g.preferences = { secret_key: 'key' }
allow(g).to receive(:provider).and_return(provider)
end
end

let(:already_captured_response) do
ActiveMerchant::Billing::Response.new(
false,
'Charge ch_123 has already been captured.',
{
'error' => {
'code' => 'charge_already_captured',
'message' => 'Charge ch_123 has already been captured.',
'type' => 'invalid_request_error'
}
},
{}
)
end

before do
allow(provider).to receive(:capture).and_return(already_captured_response)
end

subject(:capture_result) { gateway.capture(1234, 'ch_123', {}) }

it 'returns a successful response' do
expect(capture_result.success?).to be true
end

it 'uses the original response_code as the authorization' do
expect(capture_result.authorization).to eq('ch_123')
end

it 'preserves the Stripe error params on the response' do
expect(capture_result.params.dig('error', 'code')).to eq('charge_already_captured')
end

context 'when Stripe returns a different error' do
let(:other_error_response) do
ActiveMerchant::Billing::Response.new(
false,
'Your card was declined.',
{ 'error' => { 'code' => 'card_declined' } },
{}
)
end

before do
allow(provider).to receive(:capture).and_return(other_error_response)
end

it 'returns the original failed response untouched' do
expect(capture_result.success?).to be false
expect(capture_result.params.dig('error', 'code')).to eq('card_declined')
end
end
end