pay 2.2.0 → 2.4.0
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 +161 -11
- data/Rakefile +2 -4
- data/app/controllers/pay/payments_controller.rb +3 -0
- data/app/controllers/pay/webhooks/braintree_controller.rb +1 -1
- data/app/controllers/pay/webhooks/paddle_controller.rb +36 -0
- data/app/mailers/pay/user_mailer.rb +14 -35
- data/app/models/pay/application_record.rb +6 -1
- data/app/models/pay/charge.rb +7 -0
- data/app/models/pay/subscription.rb +27 -4
- data/app/views/pay/payments/show.html.erb +1 -1
- data/app/views/pay/user_mailer/payment_action_required.html.erb +1 -1
- data/app/views/pay/user_mailer/receipt.html.erb +6 -6
- data/app/views/pay/user_mailer/refund.html.erb +6 -6
- data/app/views/pay/user_mailer/subscription_renewing.html.erb +1 -1
- data/config/locales/en.yml +137 -0
- data/config/routes.rb +1 -0
- data/db/migrate/20200603134434_add_data_to_pay_models.rb +17 -0
- data/lib/generators/active_record/pay_generator.rb +1 -1
- data/lib/generators/pay/orm_helpers.rb +1 -2
- data/lib/pay.rb +9 -41
- data/lib/pay/billable.rb +16 -11
- data/lib/pay/braintree/billable.rb +25 -19
- data/lib/pay/braintree/charge.rb +7 -3
- data/lib/pay/braintree/subscription.rb +15 -5
- data/lib/pay/engine.rb +7 -0
- data/lib/pay/errors.rb +73 -0
- data/lib/pay/paddle.rb +38 -0
- data/lib/pay/paddle/billable.rb +95 -0
- data/lib/pay/paddle/charge.rb +39 -0
- data/lib/pay/paddle/subscription.rb +70 -0
- data/lib/pay/paddle/webhooks.rb +1 -0
- data/lib/pay/paddle/webhooks/signature_verifier.rb +115 -0
- data/lib/pay/paddle/webhooks/subscription_cancelled.rb +18 -0
- data/lib/pay/paddle/webhooks/subscription_created.rb +59 -0
- data/lib/pay/paddle/webhooks/subscription_payment_refunded.rb +21 -0
- data/lib/pay/paddle/webhooks/subscription_payment_succeeded.rb +43 -0
- data/lib/pay/paddle/webhooks/subscription_updated.rb +37 -0
- data/lib/pay/receipts.rb +6 -6
- data/lib/pay/stripe.rb +1 -1
- data/lib/pay/stripe/billable.rb +12 -6
- data/lib/pay/stripe/charge.rb +6 -2
- data/lib/pay/stripe/subscription.rb +15 -5
- data/lib/pay/stripe/webhooks/charge_refunded.rb +2 -2
- data/lib/pay/stripe/webhooks/charge_succeeded.rb +7 -7
- data/lib/pay/stripe/webhooks/payment_action_required.rb +7 -8
- data/lib/pay/stripe/webhooks/subscription_created.rb +1 -1
- data/lib/pay/stripe/webhooks/subscription_renewing.rb +4 -3
- data/lib/pay/version.rb +1 -1
- metadata +35 -43
@@ -0,0 +1,70 @@
|
|
1
|
+
module Pay
|
2
|
+
module Paddle
|
3
|
+
module Subscription
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
scope :paddle, -> { where(processor: :paddle) }
|
8
|
+
|
9
|
+
store_accessor :data, :paddle_update_url
|
10
|
+
store_accessor :data, :paddle_cancel_url
|
11
|
+
store_accessor :data, :paddle_paused_from
|
12
|
+
end
|
13
|
+
|
14
|
+
def paddle?
|
15
|
+
processor == "paddle"
|
16
|
+
end
|
17
|
+
|
18
|
+
def paddle_cancel
|
19
|
+
subscription = processor_subscription
|
20
|
+
PaddlePay::Subscription::User.cancel(processor_id)
|
21
|
+
if on_trial?
|
22
|
+
update(status: :canceled, ends_at: trial_ends_at)
|
23
|
+
else
|
24
|
+
update(status: :canceled, ends_at: Time.zone.parse(subscription.next_payment[:date]))
|
25
|
+
end
|
26
|
+
rescue ::PaddlePay::PaddlePayError => e
|
27
|
+
raise Pay::Paddle::Error, e
|
28
|
+
end
|
29
|
+
|
30
|
+
def paddle_cancel_now!
|
31
|
+
PaddlePay::Subscription::User.cancel(processor_id)
|
32
|
+
update(status: :canceled, ends_at: Time.zone.now)
|
33
|
+
rescue ::PaddlePay::PaddlePayError => e
|
34
|
+
raise Pay::Paddle::Error, e
|
35
|
+
end
|
36
|
+
|
37
|
+
def paddle_on_grace_period?
|
38
|
+
canceled? && Time.zone.now < ends_at || paused? && Time.zone.now < paddle_paused_from
|
39
|
+
end
|
40
|
+
|
41
|
+
def paddle_paused?
|
42
|
+
paddle_paused_from.present?
|
43
|
+
end
|
44
|
+
|
45
|
+
def paddle_pause
|
46
|
+
attributes = {pause: true}
|
47
|
+
response = PaddlePay::Subscription::User.update(processor_id, attributes)
|
48
|
+
update(paddle_paused_from: Time.zone.parse(response[:next_payment][:date]))
|
49
|
+
rescue ::PaddlePay::PaddlePayError => e
|
50
|
+
raise Pay::Paddle::Error, e
|
51
|
+
end
|
52
|
+
|
53
|
+
def paddle_resume
|
54
|
+
attributes = {pause: false}
|
55
|
+
PaddlePay::Subscription::User.update(processor_id, attributes)
|
56
|
+
update(status: :active, paddle_paused_from: nil)
|
57
|
+
rescue ::PaddlePay::PaddlePayError => e
|
58
|
+
raise Pay::Paddle::Error, e
|
59
|
+
end
|
60
|
+
|
61
|
+
def paddle_swap(plan)
|
62
|
+
attributes = {plan_id: plan, prorate: prorate}
|
63
|
+
attributes[:quantity] = quantity if quantity?
|
64
|
+
PaddlePay::Subscription::User.update(processor_id, attributes)
|
65
|
+
rescue ::PaddlePay::PaddlePayError => e
|
66
|
+
raise Pay::Paddle::Error, e
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
Dir[File.join(__dir__, "webhooks", "**", "*.rb")].sort.each { |file| require file }
|
@@ -0,0 +1,115 @@
|
|
1
|
+
require "base64"
|
2
|
+
require "json"
|
3
|
+
require "openssl"
|
4
|
+
|
5
|
+
module Pay
|
6
|
+
module Paddle
|
7
|
+
module Webhooks
|
8
|
+
class SignatureVerifier
|
9
|
+
def initialize(data)
|
10
|
+
@data = data
|
11
|
+
@public_key_base64 = Pay::Paddle.public_key_base64
|
12
|
+
end
|
13
|
+
|
14
|
+
def verify
|
15
|
+
data = @data
|
16
|
+
public_key = Base64.decode64(@public_key_base64) if @public_key_base64
|
17
|
+
return false unless data && data["p_signature"] && public_key
|
18
|
+
|
19
|
+
# 'data' represents all of the POST fields sent with the request.
|
20
|
+
# Get the p_signature parameter & base64 decode it.
|
21
|
+
signature = Base64.decode64(data["p_signature"])
|
22
|
+
|
23
|
+
# Remove the p_signature parameter
|
24
|
+
data.delete("p_signature")
|
25
|
+
|
26
|
+
# Ensure all the data fields are strings
|
27
|
+
data.each { |key, value| data[key] = String(value) }
|
28
|
+
|
29
|
+
# Sort the data
|
30
|
+
data_sorted = data.sort_by { |key, value| key }
|
31
|
+
|
32
|
+
# and serialize the fields
|
33
|
+
# serialization library is available here: https://github.com/jqr/php-serialize
|
34
|
+
data_serialized = serialize(data_sorted, true)
|
35
|
+
|
36
|
+
# verify the data
|
37
|
+
digest = OpenSSL::Digest.new("SHA1")
|
38
|
+
pub_key = OpenSSL::PKey::RSA.new(public_key)
|
39
|
+
pub_key.verify(digest, signature, data_serialized)
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
# https://github.com/jqr/php-serialize/blob/master/lib/php_serialize.rb
|
45
|
+
#
|
46
|
+
# Returns a string representing the argument in a form PHP.unserialize
|
47
|
+
# and PHP's unserialize() should both be able to load.
|
48
|
+
#
|
49
|
+
# string = PHP.serialize(mixed var[, bool assoc])
|
50
|
+
#
|
51
|
+
# Array, Hash, Fixnum, Float, True/FalseClass, NilClass, String and Struct
|
52
|
+
# are supported; as are objects which support the to_assoc method, which
|
53
|
+
# returns an array of the form [['attr_name', 'value']..]. Anything else
|
54
|
+
# will raise a TypeError.
|
55
|
+
#
|
56
|
+
# If 'assoc' is specified, Array's who's first element is a two value
|
57
|
+
# array will be assumed to be an associative array, and will be serialized
|
58
|
+
# as a PHP associative array rather than a multidimensional array.
|
59
|
+
def serialize(var, assoc = false)
|
60
|
+
s = ""
|
61
|
+
case var
|
62
|
+
when Array
|
63
|
+
s << "a:#{var.size}:{"
|
64
|
+
if assoc && var.first.is_a?(Array) && (var.first.size == 2)
|
65
|
+
var.each do |k, v|
|
66
|
+
s << serialize(k, assoc) << serialize(v, assoc)
|
67
|
+
end
|
68
|
+
else
|
69
|
+
var.each_with_index do |v, i|
|
70
|
+
s << "i:#{i};#{serialize(v, assoc)}"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
s << "}"
|
74
|
+
when Hash
|
75
|
+
s << "a:#{var.size}:{"
|
76
|
+
var.each do |k, v|
|
77
|
+
s << "#{serialize(k, assoc)}#{serialize(v, assoc)}"
|
78
|
+
end
|
79
|
+
s << "}"
|
80
|
+
when Struct
|
81
|
+
# encode as Object with same name
|
82
|
+
s << "O:#{var.class.to_s.bytesize}:\"#{var.class.to_s.downcase}\":#{var.members.length}:{"
|
83
|
+
var.members.each do |member|
|
84
|
+
s << "#{serialize(member, assoc)}#{serialize(var[member], assoc)}"
|
85
|
+
end
|
86
|
+
s << "}"
|
87
|
+
when String, Symbol
|
88
|
+
s << "s:#{var.to_s.bytesize}:\"#{var}\";"
|
89
|
+
when Integer
|
90
|
+
s << "i:#{var};"
|
91
|
+
when Float
|
92
|
+
s << "d:#{var};"
|
93
|
+
when NilClass
|
94
|
+
s << "N;"
|
95
|
+
when FalseClass, TrueClass
|
96
|
+
s << "b:#{var ? 1 : 0};"
|
97
|
+
else
|
98
|
+
if var.respond_to?(:to_assoc)
|
99
|
+
v = var.to_assoc
|
100
|
+
# encode as Object with same name
|
101
|
+
s << "O:#{var.class.to_s.bytesize}:\"#{var.class.to_s.downcase}\":#{v.length}:{"
|
102
|
+
v.each do |k, v|
|
103
|
+
s << "#{serialize(k.to_s, assoc)}#{serialize(v, assoc)}"
|
104
|
+
end
|
105
|
+
s << "}"
|
106
|
+
else
|
107
|
+
raise TypeError, "Unable to serialize type #{var.class}"
|
108
|
+
end
|
109
|
+
end
|
110
|
+
s
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Pay
|
2
|
+
module Paddle
|
3
|
+
module Webhooks
|
4
|
+
class SubscriptionCancelled
|
5
|
+
def initialize(data)
|
6
|
+
subscription = Pay.subscription_model.find_by(processor: :paddle, processor_id: data["subscription_id"])
|
7
|
+
|
8
|
+
# We couldn't find the subscription for some reason, maybe it's from another service
|
9
|
+
return if subscription.nil?
|
10
|
+
|
11
|
+
# User canceled subscriptions have an ends_at
|
12
|
+
# Automatically canceled subscriptions need this value set
|
13
|
+
subscription.update!(ends_at: Time.zone.parse(data["cancellation_effective_date"])) if subscription.ends_at.blank? && data["cancellation_effective_date"].present?
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Pay
|
2
|
+
module Paddle
|
3
|
+
module Webhooks
|
4
|
+
class SubscriptionCreated
|
5
|
+
def initialize(data)
|
6
|
+
# We may already have the subscription in the database, so we can update that record
|
7
|
+
subscription = Pay.subscription_model.find_by(processor: :paddle, processor_id: data["subscription_id"])
|
8
|
+
|
9
|
+
# Create the subscription in the database if we don't have it already
|
10
|
+
if subscription.nil?
|
11
|
+
|
12
|
+
# The customer could already be in the database
|
13
|
+
owner = Pay.find_billable(processor: :paddle, processor_id: data["user_id"])
|
14
|
+
|
15
|
+
if owner.nil?
|
16
|
+
owner = owner_by_passtrough(data["passthrough"], data["subscription_plan_id"])
|
17
|
+
owner&.update!(processor: "paddle", processor_id: data["user_id"])
|
18
|
+
end
|
19
|
+
|
20
|
+
if owner.nil?
|
21
|
+
Rails.logger.error("[Pay] Unable to find Pay::Billable with owner: '#{data["passthrough"]}'. Searched these models: #{Pay.billable_models.join(", ")}")
|
22
|
+
return
|
23
|
+
end
|
24
|
+
|
25
|
+
subscription = Pay.subscription_model.new(owner: owner, name: Pay.default_product_name, processor: "paddle", processor_id: data["subscription_id"], status: :active)
|
26
|
+
end
|
27
|
+
|
28
|
+
subscription.quantity = data["quantity"]
|
29
|
+
subscription.processor_plan = data["subscription_plan_id"]
|
30
|
+
subscription.paddle_update_url = data["update_url"]
|
31
|
+
subscription.paddle_cancel_url = data["cancel_url"]
|
32
|
+
subscription.trial_ends_at = Time.zone.parse(data["next_bill_date"]) if data["status"] == "trialing"
|
33
|
+
|
34
|
+
# If user was on trial, their subscription ends at the end of the trial
|
35
|
+
subscription.ends_at = if ["paused", "deleted"].include?(data["status"]) && subscription.on_trial?
|
36
|
+
subscription.trial_ends_at
|
37
|
+
|
38
|
+
# User wasn't on trial, so subscription ends at period end
|
39
|
+
elsif ["paused", "deleted"].include?(data["status"])
|
40
|
+
Time.zone.parse(data["next_bill_date"])
|
41
|
+
|
42
|
+
# Subscription isn't marked to cancel at period end
|
43
|
+
end
|
44
|
+
|
45
|
+
subscription.save!
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def owner_by_passtrough(passthrough, product_id)
|
51
|
+
passthrough_json = JSON.parse(passthrough)
|
52
|
+
GlobalID::Locator.locate_signed(passthrough_json["owner_sgid"])
|
53
|
+
rescue JSON::ParserError
|
54
|
+
nil
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Pay
|
2
|
+
module Paddle
|
3
|
+
module Webhooks
|
4
|
+
class SubscriptionPaymentRefunded
|
5
|
+
def initialize(data)
|
6
|
+
charge = Pay.charge_model.find_by(processor: :paddle, processor_id: data["subscription_payment_id"])
|
7
|
+
return unless charge.present?
|
8
|
+
|
9
|
+
charge.update(amount_refunded: Integer(data["gross_refund"].to_f * 100))
|
10
|
+
notify_user(charge.owner, charge)
|
11
|
+
end
|
12
|
+
|
13
|
+
def notify_user(billable, charge)
|
14
|
+
if Pay.send_emails
|
15
|
+
Pay::UserMailer.with(billable: billable, charge: charge).refund.deliver_later
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Pay
|
2
|
+
module Paddle
|
3
|
+
module Webhooks
|
4
|
+
class SubscriptionPaymentSucceeded
|
5
|
+
def initialize(data)
|
6
|
+
billable = Pay.find_billable(processor: :paddle, processor_id: data["user_id"])
|
7
|
+
return unless billable.present?
|
8
|
+
return if billable.charges.where(processor_id: data["subscription_payment_id"]).any?
|
9
|
+
|
10
|
+
charge = create_charge(billable, data)
|
11
|
+
notify_user(billable, charge)
|
12
|
+
end
|
13
|
+
|
14
|
+
def create_charge(user, data)
|
15
|
+
charge = user.charges.find_or_initialize_by(
|
16
|
+
processor: :paddle,
|
17
|
+
processor_id: data["subscription_payment_id"]
|
18
|
+
)
|
19
|
+
|
20
|
+
params = {
|
21
|
+
amount: Integer(data["sale_gross"].to_f * 100),
|
22
|
+
card_type: data["payment_method"],
|
23
|
+
paddle_receipt_url: data["receipt_url"],
|
24
|
+
created_at: Time.zone.parse(data["event_time"])
|
25
|
+
}
|
26
|
+
|
27
|
+
payment_information = user.paddle_payment_information(data["subscription_id"])
|
28
|
+
|
29
|
+
charge.update(params.merge(payment_information))
|
30
|
+
user.update(payment_information)
|
31
|
+
|
32
|
+
charge
|
33
|
+
end
|
34
|
+
|
35
|
+
def notify_user(billable, charge)
|
36
|
+
if Pay.send_emails && charge.respond_to?(:receipt)
|
37
|
+
Pay::UserMailer.with(billable: billable, charge: charge).receipt.deliver_later
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Pay
|
2
|
+
module Paddle
|
3
|
+
module Webhooks
|
4
|
+
class SubscriptionUpdated
|
5
|
+
def initialize(data)
|
6
|
+
subscription = Pay.subscription_model.find_by(processor: :paddle, processor_id: data["subscription_id"])
|
7
|
+
|
8
|
+
return if subscription.nil?
|
9
|
+
|
10
|
+
case data["status"]
|
11
|
+
when "deleted"
|
12
|
+
subscription.status = "canceled"
|
13
|
+
subscription.ends_at = Time.zone.parse(data["next_bill_date"]) || Time.zone.now if subscription.ends_at.blank?
|
14
|
+
when "trialing"
|
15
|
+
subscription.status = "trialing"
|
16
|
+
subscription.trial_ends_at = Time.zone.parse(data["next_bill_date"])
|
17
|
+
when "active"
|
18
|
+
subscription.status = "active"
|
19
|
+
subscription.paddle_paused_from = Time.zone.parse(data["paused_from"]) if data["paused_from"].present?
|
20
|
+
else
|
21
|
+
subscription.status = data["status"]
|
22
|
+
end
|
23
|
+
|
24
|
+
subscription.quantity = data["new_quantity"]
|
25
|
+
subscription.processor_plan = data["subscription_plan_id"]
|
26
|
+
subscription.paddle_update_url = data["update_url"]
|
27
|
+
subscription.paddle_cancel_url = data["cancel_url"]
|
28
|
+
|
29
|
+
# If user was on trial, their subscription ends at the end of the trial
|
30
|
+
subscription.ends_at = subscription.trial_ends_at if subscription.on_trial?
|
31
|
+
|
32
|
+
subscription.save!
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/lib/pay/receipts.rb
CHANGED
@@ -28,13 +28,13 @@ module Pay
|
|
28
28
|
|
29
29
|
def line_items
|
30
30
|
line_items = [
|
31
|
-
["
|
32
|
-
["
|
33
|
-
["
|
34
|
-
["
|
35
|
-
["
|
31
|
+
[I18n.t("receipt.date"), created_at.to_s],
|
32
|
+
[I18n.t("receipt.account_billed"), "#{owner.name} (#{owner.email})"],
|
33
|
+
[I18n.t("receipt.product"), product],
|
34
|
+
[I18n.t("receipt.amount"), ActionController::Base.helpers.number_to_currency(amount / 100.0)],
|
35
|
+
[I18n.t("receipt.charged_to"), charged_to]
|
36
36
|
]
|
37
|
-
line_items << ["
|
37
|
+
line_items << [I18n.t("receipt.additional_info"), owner.extra_billing_info] if owner.extra_billing_info?
|
38
38
|
line_items
|
39
39
|
end
|
40
40
|
end
|
data/lib/pay/stripe.rb
CHANGED
data/lib/pay/stripe/billable.rb
CHANGED
@@ -1,6 +1,12 @@
|
|
1
1
|
module Pay
|
2
2
|
module Stripe
|
3
3
|
module Billable
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
scope :stripe, -> { where(processor: :stripe) }
|
8
|
+
end
|
9
|
+
|
4
10
|
# Handles Billable#customer
|
5
11
|
#
|
6
12
|
# Returns Stripe::Customer
|
@@ -11,7 +17,7 @@ module Pay
|
|
11
17
|
create_stripe_customer
|
12
18
|
end
|
13
19
|
rescue ::Stripe::StripeError => e
|
14
|
-
raise Error, e
|
20
|
+
raise Pay::Stripe::Error, e
|
15
21
|
end
|
16
22
|
|
17
23
|
def create_setup_intent
|
@@ -41,7 +47,7 @@ module Pay
|
|
41
47
|
# Create a new charge object
|
42
48
|
Stripe::Webhooks::ChargeSucceeded.new.create_charge(self, payment_intent.charges.first)
|
43
49
|
rescue ::Stripe::StripeError => e
|
44
|
-
raise Error, e
|
50
|
+
raise Pay::Stripe::Error, e
|
45
51
|
end
|
46
52
|
|
47
53
|
# Handles Billable#subscribe
|
@@ -74,7 +80,7 @@ module Pay
|
|
74
80
|
|
75
81
|
subscription
|
76
82
|
rescue ::Stripe::StripeError => e
|
77
|
-
raise Error, e
|
83
|
+
raise Pay::Stripe::Error, e
|
78
84
|
end
|
79
85
|
|
80
86
|
# Handles Billable#update_card
|
@@ -91,13 +97,13 @@ module Pay
|
|
91
97
|
update_stripe_card_on_file(payment_method.card)
|
92
98
|
true
|
93
99
|
rescue ::Stripe::StripeError => e
|
94
|
-
raise Error, e
|
100
|
+
raise Pay::Stripe::Error, e
|
95
101
|
end
|
96
102
|
|
97
103
|
def update_stripe_email!
|
98
104
|
customer = stripe_customer
|
99
105
|
customer.email = email
|
100
|
-
customer.
|
106
|
+
customer.name = customer_name
|
101
107
|
customer.save
|
102
108
|
end
|
103
109
|
|
@@ -137,7 +143,7 @@ module Pay
|
|
137
143
|
private
|
138
144
|
|
139
145
|
def create_stripe_customer
|
140
|
-
customer = ::Stripe::Customer.create(email: email,
|
146
|
+
customer = ::Stripe::Customer.create(email: email, name: customer_name)
|
141
147
|
update(processor: "stripe", processor_id: customer.id)
|
142
148
|
|
143
149
|
# Update the user's card on file if a token was passed in
|