-
-
Notifications
You must be signed in to change notification settings - Fork 60
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Stripe doesn't guarantee that a webhook event will be delivered only once, so we need to be prepared to handle duplicated incoming refund events. Just creating a Solidus copy whenever we receive a refund event is not enough. On top of that, Stripe won't identify the refund that triggered the event. Both the `charge.refunded` and `payment_intent.succeeded` (for partial captures, which generates a refund for the remaining amount under the hood) webhooks only give us information to retrieve the list of all refunds associated with a charge or payment intent. Because of this, we are now syncing all the refunds associated with a payment intent at every webhook event. We keep track of the refunds already present on Solidus by leveraging the `transaction_id` field on `Spree::Refund`, copying the Stripe refund id as value. On top of that, we need to account for refunds created from Solidus admin panel, which calls the Stripe API. In this case, we need to avoid syncing the refund back to Solidus on the subsequent webhook. We're marking those refunds with a metadata field on Stripe, so we can exclude them from the sync. Closes #262
- Loading branch information
1 parent
d86549b
commit cd6d5d3
Showing
9 changed files
with
401 additions
and
199 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
# frozen_string_literal: true | ||
|
||
require "solidus_stripe/money_to_stripe_amount_converter" | ||
|
||
module SolidusStripe | ||
# Synchronizes refunds from Stripe to Solidus. | ||
# | ||
# For our use case, Stripe has two ways to inform us about refunds initiated | ||
# on their side: | ||
# | ||
# 1. The `charge.refunded` webhook event, which is triggered when a refund is | ||
# explicitly created. | ||
# 2. The `payment_intent.succeeded` webhook event, which is triggered when a | ||
# payment intent is captured. If the payment intent is captured for less than | ||
# the full amount, a refund is automatically created for the remaining amount. | ||
# | ||
# In both cases, Stripe doesn't tell us which refund was recently created, so | ||
# we need to fetch all the refunds for the payment intent and check if any of | ||
# them is missing on Solidus. We're using the `transaction_id` field on | ||
# `Spree::Refund` to match refunds against Stripe refunds ids. We could think | ||
# about only syncing the single refund not present on Solidus, but we need to | ||
# acknowledge concurrent partial refunds. | ||
# | ||
# The `Spree::RefundReason` with `SolidusStripe::Config.refund_reason_name` | ||
# as name is used as created refunds' reason. | ||
# | ||
# Besides, we need to account for refunds created from Solidus admin panel, | ||
# which calls the Stripe API. In this case, we need to avoid syncing the | ||
# refund back to Solidus on the subsequent webhook, otherwise we would end up | ||
# with duplicate records. We're marking those refunds with a metadata field on | ||
# Stripe, so we can filter them out (see {Gateway#credit}). | ||
class RefundsSynchronizer | ||
include MoneyToStripeAmountConverter | ||
|
||
# Metadata key used to mark refunds that shouldn't be synced back to Solidus. | ||
# @return [Symbol] | ||
SKIP_SYNC_METADATA_KEY = :solidus_skip_sync | ||
|
||
# Metadata value used to mark refunds that shouldn't be synced back to Solidus. | ||
# @return [String] | ||
SKIP_SYNC_METADATA_VALUE = 'true' | ||
|
||
# @param payment_method [SolidusStripe::PaymentMethod] | ||
def initialize(payment_method) | ||
@payment_method = payment_method | ||
end | ||
|
||
# @param payment_intent_id [String] | ||
def call(payment_intent_id) | ||
payment = Spree::Payment.find_by!(response_code: payment_intent_id) | ||
|
||
stripe_refunds(payment_intent_id) | ||
.select(&method(:stripe_refund_needs_sync?)) | ||
.map( | ||
&method(:create_refund).curry[payment] | ||
) | ||
end | ||
|
||
private | ||
|
||
def stripe_refunds(payment_intent_id) | ||
@payment_method.gateway.request do | ||
Stripe::Refund.list(payment_intent: payment_intent_id).data | ||
end | ||
end | ||
|
||
def stripe_refund_needs_sync?(refund) | ||
(refund.metadata[SKIP_SYNC_METADATA_KEY] != SKIP_SYNC_METADATA_VALUE) && | ||
Spree::Refund.find_by(transaction_id: refund.id).nil? | ||
end | ||
|
||
def create_refund(payment, refund) | ||
Spree::Refund.create!( | ||
payment: payment, | ||
amount: refund_decimal_amount(refund), | ||
transaction_id: refund.id, | ||
reason: SolidusStripe::PaymentMethod.refund_reason | ||
).tap(&method(:log_refund).curry[payment]) | ||
end | ||
|
||
def log_refund(payment, refund) | ||
SolidusStripe::LogEntries.payment_log( | ||
payment, | ||
success: true, | ||
message: "Payment was refunded after Stripe event (#{refund.money})" | ||
) | ||
end | ||
|
||
def refund_decimal_amount(refund) | ||
to_solidus_amount(refund.amount, refund.currency) | ||
.then { |amount| solidus_subunit_to_decimal(amount, refund.currency) } | ||
end | ||
end | ||
end |
Oops, something went wrong.