pay 2.6.2 → 2.6.7

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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +22 -4
  3. data/app/models/pay/charge.rb +1 -13
  4. data/app/models/pay/subscription.rb +1 -1
  5. data/config/locales/en.yml +1 -113
  6. data/lib/pay.rb +1 -0
  7. data/lib/pay/billable.rb +13 -1
  8. data/lib/pay/braintree/billable.rb +10 -8
  9. data/lib/pay/braintree/charge.rb +0 -1
  10. data/lib/pay/braintree/error.rb +16 -2
  11. data/lib/pay/braintree/subscription.rb +3 -3
  12. data/lib/pay/errors.rb +1 -5
  13. data/lib/pay/fake_processor.rb +8 -0
  14. data/lib/pay/fake_processor/billable.rb +60 -0
  15. data/lib/pay/fake_processor/charge.rb +21 -0
  16. data/lib/pay/fake_processor/error.rb +6 -0
  17. data/lib/pay/fake_processor/subscription.rb +55 -0
  18. data/lib/pay/paddle.rb +5 -0
  19. data/lib/pay/paddle/error.rb +1 -3
  20. data/lib/pay/paddle/subscription.rb +5 -1
  21. data/lib/pay/stripe.rb +3 -0
  22. data/lib/pay/stripe/billable.rb +1 -1
  23. data/lib/pay/stripe/error.rb +1 -3
  24. data/lib/pay/stripe/subscription.rb +8 -4
  25. data/lib/pay/stripe/webhooks/payment_intent_succeeded.rb +27 -0
  26. data/lib/pay/stripe_marketplace/billable.rb +246 -0
  27. data/lib/pay/stripe_marketplace/charge.rb +26 -0
  28. data/lib/pay/stripe_marketplace/error.rb +9 -0
  29. data/lib/pay/stripe_marketplace/subscription.rb +83 -0
  30. data/lib/pay/stripe_marketplace/webhooks/charge_refunded.rb +23 -0
  31. data/lib/pay/stripe_marketplace/webhooks/charge_succeeded.rb +24 -0
  32. data/lib/pay/stripe_marketplace/webhooks/customer_deleted.rb +29 -0
  33. data/lib/pay/stripe_marketplace/webhooks/customer_updated.rb +17 -0
  34. data/lib/pay/stripe_marketplace/webhooks/payment_action_required.rb +26 -0
  35. data/lib/pay/stripe_marketplace/webhooks/payment_method_updated.rb +17 -0
  36. data/lib/pay/stripe_marketplace/webhooks/subscription_created.rb +45 -0
  37. data/lib/pay/stripe_marketplace/webhooks/subscription_deleted.rb +19 -0
  38. data/lib/pay/stripe_marketplace/webhooks/subscription_renewing.rb +24 -0
  39. data/lib/pay/stripe_marketplace/webhooks/subscription_updated.rb +38 -0
  40. data/lib/pay/version.rb +1 -1
  41. metadata +22 -2
@@ -0,0 +1,60 @@
1
+ module Pay
2
+ module FakeProcessor
3
+ class Billable
4
+ attr_reader :billable
5
+
6
+ delegate :processor_id,
7
+ :processor_id?,
8
+ :email,
9
+ :customer_name,
10
+ :card_token,
11
+ to: :billable
12
+
13
+ def initialize(billable)
14
+ @billable = billable
15
+ end
16
+
17
+ def customer
18
+ billable
19
+ end
20
+
21
+ def charge(amount, options = {})
22
+ billable.charges.create(
23
+ processor: :fake_processor,
24
+ processor_id: rand(100_000_000),
25
+ amount: amount,
26
+ card_type: :fake,
27
+ card_last4: 1234,
28
+ card_exp_month: Date.today.month,
29
+ card_exp_year: Date.today.year
30
+ )
31
+ end
32
+
33
+ def subscribe(name: Pay.default_product_name, plan: Pay.default_plan_name, **options)
34
+ subscription = OpenStruct.new id: rand(1_000_000)
35
+ billable.create_pay_subscription(subscription, :fake_processor, name, plan, status: :active, quantity: options.fetch(:quantity, 1))
36
+ end
37
+
38
+ def update_card(payment_method_id)
39
+ billable.update(
40
+ card_type: :fake,
41
+ card_last4: 1234,
42
+ card_exp_month: Date.today.month,
43
+ card_exp_year: Date.today.year
44
+ )
45
+ end
46
+
47
+ def update_email!
48
+ # pass
49
+ end
50
+
51
+ def processor_subscription(subscription_id, options = {})
52
+ billable.subscriptions.find_by(processor: :fake_processor, processor_id: subscription_id)
53
+ end
54
+
55
+ def trial_end_date(subscription)
56
+ Date.today
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,21 @@
1
+ module Pay
2
+ module FakeProcessor
3
+ class Charge
4
+ attr_reader :pay_charge
5
+
6
+ delegate :processor_id, :owner, to: :pay_charge
7
+
8
+ def initialize(pay_charge)
9
+ @pay_charge = pay_charge
10
+ end
11
+
12
+ def charge
13
+ pay_charge
14
+ end
15
+
16
+ def refund!(amount_to_refund)
17
+ pay_charge.update(amount_refunded: amount_to_refund)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,6 @@
1
+ module Pay
2
+ module FakeProcessor
3
+ class Error < Pay::Error
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,55 @@
1
+ module Pay
2
+ module FakeProcessor
3
+ class Subscription
4
+ attr_reader :pay_subscription
5
+
6
+ delegate :canceled?,
7
+ :ends_at,
8
+ :on_trial?,
9
+ :owner,
10
+ :processor_subscription,
11
+ :processor_id,
12
+ :prorate,
13
+ :processor_plan,
14
+ :quantity?,
15
+ :quantity,
16
+ to: :pay_subscription
17
+
18
+ def initialize(pay_subscription)
19
+ @pay_subscription = pay_subscription
20
+ end
21
+
22
+ def cancel
23
+ pay_subscription.update(ends_at: Time.current.end_of_month)
24
+ end
25
+
26
+ def cancel_now!
27
+ pay_subscription.update(ends_at: Time.current, status: :canceled)
28
+ end
29
+
30
+ def on_grace_period?
31
+ canceled? && Time.zone.now < ends_at
32
+ end
33
+
34
+ def paused?
35
+ false
36
+ end
37
+
38
+ def pause
39
+ raise NotImplementedError, "FakeProcessor does not support pausing subscriptions"
40
+ end
41
+
42
+ def resume
43
+ unless on_grace_period?
44
+ raise StandardError, "You can only resume subscriptions within their grace period."
45
+ end
46
+
47
+ pay_subscription.update(ends_at: nil, status: :active)
48
+ end
49
+
50
+ def swap(plan)
51
+ pay_subscription.update(processor_plan: plan)
52
+ end
53
+ end
54
+ end
55
+ end
data/lib/pay/paddle.rb CHANGED
@@ -19,6 +19,7 @@ module Pay
19
19
  def self.setup
20
20
  ::PaddlePay.config.vendor_id = vendor_id
21
21
  ::PaddlePay.config.vendor_auth_code = vendor_auth_code
22
+ ::PaddlePay.config.environment = environment
22
23
 
23
24
  configure_webhooks
24
25
  end
@@ -31,6 +32,10 @@ module Pay
31
32
  find_value_by_name(:paddle, :vendor_auth_code)
32
33
  end
33
34
 
35
+ def self.environment
36
+ find_value_by_name(:paddle, :environment) || "production"
37
+ end
38
+
34
39
  def self.public_key_base64
35
40
  find_value_by_name(:paddle, :public_key_base64)
36
41
  end
@@ -1,9 +1,7 @@
1
1
  module Pay
2
2
  module Paddle
3
3
  class Error < Pay::Error
4
- def message
5
- I18n.t("errors.paddle.#{result.code}", default: result.message)
6
- end
4
+ delegate :message, to: :cause
7
5
  end
8
6
  end
9
7
  end
@@ -3,8 +3,10 @@ module Pay
3
3
  class Subscription
4
4
  attr_reader :pay_subscription
5
5
 
6
- delegate :canceled?,
6
+ delegate :active?,
7
+ :canceled?,
7
8
  :ends_at,
9
+ :name,
8
10
  :on_trial?,
9
11
  :owner,
10
12
  :paddle_paused_from,
@@ -12,8 +14,10 @@ module Pay
12
14
  :processor_plan,
13
15
  :processor_subscription,
14
16
  :prorate,
17
+ :prorate?,
15
18
  :quantity,
16
19
  :quantity?,
20
+ :trial_ends_at,
17
21
  to: :pay_subscription
18
22
 
19
23
  def initialize(pay_subscription)
data/lib/pay/stripe.rb CHANGED
@@ -11,6 +11,7 @@ module Pay
11
11
  autoload :CustomerDeleted, "pay/stripe/webhooks/customer_deleted"
12
12
  autoload :CustomerUpdated, "pay/stripe/webhooks/customer_updated"
13
13
  autoload :PaymentActionRequired, "pay/stripe/webhooks/payment_action_required"
14
+ autoload :PaymentIntentSucceeded, "pay/stripe/webhooks/payment_intent_succeeded"
14
15
  autoload :PaymentMethodUpdated, "pay/stripe/webhooks/payment_method_updated"
15
16
  autoload :SubscriptionCreated, "pay/stripe/webhooks/subscription_created"
16
17
  autoload :SubscriptionDeleted, "pay/stripe/webhooks/subscription_deleted"
@@ -50,6 +51,8 @@ module Pay
50
51
  events.subscribe "stripe.charge.succeeded", Pay::Stripe::Webhooks::ChargeSucceeded.new
51
52
  events.subscribe "stripe.charge.refunded", Pay::Stripe::Webhooks::ChargeRefunded.new
52
53
 
54
+ events.subscribe "stripe.payment_intent.succeeded", Pay::Stripe::Webhooks::PaymentIntentSucceeded.new
55
+
53
56
  # Warn user of upcoming charges for their subscription. This is handy for
54
57
  # notifying annual users their subscription will renew shortly.
55
58
  # This probably should be ignored for monthly subscriptions.
@@ -14,7 +14,7 @@ module Pay
14
14
 
15
15
  class << self
16
16
  def default_url_options
17
- Rails.application.config.action_mailer.default_url_options
17
+ Rails.application.config.action_mailer.default_url_options || {}
18
18
  end
19
19
  end
20
20
 
@@ -1,9 +1,7 @@
1
1
  module Pay
2
2
  module Stripe
3
3
  class Error < Pay::Error
4
- def message
5
- I18n.t("errors.stripe.#{result.code}", default: result.message)
6
- end
4
+ delegate :message, to: :cause
7
5
  end
8
6
  end
9
7
  end
@@ -3,16 +3,20 @@ module Pay
3
3
  class Subscription
4
4
  attr_reader :pay_subscription
5
5
 
6
- delegate :canceled?,
6
+ delegate :active?,
7
+ :canceled?,
7
8
  :ends_at,
9
+ :name,
8
10
  :on_trial?,
9
11
  :owner,
10
- :processor_subscription,
11
12
  :processor_id,
12
- :prorate,
13
13
  :processor_plan,
14
- :quantity?,
14
+ :processor_subscription,
15
+ :prorate,
16
+ :prorate?,
15
17
  :quantity,
18
+ :quantity?,
19
+ :trial_ends_at,
16
20
  to: :pay_subscription
17
21
 
18
22
  def initialize(pay_subscription)
@@ -0,0 +1,27 @@
1
+ module Pay
2
+ module Stripe
3
+ module Webhooks
4
+ class PaymentIntentSucceeded
5
+ def call(event)
6
+ object = event.data.object
7
+ billable = Pay.find_billable(processor: :stripe, processor_id: object.customer)
8
+
9
+ return unless billable.present?
10
+
11
+ object.charges.data.each do |charge|
12
+ next if billable.charges.where(processor_id: charge.id).any?
13
+
14
+ charge = Pay::Stripe::Billable.new(billable).save_pay_charge(charge)
15
+ notify_user(billable, charge)
16
+ end
17
+ end
18
+
19
+ def notify_user(billable, charge)
20
+ if Pay.send_emails && charge.respond_to?(:receipt)
21
+ Pay::UserMailer.with(billable: billable, charge: charge).receipt.deliver_later
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,246 @@
1
+ module Pay
2
+ module StripeMarketplace
3
+ class Billable
4
+ include Rails.application.routes.url_helpers
5
+
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
23
+ end
24
+
25
+ # Handles Billable#customer
26
+ #
27
+ # Returns Stripe::Customer
28
+ def customer
29
+ if processor_id?
30
+ ::Stripe::Customer.retrieve(processor_id)
31
+ else
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
45
+ end
46
+ rescue ::Stripe::StripeError => e
47
+ raise Pay::Stripe::Error, e
48
+ end
49
+
50
+ # Handles Billable#charge
51
+ #
52
+ # Returns Pay::Charge
53
+ def charge(amount, options = {})
54
+ stripe_customer = customer
55
+ args = {
56
+ amount: amount,
57
+ confirm: true,
58
+ confirmation_method: :automatic,
59
+ currency: "usd",
60
+ customer: stripe_customer.id,
61
+ payment_method: stripe_customer.invoice_settings.default_payment_method
62
+ }.merge(options)
63
+
64
+ payment_intent = ::Stripe::PaymentIntent.create(args)
65
+ Pay::Payment.new(payment_intent).validate
66
+
67
+ # Create a new charge object
68
+ save_pay_charge(payment_intent.charges.first)
69
+ rescue ::Stripe::StripeError => e
70
+ raise Pay::Stripe::Error, e
71
+ end
72
+
73
+ # Handles Billable#subscribe
74
+ #
75
+ # Returns Pay::Subscription
76
+ def subscribe(name: Pay.default_product_name, plan: Pay.default_plan_name, **options)
77
+ quantity = options.delete(:quantity) || 1
78
+ opts = {
79
+ expand: ["pending_setup_intent", "latest_invoice.payment_intent"],
80
+ items: [plan: plan, quantity: quantity],
81
+ off_session: true
82
+ }.merge(options)
83
+
84
+ # Inherit trial from plan unless trial override was specified
85
+ opts[:trial_from_plan] = true unless opts[:trial_period_days]
86
+
87
+ opts[:customer] = customer.id
88
+
89
+ stripe_sub = ::Stripe::Subscription.create(opts)
90
+ subscription = billable.create_pay_subscription(stripe_sub, "stripe", name, plan, status: stripe_sub.status, quantity: quantity)
91
+
92
+ # No trial, card requires SCA
93
+ if subscription.incomplete?
94
+ Pay::Payment.new(stripe_sub.latest_invoice.payment_intent).validate
95
+
96
+ # Trial, card requires SCA
97
+ elsif subscription.on_trial? && stripe_sub.pending_setup_intent
98
+ Pay::Payment.new(stripe_sub.pending_setup_intent).validate
99
+ end
100
+
101
+ subscription
102
+ rescue ::Stripe::StripeError => e
103
+ raise Pay::Stripe::Error, e
104
+ end
105
+
106
+ # Handles Billable#update_card
107
+ #
108
+ # Returns true if successful
109
+ def update_card(payment_method_id)
110
+ stripe_customer = customer
111
+
112
+ return true if payment_method_id == stripe_customer.invoice_settings.default_payment_method
113
+
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})
116
+
117
+ update_card_on_file(payment_method.card)
118
+ true
119
+ rescue ::Stripe::StripeError => e
120
+ raise Pay::Stripe::Error, e
121
+ end
122
+
123
+ def update_email!
124
+ ::Stripe::Customer.update(processor_id, {email: email, name: customer_name})
125
+ end
126
+
127
+ def processor_subscription(subscription_id, options = {})
128
+ ::Stripe::Subscription.retrieve(options.merge(id: subscription_id))
129
+ end
130
+
131
+ def invoice!(options = {})
132
+ return unless processor_id?
133
+ ::Stripe::Invoice.create(options.merge(customer: processor_id)).pay
134
+ end
135
+
136
+ def upcoming_invoice
137
+ ::Stripe::Invoice.upcoming(customer: processor_id)
138
+ end
139
+
140
+ # Used by webhooks when the customer or source changes
141
+ def sync_card_from_stripe
142
+ if (payment_method_id = customer.invoice_settings.default_payment_method)
143
+ update_card_on_file ::Stripe::PaymentMethod.retrieve(payment_method_id).card
144
+ else
145
+ billable.update(card_type: nil, card_last4: nil)
146
+ end
147
+ end
148
+
149
+ def create_setup_intent
150
+ ::Stripe::SetupIntent.create(customer: processor_id, usage: :off_session)
151
+ end
152
+
153
+ def trial_end_date(stripe_sub)
154
+ # Times in Stripe are returned in UTC
155
+ stripe_sub.trial_end.present? ? Time.at(stripe_sub.trial_end) : nil
156
+ end
157
+
158
+ # Save the card to the database as the user's current card
159
+ def update_card_on_file(card)
160
+ billable.update!(
161
+ card_type: card.brand.capitalize,
162
+ card_last4: card.last4,
163
+ card_exp_month: card.exp_month,
164
+ card_exp_year: card.exp_year
165
+ )
166
+
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))
243
+ end
244
+ end
245
+ end
246
+ end