pay 11.7.2 → 12.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -0
  3. data/app/controllers/pay/payments_controller.rb +5 -12
  4. data/app/controllers/pay/webhooks/base_controller.rb +50 -0
  5. data/app/controllers/pay/webhooks/braintree_controller.rb +11 -18
  6. data/app/controllers/pay/webhooks/lemon_squeezy_controller.rb +8 -26
  7. data/app/controllers/pay/webhooks/paddle_billing_controller.rb +13 -35
  8. data/app/controllers/pay/webhooks/paddle_classic_controller.rb +5 -20
  9. data/app/controllers/pay/webhooks/stripe_controller.rb +13 -29
  10. data/app/models/pay/braintree/charge.rb +6 -12
  11. data/app/models/pay/braintree/payment_method.rb +7 -5
  12. data/app/models/pay/braintree/subscription.rb +39 -44
  13. data/app/models/pay/charge.rb +1 -1
  14. data/app/models/pay/customer.rb +1 -9
  15. data/app/models/pay/lemon_squeezy/charge.rb +63 -61
  16. data/app/models/pay/lemon_squeezy/customer.rb +1 -1
  17. data/app/models/pay/lemon_squeezy/subscription.rb +43 -46
  18. data/app/models/pay/merchant.rb +1 -1
  19. data/app/models/pay/paddle_billing/charge.rb +43 -53
  20. data/app/models/pay/paddle_billing/customer.rb +1 -1
  21. data/app/models/pay/paddle_billing/payment_method.rb +19 -23
  22. data/app/models/pay/paddle_billing/subscription.rb +56 -67
  23. data/app/models/pay/paddle_classic/customer.rb +1 -1
  24. data/app/models/pay/paddle_classic/subscription.rb +42 -44
  25. data/app/models/pay/payment_method.rb +1 -5
  26. data/app/models/pay/stripe/charge.rb +54 -59
  27. data/app/models/pay/stripe/customer.rb +45 -23
  28. data/app/models/pay/stripe/merchant.rb +1 -1
  29. data/app/models/pay/stripe/payment_method.rb +27 -29
  30. data/app/models/pay/stripe/subscription.rb +128 -119
  31. data/app/models/pay/subscription.rb +2 -12
  32. data/app/views/layouts/pay/application.html.erb +0 -2
  33. data/app/views/pay/payments/show.html.erb +19 -8
  34. data/app/views/pay/user_mailer/payment_action_required.html.erb +1 -1
  35. data/app/views/pay/user_mailer/payment_action_required.text.erb +1 -1
  36. data/lib/pay/braintree/webhooks/subscription.rb +15 -0
  37. data/lib/pay/braintree/webhooks/subscription_canceled.rb +1 -7
  38. data/lib/pay/braintree/webhooks/subscription_expired.rb +1 -7
  39. data/lib/pay/braintree/webhooks/subscription_trial_ended.rb +1 -7
  40. data/lib/pay/braintree/webhooks/subscription_went_active.rb +2 -8
  41. data/lib/pay/braintree/webhooks/subscription_went_past_due.rb +2 -8
  42. data/lib/pay/braintree.rb +1 -2
  43. data/lib/pay/currency.rb +0 -4
  44. data/lib/pay/engine.rb +4 -0
  45. data/lib/pay/errors.rb +4 -0
  46. data/lib/pay/lemon_squeezy.rb +4 -7
  47. data/lib/pay/paddle_billing.rb +4 -1
  48. data/lib/pay/paddle_classic.rb +4 -1
  49. data/lib/pay/payment.rb +11 -17
  50. data/lib/pay/receipts.rb +0 -1
  51. data/lib/pay/stripe/webhooks/checkout_session_completed.rb +8 -4
  52. data/lib/pay/stripe/webhooks/invoice_updated.rb +4 -1
  53. data/lib/pay/stripe/webhooks/payment_action_required.rb +3 -2
  54. data/lib/pay/stripe/webhooks/subscription_created.rb +2 -4
  55. data/lib/pay/stripe/webhooks/subscription_deleted.rb +2 -5
  56. data/lib/pay/stripe.rb +3 -3
  57. data/lib/pay/sync.rb +29 -0
  58. data/lib/pay/version.rb +1 -1
  59. data/lib/pay.rb +7 -5
  60. metadata +5 -3
  61. data/lib/pay/braintree/webhooks/subscription_charged_unsuccessfully.rb +0 -24
data/lib/pay/engine.rb CHANGED
@@ -17,6 +17,10 @@ module Pay
17
17
  end
18
18
  end
19
19
 
20
+ initializer "pay.deprecator" do |app|
21
+ app.deprecators[:pay] = Pay.deprecator if app.respond_to?(:deprecators)
22
+ end
23
+
20
24
  initializer "pay.receipts" do
21
25
  if defined?(::Receipts::VERSION)
22
26
  raise "[Pay] receipts gem must be version ~> 2" unless Pay::Engine.version_matches?(required: "~> 2", current: ::Receipts::VERSION)
data/lib/pay/errors.rb CHANGED
@@ -3,6 +3,10 @@ module Pay
3
3
  class Error < StandardError
4
4
  end
5
5
 
6
+ # Raised when a payment processor cannot perform an operation, such as pausing a Braintree subscription
7
+ class NotSupportedError < Error
8
+ end
9
+
6
10
  class PaymentError < Error
7
11
  attr_reader :payment
8
12
 
@@ -1,7 +1,10 @@
1
1
  module Pay
2
2
  module LemonSqueezy
3
3
  class Error < Pay::Error
4
- delegate :message, to: :cause
4
+ # Wrapped processor errors report the original message; errors raised with a string keep theirs
5
+ def message
6
+ cause&.message || super
7
+ end
5
8
  end
6
9
 
7
10
  module Webhooks
@@ -38,12 +41,6 @@ module Pay
38
41
  owner.to_sgid.to_s
39
42
  end
40
43
 
41
- def self.owner_from_passthrough(passthrough)
42
- GlobalID::Locator.locate_signed passthrough
43
- rescue JSON::ParserError
44
- nil
45
- end
46
-
47
44
  def self.configure_webhooks
48
45
  Pay::Webhooks.configure do |events|
49
46
  events.subscribe "lemon_squeezy.order_created", Pay::LemonSqueezy::Webhooks::Order.new
@@ -1,7 +1,10 @@
1
1
  module Pay
2
2
  module PaddleBilling
3
3
  class Error < Pay::Error
4
- delegate :message, to: :cause
4
+ # Wrapped processor errors report the original message; errors raised with a string keep theirs
5
+ def message
6
+ cause&.message || super
7
+ end
5
8
  end
6
9
 
7
10
  module Webhooks
@@ -1,7 +1,10 @@
1
1
  module Pay
2
2
  module PaddleClassic
3
3
  class Error < Pay::Error
4
- delegate :message, to: :cause
4
+ # Wrapped processor errors report the original message; errors raised with a string keep theirs
5
+ def message
6
+ cause&.message || super
7
+ end
5
8
  end
6
9
 
7
10
  module Webhooks
data/lib/pay/payment.rb CHANGED
@@ -1,16 +1,22 @@
1
1
  module Pay
2
2
  class Payment
3
- attr_reader :intent
3
+ # The Stripe Connect account the intent lives on, if any. Needed to look the intent up again
4
+ # and to initialize Stripe.js on the SCA confirmation page.
5
+ attr_reader :intent, :stripe_account
4
6
 
5
7
  delegate :id, :amount, :client_secret, :currency, :customer, :status, :confirm, to: :intent
6
8
 
7
- def self.from_id(id)
8
- intent = id.start_with?("seti_") ? ::Stripe::SetupIntent.retrieve(id) : ::Stripe::PaymentIntent.retrieve(id)
9
- new(intent)
9
+ def self.from_id(id, stripe_account: nil)
10
+ options = {stripe_account: stripe_account}.compact
11
+ intent = id.start_with?("seti_") ? ::Stripe::SetupIntent.retrieve(id, options) : ::Stripe::PaymentIntent.retrieve(id, options)
12
+ new(intent, stripe_account: stripe_account)
13
+ rescue ::Stripe::StripeError => e
14
+ raise Pay::Stripe::Error, e
10
15
  end
11
16
 
12
- def initialize(intent)
17
+ def initialize(intent, stripe_account: nil)
13
18
  @intent = intent
19
+ @stripe_account = stripe_account
14
20
  end
15
21
 
16
22
  def requires_payment_method?
@@ -25,22 +31,10 @@ module Pay
25
31
  status == "canceled"
26
32
  end
27
33
 
28
- def cancelled?
29
- canceled?
30
- end
31
-
32
34
  def succeeded?
33
35
  status == "succeeded"
34
36
  end
35
37
 
36
- def payment_intent?
37
- intent.is_a?(::Stripe::PaymentIntent)
38
- end
39
-
40
- def setup_intent?
41
- intent.is_a?(::Stripe::SetupIntent)
42
- end
43
-
44
38
  def amount_with_currency
45
39
  Pay::Currency.format(amount, currency: currency)
46
40
  end
data/lib/pay/receipts.rb CHANGED
@@ -3,7 +3,6 @@ module Pay
3
3
  def receipt_filename
4
4
  "receipt-#{created_at.strftime("%Y-%m-%d")}.pdf"
5
5
  end
6
- alias_method :filename, :receipt_filename
7
6
 
8
7
  def receipt
9
8
  receipt_pdf.render
@@ -3,7 +3,7 @@ module Pay
3
3
  module Webhooks
4
4
  class CheckoutSessionCompleted
5
5
  def call(event)
6
- locate_owner(event.data.object)
6
+ locate_owner(event.data.object, stripe_account: event.try(:account))
7
7
 
8
8
  # By the time CheckoutSessionCompleted is fired, we probably missed the original events
9
9
  # Instead, we can sync the payment intent or subscription during this event to ensure they're in the database
@@ -18,11 +18,15 @@ module Pay
18
18
  end
19
19
  end
20
20
 
21
- def locate_owner(object)
22
- return if object.client_reference_id.nil?
21
+ # Associates the Stripe customer from the session with the record referenced by client_reference_id
22
+ #
23
+ # Sessions in payment or setup mode don't always create a Stripe customer (for example Payment Links with
24
+ # customer_creation: if_required), so skip those rather than clearing an existing processor_id
25
+ def locate_owner(object, stripe_account: nil)
26
+ return if object.client_reference_id.nil? || object.customer.nil?
23
27
 
24
28
  owner = Pay::Stripe.find_by_client_reference_id(object.client_reference_id)
25
- owner&.add_payment_processor(:stripe, processor_id: object.customer)
29
+ owner&.add_payment_processor(:stripe, processor_id: object.customer, stripe_account: stripe_account)
26
30
  end
27
31
  end
28
32
  end
@@ -19,10 +19,13 @@ module Pay
19
19
  return unless subscription
20
20
 
21
21
  # Grab the local latest_invoice from the subscription stripe_object
22
+ # It may be an expanded object, a bare ID, or nil for a subscription that hasn't been invoiced yet
22
23
  latest_invoice = subscription.stripe_object.try(:latest_invoice)
24
+ latest_invoice_id = latest_invoice.try(:id) || latest_invoice
25
+ return if latest_invoice_id.blank?
23
26
 
24
27
  # Compare the local invoice id to the event invoice id and sync if they are the same
25
- if latest_invoice.id.to_s == invoice.id.to_s
28
+ if latest_invoice_id.to_s == invoice.id.to_s
26
29
  Pay::Stripe::Subscription.sync(subscription_id, stripe_account: event.try(:account))
27
30
  end
28
31
  rescue ::Stripe::StripeError => e
@@ -13,13 +13,14 @@ module Pay
13
13
  pay_subscription = Pay::Subscription.find_by_processor_and_id(:stripe, subscription_id)
14
14
  return if pay_subscription.nil? || pay_subscription.status == "incomplete"
15
15
 
16
- invoice_payment = ::Stripe::InvoicePayment.list({invoice: invoice.id, status: :open}).first
16
+ invoice_payment = ::Stripe::InvoicePayment.list({invoice: invoice.id, status: :open}, {stripe_account: event.try(:account)}.compact).first
17
17
 
18
18
  if invoice_payment && Pay.send_email?(:payment_action_required, pay_subscription)
19
19
  Pay.mailer.with(
20
20
  pay_customer: pay_subscription.customer,
21
21
  pay_subscription: pay_subscription,
22
- payment_intent_id: invoice_payment.payment.payment_intent
22
+ payment_intent_id: invoice_payment.payment.payment_intent,
23
+ stripe_account: event.try(:account)
23
24
  ).payment_action_required.deliver_later
24
25
  end
25
26
  end
@@ -1,10 +1,8 @@
1
1
  module Pay
2
2
  module Stripe
3
3
  module Webhooks
4
- class SubscriptionCreated
5
- def call(event)
6
- Pay::Stripe::Subscription.sync(event.data.object.id, stripe_account: event.try(:account))
7
- end
4
+ # If a subscription is manually created on Stripe, we want to sync it the same way as an update
5
+ class SubscriptionCreated < SubscriptionUpdated
8
6
  end
9
7
  end
10
8
  end
@@ -1,11 +1,8 @@
1
1
  module Pay
2
2
  module Stripe
3
3
  module Webhooks
4
- class SubscriptionDeleted
5
- def call(event)
6
- # Canceled subscriptions are still accessible via the API, so we can sync their details
7
- Pay::Stripe::Subscription.sync(event.data.object.id, stripe_account: event.try(:account))
8
- end
4
+ # Canceled subscriptions are still accessible via the API, so they sync the same way as an update
5
+ class SubscriptionDeleted < SubscriptionUpdated
9
6
  end
10
7
  end
11
8
  end
data/lib/pay/stripe.rb CHANGED
@@ -175,12 +175,12 @@ module Pay
175
175
  when "subscription"
176
176
  Pay::Stripe::Subscription.sync(checkout_session.subscription, stripe_account: stripe_account)
177
177
  end
178
- rescue ::Stripe::InvalidRequestError
178
+ rescue ::Stripe::InvalidRequestError => e
179
179
  if try > retries
180
- raise
180
+ raise Pay::Stripe::Error, e
181
181
  else
182
182
  try += 1
183
- sleep 0.15**try
183
+ sleep 0.15 * try
184
184
  retry
185
185
  end
186
186
  end
data/lib/pay/sync.rb ADDED
@@ -0,0 +1,29 @@
1
+ module Pay
2
+ # Shared skeleton for the processor Charge.sync, Subscription.sync, and PaymentMethod.sync class methods
3
+ module Sync
4
+ # Runs the sync block, retrying it when a webhook and an API call race to
5
+ # create the same record. The block retrieves the processor's object itself
6
+ # when the caller didn't pass one in, so each retry gets a fresh read.
7
+ def sync_with_retries(retries: 1)
8
+ try = 0
9
+ begin
10
+ yield
11
+ rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique
12
+ try += 1
13
+ raise if try > retries
14
+ sleep 0.15 * try
15
+ retry
16
+ end
17
+ end
18
+
19
+ # The Pay::Customer for this processor, or nil when the ID is blank or the customer isn't in the database
20
+ def find_pay_customer(processor_id)
21
+ Pay::Customer.find_by(processor: pay_processor, processor_id: processor_id) if processor_id.present?
22
+ end
23
+
24
+ # "stripe" for Pay::Stripe::Charge, "paddle_billing" for Pay::PaddleBilling::Subscription, and so on
25
+ def pay_processor
26
+ name.deconstantize.demodulize.underscore
27
+ end
28
+ end
29
+ end
data/lib/pay/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Pay
2
- VERSION = "11.7.2"
2
+ VERSION = "12.0.0"
3
3
  end
data/lib/pay.rb CHANGED
@@ -12,6 +12,7 @@ module Pay
12
12
  autoload :NanoId, "pay/nano_id"
13
13
  autoload :Payment, "pay/payment"
14
14
  autoload :Receipts, "pay/receipts"
15
+ autoload :Sync, "pay/sync"
15
16
  autoload :Currency, "pay/currency"
16
17
 
17
18
  # Payment processors
@@ -75,15 +76,16 @@ module Pay
75
76
  @@emails.subscription_trial_will_end = true
76
77
  @@emails.subscription_trial_ended = true
77
78
 
79
+ mattr_writer :mailer
78
80
  @@mailer = "Pay::UserMailer"
79
81
 
80
- def self.mailer=(value)
81
- @@mailer = value
82
- @@mailer_ref = nil
82
+ # Resolved on every call rather than memoized, so code reloading in development returns the current class
83
+ def self.mailer
84
+ @@mailer.constantize
83
85
  end
84
86
 
85
- def self.mailer
86
- @@mailer_ref ||= @@mailer&.constantize
87
+ def self.deprecator
88
+ @deprecator ||= ActiveSupport::Deprecation.new("13.0", "Pay")
87
89
  end
88
90
 
89
91
  mattr_accessor :parent_mailer
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pay
3
3
  version: !ruby/object:Gem::Version
4
- version: 11.7.2
4
+ version: 12.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jason Charnes
@@ -42,6 +42,7 @@ files:
42
42
  - app/assets/stylesheets/pay/application.css
43
43
  - app/controllers/pay/application_controller.rb
44
44
  - app/controllers/pay/payments_controller.rb
45
+ - app/controllers/pay/webhooks/base_controller.rb
45
46
  - app/controllers/pay/webhooks/braintree_controller.rb
46
47
  - app/controllers/pay/webhooks/lemon_squeezy_controller.rb
47
48
  - app/controllers/pay/webhooks/paddle_billing_controller.rb
@@ -115,9 +116,9 @@ files:
115
116
  - lib/pay/attributes.rb
116
117
  - lib/pay/billable/sync_customer.rb
117
118
  - lib/pay/braintree.rb
119
+ - lib/pay/braintree/webhooks/subscription.rb
118
120
  - lib/pay/braintree/webhooks/subscription_canceled.rb
119
121
  - lib/pay/braintree/webhooks/subscription_charged_successfully.rb
120
- - lib/pay/braintree/webhooks/subscription_charged_unsuccessfully.rb
121
122
  - lib/pay/braintree/webhooks/subscription_expired.rb
122
123
  - lib/pay/braintree/webhooks/subscription_trial_ended.rb
123
124
  - lib/pay/braintree/webhooks/subscription_went_active.rb
@@ -165,6 +166,7 @@ files:
165
166
  - lib/pay/stripe/webhooks/subscription_renewing.rb
166
167
  - lib/pay/stripe/webhooks/subscription_trial_will_end.rb
167
168
  - lib/pay/stripe/webhooks/subscription_updated.rb
169
+ - lib/pay/sync.rb
168
170
  - lib/pay/version.rb
169
171
  - lib/pay/webhooks.rb
170
172
  - lib/pay/webhooks/delegator.rb
@@ -189,7 +191,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
189
191
  - !ruby/object:Gem::Version
190
192
  version: '0'
191
193
  requirements: []
192
- rubygems_version: 4.0.18
194
+ rubygems_version: 4.0.21
193
195
  specification_version: 4
194
196
  summary: Payments engine for Ruby on Rails
195
197
  test_files: []
@@ -1,24 +0,0 @@
1
- # A subscription successfully moves to the next billing cycle. This will also occur when either a new transaction is created mid-cycle due to proration on an upgrade or a billing cycle is skipped due to the presence of a negative balance that covers the cost of the subscription.
2
-
3
- module Pay
4
- module Braintree
5
- module Webhooks
6
- class SubscriptionChargedUnsuccessfully
7
- def call(event)
8
- # subscription = event.subscription
9
- # return if subscription.nil?
10
-
11
- # pay_subscription = Pay::Subscription.find_by_processor_and_id(:braintree, subscription.id)
12
- # return unless pay_subscription.present?
13
-
14
- # pay_customer = pay_subscription.customer
15
- # pay_charge = Pay::Braintree::Billable.new(pay_customer).save_transaction(subscription.transactions.first)
16
-
17
- # if Pay.send_emails
18
- # Pay.mailer.with(pay_customer: pay_charge.customer, charge: pay_charge).receipt.deliver_later
19
- # end
20
- end
21
- end
22
- end
23
- end
24
- end