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.
- checksums.yaml +4 -4
- data/README.md +8 -4
- data/app/controllers/pay/payments_controller.rb +2 -0
- data/app/controllers/pay/webhooks/lemon_squeezy_controller.rb +45 -0
- data/app/jobs/pay/customer_sync_job.rb +1 -1
- data/app/models/concerns/pay/routing.rb +13 -0
- data/{lib → app/models}/pay/braintree/charge.rb +7 -12
- data/{lib/pay/braintree/billable.rb → app/models/pay/braintree/customer.rb} +33 -71
- data/{lib → app/models}/pay/braintree/payment_method.rb +4 -10
- data/{lib → app/models}/pay/braintree/subscription.rb +23 -61
- data/app/models/pay/charge.rb +16 -45
- data/app/models/pay/customer.rb +5 -16
- data/app/models/pay/fake_processor/charge.rb +19 -0
- data/{lib/pay/fake_processor/billable.rb → app/models/pay/fake_processor/customer.rb} +28 -38
- data/{lib → app/models}/pay/fake_processor/merchant.rb +4 -9
- data/app/models/pay/fake_processor/payment_method.rb +13 -0
- data/app/models/pay/fake_processor/subscription.rb +70 -0
- data/app/models/pay/lemon_squeezy/charge.rb +96 -0
- data/app/models/pay/lemon_squeezy/customer.rb +80 -0
- data/app/models/pay/lemon_squeezy/payment_method.rb +29 -0
- data/app/models/pay/lemon_squeezy/subscription.rb +129 -0
- data/app/models/pay/merchant.rb +2 -11
- data/{lib → app/models}/pay/paddle_billing/charge.rb +15 -13
- data/{lib/pay/paddle_billing/billable.rb → app/models/pay/paddle_billing/customer.rb} +20 -35
- data/{lib → app/models}/pay/paddle_billing/payment_method.rb +13 -13
- data/{lib → app/models}/pay/paddle_billing/subscription.rb +40 -43
- data/{lib → app/models}/pay/paddle_classic/charge.rb +15 -18
- data/{lib/pay/paddle_classic/billable.rb → app/models/pay/paddle_classic/customer.rb} +11 -31
- data/{lib → app/models}/pay/paddle_classic/payment_method.rb +3 -11
- data/{lib → app/models}/pay/paddle_classic/subscription.rb +17 -37
- data/app/models/pay/payment_method.rb +4 -5
- data/app/models/pay/stripe/charge.rb +155 -0
- data/{lib/pay/stripe/billable.rb → app/models/pay/stripe/customer.rb} +78 -111
- data/{lib → app/models}/pay/stripe/merchant.rb +5 -20
- data/{lib → app/models}/pay/stripe/payment_method.rb +11 -17
- data/{lib → app/models}/pay/stripe/subscription.rb +83 -112
- data/app/models/pay/subscription.rb +13 -47
- data/app/models/pay/webhook.rb +5 -1
- data/app/views/pay/user_mailer/payment_action_required.text.erb +9 -0
- data/app/views/pay/user_mailer/payment_failed.text.erb +9 -0
- data/app/views/pay/user_mailer/receipt.text.erb +20 -0
- data/app/views/pay/user_mailer/refund.text.erb +21 -0
- data/app/views/pay/user_mailer/subscription_renewing.text.erb +8 -0
- data/app/views/pay/user_mailer/subscription_trial_ended.text.erb +8 -0
- data/app/views/pay/user_mailer/subscription_trial_will_end.text.erb +8 -0
- data/config/locales/en.yml +1 -0
- data/config/routes.rb +1 -0
- data/db/migrate/20250415151129_add_object_to_pay_models.rb +7 -0
- data/db/migrate/2_add_pay_sti_columns.rb +24 -0
- data/lib/pay/attributes.rb +16 -8
- data/lib/pay/braintree.rb +25 -6
- data/lib/pay/engine.rb +2 -0
- data/lib/pay/fake_processor.rb +2 -6
- data/lib/pay/lemon_squeezy/webhooks/order.rb +11 -0
- data/lib/pay/lemon_squeezy/webhooks/subscription.rb +3 -3
- data/lib/pay/lemon_squeezy/webhooks/subscription_payment.rb +11 -0
- data/lib/pay/lemon_squeezy.rb +58 -104
- data/lib/pay/nano_id.rb +1 -1
- data/lib/pay/paddle_billing.rb +15 -6
- data/lib/pay/paddle_classic/webhooks/signature_verifier.rb +1 -1
- data/lib/pay/paddle_classic.rb +11 -9
- data/lib/pay/receipts.rb +45 -44
- data/lib/pay/stripe/webhooks/charge_updated.rb +11 -0
- data/lib/pay/stripe/webhooks/customer_updated.rb +13 -9
- data/lib/pay/stripe/webhooks/payment_action_required.rb +10 -6
- data/lib/pay/stripe/webhooks/payment_failed.rb +6 -4
- data/lib/pay/stripe/webhooks/subscription_renewing.rb +9 -4
- data/lib/pay/stripe.rb +28 -9
- data/lib/pay/version.rb +1 -1
- data/lib/pay.rb +19 -1
- data/lib/tasks/pay.rake +2 -2
- metadata +45 -43
- data/app/views/pay/stripe/_checkout_button.html.erb +0 -21
- data/lib/pay/braintree/authorization_error.rb +0 -9
- data/lib/pay/braintree/error.rb +0 -23
- data/lib/pay/fake_processor/charge.rb +0 -21
- data/lib/pay/fake_processor/error.rb +0 -6
- data/lib/pay/fake_processor/payment_method.rb +0 -21
- data/lib/pay/fake_processor/subscription.rb +0 -90
- data/lib/pay/lemon_squeezy/billable.rb +0 -90
- data/lib/pay/lemon_squeezy/charge.rb +0 -68
- data/lib/pay/lemon_squeezy/error.rb +0 -7
- data/lib/pay/lemon_squeezy/payment_method.rb +0 -40
- data/lib/pay/lemon_squeezy/subscription.rb +0 -185
- data/lib/pay/lemon_squeezy/webhooks/transaction_completed.rb +0 -11
- data/lib/pay/paddle_billing/error.rb +0 -7
- data/lib/pay/paddle_classic/error.rb +0 -7
- data/lib/pay/stripe/charge.rb +0 -176
- 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.
|
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.
|
48
|
-
pay_payment_method =
|
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
|
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
|
-
|
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
|
81
|
-
|
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 =
|
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 =
|
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.
|
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 (
|
132
|
-
|
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
|
-
"
|
152
|
-
"latest_invoice.
|
153
|
-
"latest_invoice.
|
132
|
+
"discounts",
|
133
|
+
"latest_invoice.confirmation_secret",
|
134
|
+
"latest_invoice.payments",
|
154
135
|
"latest_invoice.total_discount_amounts.discount",
|
155
|
-
"
|
136
|
+
"pending_setup_intent",
|
137
|
+
"schedule"
|
156
138
|
]
|
157
139
|
}
|
158
140
|
end
|
159
141
|
|
160
|
-
def
|
161
|
-
|
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
|
170
|
-
@
|
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
|
-
|
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
|
-
@
|
183
|
-
|
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
|
-
@
|
199
|
-
|
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
|
-
@
|
213
|
-
|
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&.
|
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
|
-
@
|
206
|
+
@api_record = nil
|
227
207
|
else
|
228
|
-
@
|
208
|
+
@api_record = ::Stripe::Subscription.update(processor_id, options.merge(quantity: quantity).merge(expand_options), stripe_options)
|
229
209
|
end
|
230
|
-
|
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
|
-
@
|
269
|
-
behavior = @
|
270
|
-
|
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: (@
|
273
|
-
pause_starts_at: ((behavior == "void") ? Time.at(@
|
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
|
-
@
|
282
|
-
|
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
|
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
|
-
@
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
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
|
-
@
|
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 (
|
334
|
-
Pay::Payment.
|
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
|
-
|
316
|
+
sync!(object: @api_record)
|
338
317
|
rescue ::Stripe::StripeError => e
|
339
318
|
raise Pay::Stripe::Error, e
|
340
319
|
end
|
341
320
|
|
342
|
-
|
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.
|
362
|
-
subscription_item.
|
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
|
-
|
367
|
-
|
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 ||=
|
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:
|
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.
|
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
|
data/app/models/pay/webhook.rb
CHANGED
@@ -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
|
-
|
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
|