diff --git a/app/models/spree/gateway/stripe_sca.rb b/app/models/spree/gateway/stripe_sca.rb index 86b8627d5b..0d73858fc3 100644 --- a/app/models/spree/gateway/stripe_sca.rb +++ b/app/models/spree/gateway/stripe_sca.rb @@ -144,8 +144,7 @@ module Spree payment = fetch_payment(creditcard, gateway_options) raise Stripe::StripeError, I18n.t(:no_pending_payments) unless payment&.response_code - payment_intent_response = Stripe::PaymentIntentValidator.new. - call(payment.response_code, stripe_account_id) + payment_intent_response = Stripe::PaymentIntentValidator.new(payment).call raise_if_not_in_capture_state(payment_intent_response) diff --git a/app/services/process_payment_intent.rb b/app/services/process_payment_intent.rb index beb92e4869..b18319d83d 100644 --- a/app/services/process_payment_intent.rb +++ b/app/services/process_payment_intent.rb @@ -69,16 +69,6 @@ class ProcessPaymentIntent end def payment_intent_status - @payment_intent_status ||= Stripe::PaymentIntentValidator.new. - call(payment_intent, stripe_account_id). - status - end - - def stripe_account_id - StripeAccount.find_by(enterprise_id: preferred_enterprise_id).stripe_user_id - end - - def preferred_enterprise_id - payment.payment_method.preferred_enterprise_id + @payment_intent_status ||= Stripe::PaymentIntentValidator.new(payment).call.status end end diff --git a/app/services/stripe_payment_status.rb b/app/services/stripe_payment_status.rb new file mode 100644 index 0000000000..0cc03607c9 --- /dev/null +++ b/app/services/stripe_payment_status.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +class StripePaymentStatus + def initialize(payment) + @payment = payment + end + + # Returns the current payment status from a live call to the Stripe API. + # Returns nil if the payment is not a Stripe payment or does not have a payment intent. + # If the payment requires authorization the status will be "requires_action". + # If the payment has been captured the status will be "succeeded". + # Docs: https://stripe.com/docs/api/payment_intents/object#payment_intent_object-status + def stripe_status + return if payment.response_code.blank? + + Stripe::PaymentIntentValidator.new(payment).call.status + rescue Stripe::StripeError + # Stripe::PaymentIntentValidator will raise an error if the response from the Stripe API + # call indicates the last attempted action on the payment intent failed. + "failed" + end + + # If the payment is a Stripe payment and has been captured in the associated Stripe account, + # returns true, otherwise false. + def stripe_captured? + stripe_status == "succeeded" + end + + private + + attr_reader :payment +end diff --git a/lib/stripe/payment_intent_validator.rb b/lib/stripe/payment_intent_validator.rb index 879a49eac2..10f176af61 100644 --- a/lib/stripe/payment_intent_validator.rb +++ b/lib/stripe/payment_intent_validator.rb @@ -3,9 +3,15 @@ # This class validates if a given payment intent ID is valid in Stripe module Stripe class PaymentIntentValidator - def call(payment_intent_id, stripe_account_id) - payment_intent_response = Stripe::PaymentIntent.retrieve(payment_intent_id, - stripe_account: stripe_account_id) + def initialize(payment) + @payment = payment + end + + def call + payment_intent_response = Stripe::PaymentIntent.retrieve( + payment_intent_id, + stripe_account: stripe_account_id + ) raise_if_last_payment_error_present(payment_intent_response) @@ -14,6 +20,18 @@ module Stripe private + attr_accessor :payment + + def payment_intent_id + payment.response_code + end + + def stripe_account_id + enterprise_id = payment.payment_method&.preferred_enterprise_id + + StripeAccount.find_by(enterprise_id: enterprise_id)&.stripe_user_id + end + def raise_if_last_payment_error_present(payment_intent_response) return unless payment_intent_response.respond_to?(:last_payment_error) && payment_intent_response.last_payment_error.present? diff --git a/spec/controllers/spree/orders_controller_spec.rb b/spec/controllers/spree/orders_controller_spec.rb index 879d663342..7776d02903 100644 --- a/spec/controllers/spree/orders_controller_spec.rb +++ b/spec/controllers/spree/orders_controller_spec.rb @@ -113,9 +113,8 @@ describe Spree::OrdersController, type: :controller do let(:payment_intent_response) { double(id: "pi_123", status: "requires_capture") } before do - allow_any_instance_of(Stripe::PaymentIntentValidator) - .to receive(:call) - .with(payment_intent, kind_of(String)) + allow(Stripe::PaymentIntentValidator) + .to receive_message_chain(:new, :call) .and_return(payment_intent_response) allow(Spree::Order).to receive(:find_by!) { order } @@ -159,9 +158,8 @@ describe Spree::OrdersController, type: :controller do let(:payment_intent) { "pi_123" } before do - allow_any_instance_of(Stripe::PaymentIntentValidator) - .to receive(:call) - .with(payment_intent, kind_of(String)) + allow(Stripe::PaymentIntentValidator) + .to receive_message_chain(:new, :call) .and_raise(Stripe::StripeError, "error message") end @@ -183,9 +181,8 @@ describe Spree::OrdersController, type: :controller do before do allow(payment).to receive(:response_code).and_return("invalid") allow(OrderPaymentFinder).to receive(:new).with(order).and_return(finder) - allow_any_instance_of(Stripe::PaymentIntentValidator) - .to receive(:call) - .with(payment_intent, kind_of(String)) + allow(Stripe::PaymentIntentValidator) + .to receive_message_chain(:new, :call) .and_return(payment_intent) stub_payment_intent_get_request(payment_intent_id: "valid") end diff --git a/spec/lib/stripe/payment_intent_validator_spec.rb b/spec/lib/stripe/payment_intent_validator_spec.rb index 81a730ab2a..12287bafe6 100644 --- a/spec/lib/stripe/payment_intent_validator_spec.rb +++ b/spec/lib/stripe/payment_intent_validator_spec.rb @@ -6,14 +6,19 @@ require 'stripe/payment_intent_validator' module Stripe describe PaymentIntentValidator do describe "#call" do - let(:validator) { Stripe::PaymentIntentValidator.new } + let(:validator) { Stripe::PaymentIntentValidator.new(payment) } + let(:payment) { build(:payment, response_code: payment_intent_id) } let(:payment_intent_id) { "pi_123" } let(:stripe_account_id) { "abc123" } + let(:stripe_account_mock) { double(stripe_user_id: stripe_account_id) } let(:payment_intent_response_mock) { { status: 200, body: payment_intent_response_body } } before do Stripe.api_key = "sk_test_12345" + allow(payment).to receive_message_chain(:payment_method, :preferred_enterprise_id) { 1 } + allow(StripeAccount).to receive(:find_by) { stripe_account_mock } + stub_request(:get, "https://api.stripe.com/v1/payment_intents/#{payment_intent_id}") .with(headers: { 'Stripe-Account' => stripe_account_id }) .to_return(payment_intent_response_mock) @@ -26,7 +31,7 @@ module Stripe it "returns payment intent id and does not raise" do expect { - result = validator.call(payment_intent_id, stripe_account_id) + result = validator.call expect(result).to eq payment_intent_response_body }.to_not raise_error Stripe::StripeError end @@ -39,7 +44,7 @@ module Stripe it "raises Stripe error with payment intent last_payment_error as message" do expect { - validator.call(payment_intent_id, stripe_account_id) + validator.call }.to raise_error Stripe::StripeError, "No money" end end diff --git a/spec/services/process_payment_intent_spec.rb b/spec/services/process_payment_intent_spec.rb index aa14085650..515ee16065 100644 --- a/spec/services/process_payment_intent_spec.rb +++ b/spec/services/process_payment_intent_spec.rb @@ -51,9 +51,8 @@ describe ProcessPaymentIntent do context "where the stripe payment intent validation responds with errors" do before do - allow(validator) - .to receive(:call).with(intent, anything).and_raise(Stripe::StripeError, - "error message") + allow(validator).to receive(:call). + and_raise(Stripe::StripeError, "error message") end it "returns returns the error message" do @@ -77,7 +76,7 @@ describe ProcessPaymentIntent do before do allow(order).to receive(:deliver_order_confirmation_email) - allow(validator).to receive(:call).with(intent, anything).and_return(intent_response) + allow(validator).to receive(:call).and_return(intent_response) end it "validates the intent" do @@ -143,7 +142,7 @@ describe ProcessPaymentIntent do before do payment.update_attribute(:state, "failed") - allow(validator).to receive(:call).with(intent, anything).and_return(intent) + allow(validator).to receive(:call).and_return(intent) end it "does not return any error message" do @@ -166,7 +165,7 @@ describe ProcessPaymentIntent do before do allow(order).to receive(:process_payments!) { nil } - allow(validator).to receive(:call).with(intent, anything).and_return(intent_response) + allow(validator).to receive(:call).and_return(intent_response) end it "returns a failed result" do diff --git a/spec/services/stripe_payment_status_spec.rb b/spec/services/stripe_payment_status_spec.rb new file mode 100644 index 0000000000..912c87e213 --- /dev/null +++ b/spec/services/stripe_payment_status_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe StripePaymentStatus do + subject { StripePaymentStatus.new(payment) } + let(:payment) { build(:payment) } + + describe '#stripe_status' do + context "when the payment is not a Stripe payment or does not have a payment intent" do + it "returns nil" do + expect(subject.stripe_status).to be_nil + end + end + + context "when the payment has a payment intent" do + before { allow(payment).to receive(:response_code) { "pi_1234" } } + + it "fetches the status with Stripe::PaymentIntentValidator" do + expect(Stripe::PaymentIntentValidator). + to receive_message_chain(:new, :call, :status) { true } + + subject.stripe_status + end + + context "and the last action on the Stripe payment failed" do + it "returns failed response" do + allow(Stripe::PaymentIntentValidator). + to receive_message_chain(:new, :call, :status).and_raise(Stripe::StripeError) + + expect(subject.stripe_status).to eq "failed" + end + end + end + end + + describe '#stripe_captured?' do + context "when the payment is not a Stripe payment or does not have a payment intent" do + it "returns false" do + expect(subject.stripe_captured?).to eq false + end + end + + context "when the Stripe payment has been captured" do + before { allow(payment).to receive(:response_code) { "pi_1234" } } + + it "returns true" do + allow(Stripe::PaymentIntentValidator). + to receive_message_chain(:new, :call, :status) { "succeeded" } + + expect(subject.stripe_captured?).to eq true + end + end + end +end