solidus_stripe 4.4.0 → 5.0.0.rc.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.circleci/config.yml +102 -41
- data/.gitignore +3 -0
- data/.rubocop.yml +94 -4
- data/.yardopts +1 -0
- data/CHANGELOG.md +1 -265
- data/Gemfile +10 -30
- data/LICENSE +2 -2
- data/Procfile.dev +3 -0
- data/README.md +185 -213
- data/Rakefile +7 -6
- data/app/assets/javascripts/spree/backend/solidus_stripe.js +2 -0
- data/app/assets/stylesheets/spree/backend/solidus_stripe.css +4 -0
- data/app/controllers/solidus_stripe/intents_controller.rb +36 -52
- data/app/controllers/solidus_stripe/webhooks_controller.rb +28 -0
- data/app/models/concerns/solidus_stripe/log_entries.rb +31 -0
- data/app/models/solidus_stripe/customer.rb +21 -0
- data/app/models/solidus_stripe/gateway.rb +229 -0
- data/app/models/solidus_stripe/payment_intent.rb +124 -0
- data/app/models/solidus_stripe/payment_method.rb +106 -0
- data/app/models/solidus_stripe/payment_source.rb +31 -0
- data/app/models/solidus_stripe/slug_entry.rb +20 -0
- data/app/models/solidus_stripe.rb +7 -0
- data/app/subscribers/solidus_stripe/webhook/charge_subscriber.rb +28 -0
- data/app/subscribers/solidus_stripe/webhook/payment_intent_subscriber.rb +112 -0
- data/app/views/spree/admin/payments/source_forms/_stripe.html.erb +29 -0
- data/app/views/spree/admin/payments/source_forms/existing_payment/_stripe.html.erb +19 -0
- data/app/views/spree/admin/payments/source_forms/existing_payment/stripe/_card.html.erb +8 -0
- data/app/views/spree/admin/payments/source_forms/existing_payment/stripe/_default.html.erb +7 -0
- data/app/views/spree/admin/payments/source_views/_stripe.html.erb +15 -0
- data/app/views/spree/api/payments/source_views/_stripe.json.jbuilder +8 -0
- data/bin/dev +13 -0
- data/bin/dummy-app +29 -0
- data/bin/rails +38 -3
- data/bin/rails-dummy-app +3 -0
- data/bin/rails-engine +1 -11
- data/bin/rails-new +55 -0
- data/bin/rails-sandbox +1 -14
- data/bin/rspec +10 -0
- data/bin/sandbox +12 -74
- data/bin/setup +1 -0
- data/bin/update-migrations +56 -0
- data/codecov.yml +12 -0
- data/config/locales/en.yml +16 -1
- data/config/routes.rb +5 -11
- data/coverage.rb +42 -0
- data/db/migrate/20230109183332_create_solidus_stripe_payment_sources.rb +10 -0
- data/db/migrate/20230306105520_create_solidus_stripe_payment_intents.rb +14 -0
- data/db/migrate/20230308122414_create_solidus_stripe_slug_entries.rb +12 -0
- data/db/migrate/20230313150008_create_solidus_stripe_customers.rb +15 -0
- data/db/seeds.rb +6 -24
- data/lib/generators/solidus_stripe/install/install_generator.rb +121 -14
- data/lib/generators/solidus_stripe/install/templates/app/assets/stylesheets/spree/frontend/solidus_stripe.css +13 -0
- data/lib/generators/solidus_stripe/install/templates/app/javascript/controllers/solidus_stripe_confirm_controller.js +39 -0
- data/lib/generators/solidus_stripe/install/templates/app/javascript/controllers/solidus_stripe_payment_controller.js +89 -0
- data/lib/generators/solidus_stripe/install/templates/app/views/checkouts/existing_payment/_stripe.html.erb +21 -0
- data/lib/generators/solidus_stripe/install/templates/app/views/checkouts/existing_payment/stripe/_card.html.erb +8 -0
- data/lib/generators/solidus_stripe/install/templates/app/views/checkouts/existing_payment/stripe/_default.html.erb +7 -0
- data/lib/generators/solidus_stripe/install/templates/app/views/checkouts/payment/_stripe.html.erb +74 -0
- data/lib/generators/solidus_stripe/install/templates/app/views/orders/payment_info/_stripe.html.erb +32 -0
- data/lib/generators/solidus_stripe/install/templates/config/initializers/solidus_stripe.rb +31 -0
- data/lib/solidus_stripe/configuration.rb +24 -3
- data/lib/solidus_stripe/engine.rb +19 -6
- data/lib/solidus_stripe/money_to_stripe_amount_converter.rb +109 -0
- data/lib/solidus_stripe/refunds_synchronizer.rb +98 -0
- data/lib/solidus_stripe/seeds.rb +19 -0
- data/lib/solidus_stripe/testing_support/factories.rb +153 -0
- data/lib/solidus_stripe/version.rb +1 -1
- data/lib/solidus_stripe/webhook/event.rb +91 -0
- data/lib/solidus_stripe.rb +0 -2
- data/solidus_stripe.gemspec +29 -6
- data/spec/lib/solidus_stripe/configuration_spec.rb +21 -0
- data/spec/lib/solidus_stripe/money_to_stripe_amount_converter_spec.rb +133 -0
- data/spec/lib/solidus_stripe/refunds_synchronizer_spec.rb +238 -0
- data/spec/lib/solidus_stripe/seeds_spec.rb +43 -0
- data/spec/lib/solidus_stripe/webhook/event_spec.rb +134 -0
- data/spec/models/concerns/solidus_stripe/log_entries_spec.rb +54 -0
- data/spec/models/solidus_stripe/customer_spec.rb +47 -0
- data/spec/models/solidus_stripe/gateway_spec.rb +281 -0
- data/spec/models/solidus_stripe/payment_intent_spec.rb +78 -0
- data/spec/models/solidus_stripe/payment_method_spec.rb +137 -0
- data/spec/models/solidus_stripe/payment_source_spec.rb +25 -0
- data/spec/requests/solidus_stripe/intents_controller_spec.rb +29 -0
- data/spec/requests/solidus_stripe/webhooks_controller/charge/refunded_spec.rb +31 -0
- data/spec/requests/solidus_stripe/webhooks_controller/payment_intent/canceled_spec.rb +23 -0
- data/spec/requests/solidus_stripe/webhooks_controller/payment_intent/payment_failed_spec.rb +23 -0
- data/spec/requests/solidus_stripe/webhooks_controller/payment_intent/succeeded_spec.rb +29 -0
- data/spec/requests/solidus_stripe/webhooks_controller_spec.rb +56 -0
- data/spec/solidus_stripe_spec_helper.rb +10 -0
- data/spec/subscribers/solidus_stripe/webhook/charge_subscriber_spec.rb +33 -0
- data/spec/subscribers/solidus_stripe/webhook/payment_intent_subscriber_spec.rb +297 -0
- data/spec/support/solidus_stripe/backend_test_helper.rb +185 -0
- data/spec/support/solidus_stripe/checkout_test_helper.rb +365 -0
- data/spec/support/solidus_stripe/factories.rb +5 -0
- data/spec/support/solidus_stripe/webhook/data_fixtures.rb +106 -0
- data/spec/support/solidus_stripe/webhook/event_with_context_factory.rb +82 -0
- data/spec/support/solidus_stripe/webhook/request_helper.rb +32 -0
- data/spec/system/backend/solidus_stripe/orders/payments_spec.rb +145 -0
- data/spec/system/frontend/.keep +0 -0
- data/spec/system/frontend/solidus_stripe/checkout_spec.rb +206 -0
- data/tmp/.keep +0 -0
- metadata +196 -75
- data/.rubocop_todo.yml +0 -298
- data/.travis.yml +0 -28
- data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-cart-page-checkout.js +0 -122
- data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-elements.js +0 -148
- data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-init.js +0 -20
- data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-payment-intents.js +0 -84
- data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-payment-request-button-shared.js +0 -160
- data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-payment.js +0 -16
- data/app/assets/javascripts/spree/frontend/solidus_stripe.js +0 -6
- data/app/controllers/solidus_stripe/payment_request_controller.rb +0 -52
- data/app/controllers/spree/stripe_controller.rb +0 -13
- data/app/decorators/models/spree/order_update_attributes_decorator.rb +0 -39
- data/app/decorators/models/spree/payment_decorator.rb +0 -11
- data/app/decorators/models/spree/refund_decorator.rb +0 -9
- data/app/models/solidus_stripe/address_from_params_service.rb +0 -72
- data/app/models/solidus_stripe/create_intents_payment_service.rb +0 -114
- data/app/models/solidus_stripe/prepare_order_for_payment_service.rb +0 -46
- data/app/models/solidus_stripe/shipping_rates_service.rb +0 -46
- data/app/models/spree/payment_method/stripe_credit_card.rb +0 -230
- data/bin/r +0 -13
- data/bin/sandbox_rails +0 -18
- data/db/migrate/20181010123508_update_stripe_payment_method_type_to_credit_card.rb +0 -21
- data/lib/assets/stylesheets/spree/frontend/solidus_stripe.scss +0 -11
- data/lib/solidus_stripe/testing_support/card_input_helper.rb +0 -34
- data/lib/tasks/solidus_stripe/db/seed.rake +0 -14
- data/lib/views/api/spree/api/payments/source_views/_stripe.json.jbuilder +0 -3
- data/lib/views/backend/spree/admin/log_entries/_stripe.html.erb +0 -28
- data/lib/views/backend/spree/admin/payments/source_forms/_stripe.html.erb +0 -1
- data/lib/views/backend/spree/admin/payments/source_views/_stripe.html.erb +0 -1
- data/lib/views/frontend/spree/checkout/existing_payment/_stripe.html.erb +0 -1
- data/lib/views/frontend/spree/checkout/payment/_stripe.html.erb +0 -8
- data/lib/views/frontend/spree/checkout/payment/v2/_javascript.html.erb +0 -78
- data/lib/views/frontend/spree/checkout/payment/v3/_elements.html.erb +0 -1
- data/lib/views/frontend/spree/checkout/payment/v3/_form_elements.html.erb +0 -40
- data/lib/views/frontend/spree/checkout/payment/v3/_intents.html.erb +0 -1
- data/lib/views/frontend/spree/checkout/payment/v3/_stripe.html.erb +0 -2
- data/lib/views/frontend/spree/orders/_stripe_payment_request_button.html.erb +0 -14
- data/spec/features/stripe_checkout_spec.rb +0 -486
- data/spec/models/solidus_stripe/address_from_params_service_spec.rb +0 -87
- data/spec/models/solidus_stripe/create_intents_payment_service_spec.rb +0 -127
- data/spec/models/solidus_stripe/prepare_order_for_payment_service_spec.rb +0 -65
- data/spec/models/solidus_stripe/shipping_rates_service_spec.rb +0 -54
- data/spec/models/spree/payment_method/stripe_credit_card_spec.rb +0 -350
- data/spec/requests/payment_requests_spec.rb +0 -152
- data/spec/spec_helper.rb +0 -37
- data/spec/support/solidus_address_helper.rb +0 -15
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SolidusStripe::LogEntries
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
extend self
|
6
|
+
|
7
|
+
# Builds an ActiveMerchant::Billing::Response
|
8
|
+
#
|
9
|
+
# @option [true,false] :success
|
10
|
+
# @option [String] :message
|
11
|
+
# @option [String] :response_code
|
12
|
+
# @option [#to_json] :data
|
13
|
+
#
|
14
|
+
# @return [return type] return description
|
15
|
+
def build_payment_log(success:, message:, response_code: nil, data: nil)
|
16
|
+
ActiveMerchant::Billing::Response.new(
|
17
|
+
success,
|
18
|
+
message,
|
19
|
+
{ 'data' => data.to_json },
|
20
|
+
{ authorization: response_code },
|
21
|
+
)
|
22
|
+
end
|
23
|
+
|
24
|
+
def payment_log(payment, **options)
|
25
|
+
payment.log_entries.create!(details: YAML.safe_dump(
|
26
|
+
build_payment_log(**options),
|
27
|
+
permitted_classes: Spree::LogEntry.permitted_classes,
|
28
|
+
aliases: Spree::Config.log_entry_allow_aliases,
|
29
|
+
))
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SolidusStripe
|
4
|
+
class Customer < ApplicationRecord
|
5
|
+
belongs_to :payment_method
|
6
|
+
|
7
|
+
# Source is supposed to be a user or an order and needs to respond to #email
|
8
|
+
belongs_to :source, polymorphic: true
|
9
|
+
|
10
|
+
def self.retrieve_or_create_stripe_customer_id(payment_method:, order:)
|
11
|
+
instance = find_or_initialize_by(payment_method: payment_method, source: order.user || order)
|
12
|
+
|
13
|
+
instance.stripe_id ||
|
14
|
+
instance.create_stripe_customer.tap { instance.update!(stripe_id: _1.id) }.id
|
15
|
+
end
|
16
|
+
|
17
|
+
def create_stripe_customer
|
18
|
+
payment_method.gateway.request { Stripe::Customer.create(email: source.email) }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,229 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'stripe'
|
4
|
+
require "solidus_stripe/money_to_stripe_amount_converter"
|
5
|
+
require "solidus_stripe/refunds_synchronizer"
|
6
|
+
|
7
|
+
module SolidusStripe
|
8
|
+
# @see https://stripe.com/docs/payments/accept-a-payment?platform=web&ui=checkout#auth-and-capture
|
9
|
+
# @see https://stripe.com/docs/charges/placing-a-hold
|
10
|
+
# @see https://guides.solidus.io/advanced-solidus/payments-and-refunds/#custom-payment-gateways
|
11
|
+
#
|
12
|
+
# ## About fractional amounts
|
13
|
+
#
|
14
|
+
# All methods in the Gateway class will have `amount_in_cents` arguments representing the
|
15
|
+
# fractional amount as defined by Spree::Money and will be translated to the fractional expected
|
16
|
+
# by Stripe, although for most currencies it's cents some will have a different multiplier and
|
17
|
+
# that is already took into account.
|
18
|
+
#
|
19
|
+
# @see SolidusStripe::MoneyToStripeAmountConverter
|
20
|
+
class Gateway
|
21
|
+
include SolidusStripe::MoneyToStripeAmountConverter
|
22
|
+
include SolidusStripe::LogEntries
|
23
|
+
|
24
|
+
def initialize(options)
|
25
|
+
# Cannot use kwargs because of how the Gateway is initialized by Solidus.
|
26
|
+
@client = Stripe::StripeClient.new(
|
27
|
+
api_key: options.fetch(:api_key, nil),
|
28
|
+
)
|
29
|
+
@options = options
|
30
|
+
end
|
31
|
+
|
32
|
+
attr_reader :client
|
33
|
+
|
34
|
+
# Authorizes a certain amount on the provided payment source.
|
35
|
+
#
|
36
|
+
# We create and confirm the Stripe payment intent in two steps. That's to
|
37
|
+
# guarantee that we associate a Solidus payment on the creation, and we can
|
38
|
+
# fetch it after the webhook event is published on confirmation.
|
39
|
+
#
|
40
|
+
# The Stripe payment intent id is copied over the Solidus payment
|
41
|
+
# `response_code` field.
|
42
|
+
def authorize(amount_in_cents, _source, options = {})
|
43
|
+
check_given_amount_matches_payment_intent(amount_in_cents, options)
|
44
|
+
|
45
|
+
stripe_payment_intent_options = {
|
46
|
+
amount: to_stripe_amount(amount_in_cents, options[:originator].currency),
|
47
|
+
confirm: false,
|
48
|
+
capture_method: "manual",
|
49
|
+
}
|
50
|
+
|
51
|
+
stripe_payment_intent = SolidusStripe::PaymentIntent.prepare_for_payment(
|
52
|
+
options[:originator],
|
53
|
+
**stripe_payment_intent_options,
|
54
|
+
)
|
55
|
+
|
56
|
+
confirm_stripe_payment_intent(stripe_payment_intent.stripe_intent_id)
|
57
|
+
|
58
|
+
build_payment_log(
|
59
|
+
success: true,
|
60
|
+
message: "PaymentIntent was confirmed successfully",
|
61
|
+
response_code: stripe_payment_intent.stripe_intent_id,
|
62
|
+
data: stripe_payment_intent.stripe_intent
|
63
|
+
)
|
64
|
+
rescue Stripe::StripeError => e
|
65
|
+
build_payment_log(
|
66
|
+
success: false,
|
67
|
+
message: e.message,
|
68
|
+
data: e.response
|
69
|
+
)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Captures a certain amount from a previously authorized transaction.
|
73
|
+
#
|
74
|
+
# @see https://stripe.com/docs/api/payment_intents/capture#capture_payment_intent
|
75
|
+
# @see https://stripe.com/docs/payments/capture-later
|
76
|
+
#
|
77
|
+
# @todo add support for capturing custom amounts
|
78
|
+
def capture(amount_in_cents, stripe_payment_intent_id, options = {})
|
79
|
+
check_given_amount_matches_payment_intent(amount_in_cents, options)
|
80
|
+
check_stripe_payment_intent_id(stripe_payment_intent_id)
|
81
|
+
|
82
|
+
stripe_payment_intent = capture_stripe_payment_intent(stripe_payment_intent_id, amount_in_cents)
|
83
|
+
build_payment_log(
|
84
|
+
success: true,
|
85
|
+
message: "PaymentIntent was confirmed successfully",
|
86
|
+
response_code: stripe_payment_intent.id,
|
87
|
+
data: stripe_payment_intent,
|
88
|
+
)
|
89
|
+
rescue Stripe::InvalidRequestError => e
|
90
|
+
build_payment_log(
|
91
|
+
success: false,
|
92
|
+
message: e.to_s,
|
93
|
+
data: e.response,
|
94
|
+
)
|
95
|
+
end
|
96
|
+
|
97
|
+
# Authorizes and captures a certain amount on the provided payment source.
|
98
|
+
#
|
99
|
+
# See `#authorize` for how the confirmation step is performed.
|
100
|
+
#
|
101
|
+
# @todo add support for purchasing custom amounts
|
102
|
+
def purchase(amount_in_cents, _source, options = {})
|
103
|
+
check_given_amount_matches_payment_intent(amount_in_cents, options)
|
104
|
+
|
105
|
+
stripe_payment_intent_options = {
|
106
|
+
amount: to_stripe_amount(amount_in_cents, options[:originator].currency),
|
107
|
+
confirm: false,
|
108
|
+
capture_method: "automatic",
|
109
|
+
}
|
110
|
+
|
111
|
+
stripe_payment_intent = SolidusStripe::PaymentIntent.prepare_for_payment(
|
112
|
+
options[:originator],
|
113
|
+
**stripe_payment_intent_options,
|
114
|
+
)
|
115
|
+
|
116
|
+
confirm_stripe_payment_intent(stripe_payment_intent.stripe_intent_id)
|
117
|
+
|
118
|
+
build_payment_log(
|
119
|
+
success: true,
|
120
|
+
message: "PaymentIntent was confirmed and captured successfully",
|
121
|
+
response_code: stripe_payment_intent.stripe_intent_id,
|
122
|
+
data: stripe_payment_intent.stripe_intent,
|
123
|
+
)
|
124
|
+
rescue Stripe::StripeError => e
|
125
|
+
build_payment_log(
|
126
|
+
success: false,
|
127
|
+
message: e.to_s,
|
128
|
+
data: e.response,
|
129
|
+
)
|
130
|
+
end
|
131
|
+
|
132
|
+
# Voids a previously authorized transaction, releasing the funds that are on hold.
|
133
|
+
def void(stripe_payment_intent_id, _options = {})
|
134
|
+
check_stripe_payment_intent_id(stripe_payment_intent_id)
|
135
|
+
|
136
|
+
stripe_payment_intent = request do
|
137
|
+
Stripe::PaymentIntent.cancel(stripe_payment_intent_id)
|
138
|
+
end
|
139
|
+
|
140
|
+
build_payment_log(
|
141
|
+
success: true,
|
142
|
+
message: "PaymentIntent was canceled successfully",
|
143
|
+
response_code: stripe_payment_intent_id,
|
144
|
+
data: stripe_payment_intent,
|
145
|
+
)
|
146
|
+
rescue Stripe::InvalidRequestError => e
|
147
|
+
build_payment_log(
|
148
|
+
success: false,
|
149
|
+
message: e.to_s,
|
150
|
+
data: e.response,
|
151
|
+
)
|
152
|
+
end
|
153
|
+
|
154
|
+
# Refunds the provided amount on a previously captured transaction.
|
155
|
+
#
|
156
|
+
# Notice we're adding `solidus_skip_sync: 'true'` to the metadata to avoid a
|
157
|
+
# duplicated refund after the generated webhook event. See
|
158
|
+
# {RefundsSynchronizer}.
|
159
|
+
#
|
160
|
+
# TODO: check this method params twice.
|
161
|
+
def credit(amount_in_cents, stripe_payment_intent_id, options = {})
|
162
|
+
check_stripe_payment_intent_id(stripe_payment_intent_id)
|
163
|
+
|
164
|
+
payment = options[:originator].payment
|
165
|
+
currency = payment.currency
|
166
|
+
|
167
|
+
stripe_refund = request do
|
168
|
+
Stripe::Refund.create(
|
169
|
+
amount: to_stripe_amount(amount_in_cents, currency),
|
170
|
+
payment_intent: stripe_payment_intent_id,
|
171
|
+
metadata: RefundsSynchronizer.skip_sync_metadata
|
172
|
+
)
|
173
|
+
end
|
174
|
+
|
175
|
+
build_payment_log(
|
176
|
+
success: true,
|
177
|
+
message: "PaymentIntent was refunded successfully",
|
178
|
+
response_code: stripe_refund.id,
|
179
|
+
data: stripe_refund,
|
180
|
+
)
|
181
|
+
end
|
182
|
+
|
183
|
+
# Send a request to stripe using the current api keys but ignoring
|
184
|
+
# the response object.
|
185
|
+
#
|
186
|
+
# @yield Allows to use the `Stripe` gem using the credentials attached
|
187
|
+
# to the current payment method
|
188
|
+
#
|
189
|
+
# @example Retrieve a payment intent
|
190
|
+
# request { Stripe::PaymentIntent.retrieve(intent_id) }
|
191
|
+
#
|
192
|
+
# @return forwards the result of the block
|
193
|
+
def request(&block)
|
194
|
+
result, _response = client.request(&block)
|
195
|
+
result
|
196
|
+
end
|
197
|
+
|
198
|
+
private
|
199
|
+
|
200
|
+
def confirm_stripe_payment_intent(stripe_payment_intent_id)
|
201
|
+
request { Stripe::PaymentIntent.confirm(stripe_payment_intent_id) }
|
202
|
+
end
|
203
|
+
|
204
|
+
def capture_stripe_payment_intent(stripe_payment_intent_id, amount)
|
205
|
+
request { Stripe::PaymentIntent.capture(stripe_payment_intent_id, amount: amount) }
|
206
|
+
end
|
207
|
+
|
208
|
+
def check_given_amount_matches_payment_intent(amount_in_cents, options)
|
209
|
+
payment = options[:originator] or
|
210
|
+
raise ArgumentError, "please provide a payment with the :originator option"
|
211
|
+
|
212
|
+
return if amount_in_cents == payment.display_amount.cents
|
213
|
+
|
214
|
+
raise \
|
215
|
+
"Using a custom amount is not supported yet, " \
|
216
|
+
"tried #{amount_in_cents} but can only accept #{payment.display_amount.cents}."
|
217
|
+
end
|
218
|
+
|
219
|
+
def check_stripe_payment_intent_id(stripe_payment_intent_id)
|
220
|
+
unless stripe_payment_intent_id
|
221
|
+
raise ArgumentError, "missing stripe_payment_intent_id"
|
222
|
+
end
|
223
|
+
|
224
|
+
return if stripe_payment_intent_id.start_with?('pi_')
|
225
|
+
|
226
|
+
raise ArgumentError, "the payment intent id has the wrong format"
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SolidusStripe
|
4
|
+
class PaymentIntent < ApplicationRecord
|
5
|
+
belongs_to :order, class_name: 'Spree::Order'
|
6
|
+
belongs_to :payment_method, class_name: 'SolidusStripe::PaymentMethod'
|
7
|
+
|
8
|
+
def self.prepare_for_payment(payment, **stripe_creation_options)
|
9
|
+
# Find or create the intent for the payment.
|
10
|
+
intent = retrieve_for_payment(payment) || create_for_payment(payment, **stripe_creation_options)
|
11
|
+
|
12
|
+
# Attach the payment intent to the payment.
|
13
|
+
payment.update!(response_code: intent.stripe_intent.id)
|
14
|
+
|
15
|
+
intent
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.create_for_payment(payment, **stripe_intent_options)
|
19
|
+
new(payment_method: payment.payment_method, order: payment.order)
|
20
|
+
.tap { _1.update!(stripe_intent_id: _1.create_stripe_intent(payment, **stripe_intent_options).id) }
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.retrieve_for_payment(payment)
|
24
|
+
intent = where(payment_method: payment.payment_method, order: payment.order).last
|
25
|
+
|
26
|
+
return unless intent&.usable?
|
27
|
+
|
28
|
+
# Update the intent with the previously acquired payment method.
|
29
|
+
intent.payment_method.gateway.request {
|
30
|
+
Stripe::PaymentIntent.update(
|
31
|
+
intent.stripe_intent_id,
|
32
|
+
payment_method: payment.source.stripe_payment_method_id,
|
33
|
+
payment_method_types: [payment.source.stripe_payment_method.type],
|
34
|
+
)
|
35
|
+
}
|
36
|
+
|
37
|
+
intent
|
38
|
+
end
|
39
|
+
|
40
|
+
def usable?
|
41
|
+
stripe_intent_id &&
|
42
|
+
stripe_intent.status == 'requires_payment_method' &&
|
43
|
+
stripe_intent.amount == stripe_order_amount
|
44
|
+
end
|
45
|
+
|
46
|
+
def stripe_order_amount
|
47
|
+
payment_method.gateway.to_stripe_amount(
|
48
|
+
order.display_order_total_after_store_credit.money.fractional,
|
49
|
+
order.currency,
|
50
|
+
)
|
51
|
+
end
|
52
|
+
|
53
|
+
def process_payment
|
54
|
+
payment = order.payments.valid.find_by!(
|
55
|
+
payment_method: payment_method,
|
56
|
+
response_code: stripe_intent.id,
|
57
|
+
)
|
58
|
+
|
59
|
+
payment.started_processing!
|
60
|
+
|
61
|
+
case stripe_intent.status
|
62
|
+
when 'processing'
|
63
|
+
successful = true
|
64
|
+
when 'requires_capture'
|
65
|
+
payment.pend! unless payment.pending?
|
66
|
+
successful = true
|
67
|
+
when 'succeeded'
|
68
|
+
payment.complete! unless payment.completed?
|
69
|
+
successful = true
|
70
|
+
else
|
71
|
+
payment.failure!
|
72
|
+
successful = false
|
73
|
+
end
|
74
|
+
|
75
|
+
SolidusStripe::LogEntries.payment_log(
|
76
|
+
payment,
|
77
|
+
success: successful,
|
78
|
+
message: I18n.t("solidus_stripe.intent_status.#{stripe_intent.status}"),
|
79
|
+
data: stripe_intent,
|
80
|
+
)
|
81
|
+
|
82
|
+
if successful
|
83
|
+
order.complete!
|
84
|
+
order.user.wallet.add(payment.source) if order.user && stripe_intent.setup_future_usage.present?
|
85
|
+
else
|
86
|
+
order.payment_failed!
|
87
|
+
end
|
88
|
+
|
89
|
+
successful
|
90
|
+
end
|
91
|
+
|
92
|
+
def stripe_intent
|
93
|
+
@stripe_intent ||= payment_method.gateway.request do
|
94
|
+
Stripe::PaymentIntent.retrieve(stripe_intent_id)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def reload(...)
|
99
|
+
@stripe_intent = nil
|
100
|
+
super
|
101
|
+
end
|
102
|
+
|
103
|
+
def create_stripe_intent(payment, **stripe_intent_options)
|
104
|
+
stripe_customer_id = SolidusStripe::Customer.retrieve_or_create_stripe_customer_id(
|
105
|
+
payment_method: payment_method,
|
106
|
+
order: order
|
107
|
+
)
|
108
|
+
|
109
|
+
payment_method.gateway.request do
|
110
|
+
Stripe::PaymentIntent.create({
|
111
|
+
amount: stripe_order_amount,
|
112
|
+
currency: order.currency,
|
113
|
+
capture_method: payment_method.auto_capture? ? 'automatic' : 'manual',
|
114
|
+
setup_future_usage: payment_method.preferred_setup_future_usage.presence,
|
115
|
+
customer: stripe_customer_id,
|
116
|
+
payment_method: payment.source.stripe_payment_method_id,
|
117
|
+
payment_method_types: [payment.source.stripe_payment_method.type],
|
118
|
+
metadata: { solidus_order_number: order.number },
|
119
|
+
**stripe_intent_options,
|
120
|
+
})
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SolidusStripe
|
4
|
+
class PaymentMethod < ::Spree::PaymentMethod
|
5
|
+
preference :api_key, :string
|
6
|
+
preference :publishable_key, :string
|
7
|
+
preference :setup_future_usage, :string, default: ''
|
8
|
+
|
9
|
+
# @attribute [rw] preferred_webhook_endpoint_signing_secret The webhook endpoint signing secret
|
10
|
+
# for this payment method.
|
11
|
+
# @see https://stripe.com/docs/webhooks/signatures
|
12
|
+
preference :webhook_endpoint_signing_secret, :string
|
13
|
+
|
14
|
+
validates :preferred_setup_future_usage, inclusion: { in: ['', 'on_session', 'off_session'] }
|
15
|
+
|
16
|
+
has_one :slug_entry, class_name: 'SolidusStripe::SlugEntry', inverse_of: :payment_method, dependent: :destroy
|
17
|
+
|
18
|
+
after_create :assign_slug
|
19
|
+
|
20
|
+
delegate :slug, to: :slug_entry
|
21
|
+
|
22
|
+
# @return [Spree::RefundReason] the reason used for refunds
|
23
|
+
# generated from Stripe.
|
24
|
+
# @see SolidusStripe::Configuration.refund_reason_name
|
25
|
+
def self.refund_reason
|
26
|
+
Spree::RefundReason.find_by!(
|
27
|
+
name: SolidusStripe.configuration.refund_reason_name
|
28
|
+
)
|
29
|
+
end
|
30
|
+
|
31
|
+
def partial_name
|
32
|
+
"stripe"
|
33
|
+
end
|
34
|
+
|
35
|
+
alias cart_partial_name partial_name
|
36
|
+
alias product_page_partial_name partial_name
|
37
|
+
alias risky_partial_name partial_name
|
38
|
+
|
39
|
+
def source_required?
|
40
|
+
true
|
41
|
+
end
|
42
|
+
|
43
|
+
def payment_source_class
|
44
|
+
PaymentSource
|
45
|
+
end
|
46
|
+
|
47
|
+
def gateway_class
|
48
|
+
Gateway
|
49
|
+
end
|
50
|
+
|
51
|
+
def payment_profiles_supported?
|
52
|
+
# We actually support them, but not in the way expected by Solidus and its ActiveMerchant legacy.
|
53
|
+
false
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.with_slug(slug)
|
57
|
+
where(id: SlugEntry.where(slug: slug).select(:payment_method_id))
|
58
|
+
end
|
59
|
+
|
60
|
+
# TODO: re-evaluate the need for this and think of ways to always go throught the intent classes.
|
61
|
+
def self.intent_id_for_payment(payment)
|
62
|
+
return unless payment
|
63
|
+
|
64
|
+
payment.transaction_id || SolidusStripe::PaymentIntent.where(
|
65
|
+
order: payment.order, payment_method: payment.payment_method
|
66
|
+
)&.pick(:stripe_intent_id)
|
67
|
+
end
|
68
|
+
|
69
|
+
def stripe_dashboard_url(intent_id)
|
70
|
+
path_prefix = '/test' if preferred_test_mode
|
71
|
+
|
72
|
+
case intent_id
|
73
|
+
when /^pi_/
|
74
|
+
"https://dashboard.stripe.com#{path_prefix}/payments/#{intent_id}"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def assign_slug
|
79
|
+
# If there's only one payment method, we can use a default slug.
|
80
|
+
slug = preferred_test_mode ? 'test' : 'live' if self.class.count == 1
|
81
|
+
slug = SecureRandom.hex(16) while SlugEntry.exists?(slug: slug) || slug.nil?
|
82
|
+
|
83
|
+
create_slug_entry!(slug: slug)
|
84
|
+
end
|
85
|
+
|
86
|
+
# The method that should be used is "Spree::PaymentMethod#reusable_sources".
|
87
|
+
# However, in the dedicated partial source form, the reusable_sources are
|
88
|
+
# assigned to "previous_cards":
|
89
|
+
# https://github.com/solidusio/solidus/blob/e9debb976e2228bb0b7a8eff4894e0556fc15cc8/backend/app/views/spree/admin/payments/_form.html.erb#L31
|
90
|
+
# This name is inaccurate and too specific because, in our case, a
|
91
|
+
# payment-source/stripe-payment-method have many different possible types:
|
92
|
+
# https://stripe.com/docs/api/payment_methods/object#payment_method_object-type
|
93
|
+
#
|
94
|
+
# For more details:
|
95
|
+
# https://github.com/solidusio/solidus/issues/5014
|
96
|
+
#
|
97
|
+
# @todo Start using the correct method to get a user's previous sources
|
98
|
+
def previous_sources(order)
|
99
|
+
if order.user_id
|
100
|
+
order.user.wallet.wallet_payment_sources.map(&:payment_source)
|
101
|
+
else
|
102
|
+
[]
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'stripe'
|
4
|
+
|
5
|
+
module SolidusStripe
|
6
|
+
class PaymentSource < ::Spree::PaymentSource
|
7
|
+
def stripe_payment_method
|
8
|
+
return if stripe_payment_method_id.blank?
|
9
|
+
|
10
|
+
@stripe_payment_method ||= payment_method.gateway.request do
|
11
|
+
Stripe::PaymentMethod.retrieve(stripe_payment_method_id)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def actions
|
16
|
+
%w[capture void credit]
|
17
|
+
end
|
18
|
+
|
19
|
+
def can_capture?(payment)
|
20
|
+
payment.pending?
|
21
|
+
end
|
22
|
+
|
23
|
+
def can_void?(payment)
|
24
|
+
payment.pending?
|
25
|
+
end
|
26
|
+
|
27
|
+
def can_credit?(payment)
|
28
|
+
payment.completed? && payment.credit_allowed > 0
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SolidusStripe
|
4
|
+
# Represents a webhook endpoint for a {SolidusStripe::PaymentMethod}.
|
5
|
+
#
|
6
|
+
# A Stripe webhook endpoint is a URL that Stripe will send events to. A store
|
7
|
+
# could have multiple Stripe payment methods (e.g., a marketplace), so we need
|
8
|
+
# to differentiate which one a webhook request targets.
|
9
|
+
#
|
10
|
+
# This model associates a slug with a payment method. The slug is appended
|
11
|
+
# to the endpoint URL (`.../webhooks/:slug`) so that we can fetch the
|
12
|
+
# correct payment method from the database and bind it to the generated
|
13
|
+
# `Spree::Bus` event.
|
14
|
+
#
|
15
|
+
# We use a slug instead of the payment method ID to be resilient to
|
16
|
+
# database changes and to avoid guessing about valid endpoint URLs.
|
17
|
+
class SlugEntry < ::Spree::Base
|
18
|
+
belongs_to :payment_method, class_name: 'SolidusStripe::PaymentMethod'
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "solidus_stripe/refunds_synchronizer"
|
4
|
+
|
5
|
+
module SolidusStripe
|
6
|
+
module Webhook
|
7
|
+
# Handlers for Stripe charge events.
|
8
|
+
class ChargeSubscriber
|
9
|
+
include Omnes::Subscriber
|
10
|
+
include MoneyToStripeAmountConverter
|
11
|
+
|
12
|
+
handle :"stripe.charge.refunded", with: :sync_refunds
|
13
|
+
|
14
|
+
# Syncs Stripe refunds with Solidus refunds.
|
15
|
+
#
|
16
|
+
# @param event [SolidusStripe::Webhook::Event]
|
17
|
+
# @see SolidusStripe::RefundsSynchronizer
|
18
|
+
def sync_refunds(event)
|
19
|
+
payment_method = event.payment_method
|
20
|
+
stripe_payment_intent_id = event.data.object.payment_intent
|
21
|
+
|
22
|
+
RefundsSynchronizer
|
23
|
+
.new(payment_method)
|
24
|
+
.call(stripe_payment_intent_id)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|