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
@@ -0,0 +1,19 @@
1
+ # A subscription reaches the specified number of billing cycles and expires.
2
+
3
+ module Pay
4
+ module Braintree
5
+ module Webhooks
6
+ class SubscriptionExpired
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,19 @@
1
+ # A subscription's trial period ends.
2
+
3
+ module Pay
4
+ module Braintree
5
+ module Webhooks
6
+ class SubscriptionTrialEnded
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(trial_ends_at: Time.zone.now)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ # A subscription's first authorized transaction is created, or a successful transaction moves a subscription from the Past Due status to the Active status. Subscriptions with trial periods will not trigger this notification when they move from the trial period into the first billing cycle.
2
+
3
+ module Pay
4
+ module Braintree
5
+ module Webhooks
6
+ class SubscriptionWentActive
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!(status: :active)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ # A subscription has moved from the Active status to the Past Due status. This will only be triggered when the initial transaction in a billing cycle is declined. Once the status moves to past due, it will not be triggered again in that billing cycle.
2
+
3
+ module Pay
4
+ module Braintree
5
+ module Webhooks
6
+ class SubscriptionWentPastDue
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!(status: :past_due)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
data/lib/pay/engine.rb CHANGED
@@ -8,7 +8,6 @@ end
8
8
 
9
9
  begin
10
10
  require "stripe"
11
- require "stripe_event"
12
11
  rescue LoadError
13
12
  end
14
13
 
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
@@ -1 +1,9 @@
1
1
  Dir[File.join(__dir__, "webhooks", "**", "*.rb")].sort.each { |file| require file }
2
+
3
+ Pay::Webhooks.configure do |events|
4
+ events.subscribe "paddle.subscription_created", Pay::Paddle::Webhooks::SubscriptionCreated.new
5
+ events.subscribe "paddle.subscription_updated", Pay::Paddle::Webhooks::SubscriptionUpdated.new
6
+ events.subscribe "paddle.subscription_cancelled", Pay::Paddle::Webhooks::SubscriptionCancelled.new
7
+ events.subscribe "paddle.subscription_payment_succeeded", Pay::Paddle::Webhooks::SubscriptionPaymentSucceeded.new
8
+ events.subscribe "paddle.subscription_payment_refunded", Pay::Paddle::Webhooks::SubscriptionPaymentRefunded.new
9
+ end
@@ -2,15 +2,15 @@ module Pay
2
2
  module Paddle
3
3
  module Webhooks
4
4
  class SubscriptionCancelled
5
- def initialize(data)
6
- subscription = Pay.subscription_model.find_by(processor: :paddle, processor_id: data["subscription_id"])
5
+ def call(event)
6
+ subscription = Pay.subscription_model.find_by(processor: :paddle, processor_id: event["subscription_id"])
7
7
 
8
8
  # We couldn't find the subscription for some reason, maybe it's from another service
9
9
  return if subscription.nil?
10
10
 
11
11
  # User canceled subscriptions have an ends_at
12
12
  # Automatically canceled subscriptions need this value set
13
- subscription.update!(ends_at: DateTime.parse(data["cancellation_effective_date"])) if subscription.ends_at.blank? && data["cancellation_effective_date"].present?
13
+ subscription.update!(ends_at: Time.zone.parse(event["cancellation_effective_date"])) if subscription.ends_at.blank? && event["cancellation_effective_date"].present?
14
14
  end
15
15
  end
16
16
  end
@@ -2,42 +2,42 @@ module Pay
2
2
  module Paddle
3
3
  module Webhooks
4
4
  class SubscriptionCreated
5
- def initialize(data)
5
+ def call(event)
6
6
  # We may already have the subscription in the database, so we can update that record
7
- subscription = Pay.subscription_model.find_by(processor: :paddle, processor_id: data["subscription_id"])
7
+ subscription = Pay.subscription_model.find_by(processor: :paddle, processor_id: event["subscription_id"])
8
8
 
9
9
  # Create the subscription in the database if we don't have it already
10
10
  if subscription.nil?
11
11
 
12
12
  # The customer could already be in the database
13
- owner = Pay.find_billable(processor: :paddle, processor_id: data["user_id"])
13
+ owner = Pay.find_billable(processor: :paddle, processor_id: event["user_id"])
14
14
 
15
15
  if owner.nil?
16
- owner = owner_by_passtrough(data["passthrough"], data["subscription_plan_id"])
17
- owner&.update!(processor: "paddle", processor_id: data["user_id"])
16
+ owner = owner_by_passtrough(event["passthrough"], event["subscription_plan_id"])
17
+ owner&.update!(processor: "paddle", processor_id: event["user_id"])
18
18
  end
19
19
 
20
20
  if owner.nil?
21
- Rails.logger.error("[Pay] Unable to find Pay::Billable with owner: '#{data["passthrough"]}'. Searched these models: #{Pay.billable_models.join(", ")}")
21
+ Rails.logger.error("[Pay] Unable to find Pay::Billable with owner: '#{event["passthrough"]}'. Searched these models: #{Pay.billable_models.join(", ")}")
22
22
  return
23
23
  end
24
24
 
25
- subscription = Pay.subscription_model.new(owner: owner, name: "default", processor: "paddle", processor_id: data["subscription_id"], status: :active)
25
+ subscription = Pay.subscription_model.new(owner: owner, name: Pay.default_product_name, processor: "paddle", processor_id: event["subscription_id"], status: :active)
26
26
  end
27
27
 
28
- subscription.quantity = data["quantity"]
29
- subscription.processor_plan = data["subscription_plan_id"]
30
- subscription.paddle_update_url = data["update_url"]
31
- subscription.paddle_cancel_url = data["cancel_url"]
32
- subscription.trial_ends_at = Time.zone.parse(data["next_bill_date"]) if data["status"] == "trialing"
28
+ subscription.quantity = event["quantity"]
29
+ subscription.processor_plan = event["subscription_plan_id"]
30
+ subscription.paddle_update_url = event["update_url"]
31
+ subscription.paddle_cancel_url = event["cancel_url"]
32
+ subscription.trial_ends_at = Time.zone.parse(event["next_bill_date"]) if event["status"] == "trialing"
33
33
 
34
34
  # If user was on trial, their subscription ends at the end of the trial
35
- subscription.ends_at = if ["paused", "deleted"].include?(data["status"]) && subscription.on_trial?
35
+ subscription.ends_at = if ["paused", "deleted"].include?(event["status"]) && subscription.on_trial?
36
36
  subscription.trial_ends_at
37
37
 
38
38
  # User wasn't on trial, so subscription ends at period end
39
- elsif ["paused", "deleted"].include?(data["status"])
40
- Time.zone.parse(data["next_bill_date"])
39
+ elsif ["paused", "deleted"].include?(event["status"])
40
+ Time.zone.parse(event["next_bill_date"])
41
41
 
42
42
  # Subscription isn't marked to cancel at period end
43
43
  end
@@ -2,17 +2,17 @@ module Pay
2
2
  module Paddle
3
3
  module Webhooks
4
4
  class SubscriptionPaymentRefunded
5
- def initialize(data)
6
- charge = Pay.charge_model.find_by(processor: :paddle, processor_id: data["subscription_payment_id"])
5
+ def call(event)
6
+ charge = Pay.charge_model.find_by(processor: :paddle, processor_id: event["subscription_payment_id"])
7
7
  return unless charge.present?
8
8
 
9
- charge.update(amount_refunded: Integer(data["gross_refund"].to_f * 100))
9
+ charge.update(amount_refunded: Integer(event["gross_refund"].to_f * 100))
10
10
  notify_user(charge.owner, charge)
11
11
  end
12
12
 
13
- def notify_user(user, charge)
13
+ def notify_user(billable, charge)
14
14
  if Pay.send_emails
15
- Pay::UserMailer.refund(user, charge).deliver_later
15
+ Pay::UserMailer.with(billable: billable, charge: charge).refund.deliver_later
16
16
  end
17
17
  end
18
18
  end