spree_stripe 1.0.2

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.
Files changed (100) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.md +14 -0
  3. data/README.md +132 -0
  4. data/Rakefile +21 -0
  5. data/app/assets/config/spree_stripe_manifest.js +3 -0
  6. data/app/controllers/spree/api/v2/storefront/stripe/base_controller.rb +21 -0
  7. data/app/controllers/spree/api/v2/storefront/stripe/payment_intents_controller.rb +61 -0
  8. data/app/controllers/spree/api/v2/storefront/stripe/setup_intents_controller.rb +19 -0
  9. data/app/controllers/spree_stripe/apple_pay_domain_verification_controller.rb +11 -0
  10. data/app/controllers/spree_stripe/payment_intents_controller.rb +61 -0
  11. data/app/controllers/spree_stripe/store_controller_decorator.rb +9 -0
  12. data/app/controllers/stripe_event/webhook_controller_decorator.rb +17 -0
  13. data/app/helpers/spree_stripe/base_helper.rb +13 -0
  14. data/app/javascript/spree_stripe/application.js +18 -0
  15. data/app/javascript/spree_stripe/controllers/stripe_button_controller.js +452 -0
  16. data/app/javascript/spree_stripe/controllers/stripe_controller.js +311 -0
  17. data/app/jobs/spree_stripe/base_job.rb +5 -0
  18. data/app/jobs/spree_stripe/complete_order_job.rb +8 -0
  19. data/app/jobs/spree_stripe/create_webhook_endpoint_job.rb +7 -0
  20. data/app/jobs/spree_stripe/register_domain_job.rb +20 -0
  21. data/app/models/spree_stripe/base.rb +6 -0
  22. data/app/models/spree_stripe/credit_card_decorator.rb +13 -0
  23. data/app/models/spree_stripe/custom_domain_decorator.rb +18 -0
  24. data/app/models/spree_stripe/gateway.rb +366 -0
  25. data/app/models/spree_stripe/order_decorator.rb +19 -0
  26. data/app/models/spree_stripe/payment_decorator.rb +46 -0
  27. data/app/models/spree_stripe/payment_intent.rb +66 -0
  28. data/app/models/spree_stripe/payment_method_decorator.rb +18 -0
  29. data/app/models/spree_stripe/payment_methods_webhook_key.rb +8 -0
  30. data/app/models/spree_stripe/payment_source_decorator.rb +9 -0
  31. data/app/models/spree_stripe/payment_sources/affirm.rb +9 -0
  32. data/app/models/spree_stripe/payment_sources/after_pay.rb +13 -0
  33. data/app/models/spree_stripe/payment_sources/alipay.rb +9 -0
  34. data/app/models/spree_stripe/payment_sources/ideal.rb +15 -0
  35. data/app/models/spree_stripe/payment_sources/klarna.rb +9 -0
  36. data/app/models/spree_stripe/payment_sources/link.rb +13 -0
  37. data/app/models/spree_stripe/payment_sources/przelewy24.rb +11 -0
  38. data/app/models/spree_stripe/payment_sources/sepa_debit.rb +13 -0
  39. data/app/models/spree_stripe/store_decorator.rb +24 -0
  40. data/app/models/spree_stripe/webhook_key.rb +14 -0
  41. data/app/presenters/spree_stripe/customer_presenter.rb +35 -0
  42. data/app/presenters/spree_stripe/payment_intent_presenter.rb +103 -0
  43. data/app/serializers/spree/api/v2/platform/affirm_serializer.rb +9 -0
  44. data/app/serializers/spree/api/v2/platform/after_pay_serializer.rb +9 -0
  45. data/app/serializers/spree/api/v2/platform/alipay_serializer.rb +9 -0
  46. data/app/serializers/spree/api/v2/platform/ideal_serializer.rb +9 -0
  47. data/app/serializers/spree/api/v2/platform/klarna_serializer.rb +9 -0
  48. data/app/serializers/spree/api/v2/platform/link_serializer.rb +9 -0
  49. data/app/serializers/spree/api/v2/platform/przelewy24_serializer.rb +9 -0
  50. data/app/serializers/spree/api/v2/platform/sepa_debit_serializer.rb +9 -0
  51. data/app/serializers/spree/v2/storefront/affirm_serializer.rb +7 -0
  52. data/app/serializers/spree/v2/storefront/after_pay_serializer.rb +7 -0
  53. data/app/serializers/spree/v2/storefront/alipay_serializer.rb +7 -0
  54. data/app/serializers/spree/v2/storefront/ideal_serializer.rb +7 -0
  55. data/app/serializers/spree/v2/storefront/klarna_serializer.rb +7 -0
  56. data/app/serializers/spree/v2/storefront/link_serializer.rb +7 -0
  57. data/app/serializers/spree/v2/storefront/payment_intent_serializer.rb +11 -0
  58. data/app/serializers/spree/v2/storefront/przelewy24_serializer.rb +7 -0
  59. data/app/serializers/spree/v2/storefront/sepa_debit_serializer.rb +7 -0
  60. data/app/services/spree_stripe/complete_order.rb +99 -0
  61. data/app/services/spree_stripe/create_gateway_webhooks.rb +55 -0
  62. data/app/services/spree_stripe/create_payment.rb +43 -0
  63. data/app/services/spree_stripe/create_payment_intent.rb +40 -0
  64. data/app/services/spree_stripe/create_setup_intent.rb +20 -0
  65. data/app/services/spree_stripe/create_source.rb +87 -0
  66. data/app/services/spree_stripe/register_domain.rb +22 -0
  67. data/app/services/spree_stripe/webhook_handlers/payment_intent_payment_failed.rb +18 -0
  68. data/app/services/spree_stripe/webhook_handlers/payment_intent_succeeded.rb +13 -0
  69. data/app/services/spree_stripe/webhook_handlers/setup_intent_succeeded.rb +34 -0
  70. data/app/views/spree/admin/payment_methods/configuration_guides/_spree_stripe.html.erb +0 -0
  71. data/app/views/spree/admin/payment_methods/custom_form_fields/_spree_stripe.html.erb +47 -0
  72. data/app/views/spree/admin/payment_methods/descriptions/_spree_stripe.html.erb +15 -0
  73. data/app/views/spree/checkout/payment/_spree_stripe.html.erb +63 -0
  74. data/app/views/spree_stripe/_head.html.erb +2 -0
  75. data/app/views/spree_stripe/_quick_checkout.html.erb +34 -0
  76. data/config/importmap.rb +8 -0
  77. data/config/initializers/spree.rb +8 -0
  78. data/config/initializers/stripe.rb +14 -0
  79. data/config/locales/en.yml +5 -0
  80. data/config/routes.rb +24 -0
  81. data/db/migrate/20250310152812_setup_spree_stripe_models.rb +41 -0
  82. data/lib/generators/spree_stripe/install/install_generator.rb +20 -0
  83. data/lib/spree_stripe/configuration.rb +5 -0
  84. data/lib/spree_stripe/engine.rb +36 -0
  85. data/lib/spree_stripe/factories.rb +3 -0
  86. data/lib/spree_stripe/testing_support/factories/after_pay_payment_source_factory.rb +6 -0
  87. data/lib/spree_stripe/testing_support/factories/alipay_payment_source_factory.rb +6 -0
  88. data/lib/spree_stripe/testing_support/factories/gateway_factory.rb +23 -0
  89. data/lib/spree_stripe/testing_support/factories/ideal_payment_source_factory.rb +6 -0
  90. data/lib/spree_stripe/testing_support/factories/klarna_payment_source_factory.rb +6 -0
  91. data/lib/spree_stripe/testing_support/factories/link_payment_source_factory.rb +6 -0
  92. data/lib/spree_stripe/testing_support/factories/payment_intent_factory.rb +24 -0
  93. data/lib/spree_stripe/testing_support/factories/payment_source_factory.rb +8 -0
  94. data/lib/spree_stripe/testing_support/factories/przelewy24_payment_source_factory.rb +6 -0
  95. data/lib/spree_stripe/testing_support/factories/sepa_debit_payment_source_factory.rb +6 -0
  96. data/lib/spree_stripe/testing_support/factories/webhook_key_factory.rb +23 -0
  97. data/lib/spree_stripe/version.rb +7 -0
  98. data/lib/spree_stripe.rb +14 -0
  99. data/vendor/javascript/@stripe--stripe-js--dist--pure.esm.js.js +4 -0
  100. metadata +295 -0
@@ -0,0 +1,366 @@
1
+ module SpreeStripe
2
+ class Gateway < ::Spree::Gateway
3
+ include SpreeStripe::Gateway::Tax if defined?(SpreeStripe::Gateway::Tax)
4
+
5
+ preference :publishable_key, :password
6
+ preference :secret_key, :password
7
+
8
+ has_one_attached :apple_developer_merchantid_domain_association, service: Spree.private_storage_service_name
9
+
10
+ validates :preferred_secret_key, :preferred_publishable_key, presence: true
11
+ validate :validate_secret_key, unless: -> { Rails.env.test? }, if: -> { preferred_secret_key.present? }
12
+
13
+ after_commit :create_webhook_endpoint_async, on: %i[create update]
14
+ after_commit :register_domain, on: :create
15
+
16
+ has_many :payment_intents, class_name: 'SpreeStripe::PaymentIntent', foreign_key: 'payment_method_id', dependent: :delete_all
17
+
18
+ def self.webhook_url
19
+ "https://#{Rails.application.routes.default_url_options[:host]}/stripe"
20
+ end
21
+
22
+ def provider_class
23
+ self.class
24
+ end
25
+
26
+ # @param amount_in_cents [Integer] the amount in cents to capture
27
+ # @param payment_source [Spree::CreditCard | Spree::PaymentSource]
28
+ # @param gateway_options [Hash] this is an instance of Spree::Payment::GatewayOptions.to_hash
29
+ def authorize(amount_in_cents, payment_source, gateway_options = {})
30
+ handle_authorize_or_purchase(amount_in_cents, payment_source, gateway_options)
31
+ end
32
+
33
+ # @param amount_in_cents [Integer] the amount in cents to capture
34
+ # @param payment_source [Spree::CreditCard | Spree::PaymentSource]
35
+ # @param gateway_options [Hash] this is an instance of Spree::Payment::GatewayOptions.to_hash
36
+ def purchase(amount_in_cents, payment_source, gateway_options = {})
37
+ handle_authorize_or_purchase(amount_in_cents, payment_source, gateway_options)
38
+ end
39
+
40
+ # the behavior for authorize and purchase is the same, so we can use the same method to handle both
41
+ def handle_authorize_or_purchase(amount_in_cents, payment_source, gateway_options)
42
+ order_number, payment_number = gateway_options[:order_id].split('-')
43
+
44
+ return failure('Order number is invalid') if order_number.blank?
45
+ return failure('Payment number is invalid') if payment_number.blank?
46
+
47
+ order = Spree::Order.where(store_id: stores.ids).find_by(number: order_number)
48
+ payment = order.payments.find_by(number: payment_number)
49
+
50
+ protect_from_error do
51
+ # eg. payment created via admin
52
+ payment = ensure_payment_intent_exists_for_payment(payment, amount_in_cents, payment_source)
53
+ stripe_payment_intent = retrieve_payment_intent(payment.response_code)
54
+
55
+ response = if stripe_payment_intent.status == 'succeeded'
56
+ # payment intent is already confirmed via Stripe JS SDK
57
+ stripe_payment_intent
58
+ else
59
+ confirm_payment_intent(stripe_payment_intent.id)
60
+ end
61
+
62
+ success(response.id, response)
63
+ end
64
+ end
65
+
66
+ def credit(amount_in_cents, _source, payment_intent_id, _gateway_options = {})
67
+ protect_from_error do
68
+ payload = {
69
+ amount: amount_in_cents,
70
+ payment_intent: payment_intent_id
71
+ }
72
+
73
+ response = send_request { Stripe::Refund.create(payload) }
74
+
75
+ success(response.id, response)
76
+ end
77
+ end
78
+
79
+ def capture(amount_in_cents, payment_intent_id, _gateway_options = {})
80
+ protect_from_error do
81
+ stripe_payment_intent = retrieve_payment_intent(payment_intent_id)
82
+
83
+ response = if stripe_payment_intent.status == 'requires_capture'
84
+ capture_payment_intent(payment_intent_id, amount_in_cents)
85
+ elsif stripe_payment_intent.status == 'succeeded'
86
+ stripe_payment_intent
87
+ else
88
+ raise Spree::Core::GatewayError, "Payment intent status is #{stripe_payment_intent.status}"
89
+ end
90
+
91
+ success(response.id, response)
92
+ end
93
+ end
94
+
95
+ def void(response_code, source, gateway_options)
96
+ raise NotImplementedError
97
+ end
98
+
99
+ def cancel(payment_intent_id, payment = nil)
100
+ protect_from_error do
101
+ if payment&.completed?
102
+ amount = payment.credit_allowed
103
+ return success(payment_intent_id, {}) if amount.zero?
104
+ # Don't create a refund if the payment is for a shipment, we will create a refund for the whole shipping cost instead
105
+ return success(payment_intent_id, {}) if payment.respond_to?(:for_shipment?) && payment.for_shipment?
106
+
107
+ refund = payment.refunds.create!(
108
+ amount: amount,
109
+ reason: Spree::RefundReason.order_canceled_reason,
110
+ refunder_id: payment.order.canceler_id
111
+ )
112
+
113
+ # Spree::Refund#response has the response from the `credit` action
114
+ # For the authorization ID we need to use the payment.response_code (the payment intent ID)
115
+ # Otherwise we'll overwrite the payment authorization with the refund ID
116
+ success(payment.response_code, refund.response.params)
117
+ else
118
+ response = cancel_payment_intent(payment_intent_id)
119
+ success(response.id, response)
120
+ end
121
+ end
122
+ end
123
+
124
+ def fetch_or_create_customer(order: nil, user: nil)
125
+ user ||= order&.user
126
+ return nil unless user
127
+
128
+ gateway_customers.find_by(user: user) || create_customer(order: order, user: user)
129
+ end
130
+
131
+ def create_customer(order: nil, user: nil)
132
+ user ||= order&.user
133
+ address = order&.bill_address || user&.bill_address
134
+ name = order&.name || user&.name
135
+ email = order&.email || user&.email
136
+
137
+ payload = SpreeStripe::CustomerPresenter.new(
138
+ name: name,
139
+ email: email,
140
+ address: address
141
+ ).call
142
+
143
+ response = send_request { Stripe::Customer.create(payload) }
144
+
145
+ customer = gateway_customers.build(user: user, profile_id: response.id)
146
+ customer.save! if user.present?
147
+ customer
148
+ end
149
+
150
+ # Creates a Stripe payment intent for the order
151
+ #
152
+ # @param amount_in_cents [Integer] the amount in cents
153
+ # @param order [Spree::Order] the order to create a payment intent for
154
+ # @param payment_method_id [String] Stripe payment method id to use, eg. a card token
155
+ # @param off_session [Boolean] whether the payment intent is off session
156
+ # @param customer_profile_id [String] Stripe customer profile id to use, eg. cus_123
157
+ # @return [ActiveMerchant::Billing::Response] the response from the payment intent creation
158
+ def create_payment_intent(amount_in_cents, order, payment_method_id: nil, off_session: false, customer_profile_id: nil)
159
+ payload = SpreeStripe::PaymentIntentPresenter.new(
160
+ amount: amount_in_cents,
161
+ order: order,
162
+ customer: customer_profile_id || fetch_or_create_customer(order: order)&.profile_id,
163
+ payment_method_id: payment_method_id,
164
+ off_session: off_session
165
+ ).call
166
+
167
+ protect_from_error do
168
+ response = send_request { Stripe::PaymentIntent.create(payload) }
169
+
170
+ success(response.id, response)
171
+ end
172
+ end
173
+
174
+ # Updates a Stripe payment intent for the order
175
+ #
176
+ # @param payment_intent_id [String] Stripe payment intent id
177
+ # @param amount_in_cents [Integer] the amount in cents
178
+ # @param order [Spree::Order] the order to update the payment intent for
179
+ # @param payment_method_id [String] Stripe payment method id to use, eg. a card token
180
+ # @return [ActiveMerchant::Billing::Response] the response from the payment intent update
181
+ def update_payment_intent(payment_intent_id, amount_in_cents, order, payment_method_id = nil)
182
+ protect_from_error do
183
+ payload = SpreeStripe::PaymentIntentPresenter.new(
184
+ amount: amount_in_cents,
185
+ order: order,
186
+ customer: fetch_or_create_customer(order: order)&.profile_id,
187
+ payment_method_id: payment_method_id
188
+ ).call.slice(:amount, :currency, :payment_method, :shipping, :customer)
189
+
190
+ response = send_request { Stripe::PaymentIntent.update(payment_intent_id, payload) }
191
+
192
+ success(response.id, response)
193
+ end
194
+ end
195
+
196
+ def retrieve_payment_intent(payment_intent_id)
197
+ send_request { Stripe::PaymentIntent.retrieve(payment_intent_id) }
198
+ end
199
+
200
+ def confirm_payment_intent(payment_intent_id)
201
+ send_request { Stripe::PaymentIntent.confirm(payment_intent_id) }
202
+ end
203
+
204
+ def capture_payment_intent(payment_intent_id, amount_in_cents)
205
+ send_request { Stripe::PaymentIntent.capture(payment_intent_id, { amount_to_capture: amount_in_cents }) }
206
+ end
207
+
208
+ # Cancels a Stripe payment intent
209
+ #
210
+ # @param payment_intent_id [String] Stripe payment intent ID, eg. pi_123
211
+ def cancel_payment_intent(payment_intent_id)
212
+ send_request { Stripe::PaymentIntent.cancel(payment_intent_id) }
213
+ end
214
+
215
+ # Ensures a Stripe payment intent exists for Spree payment
216
+ #
217
+ # @param payment [Spree::Payment] the payment to ensure a payment intent exists for
218
+ # @param amount_in_cents [Integer] the amount in cents
219
+ # @param payment_source [Spree::CreditCard | Spree::PaymentSource] the payment source to use
220
+ # @return [Spree::Payment] the payment with the payment intent
221
+ def ensure_payment_intent_exists_for_payment(payment, amount_in_cents = nil, payment_source = nil)
222
+ return payment if payment.response_code.present?
223
+
224
+ amount_in_cents ||= payment.display_amount.cents
225
+ payment_source ||= payment.source
226
+
227
+ response = create_payment_intent(
228
+ amount_in_cents,
229
+ payment.order,
230
+ payment_method_id: payment_source.gateway_payment_profile_id,
231
+ off_session: true,
232
+ customer_profile_id: payment_source.gateway_customer_profile_id
233
+ )
234
+
235
+ payment.update_columns(
236
+ response_code: response.authorization,
237
+ updated_at: Time.current
238
+ )
239
+
240
+ payment
241
+ end
242
+
243
+ def retrieve_charge(charge_id)
244
+ send_request { Stripe::Charge.retrieve(charge_id) }
245
+ end
246
+
247
+ def create_ephemeral_key(customer_id)
248
+ protect_from_error do
249
+ response = send_request { Stripe::EphemeralKey.create({ customer: customer_id }, { stripe_version: Stripe.api_version }) }
250
+
251
+ success(response.secret, response)
252
+ end
253
+ end
254
+
255
+ def create_setup_intent(customer_id)
256
+ protect_from_error do
257
+ response = send_request { Stripe::SetupIntent.create({ customer: customer_id, automatic_payment_methods: { enabled: true } }) }
258
+
259
+ success(response.client_secret, response)
260
+ end
261
+ end
262
+
263
+ def apple_domain_association_file_content
264
+ @apple_domain_association_file_content ||= apple_developer_merchantid_domain_association&.download
265
+ end
266
+
267
+ def payment_profiles_supported?
268
+ true
269
+ end
270
+
271
+ def default_name
272
+ 'Stripe'
273
+ end
274
+
275
+ def method_type
276
+ 'spree_stripe'
277
+ end
278
+
279
+ def payment_icon_name
280
+ 'stripe'
281
+ end
282
+
283
+ def description_partial_name
284
+ 'spree_stripe'
285
+ end
286
+
287
+ def custom_form_fields_partial_name
288
+ 'spree_stripe'
289
+ end
290
+
291
+ def configuration_guide_partial_name
292
+ 'spree_stripe'
293
+ end
294
+
295
+ def gateway_dashboard_payment_url(payment)
296
+ return if payment.transaction_id.blank?
297
+
298
+ "https://dashboard.stripe.com/payments/#{payment.transaction_id}"
299
+ end
300
+
301
+ def create_webhook_endpoint
302
+ SpreeStripe::CreateGatewayWebhooks.new.call(payment_method: self)
303
+ end
304
+
305
+ def create_profile(payment)
306
+ customer = fetch_or_create_customer(order: payment.order)
307
+
308
+ payment.source.update(gateway_customer_profile_id: customer.profile_id) if payment.source.present? && customer.present?
309
+ end
310
+
311
+ def api_options
312
+ { api_key: preferred_secret_key, api_version: Stripe.api_version }
313
+ end
314
+
315
+ def send_request(&block)
316
+ result, _response = client.request(&block)
317
+ result
318
+ end
319
+
320
+ def client
321
+ @client ||= Stripe::StripeClient.new(api_options)
322
+ end
323
+
324
+ private
325
+
326
+ def validate_secret_key
327
+ Stripe::Refund.list({ limit: 0 }, api_options)
328
+ rescue Stripe::AuthenticationError
329
+ errors.add(:base, 'Secret key is invalid')
330
+ rescue Stripe::PermissionError => e
331
+ errors.add(:base, 'You have provided your publishable key instead of your secret key') if e.error&.code == 'secret_key_required'
332
+ rescue Stripe::StripeError
333
+ errors.add(:base, 'Something went wrong with Stripe. Try again later.')
334
+ end
335
+
336
+ def success(authorization, full_response)
337
+ ActiveMerchant::Billing::Response.new(true, nil, full_response.as_json, authorization: authorization)
338
+ end
339
+
340
+ def failure(error = nil)
341
+ ActiveMerchant::Billing::Response.new(false, error)
342
+ end
343
+
344
+ def protect_from_error
345
+ yield
346
+ rescue Stripe::StripeError => e
347
+ raise Spree::Core::GatewayError, e.message
348
+ end
349
+
350
+ def create_webhook_endpoint_async
351
+ return if webhook_keys.any?
352
+
353
+ SpreeStripe::CreateWebhookEndpointJob.perform_later(id)
354
+ end
355
+
356
+ def register_domain
357
+ stores.each do |store|
358
+ RegisterDomainJob.perform_later(store.id, 'store')
359
+
360
+ store.custom_domains.each do |custom_domain|
361
+ RegisterDomainJob.perform_later(custom_domain.id, 'custom_domain')
362
+ end
363
+ end
364
+ end
365
+ end
366
+ end
@@ -0,0 +1,19 @@
1
+ module SpreeStripe
2
+ module OrderDecorator
3
+ def self.prepended(base)
4
+ base.has_many :payment_intents, class_name: 'SpreeStripe::PaymentIntent', dependent: :destroy
5
+ end
6
+
7
+ def update_payment_intents
8
+ return if completed?
9
+ return unless total_minus_store_credits.positive?
10
+
11
+ payment_intents.each do |payment_intent|
12
+ payment_intent.update!(amount: total_minus_store_credits)
13
+ end
14
+ end
15
+ end
16
+ end
17
+
18
+ Spree::Order.prepend(SpreeStripe::OrderDecorator)
19
+ Spree::Order.register_update_hook :update_payment_intents
@@ -0,0 +1,46 @@
1
+ module SpreeStripe
2
+ module PaymentDecorator
3
+ AVS_CODES = {
4
+ 'pass' => {
5
+ 'pass' => 'Y',
6
+ 'fail' => 'A',
7
+ 'unchecked' => 'B'
8
+ },
9
+ 'fail' => {
10
+ 'pass' => 'Z',
11
+ 'fail' => 'N'
12
+ },
13
+ 'unchecked' => {
14
+ 'pass' => 'P',
15
+ 'unchecked' => 'I'
16
+ }
17
+ }.freeze
18
+
19
+ CVV_CODES = {
20
+ 'pass' => 'M',
21
+ 'fail' => 'N',
22
+ 'unchecked' => 'P'
23
+ }.freeze
24
+
25
+ def self.prepended(base)
26
+ base.store_accessor :private_metadata, :stripe_charge_id
27
+ base.store_accessor :private_metadata, :stripe_tax_transaction_id
28
+
29
+ base.before_save :set_cvv_and_avs_response_from_metadata, if: :credit_card_source?
30
+ end
31
+
32
+ def set_cvv_and_avs_response_from_metadata
33
+ checks = source.private_metadata[:checks]
34
+ return if checks.blank?
35
+
36
+ self.avs_response ||= AVS_CODES.dig(checks[:address_line1_check], checks[:address_postal_code_check])
37
+ self.cvv_response_code ||= CVV_CODES[checks[:cvc_check]]
38
+ end
39
+
40
+ def credit_card_source?
41
+ source.present? && source.is_a?(Spree::CreditCard)
42
+ end
43
+ end
44
+ end
45
+
46
+ Spree::Payment.prepend(SpreeStripe::PaymentDecorator)
@@ -0,0 +1,66 @@
1
+ module SpreeStripe
2
+ class PaymentIntent < Base
3
+ #
4
+ # Associations
5
+ #
6
+ belongs_to :order, class_name: 'Spree::Order', foreign_key: 'order_id'
7
+ belongs_to :payment_method, class_name: 'Spree::PaymentMethod', foreign_key: 'payment_method_id'
8
+ has_one :payment, class_name: 'Spree::Payment', foreign_key: 'response_code', primary_key: 'stripe_id'
9
+
10
+ #
11
+ # Validations
12
+ #
13
+ validates :order, :payment_method, :client_secret, presence: true
14
+ validates :stripe_id, presence: true, uniqueness: { scope: :order_id }
15
+ validates :amount, presence: true, numericality: { greater_than: 0 }
16
+
17
+ #
18
+ # Callbacks
19
+ #
20
+ before_validation :set_amount_from_order
21
+ after_update :update_stripe_payment_intent, if: :amount_or_stripe_payment_method_id_changed?
22
+
23
+ #
24
+ # Delegations
25
+ #
26
+ delegate :api_options, to: :payment_method
27
+ delegate :store, :currency, to: :order
28
+
29
+ def stripe_payment_intent
30
+ @stripe_payment_intent ||= payment_method.retrieve_payment_intent(stripe_id)
31
+ end
32
+
33
+ def stripe_charge
34
+ @stripe_charge ||= payment_method.retrieve_charge(stripe_payment_intent.latest_charge)
35
+ end
36
+
37
+ # here we create a payment if it doesn't exist
38
+ # or we find it by the stripe_payment_intent_id
39
+ def find_or_create_payment!
40
+ return unless persisted?
41
+ return payment if payment.present?
42
+
43
+ SpreeStripe::CreatePayment.new(order: order, payment_intent: self, gateway: payment_method, amount: amount).call
44
+ end
45
+
46
+ def set_amount_from_order
47
+ self.amount = order&.total_minus_store_credits if order.present? && (amount.nil? || amount.zero?)
48
+ end
49
+
50
+ def update_stripe_payment_intent
51
+ payment_method.update_payment_intent(stripe_id, amount_in_cents, order, stripe_payment_method_id)
52
+ end
53
+
54
+ def amount_or_stripe_payment_method_id_changed?
55
+ amount_previously_changed? || stripe_payment_method_id_previously_changed?
56
+ end
57
+
58
+ def amount_in_cents
59
+ @amount_in_cents ||= money.cents
60
+ end
61
+
62
+ def money
63
+ @money ||= Spree::Money.new(amount, currency: currency)
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,18 @@
1
+ module SpreeStripe
2
+ module PaymentMethodDecorator
3
+ STRIPE_TYPE = 'SpreeStripe::Gateway'.freeze
4
+
5
+ def self.prepended(base)
6
+ base.has_many :payment_methods_webhook_keys, class_name: 'SpreeStripe::PaymentMethodsWebhookKey'
7
+ base.has_many :webhook_keys, through: :payment_methods_webhook_keys, class_name: 'SpreeStripe::WebhookKey'
8
+
9
+ base.scope :stripe, -> { where(type: STRIPE_TYPE) }
10
+ end
11
+
12
+ def stripe?
13
+ type == STRIPE_TYPE
14
+ end
15
+ end
16
+ end
17
+
18
+ Spree::PaymentMethod.prepend(SpreeStripe::PaymentMethodDecorator)
@@ -0,0 +1,8 @@
1
+ module SpreeStripe
2
+ class PaymentMethodsWebhookKey < Base
3
+ belongs_to :payment_method, class_name: 'Spree::PaymentMethod'
4
+ belongs_to :webhook_key, class_name: 'SpreeStripe::WebhookKey'
5
+
6
+ validates :payment_method, presence: true, uniqueness: { scope: :webhook_key_id }
7
+ end
8
+ end
@@ -0,0 +1,9 @@
1
+ module SpreeStripe
2
+ module PaymentSourceDecorator
3
+ def self.prepended(base)
4
+ base.attr_accessor :imported
5
+ end
6
+ end
7
+ end
8
+
9
+ Spree::PaymentSource.prepend(SpreeStripe::PaymentSourceDecorator)
@@ -0,0 +1,9 @@
1
+ module SpreeStripe
2
+ module PaymentSources
3
+ class Affirm < ::Spree::PaymentSource
4
+ def actions
5
+ %w[credit]
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,13 @@
1
+ module SpreeStripe
2
+ module PaymentSources
3
+ class AfterPay < ::Spree::PaymentSource
4
+ def actions
5
+ %w[credit]
6
+ end
7
+
8
+ def self.display_name
9
+ 'Afterpay'
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,9 @@
1
+ module SpreeStripe
2
+ module PaymentSources
3
+ class Alipay < ::Spree::PaymentSource
4
+ def actions
5
+ %w[credit]
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,15 @@
1
+ module SpreeStripe
2
+ module PaymentSources
3
+ class Ideal < ::Spree::PaymentSource
4
+ store_accessor :public_metadata, :bank, :last4
5
+
6
+ def actions
7
+ %w[credit]
8
+ end
9
+
10
+ def self.display_name
11
+ 'iDEAL'
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,9 @@
1
+ module SpreeStripe
2
+ module PaymentSources
3
+ class Klarna < ::Spree::PaymentSource
4
+ def actions
5
+ %w[credit]
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,13 @@
1
+ module SpreeStripe
2
+ module PaymentSources
3
+ class Link < ::Spree::PaymentSource
4
+ def actions
5
+ %w[credit]
6
+ end
7
+
8
+ def self.display_name
9
+ 'Link'
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,11 @@
1
+ module SpreeStripe
2
+ module PaymentSources
3
+ class Przelewy24 < ::Spree::PaymentSource
4
+ store_accessor :public_metadata, :bank
5
+
6
+ def actions
7
+ %w[credit]
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,13 @@
1
+ module SpreeStripe
2
+ module PaymentSources
3
+ class SepaDebit < ::Spree::PaymentSource
4
+ def actions
5
+ %w[credit]
6
+ end
7
+
8
+ def self.display_name
9
+ 'SEPA Debit'
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,24 @@
1
+ module SpreeStripe
2
+ module StoreDecorator
3
+ def self.prepended(base)
4
+ base.store_accessor :private_metadata, :stripe_apple_pay_domain_id
5
+ base.store_accessor :private_metadata, :stripe_top_level_domain_id
6
+ end
7
+
8
+ def stripe_gateway
9
+ @stripe_gateway ||= payment_methods.stripe.active.last
10
+ end
11
+
12
+ def handle_code_changes
13
+ super
14
+
15
+ SpreeStripe::RegisterDomainJob.perform_later(id)
16
+ end
17
+
18
+ def billing_name
19
+ name
20
+ end
21
+ end
22
+ end
23
+
24
+ Spree::Store.prepend(SpreeStripe::StoreDecorator)
@@ -0,0 +1,14 @@
1
+ module SpreeStripe
2
+ class WebhookKey < Base
3
+ validates :stripe_id, presence: true, uniqueness: true
4
+ validates :signing_secret, presence: true, uniqueness: true
5
+
6
+ has_many :payment_methods_webhook_keys, class_name: 'SpreeStripe::PaymentMethodsWebhookKey', dependent: :destroy
7
+ has_many :payment_methods, through: :payment_methods_webhook_keys, class_name: 'Spree::PaymentMethod', dependent: :destroy
8
+
9
+ if Rails.configuration.active_record.encryption.include?(:primary_key)
10
+ encrypts :stripe_id, deterministic: true
11
+ encrypts :signing_secret, deterministic: true
12
+ end
13
+ end
14
+ end