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.
Files changed (148) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +102 -41
  3. data/.gitignore +3 -0
  4. data/.rubocop.yml +94 -4
  5. data/.yardopts +1 -0
  6. data/CHANGELOG.md +1 -265
  7. data/Gemfile +10 -30
  8. data/LICENSE +2 -2
  9. data/Procfile.dev +3 -0
  10. data/README.md +185 -213
  11. data/Rakefile +7 -6
  12. data/app/assets/javascripts/spree/backend/solidus_stripe.js +2 -0
  13. data/app/assets/stylesheets/spree/backend/solidus_stripe.css +4 -0
  14. data/app/controllers/solidus_stripe/intents_controller.rb +36 -52
  15. data/app/controllers/solidus_stripe/webhooks_controller.rb +28 -0
  16. data/app/models/concerns/solidus_stripe/log_entries.rb +31 -0
  17. data/app/models/solidus_stripe/customer.rb +21 -0
  18. data/app/models/solidus_stripe/gateway.rb +229 -0
  19. data/app/models/solidus_stripe/payment_intent.rb +124 -0
  20. data/app/models/solidus_stripe/payment_method.rb +106 -0
  21. data/app/models/solidus_stripe/payment_source.rb +31 -0
  22. data/app/models/solidus_stripe/slug_entry.rb +20 -0
  23. data/app/models/solidus_stripe.rb +7 -0
  24. data/app/subscribers/solidus_stripe/webhook/charge_subscriber.rb +28 -0
  25. data/app/subscribers/solidus_stripe/webhook/payment_intent_subscriber.rb +112 -0
  26. data/app/views/spree/admin/payments/source_forms/_stripe.html.erb +29 -0
  27. data/app/views/spree/admin/payments/source_forms/existing_payment/_stripe.html.erb +19 -0
  28. data/app/views/spree/admin/payments/source_forms/existing_payment/stripe/_card.html.erb +8 -0
  29. data/app/views/spree/admin/payments/source_forms/existing_payment/stripe/_default.html.erb +7 -0
  30. data/app/views/spree/admin/payments/source_views/_stripe.html.erb +15 -0
  31. data/app/views/spree/api/payments/source_views/_stripe.json.jbuilder +8 -0
  32. data/bin/dev +13 -0
  33. data/bin/dummy-app +29 -0
  34. data/bin/rails +38 -3
  35. data/bin/rails-dummy-app +3 -0
  36. data/bin/rails-engine +1 -11
  37. data/bin/rails-new +55 -0
  38. data/bin/rails-sandbox +1 -14
  39. data/bin/rspec +10 -0
  40. data/bin/sandbox +12 -74
  41. data/bin/setup +1 -0
  42. data/bin/update-migrations +56 -0
  43. data/codecov.yml +12 -0
  44. data/config/locales/en.yml +16 -1
  45. data/config/routes.rb +5 -11
  46. data/coverage.rb +42 -0
  47. data/db/migrate/20230109183332_create_solidus_stripe_payment_sources.rb +10 -0
  48. data/db/migrate/20230306105520_create_solidus_stripe_payment_intents.rb +14 -0
  49. data/db/migrate/20230308122414_create_solidus_stripe_slug_entries.rb +12 -0
  50. data/db/migrate/20230313150008_create_solidus_stripe_customers.rb +15 -0
  51. data/db/seeds.rb +6 -24
  52. data/lib/generators/solidus_stripe/install/install_generator.rb +121 -14
  53. data/lib/generators/solidus_stripe/install/templates/app/assets/stylesheets/spree/frontend/solidus_stripe.css +13 -0
  54. data/lib/generators/solidus_stripe/install/templates/app/javascript/controllers/solidus_stripe_confirm_controller.js +39 -0
  55. data/lib/generators/solidus_stripe/install/templates/app/javascript/controllers/solidus_stripe_payment_controller.js +89 -0
  56. data/lib/generators/solidus_stripe/install/templates/app/views/checkouts/existing_payment/_stripe.html.erb +21 -0
  57. data/lib/generators/solidus_stripe/install/templates/app/views/checkouts/existing_payment/stripe/_card.html.erb +8 -0
  58. data/lib/generators/solidus_stripe/install/templates/app/views/checkouts/existing_payment/stripe/_default.html.erb +7 -0
  59. data/lib/generators/solidus_stripe/install/templates/app/views/checkouts/payment/_stripe.html.erb +74 -0
  60. data/lib/generators/solidus_stripe/install/templates/app/views/orders/payment_info/_stripe.html.erb +32 -0
  61. data/lib/generators/solidus_stripe/install/templates/config/initializers/solidus_stripe.rb +31 -0
  62. data/lib/solidus_stripe/configuration.rb +24 -3
  63. data/lib/solidus_stripe/engine.rb +19 -6
  64. data/lib/solidus_stripe/money_to_stripe_amount_converter.rb +109 -0
  65. data/lib/solidus_stripe/refunds_synchronizer.rb +98 -0
  66. data/lib/solidus_stripe/seeds.rb +19 -0
  67. data/lib/solidus_stripe/testing_support/factories.rb +153 -0
  68. data/lib/solidus_stripe/version.rb +1 -1
  69. data/lib/solidus_stripe/webhook/event.rb +91 -0
  70. data/lib/solidus_stripe.rb +0 -2
  71. data/solidus_stripe.gemspec +29 -6
  72. data/spec/lib/solidus_stripe/configuration_spec.rb +21 -0
  73. data/spec/lib/solidus_stripe/money_to_stripe_amount_converter_spec.rb +133 -0
  74. data/spec/lib/solidus_stripe/refunds_synchronizer_spec.rb +238 -0
  75. data/spec/lib/solidus_stripe/seeds_spec.rb +43 -0
  76. data/spec/lib/solidus_stripe/webhook/event_spec.rb +134 -0
  77. data/spec/models/concerns/solidus_stripe/log_entries_spec.rb +54 -0
  78. data/spec/models/solidus_stripe/customer_spec.rb +47 -0
  79. data/spec/models/solidus_stripe/gateway_spec.rb +281 -0
  80. data/spec/models/solidus_stripe/payment_intent_spec.rb +78 -0
  81. data/spec/models/solidus_stripe/payment_method_spec.rb +137 -0
  82. data/spec/models/solidus_stripe/payment_source_spec.rb +25 -0
  83. data/spec/requests/solidus_stripe/intents_controller_spec.rb +29 -0
  84. data/spec/requests/solidus_stripe/webhooks_controller/charge/refunded_spec.rb +31 -0
  85. data/spec/requests/solidus_stripe/webhooks_controller/payment_intent/canceled_spec.rb +23 -0
  86. data/spec/requests/solidus_stripe/webhooks_controller/payment_intent/payment_failed_spec.rb +23 -0
  87. data/spec/requests/solidus_stripe/webhooks_controller/payment_intent/succeeded_spec.rb +29 -0
  88. data/spec/requests/solidus_stripe/webhooks_controller_spec.rb +56 -0
  89. data/spec/solidus_stripe_spec_helper.rb +10 -0
  90. data/spec/subscribers/solidus_stripe/webhook/charge_subscriber_spec.rb +33 -0
  91. data/spec/subscribers/solidus_stripe/webhook/payment_intent_subscriber_spec.rb +297 -0
  92. data/spec/support/solidus_stripe/backend_test_helper.rb +185 -0
  93. data/spec/support/solidus_stripe/checkout_test_helper.rb +365 -0
  94. data/spec/support/solidus_stripe/factories.rb +5 -0
  95. data/spec/support/solidus_stripe/webhook/data_fixtures.rb +106 -0
  96. data/spec/support/solidus_stripe/webhook/event_with_context_factory.rb +82 -0
  97. data/spec/support/solidus_stripe/webhook/request_helper.rb +32 -0
  98. data/spec/system/backend/solidus_stripe/orders/payments_spec.rb +145 -0
  99. data/spec/system/frontend/.keep +0 -0
  100. data/spec/system/frontend/solidus_stripe/checkout_spec.rb +206 -0
  101. data/tmp/.keep +0 -0
  102. metadata +196 -75
  103. data/.rubocop_todo.yml +0 -298
  104. data/.travis.yml +0 -28
  105. data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-cart-page-checkout.js +0 -122
  106. data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-elements.js +0 -148
  107. data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-init.js +0 -20
  108. data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-payment-intents.js +0 -84
  109. data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-payment-request-button-shared.js +0 -160
  110. data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-payment.js +0 -16
  111. data/app/assets/javascripts/spree/frontend/solidus_stripe.js +0 -6
  112. data/app/controllers/solidus_stripe/payment_request_controller.rb +0 -52
  113. data/app/controllers/spree/stripe_controller.rb +0 -13
  114. data/app/decorators/models/spree/order_update_attributes_decorator.rb +0 -39
  115. data/app/decorators/models/spree/payment_decorator.rb +0 -11
  116. data/app/decorators/models/spree/refund_decorator.rb +0 -9
  117. data/app/models/solidus_stripe/address_from_params_service.rb +0 -72
  118. data/app/models/solidus_stripe/create_intents_payment_service.rb +0 -114
  119. data/app/models/solidus_stripe/prepare_order_for_payment_service.rb +0 -46
  120. data/app/models/solidus_stripe/shipping_rates_service.rb +0 -46
  121. data/app/models/spree/payment_method/stripe_credit_card.rb +0 -230
  122. data/bin/r +0 -13
  123. data/bin/sandbox_rails +0 -18
  124. data/db/migrate/20181010123508_update_stripe_payment_method_type_to_credit_card.rb +0 -21
  125. data/lib/assets/stylesheets/spree/frontend/solidus_stripe.scss +0 -11
  126. data/lib/solidus_stripe/testing_support/card_input_helper.rb +0 -34
  127. data/lib/tasks/solidus_stripe/db/seed.rake +0 -14
  128. data/lib/views/api/spree/api/payments/source_views/_stripe.json.jbuilder +0 -3
  129. data/lib/views/backend/spree/admin/log_entries/_stripe.html.erb +0 -28
  130. data/lib/views/backend/spree/admin/payments/source_forms/_stripe.html.erb +0 -1
  131. data/lib/views/backend/spree/admin/payments/source_views/_stripe.html.erb +0 -1
  132. data/lib/views/frontend/spree/checkout/existing_payment/_stripe.html.erb +0 -1
  133. data/lib/views/frontend/spree/checkout/payment/_stripe.html.erb +0 -8
  134. data/lib/views/frontend/spree/checkout/payment/v2/_javascript.html.erb +0 -78
  135. data/lib/views/frontend/spree/checkout/payment/v3/_elements.html.erb +0 -1
  136. data/lib/views/frontend/spree/checkout/payment/v3/_form_elements.html.erb +0 -40
  137. data/lib/views/frontend/spree/checkout/payment/v3/_intents.html.erb +0 -1
  138. data/lib/views/frontend/spree/checkout/payment/v3/_stripe.html.erb +0 -2
  139. data/lib/views/frontend/spree/orders/_stripe_payment_request_button.html.erb +0 -14
  140. data/spec/features/stripe_checkout_spec.rb +0 -486
  141. data/spec/models/solidus_stripe/address_from_params_service_spec.rb +0 -87
  142. data/spec/models/solidus_stripe/create_intents_payment_service_spec.rb +0 -127
  143. data/spec/models/solidus_stripe/prepare_order_for_payment_service_spec.rb +0 -65
  144. data/spec/models/solidus_stripe/shipping_rates_service_spec.rb +0 -54
  145. data/spec/models/spree/payment_method/stripe_credit_card_spec.rb +0 -350
  146. data/spec/requests/payment_requests_spec.rb +0 -152
  147. data/spec/spec_helper.rb +0 -37
  148. 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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusStripe
4
+ def self.table_name_prefix
5
+ "solidus_stripe_"
6
+ end
7
+ 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