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.
- checksums.yaml +7 -0
- data/LICENSE.md +14 -0
- data/README.md +132 -0
- data/Rakefile +21 -0
- data/app/assets/config/spree_stripe_manifest.js +3 -0
- data/app/controllers/spree/api/v2/storefront/stripe/base_controller.rb +21 -0
- data/app/controllers/spree/api/v2/storefront/stripe/payment_intents_controller.rb +61 -0
- data/app/controllers/spree/api/v2/storefront/stripe/setup_intents_controller.rb +19 -0
- data/app/controllers/spree_stripe/apple_pay_domain_verification_controller.rb +11 -0
- data/app/controllers/spree_stripe/payment_intents_controller.rb +61 -0
- data/app/controllers/spree_stripe/store_controller_decorator.rb +9 -0
- data/app/controllers/stripe_event/webhook_controller_decorator.rb +17 -0
- data/app/helpers/spree_stripe/base_helper.rb +13 -0
- data/app/javascript/spree_stripe/application.js +18 -0
- data/app/javascript/spree_stripe/controllers/stripe_button_controller.js +452 -0
- data/app/javascript/spree_stripe/controllers/stripe_controller.js +311 -0
- data/app/jobs/spree_stripe/base_job.rb +5 -0
- data/app/jobs/spree_stripe/complete_order_job.rb +8 -0
- data/app/jobs/spree_stripe/create_webhook_endpoint_job.rb +7 -0
- data/app/jobs/spree_stripe/register_domain_job.rb +20 -0
- data/app/models/spree_stripe/base.rb +6 -0
- data/app/models/spree_stripe/credit_card_decorator.rb +13 -0
- data/app/models/spree_stripe/custom_domain_decorator.rb +18 -0
- data/app/models/spree_stripe/gateway.rb +366 -0
- data/app/models/spree_stripe/order_decorator.rb +19 -0
- data/app/models/spree_stripe/payment_decorator.rb +46 -0
- data/app/models/spree_stripe/payment_intent.rb +66 -0
- data/app/models/spree_stripe/payment_method_decorator.rb +18 -0
- data/app/models/spree_stripe/payment_methods_webhook_key.rb +8 -0
- data/app/models/spree_stripe/payment_source_decorator.rb +9 -0
- data/app/models/spree_stripe/payment_sources/affirm.rb +9 -0
- data/app/models/spree_stripe/payment_sources/after_pay.rb +13 -0
- data/app/models/spree_stripe/payment_sources/alipay.rb +9 -0
- data/app/models/spree_stripe/payment_sources/ideal.rb +15 -0
- data/app/models/spree_stripe/payment_sources/klarna.rb +9 -0
- data/app/models/spree_stripe/payment_sources/link.rb +13 -0
- data/app/models/spree_stripe/payment_sources/przelewy24.rb +11 -0
- data/app/models/spree_stripe/payment_sources/sepa_debit.rb +13 -0
- data/app/models/spree_stripe/store_decorator.rb +24 -0
- data/app/models/spree_stripe/webhook_key.rb +14 -0
- data/app/presenters/spree_stripe/customer_presenter.rb +35 -0
- data/app/presenters/spree_stripe/payment_intent_presenter.rb +103 -0
- data/app/serializers/spree/api/v2/platform/affirm_serializer.rb +9 -0
- data/app/serializers/spree/api/v2/platform/after_pay_serializer.rb +9 -0
- data/app/serializers/spree/api/v2/platform/alipay_serializer.rb +9 -0
- data/app/serializers/spree/api/v2/platform/ideal_serializer.rb +9 -0
- data/app/serializers/spree/api/v2/platform/klarna_serializer.rb +9 -0
- data/app/serializers/spree/api/v2/platform/link_serializer.rb +9 -0
- data/app/serializers/spree/api/v2/platform/przelewy24_serializer.rb +9 -0
- data/app/serializers/spree/api/v2/platform/sepa_debit_serializer.rb +9 -0
- data/app/serializers/spree/v2/storefront/affirm_serializer.rb +7 -0
- data/app/serializers/spree/v2/storefront/after_pay_serializer.rb +7 -0
- data/app/serializers/spree/v2/storefront/alipay_serializer.rb +7 -0
- data/app/serializers/spree/v2/storefront/ideal_serializer.rb +7 -0
- data/app/serializers/spree/v2/storefront/klarna_serializer.rb +7 -0
- data/app/serializers/spree/v2/storefront/link_serializer.rb +7 -0
- data/app/serializers/spree/v2/storefront/payment_intent_serializer.rb +11 -0
- data/app/serializers/spree/v2/storefront/przelewy24_serializer.rb +7 -0
- data/app/serializers/spree/v2/storefront/sepa_debit_serializer.rb +7 -0
- data/app/services/spree_stripe/complete_order.rb +99 -0
- data/app/services/spree_stripe/create_gateway_webhooks.rb +55 -0
- data/app/services/spree_stripe/create_payment.rb +43 -0
- data/app/services/spree_stripe/create_payment_intent.rb +40 -0
- data/app/services/spree_stripe/create_setup_intent.rb +20 -0
- data/app/services/spree_stripe/create_source.rb +87 -0
- data/app/services/spree_stripe/register_domain.rb +22 -0
- data/app/services/spree_stripe/webhook_handlers/payment_intent_payment_failed.rb +18 -0
- data/app/services/spree_stripe/webhook_handlers/payment_intent_succeeded.rb +13 -0
- data/app/services/spree_stripe/webhook_handlers/setup_intent_succeeded.rb +34 -0
- data/app/views/spree/admin/payment_methods/configuration_guides/_spree_stripe.html.erb +0 -0
- data/app/views/spree/admin/payment_methods/custom_form_fields/_spree_stripe.html.erb +47 -0
- data/app/views/spree/admin/payment_methods/descriptions/_spree_stripe.html.erb +15 -0
- data/app/views/spree/checkout/payment/_spree_stripe.html.erb +63 -0
- data/app/views/spree_stripe/_head.html.erb +2 -0
- data/app/views/spree_stripe/_quick_checkout.html.erb +34 -0
- data/config/importmap.rb +8 -0
- data/config/initializers/spree.rb +8 -0
- data/config/initializers/stripe.rb +14 -0
- data/config/locales/en.yml +5 -0
- data/config/routes.rb +24 -0
- data/db/migrate/20250310152812_setup_spree_stripe_models.rb +41 -0
- data/lib/generators/spree_stripe/install/install_generator.rb +20 -0
- data/lib/spree_stripe/configuration.rb +5 -0
- data/lib/spree_stripe/engine.rb +36 -0
- data/lib/spree_stripe/factories.rb +3 -0
- data/lib/spree_stripe/testing_support/factories/after_pay_payment_source_factory.rb +6 -0
- data/lib/spree_stripe/testing_support/factories/alipay_payment_source_factory.rb +6 -0
- data/lib/spree_stripe/testing_support/factories/gateway_factory.rb +23 -0
- data/lib/spree_stripe/testing_support/factories/ideal_payment_source_factory.rb +6 -0
- data/lib/spree_stripe/testing_support/factories/klarna_payment_source_factory.rb +6 -0
- data/lib/spree_stripe/testing_support/factories/link_payment_source_factory.rb +6 -0
- data/lib/spree_stripe/testing_support/factories/payment_intent_factory.rb +24 -0
- data/lib/spree_stripe/testing_support/factories/payment_source_factory.rb +8 -0
- data/lib/spree_stripe/testing_support/factories/przelewy24_payment_source_factory.rb +6 -0
- data/lib/spree_stripe/testing_support/factories/sepa_debit_payment_source_factory.rb +6 -0
- data/lib/spree_stripe/testing_support/factories/webhook_key_factory.rb +23 -0
- data/lib/spree_stripe/version.rb +7 -0
- data/lib/spree_stripe.rb +14 -0
- data/vendor/javascript/@stripe--stripe-js--dist--pure.esm.js.js +4 -0
- 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,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
|