pay 7.3.0 → 11.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +8 -4
  3. data/app/controllers/pay/payments_controller.rb +2 -0
  4. data/app/controllers/pay/webhooks/lemon_squeezy_controller.rb +45 -0
  5. data/app/jobs/pay/customer_sync_job.rb +1 -1
  6. data/app/models/concerns/pay/routing.rb +13 -0
  7. data/{lib → app/models}/pay/braintree/charge.rb +7 -12
  8. data/{lib/pay/braintree/billable.rb → app/models/pay/braintree/customer.rb} +33 -71
  9. data/{lib → app/models}/pay/braintree/payment_method.rb +4 -10
  10. data/{lib → app/models}/pay/braintree/subscription.rb +23 -61
  11. data/app/models/pay/charge.rb +16 -45
  12. data/app/models/pay/customer.rb +5 -16
  13. data/app/models/pay/fake_processor/charge.rb +19 -0
  14. data/{lib/pay/fake_processor/billable.rb → app/models/pay/fake_processor/customer.rb} +28 -38
  15. data/{lib → app/models}/pay/fake_processor/merchant.rb +4 -9
  16. data/app/models/pay/fake_processor/payment_method.rb +13 -0
  17. data/app/models/pay/fake_processor/subscription.rb +70 -0
  18. data/app/models/pay/lemon_squeezy/charge.rb +96 -0
  19. data/app/models/pay/lemon_squeezy/customer.rb +80 -0
  20. data/app/models/pay/lemon_squeezy/payment_method.rb +29 -0
  21. data/app/models/pay/lemon_squeezy/subscription.rb +129 -0
  22. data/app/models/pay/merchant.rb +2 -11
  23. data/{lib → app/models}/pay/paddle_billing/charge.rb +15 -13
  24. data/{lib/pay/paddle_billing/billable.rb → app/models/pay/paddle_billing/customer.rb} +20 -35
  25. data/{lib → app/models}/pay/paddle_billing/payment_method.rb +13 -13
  26. data/{lib → app/models}/pay/paddle_billing/subscription.rb +40 -43
  27. data/{lib → app/models}/pay/paddle_classic/charge.rb +15 -18
  28. data/{lib/pay/paddle_classic/billable.rb → app/models/pay/paddle_classic/customer.rb} +11 -31
  29. data/{lib → app/models}/pay/paddle_classic/payment_method.rb +3 -11
  30. data/{lib → app/models}/pay/paddle_classic/subscription.rb +17 -37
  31. data/app/models/pay/payment_method.rb +4 -5
  32. data/app/models/pay/stripe/charge.rb +155 -0
  33. data/{lib/pay/stripe/billable.rb → app/models/pay/stripe/customer.rb} +78 -111
  34. data/{lib → app/models}/pay/stripe/merchant.rb +5 -20
  35. data/{lib → app/models}/pay/stripe/payment_method.rb +11 -17
  36. data/{lib → app/models}/pay/stripe/subscription.rb +83 -112
  37. data/app/models/pay/subscription.rb +13 -47
  38. data/app/models/pay/webhook.rb +5 -1
  39. data/app/views/pay/user_mailer/payment_action_required.text.erb +9 -0
  40. data/app/views/pay/user_mailer/payment_failed.text.erb +9 -0
  41. data/app/views/pay/user_mailer/receipt.text.erb +20 -0
  42. data/app/views/pay/user_mailer/refund.text.erb +21 -0
  43. data/app/views/pay/user_mailer/subscription_renewing.text.erb +8 -0
  44. data/app/views/pay/user_mailer/subscription_trial_ended.text.erb +8 -0
  45. data/app/views/pay/user_mailer/subscription_trial_will_end.text.erb +8 -0
  46. data/config/locales/en.yml +1 -0
  47. data/config/routes.rb +1 -0
  48. data/db/migrate/20250415151129_add_object_to_pay_models.rb +7 -0
  49. data/db/migrate/2_add_pay_sti_columns.rb +24 -0
  50. data/lib/pay/attributes.rb +16 -8
  51. data/lib/pay/braintree.rb +25 -6
  52. data/lib/pay/engine.rb +2 -0
  53. data/lib/pay/fake_processor.rb +2 -6
  54. data/lib/pay/lemon_squeezy/webhooks/order.rb +11 -0
  55. data/lib/pay/lemon_squeezy/webhooks/subscription.rb +3 -3
  56. data/lib/pay/lemon_squeezy/webhooks/subscription_payment.rb +11 -0
  57. data/lib/pay/lemon_squeezy.rb +58 -104
  58. data/lib/pay/nano_id.rb +1 -1
  59. data/lib/pay/paddle_billing.rb +15 -6
  60. data/lib/pay/paddle_classic/webhooks/signature_verifier.rb +1 -1
  61. data/lib/pay/paddle_classic.rb +11 -9
  62. data/lib/pay/receipts.rb +45 -44
  63. data/lib/pay/stripe/webhooks/charge_updated.rb +11 -0
  64. data/lib/pay/stripe/webhooks/customer_updated.rb +13 -9
  65. data/lib/pay/stripe/webhooks/payment_action_required.rb +10 -6
  66. data/lib/pay/stripe/webhooks/payment_failed.rb +6 -4
  67. data/lib/pay/stripe/webhooks/subscription_renewing.rb +9 -4
  68. data/lib/pay/stripe.rb +28 -9
  69. data/lib/pay/version.rb +1 -1
  70. data/lib/pay.rb +19 -1
  71. data/lib/tasks/pay.rake +2 -2
  72. metadata +45 -43
  73. data/app/views/pay/stripe/_checkout_button.html.erb +0 -21
  74. data/lib/pay/braintree/authorization_error.rb +0 -9
  75. data/lib/pay/braintree/error.rb +0 -23
  76. data/lib/pay/fake_processor/charge.rb +0 -21
  77. data/lib/pay/fake_processor/error.rb +0 -6
  78. data/lib/pay/fake_processor/payment_method.rb +0 -21
  79. data/lib/pay/fake_processor/subscription.rb +0 -90
  80. data/lib/pay/lemon_squeezy/billable.rb +0 -90
  81. data/lib/pay/lemon_squeezy/charge.rb +0 -68
  82. data/lib/pay/lemon_squeezy/error.rb +0 -7
  83. data/lib/pay/lemon_squeezy/payment_method.rb +0 -40
  84. data/lib/pay/lemon_squeezy/subscription.rb +0 -185
  85. data/lib/pay/lemon_squeezy/webhooks/transaction_completed.rb +0 -11
  86. data/lib/pay/paddle_billing/error.rb +0 -7
  87. data/lib/pay/paddle_classic/error.rb +0 -7
  88. data/lib/pay/stripe/charge.rb +0 -176
  89. data/lib/pay/stripe/error.rb +0 -7
@@ -1,14 +1,6 @@
1
1
  module Pay
2
2
  module Stripe
3
- class PaymentMethod
4
- attr_reader :pay_payment_method
5
-
6
- delegate :customer, :processor_id, to: :pay_payment_method
7
-
8
- def initialize(pay_payment_method)
9
- @pay_payment_method = pay_payment_method
10
- end
11
-
3
+ class PaymentMethod < Pay::PaymentMethod
12
4
  # Syncs a PaymentIntent's payment method to the database
13
5
  def self.sync_payment_intent(id, stripe_account: nil)
14
6
  payment_intent = ::Stripe::PaymentIntent.retrieve({id: id, expand: ["payment_method"]}, {stripe_account: stripe_account}.compact)
@@ -39,22 +31,22 @@ module Pay
39
31
  return
40
32
  end
41
33
 
42
- default_payment_method_id = pay_customer.customer.invoice_settings.default_payment_method
34
+ default_payment_method_id = pay_customer.api_record.invoice_settings.default_payment_method
43
35
  default = (id == default_payment_method_id)
44
36
 
45
37
  attributes = extract_attributes(object).merge(default: default, stripe_account: stripe_account)
46
38
 
47
- pay_customer.payment_methods.update_all(default: false) if default
48
- pay_payment_method = pay_customer.payment_methods.where(processor_id: object.id).first_or_initialize
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
49
41
  pay_payment_method.update!(attributes)
50
42
  pay_payment_method
51
43
  rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique
52
- try += 1
53
- if try <= retries
54
- sleep 0.1
55
- retry
56
- else
44
+ if try > retries
57
45
  raise
46
+ else
47
+ try += 1
48
+ sleep 0.15**try
49
+ retry
58
50
  end
59
51
  end
60
52
 
@@ -92,3 +84,5 @@ module Pay
92
84
  end
93
85
  end
94
86
  end
87
+
88
+ ActiveSupport.run_load_hooks :pay_stripe_payment_method, Pay::Stripe::PaymentMethod
@@ -1,33 +1,7 @@
1
1
  module Pay
2
2
  module Stripe
3
- class Subscription
4
- attr_accessor :stripe_subscription
5
- attr_reader :pay_subscription
6
-
7
- delegate :active?,
8
- :canceled?,
9
- :ends_at?,
10
- :ends_at,
11
- :name,
12
- :on_trial?,
13
- :past_due?,
14
- :pause_starts_at,
15
- :pause_starts_at?,
16
- :processor_id,
17
- :processor_plan,
18
- :processor_subscription,
19
- :prorate,
20
- :prorate?,
21
- :quantity,
22
- :quantity?,
23
- :stripe_account,
24
- :subscription_items,
25
- :trial_ends_at,
26
- :pause_behavior,
27
- :pause_resumes_at,
28
- :current_period_start,
29
- :current_period_end,
30
- to: :pay_subscription
3
+ class Subscription < Pay::Subscription
4
+ attr_writer :api_record
31
5
 
32
6
  def self.sync_from_checkout_session(session_id, stripe_account: nil)
33
7
  checkout_session = ::Stripe::Checkout::Session.retrieve({id: session_id}, {stripe_account: stripe_account}.compact)
@@ -49,6 +23,7 @@ module Pay
49
23
  end
50
24
 
51
25
  attributes = {
26
+ object: object.to_hash,
52
27
  application_fee_percent: object.application_fee_percent,
53
28
  created_at: Time.at(object.created),
54
29
  processor_plan: object.items.first.price.id,
@@ -56,12 +31,11 @@ module Pay
56
31
  status: object.status,
57
32
  stripe_account: pay_customer.stripe_account,
58
33
  metadata: object.metadata,
59
- subscription_items: [],
60
34
  metered: false,
61
35
  pause_behavior: object.pause_collection&.behavior,
62
36
  pause_resumes_at: (object.pause_collection&.resumes_at ? Time.at(object.pause_collection&.resumes_at) : nil),
63
- current_period_start: (object.current_period_start ? Time.at(object.current_period_start) : nil),
64
- current_period_end: (object.current_period_end ? Time.at(object.current_period_end) : 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)
65
39
  }
66
40
 
67
41
  # Subscriptions that have ended should have their trial ended at the
@@ -73,15 +47,13 @@ module Pay
73
47
  if object.trial_end
74
48
  trial_ended_at = [object.ended_at, object.trial_end].compact.min
75
49
  attributes[:trial_ends_at] = Time.at(trial_ended_at)
50
+ else
51
+ attributes[:trial_ends_at] = nil
76
52
  end
77
53
 
78
- # Record subscription items to db
79
54
  object.items.auto_paging_each do |subscription_item|
80
- if !attributes[:metered] && (subscription_item.to_hash.dig(:price, :recurring, :usage_type) == "metered")
81
- attributes[:metered] = true
82
- end
83
-
84
- attributes[:subscription_items] << subscription_item.to_hash.slice(:id, :price, :metadata, :quantity)
55
+ next if attributes[:metered]
56
+ attributes[:metered] = true if subscription_item.price.try(:recurring).try(:usage_type) == "metered"
85
57
  end
86
58
 
87
59
  attributes[:ends_at] = if object.ended_at
@@ -92,7 +64,7 @@ module Pay
92
64
  Time.at(object.cancel_at)
93
65
  elsif object.cancel_at_period_end
94
66
  # Subscriptions cancelling in the future
95
- Time.at(object.current_period_end)
67
+ Time.at(object.items.first.current_period_end)
96
68
  end
97
69
 
98
70
  # Sync payment method if directly attached to subscription
@@ -107,13 +79,13 @@ module Pay
107
79
  end
108
80
 
109
81
  # Update or create the subscription
110
- pay_subscription = pay_customer.subscriptions.find_by(processor_id: object.id)
82
+ pay_subscription = find_by(customer: pay_customer, processor_id: object.id)
111
83
  if pay_subscription
112
84
  # If pause behavior is changing to `void`, record the pause start date
113
85
  # Any other pause status (or no pause at all) should have nil for start
114
86
  if pay_subscription.pause_behavior != attributes[:pause_behavior]
115
87
  attributes[:pause_starts_at] = if attributes[:pause_behavior] == "void"
116
- Time.at(object.current_period_end)
88
+ Time.at(object.items.first.current_period_end)
117
89
  end
118
90
  end
119
91
 
@@ -121,15 +93,24 @@ module Pay
121
93
  else
122
94
  # Allow setting the subscription name in metadata, otherwise use the default
123
95
  name ||= object.metadata["pay_name"] || Pay.default_product_name
124
- pay_subscription = pay_customer.subscriptions.create!(attributes.merge(name: name, processor_id: object.id))
96
+ pay_subscription = create!(attributes.merge(customer: pay_customer, name: name, processor_id: object.id))
125
97
  end
126
98
 
127
99
  # Cache the Stripe subscription on the Pay::Subscription that we return
128
- pay_subscription.stripe_subscription = object
100
+ pay_subscription.api_record = object
129
101
 
130
102
  # Sync the latest charge if we already have it loaded (like during subscrbe), otherwise, let webhooks take care of creating it
131
- if (charge = object.try(:latest_invoice).try(:charge)) && charge.try(:status) == "succeeded"
132
- Pay::Stripe::Charge.sync(charge.id, stripe_account: pay_subscription.stripe_account)
103
+ if (invoice = object.try(:latest_invoice))
104
+ Array(invoice.try(:payments)).each do |invoice_payment|
105
+ next unless invoice_payment.status == "paid"
106
+
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)
112
+ end
113
+ end
133
114
  end
134
115
 
135
116
  pay_subscription
@@ -148,39 +129,34 @@ module Pay
148
129
  {
149
130
  expand: [
150
131
  "default_payment_method",
151
- "pending_setup_intent",
152
- "latest_invoice.payment_intent",
153
- "latest_invoice.charge",
132
+ "discounts",
133
+ "latest_invoice.confirmation_secret",
134
+ "latest_invoice.payments",
154
135
  "latest_invoice.total_discount_amounts.discount",
155
- "latest_invoice.total_tax_amounts.tax_rate"
136
+ "pending_setup_intent",
137
+ "schedule"
156
138
  ]
157
139
  }
158
140
  end
159
141
 
160
- def initialize(pay_subscription)
161
- @pay_subscription = pay_subscription
162
- end
163
-
164
- def subscription(**options)
165
- options[:id] = processor_id
166
- @stripe_subscription ||= ::Stripe::Subscription.retrieve(options.merge(expand_options), {stripe_account: stripe_account}.compact)
142
+ def stripe_object
143
+ ::Stripe::Subscription.construct_from(object)
167
144
  end
168
145
 
169
- def reload!
170
- @stripe_subscription = nil
146
+ def api_record(**options)
147
+ @api_record ||= ::Stripe::Subscription.retrieve(options.with_defaults(id: processor_id).merge(expand_options), {stripe_account: stripe_account}.compact)
171
148
  end
172
149
 
173
150
  # Returns a SetupIntent or PaymentIntent client secret for the subscription
174
151
  def client_secret
175
- stripe_sub = subscription
176
- stripe_sub&.pending_setup_intent&.client_secret || stripe_sub&.latest_invoice&.payment_intent&.client_secret
152
+ api_record&.pending_setup_intent&.client_secret || api_record&.latest_invoice&.confirmation_secret&.client_secret
177
153
  end
178
154
 
179
155
  # Sets the default_payment_method on a subscription
180
156
  # Pass an empty string to unset
181
157
  def update_payment_method(id)
182
- @stripe_subscription = ::Stripe::Subscription.update(processor_id, {default_payment_method: id}.merge(expand_options), stripe_options)
183
- pay_subscription.update(payment_method_id: @stripe_subscription.default_payment_method&.id)
158
+ @api_record = ::Stripe::Subscription.update(processor_id, {default_payment_method: id}.merge(expand_options), stripe_options)
159
+ update(payment_method_id: @api_record.default_payment_method&.id)
184
160
  rescue ::Stripe::StripeError => e
185
161
  raise Pay::Stripe::Error, e
186
162
  end
@@ -195,8 +171,8 @@ module Pay
195
171
  if past_due? && options.fetch(:past_due_cancel_now, true)
196
172
  cancel_now!
197
173
  else
198
- @stripe_subscription = ::Stripe::Subscription.update(processor_id, {cancel_at_period_end: true}.merge(expand_options), stripe_options)
199
- pay_subscription.update(ends_at: (on_trial? ? trial_ends_at : Time.at(@stripe_subscription.current_period_end)))
174
+ @api_record = ::Stripe::Subscription.update(processor_id, {cancel_at_period_end: true}.merge(expand_options), stripe_options)
175
+ update(ends_at: Time.at(@api_record.cancel_at))
200
176
  end
201
177
  rescue ::Stripe::StripeError => e
202
178
  raise Pay::Stripe::Error, e
@@ -209,8 +185,12 @@ module Pay
209
185
  def cancel_now!(**options)
210
186
  return if canceled? && ends_at.past?
211
187
 
212
- @stripe_subscription = ::Stripe::Subscription.cancel(processor_id, options.merge(expand_options), stripe_options)
213
- pay_subscription.update(ends_at: Time.current, status: :canceled)
188
+ @api_record = ::Stripe::Subscription.cancel(processor_id, options.merge(expand_options), stripe_options)
189
+ update(
190
+ trial_ends_at: (@api_record.trial_end ? Time.at(@api_record.trial_end) : nil),
191
+ ends_at: Time.at(@api_record.ended_at),
192
+ status: @api_record.status
193
+ )
214
194
  rescue ::Stripe::StripeError => e
215
195
  raise Pay::Stripe::Error, e
216
196
  end
@@ -220,14 +200,14 @@ module Pay
220
200
  # For a subscription with a single item, we can update the subscription directly if no SubscriptionItem ID is available
221
201
  # Otherwise a SubscriptionItem ID is required so Stripe knows which entry to update
222
202
  def change_quantity(quantity, **options)
223
- subscription_item_id = options.delete(:subscription_item_id) || subscription_items&.first&.dig("id")
203
+ subscription_item_id = options.delete(:subscription_item_id) || subscription_items&.first&.id
224
204
  if subscription_item_id
225
205
  ::Stripe::SubscriptionItem.update(subscription_item_id, options.merge(quantity: quantity), stripe_options)
226
- @stripe_subscription = nil
206
+ @api_record = nil
227
207
  else
228
- @stripe_subscription = ::Stripe::Subscription.update(processor_id, options.merge(quantity: quantity).merge(expand_options), stripe_options)
208
+ @api_record = ::Stripe::Subscription.update(processor_id, options.merge(quantity: quantity).merge(expand_options), stripe_options)
229
209
  end
230
- true
210
+ update(quantity: quantity)
231
211
  rescue ::Stripe::StripeError => e
232
212
  raise Pay::Stripe::Error, e
233
213
  end
@@ -265,12 +245,12 @@ module Pay
265
245
  # https://docs.stripe.com/billing/subscriptions/pause-payment
266
246
  def pause(**options)
267
247
  attributes = {pause_collection: options.reverse_merge(behavior: "void")}
268
- @stripe_subscription = ::Stripe::Subscription.update(processor_id, attributes.merge(expand_options), stripe_options)
269
- behavior = @stripe_subscription.pause_collection&.behavior
270
- pay_subscription.update(
248
+ @api_record = ::Stripe::Subscription.update(processor_id, attributes.merge(expand_options), stripe_options)
249
+ behavior = @api_record.pause_collection&.behavior
250
+ update(
271
251
  pause_behavior: behavior,
272
- pause_resumes_at: (@stripe_subscription.pause_collection&.resumes_at ? Time.at(@stripe_subscription.pause_collection&.resumes_at) : nil),
273
- pause_starts_at: ((behavior == "void") ? Time.at(@stripe_subscription.current_period_end) : nil)
252
+ pause_resumes_at: (@api_record.pause_collection&.resumes_at ? Time.at(@api_record.pause_collection&.resumes_at) : nil),
253
+ pause_starts_at: ((behavior == "void") ? Time.at(@api_record.items.first.current_period_end) : nil)
274
254
  )
275
255
  end
276
256
 
@@ -278,8 +258,8 @@ module Pay
278
258
  #
279
259
  # https://docs.stripe.com/billing/subscriptions/pause-payment#unpausing
280
260
  def unpause
281
- @stripe_subscription = ::Stripe::Subscription.update(processor_id, {pause_collection: ""}.merge(expand_options), stripe_options)
282
- pay_subscription.update(
261
+ @api_record = ::Stripe::Subscription.update(processor_id, {pause_collection: ""}.merge(expand_options), stripe_options)
262
+ update(
283
263
  pause_behavior: nil,
284
264
  pause_resumes_at: nil,
285
265
  pause_starts_at: nil
@@ -292,22 +272,20 @@ module Pay
292
272
 
293
273
  def resume
294
274
  unless resumable?
295
- raise StandardError, "You can only resume subscriptions within their grace period."
275
+ raise Error, "You can only resume subscriptions within their grace period."
296
276
  end
297
277
 
298
278
  if paused?
299
279
  unpause
300
280
  else
301
- @stripe_subscription = ::Stripe::Subscription.update(
302
- processor_id,
303
- {
304
- plan: processor_plan,
305
- trial_end: (on_trial? ? trial_ends_at.to_i : "now"),
306
- cancel_at_period_end: false
307
- }.merge(expand_options),
308
- stripe_options
309
- )
281
+ @api_record = ::Stripe::Subscription.update(processor_id, {
282
+ plan: processor_plan,
283
+ trial_end: (on_trial? ? trial_ends_at.to_i : "now"),
284
+ cancel_at_period_end: false
285
+ }.merge(expand_options),
286
+ stripe_options)
310
287
  end
288
+ update(ends_at: nil, status: :active)
311
289
  rescue ::Stripe::StripeError => e
312
290
  raise Pay::Stripe::Error, e
313
291
  end
@@ -315,9 +293,10 @@ module Pay
315
293
  def swap(plan, **options)
316
294
  raise ArgumentError, "plan must be a string" unless plan.is_a?(String)
317
295
 
296
+ prorate = options.fetch(:prorate) { true }
318
297
  proration_behavior = options.delete(:proration_behavior) || (prorate ? "always_invoice" : "none")
319
298
 
320
- @stripe_subscription = ::Stripe::Subscription.update(
299
+ @api_record = ::Stripe::Subscription.update(
321
300
  processor_id,
322
301
  {
323
302
  cancel_at_period_end: false,
@@ -330,51 +309,37 @@ module Pay
330
309
  )
331
310
 
332
311
  # Validate that swap was successful and handle SCA if needed
333
- if (payment_intent = @stripe_subscription.latest_invoice.payment_intent)
334
- Pay::Payment.new(payment_intent).validate
312
+ if (payment_intent_id = @api_record.latest_invoice.payments.first&.payment&.payment_intent)
313
+ Pay::Payment.from_id(payment_intent_id).validate
335
314
  end
336
315
 
337
- pay_subscription.sync!(object: @stripe_subscription)
316
+ sync!(object: @api_record)
338
317
  rescue ::Stripe::StripeError => e
339
318
  raise Pay::Stripe::Error, e
340
319
  end
341
320
 
342
- # Creates a metered billing usage record
343
- #
344
- # Uses the first subscription_item ID unless `subscription_item_id: "si_1234"` is passed
345
- #
346
- # create_usage_record(quantity: 4, action: :increment)
347
- # create_usage_record(subscription_item_id: "si_1234", quantity: 100, action: :set)
348
- def create_usage_record(**options)
349
- subscription_item_id = options.delete(:subscription_item_id) || metered_subscription_item&.dig("id")
350
- ::Stripe::SubscriptionItem.create_usage_record(subscription_item_id, options, stripe_options)
351
- end
352
-
353
- # Returns usage record summaries for a subscription item
354
- def usage_record_summaries(**options)
355
- subscription_item_id = options.delete(:subscription_item_id) || metered_subscription_item&.dig("id")
356
- ::Stripe::SubscriptionItem.list_usage_record_summaries(subscription_item_id, options, stripe_options)
321
+ def subscription_items
322
+ stripe_object.items
357
323
  end
358
324
 
359
325
  # Returns the first metered subscription item
360
326
  def metered_subscription_item
361
- subscription_items.find do |subscription_item|
362
- subscription_item.dig("price", "recurring", "usage_type") == "metered"
327
+ subscription_items.auto_paging_each do |subscription_item|
328
+ return subscription_item if subscription_item.price.try(:recurring).try(:usage_type) == "metered"
363
329
  end
364
330
  end
365
331
 
366
- # Returns an upcoming invoice for a subscription
367
- def upcoming_invoice(**options)
368
- ::Stripe::Invoice.upcoming(options.merge(subscription: processor_id), stripe_options)
332
+ def preview_invoice(**options)
333
+ ::Stripe::Invoice.create_preview(options.merge(subscription: processor_id), stripe_options)
369
334
  end
370
335
 
371
336
  # Retries the latest invoice for a Past Due subscription and attempts to pay it
372
337
  def retry_failed_payment(payment_intent_id: nil)
373
- payment_intent_id ||= subscription.latest_invoice.payment_intent.id
338
+ payment_intent_id ||= api_record.latest_invoice.payment_intent.id
374
339
  payment_intent = ::Stripe::PaymentIntent.retrieve({id: payment_intent_id}, stripe_options)
375
340
 
376
341
  payment_intent = if payment_intent.status == "requires_payment_method"
377
- ::Stripe::PaymentIntent.confirm(payment_intent_id, {payment_method: pay_subscription.customer.default_payment_method.processor_id}, stripe_options)
342
+ ::Stripe::PaymentIntent.confirm(payment_intent_id, {payment_method: customer.default_payment_method.processor_id}, stripe_options)
378
343
  else
379
344
  ::Stripe::PaymentIntent.confirm(payment_intent_id, stripe_options)
380
345
  end
@@ -390,6 +355,10 @@ module Pay
390
355
  end
391
356
  end
392
357
 
358
+ def latest_payment
359
+ api_record(expand: ["latest_invoice.payment_intent"]).latest_invoice.payment_intent
360
+ end
361
+
393
362
  private
394
363
 
395
364
  # Options for Stripe requests
@@ -403,3 +372,5 @@ module Pay
403
372
  end
404
373
  end
405
374
  end
375
+
376
+ ActiveSupport.run_load_hooks :pay_stripe_subscription, Pay::Stripe::Subscription
@@ -9,7 +9,7 @@ module Pay
9
9
 
10
10
  # Scopes
11
11
  scope :for_name, ->(name) { where(name: name) }
12
- scope :on_trial, -> { where(status: ["trialing", "active"]).where("trial_ends_at > ?", Time.current) }
12
+ scope :on_trial, -> { where(status: ["on_trial", "trialing", "active"]).where("trial_ends_at > ?", Time.current) }
13
13
  scope :canceled, -> { where.not(ends_at: nil) }
14
14
  scope :cancelled, -> { canceled }
15
15
  scope :on_grace_period, -> { where("#{table_name}.ends_at IS NOT NULL AND #{table_name}.ends_at > ?", Time.current) }
@@ -27,12 +27,6 @@ module Pay
27
27
  # Callbacks
28
28
  before_destroy :cancel_if_active
29
29
 
30
- store_accessor :data, :paddle_update_url
31
- store_accessor :data, :paddle_cancel_url
32
- store_accessor :data, :subscription_items
33
-
34
- attribute :prorate, :boolean, default: true
35
-
36
30
  # Validations
37
31
  validates :name, presence: true
38
32
  validates :processor_id, presence: true, uniqueness: {scope: :customer_id, case_sensitive: true}
@@ -40,10 +34,8 @@ module Pay
40
34
  validates :quantity, presence: true, numericality: {only_integer: true, greater_than_or_equal_to: 0}
41
35
  validates :status, presence: true
42
36
 
43
- delegate_missing_to :payment_processor
44
-
45
37
  # Helper methods for payment processors
46
- %w[braintree stripe paddle_billing paddle_classic fake_processor].each do |processor_name|
38
+ %w[braintree stripe paddle_billing paddle_classic lemon_squeezy fake_processor].each do |processor_name|
47
39
  define_method :"#{processor_name}?" do
48
40
  customer.processor == processor_name
49
41
  end
@@ -51,27 +43,17 @@ module Pay
51
43
  scope processor_name, -> { joins(:customer).where(pay_customers: {processor: processor_name}) }
52
44
  end
53
45
 
46
+ delegate :owner, to: :customer
47
+
54
48
  def self.find_by_processor_and_id(processor, processor_id)
55
49
  joins(:customer).find_by(processor_id: processor_id, pay_customers: {processor: processor})
56
50
  end
57
51
 
58
- def self.pay_processor_for(name)
59
- "Pay::#{name.to_s.classify}::Subscription".constantize
60
- end
61
-
62
- def payment_processor
63
- @payment_processor ||= self.class.pay_processor_for(customer.processor).new(self)
64
- end
65
-
66
52
  def sync!(**options)
67
- self.class.pay_processor_for(customer.processor).sync(processor_id, **options)
53
+ self.class.sync(processor_id, **options)
68
54
  reload
69
55
  end
70
56
 
71
- def no_prorate
72
- self.prorate = false
73
- end
74
-
75
57
  def skip_trial
76
58
  self.trial_ends_at = nil
77
59
  end
@@ -86,10 +68,12 @@ module Pay
86
68
 
87
69
  # Does not include the last second of the trial
88
70
  def on_trial?
71
+ return false if ended?
89
72
  trial_ends_at? && trial_ends_at > Time.current
90
73
  end
91
74
 
92
75
  def trial_ended?
76
+ return true if ended?
93
77
  trial_ends_at? && trial_ends_at <= Time.current
94
78
  end
95
79
 
@@ -105,6 +89,10 @@ module Pay
105
89
  ends_at? && ends_at <= Time.current
106
90
  end
107
91
 
92
+ def on_grace_period?
93
+ ends_at? && ends_at > Time.current
94
+ end
95
+
108
96
  # If you cancel during a trial, you should still retain access until the end of the trial
109
97
  # Otherwise a subscription is active unless it has ended or is currently paused
110
98
  # Check the subscription status so we don't accidentally consider "incomplete", "unpaid", or other statuses as active
@@ -129,35 +117,11 @@ module Pay
129
117
  past_due? || incomplete?
130
118
  end
131
119
 
132
- def change_quantity(quantity, **options)
133
- payment_processor.change_quantity(quantity, **options)
134
- update(quantity: quantity)
135
- end
136
-
137
- def resume
138
- payment_processor.resume
139
- update(ends_at: nil, status: :active)
140
- self
141
- end
142
-
143
- def swap(plan, **options)
144
- raise ArgumentError, "plan must be a string. Got `#{plan.inspect}` instead." unless plan.is_a?(String)
145
- payment_processor.swap(plan, **options)
146
- end
147
-
148
120
  def swap_and_invoice(plan)
149
121
  swap(plan)
150
122
  customer.invoice!(subscription: processor_id)
151
123
  end
152
124
 
153
- def processor_subscription(**options)
154
- payment_processor.subscription(**options)
155
- end
156
-
157
- def latest_payment
158
- processor_subscription(expand: ["latest_invoice.payment_intent"]).latest_invoice.payment_intent
159
- end
160
-
161
125
  private
162
126
 
163
127
  def cancel_if_active
@@ -167,3 +131,5 @@ module Pay
167
131
  end
168
132
  end
169
133
  end
134
+
135
+ ActiveSupport.run_load_hooks :pay_subscription, Pay::Subscription
@@ -21,6 +21,8 @@ module Pay
21
21
  to_recursive_ostruct(event["data"])
22
22
  when "paddle_classic"
23
23
  to_recursive_ostruct(event)
24
+ when "lemon_squeezy"
25
+ Pay::LemonSqueezy.construct_from_webhook_event(event)
24
26
  when "stripe"
25
27
  ::Stripe::Event.construct_from(event)
26
28
  else
@@ -30,7 +32,7 @@ module Pay
30
32
 
31
33
  def to_recursive_ostruct(obj)
32
34
  if obj.is_a?(Hash)
33
- OpenStruct.new(obj.map { |key, val| [key, to_recursive_ostruct(val)] }.to_h)
35
+ ActiveSupport::InheritableOptions.new(obj.map { |key, val| [key.to_sym, to_recursive_ostruct(val)] }.to_h)
34
36
  elsif obj.is_a?(Array)
35
37
  obj.map { |o| to_recursive_ostruct(o) }
36
38
  else # Assumed to be a primitive value
@@ -39,3 +41,5 @@ module Pay
39
41
  end
40
42
  end
41
43
  end
44
+
45
+ ActiveSupport.run_load_hooks :pay_webhook, Pay::Webhook
@@ -0,0 +1,9 @@
1
+ Extra confirmation is needed to process your payment
2
+
3
+ Your <%= Pay.application_name %> subscription requires confirmation to process your payment to continue access.
4
+
5
+ Confirm your payment: <%= pay.payment_url(params[:payment_intent_id]) %>
6
+
7
+ If you have any questions, please hit reply and let us know.
8
+
9
+ — The <%= Pay.application_name %> Team
@@ -0,0 +1,9 @@
1
+ Your payment was declined
2
+
3
+ We were unable to charge your payment method for your <%= Pay.application_name %> subscription. Please update your billing information.
4
+
5
+ Update billing information: <%= root_url %>
6
+
7
+ Let us know if you have any questions.
8
+
9
+ — The <%= Pay.application_name %> Team
@@ -0,0 +1,20 @@
1
+ We received payment for your <%= Pay.application_name %> subscription. Thanks for your business!
2
+
3
+ Questions? Please reply to this email.
4
+
5
+ ------------------------------------
6
+ RECEIPT - SUBSCRIPTION
7
+
8
+ <%= Pay.application_name %>
9
+ Amount: <%= params[:pay_charge].amount_with_currency %>
10
+
11
+ Charged to: <%= params[:pay_charge].charged_to %>
12
+ Transaction ID: <%= params[:pay_charge].id %>
13
+ Date: <%= l params[:pay_charge].created_at %>
14
+ <% if params[:pay_charge].customer.owner.try(:extra_billing_info?) %>
15
+ <%= params[:pay_charge].customer.owner.extra_billing_info %>
16
+ <% end %>
17
+
18
+ <%= Pay.business_name %>
19
+ <%= Pay.business_address %>
20
+ ------------------------------------
@@ -0,0 +1,21 @@
1
+ We have processed your <%= Pay.application_name %> refund.
2
+ Please allow up to 7 business days for your refund to appear in your account
3
+
4
+ Questions? Please reply to this email.
5
+
6
+ ------------------------------------
7
+ RECEIPT - REFUND
8
+
9
+ <%= Pay.application_name %>
10
+ Amount: <%= params[:pay_charge].amount_refunded_with_currency %>
11
+
12
+ Refunded to: <%= params[:pay_charge].charged_to %>
13
+ Transaction ID: <%= params[:pay_charge].id %>
14
+ Date: <%= l params[:pay_charge].created_at %>
15
+ <% if params[:pay_charge].customer.owner.try(:extra_billing_info?) %>
16
+ <%= params[:pay_charge].customer.owner.extra_billing_info %>
17
+ <% end %>
18
+
19
+ <%= Pay.business_name %>
20
+ <%= Pay.business_address %>
21
+ ------------------------------------
@@ -0,0 +1,8 @@
1
+ Your upcoming <%= Pay.application_name %> subscription renewal
2
+
3
+ This is a friendly reminder that your <%= Pay.application_name %> subscription will renew automatically on <%= l params[:date].to_date, format: :long %>.
4
+
5
+ You may manage your subscription via your account: <%= root_url %>
6
+ If you have any questions, please hit reply and let us know.
7
+
8
+ — The <%= Pay.application_name %> Team
@@ -0,0 +1,8 @@
1
+ Your <%= Pay.application_name %> trial has ended
2
+
3
+ This is just a friendly reminder that your <%= Pay.application_name %> trial has ended.
4
+
5
+ You may manage your subscription via your account: <%= root_url %>
6
+ If you have any questions, please hit reply and let us know.
7
+
8
+ — The <%= Pay.application_name %> Team