pay 2.5.0 → 2.6.4

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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +10 -2
  3. data/app/models/pay/charge.rb +22 -3
  4. data/app/models/pay/subscription.rb +23 -24
  5. data/app/views/pay/stripe/_checkout_button.html.erb +21 -0
  6. data/lib/pay.rb +12 -14
  7. data/lib/pay/billable.rb +44 -33
  8. data/lib/pay/billable/sync_email.rb +1 -1
  9. data/lib/pay/braintree.rb +34 -16
  10. data/lib/pay/braintree/authorization_error.rb +9 -0
  11. data/lib/pay/braintree/billable.rb +33 -30
  12. data/lib/pay/braintree/charge.rb +8 -10
  13. data/lib/pay/braintree/error.rb +9 -0
  14. data/lib/pay/braintree/subscription.rb +34 -15
  15. data/lib/pay/braintree/webhooks/subscription_charged_successfully.rb +1 -1
  16. data/lib/pay/braintree/webhooks/subscription_charged_unsuccessfully.rb +1 -1
  17. data/lib/pay/engine.rb +0 -22
  18. data/lib/pay/errors.rb +0 -44
  19. data/lib/pay/fake_processor.rb +8 -0
  20. data/lib/pay/fake_processor/billable.rb +60 -0
  21. data/lib/pay/fake_processor/charge.rb +21 -0
  22. data/lib/pay/fake_processor/error.rb +6 -0
  23. data/lib/pay/fake_processor/subscription.rb +55 -0
  24. data/lib/pay/paddle.rb +30 -16
  25. data/lib/pay/paddle/billable.rb +26 -22
  26. data/lib/pay/paddle/charge.rb +8 -12
  27. data/lib/pay/paddle/error.rb +9 -0
  28. data/lib/pay/paddle/subscription.rb +33 -18
  29. data/lib/pay/paddle/webhooks/subscription_payment_succeeded.rb +1 -1
  30. data/lib/pay/stripe.rb +62 -14
  31. data/lib/pay/stripe/billable.rb +136 -69
  32. data/lib/pay/stripe/charge.rb +9 -15
  33. data/lib/pay/stripe/error.rb +9 -0
  34. data/lib/pay/stripe/subscription.rb +31 -11
  35. data/lib/pay/stripe/webhooks/charge_succeeded.rb +1 -20
  36. data/lib/pay/stripe/webhooks/customer_updated.rb +1 -1
  37. data/lib/pay/stripe/webhooks/payment_method_updated.rb +1 -1
  38. data/lib/pay/version.rb +1 -1
  39. data/lib/pay/webhooks.rb +13 -0
  40. metadata +16 -56
  41. data/lib/pay/braintree/webhooks.rb +0 -11
  42. data/lib/pay/paddle/webhooks.rb +0 -9
  43. data/lib/pay/stripe/webhooks.rb +0 -38
@@ -24,7 +24,7 @@ module Pay
24
24
  created_at: Time.zone.parse(event["event_time"])
25
25
  }
26
26
 
27
- payment_information = user.paddle_payment_information(event["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)
data/lib/pay/stripe.rb CHANGED
@@ -1,36 +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
26
 
17
27
  # Used by Stripe to identify Pay for support
18
28
  ::Stripe.set_app_info("PayRails", partner_id: "pp_partner_IqhY0UExnJYLxg", version: Pay::VERSION, url: "https://github.com/pay-rails/pay")
19
29
 
20
- Pay.charge_model.include Pay::Stripe::Charge
21
- Pay.subscription_model.include Pay::Stripe::Subscription
30
+ configure_webhooks
22
31
  end
23
32
 
24
- def public_key
33
+ def self.public_key
25
34
  find_value_by_name(:stripe, :public_key)
26
35
  end
27
36
 
28
- def private_key
37
+ def self.private_key
29
38
  find_value_by_name(:stripe, :private_key)
30
39
  end
31
40
 
32
- def signing_secret
41
+ def self.signing_secret
33
42
  find_value_by_name(:stripe, :signing_secret)
34
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
35
83
  end
36
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,140 @@ 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
+ 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
@@ -1,29 +1,23 @@
1
1
  module Pay
2
2
  module Stripe
3
- module Charge
4
- extend ActiveSupport::Concern
3
+ class Charge
4
+ attr_reader :pay_charge
5
5
 
6
- included do
7
- scope :stripe, -> { where(processor: :stripe) }
8
- end
6
+ delegate :processor_id, :owner, to: :pay_charge
9
7
 
10
- def stripe?
11
- processor == "stripe"
8
+ def initialize(pay_charge)
9
+ @pay_charge = pay_charge
12
10
  end
13
11
 
14
- def stripe_charge
12
+ def charge
15
13
  ::Stripe::Charge.retrieve(processor_id)
16
14
  rescue ::Stripe::StripeError => e
17
15
  raise Pay::Stripe::Error, e
18
16
  end
19
17
 
20
- def stripe_refund!(amount_to_refund)
21
- ::Stripe::Refund.create(
22
- charge: processor_id,
23
- amount: amount_to_refund
24
- )
25
-
26
- update(amount_refunded: amount_to_refund)
18
+ def refund!(amount_to_refund)
19
+ ::Stripe::Refund.create(charge: processor_id, amount: amount_to_refund)
20
+ pay_charge.update(amount_refunded: amount_to_refund)
27
21
  rescue ::Stripe::StripeError => e
28
22
  raise Pay::Stripe::Error, e
29
23
  end
@@ -0,0 +1,9 @@
1
+ module Pay
2
+ module Stripe
3
+ class Error < Pay::Error
4
+ def message
5
+ I18n.t("errors.stripe.#{result.code}", default: result.message)
6
+ end
7
+ end
8
+ end
9
+ end