diff --git a/app/subscribers/solidus_stripe/webhook/payment_intent_subscriber.rb b/app/subscribers/solidus_stripe/webhook/payment_intent_subscriber.rb index c6824c7e..efc3bb26 100644 --- a/app/subscribers/solidus_stripe/webhook/payment_intent_subscriber.rb +++ b/app/subscribers/solidus_stripe/webhook/payment_intent_subscriber.rb @@ -8,6 +8,7 @@ class PaymentIntentSubscriber handle :"stripe.payment_intent.succeeded", with: :complete_payment handle :"stripe.payment_intent.payment_failed", with: :fail_payment + handle :"stripe.payment_intent.canceled", with: :void_payment # Captures a payment. # @@ -47,6 +48,26 @@ def fail_payment(event) end end + # Voids a payment. + # + # Voids a Solidus payment associated to a Stripe payment intent, adding a + # log entry about the event. + # + # @param event [SolidusStripe::Webhook::Event] + def void_payment(event) + payment = extract_payment_from_event(event) + return if payment.void? + + reason = event.data.object.cancellation_reason + payment.void!.tap do + SolidusStripe::LogEntries.payment_log( + payment, + success: true, + message: "Payment was voided after payment_intent.voided webhook (#{reason})" + ) + end + end + private def extract_payment_from_event(event) diff --git a/lib/solidus_stripe/webhook/event.rb b/lib/solidus_stripe/webhook/event.rb index 7e68fd30..d8909974 100644 --- a/lib/solidus_stripe/webhook/event.rb +++ b/lib/solidus_stripe/webhook/event.rb @@ -16,6 +16,7 @@ class Event CORE_EVENTS = Set[*%i[ payment_intent.succeeded payment_intent.payment_failed + payment_intent.canceled ]].freeze private_constant :CORE_EVENTS diff --git a/spec/requests/solidus_stripe/webhooks_controller/payment_intent/canceled_spec.rb b/spec/requests/solidus_stripe/webhooks_controller/payment_intent/canceled_spec.rb new file mode 100644 index 00000000..4a18849d --- /dev/null +++ b/spec/requests/solidus_stripe/webhooks_controller/payment_intent/canceled_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.canceled" do + it "transitions the associated payment to failed" do + payment_method = create(:stripe_payment_method) + stripe_payment_intent = Stripe::PaymentIntent.construct_from(id: "pi_123", cancellation_reason: "duplicate") + 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.canceled" + ) + + expect do + webhook_request(context) + end.to change { payment.reload.state }.from("pending").to("void") + 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 index 88c55850..52e75352 100644 --- a/spec/subscribers/solidus_stripe/webhook/payment_intent_subscriber_spec.rb +++ b/spec/subscribers/solidus_stripe/webhook/payment_intent_subscriber_spec.rb @@ -124,4 +124,65 @@ expect(payment.log_entries.count).to be(0) end end + + describe "#void_payment" do + it "voids a pending payment" do + payment_method = create(:stripe_payment_method) + stripe_payment_intent = Stripe::PaymentIntent.construct_from(id: "pi_123", cancellation_reason: "duplicate") + 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.canceled" + ).solidus_stripe_object + + described_class.new.void_payment(event) + + expect(payment.reload.state).to eq "void" + 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", cancellation_reason: "duplicate") + 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.canceled" + ).solidus_stripe_object + + described_class.new.void_payment(event) + + details = payment.log_entries.last.parsed_details + expect(details.success?).to be(true) + expect( + details.message + ).to eq "Payment was voided after payment_intent.voided webhook (duplicate)" + end + + it "does nothing if the payment is already voided" 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: "void") + event = SolidusStripe::Webhook::EventWithContextFactory.from_object( + payment_method: payment_method, + object: stripe_payment_intent, + type: "payment_intent.payment_void", + ).solidus_stripe_object + + described_class.new.void_payment(event) + + expect(payment.reload.state).to eq "void" + expect(payment.log_entries.count).to be(0) + end + end end