pay 7.3.0 → 8.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -0
  3. data/app/controllers/pay/webhooks/lemon_squeezy_controller.rb +45 -0
  4. data/app/jobs/pay/customer_sync_job.rb +1 -1
  5. data/app/models/concerns/pay/routing.rb +13 -0
  6. data/{lib → app/models}/pay/braintree/charge.rb +5 -12
  7. data/{lib/pay/braintree/billable.rb → app/models/pay/braintree/customer.rb} +31 -71
  8. data/{lib → app/models}/pay/braintree/payment_method.rb +1 -9
  9. data/{lib → app/models}/pay/braintree/subscription.rb +15 -53
  10. data/app/models/pay/charge.rb +8 -27
  11. data/app/models/pay/customer.rb +2 -15
  12. data/app/models/pay/fake_processor/charge.rb +13 -0
  13. data/{lib/pay/fake_processor/billable.rb → app/models/pay/fake_processor/customer.rb} +20 -37
  14. data/{lib → app/models}/pay/fake_processor/merchant.rb +2 -9
  15. data/app/models/pay/fake_processor/payment_method.rb +11 -0
  16. data/app/models/pay/fake_processor/subscription.rb +60 -0
  17. data/app/models/pay/lemon_squeezy/charge.rb +86 -0
  18. data/app/models/pay/lemon_squeezy/customer.rb +78 -0
  19. data/app/models/pay/lemon_squeezy/payment_method.rb +27 -0
  20. data/app/models/pay/lemon_squeezy/subscription.rb +129 -0
  21. data/app/models/pay/merchant.rb +0 -11
  22. data/{lib → app/models}/pay/paddle_billing/charge.rb +2 -8
  23. data/{lib/pay/paddle_billing/billable.rb → app/models/pay/paddle_billing/customer.rb} +18 -35
  24. data/{lib → app/models}/pay/paddle_billing/payment_method.rb +2 -12
  25. data/{lib → app/models}/pay/paddle_billing/subscription.rb +9 -33
  26. data/{lib → app/models}/pay/paddle_classic/charge.rb +13 -18
  27. data/{lib/pay/paddle_classic/billable.rb → app/models/pay/paddle_classic/customer.rb} +9 -31
  28. data/{lib → app/models}/pay/paddle_classic/payment_method.rb +1 -11
  29. data/{lib → app/models}/pay/paddle_classic/subscription.rb +11 -36
  30. data/app/models/pay/payment_method.rb +0 -5
  31. data/{lib → app/models}/pay/stripe/charge.rb +6 -22
  32. data/{lib/pay/stripe/billable.rb → app/models/pay/stripe/customer.rb} +73 -108
  33. data/{lib → app/models}/pay/stripe/merchant.rb +2 -11
  34. data/{lib → app/models}/pay/stripe/payment_method.rb +2 -10
  35. data/{lib → app/models}/pay/stripe/subscription.rb +37 -71
  36. data/app/models/pay/subscription.rb +7 -37
  37. data/app/models/pay/webhook.rb +3 -1
  38. data/config/routes.rb +1 -0
  39. data/db/migrate/2_add_pay_sti_columns.rb +24 -0
  40. data/lib/pay/attributes.rb +11 -3
  41. data/lib/pay/braintree.rb +25 -6
  42. data/lib/pay/engine.rb +2 -0
  43. data/lib/pay/fake_processor.rb +2 -6
  44. data/lib/pay/lemon_squeezy/webhooks/order.rb +11 -0
  45. data/lib/pay/lemon_squeezy/webhooks/subscription.rb +3 -3
  46. data/lib/pay/lemon_squeezy/webhooks/subscription_payment.rb +11 -0
  47. data/lib/pay/lemon_squeezy.rb +56 -104
  48. data/lib/pay/paddle_billing.rb +15 -6
  49. data/lib/pay/paddle_classic.rb +11 -9
  50. data/lib/pay/receipts.rb +6 -6
  51. data/lib/pay/stripe/webhooks/customer_updated.rb +1 -1
  52. data/lib/pay/stripe.rb +16 -7
  53. data/lib/pay/version.rb +1 -1
  54. data/lib/pay.rb +12 -1
  55. metadata +34 -38
  56. data/app/views/pay/stripe/_checkout_button.html.erb +0 -21
  57. data/lib/pay/braintree/authorization_error.rb +0 -9
  58. data/lib/pay/braintree/error.rb +0 -23
  59. data/lib/pay/fake_processor/charge.rb +0 -21
  60. data/lib/pay/fake_processor/error.rb +0 -6
  61. data/lib/pay/fake_processor/payment_method.rb +0 -21
  62. data/lib/pay/fake_processor/subscription.rb +0 -90
  63. data/lib/pay/lemon_squeezy/billable.rb +0 -90
  64. data/lib/pay/lemon_squeezy/charge.rb +0 -68
  65. data/lib/pay/lemon_squeezy/error.rb +0 -7
  66. data/lib/pay/lemon_squeezy/payment_method.rb +0 -40
  67. data/lib/pay/lemon_squeezy/subscription.rb +0 -185
  68. data/lib/pay/lemon_squeezy/webhooks/transaction_completed.rb +0 -11
  69. data/lib/pay/paddle_billing/error.rb +0 -7
  70. data/lib/pay/paddle_classic/error.rb +0 -7
  71. data/lib/pay/stripe/error.rb +0 -7
@@ -1,36 +1,20 @@
1
1
  module Pay
2
2
  module Stripe
3
- class Billable
4
- include Rails.application.routes.url_helpers
5
-
6
- attr_reader :pay_customer
7
-
8
- delegate :processor_id,
9
- :processor_id?,
10
- :email,
11
- :customer_name,
12
- :payment_method_token,
13
- :payment_method_token?,
14
- :stripe_account,
15
- to: :pay_customer
16
-
17
- def self.default_url_options
18
- Rails.application.config.action_mailer.default_url_options || {}
19
- end
3
+ class Customer < Pay::Customer
4
+ include Pay::Routing
20
5
 
21
- def initialize(pay_customer)
22
- @pay_customer = pay_customer
23
- end
6
+ has_many :charges, dependent: :destroy, class_name: "Pay::Stripe::Charge"
7
+ has_many :subscriptions, dependent: :destroy, class_name: "Pay::Stripe::Subscription"
8
+ has_many :payment_methods, dependent: :destroy, class_name: "Pay::Stripe::PaymentMethod"
9
+ has_one :default_payment_method, -> { where(default: true) }, class_name: "Pay::Stripe::PaymentMethod"
24
10
 
25
11
  # Returns a hash of attributes for the Stripe::Customer object
26
- def customer_attributes
27
- owner = pay_customer.owner
28
-
12
+ def api_record_attributes
29
13
  attributes = case owner.class.pay_stripe_customer_attributes
30
14
  when Symbol
31
- owner.send(owner.class.pay_stripe_customer_attributes, pay_customer)
15
+ owner.send(owner.class.pay_stripe_customer_attributes, self)
32
16
  when Proc
33
- owner.class.pay_stripe_customer_attributes.call(pay_customer)
17
+ owner.class.pay_stripe_customer_attributes.call(self)
34
18
  end
35
19
 
36
20
  # Guard against attributes being returned nil
@@ -39,55 +23,30 @@ module Pay
39
23
  {email: email, name: customer_name}.merge(attributes)
40
24
  end
41
25
 
42
- # Retrieves a Stripe::Customer object
43
- #
44
- # Finds an existing Stripe::Customer if processor_id exists
45
- # Creates a new Stripe::Customer using `customer_attributes` if empty processor_id
46
- #
47
- # Updates the default payment method automatically if a payment_method_token is set
48
- #
49
- # Returns a Stripe::Customer object
50
- def customer
51
- stripe_customer = if processor_id?
52
- ::Stripe::Customer.retrieve({id: processor_id, expand: ["tax", "invoice_credit_balance"]}, stripe_options)
53
- else
54
- sc = ::Stripe::Customer.create(customer_attributes.merge(expand: ["tax"]), stripe_options)
55
- pay_customer.update!(processor_id: sc.id, stripe_account: stripe_account)
56
- sc
57
- end
58
-
59
- if payment_method_token?
60
- add_payment_method(payment_method_token, default: true)
61
- pay_customer.payment_method_token = nil
26
+ def api_record(expand: ["tax", "invoice_credit_balance"])
27
+ with_lock do
28
+ if processor_id?
29
+ ::Stripe::Customer.retrieve({id: processor_id, expand: expand}, stripe_options)
30
+ else
31
+ ::Stripe::Customer.create(api_record_attributes.merge(expand: expand), stripe_options).tap do |customer|
32
+ update!(processor_id: customer.id, stripe_account: stripe_account)
33
+ end
34
+ end
62
35
  end
63
-
64
- stripe_customer
65
36
  rescue ::Stripe::StripeError => e
66
37
  raise Pay::Stripe::Error, e
67
38
  end
68
39
 
69
- # Syncs name and email to Stripe::Customer
70
- # You can also pass in other attributes that will be merged into the default attributes
71
- def update_customer!(**attributes)
72
- customer unless processor_id?
73
- ::Stripe::Customer.update(
74
- processor_id,
75
- customer_attributes.merge(attributes),
76
- stripe_options
77
- )
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)
78
43
  end
79
44
 
80
45
  # Charges an amount to the customer's default payment method
81
46
  def charge(amount, options = {})
82
- add_payment_method(payment_method_token, default: true) if payment_method_token?
83
-
84
- payment_method = pay_customer.default_payment_method
85
- args = {
86
- confirm: true,
87
- payment_method: payment_method&.processor_id
88
- }.merge(options)
89
-
47
+ args = {confirm: true, payment_method: default_payment_method&.processor_id}.merge(options)
90
48
  payment_intent = create_payment_intent(amount, args)
49
+
91
50
  Pay::Payment.new(payment_intent).validate
92
51
 
93
52
  charge = payment_intent.latest_charge
@@ -96,24 +55,6 @@ module Pay
96
55
  raise Pay::Stripe::Error, e
97
56
  end
98
57
 
99
- # Creates and returns a Stripe::PaymentIntent
100
- def create_payment_intent(amount, options = {})
101
- args = {
102
- amount: amount,
103
- currency: "usd",
104
- customer: processor_id,
105
- expand: ["latest_charge.refunds"],
106
- return_url: root_url
107
- }.merge(options)
108
-
109
- ::Stripe::PaymentIntent.create(args, stripe_options)
110
- end
111
-
112
- # Used for creating Stripe Terminal charges
113
- def terminal_charge(amount, options = {})
114
- create_payment_intent(amount, options.merge(payment_method_types: ["card_present"], capture_method: "manual"))
115
- end
116
-
117
58
  def subscribe(name: Pay.default_product_name, plan: Pay.default_plan_name, **options)
118
59
  quantity = options.delete(:quantity)
119
60
  opts = {
@@ -122,7 +63,7 @@ module Pay
122
63
  }.merge(options)
123
64
 
124
65
  # Load the Stripe customer to verify it exists and update payment method if needed
125
- opts[:customer] = customer.id
66
+ opts[:customer] = processor_id || api_record.id
126
67
 
127
68
  # Create subscription on Stripe
128
69
  stripe_sub = ::Stripe::Subscription.create(opts.merge(Pay::Stripe::Subscription.expand_options), stripe_options)
@@ -141,7 +82,7 @@ module Pay
141
82
  end
142
83
 
143
84
  def add_payment_method(payment_method_id, default: false)
144
- customer unless processor_id?
85
+ api_record unless processor_id?
145
86
  payment_method = ::Stripe::PaymentMethod.attach(payment_method_id, {customer: processor_id}, stripe_options)
146
87
 
147
88
  if default
@@ -159,50 +100,56 @@ module Pay
159
100
 
160
101
  # Save the Stripe::PaymentMethod to the database
161
102
  def save_payment_method(payment_method, default:)
162
- pay_payment_method = pay_customer.payment_methods.where(processor_id: payment_method.id).first_or_initialize
103
+ pay_payment_method = payment_methods.where(processor_id: payment_method.id).first_or_initialize
163
104
 
164
105
  attributes = Pay::Stripe::PaymentMethod.extract_attributes(payment_method).merge(default: default)
165
106
 
166
107
  # Ignore the payment method if it's already in the database
167
- pay_customer.payment_methods.where.not(id: pay_payment_method.id).update_all(default: false) if default
108
+ payment_methods.where.not(id: pay_payment_method.id).update_all(default: false) if default
168
109
  pay_payment_method.update!(attributes)
169
110
 
170
111
  # Reload the Rails association
171
- pay_customer.reload_default_payment_method
112
+ reload_default_payment_method
172
113
 
173
114
  pay_payment_method
174
115
  end
175
116
 
176
- def processor_subscription(subscription_id, options = {})
177
- ::Stripe::Subscription.retrieve(options.merge(id: subscription_id), stripe_options)
178
- end
117
+ ### Stripe extras
179
118
 
180
- def invoice!(options = {})
181
- return unless processor_id?
182
- ::Stripe::Invoice.create(options.merge(customer: processor_id), stripe_options).pay
119
+ # Creates and returns a Stripe::PaymentIntent
120
+ def create_payment_intent(amount, options = {})
121
+ args = {
122
+ amount: amount,
123
+ currency: "usd",
124
+ customer: processor_id || api_record.id,
125
+ expand: ["latest_charge.refunds"],
126
+ return_url: root_url
127
+ }.merge(options)
128
+
129
+ ::Stripe::PaymentIntent.create(args, stripe_options)
183
130
  end
184
131
 
185
- def upcoming_invoice
186
- ::Stripe::Invoice.upcoming({customer: processor_id}, stripe_options)
132
+ # Used for creating Stripe Terminal charges
133
+ def terminal_charge(amount, options = {})
134
+ create_payment_intent(amount, options.merge(payment_method_types: ["card_present"], capture_method: "manual"))
187
135
  end
188
136
 
189
137
  def create_setup_intent(options = {})
190
- customer unless processor_id?
191
- ::Stripe::SetupIntent.create({
192
- customer: processor_id,
193
- usage: :off_session
194
- }.merge(options), stripe_options)
138
+ ::Stripe::SetupIntent.create({customer: processor_id || api_record.id, usage: :off_session}.merge(options), stripe_options)
195
139
  end
196
140
 
197
- def trial_end_date(stripe_sub)
198
- # Times in Stripe are returned in UTC
199
- stripe_sub.trial_end.present? ? Time.at(stripe_sub.trial_end) : nil
141
+ def invoice!(options = {})
142
+ ::Stripe::Invoice.create(options.merge(customer: processor_id || api_record.id), stripe_options).pay
143
+ end
144
+
145
+ def upcoming_invoice
146
+ ::Stripe::Invoice.upcoming({customer: processor_id || api_record.id}, stripe_options)
200
147
  end
201
148
 
202
149
  # Syncs a customer's subscriptions from Stripe to the database.
203
150
  # Note that by default canceled subscriptions are NOT returned by Stripe. In order to include them, use `sync_subscriptions(status: "all")`.
204
151
  def sync_subscriptions(**options)
205
- subscriptions = ::Stripe::Subscription.list(options.merge(customer: customer), stripe_options)
152
+ subscriptions = ::Stripe::Subscription.list(options.with_defaults(customer: processor_id), stripe_options)
206
153
  subscriptions.map do |subscription|
207
154
  Pay::Stripe::Subscription.sync(subscription.id)
208
155
  end
@@ -221,7 +168,7 @@ module Pay
221
168
  # checkout(line_items: "price_12345", allow_promotion_codes: true)
222
169
  #
223
170
  def checkout(**options)
224
- customer unless processor_id?
171
+ api_record unless processor_id?
225
172
  args = {
226
173
  customer: processor_id,
227
174
  mode: "payment"
@@ -261,7 +208,7 @@ module Pay
261
208
  # checkout_charge(amount: 15_00, name: "T-shirt", quantity: 2)
262
209
  #
263
210
  def checkout_charge(amount:, name:, quantity: 1, **options)
264
- customer unless processor_id?
211
+ api_record unless processor_id?
265
212
  currency = options.delete(:currency) || "usd"
266
213
  checkout(
267
214
  line_items: {
@@ -277,7 +224,7 @@ module Pay
277
224
  end
278
225
 
279
226
  def billing_portal(**options)
280
- customer unless processor_id?
227
+ api_record unless processor_id?
281
228
  args = {
282
229
  customer: processor_id,
283
230
  return_url: options.delete(:return_url) || root_url
@@ -285,10 +232,28 @@ module Pay
285
232
  ::Stripe::BillingPortal::Session.create(args.merge(options), stripe_options)
286
233
  end
287
234
 
235
+ def customer_session(**options)
236
+ api_record unless processor_id?
237
+ args = {customer: processor_id}
238
+ ::Stripe::CustomerSession.create(args.merge(options), stripe_options)
239
+ end
240
+
288
241
  def authorize(amount, options = {})
289
242
  charge(amount, options.merge(capture_method: :manual))
290
243
  end
291
244
 
245
+ # Creates a meter event to bill for usage
246
+ #
247
+ # create_meter_event(:api_request, value: 1)
248
+ # create_meter_event(:api_request, token: 7)
249
+ def create_meter_event(event_name, payload: {}, **options)
250
+ api_record unless processor_id?
251
+ ::Stripe::Billing::MeterEvent.create({
252
+ event_name: event_name,
253
+ payload: {stripe_customer_id: processor_id}.merge(payload)
254
+ }.merge(options))
255
+ end
256
+
292
257
  private
293
258
 
294
259
  # Options for Stripe requests
@@ -299,7 +264,7 @@ module Pay
299
264
  # Includes the `session_id` param for Stripe Checkout with existing params (and makes sure the curly braces aren't escaped)
300
265
  def merge_session_id_param(url)
301
266
  uri = URI.parse(url)
302
- uri.query = URI.encode_www_form(URI.decode_www_form(uri.query.to_s).to_h.merge("session_id" => "{CHECKOUT_SESSION_ID}").to_a)
267
+ uri.query = URI.encode_www_form(URI.decode_www_form(uri.query.to_s).to_h.merge("stripe_checkout_session_id" => "{CHECKOUT_SESSION_ID}").to_a)
303
268
  uri.to_s.gsub("%7BCHECKOUT_SESSION_ID%7D", "{CHECKOUT_SESSION_ID}")
304
269
  end
305
270
  end
@@ -1,15 +1,6 @@
1
1
  module Pay
2
2
  module Stripe
3
- class Merchant
4
- attr_reader :pay_merchant
5
-
6
- delegate :processor_id,
7
- to: :pay_merchant
8
-
9
- def initialize(pay_merchant)
10
- @pay_merchant = pay_merchant
11
- end
12
-
3
+ class Merchant < Pay::Merchant
13
4
  def create_account(**options)
14
5
  defaults = {
15
6
  type: "express",
@@ -20,7 +11,7 @@ module Pay
20
11
  }
21
12
 
22
13
  stripe_account = ::Stripe::Account.create(defaults.merge(options))
23
- pay_merchant.update(processor_id: stripe_account.id)
14
+ update(processor_id: stripe_account.id)
24
15
  stripe_account
25
16
  rescue ::Stripe::StripeError => e
26
17
  raise Pay::Stripe::Error, e
@@ -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,7 +31,7 @@ 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)
@@ -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)
@@ -125,7 +99,7 @@ module Pay
125
99
  end
126
100
 
127
101
  # Cache the Stripe subscription on the Pay::Subscription that we return
128
- pay_subscription.stripe_subscription = object
102
+ pay_subscription.api_record = object
129
103
 
130
104
  # Sync the latest charge if we already have it loaded (like during subscrbe), otherwise, let webhooks take care of creating it
131
105
  if (charge = object.try(:latest_invoice).try(:charge)) && charge.try(:status) == "succeeded"
@@ -157,30 +131,20 @@ module Pay
157
131
  }
158
132
  end
159
133
 
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)
167
- end
168
-
169
- def reload!
170
- @stripe_subscription = nil
134
+ def api_record(**options)
135
+ @api_record ||= ::Stripe::Subscription.retrieve(options.with_defaults(id: processor_id).merge(expand_options), {stripe_account: stripe_account}.compact)
171
136
  end
172
137
 
173
138
  # Returns a SetupIntent or PaymentIntent client secret for the subscription
174
139
  def client_secret
175
- stripe_sub = subscription
176
- stripe_sub&.pending_setup_intent&.client_secret || stripe_sub&.latest_invoice&.payment_intent&.client_secret
140
+ api_record&.pending_setup_intent&.client_secret || api_record&.latest_invoice&.payment_intent&.client_secret
177
141
  end
178
142
 
179
143
  # Sets the default_payment_method on a subscription
180
144
  # Pass an empty string to unset
181
145
  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)
146
+ @api_record = ::Stripe::Subscription.update(processor_id, {default_payment_method: id}.merge(expand_options), stripe_options)
147
+ update(payment_method_id: @api_record.default_payment_method&.id)
184
148
  rescue ::Stripe::StripeError => e
185
149
  raise Pay::Stripe::Error, e
186
150
  end
@@ -195,8 +159,8 @@ module Pay
195
159
  if past_due? && options.fetch(:past_due_cancel_now, true)
196
160
  cancel_now!
197
161
  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)))
162
+ @api_record = ::Stripe::Subscription.update(processor_id, {cancel_at_period_end: true}.merge(expand_options), stripe_options)
163
+ update(ends_at: (on_trial? ? trial_ends_at : Time.at(@api_record.current_period_end)))
200
164
  end
201
165
  rescue ::Stripe::StripeError => e
202
166
  raise Pay::Stripe::Error, e
@@ -209,8 +173,8 @@ module Pay
209
173
  def cancel_now!(**options)
210
174
  return if canceled? && ends_at.past?
211
175
 
212
- @stripe_subscription = ::Stripe::Subscription.cancel(processor_id, options.merge(expand_options), stripe_options)
213
- pay_subscription.update(ends_at: Time.current, status: :canceled)
176
+ @api_record = ::Stripe::Subscription.cancel(processor_id, options.merge(expand_options), stripe_options)
177
+ update(ends_at: Time.current, status: :canceled)
214
178
  rescue ::Stripe::StripeError => e
215
179
  raise Pay::Stripe::Error, e
216
180
  end
@@ -223,11 +187,11 @@ module Pay
223
187
  subscription_item_id = options.delete(:subscription_item_id) || subscription_items&.first&.dig("id")
224
188
  if subscription_item_id
225
189
  ::Stripe::SubscriptionItem.update(subscription_item_id, options.merge(quantity: quantity), stripe_options)
226
- @stripe_subscription = nil
190
+ @api_record = nil
227
191
  else
228
- @stripe_subscription = ::Stripe::Subscription.update(processor_id, options.merge(quantity: quantity).merge(expand_options), stripe_options)
192
+ @api_record = ::Stripe::Subscription.update(processor_id, options.merge(quantity: quantity).merge(expand_options), stripe_options)
229
193
  end
230
- true
194
+ update(quantity: quantity)
231
195
  rescue ::Stripe::StripeError => e
232
196
  raise Pay::Stripe::Error, e
233
197
  end
@@ -265,12 +229,12 @@ module Pay
265
229
  # https://docs.stripe.com/billing/subscriptions/pause-payment
266
230
  def pause(**options)
267
231
  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(
232
+ @api_record = ::Stripe::Subscription.update(processor_id, attributes.merge(expand_options), stripe_options)
233
+ behavior = @api_record.pause_collection&.behavior
234
+ update(
271
235
  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)
236
+ pause_resumes_at: (@api_record.pause_collection&.resumes_at ? Time.at(@api_record.pause_collection&.resumes_at) : nil),
237
+ pause_starts_at: ((behavior == "void") ? Time.at(@api_record.current_period_end) : nil)
274
238
  )
275
239
  end
276
240
 
@@ -278,8 +242,8 @@ module Pay
278
242
  #
279
243
  # https://docs.stripe.com/billing/subscriptions/pause-payment#unpausing
280
244
  def unpause
281
- @stripe_subscription = ::Stripe::Subscription.update(processor_id, {pause_collection: ""}.merge(expand_options), stripe_options)
282
- pay_subscription.update(
245
+ @api_record = ::Stripe::Subscription.update(processor_id, {pause_collection: ""}.merge(expand_options), stripe_options)
246
+ update(
283
247
  pause_behavior: nil,
284
248
  pause_resumes_at: nil,
285
249
  pause_starts_at: nil
@@ -298,16 +262,14 @@ module Pay
298
262
  if paused?
299
263
  unpause
300
264
  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
- )
265
+ @api_record = ::Stripe::Subscription.update(processor_id, {
266
+ plan: processor_plan,
267
+ trial_end: (on_trial? ? trial_ends_at.to_i : "now"),
268
+ cancel_at_period_end: false
269
+ }.merge(expand_options),
270
+ stripe_options)
310
271
  end
272
+ update(ends_at: nil, status: :active)
311
273
  rescue ::Stripe::StripeError => e
312
274
  raise Pay::Stripe::Error, e
313
275
  end
@@ -317,7 +279,7 @@ module Pay
317
279
 
318
280
  proration_behavior = options.delete(:proration_behavior) || (prorate ? "always_invoice" : "none")
319
281
 
320
- @stripe_subscription = ::Stripe::Subscription.update(
282
+ @api_record = ::Stripe::Subscription.update(
321
283
  processor_id,
322
284
  {
323
285
  cancel_at_period_end: false,
@@ -330,11 +292,11 @@ module Pay
330
292
  )
331
293
 
332
294
  # Validate that swap was successful and handle SCA if needed
333
- if (payment_intent = @stripe_subscription.latest_invoice.payment_intent)
295
+ if (payment_intent = @api_record.latest_invoice.payment_intent)
334
296
  Pay::Payment.new(payment_intent).validate
335
297
  end
336
298
 
337
- pay_subscription.sync!(object: @stripe_subscription)
299
+ sync!(object: @api_record)
338
300
  rescue ::Stripe::StripeError => e
339
301
  raise Pay::Stripe::Error, e
340
302
  end
@@ -374,7 +336,7 @@ module Pay
374
336
  payment_intent = ::Stripe::PaymentIntent.retrieve({id: payment_intent_id}, stripe_options)
375
337
 
376
338
  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)
339
+ ::Stripe::PaymentIntent.confirm(payment_intent_id, {payment_method: customer.default_payment_method.processor_id}, stripe_options)
378
340
  else
379
341
  ::Stripe::PaymentIntent.confirm(payment_intent_id, stripe_options)
380
342
  end
@@ -390,6 +352,10 @@ module Pay
390
352
  end
391
353
  end
392
354
 
355
+ def latest_payment
356
+ api_record(expand: ["latest_invoice.payment_intent"]).latest_invoice.payment_intent
357
+ end
358
+
393
359
  private
394
360
 
395
361
  # Options for Stripe requests
@@ -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) }
@@ -40,10 +40,8 @@ module Pay
40
40
  validates :quantity, presence: true, numericality: {only_integer: true, greater_than_or_equal_to: 0}
41
41
  validates :status, presence: true
42
42
 
43
- delegate_missing_to :payment_processor
44
-
45
43
  # Helper methods for payment processors
46
- %w[braintree stripe paddle_billing paddle_classic fake_processor].each do |processor_name|
44
+ %w[braintree stripe paddle_billing paddle_classic lemon_squeezy fake_processor].each do |processor_name|
47
45
  define_method :"#{processor_name}?" do
48
46
  customer.processor == processor_name
49
47
  end
@@ -55,16 +53,8 @@ module Pay
55
53
  joins(:customer).find_by(processor_id: processor_id, pay_customers: {processor: processor})
56
54
  end
57
55
 
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
56
  def sync!(**options)
67
- self.class.pay_processor_for(customer.processor).sync(processor_id, **options)
57
+ self.class.sync(processor_id, **options)
68
58
  reload
69
59
  end
70
60
 
@@ -105,6 +95,10 @@ module Pay
105
95
  ends_at? && ends_at <= Time.current
106
96
  end
107
97
 
98
+ def on_grace_period?
99
+ ends_at? && ends_at > Time.current
100
+ end
101
+
108
102
  # If you cancel during a trial, you should still retain access until the end of the trial
109
103
  # Otherwise a subscription is active unless it has ended or is currently paused
110
104
  # Check the subscription status so we don't accidentally consider "incomplete", "unpaid", or other statuses as active
@@ -129,35 +123,11 @@ module Pay
129
123
  past_due? || incomplete?
130
124
  end
131
125
 
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
126
  def swap_and_invoice(plan)
149
127
  swap(plan)
150
128
  customer.invoice!(subscription: processor_id)
151
129
  end
152
130
 
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
131
  private
162
132
 
163
133
  def cancel_if_active
@@ -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
data/config/routes.rb CHANGED
@@ -6,4 +6,5 @@ Pay::Engine.routes.draw do
6
6
  post "webhooks/braintree", to: "pay/webhooks/braintree#create" if Pay::Braintree.enabled?
7
7
  post "webhooks/paddle_billing", to: "pay/webhooks/paddle_billing#create" if Pay::PaddleBilling.enabled?
8
8
  post "webhooks/paddle_classic", to: "pay/webhooks/paddle_classic#create" if Pay::PaddleClassic.enabled?
9
+ post "webhooks/lemon_squeezy", to: "pay/webhooks/lemon_squeezy#create" if Pay::LemonSqueezy.enabled?
9
10
  end