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