Class: SpreeStripe::Gateway

Inherits:
Spree::Gateway
  • Object
show all
Includes:
Tax
Defined in:
app/models/spree_stripe/gateway.rb

Constant Summary collapse

DELAYED_NOTIFICATION_PAYMENT_METHOD_TYPES =
%w[sepa_debit us_bank_account].freeze
BANK_PAYMENT_METHOD_TYPES =
%w[customer_balance us_bank_account].freeze

Instance Method Summary collapse

Instance Method Details

#api_optionsObject



389
390
391
# File 'app/models/spree_stripe/gateway.rb', line 389

def api_options
  { api_key: preferred_secret_key, api_version: Stripe.api_version }
end

#apple_domain_association_file_contentObject



341
342
343
# File 'app/models/spree_stripe/gateway.rb', line 341

def apple_domain_association_file_content
  @apple_domain_association_file_content ||= apple_developer_merchantid_domain_association&.download
end

#attach_customer_to_credit_card(user) ⇒ Object



326
327
328
329
330
331
332
333
334
335
336
337
338
339
# File 'app/models/spree_stripe/gateway.rb', line 326

def attach_customer_to_credit_card(user)
  payment_method_id = user&.default_credit_card&.gateway_payment_profile_id
  return if payment_method_id.blank? || user&.default_credit_card&.gateway_customer_profile_id.present?

  customer = fetch_or_create_customer(user: user)
  return if customer.blank?

  send_request { Stripe::PaymentMethod.attach(payment_method_id, { customer: customer.profile_id }) }

  user.default_credit_card.update(gateway_customer_profile_id: customer.profile_id, gateway_customer_id: customer.id)
rescue Stripe::StripeError => e
  Rails.error.report(e, context: { payment_method_id: id, user_id: user.id }, source: 'spree_stripe')
  nil
end

#authorize(amount_in_cents, payment_source, gateway_options = {}) ⇒ Object

Parameters:

  • amount_in_cents (Integer)

    the amount in cents to capture

  • payment_source (Spree::CreditCard | Spree::PaymentSource)
  • gateway_options (Hash) (defaults to: {})

    this is an instance of Spree::Payment::GatewayOptions.to_hash



54
55
56
# File 'app/models/spree_stripe/gateway.rb', line 54

def authorize(amount_in_cents, payment_source, gateway_options = {})
  handle_authorize_or_purchase(amount_in_cents, payment_source, gateway_options)
end

#cancel(payment_intent_id, payment = nil) ⇒ Object



129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'app/models/spree_stripe/gateway.rb', line 129

def cancel(payment_intent_id, payment = nil)
  protect_from_error do
    if payment&.completed?
      amount = payment.credit_allowed
      return success(payment_intent_id, {}) if amount.zero?
      # Don't create a refund if the payment is for a shipment, we will create a refund for the whole shipping cost instead
      return success(payment_intent_id, {}) if payment.respond_to?(:for_shipment?) && payment.for_shipment?

      refund = payment.refunds.create!(
        amount: amount,
        reason: Spree::RefundReason.order_canceled_reason,
        refunder_id: payment.order.canceler_id
      )

      # Spree::Refund#response has the response from the `credit` action
      # For the authorization ID we need to use the payment.response_code (the payment intent ID)
      # Otherwise we'll overwrite the payment authorization with the refund ID
      success(payment.response_code, refund.response.params)
    else
      response = cancel_payment_intent(payment_intent_id)
      success(response.id, response)
    end
  end
end

#cancel_payment_intent(payment_intent_id) ⇒ Object

Cancels a Stripe payment intent

Parameters:

  • payment_intent_id (String)

    Stripe payment intent ID, eg. pi_123



252
253
254
# File 'app/models/spree_stripe/gateway.rb', line 252

def cancel_payment_intent(payment_intent_id)
  send_request { Stripe::PaymentIntent.cancel(payment_intent_id) }
end

#capture(amount_in_cents, payment_intent_id, _gateway_options = {}) ⇒ Object



104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'app/models/spree_stripe/gateway.rb', line 104

def capture(amount_in_cents, payment_intent_id, _gateway_options = {})
  protect_from_error do
    stripe_payment_intent = retrieve_payment_intent(payment_intent_id)

    response = if stripe_payment_intent.status == 'requires_capture'
                 capture_payment_intent(payment_intent_id, amount_in_cents)
               elsif stripe_payment_intent.status == 'succeeded'
                 stripe_payment_intent
               else
                 raise Spree::Core::GatewayError, "Payment intent status is #{stripe_payment_intent.status}"
               end

    success(response.id, response)
  end
end

#capture_payment_intent(payment_intent_id, amount_in_cents) ⇒ Object



245
246
247
# File 'app/models/spree_stripe/gateway.rb', line 245

def capture_payment_intent(payment_intent_id, amount_in_cents)
  send_request { Stripe::PaymentIntent.capture(payment_intent_id, { amount_to_capture: amount_in_cents }) }
end

#clientObject



398
399
400
# File 'app/models/spree_stripe/gateway.rb', line 398

def client
  @client ||= Stripe::StripeClient.new(api_options)
end

#configuration_guide_partial_nameObject



369
370
371
# File 'app/models/spree_stripe/gateway.rb', line 369

def configuration_guide_partial_name
  'spree_stripe'
end

#confirm_payment_intent(payment_intent_id) ⇒ Object



241
242
243
# File 'app/models/spree_stripe/gateway.rb', line 241

def confirm_payment_intent(payment_intent_id)
  send_request { Stripe::PaymentIntent.confirm(payment_intent_id) }
end

#create_customer(order: nil, user: nil) ⇒ Stripe::Customer

Creates a Stripe customer based on the order or user

Parameters:

  • order (Spree::Order) (defaults to: nil)

    the order to use for creating the Stripe customer

  • user (Spree::User) (defaults to: nil)

    the user to use for creating the Stripe customer

Returns:

  • (Stripe::Customer)

    the created Stripe customer



166
167
168
169
170
171
172
173
# File 'app/models/spree_stripe/gateway.rb', line 166

def create_customer(order: nil, user: nil)
  payload = build_customer_payload(order: order, user: user)
  response = send_request { Stripe::Customer.create(payload) }

  customer = gateway_customers.build(user: user, profile_id: response.id)
  customer.save! if user.present?
  customer
end

#create_ephemeral_key(customer_id) ⇒ Object



288
289
290
291
292
293
294
# File 'app/models/spree_stripe/gateway.rb', line 288

def create_ephemeral_key(customer_id)
  protect_from_error do
    response = send_request { Stripe::EphemeralKey.create({ customer: customer_id }, { stripe_version: Stripe.api_version }) }

    success(response.secret, response)
  end
end

#create_payment_intent(amount_in_cents, order, payment_method_id: nil, off_session: false, customer_profile_id: nil) ⇒ ActiveMerchant::Billing::Response

Creates a Stripe payment intent for the order

Parameters:

  • amount_in_cents (Integer)

    the amount in cents

  • order (Spree::Order)

    the order to create a payment intent for

  • payment_method_id (String) (defaults to: nil)

    Stripe payment method id to use, eg. a card token

  • off_session (Boolean) (defaults to: false)

    whether the payment intent is off session

  • customer_profile_id (String) (defaults to: nil)

    Stripe customer profile id to use, eg. cus_123

Returns:

  • (ActiveMerchant::Billing::Response)

    the response from the payment intent creation



199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
# File 'app/models/spree_stripe/gateway.rb', line 199

def create_payment_intent(amount_in_cents, order, payment_method_id: nil, off_session: false, customer_profile_id: nil)
  payload = SpreeStripe::PaymentIntentPresenter.new(
    amount: amount_in_cents,
    order: order,
    customer: customer_profile_id || fetch_or_create_customer(order: order)&.profile_id,
    payment_method_id: payment_method_id,
    off_session: off_session
  ).call

  protect_from_error do
    response = send_request { Stripe::PaymentIntent.create(payload) }

    success(response.id, response)
  end
end

#create_profile(payment) ⇒ Object



383
384
385
386
387
# File 'app/models/spree_stripe/gateway.rb', line 383

def create_profile(payment)
  customer = fetch_or_create_customer(order: payment.order)

  payment.source.update(gateway_customer_profile_id: customer.profile_id) if payment.source.present? && customer.present?
end

#create_setup_intent(customer_id) ⇒ Object



296
297
298
299
300
301
302
# File 'app/models/spree_stripe/gateway.rb', line 296

def create_setup_intent(customer_id)
  protect_from_error do
    response = send_request { Stripe::SetupIntent.create({ customer: customer_id, automatic_payment_methods: { enabled: true } }) }

    success(response.client_secret, response)
  end
end

#create_tax_calculation(order) ⇒ Object



304
305
306
307
308
309
310
311
312
# File 'app/models/spree_stripe/gateway.rb', line 304

def create_tax_calculation(order)
  protect_from_error do
    send_request do
      Stripe::Tax::Calculation.create(
        SpreeStripe::TaxPresenter.new(order: order).call
      )
    end
  end
end

#create_tax_transaction(payment_intent_id, tax_calculation_id) ⇒ Object



314
315
316
317
318
319
320
321
322
323
324
# File 'app/models/spree_stripe/gateway.rb', line 314

def create_tax_transaction(payment_intent_id, tax_calculation_id)
  protect_from_error do
    payload = {
      calculation: tax_calculation_id,
      reference: payment_intent_id,
      expand: ['line_items']
    }

    send_request { Stripe::Tax::Transaction.create_from_calculation(payload) }
  end
end

#create_webhook_endpointObject



379
380
381
# File 'app/models/spree_stripe/gateway.rb', line 379

def create_webhook_endpoint
  SpreeStripe::CreateGatewayWebhooks.new.call(payment_method: self)
end

#credit(amount_in_cents, _source, payment_intent_id, _gateway_options = {}) ⇒ Object



91
92
93
94
95
96
97
98
99
100
101
102
# File 'app/models/spree_stripe/gateway.rb', line 91

def credit(amount_in_cents, _source, payment_intent_id, _gateway_options = {})
  protect_from_error do
    payload = {
      amount: amount_in_cents,
      payment_intent: payment_intent_id
    }

    response = send_request { Stripe::Refund.create(payload) }

    success(response.id, response)
  end
end

#custom_form_fields_partial_nameObject



365
366
367
# File 'app/models/spree_stripe/gateway.rb', line 365

def custom_form_fields_partial_name
  'spree_stripe'
end

#default_nameObject



349
350
351
# File 'app/models/spree_stripe/gateway.rb', line 349

def default_name
  'Stripe'
end

#description_partial_nameObject



361
362
363
# File 'app/models/spree_stripe/gateway.rb', line 361

def description_partial_name
  'spree_stripe'
end

#ensure_payment_intent_exists_for_payment(payment, amount_in_cents = nil, payment_source = nil) ⇒ Spree::Payment

Ensures a Stripe payment intent exists for Spree payment

Parameters:

  • payment (Spree::Payment)

    the payment to ensure a payment intent exists for

  • amount_in_cents (Integer) (defaults to: nil)

    the amount in cents

  • payment_source (Spree::CreditCard | Spree::PaymentSource) (defaults to: nil)

    the payment source to use

Returns:

  • (Spree::Payment)

    the payment with the payment intent



262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
# File 'app/models/spree_stripe/gateway.rb', line 262

def ensure_payment_intent_exists_for_payment(payment, amount_in_cents = nil, payment_source = nil)
  return payment if payment.response_code.present?

  amount_in_cents ||= payment.display_amount.cents
  payment_source ||= payment.source

  response = create_payment_intent(
    amount_in_cents,
    payment.order,
    payment_method_id: payment_source.gateway_payment_profile_id,
    off_session: true,
    customer_profile_id: payment_source.gateway_customer_profile_id
  )

  payment.update_columns(
    response_code: response.authorization,
    updated_at: Time.current
  )

  payment
end

#fetch_or_create_customer(order: nil, user: nil) ⇒ Object



154
155
156
157
158
159
# File 'app/models/spree_stripe/gateway.rb', line 154

def fetch_or_create_customer(order: nil, user: nil)
  user ||= order&.user
  return nil unless user

  gateway_customers.find_by(user: user) || create_customer(order: order, user: user)
end

#gateway_dashboard_payment_url(payment) ⇒ Object



373
374
375
376
377
# File 'app/models/spree_stripe/gateway.rb', line 373

def gateway_dashboard_payment_url(payment)
  return if payment.transaction_id.blank?

  "https://dashboard.stripe.com/payments/#{payment.transaction_id}"
end

#handle_authorize_or_purchase(amount_in_cents, payment_source, gateway_options) ⇒ Object

the behavior for authorize and purchase is the same, so we can use the same method to handle both



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'app/models/spree_stripe/gateway.rb', line 66

def handle_authorize_or_purchase(amount_in_cents, payment_source, gateway_options)
  order_number, payment_number = gateway_options[:order_id].split('-')

  return failure('Order number is invalid') if order_number.blank?
  return failure('Payment number is invalid') if payment_number.blank?

  order = Spree::Order.where(store_id: stores.ids).find_by(number: order_number)
  payment = order.payments.find_by(number: payment_number)

  protect_from_error do
    # eg. payment created via admin
    payment = ensure_payment_intent_exists_for_payment(payment, amount_in_cents, payment_source)
    stripe_payment_intent = retrieve_payment_intent(payment.response_code)

    response = if payment_intent_accepted?(stripe_payment_intent)
                 # payment intent is already confirmed via Stripe JS SDK
                 stripe_payment_intent
               else
                 confirm_payment_intent(stripe_payment_intent.id)
               end

    success(response.id, response)
  end
end

#method_typeObject



353
354
355
# File 'app/models/spree_stripe/gateway.rb', line 353

def method_type
  'spree_stripe'
end

#payment_icon_nameObject



357
358
359
# File 'app/models/spree_stripe/gateway.rb', line 357

def payment_icon_name
  'stripe'
end

#payment_intent_accepted?(payment_intent) ⇒ Boolean

Returns:

  • (Boolean)


29
30
31
# File 'app/models/spree_stripe/gateway.rb', line 29

def payment_intent_accepted?(payment_intent)
  payment_intent.status.in?(payment_intent_accepted_statuses(payment_intent))
end

#payment_intent_bank_payment_method?(payment_intent) ⇒ Boolean

Returns:

  • (Boolean)


44
45
46
47
48
49
# File 'app/models/spree_stripe/gateway.rb', line 44

def payment_intent_bank_payment_method?(payment_intent)
  payment_method = payment_intent.payment_method
  return false unless payment_method.respond_to?(:type)

  payment_intent.payment_method.type.in?(BANK_PAYMENT_METHOD_TYPES)
end

#payment_intent_charge_not_required?(payment_intent) ⇒ Boolean

Returns:

  • (Boolean)


40
41
42
# File 'app/models/spree_stripe/gateway.rb', line 40

def payment_intent_charge_not_required?(payment_intent)
  payment_intent_bank_payment_method?(payment_intent)
end

#payment_intent_delayed_notification?(payment_intent) ⇒ Boolean

Returns:

  • (Boolean)


33
34
35
36
37
38
# File 'app/models/spree_stripe/gateway.rb', line 33

def payment_intent_delayed_notification?(payment_intent)
  payment_method = payment_intent.payment_method
  return false unless payment_method.respond_to?(:type)

  payment_intent.payment_method.type.in?(DELAYED_NOTIFICATION_PAYMENT_METHOD_TYPES)
end

#payment_profiles_supported?Boolean

Returns:

  • (Boolean)


345
346
347
# File 'app/models/spree_stripe/gateway.rb', line 345

def payment_profiles_supported?
  true
end

#provider_classObject



25
26
27
# File 'app/models/spree_stripe/gateway.rb', line 25

def provider_class
  self.class
end

#purchase(amount_in_cents, payment_source, gateway_options = {}) ⇒ Object

Parameters:

  • amount_in_cents (Integer)

    the amount in cents to capture

  • payment_source (Spree::CreditCard | Spree::PaymentSource)
  • gateway_options (Hash) (defaults to: {})

    this is an instance of Spree::Payment::GatewayOptions.to_hash



61
62
63
# File 'app/models/spree_stripe/gateway.rb', line 61

def purchase(amount_in_cents, payment_source, gateway_options = {})
  handle_authorize_or_purchase(amount_in_cents, payment_source, gateway_options)
end

#retrieve_charge(charge_id) ⇒ Object



284
285
286
# File 'app/models/spree_stripe/gateway.rb', line 284

def retrieve_charge(charge_id)
  send_request { Stripe::Charge.retrieve(charge_id) }
end

#retrieve_payment_intent(payment_intent_id) ⇒ Object



237
238
239
# File 'app/models/spree_stripe/gateway.rb', line 237

def retrieve_payment_intent(payment_intent_id)
  send_request { Stripe::PaymentIntent.retrieve({ id: payment_intent_id, expand: ['payment_method'] }) }
end

#send_request(&block) ⇒ Object



393
394
395
396
# File 'app/models/spree_stripe/gateway.rb', line 393

def send_request(&block)
  result, _response = client.request(&block)
  result
end

#update_customer(order: nil, user: nil) ⇒ Stripe::Customer

Updates a Stripe customer based on the order or user

Parameters:

  • order (Spree::Order) (defaults to: nil)

    the order to use for updating the Stripe customer

  • user (Spree::User) (defaults to: nil)

    the user to use for updating the Stripe customer

Returns:

  • (Stripe::Customer)

    the updated Stripe customer



180
181
182
183
184
185
186
187
188
189
# File 'app/models/spree_stripe/gateway.rb', line 180

def update_customer(order: nil, user: nil)
  user ||= order&.user
  return if user.blank?

  customer = gateway_customers.find_by(user: user)
  return if customer.blank?

  payload = build_customer_payload(order: order, user: user)
  send_request { Stripe::Customer.update(customer.profile_id, payload) }
end

#update_payment_intent(payment_intent_id, amount_in_cents, order, payment_method_id = nil) ⇒ ActiveMerchant::Billing::Response

Updates a Stripe payment intent for the order

Parameters:

  • payment_intent_id (String)

    Stripe payment intent id

  • amount_in_cents (Integer)

    the amount in cents

  • order (Spree::Order)

    the order to update the payment intent for

  • payment_method_id (String) (defaults to: nil)

    Stripe payment method id to use, eg. a card token

Returns:

  • (ActiveMerchant::Billing::Response)

    the response from the payment intent update



222
223
224
225
226
227
228
229
230
231
232
233
234
235
# File 'app/models/spree_stripe/gateway.rb', line 222

def update_payment_intent(payment_intent_id, amount_in_cents, order, payment_method_id = nil)
  protect_from_error do
    payload = SpreeStripe::PaymentIntentPresenter.new(
      amount: amount_in_cents,
      order: order,
      customer: fetch_or_create_customer(order: order)&.profile_id,
      payment_method_id: payment_method_id
    ).call.slice(:amount, :currency, :payment_method, :shipping, :customer)

    response = send_request { Stripe::PaymentIntent.update(payment_intent_id, payload) }

    success(response.id, response)
  end
end

#void(response_code, _source, _gateway_options) ⇒ Object



120
121
122
123
124
125
126
127
# File 'app/models/spree_stripe/gateway.rb', line 120

def void(response_code, _source, _gateway_options)
  return failure('Response code is blank') if response_code.blank?

  protect_from_error do
    response = cancel_payment_intent(response_code)
    success(response.id, response)
  end
end

#webhook_urlObject



21
22
23
# File 'app/models/spree_stripe/gateway.rb', line 21

def webhook_url
  StripeEvent::Engine.routes.url_helpers.root_url(host: stores.first.url, protocol: 'https')
end