pay 11.7.2 → 12.0.0

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -0
  3. data/app/controllers/pay/payments_controller.rb +5 -12
  4. data/app/controllers/pay/webhooks/base_controller.rb +50 -0
  5. data/app/controllers/pay/webhooks/braintree_controller.rb +11 -18
  6. data/app/controllers/pay/webhooks/lemon_squeezy_controller.rb +8 -26
  7. data/app/controllers/pay/webhooks/paddle_billing_controller.rb +13 -35
  8. data/app/controllers/pay/webhooks/paddle_classic_controller.rb +5 -20
  9. data/app/controllers/pay/webhooks/stripe_controller.rb +13 -29
  10. data/app/models/pay/braintree/charge.rb +6 -12
  11. data/app/models/pay/braintree/payment_method.rb +7 -5
  12. data/app/models/pay/braintree/subscription.rb +39 -44
  13. data/app/models/pay/charge.rb +1 -1
  14. data/app/models/pay/customer.rb +1 -9
  15. data/app/models/pay/lemon_squeezy/charge.rb +63 -61
  16. data/app/models/pay/lemon_squeezy/customer.rb +1 -1
  17. data/app/models/pay/lemon_squeezy/subscription.rb +43 -46
  18. data/app/models/pay/merchant.rb +1 -1
  19. data/app/models/pay/paddle_billing/charge.rb +43 -53
  20. data/app/models/pay/paddle_billing/customer.rb +1 -1
  21. data/app/models/pay/paddle_billing/payment_method.rb +19 -23
  22. data/app/models/pay/paddle_billing/subscription.rb +56 -67
  23. data/app/models/pay/paddle_classic/customer.rb +1 -1
  24. data/app/models/pay/paddle_classic/subscription.rb +42 -44
  25. data/app/models/pay/payment_method.rb +1 -5
  26. data/app/models/pay/stripe/charge.rb +54 -59
  27. data/app/models/pay/stripe/customer.rb +45 -23
  28. data/app/models/pay/stripe/merchant.rb +1 -1
  29. data/app/models/pay/stripe/payment_method.rb +27 -29
  30. data/app/models/pay/stripe/subscription.rb +128 -119
  31. data/app/models/pay/subscription.rb +2 -12
  32. data/app/views/layouts/pay/application.html.erb +0 -2
  33. data/app/views/pay/payments/show.html.erb +19 -8
  34. data/app/views/pay/user_mailer/payment_action_required.html.erb +1 -1
  35. data/app/views/pay/user_mailer/payment_action_required.text.erb +1 -1
  36. data/lib/pay/braintree/webhooks/subscription.rb +15 -0
  37. data/lib/pay/braintree/webhooks/subscription_canceled.rb +1 -7
  38. data/lib/pay/braintree/webhooks/subscription_expired.rb +1 -7
  39. data/lib/pay/braintree/webhooks/subscription_trial_ended.rb +1 -7
  40. data/lib/pay/braintree/webhooks/subscription_went_active.rb +2 -8
  41. data/lib/pay/braintree/webhooks/subscription_went_past_due.rb +2 -8
  42. data/lib/pay/braintree.rb +1 -2
  43. data/lib/pay/currency.rb +0 -4
  44. data/lib/pay/engine.rb +4 -0
  45. data/lib/pay/errors.rb +4 -0
  46. data/lib/pay/lemon_squeezy.rb +4 -7
  47. data/lib/pay/paddle_billing.rb +4 -1
  48. data/lib/pay/paddle_classic.rb +4 -1
  49. data/lib/pay/payment.rb +11 -17
  50. data/lib/pay/receipts.rb +0 -1
  51. data/lib/pay/stripe/webhooks/checkout_session_completed.rb +8 -4
  52. data/lib/pay/stripe/webhooks/invoice_updated.rb +4 -1
  53. data/lib/pay/stripe/webhooks/payment_action_required.rb +3 -2
  54. data/lib/pay/stripe/webhooks/subscription_created.rb +2 -4
  55. data/lib/pay/stripe/webhooks/subscription_deleted.rb +2 -5
  56. data/lib/pay/stripe.rb +3 -3
  57. data/lib/pay/sync.rb +29 -0
  58. data/lib/pay/version.rb +1 -1
  59. data/lib/pay.rb +7 -5
  60. metadata +5 -3
  61. data/lib/pay/braintree/webhooks/subscription_charged_unsuccessfully.rb +0 -24
@@ -1,52 +1,54 @@
1
1
  module Pay
2
2
  module PaddleClassic
3
3
  class Subscription < Pay::Subscription
4
+ extend Pay::Sync
5
+
4
6
  store_accessor :data, :paddle_update_url
5
7
  store_accessor :data, :paddle_cancel_url
6
8
 
7
9
  def self.sync(subscription_id, object: nil, name: Pay.default_product_name)
8
- # Passthrough is not return from this API, so we can't use that
9
- object ||= PaddleClassic.client.users.list(subscription_id: subscription_id).data.try(:first)
10
-
11
- pay_customer = Pay::Customer.find_by(processor: :paddle_classic, processor_id: object.user_id)
12
-
13
- # If passthrough exists (only on webhooks) we can use it to create the Pay::Customer
14
- if pay_customer.nil? && object.passthrough
15
- owner = Pay::PaddleClassic.owner_from_passthrough(object.passthrough)
16
- pay_customer = owner&.set_payment_processor(:paddle_classic, processor_id: object.user_id)
17
- end
10
+ sync_with_retries do
11
+ subscription = object || PaddleClassic.client.users.list(subscription_id: subscription_id).data.first
12
+ pay_customer = find_pay_customer(subscription.user_id)
13
+
14
+ # If passthrough exists (only on webhooks) we can use it to create the Pay::Customer
15
+ if pay_customer.nil? && subscription.passthrough
16
+ owner = Pay::PaddleClassic.owner_from_passthrough(subscription.passthrough)
17
+ pay_customer = owner&.set_payment_processor(:paddle_classic, processor_id: subscription.user_id)
18
+ end
18
19
 
19
- return unless pay_customer
20
-
21
- attributes = {
22
- paddle_cancel_url: object.cancel_url,
23
- paddle_update_url: object.update_url,
24
- processor_plan: object.plan_id || object.subscription_plan_id,
25
- quantity: object.quantity || 1,
26
- status: object.state || object.status
27
- }
28
-
29
- case attributes[:status]
30
- when "trialing"
31
- attributes[:trial_ends_at] = Time.zone.parse(object.next_bill_date)
32
- attributes[:ends_at] = nil
33
- when "active", "past_due"
34
- attributes[:trial_ends_at] = nil
35
- attributes[:ends_at] = nil
36
- when "paused", "deleted"
37
- # If paused or delete while on trial, set ends_at to match
38
- attributes[:trial_ends_at] = nil
39
- attributes[:ends_at] = Time.zone.parse(object.next_bill_date)
40
- end
20
+ return unless pay_customer
21
+
22
+ attributes = {
23
+ paddle_cancel_url: subscription.cancel_url,
24
+ paddle_update_url: subscription.update_url,
25
+ processor_plan: subscription.plan_id || subscription.subscription_plan_id,
26
+ quantity: subscription.quantity || 1,
27
+ status: subscription.state || subscription.status
28
+ }
29
+
30
+ case attributes[:status]
31
+ when "trialing"
32
+ attributes[:trial_ends_at] = Time.zone.parse(subscription.next_bill_date)
33
+ attributes[:ends_at] = nil
34
+ when "active", "past_due"
35
+ attributes[:trial_ends_at] = nil
36
+ attributes[:ends_at] = nil
37
+ when "paused", "deleted"
38
+ # If paused or delete while on trial, set ends_at to match
39
+ attributes[:trial_ends_at] = nil
40
+ attributes[:ends_at] = Time.zone.parse(subscription.next_bill_date)
41
+ end
41
42
 
42
- # Update or create the subscription
43
- if (pay_subscription = pay_customer.subscriptions.find_by(processor_id: object.subscription_id))
44
- pay_subscription.with_lock do
45
- pay_subscription.update!(attributes)
43
+ # Update or create the subscription
44
+ if (pay_subscription = pay_customer.subscriptions.find_by(processor_id: subscription.subscription_id))
45
+ pay_subscription.with_lock do
46
+ pay_subscription.update!(attributes)
47
+ end
48
+ pay_subscription
49
+ else
50
+ pay_customer.subscriptions.create!(attributes.merge(name: name, processor_id: subscription.subscription_id))
46
51
  end
47
- pay_subscription
48
- else
49
- pay_customer.subscriptions.create!(attributes.merge(name: name, processor_id: object.subscription_id))
50
52
  end
51
53
  end
52
54
 
@@ -93,7 +95,7 @@ module Pay
93
95
  end
94
96
 
95
97
  def change_quantity(quantity, **options)
96
- raise NotImplementedError, "Paddle does not support setting quantity on subscriptions"
98
+ raise Pay::NotSupportedError, "Paddle does not support setting quantity on subscriptions"
97
99
  end
98
100
 
99
101
  # A subscription could be set to cancel or pause in the future
@@ -139,10 +141,6 @@ module Pay
139
141
  rescue ::Paddle::Error => e
140
142
  raise Pay::PaddleClassic::Error, e
141
143
  end
142
-
143
- # Retries the latest invoice for a Past Due subscription
144
- def retry_failed_payment
145
- end
146
144
  end
147
145
  end
148
146
  end
@@ -12,15 +12,11 @@ module Pay
12
12
  store_accessor :data, :username
13
13
  store_accessor :data, :bank
14
14
 
15
- validates :processor_id, presence: true, uniqueness: {scope: :customer_id, case_sensitive: true}
15
+ validates :processor_id, presence: true
16
16
 
17
17
  def self.find_by_processor_and_id(processor, processor_id)
18
18
  joins(:customer).find_by(processor_id: processor_id, pay_customers: {processor: processor})
19
19
  end
20
-
21
- def self.pay_processor_for(name)
22
- "Pay::#{name.to_s.classify}::PaymentMethod".constantize
23
- end
24
20
  end
25
21
  end
26
22
 
@@ -1,6 +1,8 @@
1
1
  module Pay
2
2
  module Stripe
3
3
  class Charge < Pay::Charge
4
+ extend Pay::Sync
5
+
4
6
  EXPAND = ["balance_transaction", "payment_intent", "refunds.data.balance_transaction"]
5
7
 
6
8
  delegate :amount_captured, :payment_intent, to: :stripe_object, allow_nil: true
@@ -11,70 +13,59 @@ module Pay
11
13
  def self.sync_payment_intent(id, stripe_account: nil)
12
14
  payment_intent = ::Stripe::PaymentIntent.retrieve({id: id}, {stripe_account: stripe_account}.compact)
13
15
  sync(payment_intent.latest_charge, stripe_account: stripe_account)
16
+ rescue ::Stripe::StripeError => e
17
+ raise Pay::Stripe::Error, e
14
18
  end
15
19
 
16
- def self.sync(charge_id, object: nil, stripe_account: nil, try: 0, retries: 1)
17
- # Skip loading the latest charge details from the API if we already have it
18
- object ||= ::Stripe::Charge.retrieve({id: charge_id, expand: EXPAND}, {stripe_account: stripe_account}.compact)
19
- if object.customer.blank?
20
- Rails.logger.debug "Stripe Charge #{object.id} does not have a customer"
21
- return
22
- end
23
-
24
- pay_customer = Pay::Customer.find_by(processor: :stripe, processor_id: object.customer)
25
- if pay_customer.blank?
26
- Rails.logger.debug "Pay::Customer #{object.customer} is not in the database while syncing Stripe Charge #{object.id}"
27
- return
28
- end
29
-
30
- payment_method = object.payment_method_details.try(object.payment_method_details.type)
31
- attrs = {
32
- object: object.to_hash,
33
- amount: object.amount,
34
- amount_refunded: object.amount_refunded,
35
- application_fee_amount: object.application_fee_amount,
36
- bank: payment_method.try(:bank_name) || payment_method.try(:bank), # eps, fpx, ideal, p24, acss_debit, etc
37
- brand: payment_method.try(:brand)&.capitalize,
38
- created_at: Time.at(object.created),
39
- currency: object.currency,
40
- exp_month: payment_method.try(:exp_month).to_s,
41
- exp_year: payment_method.try(:exp_year).to_s,
42
- last4: payment_method.try(:last4).to_s,
43
- metadata: object.metadata,
44
- payment_method_type: object.payment_method_details.type,
45
- stripe_account: pay_customer.stripe_account,
46
- stripe_receipt_url: object.receipt_url
47
- }
48
-
49
- # Associate charge with subscription if we can
50
- if object.payment_intent.present?
51
- invoice_payments = ::Stripe::InvoicePayment.list({payment: {type: :payment_intent, payment_intent: object.payment_intent}, status: :paid}, {stripe_account: stripe_account}.compact)
52
- if invoice_payments.any?
53
- invoice = ::Stripe::Invoice.retrieve({id: invoice_payments.first.invoice, expand: ["total_discount_amounts.discount.source.coupon"]}, {stripe_account: stripe_account}.compact)
54
- attrs[:stripe_invoice] = invoice.to_hash
55
- attrs[:subtotal] = invoice.subtotal
56
- attrs[:tax] = invoice.total - invoice.total_excluding_tax.to_i
57
- if (subscription = invoice.parent.try(:subscription_details).try(:subscription))
58
- attrs[:subscription] = pay_customer.subscriptions.find_by(processor_id: subscription)
20
+ def self.sync(charge_id, object: nil, stripe_account: nil, retries: 1)
21
+ sync_with_retries(retries: retries) do
22
+ charge = object || ::Stripe::Charge.retrieve({id: charge_id, expand: EXPAND}, {stripe_account: stripe_account}.compact)
23
+ return unless (pay_customer = find_pay_customer(charge.customer))
24
+ stripe_account ||= pay_customer.stripe_account
25
+
26
+ payment_method = charge.payment_method_details.try(charge.payment_method_details.type)
27
+ attrs = {
28
+ object: charge.to_hash,
29
+ amount: charge.amount,
30
+ amount_refunded: charge.amount_refunded,
31
+ application_fee_amount: charge.application_fee_amount,
32
+ bank: payment_method.try(:bank_name) || payment_method.try(:bank), # eps, fpx, ideal, p24, acss_debit, etc
33
+ brand: payment_method.try(:brand)&.capitalize,
34
+ created_at: Time.at(charge.created),
35
+ currency: charge.currency,
36
+ exp_month: payment_method.try(:exp_month).to_s,
37
+ exp_year: payment_method.try(:exp_year).to_s,
38
+ last4: payment_method.try(:last4).to_s,
39
+ metadata: charge.metadata,
40
+ payment_method_type: charge.payment_method_details.type,
41
+ stripe_account: stripe_account,
42
+ stripe_receipt_url: charge.receipt_url
43
+ }
44
+
45
+ # Associate charge with subscription if we can
46
+ if charge.payment_intent.present?
47
+ invoice_payments = ::Stripe::InvoicePayment.list({payment: {type: :payment_intent, payment_intent: charge.payment_intent}, status: :paid}, {stripe_account: stripe_account}.compact)
48
+ if invoice_payments.any?
49
+ invoice = ::Stripe::Invoice.retrieve({id: invoice_payments.first.invoice, expand: ["total_discount_amounts.discount.source.coupon"]}, {stripe_account: stripe_account}.compact)
50
+ attrs[:stripe_invoice] = invoice.to_hash
51
+ attrs[:subtotal] = invoice.subtotal
52
+ attrs[:tax] = invoice.total - invoice.total_excluding_tax.to_i
53
+ if (subscription = invoice.parent.try(:subscription_details).try(:subscription))
54
+ attrs[:subscription] = pay_customer.subscriptions.find_by(processor_id: subscription)
55
+ end
59
56
  end
60
57
  end
61
- end
62
58
 
63
- # Update or create the charge
64
- if (pay_charge = find_by(customer: pay_customer, processor_id: object.id))
65
- pay_charge.with_lock { pay_charge.update!(attrs) }
66
- pay_charge
67
- else
68
- create!(attrs.merge(customer: pay_customer, processor_id: object.id))
69
- end
70
- rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique
71
- if try > retries
72
- raise
73
- else
74
- try += 1
75
- sleep 0.15**try
76
- retry
59
+ # Update or create the charge
60
+ if (pay_charge = find_by(customer: pay_customer, processor_id: charge.id))
61
+ pay_charge.with_lock { pay_charge.update!(attrs) }
62
+ pay_charge
63
+ else
64
+ create!(attrs.merge(customer: pay_customer, processor_id: charge.id))
65
+ end
77
66
  end
67
+ rescue ::Stripe::StripeError => e
68
+ raise Pay::Stripe::Error, e
78
69
  end
79
70
 
80
71
  def api_record
@@ -124,7 +115,7 @@ module Pay
124
115
  raise Pay::Stripe::Error, "no payment_intent on charge" unless payment_intent.present?
125
116
  payment_intent_id = payment_intent.is_a?(::Stripe::PaymentIntent) ? payment_intent.id : payment_intent
126
117
  ::Stripe::PaymentIntent.capture(payment_intent_id, options, stripe_options)
127
- self.class.sync(processor_id)
118
+ self.class.sync(processor_id, stripe_account: stripe_account)
128
119
  rescue ::Stripe::StripeError => e
129
120
  raise Pay::Stripe::Error, e
130
121
  end
@@ -144,6 +135,10 @@ module Pay
144
135
  ::Stripe::Charge.construct_from(object)
145
136
  end
146
137
 
138
+ def sync!(**options)
139
+ super(**options.with_defaults(stripe_account: stripe_account))
140
+ end
141
+
147
142
  private
148
143
 
149
144
  # Options for Stripe requests
@@ -38,8 +38,9 @@ module Pay
38
38
  end
39
39
 
40
40
  def update_api_record(**attributes)
41
- api_record unless processor_id?
42
- ::Stripe::Customer.update(processor_id, api_record_attributes.merge(attributes), stripe_options)
41
+ ::Stripe::Customer.update(stripe_customer_id, api_record_attributes.merge(attributes), stripe_options)
42
+ rescue ::Stripe::StripeError => e
43
+ raise Pay::Stripe::Error, e
43
44
  end
44
45
 
45
46
  # Charges an amount to the customer's default payment method
@@ -50,7 +51,7 @@ module Pay
50
51
  Pay::Payment.new(payment_intent).validate
51
52
 
52
53
  charge = payment_intent.latest_charge
53
- Pay::Stripe::Charge.sync(charge.id, object: charge)
54
+ Pay::Stripe::Charge.sync(charge.id, object: charge, stripe_account: stripe_account)
54
55
  rescue ::Stripe::StripeError => e
55
56
  raise Pay::Stripe::Error, e
56
57
  end
@@ -62,7 +63,7 @@ module Pay
62
63
  }.merge(options)
63
64
 
64
65
  # Load the Stripe customer to verify it exists and update payment method if needed
65
- opts[:customer] = processor_id || api_record.id
66
+ opts[:customer] = stripe_customer_id
66
67
 
67
68
  # Create subscription on Stripe
68
69
  stripe_sub = ::Stripe::Subscription.create(opts.merge(Pay::Stripe::Subscription.expand_options), stripe_options)
@@ -73,7 +74,7 @@ module Pay
73
74
  # No trial, payment method requires SCA
74
75
  if options[:payment_behavior].to_s != "default_incomplete" && subscription.incomplete?
75
76
  payment_intent_id = stripe_sub.latest_invoice.payments.first.payment.payment_intent
76
- Pay::Payment.from_id(payment_intent_id).validate
77
+ Pay::Payment.from_id(payment_intent_id, stripe_account: stripe_account).validate
77
78
  end
78
79
 
79
80
  subscription
@@ -82,11 +83,10 @@ module Pay
82
83
  end
83
84
 
84
85
  def add_payment_method(payment_method_id, default: false)
85
- api_record unless processor_id?
86
- payment_method = ::Stripe::PaymentMethod.attach(payment_method_id, {customer: processor_id}, stripe_options)
86
+ payment_method = ::Stripe::PaymentMethod.attach(payment_method_id, {customer: stripe_customer_id}, stripe_options)
87
87
 
88
88
  if default
89
- ::Stripe::Customer.update(processor_id, {
89
+ ::Stripe::Customer.update(stripe_customer_id, {
90
90
  invoice_settings: {
91
91
  default_payment_method: payment_method.id
92
92
  }
@@ -121,12 +121,14 @@ module Pay
121
121
  args = {
122
122
  amount: amount,
123
123
  currency: "usd",
124
- customer: processor_id || api_record.id,
124
+ customer: stripe_customer_id,
125
125
  expand: Pay::Stripe::Charge::EXPAND.map { |option| "latest_charge.#{option}" },
126
126
  return_url: root_url
127
127
  }.merge(options)
128
128
 
129
129
  ::Stripe::PaymentIntent.create(args, stripe_options)
130
+ rescue ::Stripe::StripeError => e
131
+ raise Pay::Stripe::Error, e
130
132
  end
131
133
 
132
134
  # Used for creating Stripe Terminal charges
@@ -135,15 +137,21 @@ module Pay
135
137
  end
136
138
 
137
139
  def create_setup_intent(options = {})
138
- ::Stripe::SetupIntent.create({customer: processor_id || api_record.id, usage: :off_session}.merge(options), stripe_options)
140
+ ::Stripe::SetupIntent.create({customer: stripe_customer_id, usage: :off_session}.merge(options), stripe_options)
141
+ rescue ::Stripe::StripeError => e
142
+ raise Pay::Stripe::Error, e
139
143
  end
140
144
 
141
145
  def invoice!(options = {})
142
- ::Stripe::Invoice.create(options.merge(customer: processor_id || api_record.id), stripe_options).pay
146
+ ::Stripe::Invoice.create(options.merge(customer: stripe_customer_id), stripe_options).pay
147
+ rescue ::Stripe::StripeError => e
148
+ raise Pay::Stripe::Error, e
143
149
  end
144
150
 
145
151
  def preview_invoice(**options)
146
- ::Stripe::Invoice.create_preview(options.merge(customer: processor_id || api_record.id), stripe_options)
152
+ ::Stripe::Invoice.create_preview(options.merge(customer: stripe_customer_id), stripe_options)
153
+ rescue ::Stripe::StripeError => e
154
+ raise Pay::Stripe::Error, e
147
155
  end
148
156
 
149
157
  # Syncs a customer's subscriptions from Stripe to the database.
@@ -151,7 +159,7 @@ module Pay
151
159
  def sync_subscriptions(**options)
152
160
  subscriptions = ::Stripe::Subscription.list(options.with_defaults(customer: processor_id), stripe_options)
153
161
  subscriptions.map do |subscription|
154
- Pay::Stripe::Subscription.sync(subscription.id)
162
+ Pay::Stripe::Subscription.sync(subscription.id, stripe_account: stripe_account)
155
163
  end
156
164
  rescue ::Stripe::StripeError => e
157
165
  raise Pay::Stripe::Error, e
@@ -168,9 +176,8 @@ module Pay
168
176
  # checkout(line_items: "price_12345", allow_promotion_codes: true)
169
177
  #
170
178
  def checkout(**options)
171
- api_record unless processor_id?
172
179
  args = {
173
- customer: processor_id,
180
+ customer: stripe_customer_id,
174
181
  mode: "payment"
175
182
  }
176
183
 
@@ -201,6 +208,8 @@ module Pay
201
208
  end
202
209
 
203
210
  ::Stripe::Checkout::Session.create(args.merge(options), stripe_options)
211
+ rescue ::Stripe::StripeError => e
212
+ raise Pay::Stripe::Error, e
204
213
  end
205
214
 
206
215
  # https://stripe.com/docs/api/checkout/sessions/create
@@ -208,7 +217,6 @@ module Pay
208
217
  # checkout_charge(amount: 15_00, name: "T-shirt", quantity: 2)
209
218
  #
210
219
  def checkout_charge(amount:, name:, quantity: 1, **options)
211
- api_record unless processor_id?
212
220
  currency = options.delete(:currency) || "usd"
213
221
  checkout(
214
222
  line_items: {
@@ -224,38 +232,52 @@ module Pay
224
232
  end
225
233
 
226
234
  def billing_portal(**options)
227
- api_record unless processor_id?
228
235
  args = {
229
- customer: processor_id,
236
+ customer: stripe_customer_id,
230
237
  return_url: options.delete(:return_url) || root_url
231
238
  }
232
239
  ::Stripe::BillingPortal::Session.create(args.merge(options), stripe_options)
240
+ rescue ::Stripe::StripeError => e
241
+ raise Pay::Stripe::Error, e
233
242
  end
234
243
 
235
244
  def customer_session(**options)
236
- api_record unless processor_id?
237
- args = {customer: processor_id}
245
+ args = {customer: stripe_customer_id}
238
246
  ::Stripe::CustomerSession.create(args.merge(options), stripe_options)
247
+ rescue ::Stripe::StripeError => e
248
+ raise Pay::Stripe::Error, e
239
249
  end
240
250
 
241
251
  def authorize(amount, options = {})
242
252
  charge(amount, options.merge(capture_method: :manual))
243
253
  end
244
254
 
255
+ # Attempts to pay all past_due subscription invoices to bring them back to active state
256
+ # Pass in `status: []` to include other subscription statuses
257
+ def retry_past_due_subscriptions!(status: [:past_due])
258
+ subscriptions.where(status: Array.wrap(status)).each(&:pay_open_invoices)
259
+ end
260
+
245
261
  # Creates a meter event to bill for usage
246
262
  #
247
263
  # create_meter_event(:api_request, value: 1)
248
264
  # create_meter_event(:api_request, token: 7)
249
265
  def create_meter_event(event_name, payload: {}, **options)
250
- api_record unless processor_id?
251
266
  ::Stripe::Billing::MeterEvent.create({
252
267
  event_name: event_name,
253
- payload: {stripe_customer_id: processor_id}.merge(payload)
254
- }.merge(options))
268
+ payload: {stripe_customer_id: stripe_customer_id}.merge(payload)
269
+ }.merge(options), stripe_options)
270
+ rescue ::Stripe::StripeError => e
271
+ raise Pay::Stripe::Error, e
255
272
  end
256
273
 
257
274
  private
258
275
 
276
+ # The Stripe::Customer ID, creating the customer on Stripe first if this record doesn't have one yet
277
+ def stripe_customer_id
278
+ processor_id || api_record.id
279
+ end
280
+
259
281
  # Options for Stripe requests
260
282
  def stripe_options
261
283
  {stripe_account: stripe_account}.compact
@@ -28,7 +28,7 @@ module Pay
28
28
 
29
29
  # A single-use login link for Express accounts to access their Stripe dashboard
30
30
  def login_link(**options)
31
- ::Stripe::Account.create_login_link(processor_id)
31
+ ::Stripe::Account.create_login_link(processor_id, options)
32
32
  rescue ::Stripe::StripeError => e
33
33
  raise Pay::Stripe::Error, e
34
34
  end
@@ -1,12 +1,16 @@
1
1
  module Pay
2
2
  module Stripe
3
3
  class PaymentMethod < Pay::PaymentMethod
4
+ extend Pay::Sync
5
+
4
6
  # Syncs a PaymentIntent's payment method to the database
5
7
  def self.sync_payment_intent(id, stripe_account: nil)
6
8
  payment_intent = ::Stripe::PaymentIntent.retrieve({id: id, expand: ["payment_method"]}, {stripe_account: stripe_account}.compact)
7
9
  payment_method = payment_intent.payment_method
8
10
  return unless payment_method
9
- Pay::Stripe::PaymentMethod.sync(payment_method.id, object: payment_method)
11
+ Pay::Stripe::PaymentMethod.sync(payment_method.id, object: payment_method, stripe_account: stripe_account)
12
+ rescue ::Stripe::StripeError => e
13
+ raise Pay::Stripe::Error, e
10
14
  end
11
15
 
12
16
  # Syncs a SetupIntent's payment method to the database
@@ -14,40 +18,30 @@ module Pay
14
18
  setup_intent = ::Stripe::SetupIntent.retrieve({id: id, expand: ["payment_method"]}, {stripe_account: stripe_account}.compact)
15
19
  payment_method = setup_intent.payment_method
16
20
  return unless payment_method
17
- Pay::Stripe::PaymentMethod.sync(payment_method.id, object: payment_method)
21
+ Pay::Stripe::PaymentMethod.sync(payment_method.id, object: payment_method, stripe_account: stripe_account)
22
+ rescue ::Stripe::StripeError => e
23
+ raise Pay::Stripe::Error, e
18
24
  end
19
25
 
20
26
  # Syncs PaymentMethod objects from Stripe
21
- def self.sync(id, object: nil, stripe_account: nil, try: 0, retries: 1)
22
- object ||= ::Stripe::PaymentMethod.retrieve(id, {stripe_account: stripe_account}.compact)
23
- if object.customer.blank?
24
- Rails.logger.debug "Stripe PaymentMethod #{object.id} does not have a customer"
25
- return
26
- end
27
-
28
- pay_customer = Pay::Customer.find_by(processor: :stripe, processor_id: object.customer)
29
- if pay_customer.blank?
30
- Rails.logger.debug "Pay::Customer #{object.customer} is not in the database while syncing Stripe PaymentMethod #{object.id}"
31
- return
32
- end
27
+ def self.sync(id, object: nil, stripe_account: nil, retries: 1)
28
+ sync_with_retries(retries: retries) do
29
+ payment_method = object || ::Stripe::PaymentMethod.retrieve(id, {stripe_account: stripe_account}.compact)
30
+ return unless (pay_customer = find_pay_customer(payment_method.customer))
31
+ stripe_account ||= pay_customer.stripe_account
33
32
 
34
- default_payment_method_id = pay_customer.api_record.invoice_settings&.default_payment_method
35
- default = (id == default_payment_method_id)
33
+ default_payment_method_id = pay_customer.api_record.invoice_settings&.default_payment_method
34
+ default = (id == default_payment_method_id)
36
35
 
37
- attributes = extract_attributes(object).merge(default: default, stripe_account: stripe_account)
36
+ attributes = extract_attributes(payment_method).merge(default: default, stripe_account: stripe_account)
38
37
 
39
- where(customer: pay_customer).update_all(default: false) if default
40
- pay_payment_method = where(customer: pay_customer, processor_id: object.id).first_or_initialize
41
- pay_payment_method.update!(attributes)
42
- pay_payment_method
43
- rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique
44
- if try > retries
45
- raise
46
- else
47
- try += 1
48
- sleep 0.15**try
49
- retry
38
+ where(customer: pay_customer).update_all(default: false) if default
39
+ pay_payment_method = where(customer: pay_customer, processor_id: payment_method.id).first_or_initialize
40
+ pay_payment_method.update!(attributes)
41
+ pay_payment_method
50
42
  end
43
+ rescue ::Stripe::StripeError => e
44
+ raise Pay::Stripe::Error, e
51
45
  end
52
46
 
53
47
  # Extracts payment method details from a Stripe::PaymentMethod object
@@ -73,11 +67,15 @@ module Pay
73
67
 
74
68
  customer.payment_methods.update_all(default: false)
75
69
  update!(default: true)
70
+ rescue ::Stripe::StripeError => e
71
+ raise Pay::Stripe::Error, e
76
72
  end
77
73
 
78
74
  # Remove payment method
79
75
  def detach
80
- ::Stripe::PaymentMethod.detach(processor_id, stripe_options)
76
+ ::Stripe::PaymentMethod.detach(processor_id, {}, stripe_options)
77
+ rescue ::Stripe::StripeError => e
78
+ raise Pay::Stripe::Error, e
81
79
  end
82
80
 
83
81
  private