Merge pull request #7990 from Matt-Yorkley/void-transactions

Cancel payments on stock shortages
This commit is contained in:
Matt-Yorkley
2021-08-05 00:38:33 +02:00
committed by GitHub
5 changed files with 90 additions and 57 deletions

View File

@@ -119,7 +119,7 @@ class CheckoutController < ::BaseController
# The checkout could not complete due to stock running out. We void any pending (incomplete)
# Stripe payments here as the order will need to be changed and resubmitted (or abandoned).
@order.payments.incomplete.each do |payment|
payment.void!
payment.void_transaction!
payment.adjustment&.update_columns(eligible: false, state: "finalized")
end
flash[:notice] = I18n.t("checkout.payment_cancelled_due_to_stock")

View File

@@ -12,6 +12,10 @@ module Spree
class StripeSCA < Gateway
include FullUrlHelper
VOIDABLE_STATES = [
"requires_payment_method", "requires_capture", "requires_confirmation", "requires_action"
]
preference :enterprise_id, :integer
validate :ensure_enterprise_selected
@@ -78,7 +82,13 @@ module Spree
payment_intent_response = Stripe::PaymentIntent.retrieve(payment_intent_id,
stripe_account: stripe_account_id)
gateway_options[:stripe_account] = stripe_account_id
provider.refund(refundable_amount(payment_intent_response), response_code, gateway_options)
# If a payment has been confirmed it cannot be voided by Stripe, and must be refunded instead
if voidable?(payment_intent_response)
provider.void(response_code, gateway_options)
else
provider.refund(refundable_amount(payment_intent_response), response_code, gateway_options)
end
end
# NOTE: the name of this method is determined by Spree::Payment::Processing
@@ -96,6 +106,10 @@ module Spree
private
def voidable?(payment_intent_response)
VOIDABLE_STATES.include? payment_intent_response.status
end
def refundable_amount(payment_intent_response)
payment_intent_response.amount_received -
payment_intent_response.charges.data.map(&:amount_refunded).sum

View File

@@ -126,7 +126,7 @@ describe CheckoutController, type: :controller do
end
it "cancels the payment and resets the order to cart" do
expect(payment).to receive(:void!).and_call_original
expect(payment).to receive(:void_transaction!).and_call_original
spree_post :edit

View File

@@ -137,7 +137,7 @@ describe Spree::Admin::PaymentsController, type: :controller do
end
context "StripeSCA" do
context "requesting a refund on a payment" do
context "voiding a payment" do
let(:params) { { id: payment.id, order_id: order.number, e: :void } }
# Required for the respond override in the controller decorator to work
@@ -156,14 +156,79 @@ describe Spree::Admin::PaymentsController, type: :controller do
allow(StripeAccount).to receive(:find_by) { stripe_account }
end
context "where the request succeeds" do
context "when the payment has been confirmed" do
context "where the request succeeds" do
before do
stub_payment_intent_get_request(response: { intent_status: "succeeded" })
# Issues the refund
stub_request(:post, "https://api.stripe.com/v1/charges/ch_1234/refunds").
with(basic_auth: ["sk_test_12345", ""]).
to_return(status: 200,
body: JSON.generate(id: 're_123', object: 'refund', status: 'succeeded') )
end
it "voids the payment" do
order.reload
expect(order.payment_total).to_not eq 0
expect(order.outstanding_balance.to_f).to eq 0
spree_put :fire, params
expect(payment.reload.state).to eq 'void'
order.reload
expect(order.payment_total).to eq 0
expect(order.outstanding_balance.to_f).to_not eq 0
end
end
context "where the request fails" do
before do
stub_payment_intent_get_request(response: { intent_status: "succeeded" })
stub_request(:post, "https://api.stripe.com/v1/charges/ch_1234/refunds").
with(basic_auth: ["sk_test_12345", ""]).
to_return(status: 200, body: JSON.generate(error: { message: "Bup-bow!" }) )
end
it "does not void the payment" do
order.reload
expect(order.payment_total).to_not eq 0
expect(order.outstanding_balance.to_f).to eq 0
spree_put :fire, params
expect(payment.reload.state).to eq 'completed'
order.reload
expect(order.payment_total).to_not eq 0
expect(order.outstanding_balance.to_f).to eq 0
expect(flash[:error]).to eq "Bup-bow!"
end
end
context "when a partial refund has already been issued" do
before do
stub_payment_intent_get_request(response: { intent_status: "succeeded", amount_refunded: 200 })
stub_request(:post, "https://api.stripe.com/v1/charges/ch_1234/refunds").
with(basic_auth: ["sk_test_12345", ""]).
to_return(status: 200,
body: JSON.generate(id: 're_123', object: 'refund', status: 'succeeded') )
end
it "can still void the payment" do
order.reload
expect(order.payment_total).to_not eq 0
expect(order.outstanding_balance.to_f).to eq 0
spree_put :fire, params
expect(payment.reload.state).to eq 'void'
order.reload
expect(order.payment_total).to eq 0
expect(order.outstanding_balance.to_f).to_not eq 0
end
end
end
context "when the payment has not been confirmed yet" do
before do
stub_payment_intent_get_request
# Issues the refund
stub_request(:post, "https://api.stripe.com/v1/charges/ch_1234/refunds").
stub_payment_intent_get_request(response: { intent_status: "requires_action" })
stub_request(:post, "https://api.stripe.com/v1/payment_intents/pi_123/cancel").
with(basic_auth: ["sk_test_12345", ""]).
to_return(status: 200,
body: JSON.generate(id: 're_123', object: 'refund', status: 'succeeded') )
body: JSON.generate(id: 'pi_123', object: 'payment_intent', status: 'canceled') )
end
it "voids the payment" do
@@ -177,48 +242,6 @@ describe Spree::Admin::PaymentsController, type: :controller do
expect(order.outstanding_balance.to_f).to_not eq 0
end
end
context "where the request fails" do
before do
stub_payment_intent_get_request
stub_request(:post, "https://api.stripe.com/v1/charges/ch_1234/refunds").
with(basic_auth: ["sk_test_12345", ""]).
to_return(status: 200, body: JSON.generate(error: { message: "Bup-bow!" }) )
end
it "does not void the payment" do
order.reload
expect(order.payment_total).to_not eq 0
expect(order.outstanding_balance.to_f).to eq 0
spree_put :fire, params
expect(payment.reload.state).to eq 'completed'
order.reload
expect(order.payment_total).to_not eq 0
expect(order.outstanding_balance.to_f).to eq 0
expect(flash[:error]).to eq "Bup-bow!"
end
end
context "when a partial refund has already been issued" do
before do
stub_payment_intent_get_request(response: { amount_refunded: 200 })
stub_request(:post, "https://api.stripe.com/v1/charges/ch_1234/refunds").
with(basic_auth: ["sk_test_12345", ""]).
to_return(status: 200,
body: JSON.generate(id: 're_123', object: 'refund', status: 'succeeded') )
end
it "can still void the payment" do
order.reload
expect(order.payment_total).to_not eq 0
expect(order.outstanding_balance.to_f).to eq 0
spree_put :fire, params
expect(payment.reload.state).to eq 'void'
order.reload
expect(order.payment_total).to eq 0
expect(order.outstanding_balance.to_f).to_not eq 0
end
end
end
end

View File

@@ -140,12 +140,8 @@ feature '
let(:payment) { OrderPaymentFinder.new(order.reload).last_payment }
before do
stub_payment_intent_get_request stripe_account_header: false
stub_successful_capture_request order: order
payment.update response_code: "pi_123", amount: order.total
payment.purchase!
payment.update response_code: "pi_123", amount: order.total, state: "completed"
stub_payment_intent_get_request response: { intent_status: "succeeded" }, stripe_account_header: false
stub_refund_request
end