pay 2.4.0 → 2.6.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.

Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -3
  3. data/app/controllers/pay/webhooks/braintree_controller.rb +7 -53
  4. data/app/controllers/pay/webhooks/paddle_controller.rb +19 -18
  5. data/app/controllers/pay/webhooks/stripe_controller.rb +47 -0
  6. data/app/models/pay/charge.rb +22 -3
  7. data/app/models/pay/subscription.rb +32 -41
  8. data/app/views/pay/stripe/_checkout_button.html.erb +21 -0
  9. data/config/routes.rb +1 -1
  10. data/db/migrate/20200603134434_add_data_to_pay_models.rb +6 -1
  11. data/lib/pay.rb +13 -12
  12. data/lib/pay/billable.rb +33 -33
  13. data/lib/pay/billable/sync_email.rb +1 -1
  14. data/lib/pay/braintree.rb +34 -15
  15. data/lib/pay/braintree/authorization_error.rb +9 -0
  16. data/lib/pay/braintree/billable.rb +33 -30
  17. data/lib/pay/braintree/charge.rb +8 -10
  18. data/lib/pay/braintree/error.rb +9 -0
  19. data/lib/pay/braintree/subscription.rb +43 -20
  20. data/lib/pay/braintree/webhooks/subscription_canceled.rb +19 -0
  21. data/lib/pay/braintree/webhooks/subscription_charged_successfully.rb +24 -0
  22. data/lib/pay/braintree/webhooks/subscription_charged_unsuccessfully.rb +24 -0
  23. data/lib/pay/braintree/webhooks/subscription_expired.rb +19 -0
  24. data/lib/pay/braintree/webhooks/subscription_trial_ended.rb +19 -0
  25. data/lib/pay/braintree/webhooks/subscription_went_active.rb +19 -0
  26. data/lib/pay/braintree/webhooks/subscription_went_past_due.rb +19 -0
  27. data/lib/pay/engine.rb +0 -23
  28. data/lib/pay/errors.rb +0 -44
  29. data/lib/pay/paddle.rb +30 -16
  30. data/lib/pay/paddle/billable.rb +26 -22
  31. data/lib/pay/paddle/charge.rb +8 -12
  32. data/lib/pay/paddle/error.rb +9 -0
  33. data/lib/pay/paddle/subscription.rb +32 -23
  34. data/lib/pay/paddle/webhooks/subscription_cancelled.rb +3 -3
  35. data/lib/pay/paddle/webhooks/subscription_created.rb +15 -15
  36. data/lib/pay/paddle/webhooks/subscription_payment_refunded.rb +3 -3
  37. data/lib/pay/paddle/webhooks/subscription_payment_succeeded.rb +11 -11
  38. data/lib/pay/paddle/webhooks/subscription_updated.rb +11 -11
  39. data/lib/pay/stripe.rb +65 -15
  40. data/lib/pay/stripe/billable.rb +127 -69
  41. data/lib/pay/stripe/charge.rb +9 -15
  42. data/lib/pay/stripe/error.rb +9 -0
  43. data/lib/pay/stripe/subscription.rb +34 -14
  44. data/lib/pay/stripe/webhooks/charge_succeeded.rb +1 -20
  45. data/lib/pay/stripe/webhooks/subscription_created.rb +1 -0
  46. data/lib/pay/version.rb +1 -1
  47. data/lib/pay/webhooks.rb +13 -0
  48. data/lib/pay/webhooks/delegator.rb +61 -0
  49. metadata +20 -69
  50. data/lib/pay/paddle/webhooks.rb +0 -1
  51. 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 initialize(data)
6
- subscription = Pay.subscription_model.find_by(processor: :paddle, processor_id: data["subscription_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(data["cancellation_effective_date"])) if subscription.ends_at.blank? && data["cancellation_effective_date"].present?
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 initialize(data)
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: data["subscription_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: data["user_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(data["passthrough"], data["subscription_plan_id"])
17
- owner&.update!(processor: "paddle", processor_id: data["user_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: '#{data["passthrough"]}'. Searched these models: #{Pay.billable_models.join(", ")}")
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: data["subscription_id"], status: :active)
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 = 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"
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?(data["status"]) && subscription.on_trial?
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?(data["status"])
40
- Time.zone.parse(data["next_bill_date"])
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 initialize(data)
6
- charge = Pay.charge_model.find_by(processor: :paddle, processor_id: data["subscription_payment_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(data["gross_refund"].to_f * 100))
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 initialize(data)
6
- billable = Pay.find_billable(processor: :paddle, processor_id: data["user_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: data["subscription_payment_id"]).any?
8
+ return if billable.charges.where(processor_id: event["subscription_payment_id"]).any?
9
9
 
10
- charge = create_charge(billable, data)
10
+ charge = create_charge(billable, event)
11
11
  notify_user(billable, charge)
12
12
  end
13
13
 
14
- def create_charge(user, data)
14
+ def create_charge(user, event)
15
15
  charge = user.charges.find_or_initialize_by(
16
16
  processor: :paddle,
17
- processor_id: data["subscription_payment_id"]
17
+ processor_id: event["subscription_payment_id"]
18
18
  )
19
19
 
20
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"])
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.paddle_payment_information(data["subscription_id"])
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 initialize(data)
6
- subscription = Pay.subscription_model.find_by(processor: :paddle, processor_id: data["subscription_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 data["status"]
10
+ case event["status"]
11
11
  when "deleted"
12
12
  subscription.status = "canceled"
13
- subscription.ends_at = Time.zone.parse(data["next_bill_date"]) || Time.zone.now if subscription.ends_at.blank?
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(data["next_bill_date"])
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(data["paused_from"]) if data["paused_from"].present?
19
+ subscription.paddle_paused_from = Time.zone.parse(event["paused_from"]) if event["paused_from"].present?
20
20
  else
21
- subscription.status = data["status"]
21
+ subscription.status = event["status"]
22
22
  end
23
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"]
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
- include Env
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 self
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.charge_model.include Pay::Stripe::Charge
19
- Pay.subscription_model.include Pay::Stripe::Subscription
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
@@ -1,51 +1,71 @@
1
1
  module Pay
2
2
  module Stripe
3
- module Billable
4
- extend ActiveSupport::Concern
3
+ class Billable
4
+ include Rails.application.routes.url_helpers
5
5
 
6
- included do
7
- scope :stripe, -> { where(processor: :stripe) }
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 stripe_customer
28
+ def customer
14
29
  if processor_id?
15
30
  ::Stripe::Customer.retrieve(processor_id)
16
31
  else
17
- create_stripe_customer
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 create_stripe_charge(amount, options = {})
34
- customer = stripe_customer
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: customer.id,
41
- payment_method: customer.invoice_settings.default_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
- Stripe::Webhooks::ChargeSucceeded.new.create_charge(self, payment_intent.charges.first)
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 create_stripe_subscription(name, plan, options = {})
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] = stripe_customer.id
87
+ opts[:customer] = customer.id
68
88
 
69
89
  stripe_sub = ::Stripe::Subscription.create(opts)
70
- subscription = create_subscription(stripe_sub, "stripe", name, plan, status: stripe_sub.status, quantity: quantity)
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,131 @@ module Pay
86
106
  # Handles Billable#update_card
87
107
  #
88
108
  # Returns true if successful
89
- def update_stripe_card(payment_method_id)
90
- customer = stripe_customer
109
+ def update_card(payment_method_id)
110
+ stripe_customer = customer
91
111
 
92
- return true if payment_method_id == customer.invoice_settings.default_payment_method
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: customer.id)
95
- ::Stripe::Customer.update(customer.id, invoice_settings: {default_payment_method: payment_method.id})
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
- update_stripe_card_on_file(payment_method.card)
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 update_stripe_email!
104
- customer = stripe_customer
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 stripe_subscription(subscription_id, options = {})
127
+ def processor_subscription(subscription_id, options = {})
111
128
  ::Stripe::Subscription.retrieve(options.merge(id: subscription_id))
112
129
  end
113
130
 
114
- def stripe_invoice!(options = {})
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 stripe_upcoming_invoice
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
- stripe_cust = stripe_customer
126
- default_payment_method_id = stripe_cust.invoice_settings.default_payment_method
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
- private
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 stripe_trial_end_date(stripe_sub)
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 update_stripe_card_on_file(card)
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
- self.card_token = nil
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
+ payment_method_types: ["card"],
198
+ mode: "payment",
199
+ # These placeholder URLs will be replaced in a following step.
200
+ success_url: root_url,
201
+ cancel_url: root_url
202
+ }
203
+
204
+ # Line items are optional
205
+ if (line_items = options.delete(:line_items))
206
+ args[:line_items] = Array.wrap(line_items).map { |item|
207
+ if item.is_a? Hash
208
+ item
209
+ else
210
+ {price: item, quantity: options.fetch(:quantity, 1)}
211
+ end
212
+ }
213
+ end
214
+
215
+ ::Stripe::Checkout::Session.create(args.merge(options))
216
+ end
217
+
218
+ # https://stripe.com/docs/api/checkout/sessions/create
219
+ #
220
+ # checkout_charge(amount: 15_00, name: "T-shirt", quantity: 2)
221
+ #
222
+ def checkout_charge(amount:, name:, quantity: 1, **options)
223
+ checkout(
224
+ line_items: {
225
+ price_data: {
226
+ currency: options[:currency] || "usd",
227
+ product_data: {name: name},
228
+ unit_amount: amount
229
+ },
230
+ quantity: quantity
231
+ },
232
+ **options
233
+ )
176
234
  end
177
235
  end
178
236
  end