pay 2.7.0 → 2.7.2

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 +93 -57
  3. data/app/models/pay/application_record.rb +1 -0
  4. data/app/models/pay/charge.rb +1 -1
  5. data/app/models/pay/subscription.rb +1 -1
  6. data/db/migrate/20200603134434_add_data_to_pay_models.rb +2 -11
  7. data/db/migrate/20210309004259_add_data_to_pay_billable.rb +1 -10
  8. data/db/migrate/20210714175351_add_uniqueness_to_pay_models.rb +6 -0
  9. data/lib/generators/active_record/billable_generator.rb +44 -0
  10. data/lib/generators/active_record/merchant_generator.rb +44 -0
  11. data/lib/generators/active_record/templates/billable_migration.rb +17 -0
  12. data/lib/generators/active_record/templates/merchant_migration.rb +12 -0
  13. data/lib/generators/pay/{pay_generator.rb → billable_generator.rb} +2 -3
  14. data/lib/generators/pay/merchant_generator.rb +17 -0
  15. data/lib/generators/pay/orm_helpers.rb +10 -6
  16. data/lib/pay/adapter.rb +9 -0
  17. data/lib/pay/braintree/subscription.rb +2 -0
  18. data/lib/pay/env.rb +8 -0
  19. data/lib/pay/fake_processor/subscription.rb +2 -0
  20. data/lib/pay/paddle/subscription.rb +2 -0
  21. data/lib/pay/stripe/billable.rb +36 -44
  22. data/lib/pay/stripe/charge.rb +55 -2
  23. data/lib/pay/stripe/subscription.rb +62 -5
  24. data/lib/pay/stripe/webhooks/charge_refunded.rb +2 -7
  25. data/lib/pay/stripe/webhooks/charge_succeeded.rb +2 -8
  26. data/lib/pay/stripe/webhooks/checkout_session_async_payment_succeeded.rb +13 -0
  27. data/lib/pay/stripe/webhooks/checkout_session_completed.rb +13 -0
  28. data/lib/pay/stripe/webhooks/payment_intent_succeeded.rb +2 -8
  29. data/lib/pay/stripe/webhooks/payment_method_attached.rb +17 -0
  30. data/lib/pay/stripe/webhooks/payment_method_automatically_updated.rb +17 -0
  31. data/lib/pay/stripe/webhooks/payment_method_detached.rb +17 -0
  32. data/lib/pay/stripe/webhooks/subscription_created.rb +1 -36
  33. data/lib/pay/stripe/webhooks/subscription_deleted.rb +1 -9
  34. data/lib/pay/stripe/webhooks/subscription_renewing.rb +4 -6
  35. data/lib/pay/stripe/webhooks/subscription_updated.rb +1 -29
  36. data/lib/pay/stripe.rb +6 -0
  37. data/lib/pay/version.rb +1 -1
  38. metadata +14 -6
  39. data/app/models/pay.rb +0 -5
  40. data/lib/generators/active_record/pay_generator.rb +0 -58
  41. data/lib/generators/active_record/templates/migration.rb +0 -9
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/named_base"
4
+
5
+ module Pay
6
+ module Generators
7
+ class MerchantGenerator < Rails::Generators::NamedBase
8
+ include Rails::Generators::ResourceHelpers
9
+
10
+ source_root File.expand_path("../templates", __FILE__)
11
+
12
+ desc "Generates a migration to add Pay::Merchant fields to a model."
13
+
14
+ hook_for :orm
15
+ end
16
+ end
17
+ end
@@ -3,12 +3,6 @@
3
3
  module Pay
4
4
  module Generators
5
5
  module OrmHelpers
6
- def model_contents
7
- <<-CONTENT
8
- include Pay::Billable
9
- CONTENT
10
- end
11
-
12
6
  private
13
7
 
14
8
  def model_exists?
@@ -30,6 +24,16 @@ module Pay
30
24
  def model_path
31
25
  @model_path ||= File.join("app", "models", "#{file_path}.rb")
32
26
  end
27
+
28
+ def migration_version
29
+ if rails5_and_up?
30
+ "[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
31
+ end
32
+ end
33
+
34
+ def rails5_and_up?
35
+ Rails::VERSION::MAJOR >= 5
36
+ end
33
37
  end
34
38
  end
35
39
  end
data/lib/pay/adapter.rb CHANGED
@@ -9,5 +9,14 @@ module Pay
9
9
  ActiveRecord::Base.connection_config[:adapter]
10
10
  end
11
11
  end
12
+
13
+ def self.json_column_type
14
+ case current_adapter
15
+ when "postgresql"
16
+ :jsonb
17
+ else
18
+ :json
19
+ end
20
+ end
12
21
  end
13
22
  end
@@ -92,6 +92,8 @@ module Pay
92
92
  end
93
93
 
94
94
  def swap(plan)
95
+ raise ArgumentError, "plan must be a string" unless plan.is_a?(String)
96
+
95
97
  if on_grace_period? && processor_plan == plan
96
98
  resume
97
99
  return
data/lib/pay/env.rb CHANGED
@@ -17,6 +17,14 @@ module Pay
17
17
  secrets&.dig(env, scope, name) ||
18
18
  credentials&.dig(scope, name) ||
19
19
  secrets&.dig(scope, name)
20
+ rescue ActiveSupport::MessageEncryptor::InvalidMessage
21
+ Rails.logger.error <<~MESSAGE
22
+ Rails was unable to decrypt credentials. Pay checks the Rails credentials to look for API keys for payment processors.
23
+
24
+ Make sure to set the `RAILS_MASTER_KEY` env variable or in the .key file. To learn more, run "bin/rails credentials:help"
25
+
26
+ If you're not using Rails credentials, you can delete `config/credentials.yml.enc` and `config/credentials/`.
27
+ MESSAGE
20
28
  end
21
29
 
22
30
  def env
@@ -52,6 +52,8 @@ module Pay
52
52
  end
53
53
 
54
54
  def swap(plan)
55
+ raise ArgumentError, "plan must be a string" unless plan.is_a?(String)
56
+
55
57
  pay_subscription.update(processor_plan: plan)
56
58
  end
57
59
  end
@@ -79,6 +79,8 @@ module Pay
79
79
  end
80
80
 
81
81
  def swap(plan)
82
+ raise ArgumentError, "plan must be a string" unless plan.is_a?(String)
83
+
82
84
  attributes = {plan_id: plan, prorate: prorate}
83
85
  attributes[:quantity] = quantity if quantity?
84
86
  PaddlePay::Subscription::User.update(processor_id, attributes)
@@ -28,17 +28,17 @@ module Pay
28
28
  # Returns Stripe::Customer
29
29
  def customer
30
30
  stripe_customer = if processor_id?
31
- ::Stripe::Customer.retrieve(processor_id, {stripe_account: stripe_account})
31
+ ::Stripe::Customer.retrieve(processor_id, stripe_options)
32
32
  else
33
- sc = ::Stripe::Customer.create({email: email, name: customer_name}, {stripe_account: stripe_account})
33
+ sc = ::Stripe::Customer.create({email: email, name: customer_name}, stripe_options)
34
34
  billable.update(processor: :stripe, processor_id: sc.id, stripe_account: stripe_account)
35
35
  sc
36
36
  end
37
37
 
38
38
  # Update the user's card on file if a token was passed in
39
39
  if card_token.present?
40
- payment_method = ::Stripe::PaymentMethod.attach(card_token, {customer: stripe_customer.id}, {stripe_account: stripe_account})
41
- stripe_customer = ::Stripe::Customer.update(stripe_customer.id, {invoice_settings: {default_payment_method: payment_method.id}}, {stripe_account: stripe_account})
40
+ payment_method = ::Stripe::PaymentMethod.attach(card_token, {customer: stripe_customer.id}, stripe_options)
41
+ stripe_customer = ::Stripe::Customer.update(stripe_customer.id, {invoice_settings: {default_payment_method: payment_method.id}}, stripe_options)
42
42
  update_card_on_file(payment_method.card)
43
43
  end
44
44
 
@@ -61,11 +61,12 @@ module Pay
61
61
  payment_method: stripe_customer.invoice_settings.default_payment_method
62
62
  }.merge(options)
63
63
 
64
- payment_intent = ::Stripe::PaymentIntent.create(args, {stripe_account: stripe_account})
64
+ payment_intent = ::Stripe::PaymentIntent.create(args, stripe_options)
65
65
  Pay::Payment.new(payment_intent).validate
66
66
 
67
67
  # Create a new charge object
68
- save_pay_charge(payment_intent.charges.first)
68
+ charge = payment_intent.charges.first
69
+ Pay::Stripe::Charge.sync(charge.id, object: charge)
69
70
  rescue ::Stripe::StripeError => e
70
71
  raise Pay::Stripe::Error, e
71
72
  end
@@ -87,16 +88,15 @@ module Pay
87
88
  # Load the Stripe customer to verify it exists and update card if needed
88
89
  opts[:customer] = customer.id
89
90
 
90
- stripe_sub = ::Stripe::Subscription.create(opts, {stripe_account: stripe_account})
91
- subscription = billable.create_pay_subscription(stripe_sub, "stripe", name, plan, status: stripe_sub.status, quantity: quantity, stripe_account: stripe_account, application_fee_percent: stripe_sub.application_fee_percent)
91
+ # Create subscription on Stripe
92
+ stripe_sub = ::Stripe::Subscription.create(opts, stripe_options)
93
+
94
+ # Save Pay::Subscription
95
+ subscription = Pay::Stripe::Subscription.sync(stripe_sub.id, object: stripe_sub, name: name)
92
96
 
93
97
  # No trial, card requires SCA
94
98
  if subscription.incomplete?
95
99
  Pay::Payment.new(stripe_sub.latest_invoice.payment_intent).validate
96
-
97
- # Trial, card requires SCA
98
- elsif subscription.on_trial? && stripe_sub.pending_setup_intent
99
- Pay::Payment.new(stripe_sub.pending_setup_intent).validate
100
100
  end
101
101
 
102
102
  subscription
@@ -112,8 +112,8 @@ module Pay
112
112
 
113
113
  return true if payment_method_id == stripe_customer.invoice_settings.default_payment_method
114
114
 
115
- payment_method = ::Stripe::PaymentMethod.attach(payment_method_id, {customer: stripe_customer.id}, {stripe_account: stripe_account})
116
- ::Stripe::Customer.update(stripe_customer.id, {invoice_settings: {default_payment_method: payment_method.id}}, {stripe_account: stripe_account})
115
+ payment_method = ::Stripe::PaymentMethod.attach(payment_method_id, {customer: stripe_customer.id}, stripe_options)
116
+ ::Stripe::Customer.update(stripe_customer.id, {invoice_settings: {default_payment_method: payment_method.id}}, stripe_options)
117
117
 
118
118
  update_card_on_file(payment_method.card)
119
119
  true
@@ -122,33 +122,33 @@ module Pay
122
122
  end
123
123
 
124
124
  def update_email!
125
- ::Stripe::Customer.update(processor_id, {email: email, name: customer_name}, {stripe_account: stripe_account})
125
+ ::Stripe::Customer.update(processor_id, {email: email, name: customer_name}, stripe_options)
126
126
  end
127
127
 
128
128
  def processor_subscription(subscription_id, options = {})
129
- ::Stripe::Subscription.retrieve(options.merge(id: subscription_id), {stripe_account: stripe_account})
129
+ ::Stripe::Subscription.retrieve(options.merge(id: subscription_id), stripe_options)
130
130
  end
131
131
 
132
132
  def invoice!(options = {})
133
133
  return unless processor_id?
134
- ::Stripe::Invoice.create(options.merge(customer: processor_id), {stripe_account: stripe_account}).pay
134
+ ::Stripe::Invoice.create(options.merge(customer: processor_id), stripe_options).pay
135
135
  end
136
136
 
137
137
  def upcoming_invoice
138
- ::Stripe::Invoice.upcoming({customer: processor_id}, {stripe_account: stripe_account})
138
+ ::Stripe::Invoice.upcoming({customer: processor_id}, stripe_options)
139
139
  end
140
140
 
141
141
  # Used by webhooks when the customer or source changes
142
142
  def sync_card_from_stripe
143
143
  if (payment_method_id = customer.invoice_settings.default_payment_method)
144
- update_card_on_file ::Stripe::PaymentMethod.retrieve(payment_method_id, {stripe_account: stripe_account}).card
144
+ update_card_on_file ::Stripe::PaymentMethod.retrieve(payment_method_id, stripe_options).card
145
145
  else
146
146
  billable.update(card_type: nil, card_last4: nil)
147
147
  end
148
148
  end
149
149
 
150
150
  def create_setup_intent
151
- ::Stripe::SetupIntent.create({customer: processor_id, usage: :off_session}, {stripe_account: stripe_account})
151
+ ::Stripe::SetupIntent.create({customer: processor_id, usage: :off_session}, stripe_options)
152
152
  end
153
153
 
154
154
  def trial_end_date(stripe_sub)
@@ -168,29 +168,14 @@ module Pay
168
168
  billable.card_token = nil
169
169
  end
170
170
 
171
- def save_pay_charge(object)
172
- charge = billable.charges.find_or_initialize_by(processor: :stripe, processor_id: object.id)
173
-
174
- attrs = {
175
- amount: object.amount,
176
- card_last4: object.payment_method_details.card.last4,
177
- card_type: object.payment_method_details.card.brand,
178
- card_exp_month: object.payment_method_details.card.exp_month,
179
- card_exp_year: object.payment_method_details.card.exp_year,
180
- created_at: Time.zone.at(object.created),
181
- currency: object.currency,
182
- stripe_account: stripe_account,
183
- application_fee_amount: object.application_fee_amount
184
- }
185
-
186
- # Associate charge with subscription if we can
187
- if object.invoice
188
- invoice = (object.invoice.is_a?(::Stripe::Invoice) ? object.invoice : ::Stripe::Invoice.retrieve(object.invoice))
189
- attrs[:subscription] = Pay::Subscription.find_by(processor: :stripe, processor_id: invoice.subscription)
171
+ # Syncs a customer's subscriptions from Stripe to the database
172
+ def sync_subscriptions
173
+ subscriptions = ::Stripe::Subscription.list({customer: customer}, stripe_options)
174
+ subscriptions.map do |subscription|
175
+ Pay::Stripe::Subscription.sync(subscription.id)
190
176
  end
191
-
192
- charge.update(attrs)
193
- charge
177
+ rescue ::Stripe::StripeError => e
178
+ raise Pay::Stripe::Error, e
194
179
  end
195
180
 
196
181
  # https://stripe.com/docs/api/checkout/sessions/create
@@ -224,7 +209,7 @@ module Pay
224
209
  }
225
210
  end
226
211
 
227
- ::Stripe::Checkout::Session.create(args.merge(options), {stripe_account: stripe_account})
212
+ ::Stripe::Checkout::Session.create(args.merge(options), stripe_options)
228
213
  end
229
214
 
230
215
  # https://stripe.com/docs/api/checkout/sessions/create
@@ -251,7 +236,14 @@ module Pay
251
236
  customer: processor_id,
252
237
  return_url: options.delete(:return_url) || root_url
253
238
  }
254
- ::Stripe::BillingPortal::Session.create(args.merge(options), {stripe_account: stripe_account})
239
+ ::Stripe::BillingPortal::Session.create(args.merge(options), stripe_options)
240
+ end
241
+
242
+ private
243
+
244
+ # Options for Stripe requests
245
+ def stripe_options
246
+ {stripe_account: stripe_account}.compact
255
247
  end
256
248
  end
257
249
  end
@@ -5,12 +5,58 @@ module Pay
5
5
 
6
6
  delegate :processor_id, :owner, :stripe_account, to: :pay_charge
7
7
 
8
+ def self.sync(charge_id, object: nil, try: 0, retries: 1)
9
+ # Skip loading the latest charge details from the API if we already have it
10
+ object ||= ::Stripe::Charge.retrieve(id: charge_id)
11
+
12
+ owner = Pay.find_billable(processor: :stripe, processor_id: object.customer)
13
+ return unless owner
14
+
15
+ attrs = {
16
+ amount: object.amount,
17
+ amount_refunded: object.amount_refunded,
18
+ application_fee_amount: object.application_fee_amount,
19
+ card_exp_month: object.payment_method_details.card.exp_month,
20
+ card_exp_year: object.payment_method_details.card.exp_year,
21
+ card_last4: object.payment_method_details.card.last4,
22
+ card_type: object.payment_method_details.card.brand,
23
+ created_at: Time.at(object.created),
24
+ currency: object.currency,
25
+ stripe_account: owner.stripe_account
26
+ }
27
+
28
+ # Associate charge with subscription if we can
29
+ if object.invoice
30
+ invoice = (object.invoice.is_a?(::Stripe::Invoice) ? object.invoice : ::Stripe::Invoice.retrieve(object.invoice))
31
+ attrs[:subscription] = Pay::Subscription.find_by(processor: :stripe, processor_id: invoice.subscription)
32
+ end
33
+
34
+ # Update or create the charge
35
+ processor_details = {processor: :stripe, processor_id: object.id}
36
+ if (pay_charge = owner.charges.find_by(processor_details))
37
+ pay_charge.with_lock do
38
+ pay_charge.update!(attrs)
39
+ end
40
+ pay_charge
41
+ else
42
+ owner.charges.create!(attrs.merge(processor_details))
43
+ end
44
+ rescue ActiveRecord::RecordInvalid
45
+ try += 1
46
+ if try <= retries
47
+ sleep 0.1
48
+ retry
49
+ else
50
+ raise
51
+ end
52
+ end
53
+
8
54
  def initialize(pay_charge)
9
55
  @pay_charge = pay_charge
10
56
  end
11
57
 
12
58
  def charge
13
- ::Stripe::Charge.retrieve({id: processor_id, expand: ["customer", "invoice.subscription"]}, {stripe_account: stripe_account})
59
+ ::Stripe::Charge.retrieve({id: processor_id, expand: ["customer", "invoice.subscription"]}, stripe_options)
14
60
  rescue ::Stripe::StripeError => e
15
61
  raise Pay::Stripe::Error, e
16
62
  end
@@ -21,11 +67,18 @@ module Pay
21
67
  # refund!(5_00)
22
68
  # refund!(5_00, refund_application_fee: true)
23
69
  def refund!(amount_to_refund, **options)
24
- ::Stripe::Refund.create(options.merge(charge: processor_id, amount: amount_to_refund), {stripe_account: stripe_account})
70
+ ::Stripe::Refund.create(options.merge(charge: processor_id, amount: amount_to_refund), stripe_options)
25
71
  pay_charge.update(amount_refunded: amount_to_refund)
26
72
  rescue ::Stripe::StripeError => e
27
73
  raise Pay::Stripe::Error, e
28
74
  end
75
+
76
+ private
77
+
78
+ # Options for Stripe requests
79
+ def stripe_options
80
+ {stripe_account: stripe_account}.compact
81
+ end
29
82
  end
30
83
  end
31
84
  end
@@ -20,6 +20,54 @@ module Pay
20
20
  :trial_ends_at,
21
21
  to: :pay_subscription
22
22
 
23
+ def self.sync(subscription_id, object: nil, name: Pay.default_product_name, try: 0, retries: 1)
24
+ # Skip loading the latest subscription details from the API if we already have it
25
+ object ||= ::Stripe::Subscription.retrieve({id: subscription_id, expand: ["pending_setup_intent", "latest_invoice.payment_intent"]})
26
+
27
+ owner = Pay.find_billable(processor: :stripe, processor_id: object.customer)
28
+ return unless owner
29
+
30
+ attributes = {
31
+ application_fee_percent: object.application_fee_percent,
32
+ processor_plan: object.plan.id,
33
+ quantity: object.quantity,
34
+ name: name,
35
+ status: object.status,
36
+ stripe_account: owner.stripe_account,
37
+ trial_ends_at: (object.trial_end ? Time.at(object.trial_end) : nil)
38
+ }
39
+
40
+ attributes[:ends_at] = if object.ended_at
41
+ # Fully cancelled subscription
42
+ Time.at(object.ended_at)
43
+ elsif object.cancel_at
44
+ # subscription cancelling in the future
45
+ Time.at(object.cancel_at)
46
+ elsif object.cancel_at_period_end
47
+ # Subscriptions cancelling in the future
48
+ Time.at(object.current_period_end)
49
+ end
50
+
51
+ # Update or create the subscription
52
+ processor_details = {processor: :stripe, processor_id: object.id}
53
+ if (pay_subscription = owner.subscriptions.find_by(processor_details))
54
+ pay_subscription.with_lock do
55
+ pay_subscription.update!(attributes)
56
+ end
57
+ pay_subscription
58
+ else
59
+ owner.subscriptions.create!(attributes.merge(processor_details))
60
+ end
61
+ rescue ActiveRecord::RecordInvalid
62
+ try += 1
63
+ if try <= retries
64
+ sleep 0.1
65
+ retry
66
+ else
67
+ raise
68
+ end
69
+ end
70
+
23
71
  def initialize(pay_subscription)
24
72
  @pay_subscription = pay_subscription
25
73
  end
@@ -29,21 +77,21 @@ module Pay
29
77
  end
30
78
 
31
79
  def cancel
32
- stripe_sub = ::Stripe::Subscription.update(processor_id, {cancel_at_period_end: true}, {stripe_account: stripe_account})
80
+ stripe_sub = ::Stripe::Subscription.update(processor_id, {cancel_at_period_end: true}, stripe_options)
33
81
  pay_subscription.update(ends_at: (on_trial? ? trial_ends_at : Time.at(stripe_sub.current_period_end)))
34
82
  rescue ::Stripe::StripeError => e
35
83
  raise Pay::Stripe::Error, e
36
84
  end
37
85
 
38
86
  def cancel_now!
39
- ::Stripe::Subscription.delete(processor_id, {stripe_account: stripe_account})
87
+ ::Stripe::Subscription.delete(processor_id, {}, stripe_options)
40
88
  pay_subscription.update(ends_at: Time.current, status: :canceled)
41
89
  rescue ::Stripe::StripeError => e
42
90
  raise Pay::Stripe::Error, e
43
91
  end
44
92
 
45
93
  def change_quantity(quantity)
46
- ::Stripe::Subscription.update(processor_id, quantity: quantity)
94
+ ::Stripe::Subscription.update(processor_id, {quantity: quantity}, stripe_options)
47
95
  rescue ::Stripe::StripeError => e
48
96
  raise Pay::Stripe::Error, e
49
97
  end
@@ -72,13 +120,15 @@ module Pay
72
120
  trial_end: (on_trial? ? trial_ends_at.to_i : "now"),
73
121
  cancel_at_period_end: false
74
122
  },
75
- {stripe_account: stripe_account}
123
+ stripe_options
76
124
  )
77
125
  rescue ::Stripe::StripeError => e
78
126
  raise Pay::Stripe::Error, e
79
127
  end
80
128
 
81
129
  def swap(plan)
130
+ raise ArgumentError, "plan must be a string" unless plan.is_a?(String)
131
+
82
132
  ::Stripe::Subscription.update(
83
133
  processor_id,
84
134
  {
@@ -88,11 +138,18 @@ module Pay
88
138
  trial_end: (on_trial? ? trial_ends_at.to_i : "now"),
89
139
  quantity: quantity
90
140
  },
91
- {stripe_account: stripe_account}
141
+ stripe_options
92
142
  )
93
143
  rescue ::Stripe::StripeError => e
94
144
  raise Pay::Stripe::Error, e
95
145
  end
146
+
147
+ private
148
+
149
+ # Options for Stripe requests
150
+ def stripe_options
151
+ {stripe_account: stripe_account}.compact
152
+ end
96
153
  end
97
154
  end
98
155
  end