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,127 +1,116 @@
1
1
  module Pay
2
2
  module Stripe
3
3
  class Subscription < Pay::Subscription
4
+ extend Pay::Sync
5
+
4
6
  attr_writer :api_record
5
7
 
6
8
  def self.sync_from_checkout_session(session_id, stripe_account: nil)
7
- checkout_session = ::Stripe::Checkout::Session.retrieve({id: session_id}, {stripe_account: stripe_account}.compact)
8
- sync(checkout_session.subscription)
9
+ Pay.deprecator.warn "Pay::Stripe::Subscription.sync_from_checkout_session is deprecated, use Pay::Stripe.sync_checkout_session instead"
10
+ Pay::Stripe.sync_checkout_session(session_id, stripe_account: stripe_account)
9
11
  end
10
12
 
11
- def self.sync(subscription_id, object: nil, name: nil, stripe_account: nil, try: 0, retries: 1)
12
- # Skip loading the latest subscription details from the API if we already have it
13
- object ||= ::Stripe::Subscription.retrieve({id: subscription_id}.merge(expand_options), {stripe_account: stripe_account}.compact)
14
- if object.customer.blank?
15
- Rails.logger.debug "Stripe Subscription #{object.id} does not have a customer"
16
- return
17
- end
18
-
19
- pay_customer = Pay::Customer.find_by(processor: :stripe, processor_id: object.customer)
20
- if pay_customer.blank?
21
- Rails.logger.debug "Pay::Customer #{object.customer} is not in the database while syncing Stripe Subscription #{object.id}"
22
- return
23
- end
24
-
25
- attributes = {
26
- object: object.to_hash,
27
- application_fee_percent: object.application_fee_percent,
28
- created_at: Time.at(object.created),
29
- processor_plan: object.items.first.price.id,
30
- quantity: object.items.first.try(:quantity) || 0,
31
- status: object.status,
32
- stripe_account: pay_customer.stripe_account,
33
- metadata: object.metadata,
34
- metered: false,
35
- pause_behavior: object.pause_collection&.behavior,
36
- pause_resumes_at: (object.pause_collection&.resumes_at ? Time.at(object.pause_collection&.resumes_at) : nil),
37
- current_period_start: (object.items.first.current_period_start ? Time.at(object.items.first.current_period_start) : nil),
38
- current_period_end: (object.items.first.current_period_end ? Time.at(object.items.first.current_period_end) : nil)
39
- }
40
-
41
- # Subscriptions that have ended should have their trial ended at the
42
- # same time if they were still on trial (if you cancel a
43
- # subscription, your are cancelling your trial as well at the same
44
- # instant). This avoids canceled subscriptions responding `true`
45
- # to #on_trial? due to the `trial_ends_at` being left set in the
46
- # future.
47
- if object.trial_end
48
- trial_ended_at = [object.ended_at, object.trial_end].compact.min
49
- attributes[:trial_ends_at] = Time.at(trial_ended_at)
50
- else
51
- attributes[:trial_ends_at] = nil
52
- end
53
-
54
- object.items.auto_paging_each do |subscription_item|
55
- next if attributes[:metered]
56
- attributes[:metered] = true if subscription_item.price.try(:recurring).try(:usage_type) == "metered"
57
- end
13
+ def self.sync(subscription_id, object: nil, name: nil, stripe_account: nil, retries: 1)
14
+ sync_with_retries(retries: retries) do
15
+ subscription = object || ::Stripe::Subscription.retrieve({id: subscription_id}.merge(expand_options), {stripe_account: stripe_account}.compact)
16
+ return unless (pay_customer = find_pay_customer(subscription.customer))
17
+ stripe_account ||= pay_customer.stripe_account
18
+
19
+ attributes = {
20
+ object: subscription.to_hash,
21
+ application_fee_percent: subscription.application_fee_percent,
22
+ created_at: Time.at(subscription.created),
23
+ processor_plan: subscription.items.first.price.id,
24
+ quantity: subscription.items.first.try(:quantity) || 0,
25
+ status: subscription.status,
26
+ stripe_account: stripe_account,
27
+ metadata: subscription.metadata,
28
+ metered: false,
29
+ pause_behavior: subscription.pause_collection&.behavior,
30
+ pause_resumes_at: (subscription.pause_collection&.resumes_at ? Time.at(subscription.pause_collection&.resumes_at) : nil),
31
+ current_period_start: (subscription.items.first.current_period_start ? Time.at(subscription.items.first.current_period_start) : nil),
32
+ current_period_end: (subscription.items.first.current_period_end ? Time.at(subscription.items.first.current_period_end) : nil)
33
+ }
34
+
35
+ # Subscriptions that have ended should have their trial ended at the
36
+ # same time if they were still on trial (if you cancel a
37
+ # subscription, your are cancelling your trial as well at the same
38
+ # instant). This avoids canceled subscriptions responding `true`
39
+ # to #on_trial? due to the `trial_ends_at` being left set in the
40
+ # future.
41
+ if subscription.trial_end
42
+ trial_ended_at = [subscription.ended_at, subscription.trial_end].compact.min
43
+ attributes[:trial_ends_at] = Time.at(trial_ended_at)
44
+ else
45
+ attributes[:trial_ends_at] = nil
46
+ end
58
47
 
59
- attributes[:ends_at] = if object.ended_at
60
- # Fully cancelled subscription
61
- Time.at(object.ended_at)
62
- elsif object.cancel_at
63
- # subscription cancelling in the future
64
- Time.at(object.cancel_at)
65
- elsif object.cancel_at_period_end
66
- # Subscriptions cancelling in the future
67
- Time.at(object.items.first.current_period_end)
68
- end
48
+ subscription.items.auto_paging_each do |subscription_item|
49
+ next if attributes[:metered]
50
+ attributes[:metered] = true if subscription_item.price.try(:recurring).try(:usage_type) == "metered"
51
+ end
69
52
 
70
- # Sync payment method if directly attached to subscription
71
- if object.default_payment_method
72
- if object.default_payment_method.is_a? String
73
- Pay::Stripe::PaymentMethod.sync(object.default_payment_method)
74
- attributes[:payment_method_id] = object.default_payment_method
75
- else
76
- Pay::Stripe::PaymentMethod.sync(object.default_payment_method.id, object: object.default_payment_method)
77
- attributes[:payment_method_id] = object.default_payment_method.id
53
+ attributes[:ends_at] = if subscription.ended_at
54
+ # Fully cancelled subscription
55
+ Time.at(subscription.ended_at)
56
+ elsif subscription.cancel_at
57
+ # subscription cancelling in the future
58
+ Time.at(subscription.cancel_at)
59
+ elsif subscription.cancel_at_period_end
60
+ # Subscriptions cancelling in the future
61
+ Time.at(subscription.items.first.current_period_end)
78
62
  end
79
- end
80
63
 
81
- # Update or create the subscription
82
- pay_subscription = find_by(customer: pay_customer, processor_id: object.id)
83
- if pay_subscription
84
- # If pause behavior is changing to `void`, record the pause start date
85
- # Any other pause status (or no pause at all) should have nil for start
86
- if pay_subscription.pause_behavior != attributes[:pause_behavior]
87
- attributes[:pause_starts_at] = if attributes[:pause_behavior] == "void"
88
- Time.at(object.items.first.current_period_end)
64
+ # Sync payment method if directly attached to subscription
65
+ if subscription.default_payment_method
66
+ if subscription.default_payment_method.is_a? String
67
+ Pay::Stripe::PaymentMethod.sync(subscription.default_payment_method, stripe_account: stripe_account)
68
+ attributes[:payment_method_id] = subscription.default_payment_method
69
+ else
70
+ Pay::Stripe::PaymentMethod.sync(subscription.default_payment_method.id, object: subscription.default_payment_method, stripe_account: stripe_account)
71
+ attributes[:payment_method_id] = subscription.default_payment_method.id
89
72
  end
90
73
  end
91
74
 
92
- pay_subscription.with_lock { pay_subscription.update!(attributes) }
93
- else
94
- # Allow setting the subscription name in metadata, otherwise use the default
95
- name ||= object.metadata["pay_name"] || Pay.default_product_name
96
- pay_subscription = create!(attributes.merge(customer: pay_customer, name: name, processor_id: object.id))
97
- end
75
+ # Update or create the subscription
76
+ pay_subscription = find_by(customer: pay_customer, processor_id: subscription.id)
77
+ if pay_subscription
78
+ # If pause behavior is changing to `void`, record the pause start date
79
+ # Any other pause status (or no pause at all) should have nil for start
80
+ if pay_subscription.pause_behavior != attributes[:pause_behavior]
81
+ attributes[:pause_starts_at] = if attributes[:pause_behavior] == "void"
82
+ Time.at(subscription.items.first.current_period_end)
83
+ end
84
+ end
85
+
86
+ pay_subscription.with_lock { pay_subscription.update!(attributes) }
87
+ else
88
+ # Allow setting the subscription name in metadata, otherwise use the default
89
+ name ||= subscription.metadata["pay_name"] || Pay.default_product_name
90
+ pay_subscription = create!(attributes.merge(customer: pay_customer, name: name, processor_id: subscription.id))
91
+ end
98
92
 
99
- # Cache the Stripe subscription on the Pay::Subscription that we return
100
- pay_subscription.api_record = object
93
+ # Cache the Stripe subscription on the Pay::Subscription that we return
94
+ pay_subscription.api_record = subscription
101
95
 
102
- # Sync the latest charge if we already have it loaded (like during subscrbe), otherwise, let webhooks take care of creating it
103
- if (invoice = object.try(:latest_invoice))
104
- Array(invoice.try(:payments)).each do |invoice_payment|
105
- next unless invoice_payment.status == "paid"
96
+ # Sync the latest charge if we already have it loaded (like during subscrbe), otherwise, let webhooks take care of creating it
97
+ if (invoice = subscription.try(:latest_invoice))
98
+ Array(invoice.try(:payments)).each do |invoice_payment|
99
+ next unless invoice_payment.status == "paid"
106
100
 
107
- case invoice_payment.payment.type
108
- when "payment_intent"
109
- Pay::Stripe::Charge.sync_payment_intent(invoice_payment.payment.payment_intent, stripe_account: pay_subscription.stripe_account)
110
- when "charge"
111
- Pay::Stripe::Charge.sync(invoice_payment.payment.charge, stripe_account: pay_subscription.stripe_account)
101
+ case invoice_payment.payment.type
102
+ when "payment_intent"
103
+ Pay::Stripe::Charge.sync_payment_intent(invoice_payment.payment.payment_intent, stripe_account: stripe_account)
104
+ when "charge"
105
+ Pay::Stripe::Charge.sync(invoice_payment.payment.charge, stripe_account: stripe_account)
106
+ end
112
107
  end
113
108
  end
114
- end
115
109
 
116
- pay_subscription
117
- rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique
118
- try += 1
119
- if try <= retries
120
- sleep 0.1
121
- retry
122
- else
123
- raise
110
+ pay_subscription
124
111
  end
112
+ rescue ::Stripe::StripeError => e
113
+ raise Pay::Stripe::Error, e
125
114
  end
126
115
 
127
116
  # Common expand options for all requests that create, retrieve, or update a Stripe Subscription
@@ -144,8 +133,14 @@ module Pay
144
133
  ::Stripe::Subscription.construct_from(object)
145
134
  end
146
135
 
136
+ def sync!(**options)
137
+ super(**options.with_defaults(stripe_account: stripe_account))
138
+ end
139
+
147
140
  def api_record(**options)
148
141
  @api_record ||= ::Stripe::Subscription.retrieve(options.with_defaults(id: processor_id).merge(expand_options), {stripe_account: stripe_account}.compact)
142
+ rescue ::Stripe::StripeError => e
143
+ raise Pay::Stripe::Error, e
149
144
  end
150
145
 
151
146
  # Returns a SetupIntent or PaymentIntent client secret for the subscription
@@ -198,16 +193,12 @@ module Pay
198
193
 
199
194
  # This updates a SubscriptionItem's quantity in Stripe
200
195
  #
201
- # For a subscription with a single item, we can update the subscription directly if no SubscriptionItem ID is available
202
- # Otherwise a SubscriptionItem ID is required so Stripe knows which entry to update
196
+ # Defaults to the first SubscriptionItem. Pass subscription_item_id: to update a different one
197
+ # (Stripe no longer accepts a top-level quantity on the subscription itself)
203
198
  def change_quantity(quantity, **options)
204
- subscription_item_id = options.delete(:subscription_item_id) || subscription_items&.first&.id
205
- if subscription_item_id
206
- ::Stripe::SubscriptionItem.update(subscription_item_id, options.merge(quantity: quantity), stripe_options)
207
- @api_record = nil
208
- else
209
- @api_record = ::Stripe::Subscription.update(processor_id, options.merge(quantity: quantity).merge(expand_options), stripe_options)
210
- end
199
+ subscription_item_id = options.delete(:subscription_item_id) || subscription_items.first.id
200
+ ::Stripe::SubscriptionItem.update(subscription_item_id, options.merge(quantity: quantity), stripe_options)
201
+ @api_record = nil
211
202
  update(quantity: quantity)
212
203
  rescue ::Stripe::StripeError => e
213
204
  raise Pay::Stripe::Error, e
@@ -253,6 +244,8 @@ module Pay
253
244
  pause_resumes_at: (@api_record.pause_collection&.resumes_at ? Time.at(@api_record.pause_collection&.resumes_at) : nil),
254
245
  pause_starts_at: ((behavior == "void") ? Time.at(@api_record.items.first.current_period_end) : nil)
255
246
  )
247
+ rescue ::Stripe::StripeError => e
248
+ raise Pay::Stripe::Error, e
256
249
  end
257
250
 
258
251
  # Unpauses a subscription
@@ -265,6 +258,8 @@ module Pay
265
258
  pause_resumes_at: nil,
266
259
  pause_starts_at: nil
267
260
  )
261
+ rescue ::Stripe::StripeError => e
262
+ raise Pay::Stripe::Error, e
268
263
  end
269
264
 
270
265
  def resumable?
@@ -285,7 +280,7 @@ module Pay
285
280
  }.merge(expand_options),
286
281
  stripe_options)
287
282
  end
288
- update(ends_at: nil, status: :active)
283
+ update(ends_at: nil, status: @api_record.status)
289
284
  rescue ::Stripe::StripeError => e
290
285
  raise Pay::Stripe::Error, e
291
286
  end
@@ -293,7 +288,7 @@ module Pay
293
288
  def swap(plan, **options)
294
289
  raise ArgumentError, "plan must be a string" unless plan.is_a?(String)
295
290
 
296
- prorate = options.fetch(:prorate) { true }
291
+ prorate = options.delete(:prorate) { true }
297
292
  proration_behavior = options.delete(:proration_behavior) || (prorate ? "always_invoice" : "none")
298
293
 
299
294
  @api_record = ::Stripe::Subscription.update(
@@ -309,7 +304,7 @@ module Pay
309
304
 
310
305
  # Validate that swap was successful and handle SCA if needed
311
306
  if (payment_intent_id = @api_record.latest_invoice.payments.first&.payment&.payment_intent)
312
- Pay::Payment.from_id(payment_intent_id).validate
307
+ Pay::Payment.from_id(payment_intent_id, stripe_account: stripe_account).validate
313
308
  end
314
309
 
315
310
  sync!(object: @api_record)
@@ -330,6 +325,8 @@ module Pay
330
325
 
331
326
  def preview_invoice(**options)
332
327
  ::Stripe::Invoice.create_preview(options.merge(subscription: processor_id), stripe_options)
328
+ rescue ::Stripe::StripeError => e
329
+ raise Pay::Stripe::Error, e
333
330
  end
334
331
 
335
332
  # Retries the latest invoice for a Past Due subscription and attempts to pay it
@@ -338,9 +335,12 @@ module Pay
338
335
  payment_intent = ::Stripe::PaymentIntent.retrieve({id: payment_intent_id}, stripe_options)
339
336
 
340
337
  payment_intent = if payment_intent.status == "requires_payment_method"
341
- ::Stripe::PaymentIntent.confirm(payment_intent_id, {payment_method: customer.default_payment_method.processor_id}, stripe_options)
338
+ payment_method = customer.default_payment_method
339
+ raise Pay::Stripe::Error, "no default payment method to retry the payment with" if payment_method.nil?
340
+
341
+ ::Stripe::PaymentIntent.confirm(payment_intent_id, {payment_method: payment_method.processor_id}, stripe_options)
342
342
  else
343
- ::Stripe::PaymentIntent.confirm(payment_intent_id, stripe_options)
343
+ ::Stripe::PaymentIntent.confirm(payment_intent_id, {}, stripe_options)
344
344
  end
345
345
  Pay::Payment.new(payment_intent).validate
346
346
  rescue ::Stripe::StripeError => e
@@ -348,14 +348,23 @@ module Pay
348
348
  end
349
349
 
350
350
  # Looks up open invoices for a subscription and attempts to pay them
351
+ #
352
+ # Invoices no longer expose a `payment_intent` directly, so the PaymentIntent is looked up through the invoice's payments
351
353
  def pay_open_invoices
352
- ::Stripe::Invoice.list({subscription: processor_id, status: :open}, stripe_options).auto_paging_each do |invoice|
353
- retry_failed_payment(payment_intent_id: invoice.payment_intent)
354
+ ::Stripe::Invoice.list({subscription: processor_id, status: :open, expand: ["data.payments"]}, stripe_options).auto_paging_each do |invoice|
355
+ payment_intent_id = invoice.payments.first&.payment&.payment_intent
356
+ retry_failed_payment(payment_intent_id: payment_intent_id) if payment_intent_id
354
357
  end
358
+ rescue ::Stripe::StripeError => e
359
+ raise Pay::Stripe::Error, e
355
360
  end
356
361
 
362
+ # Returns the Stripe::PaymentIntent for the latest invoice, if there is one
357
363
  def latest_payment
358
- api_record(expand: ["latest_invoice.payment_intent"]).latest_invoice.payment_intent
364
+ payment_intent_id = api_record.latest_invoice&.payments&.first&.payment&.payment_intent
365
+ ::Stripe::PaymentIntent.retrieve({id: payment_intent_id}, stripe_options) if payment_intent_id
366
+ rescue ::Stripe::StripeError => e
367
+ raise Pay::Stripe::Error, e
359
368
  end
360
369
 
361
370
  private
@@ -1,7 +1,5 @@
1
1
  module Pay
2
2
  class Subscription < Pay::ApplicationRecord
3
- STATUSES = %w[incomplete incomplete_expired trialing active past_due canceled unpaid paused]
4
-
5
3
  # Associations
6
4
  belongs_to :customer, touch: true
7
5
  belongs_to :payment_method, optional: true, primary_key: :processor_id
@@ -9,7 +7,7 @@ module Pay
9
7
 
10
8
  # Scopes
11
9
  scope :for_name, ->(name) { where(name: name) }
12
- scope :on_trial, -> { where(status: ["on_trial", "trialing", "active"]).where("trial_ends_at > ?", Time.current) }
10
+ scope :on_trial, -> { where(status: ["trialing", "active"]).where("trial_ends_at > ?", Time.current) }
13
11
  scope :canceled, -> { where.not(ends_at: nil) }
14
12
  scope :cancelled, -> { canceled }
15
13
  scope :on_grace_period, -> { where("#{table_name}.ends_at IS NOT NULL AND #{table_name}.ends_at > ?", Time.current) }
@@ -29,7 +27,7 @@ module Pay
29
27
 
30
28
  # Validations
31
29
  validates :name, presence: true
32
- validates :processor_id, presence: true, uniqueness: {scope: :customer_id, case_sensitive: true}
30
+ validates :processor_id, presence: true
33
31
  validates :processor_plan, presence: true
34
32
  validates :quantity, presence: true, numericality: {only_integer: true, greater_than_or_equal_to: 0}
35
33
  validates :status, presence: true
@@ -54,18 +52,10 @@ module Pay
54
52
  reload
55
53
  end
56
54
 
57
- def skip_trial
58
- self.trial_ends_at = nil
59
- end
60
-
61
55
  def generic_trial?
62
56
  fake_processor? && trial_ends_at?
63
57
  end
64
58
 
65
- def has_trial?
66
- trial_ends_at?
67
- end
68
-
69
59
  # Does not include the last second of the trial
70
60
  def on_trial?
71
61
  return false if ended?
@@ -4,8 +4,6 @@
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
6
  <title>Payment Confirmation - <%= Pay.business_name %></title>
7
- <%#= stylesheet_link_tag "pay/application", media: "all" %>
8
- <%#= javascript_include_tag "pay/application" %>
9
7
  <%= csrf_meta_tags %>
10
8
 
11
9
  <link href="https://unpkg.com/tailwindcss@^2.2/dist/tailwind.min.css" rel="stylesheet">
@@ -1,8 +1,12 @@
1
1
  <div
2
2
  data-controller="payment-intent"
3
+ data-payment-intent-public-key-value="<%= Pay::Stripe.public_key %>"
4
+ data-payment-intent-stripe-account-value="<%= @payment.stripe_account %>"
3
5
  data-payment-intent-client-secret-value="<%= @payment.client_secret %>"
4
6
  data-payment-intent-status-value="<%= @payment.status %>"
5
7
  data-payment-intent-customer-value="<%= @payment.customer %>"
8
+ data-payment-intent-name-missing-message-value="<%= t("pay.requires_action.name_missing") %>"
9
+ data-payment-intent-confirmed-message-value="<%= t("pay.requires_action.success") %>"
6
10
  class="h-screen md:flex md:justify-center md:items-center">
7
11
  <div class="w-full max-w-lg">
8
12
  <!-- Status Messages -->
@@ -54,7 +58,7 @@
54
58
  </div>
55
59
  <% end %>
56
60
 
57
- <%= sanitize link_to(t("pay.back"), @redirect_to, class: "inline-block w-full px-4 py-3 bg-gray-100 hover:bg-gray-200 text-center text-gray-600 rounded-lg") %>
61
+ <%= link_to t("pay.back"), @redirect_to, class: "inline-block w-full px-4 py-3 bg-gray-100 hover:bg-gray-200 text-center text-gray-600 rounded-lg" %>
58
62
  </div>
59
63
 
60
64
  <p class="text-center text-gray-500 text-sm">
@@ -64,26 +68,33 @@
64
68
  </div>
65
69
 
66
70
  <script type="module">
67
- window.stripe = Stripe('<%= Pay::Stripe.public_key %>')
68
-
69
71
  import { Application, Controller } from 'https://unpkg.com/@hotwired/stimulus/dist/stimulus.js'
70
72
  const application = Application.start()
71
73
 
72
74
  application.register('payment-intent', class extends Controller {
73
75
  static targets = ["error", "errorMessage", "success", "successMessage", "form", "button", "name", "card", "cardFields"]
74
76
  static values = {
77
+ publicKey: String,
78
+ stripeAccount: String,
75
79
  clientSecret: String,
76
80
  customer: String,
77
81
  status: String,
82
+ nameMissingMessage: String,
83
+ confirmedMessage: String,
78
84
  errorMessage: String,
79
85
  successMessage: String,
80
86
  complete: Boolean,
81
87
  processing: Boolean
82
88
  }
83
89
 
90
+ // Value callbacks run before connect(), so Stripe.js must be ready here
91
+ initialize() {
92
+ this.stripe = Stripe(this.publicKeyValue, this.stripeAccountValue ? {stripeAccount: this.stripeAccountValue} : {})
93
+ }
94
+
84
95
  connect() {
85
96
  if (this.hasCardTarget) {
86
- this.elements = stripe.elements({clientSecret: this.clientSecretValue})
97
+ this.elements = this.stripe.elements({clientSecret: this.clientSecretValue})
87
98
  this.payment = this.elements.create('payment')
88
99
  this.payment.mount(this.cardTarget)
89
100
  }
@@ -93,7 +104,7 @@
93
104
  switch(this.statusValue) {
94
105
  case "requires_action":
95
106
  this.processingValue = true
96
- stripe.confirmPayment({clientSecret: this.clientSecretValue, confirmParams: { return_url: document.location.href }}).then(this.handleConfirmResult.bind(this))
107
+ this.stripe.confirmPayment({clientSecret: this.clientSecretValue, confirmParams: { return_url: document.location.href }}).then(this.handleConfirmResult.bind(this))
97
108
  break
98
109
  case "requires_payment_method":
99
110
  this.cardFieldsTarget.classList.toggle("hidden", false)
@@ -103,7 +114,7 @@
103
114
 
104
115
  confirmPayment() {
105
116
  this.processingValue = true
106
- stripe.confirmPayment({elements: this.elements, confirmParams: { return_url: document.location.href }}).then(this.handleConfirmResult.bind(this))
117
+ this.stripe.confirmPayment({elements: this.elements, confirmParams: { return_url: document.location.href }}).then(this.handleConfirmResult.bind(this))
107
118
  }
108
119
 
109
120
  handleConfirmResult(result) {
@@ -112,14 +123,14 @@
112
123
  if (result.error) {
113
124
  if (result.error.code === 'parameter_invalid_empty' &&
114
125
  result.error.param === 'payment_method_data[billing_details][name]') {
115
- this.errorMessageValue = '<%=t "pay.requires_action.name_missing" %>'
126
+ this.errorMessageValue = this.nameMissingMessageValue
116
127
  } else {
117
128
  this.errorMessageValue = result.error.message
118
129
  this.statusValue = result.error.payment_intent.status
119
130
  }
120
131
  } else {
121
132
  this.completeValue = true
122
- this.successMessageValue = '<%=t "pay.requires_action.success" %>'
133
+ this.successMessageValue = this.confirmedMessageValue
123
134
  }
124
135
  }
125
136
 
@@ -1,5 +1,5 @@
1
1
  <h3>Extra confirmation is needed to process your payment</h3>
2
2
  <p>Your <%= Pay.application_name %> subscription requires confirmation to process your payment to continue access.</p>
3
- <p><%= link_to "Confirm your payment", pay.payment_url(params[:payment_intent_id]) %></p>
3
+ <p><%= link_to "Confirm your payment", pay.payment_url(params[:payment_intent_id], stripe_account: params[:stripe_account]) %></p>
4
4
  <p>If you have any questions, please hit reply and let us know.</p>
5
5
  <p>— The <%= Pay.application_name %> Team</p>
@@ -2,7 +2,7 @@ Extra confirmation is needed to process your payment
2
2
 
3
3
  Your <%= Pay.application_name %> subscription requires confirmation to process your payment to continue access.
4
4
 
5
- Confirm your payment: <%= pay.payment_url(params[:payment_intent_id]) %>
5
+ Confirm your payment: <%= pay.payment_url(params[:payment_intent_id], stripe_account: params[:stripe_account]) %>
6
6
 
7
7
  If you have any questions, please hit reply and let us know.
8
8
 
@@ -0,0 +1,15 @@
1
+ module Pay
2
+ module Braintree
3
+ module Webhooks
4
+ # Every Braintree subscription event is handled the same way: sync the subscription
5
+ class Subscription
6
+ def call(event)
7
+ subscription = event.subscription
8
+ return if subscription.nil?
9
+
10
+ Pay::Braintree::Subscription.sync(subscription.id)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -3,13 +3,7 @@
3
3
  module Pay
4
4
  module Braintree
5
5
  module Webhooks
6
- class SubscriptionCanceled
7
- def call(event)
8
- subscription = event.subscription
9
- return if subscription.nil?
10
-
11
- Pay::Braintree::Subscription.sync(subscription.id)
12
- end
6
+ class SubscriptionCanceled < Subscription
13
7
  end
14
8
  end
15
9
  end
@@ -3,13 +3,7 @@
3
3
  module Pay
4
4
  module Braintree
5
5
  module Webhooks
6
- class SubscriptionExpired
7
- def call(event)
8
- subscription = event.subscription
9
- return if subscription.nil?
10
-
11
- Pay::Braintree::Subscription.sync(subscription.id)
12
- end
6
+ class SubscriptionExpired < Subscription
13
7
  end
14
8
  end
15
9
  end
@@ -3,13 +3,7 @@
3
3
  module Pay
4
4
  module Braintree
5
5
  module Webhooks
6
- class SubscriptionTrialEnded
7
- def call(event)
8
- subscription = event.subscription
9
- return if subscription.nil?
10
-
11
- Pay::Braintree::Subscription.sync(subscription.id)
12
- end
6
+ class SubscriptionTrialEnded < Subscription
13
7
  end
14
8
  end
15
9
  end
@@ -1,15 +1,9 @@
1
- # A subscription's first authorized transaction is created, or a successful transaction moves a subscription from the Past Due status to the Active status. Subscriptions with trial periods will not trigger this notification when they move from the trial period into the first billing cycle.
1
+ # A subscription's first authorized transaction is created, or a successful transaction moves a subscription from the Past Due status to the Active status.
2
2
 
3
3
  module Pay
4
4
  module Braintree
5
5
  module Webhooks
6
- class SubscriptionWentActive
7
- def call(event)
8
- subscription = event.subscription
9
- return if subscription.nil?
10
-
11
- Pay::Braintree::Subscription.sync(subscription.id)
12
- end
6
+ class SubscriptionWentActive < Subscription
13
7
  end
14
8
  end
15
9
  end
@@ -1,15 +1,9 @@
1
- # A subscription has moved from the Active status to the Past Due status. This will only be triggered when the initial transaction in a billing cycle is declined. Once the status moves to past due, it will not be triggered again in that billing cycle.
1
+ # A subscription already exists and fails to create a successful charge.
2
2
 
3
3
  module Pay
4
4
  module Braintree
5
5
  module Webhooks
6
- class SubscriptionWentPastDue
7
- def call(event)
8
- subscription = event.subscription
9
- return if subscription.nil?
10
-
11
- Pay::Braintree::Subscription.sync(subscription.id)
12
- end
6
+ class SubscriptionWentPastDue < Subscription
13
7
  end
14
8
  end
15
9
  end
data/lib/pay/braintree.rb CHANGED
@@ -27,9 +27,9 @@ module Pay
27
27
  end
28
28
 
29
29
  module Webhooks
30
+ autoload :Subscription, "pay/braintree/webhooks/subscription"
30
31
  autoload :SubscriptionCanceled, "pay/braintree/webhooks/subscription_canceled"
31
32
  autoload :SubscriptionChargedSuccessfully, "pay/braintree/webhooks/subscription_charged_successfully"
32
- autoload :SubscriptionChargedUnsuccessfully, "pay/braintree/webhooks/subscription_charged_unsuccessfully"
33
33
  autoload :SubscriptionExpired, "pay/braintree/webhooks/subscription_expired"
34
34
  autoload :SubscriptionTrialEnded, "pay/braintree/webhooks/subscription_trial_ended"
35
35
  autoload :SubscriptionWentActive, "pay/braintree/webhooks/subscription_went_active"
@@ -73,7 +73,6 @@ module Pay
73
73
  Pay::Webhooks.configure do |events|
74
74
  events.subscribe "braintree.subscription_canceled", Pay::Braintree::Webhooks::SubscriptionCanceled.new
75
75
  events.subscribe "braintree.subscription_charged_successfully", Pay::Braintree::Webhooks::SubscriptionChargedSuccessfully.new
76
- events.subscribe "braintree.subscription_charged_unsuccessfully", Pay::Braintree::Webhooks::SubscriptionChargedUnsuccessfully.new
77
76
  events.subscribe "braintree.subscription_expired", Pay::Braintree::Webhooks::SubscriptionExpired.new
78
77
  events.subscribe "braintree.subscription_trial_ended", Pay::Braintree::Webhooks::SubscriptionTrialEnded.new
79
78
  events.subscribe "braintree.subscription_went_active", Pay::Braintree::Webhooks::SubscriptionWentActive.new
data/lib/pay/currency.rb CHANGED
@@ -65,10 +65,6 @@ module Pay
65
65
  attributes["format"]
66
66
  end
67
67
 
68
- def subunit?
69
- subunit.blank?
70
- end
71
-
72
68
  def subunit
73
69
  attributes["subunit"]
74
70
  end