pay 2.3.0 → 2.4.4

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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -2
  3. data/app/controllers/pay/webhooks/braintree_controller.rb +1 -1
  4. data/app/mailers/pay/user_mailer.rb +14 -35
  5. data/app/models/pay/subscription.rb +12 -18
  6. data/app/views/pay/user_mailer/payment_action_required.html.erb +1 -1
  7. data/app/views/pay/user_mailer/receipt.html.erb +6 -6
  8. data/app/views/pay/user_mailer/refund.html.erb +6 -6
  9. data/app/views/pay/user_mailer/subscription_renewing.html.erb +1 -1
  10. data/config/locales/en.yml +137 -0
  11. data/db/migrate/20200603134434_add_data_to_pay_models.rb +6 -1
  12. data/lib/pay.rb +7 -41
  13. data/lib/pay/billable.rb +9 -9
  14. data/lib/pay/braintree/billable.rb +11 -11
  15. data/lib/pay/braintree/charge.rb +3 -3
  16. data/lib/pay/braintree/subscription.rb +21 -13
  17. data/lib/pay/errors.rb +73 -0
  18. data/lib/pay/paddle/billable.rb +34 -5
  19. data/lib/pay/paddle/charge.rb +2 -2
  20. data/lib/pay/paddle/subscription.rb +22 -13
  21. data/lib/pay/paddle/webhooks/subscription_cancelled.rb +1 -1
  22. data/lib/pay/paddle/webhooks/subscription_created.rb +1 -1
  23. data/lib/pay/paddle/webhooks/subscription_payment_refunded.rb +2 -2
  24. data/lib/pay/paddle/webhooks/subscription_payment_succeeded.rb +8 -29
  25. data/lib/pay/paddle/webhooks/subscription_updated.rb +15 -12
  26. data/lib/pay/receipts.rb +6 -6
  27. data/lib/pay/stripe.rb +3 -0
  28. data/lib/pay/stripe/billable.rb +4 -4
  29. data/lib/pay/stripe/charge.rb +2 -2
  30. data/lib/pay/stripe/subscription.rb +20 -12
  31. data/lib/pay/stripe/webhooks/charge_refunded.rb +2 -2
  32. data/lib/pay/stripe/webhooks/charge_succeeded.rb +1 -1
  33. data/lib/pay/stripe/webhooks/payment_action_required.rb +1 -1
  34. data/lib/pay/stripe/webhooks/subscription_created.rb +1 -1
  35. data/lib/pay/stripe/webhooks/subscription_renewing.rb +4 -3
  36. data/lib/pay/version.rb +1 -1
  37. metadata +11 -74
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
@@ -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
data/lib/pay/errors.rb ADDED
@@ -0,0 +1,73 @@
1
+ module Pay
2
+ class Error < StandardError
3
+ attr_reader :result
4
+
5
+ def initialize(result = nil)
6
+ @result = result
7
+ end
8
+ end
9
+
10
+ class PaymentError < StandardError
11
+ attr_reader :payment
12
+
13
+ def initialize(payment)
14
+ @payment = payment
15
+ end
16
+ end
17
+
18
+ class ActionRequired < PaymentError
19
+ def message
20
+ I18n.t("errors.action_required")
21
+ end
22
+ end
23
+
24
+ class InvalidPaymentMethod < PaymentError
25
+ def message
26
+ I18n.t("errors.invalid_payment")
27
+ end
28
+ end
29
+
30
+ module Braintree
31
+ class Error < Error
32
+ def message
33
+ result.message
34
+ end
35
+ end
36
+
37
+ class AuthorizationError < Braintree::Error
38
+ def message
39
+ I18n.t("errors.braintree.authorization")
40
+ end
41
+ end
42
+ end
43
+
44
+ module Stripe
45
+ class Error < Error
46
+ def message
47
+ I18n.t("errors.stripe.#{result.code}", default: result.message)
48
+ end
49
+ end
50
+ end
51
+
52
+ module Paddle
53
+ class Error < Error
54
+ def message
55
+ I18n.t("errors.paddle.#{result.code}", default: result.message)
56
+ end
57
+ end
58
+ end
59
+
60
+ class BraintreeError < Braintree::Error
61
+ def message
62
+ ActiveSupport::Deprecation.warn("Pay::BraintreeError is deprecated. Instead, use `Pay::Braintree::Error`.")
63
+ super
64
+ end
65
+ end
66
+
67
+ class BraintreeAuthorizationError < BraintreeError
68
+ def message
69
+ ActiveSupport::Deprecation.warn("Pay::BraintreeAuthorizationError is deprecated. Instead, use `Pay::Braintree::AuthorizationError`.")
70
+ I18n.t("errors.braintree.authorization")
71
+ end
72
+ end
73
+ end
@@ -23,11 +23,11 @@ module Pay
23
23
  amount: Integer(response[:amount].to_f * 100),
24
24
  card_type: subscription.processor_subscription.payment_information[:payment_method],
25
25
  paddle_receipt_url: response[:receipt_url],
26
- created_at: DateTime.parse(response[:payment_date])
26
+ created_at: Time.zone.parse(response[:payment_date])
27
27
  )
28
28
  charge
29
29
  rescue ::PaddlePay::PaddlePayError => e
30
- raise Error, e.message
30
+ raise Pay::Paddle::Error, e
31
31
  end
32
32
 
33
33
  def create_paddle_subscription(name, plan, options = {})
@@ -35,7 +35,7 @@ module Pay
35
35
  end
36
36
 
37
37
  def update_paddle_card(token)
38
- # pass
38
+ sync_payment_information_from_paddle
39
39
  end
40
40
 
41
41
  def update_paddle_email!
@@ -44,14 +44,14 @@ module Pay
44
44
 
45
45
  def paddle_trial_end_date(subscription)
46
46
  return unless subscription.state == "trialing"
47
- DateTime.parse(subscription.next_payment[:date]).end_of_day
47
+ Time.zone.parse(subscription.next_payment[:date]).end_of_day
48
48
  end
49
49
 
50
50
  def paddle_subscription(subscription_id, options = {})
51
51
  hash = PaddlePay::Subscription::User.list({subscription_id: subscription_id}, options).try(:first)
52
52
  OpenStruct.new(hash)
53
53
  rescue ::PaddlePay::PaddlePayError => e
54
- raise Error, e.message
54
+ raise Pay::Paddle::Error, e
55
55
  end
56
56
 
57
57
  def paddle_invoice!(options = {})
@@ -61,6 +61,35 @@ module Pay
61
61
  def paddle_upcoming_invoice
62
62
  # pass
63
63
  end
64
+
65
+ def sync_payment_information_from_paddle
66
+ payment_information = paddle_payment_information(subscription.processor_id)
67
+ update!(payment_information) unless payment_information.empty?
68
+ rescue ::PaddlePay::PaddlePayError => e
69
+ raise Pay::Paddle::Error, e
70
+ end
71
+
72
+ def paddle_payment_information(subscription_id)
73
+ subscription_user = PaddlePay::Subscription::User.list({subscription_id: subscription_id}).try(:first)
74
+ payment_information = subscription_user ? subscription_user[:payment_information] : nil
75
+ return {} if payment_information.nil?
76
+
77
+ case payment_information[:payment_method]
78
+ when "card"
79
+ {
80
+ card_type: payment_information[:card_type],
81
+ card_last4: payment_information[:last_four_digits],
82
+ card_exp_month: payment_information[:expiry_date].split("/").first,
83
+ card_exp_year: payment_information[:expiry_date].split("/").last
84
+ }
85
+ when "paypal"
86
+ {
87
+ card_type: "PayPal"
88
+ }
89
+ else
90
+ {}
91
+ end
92
+ end
64
93
  end
65
94
  end
66
95
  end
@@ -19,7 +19,7 @@ module Pay
19
19
  charges = payments.select { |p| p[:id].to_s == processor_id }
20
20
  charges.try(:first)
21
21
  rescue ::PaddlePay::PaddlePayError => e
22
- raise Error, e.message
22
+ raise Pay::Paddle::Error, e
23
23
  end
24
24
 
25
25
  def paddle_refund!(amount_to_refund)
@@ -32,7 +32,7 @@ module Pay
32
32
  raise Error, "Payment not found"
33
33
  end
34
34
  rescue ::PaddlePay::PaddlePayError => e
35
- raise Error, e.message
35
+ raise Pay::Paddle::Error, e
36
36
  end
37
37
  end
38
38
  end
@@ -4,14 +4,9 @@ module Pay
4
4
  extend ActiveSupport::Concern
5
5
 
6
6
  included do
7
- scope :paddle, -> { where(processor: :paddle) }
8
-
9
7
  store_accessor :data, :paddle_update_url
10
8
  store_accessor :data, :paddle_cancel_url
11
- end
12
-
13
- def paddle?
14
- processor == "paddle"
9
+ store_accessor :data, :paddle_paused_from
15
10
  end
16
11
 
17
12
  def paddle_cancel
@@ -20,31 +15,45 @@ module Pay
20
15
  if on_trial?
21
16
  update(status: :canceled, ends_at: trial_ends_at)
22
17
  else
23
- update(status: :canceled, ends_at: DateTime.parse(subscription.next_payment[:date]))
18
+ update(status: :canceled, ends_at: Time.zone.parse(subscription.next_payment[:date]))
24
19
  end
25
20
  rescue ::PaddlePay::PaddlePayError => e
26
- raise Error, e.message
21
+ raise Pay::Paddle::Error, e
27
22
  end
28
23
 
29
24
  def paddle_cancel_now!
30
25
  PaddlePay::Subscription::User.cancel(processor_id)
31
26
  update(status: :canceled, ends_at: Time.zone.now)
32
27
  rescue ::PaddlePay::PaddlePayError => e
33
- raise Error, e.message
28
+ raise Pay::Paddle::Error, e
29
+ end
30
+
31
+ def paddle_on_grace_period?
32
+ canceled? && Time.zone.now < ends_at || paused? && Time.zone.now < paddle_paused_from
33
+ end
34
+
35
+ def paddle_paused?
36
+ paddle_paused_from.present?
34
37
  end
35
38
 
36
39
  def paddle_pause
37
40
  attributes = {pause: true}
38
41
  response = PaddlePay::Subscription::User.update(processor_id, attributes)
39
- update(status: :paused, ends_at: DateTime.parse(response[:next_payment][:date]))
42
+ update(paddle_paused_from: Time.zone.parse(response[:next_payment][:date]))
43
+ rescue ::PaddlePay::PaddlePayError => e
44
+ raise Pay::Paddle::Error, e
40
45
  end
41
46
 
42
47
  def paddle_resume
48
+ unless paused?
49
+ raise StandardError, "You can only resume paused subscriptions."
50
+ end
51
+
43
52
  attributes = {pause: false}
44
53
  PaddlePay::Subscription::User.update(processor_id, attributes)
45
- update(status: :active, ends_at: nil)
54
+ update(status: :active, paddle_paused_from: nil)
46
55
  rescue ::PaddlePay::PaddlePayError => e
47
- raise Error, e.message
56
+ raise Pay::Paddle::Error, e
48
57
  end
49
58
 
50
59
  def paddle_swap(plan)
@@ -52,7 +61,7 @@ module Pay
52
61
  attributes[:quantity] = quantity if quantity?
53
62
  PaddlePay::Subscription::User.update(processor_id, attributes)
54
63
  rescue ::PaddlePay::PaddlePayError => e
55
- raise Error, e.message
64
+ raise Pay::Paddle::Error, e
56
65
  end
57
66
  end
58
67
  end