pay 2.4.2 → 2.6.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 +3 -4
- data/app/controllers/pay/webhooks/braintree_controller.rb +7 -53
- data/app/controllers/pay/webhooks/paddle_controller.rb +19 -18
- data/app/controllers/pay/webhooks/stripe_controller.rb +47 -0
- data/app/models/pay/charge.rb +22 -3
- data/app/models/pay/subscription.rb +23 -24
- data/app/views/pay/stripe/_checkout_button.html.erb +21 -0
- data/config/routes.rb +1 -1
- data/lib/pay.rb +13 -12
- data/lib/pay/billable.rb +33 -33
- data/lib/pay/billable/sync_email.rb +1 -1
- data/lib/pay/braintree.rb +34 -15
- data/lib/pay/braintree/authorization_error.rb +9 -0
- data/lib/pay/braintree/billable.rb +33 -30
- data/lib/pay/braintree/charge.rb +8 -10
- data/lib/pay/braintree/error.rb +9 -0
- data/lib/pay/braintree/subscription.rb +34 -15
- data/lib/pay/braintree/webhooks/subscription_canceled.rb +19 -0
- data/lib/pay/braintree/webhooks/subscription_charged_successfully.rb +24 -0
- data/lib/pay/braintree/webhooks/subscription_charged_unsuccessfully.rb +24 -0
- data/lib/pay/braintree/webhooks/subscription_expired.rb +19 -0
- data/lib/pay/braintree/webhooks/subscription_trial_ended.rb +19 -0
- data/lib/pay/braintree/webhooks/subscription_went_active.rb +19 -0
- data/lib/pay/braintree/webhooks/subscription_went_past_due.rb +19 -0
- data/lib/pay/engine.rb +0 -23
- data/lib/pay/errors.rb +0 -44
- data/lib/pay/paddle.rb +30 -16
- data/lib/pay/paddle/billable.rb +26 -22
- data/lib/pay/paddle/charge.rb +8 -12
- data/lib/pay/paddle/error.rb +9 -0
- data/lib/pay/paddle/subscription.rb +29 -18
- data/lib/pay/paddle/webhooks/subscription_cancelled.rb +3 -3
- data/lib/pay/paddle/webhooks/subscription_created.rb +15 -15
- data/lib/pay/paddle/webhooks/subscription_payment_refunded.rb +3 -3
- data/lib/pay/paddle/webhooks/subscription_payment_succeeded.rb +11 -11
- data/lib/pay/paddle/webhooks/subscription_updated.rb +11 -11
- data/lib/pay/stripe.rb +65 -15
- data/lib/pay/stripe/billable.rb +136 -69
- data/lib/pay/stripe/charge.rb +9 -15
- data/lib/pay/stripe/error.rb +9 -0
- data/lib/pay/stripe/subscription.rb +27 -11
- data/lib/pay/stripe/webhooks/charge_succeeded.rb +1 -20
- data/lib/pay/stripe/webhooks/subscription_created.rb +1 -0
- data/lib/pay/version.rb +1 -1
- data/lib/pay/webhooks.rb +13 -0
- data/lib/pay/webhooks/delegator.rb +61 -0
- metadata +20 -69
- data/lib/pay/paddle/webhooks.rb +0 -1
- data/lib/pay/stripe/webhooks.rb +0 -39
@@ -2,15 +2,15 @@ module Pay
|
|
2
2
|
module Paddle
|
3
3
|
module Webhooks
|
4
4
|
class SubscriptionCancelled
|
5
|
-
def
|
6
|
-
subscription = Pay.subscription_model.find_by(processor: :paddle, processor_id:
|
5
|
+
def call(event)
|
6
|
+
subscription = Pay.subscription_model.find_by(processor: :paddle, processor_id: event["subscription_id"])
|
7
7
|
|
8
8
|
# We couldn't find the subscription for some reason, maybe it's from another service
|
9
9
|
return if subscription.nil?
|
10
10
|
|
11
11
|
# User canceled subscriptions have an ends_at
|
12
12
|
# Automatically canceled subscriptions need this value set
|
13
|
-
subscription.update!(ends_at: Time.zone.parse(
|
13
|
+
subscription.update!(ends_at: Time.zone.parse(event["cancellation_effective_date"])) if subscription.ends_at.blank? && event["cancellation_effective_date"].present?
|
14
14
|
end
|
15
15
|
end
|
16
16
|
end
|
@@ -2,42 +2,42 @@ module Pay
|
|
2
2
|
module Paddle
|
3
3
|
module Webhooks
|
4
4
|
class SubscriptionCreated
|
5
|
-
def
|
5
|
+
def call(event)
|
6
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:
|
7
|
+
subscription = Pay.subscription_model.find_by(processor: :paddle, processor_id: event["subscription_id"])
|
8
8
|
|
9
9
|
# Create the subscription in the database if we don't have it already
|
10
10
|
if subscription.nil?
|
11
11
|
|
12
12
|
# The customer could already be in the database
|
13
|
-
owner = Pay.find_billable(processor: :paddle, processor_id:
|
13
|
+
owner = Pay.find_billable(processor: :paddle, processor_id: event["user_id"])
|
14
14
|
|
15
15
|
if owner.nil?
|
16
|
-
owner = owner_by_passtrough(
|
17
|
-
owner&.update!(processor: "paddle", processor_id:
|
16
|
+
owner = owner_by_passtrough(event["passthrough"], event["subscription_plan_id"])
|
17
|
+
owner&.update!(processor: "paddle", processor_id: event["user_id"])
|
18
18
|
end
|
19
19
|
|
20
20
|
if owner.nil?
|
21
|
-
Rails.logger.error("[Pay] Unable to find Pay::Billable with owner: '#{
|
21
|
+
Rails.logger.error("[Pay] Unable to find Pay::Billable with owner: '#{event["passthrough"]}'. Searched these models: #{Pay.billable_models.join(", ")}")
|
22
22
|
return
|
23
23
|
end
|
24
24
|
|
25
|
-
subscription = Pay.subscription_model.new(owner: owner, name: Pay.default_product_name, processor: "paddle", processor_id:
|
25
|
+
subscription = Pay.subscription_model.new(owner: owner, name: Pay.default_product_name, processor: "paddle", processor_id: event["subscription_id"], status: :active)
|
26
26
|
end
|
27
27
|
|
28
|
-
subscription.quantity =
|
29
|
-
subscription.processor_plan =
|
30
|
-
subscription.paddle_update_url =
|
31
|
-
subscription.paddle_cancel_url =
|
32
|
-
subscription.trial_ends_at = Time.zone.parse(
|
28
|
+
subscription.quantity = event["quantity"]
|
29
|
+
subscription.processor_plan = event["subscription_plan_id"]
|
30
|
+
subscription.paddle_update_url = event["update_url"]
|
31
|
+
subscription.paddle_cancel_url = event["cancel_url"]
|
32
|
+
subscription.trial_ends_at = Time.zone.parse(event["next_bill_date"]) if event["status"] == "trialing"
|
33
33
|
|
34
34
|
# If user was on trial, their subscription ends at the end of the trial
|
35
|
-
subscription.ends_at = if ["paused", "deleted"].include?(
|
35
|
+
subscription.ends_at = if ["paused", "deleted"].include?(event["status"]) && subscription.on_trial?
|
36
36
|
subscription.trial_ends_at
|
37
37
|
|
38
38
|
# User wasn't on trial, so subscription ends at period end
|
39
|
-
elsif ["paused", "deleted"].include?(
|
40
|
-
Time.zone.parse(
|
39
|
+
elsif ["paused", "deleted"].include?(event["status"])
|
40
|
+
Time.zone.parse(event["next_bill_date"])
|
41
41
|
|
42
42
|
# Subscription isn't marked to cancel at period end
|
43
43
|
end
|
@@ -2,11 +2,11 @@ module Pay
|
|
2
2
|
module Paddle
|
3
3
|
module Webhooks
|
4
4
|
class SubscriptionPaymentRefunded
|
5
|
-
def
|
6
|
-
charge = Pay.charge_model.find_by(processor: :paddle, processor_id:
|
5
|
+
def call(event)
|
6
|
+
charge = Pay.charge_model.find_by(processor: :paddle, processor_id: event["subscription_payment_id"])
|
7
7
|
return unless charge.present?
|
8
8
|
|
9
|
-
charge.update(amount_refunded: Integer(
|
9
|
+
charge.update(amount_refunded: Integer(event["gross_refund"].to_f * 100))
|
10
10
|
notify_user(charge.owner, charge)
|
11
11
|
end
|
12
12
|
|
@@ -2,29 +2,29 @@ module Pay
|
|
2
2
|
module Paddle
|
3
3
|
module Webhooks
|
4
4
|
class SubscriptionPaymentSucceeded
|
5
|
-
def
|
6
|
-
billable = Pay.find_billable(processor: :paddle, processor_id:
|
5
|
+
def call(event)
|
6
|
+
billable = Pay.find_billable(processor: :paddle, processor_id: event["user_id"])
|
7
7
|
return unless billable.present?
|
8
|
-
return if billable.charges.where(processor_id:
|
8
|
+
return if billable.charges.where(processor_id: event["subscription_payment_id"]).any?
|
9
9
|
|
10
|
-
charge = create_charge(billable,
|
10
|
+
charge = create_charge(billable, event)
|
11
11
|
notify_user(billable, charge)
|
12
12
|
end
|
13
13
|
|
14
|
-
def create_charge(user,
|
14
|
+
def create_charge(user, event)
|
15
15
|
charge = user.charges.find_or_initialize_by(
|
16
16
|
processor: :paddle,
|
17
|
-
processor_id:
|
17
|
+
processor_id: event["subscription_payment_id"]
|
18
18
|
)
|
19
19
|
|
20
20
|
params = {
|
21
|
-
amount: Integer(
|
22
|
-
card_type:
|
23
|
-
paddle_receipt_url:
|
24
|
-
created_at: Time.zone.parse(
|
21
|
+
amount: Integer(event["sale_gross"].to_f * 100),
|
22
|
+
card_type: event["payment_method"],
|
23
|
+
paddle_receipt_url: event["receipt_url"],
|
24
|
+
created_at: Time.zone.parse(event["event_time"])
|
25
25
|
}
|
26
26
|
|
27
|
-
payment_information = user.
|
27
|
+
payment_information = Pay::Paddle::Billable.new(user).payment_information(event["subscription_id"])
|
28
28
|
|
29
29
|
charge.update(params.merge(payment_information))
|
30
30
|
user.update(payment_information)
|
@@ -2,29 +2,29 @@ module Pay
|
|
2
2
|
module Paddle
|
3
3
|
module Webhooks
|
4
4
|
class SubscriptionUpdated
|
5
|
-
def
|
6
|
-
subscription = Pay.subscription_model.find_by(processor: :paddle, processor_id:
|
5
|
+
def call(event)
|
6
|
+
subscription = Pay.subscription_model.find_by(processor: :paddle, processor_id: event["subscription_id"])
|
7
7
|
|
8
8
|
return if subscription.nil?
|
9
9
|
|
10
|
-
case
|
10
|
+
case event["status"]
|
11
11
|
when "deleted"
|
12
12
|
subscription.status = "canceled"
|
13
|
-
subscription.ends_at = Time.zone.parse(
|
13
|
+
subscription.ends_at = Time.zone.parse(event["next_bill_date"]) || Time.zone.now if subscription.ends_at.blank?
|
14
14
|
when "trialing"
|
15
15
|
subscription.status = "trialing"
|
16
|
-
subscription.trial_ends_at = Time.zone.parse(
|
16
|
+
subscription.trial_ends_at = Time.zone.parse(event["next_bill_date"])
|
17
17
|
when "active"
|
18
18
|
subscription.status = "active"
|
19
|
-
subscription.paddle_paused_from = Time.zone.parse(
|
19
|
+
subscription.paddle_paused_from = Time.zone.parse(event["paused_from"]) if event["paused_from"].present?
|
20
20
|
else
|
21
|
-
subscription.status =
|
21
|
+
subscription.status = event["status"]
|
22
22
|
end
|
23
23
|
|
24
|
-
subscription.quantity =
|
25
|
-
subscription.processor_plan =
|
26
|
-
subscription.paddle_update_url =
|
27
|
-
subscription.paddle_cancel_url =
|
24
|
+
subscription.quantity = event["new_quantity"]
|
25
|
+
subscription.processor_plan = event["subscription_plan_id"]
|
26
|
+
subscription.paddle_update_url = event["update_url"]
|
27
|
+
subscription.paddle_cancel_url = event["cancel_url"]
|
28
28
|
|
29
29
|
# If user was on trial, their subscription ends at the end of the trial
|
30
30
|
subscription.ends_at = subscription.trial_ends_at if subscription.on_trial?
|
data/lib/pay/stripe.rb
CHANGED
@@ -1,34 +1,84 @@
|
|
1
|
-
require "pay/env"
|
2
|
-
require "pay/stripe/billable"
|
3
|
-
require "pay/stripe/charge"
|
4
|
-
require "pay/stripe/subscription"
|
5
|
-
require "pay/stripe/webhooks"
|
6
|
-
|
7
1
|
module Pay
|
8
2
|
module Stripe
|
9
|
-
|
3
|
+
autoload :Billable, "pay/stripe/billable"
|
4
|
+
autoload :Charge, "pay/stripe/charge"
|
5
|
+
autoload :Subscription, "pay/stripe/subscription"
|
6
|
+
autoload :Error, "pay/stripe/error"
|
7
|
+
|
8
|
+
module Webhooks
|
9
|
+
autoload :ChargeRefunded, "pay/stripe/webhooks/charge_refunded"
|
10
|
+
autoload :ChargeSucceeded, "pay/stripe/webhooks/charge_succeeded"
|
11
|
+
autoload :CustomerDeleted, "pay/stripe/webhooks/customer_deleted"
|
12
|
+
autoload :CustomerUpdated, "pay/stripe/webhooks/customer_updated"
|
13
|
+
autoload :PaymentActionRequired, "pay/stripe/webhooks/payment_action_required"
|
14
|
+
autoload :PaymentMethodUpdated, "pay/stripe/webhooks/payment_method_updated"
|
15
|
+
autoload :SubscriptionCreated, "pay/stripe/webhooks/subscription_created"
|
16
|
+
autoload :SubscriptionDeleted, "pay/stripe/webhooks/subscription_deleted"
|
17
|
+
autoload :SubscriptionRenewing, "pay/stripe/webhooks/subscription_renewing"
|
18
|
+
autoload :SubscriptionUpdated, "pay/stripe/webhooks/subscription_updated"
|
19
|
+
end
|
10
20
|
|
11
|
-
extend
|
21
|
+
extend Env
|
12
22
|
|
13
|
-
def setup
|
23
|
+
def self.setup
|
14
24
|
::Stripe.api_key = private_key
|
15
25
|
::Stripe.api_version = "2020-08-27"
|
16
|
-
::StripeEvent.signing_secret = signing_secret
|
17
26
|
|
18
|
-
Pay
|
19
|
-
|
27
|
+
# Used by Stripe to identify Pay for support
|
28
|
+
::Stripe.set_app_info("PayRails", partner_id: "pp_partner_IqhY0UExnJYLxg", version: Pay::VERSION, url: "https://github.com/pay-rails/pay")
|
29
|
+
|
30
|
+
configure_webhooks
|
20
31
|
end
|
21
32
|
|
22
|
-
def public_key
|
33
|
+
def self.public_key
|
23
34
|
find_value_by_name(:stripe, :public_key)
|
24
35
|
end
|
25
36
|
|
26
|
-
def private_key
|
37
|
+
def self.private_key
|
27
38
|
find_value_by_name(:stripe, :private_key)
|
28
39
|
end
|
29
40
|
|
30
|
-
def signing_secret
|
41
|
+
def self.signing_secret
|
31
42
|
find_value_by_name(:stripe, :signing_secret)
|
32
43
|
end
|
44
|
+
|
45
|
+
def self.configure_webhooks
|
46
|
+
Pay::Webhooks.configure do |events|
|
47
|
+
# Listen to the charge event to make sure we get non-subscription
|
48
|
+
# purchases as well. Invoice is only for subscriptions and manual creation
|
49
|
+
# so it does not include individual charges.
|
50
|
+
events.subscribe "stripe.charge.succeeded", Pay::Stripe::Webhooks::ChargeSucceeded.new
|
51
|
+
events.subscribe "stripe.charge.refunded", Pay::Stripe::Webhooks::ChargeRefunded.new
|
52
|
+
|
53
|
+
# Warn user of upcoming charges for their subscription. This is handy for
|
54
|
+
# notifying annual users their subscription will renew shortly.
|
55
|
+
# This probably should be ignored for monthly subscriptions.
|
56
|
+
events.subscribe "stripe.invoice.upcoming", Pay::Stripe::Webhooks::SubscriptionRenewing.new
|
57
|
+
|
58
|
+
# Payment action is required to process an invoice
|
59
|
+
events.subscribe "stripe.invoice.payment_action_required", Pay::Stripe::Webhooks::PaymentActionRequired.new
|
60
|
+
|
61
|
+
# If a subscription is manually created on Stripe, we want to sync
|
62
|
+
events.subscribe "stripe.customer.subscription.created", Pay::Stripe::Webhooks::SubscriptionCreated.new
|
63
|
+
|
64
|
+
# If the plan, quantity, or trial ending date is updated on Stripe, we want to sync
|
65
|
+
events.subscribe "stripe.customer.subscription.updated", Pay::Stripe::Webhooks::SubscriptionUpdated.new
|
66
|
+
|
67
|
+
# When a customers subscription is canceled, we want to update our records
|
68
|
+
events.subscribe "stripe.customer.subscription.deleted", Pay::Stripe::Webhooks::SubscriptionDeleted.new
|
69
|
+
|
70
|
+
# Monitor changes for customer's default card changing
|
71
|
+
events.subscribe "stripe.customer.updated", Pay::Stripe::Webhooks::CustomerUpdated.new
|
72
|
+
|
73
|
+
# If a customer was deleted in Stripe, their subscriptions should be cancelled
|
74
|
+
events.subscribe "stripe.customer.deleted", Pay::Stripe::Webhooks::CustomerDeleted.new
|
75
|
+
|
76
|
+
# If a customer's payment source was deleted in Stripe, we should update as well
|
77
|
+
events.subscribe "stripe.payment_method.attached", Pay::Stripe::Webhooks::PaymentMethodUpdated.new
|
78
|
+
events.subscribe "stripe.payment_method.updated", Pay::Stripe::Webhooks::PaymentMethodUpdated.new
|
79
|
+
events.subscribe "stripe.payment_method.card_automatically_updated", Pay::Stripe::Webhooks::PaymentMethodUpdated.new
|
80
|
+
events.subscribe "stripe.payment_method.detached", Pay::Stripe::Webhooks::PaymentMethodUpdated.new
|
81
|
+
end
|
82
|
+
end
|
33
83
|
end
|
34
84
|
end
|
data/lib/pay/stripe/billable.rb
CHANGED
@@ -1,51 +1,71 @@
|
|
1
1
|
module Pay
|
2
2
|
module Stripe
|
3
|
-
|
4
|
-
|
3
|
+
class Billable
|
4
|
+
include Rails.application.routes.url_helpers
|
5
5
|
|
6
|
-
|
7
|
-
|
6
|
+
attr_reader :billable
|
7
|
+
|
8
|
+
delegate :processor_id,
|
9
|
+
:processor_id?,
|
10
|
+
:email,
|
11
|
+
:customer_name,
|
12
|
+
:card_token,
|
13
|
+
to: :billable
|
14
|
+
|
15
|
+
class << self
|
16
|
+
def default_url_options
|
17
|
+
Rails.application.config.action_mailer.default_url_options
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize(billable)
|
22
|
+
@billable = billable
|
8
23
|
end
|
9
24
|
|
10
25
|
# Handles Billable#customer
|
11
26
|
#
|
12
27
|
# Returns Stripe::Customer
|
13
|
-
def
|
28
|
+
def customer
|
14
29
|
if processor_id?
|
15
30
|
::Stripe::Customer.retrieve(processor_id)
|
16
31
|
else
|
17
|
-
|
32
|
+
stripe_customer = ::Stripe::Customer.create(email: email, name: customer_name)
|
33
|
+
billable.update(processor: :stripe, processor_id: stripe_customer.id)
|
34
|
+
|
35
|
+
# Update the user's card on file if a token was passed in
|
36
|
+
if card_token.present?
|
37
|
+
payment_method = ::Stripe::PaymentMethod.attach(card_token, {customer: stripe_customer.id})
|
38
|
+
stripe_customer.invoice_settings.default_payment_method = payment_method.id
|
39
|
+
stripe_customer.save
|
40
|
+
|
41
|
+
update_card_on_file ::Stripe::PaymentMethod.retrieve(card_token).card
|
42
|
+
end
|
43
|
+
|
44
|
+
stripe_customer
|
18
45
|
end
|
19
46
|
rescue ::Stripe::StripeError => e
|
20
47
|
raise Pay::Stripe::Error, e
|
21
48
|
end
|
22
49
|
|
23
|
-
def create_setup_intent
|
24
|
-
::Stripe::SetupIntent.create(
|
25
|
-
customer: processor_id,
|
26
|
-
usage: :off_session
|
27
|
-
)
|
28
|
-
end
|
29
|
-
|
30
50
|
# Handles Billable#charge
|
31
51
|
#
|
32
52
|
# Returns Pay::Charge
|
33
|
-
def
|
34
|
-
|
53
|
+
def charge(amount, options = {})
|
54
|
+
stripe_customer = customer
|
35
55
|
args = {
|
36
56
|
amount: amount,
|
37
57
|
confirm: true,
|
38
58
|
confirmation_method: :automatic,
|
39
59
|
currency: "usd",
|
40
|
-
customer:
|
41
|
-
payment_method:
|
60
|
+
customer: stripe_customer.id,
|
61
|
+
payment_method: stripe_customer.invoice_settings.default_payment_method
|
42
62
|
}.merge(options)
|
43
63
|
|
44
64
|
payment_intent = ::Stripe::PaymentIntent.create(args)
|
45
65
|
Pay::Payment.new(payment_intent).validate
|
46
66
|
|
47
67
|
# Create a new charge object
|
48
|
-
|
68
|
+
save_pay_charge(payment_intent.charges.first)
|
49
69
|
rescue ::Stripe::StripeError => e
|
50
70
|
raise Pay::Stripe::Error, e
|
51
71
|
end
|
@@ -53,7 +73,7 @@ module Pay
|
|
53
73
|
# Handles Billable#subscribe
|
54
74
|
#
|
55
75
|
# Returns Pay::Subscription
|
56
|
-
def
|
76
|
+
def subscribe(name: Pay.default_product_name, plan: Pay.default_plan_name, **options)
|
57
77
|
quantity = options.delete(:quantity) || 1
|
58
78
|
opts = {
|
59
79
|
expand: ["pending_setup_intent", "latest_invoice.payment_intent"],
|
@@ -64,10 +84,10 @@ module Pay
|
|
64
84
|
# Inherit trial from plan unless trial override was specified
|
65
85
|
opts[:trial_from_plan] = true unless opts[:trial_period_days]
|
66
86
|
|
67
|
-
opts[:customer] =
|
87
|
+
opts[:customer] = customer.id
|
68
88
|
|
69
89
|
stripe_sub = ::Stripe::Subscription.create(opts)
|
70
|
-
subscription =
|
90
|
+
subscription = billable.create_pay_subscription(stripe_sub, "stripe", name, plan, status: stripe_sub.status, quantity: quantity)
|
71
91
|
|
72
92
|
# No trial, card requires SCA
|
73
93
|
if subscription.incomplete?
|
@@ -86,93 +106,140 @@ module Pay
|
|
86
106
|
# Handles Billable#update_card
|
87
107
|
#
|
88
108
|
# Returns true if successful
|
89
|
-
def
|
90
|
-
|
109
|
+
def update_card(payment_method_id)
|
110
|
+
stripe_customer = customer
|
91
111
|
|
92
|
-
return true if payment_method_id ==
|
112
|
+
return true if payment_method_id == stripe_customer.invoice_settings.default_payment_method
|
93
113
|
|
94
|
-
payment_method = ::Stripe::PaymentMethod.attach(payment_method_id, customer:
|
95
|
-
::Stripe::Customer.update(
|
114
|
+
payment_method = ::Stripe::PaymentMethod.attach(payment_method_id, customer: stripe_customer.id)
|
115
|
+
::Stripe::Customer.update(stripe_customer.id, invoice_settings: {default_payment_method: payment_method.id})
|
96
116
|
|
97
|
-
|
117
|
+
update_card_on_file(payment_method.card)
|
98
118
|
true
|
99
119
|
rescue ::Stripe::StripeError => e
|
100
120
|
raise Pay::Stripe::Error, e
|
101
121
|
end
|
102
122
|
|
103
|
-
def
|
104
|
-
|
105
|
-
customer.email = email
|
106
|
-
customer.name = customer_name
|
107
|
-
customer.save
|
123
|
+
def update_email!
|
124
|
+
::Stripe::Customer.update(processor_id, {email: email, name: customer_name})
|
108
125
|
end
|
109
126
|
|
110
|
-
def
|
127
|
+
def processor_subscription(subscription_id, options = {})
|
111
128
|
::Stripe::Subscription.retrieve(options.merge(id: subscription_id))
|
112
129
|
end
|
113
130
|
|
114
|
-
def
|
131
|
+
def invoice!(options = {})
|
115
132
|
return unless processor_id?
|
116
133
|
::Stripe::Invoice.create(options.merge(customer: processor_id)).pay
|
117
134
|
end
|
118
135
|
|
119
|
-
def
|
136
|
+
def upcoming_invoice
|
120
137
|
::Stripe::Invoice.upcoming(customer: processor_id)
|
121
138
|
end
|
122
139
|
|
123
140
|
# Used by webhooks when the customer or source changes
|
124
141
|
def sync_card_from_stripe
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
if default_payment_method_id.present?
|
129
|
-
payment_method = ::Stripe::PaymentMethod.retrieve(default_payment_method_id)
|
130
|
-
update(
|
131
|
-
card_type: payment_method.card.brand,
|
132
|
-
card_last4: payment_method.card.last4,
|
133
|
-
card_exp_month: payment_method.card.exp_month,
|
134
|
-
card_exp_year: payment_method.card.exp_year
|
135
|
-
)
|
136
|
-
|
137
|
-
# Customer has no default payment method
|
142
|
+
if (payment_method_id = customer.invoice_settings.default_payment_method)
|
143
|
+
update_card_on_file ::Stripe::PaymentMethod.retrieve(payment_method_id).card
|
138
144
|
else
|
139
|
-
update(card_type: nil, card_last4: nil)
|
145
|
+
billable.update(card_type: nil, card_last4: nil)
|
140
146
|
end
|
141
147
|
end
|
142
148
|
|
143
|
-
|
144
|
-
|
145
|
-
def create_stripe_customer
|
146
|
-
customer = ::Stripe::Customer.create(email: email, name: customer_name)
|
147
|
-
update(processor: "stripe", processor_id: customer.id)
|
148
|
-
|
149
|
-
# Update the user's card on file if a token was passed in
|
150
|
-
if card_token.present?
|
151
|
-
payment_method = ::Stripe::PaymentMethod.attach(card_token, {customer: customer.id})
|
152
|
-
customer.invoice_settings.default_payment_method = payment_method.id
|
153
|
-
customer.save
|
154
|
-
|
155
|
-
update_stripe_card_on_file ::Stripe::PaymentMethod.retrieve(card_token).card
|
156
|
-
end
|
157
|
-
|
158
|
-
customer
|
149
|
+
def create_setup_intent
|
150
|
+
::Stripe::SetupIntent.create(customer: processor_id, usage: :off_session)
|
159
151
|
end
|
160
152
|
|
161
|
-
def
|
153
|
+
def trial_end_date(stripe_sub)
|
162
154
|
# Times in Stripe are returned in UTC
|
163
155
|
stripe_sub.trial_end.present? ? Time.at(stripe_sub.trial_end) : nil
|
164
156
|
end
|
165
157
|
|
166
158
|
# Save the card to the database as the user's current card
|
167
|
-
def
|
168
|
-
update!(
|
159
|
+
def update_card_on_file(card)
|
160
|
+
billable.update!(
|
169
161
|
card_type: card.brand.capitalize,
|
170
162
|
card_last4: card.last4,
|
171
163
|
card_exp_month: card.exp_month,
|
172
164
|
card_exp_year: card.exp_year
|
173
165
|
)
|
174
166
|
|
175
|
-
|
167
|
+
billable.card_token = nil
|
168
|
+
end
|
169
|
+
|
170
|
+
def save_pay_charge(object)
|
171
|
+
charge = billable.charges.find_or_initialize_by(processor: :stripe, processor_id: object.id)
|
172
|
+
|
173
|
+
charge.update(
|
174
|
+
amount: object.amount,
|
175
|
+
card_last4: object.payment_method_details.card.last4,
|
176
|
+
card_type: object.payment_method_details.card.brand,
|
177
|
+
card_exp_month: object.payment_method_details.card.exp_month,
|
178
|
+
card_exp_year: object.payment_method_details.card.exp_year,
|
179
|
+
created_at: Time.zone.at(object.created)
|
180
|
+
)
|
181
|
+
|
182
|
+
charge
|
183
|
+
end
|
184
|
+
|
185
|
+
# https://stripe.com/docs/api/checkout/sessions/create
|
186
|
+
#
|
187
|
+
# checkout(mode: "payment")
|
188
|
+
# checkout(mode: "setup")
|
189
|
+
# checkout(mode: "subscription")
|
190
|
+
#
|
191
|
+
# checkout(line_items: "price_12345", quantity: 2)
|
192
|
+
# checkout(line_items [{ price: "price_123" }, { price: "price_456" }])
|
193
|
+
# checkout(line_items, "price_12345", allow_promotion_codes: true)
|
194
|
+
#
|
195
|
+
def checkout(**options)
|
196
|
+
args = {
|
197
|
+
customer: processor_id,
|
198
|
+
payment_method_types: ["card"],
|
199
|
+
mode: "payment",
|
200
|
+
# These placeholder URLs will be replaced in a following step.
|
201
|
+
success_url: root_url,
|
202
|
+
cancel_url: root_url
|
203
|
+
}
|
204
|
+
|
205
|
+
# Line items are optional
|
206
|
+
if (line_items = options.delete(:line_items))
|
207
|
+
args[:line_items] = Array.wrap(line_items).map { |item|
|
208
|
+
if item.is_a? Hash
|
209
|
+
item
|
210
|
+
else
|
211
|
+
{price: item, quantity: options.fetch(:quantity, 1)}
|
212
|
+
end
|
213
|
+
}
|
214
|
+
end
|
215
|
+
|
216
|
+
::Stripe::Checkout::Session.create(args.merge(options))
|
217
|
+
end
|
218
|
+
|
219
|
+
# https://stripe.com/docs/api/checkout/sessions/create
|
220
|
+
#
|
221
|
+
# checkout_charge(amount: 15_00, name: "T-shirt", quantity: 2)
|
222
|
+
#
|
223
|
+
def checkout_charge(amount:, name:, quantity: 1, **options)
|
224
|
+
checkout(
|
225
|
+
line_items: {
|
226
|
+
price_data: {
|
227
|
+
currency: options[:currency] || "usd",
|
228
|
+
product_data: {name: name},
|
229
|
+
unit_amount: amount
|
230
|
+
},
|
231
|
+
quantity: quantity
|
232
|
+
},
|
233
|
+
**options
|
234
|
+
)
|
235
|
+
end
|
236
|
+
|
237
|
+
def billing_portal(**options)
|
238
|
+
args = {
|
239
|
+
customer: processor_id,
|
240
|
+
return_url: options[:return_url] || root_url
|
241
|
+
}
|
242
|
+
::Stripe::BillingPortal::Session.create(args.merge(options))
|
176
243
|
end
|
177
244
|
end
|
178
245
|
end
|