pay 3.0.23 → 4.0.1
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of pay might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/app/controllers/pay/webhooks/braintree_controller.rb +1 -1
- data/app/controllers/pay/webhooks/paddle_controller.rb +1 -1
- data/app/controllers/pay/webhooks/stripe_controller.rb +1 -1
- data/app/jobs/pay/customer_sync_job.rb +1 -3
- data/app/mailers/pay/application_mailer.rb +1 -1
- data/app/mailers/pay/user_mailer.rb +3 -3
- data/app/models/pay/charge.rb +23 -0
- data/app/models/pay/customer.rb +2 -6
- data/app/models/pay/merchant.rb +6 -0
- data/app/models/pay/subscription.rb +35 -8
- data/app/views/pay/user_mailer/receipt.html.erb +6 -6
- data/app/views/pay/user_mailer/refund.html.erb +6 -6
- data/config/locales/en.yml +31 -24
- data/config/routes.rb +3 -3
- data/lib/pay/attributes.rb +28 -2
- data/lib/pay/billable/sync_customer.rb +3 -3
- data/lib/pay/braintree/billable.rb +61 -48
- data/lib/pay/braintree/subscription.rb +8 -3
- data/lib/pay/braintree/webhooks/subscription_canceled.rb +6 -1
- data/lib/pay/braintree/webhooks/subscription_charged_successfully.rb +2 -2
- data/lib/pay/braintree/webhooks/subscription_charged_unsuccessfully.rb +1 -1
- data/lib/pay/braintree/webhooks/subscription_trial_ended.rb +1 -1
- data/lib/pay/braintree.rb +6 -2
- data/lib/pay/currency.rb +8 -2
- data/lib/pay/engine.rb +22 -4
- data/lib/pay/fake_processor/billable.rb +11 -8
- data/lib/pay/fake_processor/subscription.rb +11 -3
- data/lib/pay/paddle/billable.rb +0 -4
- data/lib/pay/paddle/subscription.rb +2 -2
- data/lib/pay/paddle/webhooks/signature_verifier.rb +45 -41
- data/lib/pay/paddle/webhooks/subscription_cancelled.rb +7 -2
- data/lib/pay/paddle/webhooks/subscription_payment_refunded.rb +3 -3
- data/lib/pay/paddle/webhooks/subscription_payment_succeeded.rb +7 -7
- data/lib/pay/paddle/webhooks/subscription_updated.rb +15 -15
- data/lib/pay/paddle.rb +14 -2
- data/lib/pay/receipts.rb +106 -32
- data/lib/pay/stripe/billable.rb +58 -21
- data/lib/pay/stripe/charge.rb +93 -11
- data/lib/pay/stripe/merchant.rb +1 -1
- data/lib/pay/stripe/subscription.rb +132 -30
- data/lib/pay/stripe/webhooks/charge_refunded.rb +2 -2
- data/lib/pay/stripe/webhooks/charge_succeeded.rb +2 -2
- data/lib/pay/stripe/webhooks/payment_action_required.rb +6 -6
- data/lib/pay/stripe/webhooks/subscription_renewing.rb +6 -10
- data/lib/pay/stripe/webhooks/subscription_updated.rb +1 -1
- data/lib/pay/stripe.rb +10 -1
- data/lib/pay/version.rb +1 -1
- data/lib/pay.rb +39 -4
- data/lib/tasks/pay.rake +1 -1
- metadata +3 -4
- data/lib/pay/merchant.rb +0 -37
data/lib/pay/stripe/billable.rb
CHANGED
@@ -22,11 +22,36 @@ module Pay
|
|
22
22
|
@pay_customer = pay_customer
|
23
23
|
end
|
24
24
|
|
25
|
+
# Returns a hash of attributes for the Stripe::Customer object
|
26
|
+
def customer_attributes
|
27
|
+
owner = pay_customer.owner
|
28
|
+
|
29
|
+
attributes = case owner.class.pay_stripe_customer_attributes
|
30
|
+
when Symbol
|
31
|
+
owner.send(owner.class.pay_stripe_customer_attributes, pay_customer)
|
32
|
+
when Proc
|
33
|
+
owner.class.pay_stripe_customer_attributes.call(pay_customer)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Guard against attributes being returned nil
|
37
|
+
attributes ||= {}
|
38
|
+
|
39
|
+
{email: email, name: customer_name}.merge(attributes)
|
40
|
+
end
|
41
|
+
|
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
|
25
50
|
def customer
|
26
51
|
stripe_customer = if processor_id?
|
27
|
-
::Stripe::Customer.retrieve({id: processor_id}, stripe_options)
|
52
|
+
::Stripe::Customer.retrieve({id: processor_id, expand: ["tax"]}, stripe_options)
|
28
53
|
else
|
29
|
-
sc = ::Stripe::Customer.create(
|
54
|
+
sc = ::Stripe::Customer.create(customer_attributes.merge(expand: ["tax"]), stripe_options)
|
30
55
|
pay_customer.update!(processor_id: sc.id, stripe_account: stripe_account)
|
31
56
|
sc
|
32
57
|
end
|
@@ -45,9 +70,14 @@ module Pay
|
|
45
70
|
end
|
46
71
|
|
47
72
|
# Syncs name and email to Stripe::Customer
|
48
|
-
|
49
|
-
|
50
|
-
|
73
|
+
# You can also pass in other attributes that will be merged into the default attributes
|
74
|
+
def update_customer!(**attributes)
|
75
|
+
customer unless processor_id?
|
76
|
+
::Stripe::Customer.update(
|
77
|
+
processor_id,
|
78
|
+
customer_attributes.merge(attributes),
|
79
|
+
stripe_options
|
80
|
+
)
|
51
81
|
end
|
52
82
|
|
53
83
|
def charge(amount, options = {})
|
@@ -57,7 +87,6 @@ module Pay
|
|
57
87
|
args = {
|
58
88
|
amount: amount,
|
59
89
|
confirm: true,
|
60
|
-
confirmation_method: :automatic,
|
61
90
|
currency: "usd",
|
62
91
|
customer: processor_id,
|
63
92
|
payment_method: payment_method&.processor_id
|
@@ -73,7 +102,7 @@ module Pay
|
|
73
102
|
end
|
74
103
|
|
75
104
|
def subscribe(name: Pay.default_product_name, plan: Pay.default_plan_name, **options)
|
76
|
-
quantity = options.delete(:quantity)
|
105
|
+
quantity = options.delete(:quantity)
|
77
106
|
opts = {
|
78
107
|
expand: ["pending_setup_intent", "latest_invoice.payment_intent", "latest_invoice.charge.invoice"],
|
79
108
|
items: [plan: plan, quantity: quantity],
|
@@ -87,13 +116,13 @@ module Pay
|
|
87
116
|
opts[:customer] = customer.id
|
88
117
|
|
89
118
|
# Create subscription on Stripe
|
90
|
-
stripe_sub = ::Stripe::Subscription.create(opts, stripe_options)
|
119
|
+
stripe_sub = ::Stripe::Subscription.create(opts.merge(Pay::Stripe::Subscription.expand_options), stripe_options)
|
91
120
|
|
92
121
|
# Save Pay::Subscription
|
93
122
|
subscription = Pay::Stripe::Subscription.sync(stripe_sub.id, object: stripe_sub, name: name)
|
94
123
|
|
95
124
|
# No trial, payment method requires SCA
|
96
|
-
if subscription.incomplete?
|
125
|
+
if options[:payment_behavior].to_s != "default_incomplete" && subscription.incomplete?
|
97
126
|
Pay::Payment.new(stripe_sub.latest_invoice.payment_intent).validate
|
98
127
|
end
|
99
128
|
|
@@ -125,19 +154,16 @@ module Pay
|
|
125
154
|
|
126
155
|
attributes = Pay::Stripe::PaymentMethod.extract_attributes(payment_method).merge(default: default)
|
127
156
|
|
128
|
-
|
157
|
+
# Ignore the payment method if it's already in the database
|
158
|
+
pay_customer.payment_methods.where.not(id: pay_payment_method.id).update_all(default: false) if default
|
129
159
|
pay_payment_method.update!(attributes)
|
130
160
|
|
131
161
|
# Reload the Rails association
|
132
|
-
pay_customer.reload_default_payment_method
|
162
|
+
pay_customer.reload_default_payment_method
|
133
163
|
|
134
164
|
pay_payment_method
|
135
165
|
end
|
136
166
|
|
137
|
-
def update_email!
|
138
|
-
::Stripe::Customer.update(processor_id, {email: email, name: customer_name}, stripe_options)
|
139
|
-
end
|
140
|
-
|
141
167
|
def processor_subscription(subscription_id, options = {})
|
142
168
|
::Stripe::Subscription.retrieve(options.merge(id: subscription_id), stripe_options)
|
143
169
|
end
|
@@ -151,8 +177,11 @@ module Pay
|
|
151
177
|
::Stripe::Invoice.upcoming({customer: processor_id}, stripe_options)
|
152
178
|
end
|
153
179
|
|
154
|
-
def create_setup_intent
|
155
|
-
::Stripe::SetupIntent.create({
|
180
|
+
def create_setup_intent(options = {})
|
181
|
+
::Stripe::SetupIntent.create({
|
182
|
+
customer: processor_id,
|
183
|
+
usage: :off_session
|
184
|
+
}.merge(options), stripe_options)
|
156
185
|
end
|
157
186
|
|
158
187
|
def trial_end_date(stripe_sub)
|
@@ -177,14 +206,13 @@ module Pay
|
|
177
206
|
# checkout(mode: "subscription")
|
178
207
|
#
|
179
208
|
# checkout(line_items: "price_12345", quantity: 2)
|
180
|
-
# checkout(line_items [{ price: "price_123" }, { price: "price_456" }])
|
181
|
-
# checkout(line_items
|
209
|
+
# checkout(line_items: [{ price: "price_123" }, { price: "price_456" }])
|
210
|
+
# checkout(line_items: "price_12345", allow_promotion_codes: true)
|
182
211
|
#
|
183
212
|
def checkout(**options)
|
184
213
|
customer unless processor_id?
|
185
214
|
args = {
|
186
215
|
customer: processor_id,
|
187
|
-
payment_method_types: ["card"],
|
188
216
|
mode: "payment",
|
189
217
|
# These placeholder URLs will be replaced in a following step.
|
190
218
|
success_url: merge_session_id_param(options.delete(:success_url) || root_url),
|
@@ -193,11 +221,16 @@ module Pay
|
|
193
221
|
|
194
222
|
# Line items are optional
|
195
223
|
if (line_items = options.delete(:line_items))
|
224
|
+
quantity = options.delete(:quantity) || 1
|
225
|
+
|
196
226
|
args[:line_items] = Array.wrap(line_items).map { |item|
|
197
227
|
if item.is_a? Hash
|
198
228
|
item
|
199
229
|
else
|
200
|
-
{
|
230
|
+
{
|
231
|
+
price: item,
|
232
|
+
quantity: quantity
|
233
|
+
}
|
201
234
|
end
|
202
235
|
}
|
203
236
|
end
|
@@ -234,6 +267,10 @@ module Pay
|
|
234
267
|
::Stripe::BillingPortal::Session.create(args.merge(options), stripe_options)
|
235
268
|
end
|
236
269
|
|
270
|
+
def authorize(amount, options = {})
|
271
|
+
charge(amount, options.merge(capture_method: :manual))
|
272
|
+
end
|
273
|
+
|
237
274
|
private
|
238
275
|
|
239
276
|
# Options for Stripe requests
|
data/lib/pay/stripe/charge.rb
CHANGED
@@ -3,11 +3,21 @@ module Pay
|
|
3
3
|
class Charge
|
4
4
|
attr_reader :pay_charge
|
5
5
|
|
6
|
-
delegate :
|
6
|
+
delegate :amount,
|
7
|
+
:amount_captured,
|
8
|
+
:invoice_id,
|
9
|
+
:line_items,
|
10
|
+
:payment_intent_id,
|
11
|
+
:processor_id,
|
12
|
+
:stripe_account,
|
13
|
+
to: :pay_charge
|
7
14
|
|
8
15
|
def self.sync(charge_id, object: nil, stripe_account: nil, try: 0, retries: 1)
|
9
16
|
# Skip loading the latest charge details from the API if we already have it
|
10
|
-
object ||= ::Stripe::Charge.retrieve(charge_id, {stripe_account: stripe_account}.compact)
|
17
|
+
object ||= ::Stripe::Charge.retrieve({id: charge_id, expand: ["invoice.total_discount_amounts.discount", "invoice.total_tax_amounts.tax_rate"]}, {stripe_account: stripe_account}.compact)
|
18
|
+
|
19
|
+
# Ignore charges without a Customer
|
20
|
+
return if object.customer.blank?
|
11
21
|
|
12
22
|
pay_customer = Pay::Customer.find_by(processor: :stripe, processor_id: object.customer)
|
13
23
|
return unless pay_customer
|
@@ -15,24 +25,61 @@ module Pay
|
|
15
25
|
payment_method = object.payment_method_details.send(object.payment_method_details.type)
|
16
26
|
attrs = {
|
17
27
|
amount: object.amount,
|
28
|
+
amount_captured: object.amount_captured,
|
18
29
|
amount_refunded: object.amount_refunded,
|
19
30
|
application_fee_amount: object.application_fee_amount,
|
31
|
+
bank: payment_method.try(:bank_name) || payment_method.try(:bank), # eps, fpx, ideal, p24, acss_debit, etc
|
32
|
+
brand: payment_method.try(:brand)&.capitalize,
|
20
33
|
created_at: Time.at(object.created),
|
21
34
|
currency: object.currency,
|
22
|
-
|
23
|
-
metadata: object.metadata,
|
24
|
-
payment_method_type: object.payment_method_details.type,
|
25
|
-
brand: payment_method.try(:brand)&.capitalize,
|
26
|
-
last4: payment_method.try(:last4).to_s,
|
35
|
+
discounts: [],
|
27
36
|
exp_month: payment_method.try(:exp_month).to_s,
|
28
37
|
exp_year: payment_method.try(:exp_year).to_s,
|
29
|
-
|
38
|
+
last4: payment_method.try(:last4).to_s,
|
39
|
+
line_items: [],
|
40
|
+
metadata: object.metadata,
|
41
|
+
payment_intent_id: object.payment_intent,
|
42
|
+
payment_method_type: object.payment_method_details.type,
|
43
|
+
stripe_account: pay_customer.stripe_account,
|
44
|
+
stripe_receipt_url: object.receipt_url,
|
45
|
+
total_tax_amounts: []
|
30
46
|
}
|
31
47
|
|
32
48
|
# Associate charge with subscription if we can
|
33
49
|
if object.invoice
|
34
|
-
invoice = (object.invoice.is_a?(::Stripe::Invoice) ? object.invoice : ::Stripe::Invoice.retrieve(object.invoice, {stripe_account: stripe_account}.compact))
|
50
|
+
invoice = (object.invoice.is_a?(::Stripe::Invoice) ? object.invoice : ::Stripe::Invoice.retrieve({id: object.invoice, expand: ["total_discount_amounts.discount", "total_tax_amounts.tax_rate"]}, {stripe_account: stripe_account}.compact))
|
51
|
+
attrs[:invoice_id] = invoice.id
|
35
52
|
attrs[:subscription] = pay_customer.subscriptions.find_by(processor_id: invoice.subscription)
|
53
|
+
|
54
|
+
attrs[:period_start] = Time.at(invoice.period_start)
|
55
|
+
attrs[:period_end] = Time.at(invoice.period_end)
|
56
|
+
attrs[:subtotal] = invoice.subtotal
|
57
|
+
attrs[:tax] = invoice.tax
|
58
|
+
attrs[:discounts] = invoice.discounts
|
59
|
+
attrs[:total_tax_amounts] = invoice.total_tax_amounts.map(&:to_hash)
|
60
|
+
attrs[:total_discount_amounts] = invoice.total_discount_amounts.map(&:to_hash)
|
61
|
+
|
62
|
+
invoice.lines.auto_paging_each do |line_item|
|
63
|
+
# Currency is tied to the charge, so storing it would be duplication
|
64
|
+
attrs[:line_items] << {
|
65
|
+
id: line_item.id,
|
66
|
+
description: line_item.description,
|
67
|
+
price_id: line_item.price&.id,
|
68
|
+
quantity: line_item.quantity,
|
69
|
+
unit_amount: line_item.price&.unit_amount,
|
70
|
+
amount: line_item.amount,
|
71
|
+
discounts: line_item.discounts,
|
72
|
+
tax_amounts: line_item.tax_amounts,
|
73
|
+
proration: line_item.proration,
|
74
|
+
period_start: Time.at(line_item.period.start),
|
75
|
+
period_end: Time.at(line_item.period.end)
|
76
|
+
}
|
77
|
+
end
|
78
|
+
|
79
|
+
# Charges without invoices
|
80
|
+
else
|
81
|
+
attrs[:period_start] = Time.at(object.created)
|
82
|
+
attrs[:period_end] = Time.at(object.created)
|
36
83
|
end
|
37
84
|
|
38
85
|
# Update or create the charge
|
@@ -64,14 +111,49 @@ module Pay
|
|
64
111
|
raise Pay::Stripe::Error, e
|
65
112
|
end
|
66
113
|
|
114
|
+
# Issues a CreditNote if there's an invoice, otherwise uses a Refund
|
115
|
+
# This allows Tax to be handled properly
|
116
|
+
#
|
117
|
+
# https://stripe.com/docs/api/credit_notes/create
|
67
118
|
# https://stripe.com/docs/api/refunds/create
|
68
119
|
#
|
69
120
|
# refund!
|
70
121
|
# refund!(5_00)
|
71
122
|
# refund!(5_00, refund_application_fee: true)
|
72
123
|
def refund!(amount_to_refund, **options)
|
73
|
-
|
74
|
-
|
124
|
+
if invoice_id.present?
|
125
|
+
description = options.delete(:description) || I18n.t("refund")
|
126
|
+
lines = [{type: :custom_line_item, description: description, quantity: 1, unit_amount: amount_to_refund}]
|
127
|
+
credit_note!(**options.merge(refund_amount: amount_to_refund, lines: lines))
|
128
|
+
else
|
129
|
+
::Stripe::Refund.create(options.merge(charge: processor_id, amount: amount_to_refund), stripe_options)
|
130
|
+
end
|
131
|
+
pay_charge.update!(amount_refunded: pay_charge.amount_refunded + amount_to_refund)
|
132
|
+
rescue ::Stripe::StripeError => e
|
133
|
+
raise Pay::Stripe::Error, e
|
134
|
+
end
|
135
|
+
|
136
|
+
# Adds a credit note to a Stripe Invoice
|
137
|
+
def credit_note!(**options)
|
138
|
+
raise Pay::Stripe::Error, "no Stripe invoice_id on Pay::Charge" if invoice_id.blank?
|
139
|
+
::Stripe::CreditNote.create({invoice: invoice_id}.merge(options), stripe_options)
|
140
|
+
rescue ::Stripe::StripeError => e
|
141
|
+
raise Pay::Stripe::Error, e
|
142
|
+
end
|
143
|
+
|
144
|
+
def credit_notes(**options)
|
145
|
+
raise Pay::Stripe::Error, "no Stripe invoice_id on Pay::Charge" if invoice_id.blank?
|
146
|
+
::Stripe::CreditNote.list({invoice: invoice_id}.merge(options), stripe_options)
|
147
|
+
end
|
148
|
+
|
149
|
+
# https://stripe.com/docs/payments/capture-later
|
150
|
+
#
|
151
|
+
# capture
|
152
|
+
# capture(amount_to_capture: 15_00)
|
153
|
+
def capture(**options)
|
154
|
+
raise Pay::Stripe::Error, "no payment_intent_id on charge" unless payment_intent_id.present?
|
155
|
+
::Stripe::PaymentIntent.capture(payment_intent_id, options, stripe_options)
|
156
|
+
self.class.sync(processor_id)
|
75
157
|
rescue ::Stripe::StripeError => e
|
76
158
|
raise Pay::Stripe::Error, e
|
77
159
|
end
|
data/lib/pay/stripe/merchant.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
module Pay
|
2
2
|
module Stripe
|
3
3
|
class Subscription
|
4
|
+
attr_accessor :stripe_subscription
|
4
5
|
attr_reader :pay_subscription
|
5
6
|
|
6
7
|
delegate :active?,
|
@@ -16,26 +17,46 @@ module Pay
|
|
16
17
|
:quantity,
|
17
18
|
:quantity?,
|
18
19
|
:stripe_account,
|
20
|
+
:subscription_items,
|
19
21
|
:trial_ends_at,
|
22
|
+
:pause_behavior,
|
23
|
+
:pause_resumes_at,
|
20
24
|
to: :pay_subscription
|
21
25
|
|
22
|
-
def self.sync(subscription_id, object: nil, name:
|
26
|
+
def self.sync(subscription_id, object: nil, name: nil, stripe_account: nil, try: 0, retries: 1)
|
23
27
|
# Skip loading the latest subscription details from the API if we already have it
|
24
|
-
object ||= ::Stripe::Subscription.retrieve({id: subscription_id
|
28
|
+
object ||= ::Stripe::Subscription.retrieve({id: subscription_id}.merge(expand_options), {stripe_account: stripe_account}.compact)
|
25
29
|
|
26
30
|
pay_customer = Pay::Customer.find_by(processor: :stripe, processor_id: object.customer)
|
27
31
|
return unless pay_customer
|
28
32
|
|
29
33
|
attributes = {
|
30
34
|
application_fee_percent: object.application_fee_percent,
|
31
|
-
processor_plan: object.
|
32
|
-
quantity: object.quantity,
|
35
|
+
processor_plan: object.items.first.price.id,
|
36
|
+
quantity: object.items.first.try(:quantity) || 0,
|
33
37
|
status: object.status,
|
34
38
|
stripe_account: pay_customer.stripe_account,
|
35
|
-
|
36
|
-
|
39
|
+
metadata: object.metadata,
|
40
|
+
subscription_items: [],
|
41
|
+
metered: false,
|
42
|
+
pause_behavior: object.pause_collection&.behavior,
|
43
|
+
pause_resumes_at: (object.pause_collection&.resumes_at ? Time.at(object.pause_collection&.resumes_at) : nil)
|
37
44
|
}
|
38
45
|
|
46
|
+
# Subscriptions that have ended should have their trial ended at the same time
|
47
|
+
if object.trial_end
|
48
|
+
attributes[:trial_ends_at] = Time.at(object.ended_at || object.trial_end)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Record subscription items to db
|
52
|
+
object.items.auto_paging_each do |subscription_item|
|
53
|
+
if !attributes[:metered] && (subscription_item.to_hash.dig(:price, :recurring, :usage_type) == "metered")
|
54
|
+
attributes[:metered] = true
|
55
|
+
end
|
56
|
+
|
57
|
+
attributes[:subscription_items] << subscription_item.to_hash.slice(:id, :price, :metadata, :quantity)
|
58
|
+
end
|
59
|
+
|
39
60
|
attributes[:ends_at] = if object.ended_at
|
40
61
|
# Fully cancelled subscription
|
41
62
|
Time.at(object.ended_at)
|
@@ -52,9 +73,15 @@ module Pay
|
|
52
73
|
if pay_subscription
|
53
74
|
pay_subscription.with_lock { pay_subscription.update!(attributes) }
|
54
75
|
else
|
76
|
+
# Allow setting the subscription name in metadata, otherwise use the default
|
77
|
+
name ||= object.metadata["pay_name"] || Pay.default_product_name
|
78
|
+
|
55
79
|
pay_subscription = pay_customer.subscriptions.create!(attributes.merge(name: name, processor_id: object.id))
|
56
80
|
end
|
57
81
|
|
82
|
+
# Cache the Stripe subscription on the Pay::Subscription that we return
|
83
|
+
pay_subscription.stripe_subscription = object
|
84
|
+
|
58
85
|
# Sync the latest charge if we already have it loaded (like during subscrbe), otherwise, let webhooks take care of creating it
|
59
86
|
if (charge = object.try(:latest_invoice).try(:charge)) && charge.try(:status) == "succeeded"
|
60
87
|
Pay::Stripe::Charge.sync(charge.id, object: charge)
|
@@ -71,32 +98,61 @@ module Pay
|
|
71
98
|
end
|
72
99
|
end
|
73
100
|
|
101
|
+
# Common expand options for all requests that create, retrieve, or update a Stripe Subscription
|
102
|
+
def self.expand_options
|
103
|
+
{expand: ["pending_setup_intent", "latest_invoice.payment_intent", "latest_invoice.charge.invoice"]}
|
104
|
+
end
|
105
|
+
|
74
106
|
def initialize(pay_subscription)
|
75
107
|
@pay_subscription = pay_subscription
|
76
108
|
end
|
77
109
|
|
78
110
|
def subscription(**options)
|
79
111
|
options[:id] = processor_id
|
80
|
-
|
81
|
-
::Stripe::Subscription.retrieve(options, {stripe_account: stripe_account}.compact)
|
112
|
+
@stripe_subscription ||= ::Stripe::Subscription.retrieve(options.merge(expand_options), {stripe_account: stripe_account}.compact)
|
82
113
|
end
|
83
114
|
|
84
|
-
def
|
85
|
-
|
86
|
-
|
115
|
+
def reload!
|
116
|
+
@stripe_subscription = nil
|
117
|
+
end
|
118
|
+
|
119
|
+
# Returns a SetupIntent or PaymentIntent client secret for the subscription
|
120
|
+
def client_secret
|
121
|
+
stripe_sub = subscription
|
122
|
+
stripe_sub&.pending_setup_intent&.client_secret || stripe_sub&.latest_invoice&.payment_intent&.client_secret
|
123
|
+
end
|
124
|
+
|
125
|
+
def cancel(**options)
|
126
|
+
@stripe_subscription = ::Stripe::Subscription.update(processor_id, {cancel_at_period_end: true}.merge(expand_options), stripe_options)
|
127
|
+
pay_subscription.update(ends_at: (on_trial? ? trial_ends_at : Time.at(@stripe_subscription.current_period_end)))
|
87
128
|
rescue ::Stripe::StripeError => e
|
88
129
|
raise Pay::Stripe::Error, e
|
89
130
|
end
|
90
131
|
|
91
|
-
|
92
|
-
|
132
|
+
# Cancels a subscription immediately
|
133
|
+
#
|
134
|
+
# cancel_now!(prorate: true)
|
135
|
+
# cancel_now!(invoice_now: true)
|
136
|
+
def cancel_now!(**options)
|
137
|
+
@stripe_subscription = ::Stripe::Subscription.delete(processor_id, options.merge(expand_options), stripe_options)
|
93
138
|
pay_subscription.update(ends_at: Time.current, status: :canceled)
|
94
139
|
rescue ::Stripe::StripeError => e
|
95
140
|
raise Pay::Stripe::Error, e
|
96
141
|
end
|
97
142
|
|
98
|
-
|
99
|
-
|
143
|
+
# This updates a SubscriptionItem's quantity in Stripe
|
144
|
+
#
|
145
|
+
# For a subscription with a single item, we can update the subscription directly if no SubscriptionItem ID is available
|
146
|
+
# Otherwise a SubscriptionItem ID is required so Stripe knows which entry to update
|
147
|
+
def change_quantity(quantity, **options)
|
148
|
+
subscription_item_id = options.fetch(:subscription_item_id, subscription_items.first["id"])
|
149
|
+
if subscription_item_id
|
150
|
+
::Stripe::SubscriptionItem.update(subscription_item_id, options.merge(quantity: quantity), stripe_options)
|
151
|
+
@stripe_subscription = nil
|
152
|
+
else
|
153
|
+
@stripe_subscription = ::Stripe::Subscription.update(processor_id, options.merge(quantity: quantity).merge(expand_options), stripe_options)
|
154
|
+
end
|
155
|
+
true
|
100
156
|
rescue ::Stripe::StripeError => e
|
101
157
|
raise Pay::Stripe::Error, e
|
102
158
|
end
|
@@ -106,27 +162,47 @@ module Pay
|
|
106
162
|
end
|
107
163
|
|
108
164
|
def paused?
|
109
|
-
|
165
|
+
pause_behavior.present?
|
110
166
|
end
|
111
167
|
|
112
|
-
|
113
|
-
|
168
|
+
# Pauses a Stripe subscription
|
169
|
+
#
|
170
|
+
# pause(behavior: "mark_uncollectible")
|
171
|
+
# pause(behavior: "keep_as_draft")
|
172
|
+
# pause(behavior: "void")
|
173
|
+
# pause(behavior: "mark_uncollectible", resumes_at: 1.month.from_now)
|
174
|
+
def pause(**options)
|
175
|
+
attributes = {pause_collection: options.reverse_merge(behavior: "mark_uncollectible")}
|
176
|
+
@stripe_subscription = ::Stripe::Subscription.update(processor_id, attributes.merge(expand_options), stripe_options)
|
177
|
+
pay_subscription.update(
|
178
|
+
pause_behavior: @stripe_subscription.pause_collection&.behavior,
|
179
|
+
pause_resumes_at: (@stripe_subscription.pause_collection&.resumes_at ? Time.at(@stripe_subscription.pause_collection&.resumes_at) : nil)
|
180
|
+
)
|
181
|
+
end
|
182
|
+
|
183
|
+
def unpause
|
184
|
+
@stripe_subscription = ::Stripe::Subscription.update(processor_id, {pause_collection: nil}.merge(expand_options), stripe_options)
|
185
|
+
pay_subscription.update(pause_behavior: nil, pause_resumes_at: nil)
|
114
186
|
end
|
115
187
|
|
116
188
|
def resume
|
117
|
-
unless on_grace_period?
|
189
|
+
unless on_grace_period? || paused?
|
118
190
|
raise StandardError, "You can only resume subscriptions within their grace period."
|
119
191
|
end
|
120
192
|
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
193
|
+
if paused?
|
194
|
+
unpause
|
195
|
+
else
|
196
|
+
@stripe_subscription = ::Stripe::Subscription.update(
|
197
|
+
processor_id,
|
198
|
+
{
|
199
|
+
plan: processor_plan,
|
200
|
+
trial_end: (on_trial? ? trial_ends_at.to_i : "now"),
|
201
|
+
cancel_at_period_end: false
|
202
|
+
}.merge(expand_options),
|
203
|
+
stripe_options
|
204
|
+
)
|
205
|
+
end
|
130
206
|
rescue ::Stripe::StripeError => e
|
131
207
|
raise Pay::Stripe::Error, e
|
132
208
|
end
|
@@ -134,7 +210,7 @@ module Pay
|
|
134
210
|
def swap(plan)
|
135
211
|
raise ArgumentError, "plan must be a string" unless plan.is_a?(String)
|
136
212
|
|
137
|
-
::Stripe::Subscription.update(
|
213
|
+
@stripe_subscription = ::Stripe::Subscription.update(
|
138
214
|
processor_id,
|
139
215
|
{
|
140
216
|
cancel_at_period_end: false,
|
@@ -142,19 +218,45 @@ module Pay
|
|
142
218
|
proration_behavior: (prorate ? "create_prorations" : "none"),
|
143
219
|
trial_end: (on_trial? ? trial_ends_at.to_i : "now"),
|
144
220
|
quantity: quantity
|
145
|
-
},
|
221
|
+
}.merge(expand_options),
|
146
222
|
stripe_options
|
147
223
|
)
|
148
224
|
rescue ::Stripe::StripeError => e
|
149
225
|
raise Pay::Stripe::Error, e
|
150
226
|
end
|
151
227
|
|
228
|
+
# Creates a metered billing usage record
|
229
|
+
#
|
230
|
+
# Uses the first subscription_item ID unless `subscription_item_id: "si_1234"` is passed
|
231
|
+
#
|
232
|
+
# create_usage_record(quantity: 4, action: :increment)
|
233
|
+
# create_usage_record(subscription_item_id: "si_1234", quantity: 100, action: :set)
|
234
|
+
def create_usage_record(**options)
|
235
|
+
subscription_item_id = options.fetch(:subscription_item_id, subscription_items.first["id"])
|
236
|
+
::Stripe::SubscriptionItem.create_usage_record(subscription_item_id, options, stripe_options)
|
237
|
+
end
|
238
|
+
|
239
|
+
# Returns usage record summaries for a subscription item
|
240
|
+
def usage_record_summaries(**options)
|
241
|
+
subscription_item_id = options.fetch(:subscription_item_id, subscription_items.first["id"])
|
242
|
+
::Stripe::SubscriptionItem.list_usage_record_summaries(subscription_item_id, options, stripe_options)
|
243
|
+
end
|
244
|
+
|
245
|
+
# Returns an upcoming invoice for a subscription
|
246
|
+
def upcoming_invoice(**options)
|
247
|
+
::Stripe::Invoice.upcoming(options.merge(subscription: processor_id), stripe_options)
|
248
|
+
end
|
249
|
+
|
152
250
|
private
|
153
251
|
|
154
252
|
# Options for Stripe requests
|
155
253
|
def stripe_options
|
156
254
|
{stripe_account: stripe_account}.compact
|
157
255
|
end
|
256
|
+
|
257
|
+
def expand_options
|
258
|
+
self.class.expand_options
|
259
|
+
end
|
158
260
|
end
|
159
261
|
end
|
160
262
|
end
|
@@ -5,8 +5,8 @@ module Pay
|
|
5
5
|
def call(event)
|
6
6
|
pay_charge = Pay::Stripe::Charge.sync(event.data.object.id, stripe_account: event.try(:account))
|
7
7
|
|
8
|
-
if pay_charge && Pay.
|
9
|
-
Pay
|
8
|
+
if pay_charge && Pay.send_email?(:refund, pay_charge)
|
9
|
+
Pay.mailer.with(pay_customer: pay_charge.customer, pay_charge: pay_charge).refund.deliver_later
|
10
10
|
end
|
11
11
|
end
|
12
12
|
end
|
@@ -5,8 +5,8 @@ module Pay
|
|
5
5
|
def call(event)
|
6
6
|
pay_charge = Pay::Stripe::Charge.sync(event.data.object.id, stripe_account: event.try(:account))
|
7
7
|
|
8
|
-
if pay_charge && Pay.
|
9
|
-
Pay
|
8
|
+
if pay_charge && Pay.send_email?(:receipt, pay_charge)
|
9
|
+
Pay.mailer.with(pay_customer: pay_charge.customer, pay_charge: pay_charge).receipt.deliver_later
|
10
10
|
end
|
11
11
|
end
|
12
12
|
end
|
@@ -8,14 +8,14 @@ module Pay
|
|
8
8
|
|
9
9
|
object = event.data.object
|
10
10
|
|
11
|
-
|
12
|
-
return if
|
11
|
+
pay_subscription = Pay::Subscription.find_by_processor_and_id(:stripe, object.subscription)
|
12
|
+
return if pay_subscription.nil?
|
13
13
|
|
14
|
-
if Pay.
|
15
|
-
Pay
|
16
|
-
pay_customer:
|
14
|
+
if Pay.send_email?(:payment_action_required, pay_subscription)
|
15
|
+
Pay.mailer.with(
|
16
|
+
pay_customer: pay_subscription.customer,
|
17
17
|
payment_intent_id: event.data.object.payment_intent,
|
18
|
-
|
18
|
+
pay_subscription: pay_subscription
|
19
19
|
).payment_action_required.deliver_later
|
20
20
|
end
|
21
21
|
end
|
@@ -8,20 +8,16 @@ module Pay
|
|
8
8
|
def call(event)
|
9
9
|
# Event is of type "invoice" see:
|
10
10
|
# https://stripe.com/docs/api/invoices/object
|
11
|
-
|
12
|
-
return unless
|
11
|
+
pay_subscription = Pay::Subscription.find_by_processor_and_id(:stripe, event.data.object.subscription)
|
12
|
+
return unless pay_subscription
|
13
13
|
|
14
14
|
# Stripe subscription items all have the same interval
|
15
15
|
price = event.data.object.lines.data.first.price
|
16
|
-
return unless price.type == "recurring"
|
17
16
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
Pay::UserMailer.with(
|
23
|
-
pay_customer: subscription.customer,
|
24
|
-
subscription: subscription,
|
17
|
+
if Pay.send_email?(:subscription_renewing, pay_subscription, price)
|
18
|
+
Pay.mailer.with(
|
19
|
+
pay_customer: pay_subscription.customer,
|
20
|
+
pay_subscription: pay_subscription,
|
25
21
|
date: Time.zone.at(event.data.object.next_payment_attempt)
|
26
22
|
).subscription_renewing.deliver_later
|
27
23
|
end
|