spree_stripe 1.5.1 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Rakefile +2 -0
- data/app/jobs/spree_stripe/complete_order_from_session_job.rb +12 -0
- data/app/models/spree/payment_sessions/stripe.rb +54 -0
- data/app/models/spree/payment_setup_sessions/stripe.rb +21 -0
- data/app/models/spree_stripe/custom_domain_decorator.rb +3 -1
- data/app/models/spree_stripe/gateway/payment_intents.rb +153 -0
- data/app/models/spree_stripe/gateway/payment_sessions.rb +180 -0
- data/app/models/spree_stripe/gateway/payment_setup_sessions.rb +63 -0
- data/app/models/spree_stripe/gateway.rb +55 -125
- data/app/presenters/spree_stripe/payment_intent_presenter.rb +8 -2
- data/app/presenters/spree_stripe/statement_descriptor_suffix_presenter.rb +1 -1
- data/app/services/spree_stripe/create_payment_intent.rb +1 -2
- data/app/services/spree_stripe/create_source.rb +1 -1
- data/app/services/spree_stripe/register_domain.rb +1 -1
- data/app/services/spree_stripe/webhook_handlers/base.rb +19 -0
- data/app/services/spree_stripe/webhook_handlers/payment_intent_amount_capturable_updated.rb +13 -0
- data/app/services/spree_stripe/webhook_handlers/payment_intent_payment_failed.rb +13 -1
- data/app/services/spree_stripe/webhook_handlers/payment_intent_succeeded.rb +8 -4
- data/config/initializers/stripe.rb +1 -0
- data/config/routes.rb +11 -9
- data/lib/spree_stripe/configuration.rb +8 -1
- data/lib/spree_stripe/engine.rb +13 -2
- data/lib/spree_stripe/testing_support/factories/stripe_payment_session_factory.rb +33 -0
- data/lib/spree_stripe/version.rb +1 -1
- metadata +38 -29
- /data/{app/serializers → lib/spree_api_v2}/spree/api/v2/platform/affirm_serializer.rb +0 -0
- /data/{app/serializers → lib/spree_api_v2}/spree/api/v2/platform/after_pay_serializer.rb +0 -0
- /data/{app/serializers → lib/spree_api_v2}/spree/api/v2/platform/alipay_serializer.rb +0 -0
- /data/{app/serializers → lib/spree_api_v2}/spree/api/v2/platform/ideal_serializer.rb +0 -0
- /data/{app/serializers → lib/spree_api_v2}/spree/api/v2/platform/klarna_serializer.rb +0 -0
- /data/{app/serializers → lib/spree_api_v2}/spree/api/v2/platform/link_serializer.rb +0 -0
- /data/{app/serializers → lib/spree_api_v2}/spree/api/v2/platform/przelewy24_serializer.rb +0 -0
- /data/{app/serializers → lib/spree_api_v2}/spree/api/v2/platform/sepa_debit_serializer.rb +0 -0
- /data/{app/controllers → lib/spree_api_v2}/spree/api/v2/storefront/stripe/base_controller.rb +0 -0
- /data/{app/controllers → lib/spree_api_v2}/spree/api/v2/storefront/stripe/payment_intents_controller.rb +0 -0
- /data/{app/controllers → lib/spree_api_v2}/spree/api/v2/storefront/stripe/setup_intents_controller.rb +0 -0
- /data/{app/serializers → lib/spree_api_v2}/spree/v2/storefront/affirm_serializer.rb +0 -0
- /data/{app/serializers → lib/spree_api_v2}/spree/v2/storefront/after_pay_serializer.rb +0 -0
- /data/{app/serializers → lib/spree_api_v2}/spree/v2/storefront/alipay_serializer.rb +0 -0
- /data/{app/serializers → lib/spree_api_v2}/spree/v2/storefront/ideal_serializer.rb +0 -0
- /data/{app/serializers → lib/spree_api_v2}/spree/v2/storefront/klarna_serializer.rb +0 -0
- /data/{app/serializers → lib/spree_api_v2}/spree/v2/storefront/link_serializer.rb +0 -0
- /data/{app/serializers → lib/spree_api_v2}/spree/v2/storefront/payment_intent_serializer.rb +0 -0
- /data/{app/serializers → lib/spree_api_v2}/spree/v2/storefront/przelewy24_serializer.rb +0 -0
- /data/{app/serializers → lib/spree_api_v2}/spree/v2/storefront/sepa_debit_serializer.rb +0 -0
- /data/{app/serializers → lib/spree_api_v2}/spree_stripe/v2/storefront/payment_method_serializer_decorator.rb +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a10e7ebb4ff5cc5960d0ecb894cfc0e15ef4ed058bf2133fc412ca9fc4c95910
|
|
4
|
+
data.tar.gz: 49fd3295806ed476343813ac3ce3356d74054bf84db8ecf9f4ade86099f139c3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3ceff451db2064b42fe695aaa91b0ff7c2512fa455504ba459f84a1c788731fd490d85c3a950972a3a7942acaf9cce64a2d6d2de2ce0ace9d2bfccacccc95770
|
|
7
|
+
data.tar.gz: cd0aff0b871a315fd19b5529912ce431ab5c5c35657bab1853fd2548e80591f031f79ee81bab107f64da05f985bfad7271b288fc813a87d4a583935581ad66c0
|
data/Rakefile
CHANGED
|
@@ -21,5 +21,7 @@ task :test_app do
|
|
|
21
21
|
install_storefront: true,
|
|
22
22
|
install_admin: true
|
|
23
23
|
)
|
|
24
|
+
system({ 'BUNDLE_GEMFILE' => File.expand_path('Gemfile', __dir__) }, 'rails g spree_posts:install')
|
|
24
25
|
system({ 'BUNDLE_GEMFILE' => File.expand_path('Gemfile', __dir__) }, 'rails g spree_legacy_api_v2:install')
|
|
26
|
+
system({ 'BUNDLE_GEMFILE' => File.expand_path('Gemfile', __dir__) }, 'rails g spree_legacy_product_properties:install')
|
|
25
27
|
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
module SpreeStripe
|
|
2
|
+
class CompleteOrderFromSessionJob < BaseJob
|
|
3
|
+
def perform(payment_session_id)
|
|
4
|
+
payment_session = Spree::PaymentSessions::Stripe.find(payment_session_id)
|
|
5
|
+
|
|
6
|
+
# PaymentSessions::Stripe duck-types as PaymentIntent
|
|
7
|
+
SpreeStripe::CompleteOrder.new(payment_intent: payment_session).call
|
|
8
|
+
|
|
9
|
+
payment_session.complete unless payment_session.completed?
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
class PaymentSessions::Stripe < PaymentSession
|
|
3
|
+
delegate :api_options, to: :payment_method
|
|
4
|
+
|
|
5
|
+
# Duck-type interface matching SpreeStripe::PaymentIntent
|
|
6
|
+
# This allows reuse of CompleteOrder and CreatePayment services
|
|
7
|
+
def stripe_id
|
|
8
|
+
external_id
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def client_secret
|
|
12
|
+
external_data&.dig('client_secret')
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def ephemeral_key_secret
|
|
16
|
+
external_data&.dig('ephemeral_key_secret')
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def stripe_payment_intent
|
|
20
|
+
@stripe_payment_intent ||= payment_method.retrieve_payment_intent(external_id)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def stripe_charge
|
|
24
|
+
@stripe_charge ||= begin
|
|
25
|
+
latest_charge = stripe_payment_intent.latest_charge
|
|
26
|
+
latest_charge.present? ? payment_method.retrieve_charge(latest_charge) : nil
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def accepted?
|
|
31
|
+
payment_method.payment_intent_accepted?(stripe_payment_intent)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def successful?
|
|
35
|
+
stripe_payment_intent.status == 'succeeded'
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def charge_not_required?
|
|
39
|
+
payment_method.payment_intent_charge_not_required?(stripe_payment_intent)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def find_or_create_payment!(metadata = {})
|
|
43
|
+
return unless persisted?
|
|
44
|
+
return payment if payment.present?
|
|
45
|
+
|
|
46
|
+
SpreeStripe::CreatePayment.new(
|
|
47
|
+
order: order,
|
|
48
|
+
payment_intent: self,
|
|
49
|
+
gateway: payment_method,
|
|
50
|
+
amount: amount
|
|
51
|
+
).call
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
class PaymentSetupSessions::Stripe < PaymentSetupSession
|
|
3
|
+
delegate :api_options, to: :payment_method
|
|
4
|
+
|
|
5
|
+
def stripe_id
|
|
6
|
+
external_id
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def client_secret
|
|
10
|
+
external_client_secret
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def stripe_setup_intent
|
|
14
|
+
@stripe_setup_intent ||= payment_method.retrieve_setup_intent(external_id)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def successful?
|
|
18
|
+
stripe_setup_intent.status == 'succeeded'
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
module SpreeStripe
|
|
2
|
+
class Gateway < ::Spree::Gateway
|
|
3
|
+
module PaymentIntents
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
DELAYED_NOTIFICATION_PAYMENT_METHOD_TYPES = %w[sepa_debit us_bank_account].freeze
|
|
7
|
+
BANK_PAYMENT_METHOD_TYPES = %w[customer_balance us_bank_account].freeze
|
|
8
|
+
MANUAL_CAPTURE_METHOD = 'manual'.freeze
|
|
9
|
+
|
|
10
|
+
included do
|
|
11
|
+
has_many :payment_intents, class_name: 'SpreeStripe::PaymentIntent', foreign_key: 'payment_method_id', dependent: :delete_all
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def payment_intent_accepted?(payment_intent)
|
|
15
|
+
payment_intent.status.in?(payment_intent_accepted_statuses(payment_intent))
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def payment_intent_delayed_notification?(payment_intent)
|
|
19
|
+
payment_method = payment_intent.payment_method
|
|
20
|
+
return false unless payment_method.respond_to?(:type)
|
|
21
|
+
|
|
22
|
+
payment_intent.payment_method.type.in?(DELAYED_NOTIFICATION_PAYMENT_METHOD_TYPES)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def payment_intent_charge_not_required?(payment_intent)
|
|
26
|
+
payment_intent_bank_payment_method?(payment_intent)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def payment_intent_bank_payment_method?(payment_intent)
|
|
30
|
+
payment_method = payment_intent.payment_method
|
|
31
|
+
return false unless payment_method.respond_to?(:type)
|
|
32
|
+
|
|
33
|
+
payment_intent.payment_method.type.in?(BANK_PAYMENT_METHOD_TYPES)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def payment_intent_requires_capture?(payment_intent)
|
|
37
|
+
payment_intent.status == 'requires_capture'
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def payment_intent_manual_capture?(payment_intent)
|
|
41
|
+
payment_intent.respond_to?(:capture_method) && payment_intent.capture_method == MANUAL_CAPTURE_METHOD
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Creates a Stripe payment intent for the order
|
|
45
|
+
#
|
|
46
|
+
# @param amount_in_cents [Integer] the amount in cents
|
|
47
|
+
# @param order [Spree::Order] the order to create a payment intent for
|
|
48
|
+
# @param payment_method_id [String] Stripe payment method id to use, eg. a card token
|
|
49
|
+
# @param off_session [Boolean] whether the payment intent is off session
|
|
50
|
+
# @param customer_profile_id [String] Stripe customer profile id to use, eg. cus_123
|
|
51
|
+
# @return [Spree::PaymentResponse] the response from the payment intent creation
|
|
52
|
+
def create_payment_intent(amount_in_cents, order, payment_method_id: nil, off_session: false, customer_profile_id: nil)
|
|
53
|
+
payload = SpreeStripe::PaymentIntentPresenter.new(
|
|
54
|
+
amount: amount_in_cents,
|
|
55
|
+
order: order,
|
|
56
|
+
customer: customer_profile_id || fetch_or_create_customer(order: order)&.profile_id,
|
|
57
|
+
payment_method_id: payment_method_id,
|
|
58
|
+
off_session: off_session,
|
|
59
|
+
capture_method: stripe_capture_method
|
|
60
|
+
).call
|
|
61
|
+
|
|
62
|
+
protect_from_error do
|
|
63
|
+
response = send_request { |opts| Stripe::PaymentIntent.create(payload, opts) }
|
|
64
|
+
|
|
65
|
+
success(response.id, response)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Updates a Stripe payment intent for the order
|
|
70
|
+
#
|
|
71
|
+
# @param payment_intent_id [String] Stripe payment intent id
|
|
72
|
+
# @param amount_in_cents [Integer] the amount in cents
|
|
73
|
+
# @param order [Spree::Order] the order to update the payment intent for
|
|
74
|
+
# @param payment_method_id [String] Stripe payment method id to use, eg. a card token
|
|
75
|
+
# @return [Spree::PaymentResponse] the response from the payment intent update
|
|
76
|
+
def update_payment_intent(payment_intent_id, amount_in_cents, order, payment_method_id = nil)
|
|
77
|
+
protect_from_error do
|
|
78
|
+
payload = SpreeStripe::PaymentIntentPresenter.new(
|
|
79
|
+
amount: amount_in_cents,
|
|
80
|
+
order: order,
|
|
81
|
+
customer: fetch_or_create_customer(order: order)&.profile_id,
|
|
82
|
+
payment_method_id: payment_method_id
|
|
83
|
+
).call.slice(:amount, :currency, :payment_method, :shipping, :customer)
|
|
84
|
+
|
|
85
|
+
response = send_request { |opts| Stripe::PaymentIntent.update(payment_intent_id, payload, opts) }
|
|
86
|
+
|
|
87
|
+
success(response.id, response)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def retrieve_payment_intent(payment_intent_id)
|
|
92
|
+
send_request { |opts| Stripe::PaymentIntent.retrieve({ id: payment_intent_id, expand: ['payment_method'] }, opts) }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def confirm_payment_intent(payment_intent_id)
|
|
96
|
+
send_request { |opts| Stripe::PaymentIntent.confirm(payment_intent_id, {}, opts) }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def capture_payment_intent(payment_intent_id, amount_in_cents)
|
|
100
|
+
send_request { |opts| Stripe::PaymentIntent.capture(payment_intent_id, { amount_to_capture: amount_in_cents }, opts) }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Cancels a Stripe payment intent
|
|
104
|
+
#
|
|
105
|
+
# @param payment_intent_id [String] Stripe payment intent ID, eg. pi_123
|
|
106
|
+
def cancel_payment_intent(payment_intent_id)
|
|
107
|
+
send_request { |opts| Stripe::PaymentIntent.cancel(payment_intent_id, {}, opts) }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Ensures a Stripe payment intent exists for Spree payment
|
|
111
|
+
#
|
|
112
|
+
# @param payment [Spree::Payment] the payment to ensure a payment intent exists for
|
|
113
|
+
# @param amount_in_cents [Integer] the amount in cents
|
|
114
|
+
# @param payment_source [Spree::CreditCard | Spree::PaymentSource] the payment source to use
|
|
115
|
+
# @return [Spree::Payment] the payment with the payment intent
|
|
116
|
+
def ensure_payment_intent_exists_for_payment(payment, amount_in_cents = nil, payment_source = nil)
|
|
117
|
+
return payment if payment.response_code.present?
|
|
118
|
+
|
|
119
|
+
amount_in_cents ||= payment.display_amount.cents
|
|
120
|
+
payment_source ||= payment.source
|
|
121
|
+
|
|
122
|
+
response = create_payment_intent(
|
|
123
|
+
amount_in_cents,
|
|
124
|
+
payment.order,
|
|
125
|
+
payment_method_id: payment_source.gateway_payment_profile_id,
|
|
126
|
+
off_session: true,
|
|
127
|
+
customer_profile_id: payment_source.gateway_customer_profile_id
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
payment.update_columns(
|
|
131
|
+
response_code: response.authorization,
|
|
132
|
+
updated_at: Time.current
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
payment
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
private
|
|
139
|
+
|
|
140
|
+
def stripe_capture_method
|
|
141
|
+
auto_capture? ? nil : MANUAL_CAPTURE_METHOD
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def payment_intent_accepted_statuses(payment_intent)
|
|
145
|
+
statuses = %w[succeeded]
|
|
146
|
+
statuses << 'requires_capture' if payment_intent_manual_capture?(payment_intent)
|
|
147
|
+
statuses << 'processing' if payment_intent_delayed_notification?(payment_intent)
|
|
148
|
+
statuses << 'requires_action' if payment_intent_charge_not_required?(payment_intent)
|
|
149
|
+
statuses
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
module SpreeStripe
|
|
2
|
+
class Gateway < ::Spree::Gateway
|
|
3
|
+
module PaymentSessions
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
def session_required?
|
|
7
|
+
!SpreeStripe::Config[:use_legacy_payment_intents]
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def payment_session_class
|
|
11
|
+
Spree::PaymentSessions::Stripe
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def create_payment_session(order:, amount: nil, external_data: {})
|
|
15
|
+
total = amount.presence || order.total_minus_store_credits
|
|
16
|
+
amount_in_cents = Spree::Money.new(total, currency: order.currency).cents
|
|
17
|
+
|
|
18
|
+
return nil if amount_in_cents.zero?
|
|
19
|
+
|
|
20
|
+
customer = fetch_or_create_customer(order: order)
|
|
21
|
+
stripe_pm_id = external_data[:stripe_payment_method_id] || external_data['stripe_payment_method_id']
|
|
22
|
+
|
|
23
|
+
response = create_payment_intent(
|
|
24
|
+
amount_in_cents, order,
|
|
25
|
+
payment_method_id: stripe_pm_id,
|
|
26
|
+
customer_profile_id: customer&.profile_id
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
ephemeral_key_response = create_ephemeral_key(customer.profile_id) if customer.present?
|
|
30
|
+
ephemeral_key_secret = ephemeral_key_response&.params&.dig('secret')
|
|
31
|
+
|
|
32
|
+
payment_session_class.create!(
|
|
33
|
+
order: order,
|
|
34
|
+
payment_method: self,
|
|
35
|
+
amount: total,
|
|
36
|
+
currency: order.currency,
|
|
37
|
+
status: 'pending',
|
|
38
|
+
external_id: response.authorization,
|
|
39
|
+
customer: order.user,
|
|
40
|
+
customer_external_id: customer&.profile_id,
|
|
41
|
+
external_data: {
|
|
42
|
+
'client_secret' => response.params['client_secret'],
|
|
43
|
+
'ephemeral_key_secret' => ephemeral_key_secret,
|
|
44
|
+
'stripe_payment_method_id' => stripe_pm_id
|
|
45
|
+
}.compact
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def update_payment_session(payment_session:, amount: nil, external_data: {})
|
|
50
|
+
attrs = {}
|
|
51
|
+
amount_in_cents = nil
|
|
52
|
+
|
|
53
|
+
if amount.present?
|
|
54
|
+
attrs[:amount] = amount
|
|
55
|
+
amount_in_cents = Spree::Money.new(amount, currency: payment_session.currency).cents
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
stripe_pm_id = external_data[:stripe_payment_method_id] || external_data['stripe_payment_method_id']
|
|
59
|
+
|
|
60
|
+
update_payment_intent(
|
|
61
|
+
payment_session.external_id,
|
|
62
|
+
amount_in_cents || payment_session.amount_in_cents,
|
|
63
|
+
payment_session.order,
|
|
64
|
+
stripe_pm_id
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
if external_data.present?
|
|
68
|
+
attrs[:external_data] = (payment_session.external_data || {}).merge(external_data.stringify_keys)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
payment_session.update!(attrs) if attrs.any?
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Completes a payment session by verifying with Stripe, creating the
|
|
75
|
+
# Payment record, and patching wallet address data.
|
|
76
|
+
#
|
|
77
|
+
# Does NOT complete the order — that is handled by Carts::Complete
|
|
78
|
+
# (called by the storefront or by the webhook handler).
|
|
79
|
+
def complete_payment_session(payment_session:, params: {})
|
|
80
|
+
stripe_pi = retrieve_payment_intent(payment_session.external_id)
|
|
81
|
+
|
|
82
|
+
if payment_intent_accepted?(stripe_pi)
|
|
83
|
+
payment_session.process if payment_session.can_process?
|
|
84
|
+
|
|
85
|
+
charge = payment_session.stripe_charge
|
|
86
|
+
|
|
87
|
+
# Patch wallet billing address (Apple Pay, Google Pay)
|
|
88
|
+
patch_wallet_address(payment_session.order, charge) if charge.present?
|
|
89
|
+
|
|
90
|
+
# Create the Payment record
|
|
91
|
+
payment_session.find_or_create_payment!
|
|
92
|
+
|
|
93
|
+
# `else` covers requires_capture (manual capture), processing (delayed-notification
|
|
94
|
+
# banks), and requires_action (bank transfer awaiting funds) — all auth-only states.
|
|
95
|
+
payment = payment_session.payment
|
|
96
|
+
if payment.present? && !payment.completed?
|
|
97
|
+
if payment_intent_successful?(stripe_pi)
|
|
98
|
+
payment.process!
|
|
99
|
+
else
|
|
100
|
+
payment.authorize!
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
payment_session.complete unless payment_session.completed?
|
|
105
|
+
else
|
|
106
|
+
payment_session.fail if payment_session.can_fail?
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
private
|
|
111
|
+
|
|
112
|
+
def payment_intent_successful?(stripe_pi)
|
|
113
|
+
stripe_pi.status == 'succeeded'
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Patches the order's billing address with data from the Stripe charge.
|
|
117
|
+
# Needed for quick checkout (Apple Pay/Google Pay) where the storefront
|
|
118
|
+
# doesn't have the billing address before payment confirmation.
|
|
119
|
+
def patch_wallet_address(order, charge)
|
|
120
|
+
return if charge.blank?
|
|
121
|
+
|
|
122
|
+
billing_details = charge.billing_details
|
|
123
|
+
address = billing_details.address
|
|
124
|
+
|
|
125
|
+
order.email ||= billing_details.email
|
|
126
|
+
order.save! if order.email_changed?
|
|
127
|
+
|
|
128
|
+
# Skip if billing address is already valid
|
|
129
|
+
return if order.bill_address.present? && order.bill_address.valid?
|
|
130
|
+
|
|
131
|
+
country_iso = address.country
|
|
132
|
+
country = (country_iso.present? && Spree::Country.by_iso(country_iso)) ||
|
|
133
|
+
order.store.default_market&.default_country ||
|
|
134
|
+
Spree::Country.by_iso('US')
|
|
135
|
+
|
|
136
|
+
order.bill_address ||= Spree::Address.new(country: country, user: order.user)
|
|
137
|
+
order.bill_address.quick_checkout = true
|
|
138
|
+
|
|
139
|
+
first_name = billing_details.name&.split(' ')&.first || order.ship_address&.first_name || order.user&.first_name
|
|
140
|
+
last_name = billing_details.name&.split(' ')&.last || order.ship_address&.last_name || order.user&.last_name
|
|
141
|
+
|
|
142
|
+
order.bill_address.first_name ||= first_name
|
|
143
|
+
order.bill_address.last_name ||= last_name
|
|
144
|
+
order.bill_address.phone ||= billing_details.phone
|
|
145
|
+
order.bill_address.address1 ||= address.line1
|
|
146
|
+
order.bill_address.address2 ||= address.line2
|
|
147
|
+
order.bill_address.city ||= address.city
|
|
148
|
+
order.bill_address.zipcode ||= address.postal_code
|
|
149
|
+
|
|
150
|
+
state_name = address.state
|
|
151
|
+
if country.states_required?
|
|
152
|
+
order.bill_address.state = country.states.find_all_by_name_or_abbr(state_name)&.first
|
|
153
|
+
else
|
|
154
|
+
order.bill_address.state_name = state_name
|
|
155
|
+
end
|
|
156
|
+
order.bill_address.state_name ||= state_name
|
|
157
|
+
|
|
158
|
+
if order.bill_address.invalid?
|
|
159
|
+
return if order.ship_address.blank?
|
|
160
|
+
|
|
161
|
+
order.bill_address = order.ship_address
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
order.bill_address.save! if order.bill_address&.changed?
|
|
165
|
+
order.save!
|
|
166
|
+
|
|
167
|
+
copy_bill_info_to_user(order) if order.user.present?
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def copy_bill_info_to_user(order)
|
|
171
|
+
user = order.user
|
|
172
|
+
user.first_name ||= order.bill_address.first_name
|
|
173
|
+
user.last_name ||= order.bill_address.last_name
|
|
174
|
+
user.phone ||= order.bill_address.phone
|
|
175
|
+
user.bill_address_id ||= order.bill_address.id
|
|
176
|
+
user.save! if user.changed?
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
module SpreeStripe
|
|
2
|
+
class Gateway < ::Spree::Gateway
|
|
3
|
+
module PaymentSetupSessions
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
def setup_session_supported?
|
|
7
|
+
true
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def payment_setup_session_class
|
|
11
|
+
Spree::PaymentSetupSessions::Stripe
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def create_payment_setup_session(customer:, external_data: {})
|
|
15
|
+
gateway_customer = fetch_or_create_customer(user: customer)
|
|
16
|
+
|
|
17
|
+
setup_intent_response = create_setup_intent(gateway_customer.profile_id)
|
|
18
|
+
ephemeral_key_response = create_ephemeral_key(gateway_customer.profile_id)
|
|
19
|
+
|
|
20
|
+
payment_setup_session_class.create!(
|
|
21
|
+
customer: customer,
|
|
22
|
+
payment_method: self,
|
|
23
|
+
status: 'pending',
|
|
24
|
+
external_id: setup_intent_response.params['id'],
|
|
25
|
+
external_client_secret: setup_intent_response.authorization,
|
|
26
|
+
external_data: external_data.to_h.stringify_keys.merge(
|
|
27
|
+
'customer_id' => gateway_customer.profile_id,
|
|
28
|
+
'ephemeral_key_secret' => ephemeral_key_response&.params&.dig('secret')
|
|
29
|
+
).compact
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def complete_payment_setup_session(setup_session:, params: {})
|
|
34
|
+
stripe_setup_intent = retrieve_setup_intent(setup_session.external_id)
|
|
35
|
+
|
|
36
|
+
if stripe_setup_intent.status == 'succeeded'
|
|
37
|
+
setup_session.process if setup_session.can_process?
|
|
38
|
+
|
|
39
|
+
stripe_payment_method = stripe_setup_intent.payment_method
|
|
40
|
+
|
|
41
|
+
source = SpreeStripe::CreateSource.new(
|
|
42
|
+
stripe_payment_method_details: stripe_payment_method,
|
|
43
|
+
stripe_payment_method_id: stripe_payment_method.id,
|
|
44
|
+
stripe_billing_details: stripe_payment_method.billing_details,
|
|
45
|
+
gateway: self,
|
|
46
|
+
user: setup_session.customer
|
|
47
|
+
).call
|
|
48
|
+
|
|
49
|
+
setup_session.payment_source = source
|
|
50
|
+
setup_session.complete unless setup_session.completed?
|
|
51
|
+
else
|
|
52
|
+
setup_session.fail if setup_session.can_fail?
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
setup_session
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def retrieve_setup_intent(setup_intent_id)
|
|
59
|
+
send_request { |opts| Stripe::SetupIntent.retrieve({ id: setup_intent_id, expand: ['payment_method'] }, opts) }
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|