pay 2.3.1 → 2.5.0

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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +7 -5
  3. data/app/controllers/pay/webhooks/braintree_controller.rb +7 -53
  4. data/app/controllers/pay/webhooks/paddle_controller.rb +19 -18
  5. data/app/controllers/pay/webhooks/stripe_controller.rb +47 -0
  6. data/app/mailers/pay/user_mailer.rb +14 -35
  7. data/app/models/pay/subscription.rb +12 -18
  8. data/app/views/pay/user_mailer/payment_action_required.html.erb +1 -1
  9. data/app/views/pay/user_mailer/receipt.html.erb +6 -6
  10. data/app/views/pay/user_mailer/refund.html.erb +6 -6
  11. data/app/views/pay/user_mailer/subscription_renewing.html.erb +1 -1
  12. data/config/locales/en.yml +137 -0
  13. data/config/routes.rb +1 -1
  14. data/db/migrate/20200603134434_add_data_to_pay_models.rb +6 -1
  15. data/lib/pay.rb +19 -50
  16. data/lib/pay/billable.rb +9 -9
  17. data/lib/pay/braintree.rb +1 -0
  18. data/lib/pay/braintree/billable.rb +11 -11
  19. data/lib/pay/braintree/charge.rb +3 -3
  20. data/lib/pay/braintree/subscription.rb +21 -13
  21. data/lib/pay/braintree/webhooks.rb +11 -0
  22. data/lib/pay/braintree/webhooks/subscription_canceled.rb +19 -0
  23. data/lib/pay/braintree/webhooks/subscription_charged_successfully.rb +24 -0
  24. data/lib/pay/braintree/webhooks/subscription_charged_unsuccessfully.rb +24 -0
  25. data/lib/pay/braintree/webhooks/subscription_expired.rb +19 -0
  26. data/lib/pay/braintree/webhooks/subscription_trial_ended.rb +19 -0
  27. data/lib/pay/braintree/webhooks/subscription_went_active.rb +19 -0
  28. data/lib/pay/braintree/webhooks/subscription_went_past_due.rb +19 -0
  29. data/lib/pay/engine.rb +0 -1
  30. data/lib/pay/errors.rb +73 -0
  31. data/lib/pay/paddle/billable.rb +34 -5
  32. data/lib/pay/paddle/charge.rb +2 -2
  33. data/lib/pay/paddle/subscription.rb +22 -13
  34. data/lib/pay/paddle/webhooks.rb +8 -0
  35. data/lib/pay/paddle/webhooks/subscription_cancelled.rb +3 -3
  36. data/lib/pay/paddle/webhooks/subscription_created.rb +15 -15
  37. data/lib/pay/paddle/webhooks/subscription_payment_refunded.rb +5 -5
  38. data/lib/pay/paddle/webhooks/subscription_payment_succeeded.rb +17 -38
  39. data/lib/pay/paddle/webhooks/subscription_updated.rb +20 -17
  40. data/lib/pay/receipts.rb +6 -6
  41. data/lib/pay/stripe.rb +3 -1
  42. data/lib/pay/stripe/billable.rb +4 -4
  43. data/lib/pay/stripe/charge.rb +2 -2
  44. data/lib/pay/stripe/subscription.rb +20 -12
  45. data/lib/pay/stripe/webhooks.rb +14 -15
  46. data/lib/pay/stripe/webhooks/charge_refunded.rb +2 -2
  47. data/lib/pay/stripe/webhooks/charge_succeeded.rb +1 -1
  48. data/lib/pay/stripe/webhooks/payment_action_required.rb +1 -1
  49. data/lib/pay/stripe/webhooks/subscription_created.rb +2 -1
  50. data/lib/pay/stripe/webhooks/subscription_renewing.rb +4 -3
  51. data/lib/pay/version.rb +1 -1
  52. data/lib/pay/webhooks/delegator.rb +61 -0
  53. metadata +21 -88
data/config/routes.rb CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Pay::Engine.routes.draw do
4
4
  resources :payments, only: [:show], module: :pay
5
- post "webhooks/stripe", to: "stripe_event/webhook#event"
5
+ post "webhooks/stripe", to: "pay/webhooks/stripe#create"
6
6
  post "webhooks/braintree", to: "pay/webhooks/braintree#create"
7
7
  post "webhooks/paddle", to: "pay/webhooks/paddle#create"
8
8
  end
@@ -5,7 +5,12 @@ class AddDataToPayModels < ActiveRecord::Migration[4.2]
5
5
  end
6
6
 
7
7
  def data_column_type
8
- case ActiveRecord::Base.configurations.default_hash.dig("adapter")
8
+ default_hash = ActiveRecord::Base.configurations.default_hash
9
+
10
+ # Rails 6.1 uses a symbol key instead of a string
11
+ adapter = default_hash.dig(:adapter) || default_hash.dig("adapter")
12
+
13
+ case adapter
9
14
  when "mysql2"
10
15
  :json
11
16
  when "postgresql"
data/lib/pay.rb CHANGED
@@ -1,9 +1,23 @@
1
+ require "pay/version"
1
2
  require "pay/engine"
2
3
  require "pay/billable"
3
4
  require "pay/receipts"
4
5
  require "pay/payment"
6
+ require "pay/errors"
5
7
 
6
8
  module Pay
9
+ module Webhooks
10
+ autoload :Delegator, "pay/webhooks/delegator"
11
+
12
+ class << self
13
+ delegate :configure, :instrument, to: :delegator
14
+
15
+ def delegator
16
+ @delegator ||= Delegator.new
17
+ end
18
+ end
19
+ end
20
+
7
21
  # Define who owns the subscription
8
22
  mattr_accessor :billable_class
9
23
  mattr_accessor :billable_table
@@ -35,18 +49,14 @@ module Pay
35
49
  mattr_accessor :send_emails
36
50
  @@send_emails = true
37
51
 
38
- mattr_accessor :email_receipt_subject
39
- @@email_receipt_subject = "Payment receipt"
40
- mattr_accessor :email_refund_subject
41
- @@email_refund_subject = "Payment refunded"
42
- mattr_accessor :email_renewing_subject
43
- @@email_renewing_subject = "Your upcoming subscription renewal"
44
- mattr_accessor :email_action_required_subject
45
- @@email_action_required_subject = "Confirm your payment"
46
-
47
52
  mattr_accessor :automount_routes
48
53
  @@automount_routes = true
49
54
 
55
+ mattr_accessor :default_product_name
56
+ @@default_product_name = "default"
57
+ mattr_accessor :default_plan_name
58
+ @@default_plan_name = "default"
59
+
50
60
  mattr_accessor :routes_path
51
61
  @@routes_path = "/pay"
52
62
 
@@ -101,45 +111,4 @@ module Pay
101
111
  business_address &&
102
112
  support_email
103
113
  end
104
-
105
- class Error < StandardError
106
- end
107
-
108
- class BraintreeError < Error
109
- attr_reader :result
110
-
111
- def initialize(result = nil)
112
- @result = result
113
- end
114
- end
115
-
116
- class BraintreeAuthorizationError < BraintreeError
117
- def message
118
- "Either the data you submitted is malformed and does not match the API or the API key you used may not be authorized to perform this action."
119
- end
120
- end
121
-
122
- class InvalidPaymentMethod < Error
123
- attr_reader :payment
124
-
125
- def initialize(payment)
126
- @payment = payment
127
- end
128
-
129
- def message
130
- "This payment attempt failed because of an invalid payment method."
131
- end
132
- end
133
-
134
- class ActionRequired < Error
135
- attr_reader :payment
136
-
137
- def initialize(payment)
138
- @payment = payment
139
- end
140
-
141
- def message
142
- "This payment attempt failed because additional action is required before it can be completed."
143
- end
144
- end
145
114
  end
data/lib/pay/billable.rb CHANGED
@@ -36,7 +36,7 @@ module Pay
36
36
 
37
37
  def customer
38
38
  check_for_processor
39
- raise Pay::Error, "Email is required to create a customer" if email.nil?
39
+ raise Pay::Error, I18n.t("errors.email_required") if email.nil?
40
40
 
41
41
  customer = send("#{processor}_customer")
42
42
  update_card(card_token) if card_token.present?
@@ -52,7 +52,7 @@ module Pay
52
52
  send("create_#{processor}_charge", amount_in_cents, options)
53
53
  end
54
54
 
55
- def subscribe(name: "default", plan: "default", **options)
55
+ def subscribe(name: Pay.default_product_name, plan: Pay.default_plan_name, **options)
56
56
  check_for_processor
57
57
  send("create_#{processor}_subscription", name, plan, options)
58
58
  end
@@ -63,7 +63,7 @@ module Pay
63
63
  send("update_#{processor}_card", token)
64
64
  end
65
65
 
66
- def on_trial?(name: "default", plan: nil)
66
+ def on_trial?(name: Pay.default_product_name, plan: nil)
67
67
  return true if default_generic_trial?(name, plan)
68
68
 
69
69
  sub = subscription(name: name)
@@ -81,7 +81,7 @@ module Pay
81
81
  send("#{processor}_subscription", subscription_id, options)
82
82
  end
83
83
 
84
- def subscribed?(name: "default", processor_plan: nil)
84
+ def subscribed?(name: Pay.default_product_name, processor_plan: nil)
85
85
  subscription = subscription(name: name)
86
86
 
87
87
  return false if subscription.nil?
@@ -90,13 +90,13 @@ module Pay
90
90
  subscription.active? && subscription.processor_plan == processor_plan
91
91
  end
92
92
 
93
- def on_trial_or_subscribed?(name: "default", processor_plan: nil)
93
+ def on_trial_or_subscribed?(name: Pay.default_product_name, processor_plan: nil)
94
94
  on_trial?(name: name, plan: processor_plan) ||
95
95
  subscribed?(name: name, processor_plan: processor_plan)
96
96
  end
97
97
 
98
- def subscription(name: "default")
99
- subscriptions.for_name(name).last
98
+ def subscription(name: Pay.default_product_name)
99
+ subscriptions.loaded? ? subscriptions.reverse.detect { |s| s.name == name } : subscriptions.for_name(name).last
100
100
  end
101
101
 
102
102
  def invoice!(options = {})
@@ -123,14 +123,14 @@ module Pay
123
123
  processor == "paddle"
124
124
  end
125
125
 
126
- def has_incomplete_payment?(name: "default")
126
+ def has_incomplete_payment?(name: Pay.default_product_name)
127
127
  subscription(name: name)&.has_incomplete_payment?
128
128
  end
129
129
 
130
130
  private
131
131
 
132
132
  def check_for_processor
133
- raise StandardError, "No payment processor selected. Make sure to set the #{self.class.name}'s `processor` attribute to either 'stripe' or 'braintree'." unless processor
133
+ raise StandardError, I18n.t("errors.no_processor", class_name: self.class.name) unless processor
134
134
  end
135
135
 
136
136
  # Used for creating a Pay::Subscription in the database
data/lib/pay/braintree.rb CHANGED
@@ -2,6 +2,7 @@ require "pay/env"
2
2
  require "pay/braintree/billable"
3
3
  require "pay/braintree/charge"
4
4
  require "pay/braintree/subscription"
5
+ require "pay/braintree/webhooks"
5
6
 
6
7
  module Pay
7
8
  module Braintree
@@ -20,7 +20,7 @@ module Pay
20
20
  last_name: try(:last_name),
21
21
  payment_method_nonce: card_token
22
22
  )
23
- raise BraintreeError.new(result), result.message unless result.success?
23
+ raise Pay::Braintree::Error, result unless result.success?
24
24
 
25
25
  update(processor: "braintree", processor_id: result.customer.id)
26
26
 
@@ -33,7 +33,7 @@ module Pay
33
33
  rescue ::Braintree::AuthorizationError
34
34
  raise BraintreeAuthorizationError
35
35
  rescue ::Braintree::BraintreeError => e
36
- raise BraintreeError, e.message
36
+ raise Pay::Braintree::Error, e
37
37
  end
38
38
 
39
39
  # Handles Billable#charge
@@ -47,13 +47,13 @@ module Pay
47
47
  }.merge(options)
48
48
 
49
49
  result = gateway.transaction.sale(args)
50
- raise BraintreeError.new(result), result.message unless result.success?
50
+ raise Pay::Braintree::Error, result unless result.success?
51
51
 
52
52
  save_braintree_transaction(result.transaction)
53
53
  rescue ::Braintree::AuthorizationError
54
- raise BraintreeAuthorizationError
54
+ raise Pay::Braintree::AuthorizationError
55
55
  rescue ::Braintree::BraintreeError => e
56
- raise BraintreeError, e.message
56
+ raise Pay::Braintree::Error, e
57
57
  end
58
58
 
59
59
  # Handles Billable#subscribe
@@ -74,13 +74,13 @@ module Pay
74
74
  )
75
75
 
76
76
  result = gateway.subscription.create(subscription_options)
77
- raise BraintreeError.new(result), result.message unless result.success?
77
+ raise Pay::Braintree::Error, result unless result.success?
78
78
 
79
79
  create_subscription(result.subscription, "braintree", name, plan, status: :active)
80
80
  rescue ::Braintree::AuthorizationError
81
- raise BraintreeAuthorizationError
81
+ raise Pay::Braintree::AuthorizationError
82
82
  rescue ::Braintree::BraintreeError => e
83
- raise BraintreeError, e.message
83
+ raise Pay::Braintree::Error, e
84
84
  end
85
85
 
86
86
  # Handles Billable#update_card
@@ -95,15 +95,15 @@ module Pay
95
95
  verify_card: true
96
96
  }
97
97
  )
98
- raise BraintreeError.new(result), result.message unless result.success?
98
+ raise Pay::Braintree::Error, result unless result.success?
99
99
 
100
100
  update_braintree_card_on_file result.payment_method
101
101
  update_subscriptions_to_payment_method(result.payment_method.token)
102
102
  true
103
103
  rescue ::Braintree::AuthorizationError
104
- raise BraintreeAuthorizationError
104
+ raise Pay::Braintree::AuthorizationError
105
105
  rescue ::Braintree::BraintreeError => e
106
- raise BraintreeError, e.message
106
+ raise Pay::Braintree::Error, e
107
107
  end
108
108
 
109
109
  def update_braintree_email!
@@ -13,8 +13,8 @@ module Pay
13
13
 
14
14
  def braintree_charge
15
15
  Pay.braintree_gateway.transaction.find(processor_id)
16
- rescue ::Braintree::BraintreeError => e
17
- raise Error, e.message
16
+ rescue ::Braintree::Braintree::Error => e
17
+ raise Pay::Braintree::Error, e
18
18
  end
19
19
 
20
20
  def braintree_refund!(amount_to_refund)
@@ -22,7 +22,7 @@ module Pay
22
22
 
23
23
  update(amount_refunded: amount_to_refund)
24
24
  rescue ::Braintree::BraintreeError => e
25
- raise Error, e.message
25
+ raise Pay::Braintree::Error, e
26
26
  end
27
27
  end
28
28
  end
@@ -3,14 +3,6 @@ module Pay
3
3
  module Subscription
4
4
  extend ActiveSupport::Concern
5
5
 
6
- included do
7
- scope :braintree, -> { where(processor: :braintree) }
8
- end
9
-
10
- def braintree?
11
- processor == "braintree"
12
- end
13
-
14
6
  def braintree_cancel
15
7
  subscription = processor_subscription
16
8
 
@@ -24,17 +16,33 @@ module Pay
24
16
  update(status: :canceled, ends_at: subscription.billing_period_end_date.to_date)
25
17
  end
26
18
  rescue ::Braintree::BraintreeError => e
27
- raise Error, e.message
19
+ raise Pay::Braintree::Error, e
28
20
  end
29
21
 
30
22
  def braintree_cancel_now!
31
23
  gateway.subscription.cancel(processor_subscription.id)
32
24
  update(status: :canceled, ends_at: Time.zone.now)
33
25
  rescue ::Braintree::BraintreeError => e
34
- raise Error, e.message
26
+ raise Pay::Braintree::Error, e
27
+ end
28
+
29
+ def braintree_on_grace_period?
30
+ canceled? && Time.zone.now < ends_at
31
+ end
32
+
33
+ def braintree_paused?
34
+ false
35
+ end
36
+
37
+ def braintree_pause
38
+ raise NotImplementedError, "Braintree does not support pausing subscriptions"
35
39
  end
36
40
 
37
41
  def braintree_resume
42
+ unless on_grace_period?
43
+ raise StandardError, "You can only resume subscriptions within their grace period."
44
+ end
45
+
38
46
  if canceled? && on_trial?
39
47
  duration = trial_ends_at.to_date - Date.today
40
48
 
@@ -57,7 +65,7 @@ module Pay
57
65
 
58
66
  update(status: :active)
59
67
  rescue ::Braintree::BraintreeError => e
60
- raise Error, e.message
68
+ raise Pay::Braintree::Error, e
61
69
  end
62
70
 
63
71
  def braintree_swap(plan)
@@ -96,7 +104,7 @@ module Pay
96
104
  raise Error, "Braintree failed to swap plans: #{result.message}"
97
105
  end
98
106
  rescue ::Braintree::BraintreeError => e
99
- raise Error, e.message
107
+ raise Pay::Braintree::Error, e
100
108
  end
101
109
 
102
110
  private
@@ -174,7 +182,7 @@ module Pay
174
182
 
175
183
  cancel_now!
176
184
 
177
- owner.subscribe(options.merge(name: name, plan: plan.id))
185
+ owner.subscribe(**options.merge(name: name, plan: plan.id))
178
186
  end
179
187
  end
180
188
  end
@@ -0,0 +1,11 @@
1
+ Dir[File.join(__dir__, "webhooks", "**", "*.rb")].sort.each { |file| require file }
2
+
3
+ Pay::Webhooks.configure do |events|
4
+ events.subscribe "braintree.subscription_canceled", Pay::Braintree::Webhooks::SubscriptionCanceled.new
5
+ events.subscribe "braintree.subscription_charged_successfully", Pay::Braintree::Webhooks::SubscriptionChargedSuccessfully.new
6
+ events.subscribe "braintree.subscription_charged_unsuccessfully", Pay::Braintree::Webhooks::SubscriptionChargedUnsuccessfully.new
7
+ events.subscribe "braintree.subscription_expired", Pay::Braintree::Webhooks::SubscriptionExpired.new
8
+ events.subscribe "braintree.subscription_trial_ended", Pay::Braintree::Webhooks::SubscriptionTrialEnded.new
9
+ events.subscribe "braintree.subscription_went_active", Pay::Braintree::Webhooks::SubscriptionWentActive.new
10
+ events.subscribe "braintree.subscription_went_past_due", Pay::Braintree::Webhooks::SubscriptionWentPastDue.new
11
+ end
@@ -0,0 +1,19 @@
1
+ # A subscription is canceled.
2
+
3
+ module Pay
4
+ module Braintree
5
+ module Webhooks
6
+ class SubscriptionCanceled
7
+ def call(event)
8
+ subscription = event.subscription
9
+ return if subscription.nil?
10
+
11
+ pay_subscription = Pay.subscription_model.find_by(processor: :braintree, processor_id: subscription.id)
12
+ return unless pay_subscription.present?
13
+
14
+ pay_subscription.update!(ends_at: Time.current, status: :canceled)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,24 @@
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 SubscriptionChargedSuccessfully
7
+ def call(event)
8
+ subscription = event.subscription
9
+ return if subscription.nil?
10
+
11
+ pay_subscription = Pay.subscription_model.find_by(processor: :braintree, processor_id: subscription.id)
12
+ return unless pay_subscription.present?
13
+
14
+ billable = pay_subscription.owner
15
+ charge = billable.save_braintree_transaction(subscription.transactions.first)
16
+
17
+ if Pay.send_emails
18
+ Pay::UserMailer.with(billable: billable, charge: charge).receipt.deliver_later
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,24 @@
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_model.find_by(processor: :braintree, processor_id: subscription.id)
12
+ return unless pay_subscription.present?
13
+
14
+ # billable = pay_subscription.owner
15
+ # charge = billable.save_braintree_transaction(subscription.transactions.first)
16
+
17
+ # if Pay.send_emails
18
+ # Pay::UserMailer.with(billable: billable, charge: charge).receipt.deliver_later
19
+ # end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end