diff --git a/.rubocop.yml b/.rubocop.yml index 868cf06b..a47c2538 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -42,3 +42,7 @@ Rails/NotNullColumn: Layout/LineLength: Exclude: - db/migrate/**/* + +RSpec/FilePath: + Exclude: + - spec/requests/solidus_stripe/webhooks_controller/**/* diff --git a/app/subscribers/solidus_stripe/webhook/payment_intent_subscriber.rb b/app/subscribers/solidus_stripe/webhook/payment_intent_subscriber.rb new file mode 100644 index 00000000..6c144b8c --- /dev/null +++ b/app/subscribers/solidus_stripe/webhook/payment_intent_subscriber.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module SolidusStripe + module Webhook + # Handlers for Stripe payment_intent events. + class PaymentIntentSubscriber + include Omnes::Subscriber + + handle :"stripe.payment_intent.succeeded", with: :complete_payment + + # Captures a payment. + # + # Marks a Solidus payment associated to a Stripe payment intent as + # completed, adding a log entry about the event. + # + # @param event [SolidusStripe::Webhook::Event] + def complete_payment(event) + payment_intent_id = event.data.object.id + payment = Spree::Payment.find_by!(response_code: payment_intent_id) + return if payment.completed? + + payment.complete!.tap do + SolidusStripe::LogEntries.payment_log( + payment, + success: true, + message: "Capture was successful after payment_intent.succeeded webhook" + ) + end + end + end + end +end diff --git a/lib/solidus_stripe/engine.rb b/lib/solidus_stripe/engine.rb index b49405b4..8ded1ef5 100644 --- a/lib/solidus_stripe/engine.rb +++ b/lib/solidus_stripe/engine.rb @@ -21,6 +21,7 @@ class Engine < Rails::Engine user_events: SolidusStripe.configuration.webhook_events, bus: Spree::Bus ) + SolidusStripe::Webhook::PaymentIntentSubscriber.new.subscribe_to(Spree::Bus) end end diff --git a/lib/solidus_stripe/webhook/event.rb b/lib/solidus_stripe/webhook/event.rb index c7a4d1c6..9f0995bf 100644 --- a/lib/solidus_stripe/webhook/event.rb +++ b/lib/solidus_stripe/webhook/event.rb @@ -13,8 +13,9 @@ class Event PREFIX = "stripe." private_constant :PREFIX - # TBD - CORE_EVENTS = Set[*%i[]].freeze + CORE_EVENTS = Set[*%i[ + payment_intent.succeeded + ]].freeze private_constant :CORE_EVENTS # @api private diff --git a/spec/requests/solidus_stripe/webhooks_controller/payment_intent/succeeded_spec.rb b/spec/requests/solidus_stripe/webhooks_controller/payment_intent/succeeded_spec.rb new file mode 100644 index 00000000..b1a2cdfc --- /dev/null +++ b/spec/requests/solidus_stripe/webhooks_controller/payment_intent/succeeded_spec.rb @@ -0,0 +1,23 @@ +require "solidus_stripe_spec_helper" + +RSpec.describe SolidusStripe::WebhooksController, type: %i[request webhook_request] do + describe "POST /create payment_intent.succeeded" do + it "transitions the associated payment to completed" do + payment_method = create(:stripe_payment_method) + stripe_payment_intent = Stripe::PaymentIntent.construct_from(id: "pi_123") + payment = create(:payment, + payment_method: payment_method, + response_code: stripe_payment_intent.id, + state: "pending") + context = SolidusStripe::Webhook::EventWithContextFactory.from_object( + payment_method: payment_method, + object: stripe_payment_intent, + type: "payment_intent.succeeded" + ) + + expect do + webhook_request(context) + end.to change { payment.reload.state }.from("pending").to("completed") + end + end +end diff --git a/spec/subscribers/solidus_stripe/webhook/payment_intent_subscriber_spec.rb b/spec/subscribers/solidus_stripe/webhook/payment_intent_subscriber_spec.rb new file mode 100644 index 00000000..0097124b --- /dev/null +++ b/spec/subscribers/solidus_stripe/webhook/payment_intent_subscriber_spec.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +require "solidus_stripe_spec_helper" + +RSpec.describe SolidusStripe::Webhook::PaymentIntentSubscriber do + describe "#complete_payment" do + it "completes a pending payment" do + payment_method = create(:stripe_payment_method) + stripe_payment_intent = Stripe::PaymentIntent.construct_from(id: "pi_123") + payment = create(:payment, + payment_method: payment_method, + response_code: stripe_payment_intent.id, + state: "pending") + event = SolidusStripe::Webhook::EventWithContextFactory.from_object( + payment_method: payment_method, + object: stripe_payment_intent, + type: "payment_intent.succeeded" + ).solidus_stripe_object + + described_class.new.complete_payment(event) + + expect(payment.reload.state).to eq "completed" + end + + it "adds a log entry to the payment" do + payment_method = create(:stripe_payment_method) + stripe_payment_intent = Stripe::PaymentIntent.construct_from(id: "pi_123") + payment = create(:payment, + payment_method: payment_method, + response_code: stripe_payment_intent.id, + state: "pending") + event = SolidusStripe::Webhook::EventWithContextFactory.from_object( + payment_method: payment_method, + object: stripe_payment_intent, + type: "payment_intent.succeeded" + ).solidus_stripe_object + + described_class.new.complete_payment(event) + + expect( + payment.log_entries.last.parsed_details.message + ).to eq "Capture was successful after payment_intent.succeeded webhook" + end + + it "does nothing if the payment is already completed" do + payment_method = create(:stripe_payment_method) + stripe_payment_intent = Stripe::PaymentIntent.construct_from(id: "pi_123") + payment = create(:payment, + payment_method: payment_method, + response_code: stripe_payment_intent.id, + state: "completed") + event = SolidusStripe::Webhook::EventWithContextFactory.from_object( + payment_method: payment_method, + object: stripe_payment_intent, + type: "payment_intent.succeeded", + ).solidus_stripe_object + + described_class.new.complete_payment(event) + + expect(payment.reload.state).to eq "completed" + expect(payment.log_entries.count).to be(0) + end + end +end diff --git a/spec/support/solidus_stripe/webhook/event_with_context_factory.rb b/spec/support/solidus_stripe/webhook/event_with_context_factory.rb index d2b17dd0..c6266a18 100644 --- a/spec/support/solidus_stripe/webhook/event_with_context_factory.rb +++ b/spec/support/solidus_stripe/webhook/event_with_context_factory.rb @@ -8,7 +8,8 @@ module Webhook # Factory to create a webhook event along with its context. # # It allows to create Stripe webhook from different sources (hash, stripe - # object) in different representations (json, stripe event object, header). + # object) in different representations (json, stripe event object, solidus + # stripe object, header). # # The context for a event is composed by the timestamp and its secret, which # in turn affect the header representation. @@ -56,6 +57,11 @@ def stripe_object @stripe_object ||= Stripe::Event.construct_from(data) end + def solidus_stripe_object + @solidus_stripe_object = SolidusStripe::Webhook::Event.new(stripe_event: stripe_object, + spree_payment_method: @payment_method) + end + def json @json ||= JSON.generate(data) end