pay 2.6.8 → 2.7.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.

Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +73 -50
  3. data/app/models/pay.rb +5 -0
  4. data/app/models/pay/charge.rb +2 -0
  5. data/app/models/pay/subscription.rb +9 -2
  6. data/db/migrate/20200603134434_add_data_to_pay_models.rb +2 -18
  7. data/db/migrate/20210309004259_add_data_to_pay_billable.rb +10 -0
  8. data/db/migrate/20210406215234_add_currency_to_pay_charges.rb +5 -0
  9. data/db/migrate/20210406215506_add_application_fee_to_pay_models.rb +7 -0
  10. data/lib/generators/active_record/billable_generator.rb +44 -0
  11. data/lib/generators/active_record/merchant_generator.rb +44 -0
  12. data/lib/generators/active_record/templates/billable_migration.rb +16 -0
  13. data/lib/generators/active_record/templates/merchant_migration.rb +12 -0
  14. data/lib/generators/pay/{pay_generator.rb → billable_generator.rb} +2 -3
  15. data/lib/generators/pay/merchant_generator.rb +17 -0
  16. data/lib/generators/pay/orm_helpers.rb +10 -6
  17. data/lib/pay.rb +22 -0
  18. data/lib/pay/adapter.rb +22 -0
  19. data/lib/pay/billable.rb +4 -0
  20. data/lib/pay/braintree/billable.rb +11 -2
  21. data/lib/pay/braintree/subscription.rb +4 -0
  22. data/lib/pay/env.rb +8 -0
  23. data/lib/pay/fake_processor/subscription.rb +4 -0
  24. data/lib/pay/merchant.rb +37 -0
  25. data/lib/pay/paddle/subscription.rb +7 -0
  26. data/lib/pay/paddle/webhooks/subscription_payment_succeeded.rb +3 -1
  27. data/lib/pay/stripe.rb +11 -0
  28. data/lib/pay/stripe/billable.rb +38 -47
  29. data/lib/pay/stripe/charge.rb +38 -4
  30. data/lib/pay/stripe/merchant.rb +66 -0
  31. data/lib/pay/stripe/subscription.rb +64 -20
  32. data/lib/pay/stripe/webhooks/account_updated.rb +17 -0
  33. data/lib/pay/stripe/webhooks/charge_refunded.rb +2 -7
  34. data/lib/pay/stripe/webhooks/charge_succeeded.rb +2 -8
  35. data/lib/pay/stripe/webhooks/checkout_session_async_payment_succeeded.rb +13 -0
  36. data/lib/pay/stripe/webhooks/checkout_session_completed.rb +13 -0
  37. data/lib/pay/stripe/webhooks/payment_intent_succeeded.rb +2 -8
  38. data/lib/pay/stripe/webhooks/subscription_created.rb +1 -35
  39. data/lib/pay/stripe/webhooks/subscription_deleted.rb +1 -9
  40. data/lib/pay/stripe/webhooks/subscription_renewing.rb +4 -6
  41. data/lib/pay/stripe/webhooks/subscription_updated.rb +1 -28
  42. data/lib/pay/version.rb +1 -1
  43. metadata +19 -6
  44. data/lib/generators/active_record/pay_generator.rb +0 -58
  45. 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.rb CHANGED
@@ -1,10 +1,12 @@
1
1
  require "pay/version"
2
2
  require "pay/engine"
3
3
  require "pay/errors"
4
+ require "pay/adapter"
4
5
 
5
6
  module Pay
6
7
  autoload :Billable, "pay/billable"
7
8
  autoload :Env, "pay/env"
9
+ autoload :Merchant, "pay/merchant"
8
10
  autoload :Payment, "pay/payment"
9
11
  autoload :Receipts, "pay/receipts"
10
12
 
@@ -66,6 +68,26 @@ module Pay
66
68
  Pay::Billable.includers
67
69
  end
68
70
 
71
+ def self.merchant_models
72
+ Pay::Merchant.includers
73
+ end
74
+
75
+ def self.find_merchant(account_key, account_value)
76
+ merchant_models.each do |model|
77
+ case Pay::Adapter.current_adapter
78
+ when "postgresql"
79
+ return model.find_by("pay_data @> ?", {account_key.to_sym => account_value}.to_json)
80
+ when "mysql2"
81
+ return model.find_by("JSON_CONTAINS(pay_data, ?)", {account_key.to_sym => account_value}.to_json)
82
+ when "sqlite3"
83
+ return model.find_by("json_extract(pay_data, ?) =?", "$.#{account_key}", account_value)
84
+ else
85
+ model.find_by(pay_data: {account_key.to_sym => account_value})
86
+ end
87
+ end
88
+ nil
89
+ end
90
+
69
91
  def self.find_billable(processor:, processor_id:)
70
92
  billable_models.each do |model|
71
93
  if (record = model.find_by(processor: processor, processor_id: processor_id))
@@ -0,0 +1,22 @@
1
+ module Pay
2
+ module Adapter
3
+ extend ActiveSupport::Concern
4
+
5
+ def self.current_adapter
6
+ if ActiveRecord::Base.respond_to?(:connection_db_config)
7
+ ActiveRecord::Base.connection_db_config.adapter
8
+ else
9
+ ActiveRecord::Base.connection_config[:adapter]
10
+ end
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
21
+ end
22
+ end
data/lib/pay/billable.rb CHANGED
@@ -26,6 +26,10 @@ module Pay
26
26
  attribute :card_token, :string
27
27
  attribute :pay_fake_processor_allowed, :boolean, default: false
28
28
 
29
+ # Account(s) for marketplace payments
30
+ store_accessor :pay_data, :stripe_account
31
+ store_accessor :pay_data, :braintree_account
32
+
29
33
  validate :pay_fake_processor_is_allowed, if: :processor_changed?
30
34
  end
31
35
 
@@ -19,7 +19,9 @@ module Pay
19
19
  # Returns Braintree::Customer
20
20
  def customer
21
21
  if processor_id?
22
- gateway.customer.find(processor_id)
22
+ customer = gateway.customer.find(processor_id)
23
+ update_card card_token if card_token.present?
24
+ customer
23
25
  else
24
26
  result = gateway.customer.create(
25
27
  email: email,
@@ -149,9 +151,16 @@ module Pay
149
151
  attrs = card_details_for_braintree_transaction(transaction)
150
152
  attrs[:amount] = transaction.amount.to_f * 100
151
153
 
154
+ # Associate charge with subscription if we can
155
+ if transaction.subscription_id
156
+ attrs[:subscription] = Pay::Subscription.find_by(processor: :braintree, processor_id: transaction.subscription_id)
157
+ end
158
+
152
159
  charge = billable.charges.find_or_initialize_by(
153
160
  processor: :braintree,
154
- processor_id: transaction.id
161
+ processor_id: transaction.id,
162
+ currency: transaction.currency_iso_code,
163
+ application_fee_amount: transaction.service_fee_amount
155
164
  )
156
165
  charge.update(attrs)
157
166
  charge
@@ -23,6 +23,10 @@ module Pay
23
23
  @pay_subscription = pay_subscription
24
24
  end
25
25
 
26
+ def subscription(**options)
27
+ gateway.subscription.find(processor_id)
28
+ end
29
+
26
30
  def cancel
27
31
  subscription = processor_subscription
28
32
 
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
@@ -19,6 +19,10 @@ module Pay
19
19
  @pay_subscription = pay_subscription
20
20
  end
21
21
 
22
+ def subscription(**options)
23
+ pay_subscription
24
+ end
25
+
22
26
  def cancel
23
27
  pay_subscription.update(ends_at: Time.current.end_of_month)
24
28
  end
@@ -0,0 +1,37 @@
1
+ module Pay
2
+ module Merchant
3
+ extend ActiveSupport::Concern
4
+
5
+ # Keep track of which Merchant models we have
6
+ class << self
7
+ attr_reader :includers
8
+ end
9
+
10
+ def self.included(base = nil, &block)
11
+ @includers ||= []
12
+ @includers << base if base
13
+ super
14
+ end
15
+
16
+ included do
17
+ store_accessor :pay_data, :stripe_connect_account_id
18
+ store_accessor :pay_data, :onboarding_complete
19
+ end
20
+
21
+ def merchant
22
+ @merchant ||= merchant_processor_for(merchant_processor).new(self)
23
+ end
24
+
25
+ def merchant_processor_for(name)
26
+ "Pay::#{name.to_s.classify}::Merchant".constantize
27
+ end
28
+
29
+ def stripe_connect_account_id?
30
+ !!stripe_connect_account_id
31
+ end
32
+
33
+ def onboarding_complete?
34
+ !!onboarding_complete
35
+ end
36
+ end
37
+ end
@@ -24,6 +24,13 @@ module Pay
24
24
  @pay_subscription = pay_subscription
25
25
  end
26
26
 
27
+ def subscription(**options)
28
+ hash = PaddlePay::Subscription::User.list({subscription_id: processor_id}, options).try(:first)
29
+ OpenStruct.new(hash)
30
+ rescue ::PaddlePay::PaddlePayError => e
31
+ raise Pay::Paddle::Error, e
32
+ end
33
+
27
34
  def cancel
28
35
  subscription = processor_subscription
29
36
  PaddlePay::Subscription::User.cancel(processor_id)
@@ -30,8 +30,10 @@ module Pay
30
30
  params = {
31
31
  amount: Integer(event["sale_gross"].to_f * 100),
32
32
  card_type: event["payment_method"],
33
+ created_at: Time.zone.parse(event["event_time"]),
34
+ currency: event["currency"],
33
35
  paddle_receipt_url: event["receipt_url"],
34
- created_at: Time.zone.parse(event["event_time"])
36
+ subscription: Pay::Subscription.find_by(processor: :paddle, processor_id: event["subscription_id"])
35
37
  }
36
38
 
37
39
  payment_information = Pay::Paddle::Billable.new(user).payment_information(event["subscription_id"])
data/lib/pay/stripe.rb CHANGED
@@ -4,10 +4,14 @@ module Pay
4
4
  autoload :Charge, "pay/stripe/charge"
5
5
  autoload :Subscription, "pay/stripe/subscription"
6
6
  autoload :Error, "pay/stripe/error"
7
+ autoload :Merchant, "pay/stripe/merchant"
7
8
 
8
9
  module Webhooks
10
+ autoload :AccountUpdated, "pay/stripe/webhooks/account_updated"
9
11
  autoload :ChargeRefunded, "pay/stripe/webhooks/charge_refunded"
10
12
  autoload :ChargeSucceeded, "pay/stripe/webhooks/charge_succeeded"
13
+ autoload :CheckoutSessionCompleted, "pay/stripe/webhooks/checkout_session_completed"
14
+ autoload :CheckoutSessionAsyncPaymentSucceeded, "pay/stripe/webhooks/checkout_session_async_payment_succeeded"
11
15
  autoload :CustomerDeleted, "pay/stripe/webhooks/customer_deleted"
12
16
  autoload :CustomerUpdated, "pay/stripe/webhooks/customer_updated"
13
17
  autoload :PaymentActionRequired, "pay/stripe/webhooks/payment_action_required"
@@ -81,6 +85,13 @@ module Pay
81
85
  events.subscribe "stripe.payment_method.updated", Pay::Stripe::Webhooks::PaymentMethodUpdated.new
82
86
  events.subscribe "stripe.payment_method.card_automatically_updated", Pay::Stripe::Webhooks::PaymentMethodUpdated.new
83
87
  events.subscribe "stripe.payment_method.detached", Pay::Stripe::Webhooks::PaymentMethodUpdated.new
88
+
89
+ # If an account is updated in stripe, we should update it as well
90
+ events.subscribe "stripe.account.updated", Pay::Stripe::Webhooks::AccountUpdated.new
91
+
92
+ # Handle subscriptions in Stripe Checkout Sessions
93
+ events.subscribe "stripe.checkout.session.completed", Pay::Stripe::Webhooks::CheckoutSessionCompleted.new
94
+ events.subscribe "stripe.checkout.session.async_payment_succeeded", Pay::Stripe::Webhooks::CheckoutSessionAsyncPaymentSucceeded.new
84
95
  end
85
96
  end
86
97
  end
@@ -10,6 +10,7 @@ module Pay
10
10
  :email,
11
11
  :customer_name,
12
12
  :card_token,
13
+ :stripe_account,
13
14
  to: :billable
14
15
 
15
16
  class << self
@@ -26,23 +27,22 @@ module Pay
26
27
  #
27
28
  # Returns Stripe::Customer
28
29
  def customer
29
- if processor_id?
30
- ::Stripe::Customer.retrieve(processor_id)
30
+ stripe_customer = if processor_id?
31
+ ::Stripe::Customer.retrieve(processor_id, {stripe_account: stripe_account})
31
32
  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
33
+ sc = ::Stripe::Customer.create({email: email, name: customer_name}, {stripe_account: stripe_account})
34
+ billable.update(processor: :stripe, processor_id: sc.id, stripe_account: stripe_account)
35
+ sc
36
+ end
43
37
 
44
- stripe_customer
38
+ # Update the user's card on file if a token was passed in
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})
42
+ update_card_on_file(payment_method.card)
45
43
  end
44
+
45
+ stripe_customer
46
46
  rescue ::Stripe::StripeError => e
47
47
  raise Pay::Stripe::Error, e
48
48
  end
@@ -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)
64
+ payment_intent = ::Stripe::PaymentIntent.create(args, {stripe_account: stripe_account})
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
@@ -84,10 +85,14 @@ module Pay
84
85
  # Inherit trial from plan unless trial override was specified
85
86
  opts[:trial_from_plan] = true unless opts[:trial_period_days]
86
87
 
88
+ # Load the Stripe customer to verify it exists and update card if needed
87
89
  opts[:customer] = customer.id
88
90
 
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
+ # Create subscription on Stripe
92
+ stripe_sub = ::Stripe::Subscription.create(opts, {stripe_account: stripe_account})
93
+
94
+ # Save Pay::Subscription
95
+ subscription = Pay::Stripe::Subscription.sync(stripe_sub.id, object: stripe_sub, name: name)
91
96
 
92
97
  # No trial, card requires SCA
93
98
  if subscription.incomplete?
@@ -111,8 +116,8 @@ module Pay
111
116
 
112
117
  return true if payment_method_id == stripe_customer.invoice_settings.default_payment_method
113
118
 
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})
119
+ payment_method = ::Stripe::PaymentMethod.attach(payment_method_id, {customer: stripe_customer.id}, {stripe_account: stripe_account})
120
+ ::Stripe::Customer.update(stripe_customer.id, {invoice_settings: {default_payment_method: payment_method.id}}, {stripe_account: stripe_account})
116
121
 
117
122
  update_card_on_file(payment_method.card)
118
123
  true
@@ -121,33 +126,33 @@ module Pay
121
126
  end
122
127
 
123
128
  def update_email!
124
- ::Stripe::Customer.update(processor_id, {email: email, name: customer_name})
129
+ ::Stripe::Customer.update(processor_id, {email: email, name: customer_name}, {stripe_account: stripe_account})
125
130
  end
126
131
 
127
132
  def processor_subscription(subscription_id, options = {})
128
- ::Stripe::Subscription.retrieve(options.merge(id: subscription_id))
133
+ ::Stripe::Subscription.retrieve(options.merge(id: subscription_id), {stripe_account: stripe_account})
129
134
  end
130
135
 
131
136
  def invoice!(options = {})
132
137
  return unless processor_id?
133
- ::Stripe::Invoice.create(options.merge(customer: processor_id)).pay
138
+ ::Stripe::Invoice.create(options.merge(customer: processor_id), {stripe_account: stripe_account}).pay
134
139
  end
135
140
 
136
141
  def upcoming_invoice
137
- ::Stripe::Invoice.upcoming(customer: processor_id)
142
+ ::Stripe::Invoice.upcoming({customer: processor_id}, {stripe_account: stripe_account})
138
143
  end
139
144
 
140
145
  # Used by webhooks when the customer or source changes
141
146
  def sync_card_from_stripe
142
147
  if (payment_method_id = customer.invoice_settings.default_payment_method)
143
- update_card_on_file ::Stripe::PaymentMethod.retrieve(payment_method_id).card
148
+ update_card_on_file ::Stripe::PaymentMethod.retrieve(payment_method_id, {stripe_account: stripe_account}).card
144
149
  else
145
150
  billable.update(card_type: nil, card_last4: nil)
146
151
  end
147
152
  end
148
153
 
149
154
  def create_setup_intent
150
- ::Stripe::SetupIntent.create(customer: processor_id, usage: :off_session)
155
+ ::Stripe::SetupIntent.create({customer: processor_id, usage: :off_session}, {stripe_account: stripe_account})
151
156
  end
152
157
 
153
158
  def trial_end_date(stripe_sub)
@@ -167,21 +172,6 @@ module Pay
167
172
  billable.card_token = nil
168
173
  end
169
174
 
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
175
  # https://stripe.com/docs/api/checkout/sessions/create
186
176
  #
187
177
  # checkout(mode: "payment")
@@ -198,8 +188,8 @@ module Pay
198
188
  payment_method_types: ["card"],
199
189
  mode: "payment",
200
190
  # These placeholder URLs will be replaced in a following step.
201
- success_url: root_url,
202
- cancel_url: root_url
191
+ success_url: options.delete(:success_url) || root_url,
192
+ cancel_url: options.delete(:cancel_url) || root_url
203
193
  }
204
194
 
205
195
  # Line items are optional
@@ -213,7 +203,7 @@ module Pay
213
203
  }
214
204
  end
215
205
 
216
- ::Stripe::Checkout::Session.create(args.merge(options))
206
+ ::Stripe::Checkout::Session.create(args.merge(options), {stripe_account: stripe_account})
217
207
  end
218
208
 
219
209
  # https://stripe.com/docs/api/checkout/sessions/create
@@ -221,10 +211,11 @@ module Pay
221
211
  # checkout_charge(amount: 15_00, name: "T-shirt", quantity: 2)
222
212
  #
223
213
  def checkout_charge(amount:, name:, quantity: 1, **options)
214
+ currency = options.delete(:currency) || "usd"
224
215
  checkout(
225
216
  line_items: {
226
217
  price_data: {
227
- currency: options[:currency] || "usd",
218
+ currency: currency,
228
219
  product_data: {name: name},
229
220
  unit_amount: amount
230
221
  },
@@ -237,9 +228,9 @@ module Pay
237
228
  def billing_portal(**options)
238
229
  args = {
239
230
  customer: processor_id,
240
- return_url: options[:return_url] || root_url
231
+ return_url: options.delete(:return_url) || root_url
241
232
  }
242
- ::Stripe::BillingPortal::Session.create(args.merge(options))
233
+ ::Stripe::BillingPortal::Session.create(args.merge(options), {stripe_account: stripe_account})
243
234
  end
244
235
  end
245
236
  end