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
@@ -2,60 +2,39 @@ module Pay
2
2
  module Paddle
3
3
  module Webhooks
4
4
  class SubscriptionPaymentSucceeded
5
- def initialize(data)
6
- billable = Pay.find_billable(processor: :paddle, processor_id: data["user_id"])
5
+ def call(event)
6
+ billable = Pay.find_billable(processor: :paddle, processor_id: event["user_id"])
7
7
  return unless billable.present?
8
- return if billable.charges.where(processor_id: data["subscription_payment_id"]).any?
8
+ return if billable.charges.where(processor_id: event["subscription_payment_id"]).any?
9
9
 
10
- charge = create_charge(billable, data)
10
+ charge = create_charge(billable, event)
11
11
  notify_user(billable, charge)
12
12
  end
13
13
 
14
- def create_charge(user, data)
14
+ def create_charge(user, event)
15
15
  charge = user.charges.find_or_initialize_by(
16
16
  processor: :paddle,
17
- processor_id: data["subscription_payment_id"]
17
+ processor_id: event["subscription_payment_id"]
18
18
  )
19
19
 
20
20
  params = {
21
- amount: Integer(data["sale_gross"].to_f * 100),
22
- card_type: data["payment_method"],
23
- paddle_receipt_url: data["receipt_url"],
24
- created_at: DateTime.parse(data["event_time"])
25
- }.merge(payment_params(data["subscription_id"]))
21
+ amount: Integer(event["sale_gross"].to_f * 100),
22
+ card_type: event["payment_method"],
23
+ paddle_receipt_url: event["receipt_url"],
24
+ created_at: Time.zone.parse(event["event_time"])
25
+ }
26
26
 
27
- charge.update(params)
27
+ payment_information = user.paddle_payment_information(event["subscription_id"])
28
+
29
+ charge.update(params.merge(payment_information))
30
+ user.update(payment_information)
28
31
 
29
32
  charge
30
33
  end
31
34
 
32
- def notify_user(user, charge)
35
+ def notify_user(billable, charge)
33
36
  if Pay.send_emails && charge.respond_to?(:receipt)
34
- Pay::UserMailer.receipt(user, charge).deliver_later
35
- end
36
- end
37
-
38
- private
39
-
40
- def payment_params(subscription_id)
41
- subscription_user = PaddlePay::Subscription::User.list({subscription_id: subscription_id}).try(:first)
42
- payment_information = subscription_user ? subscription_user[:payment_information] : nil
43
- return {} if payment_information.nil?
44
-
45
- case payment_information[:payment_method]
46
- when "card"
47
- {
48
- card_type: payment_information[:card_type],
49
- card_last4: payment_information[:last_four_digits],
50
- card_exp_month: payment_information[:expiry_date].split("/").first,
51
- card_exp_year: payment_information[:expiry_date].split("/").last
52
- }
53
- when "paypal"
54
- {
55
- card_type: "PayPal"
56
- }
57
- else
58
- {}
37
+ Pay::UserMailer.with(billable: billable, charge: charge).receipt.deliver_later
59
38
  end
60
39
  end
61
40
  end
@@ -2,29 +2,32 @@ module Pay
2
2
  module Paddle
3
3
  module Webhooks
4
4
  class SubscriptionUpdated
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
  return if subscription.nil?
9
9
 
10
- subscription.status = data["status"] == "deleted" ? "canceled" : data["status"]
11
- subscription.quantity = data["new_quantity"]
12
- subscription.processor_plan = data["subscription_plan_id"]
13
- subscription.paddle_update_url = data["update_url"]
14
- subscription.paddle_cancel_url = data["cancel_url"]
10
+ case event["status"]
11
+ when "deleted"
12
+ subscription.status = "canceled"
13
+ subscription.ends_at = Time.zone.parse(event["next_bill_date"]) || Time.zone.now if subscription.ends_at.blank?
14
+ when "trialing"
15
+ subscription.status = "trialing"
16
+ subscription.trial_ends_at = Time.zone.parse(event["next_bill_date"])
17
+ when "active"
18
+ subscription.status = "active"
19
+ subscription.paddle_paused_from = Time.zone.parse(event["paused_from"]) if event["paused_from"].present?
20
+ else
21
+ subscription.status = event["status"]
22
+ end
15
23
 
16
- subscription.trial_ends_at = DateTime.parse(data["next_bill_date"]) if data["status"] == "trialing"
24
+ subscription.quantity = event["new_quantity"]
25
+ subscription.processor_plan = event["subscription_plan_id"]
26
+ subscription.paddle_update_url = event["update_url"]
27
+ subscription.paddle_cancel_url = event["cancel_url"]
17
28
 
18
29
  # If user was on trial, their subscription ends at the end of the trial
19
- subscription.ends_at = if ["paused", "deleted"].include?(data["status"]) && subscription.on_trial?
20
- subscription.trial_ends_at
21
-
22
- # User wasn't on trial, so subscription ends at period end
23
- elsif ["paused", "deleted"].include?(data["status"])
24
- DateTime.parse(data["next_bill_date"])
25
-
26
- # Subscription isn't marked to cancel at period end
27
- end
30
+ subscription.ends_at = subscription.trial_ends_at if subscription.on_trial?
28
31
 
29
32
  subscription.save!
30
33
  end
data/lib/pay/receipts.rb CHANGED
@@ -28,13 +28,13 @@ module Pay
28
28
 
29
29
  def line_items
30
30
  line_items = [
31
- ["Date", created_at.to_s],
32
- ["Account Billed", "#{owner.name} (#{owner.email})"],
33
- ["Product", product],
34
- ["Amount", ActionController::Base.helpers.number_to_currency(amount / 100.0)],
35
- ["Charged to", charged_to]
31
+ [I18n.t("receipt.date"), created_at.to_s],
32
+ [I18n.t("receipt.account_billed"), "#{owner.name} (#{owner.email})"],
33
+ [I18n.t("receipt.product"), product],
34
+ [I18n.t("receipt.amount"), ActionController::Base.helpers.number_to_currency(amount / 100.0)],
35
+ [I18n.t("receipt.charged_to"), charged_to]
36
36
  ]
37
- line_items << ["Additional Info", owner.extra_billing_info] if owner.extra_billing_info?
37
+ line_items << [I18n.t("receipt.additional_info"), owner.extra_billing_info] if owner.extra_billing_info?
38
38
  line_items
39
39
  end
40
40
  end
data/lib/pay/stripe.rb CHANGED
@@ -13,7 +13,9 @@ module Pay
13
13
  def setup
14
14
  ::Stripe.api_key = private_key
15
15
  ::Stripe.api_version = "2020-08-27"
16
- ::StripeEvent.signing_secret = signing_secret
16
+
17
+ # Used by Stripe to identify Pay for support
18
+ ::Stripe.set_app_info("PayRails", partner_id: "pp_partner_IqhY0UExnJYLxg", version: Pay::VERSION, url: "https://github.com/pay-rails/pay")
17
19
 
18
20
  Pay.charge_model.include Pay::Stripe::Charge
19
21
  Pay.subscription_model.include Pay::Stripe::Subscription
@@ -17,7 +17,7 @@ module Pay
17
17
  create_stripe_customer
18
18
  end
19
19
  rescue ::Stripe::StripeError => e
20
- raise Error, e.message
20
+ raise Pay::Stripe::Error, e
21
21
  end
22
22
 
23
23
  def create_setup_intent
@@ -47,7 +47,7 @@ module Pay
47
47
  # Create a new charge object
48
48
  Stripe::Webhooks::ChargeSucceeded.new.create_charge(self, payment_intent.charges.first)
49
49
  rescue ::Stripe::StripeError => e
50
- raise Error, e.message
50
+ raise Pay::Stripe::Error, e
51
51
  end
52
52
 
53
53
  # Handles Billable#subscribe
@@ -80,7 +80,7 @@ module Pay
80
80
 
81
81
  subscription
82
82
  rescue ::Stripe::StripeError => e
83
- raise Error, e.message
83
+ raise Pay::Stripe::Error, e
84
84
  end
85
85
 
86
86
  # Handles Billable#update_card
@@ -97,7 +97,7 @@ module Pay
97
97
  update_stripe_card_on_file(payment_method.card)
98
98
  true
99
99
  rescue ::Stripe::StripeError => e
100
- raise Error, e.message
100
+ raise Pay::Stripe::Error, e
101
101
  end
102
102
 
103
103
  def update_stripe_email!
@@ -14,7 +14,7 @@ module Pay
14
14
  def stripe_charge
15
15
  ::Stripe::Charge.retrieve(processor_id)
16
16
  rescue ::Stripe::StripeError => e
17
- raise Error, e.message
17
+ raise Pay::Stripe::Error, e
18
18
  end
19
19
 
20
20
  def stripe_refund!(amount_to_refund)
@@ -25,7 +25,7 @@ module Pay
25
25
 
26
26
  update(amount_refunded: amount_to_refund)
27
27
  rescue ::Stripe::StripeError => e
28
- raise Error, e.message
28
+ raise Pay::Stripe::Error, e
29
29
  end
30
30
  end
31
31
  end
@@ -3,14 +3,6 @@ module Pay
3
3
  module Subscription
4
4
  extend ActiveSupport::Concern
5
5
 
6
- included do
7
- scope :stripe, -> { where(processor: :stripe) }
8
- end
9
-
10
- def stripe?
11
- processor == "stripe"
12
- end
13
-
14
6
  def stripe_cancel
15
7
  subscription = processor_subscription
16
8
  subscription.cancel_at_period_end = true
@@ -19,24 +11,40 @@ module Pay
19
11
  new_ends_at = on_trial? ? trial_ends_at : Time.at(subscription.current_period_end)
20
12
  update(ends_at: new_ends_at)
21
13
  rescue ::Stripe::StripeError => e
22
- raise Error, e.message
14
+ raise Pay::Stripe::Error, e
23
15
  end
24
16
 
25
17
  def stripe_cancel_now!
26
18
  processor_subscription.delete
27
19
  update(ends_at: Time.zone.now, status: :canceled)
28
20
  rescue ::Stripe::StripeError => e
29
- raise Error, e.message
21
+ raise Pay::Stripe::Error, e
22
+ end
23
+
24
+ def stripe_on_grace_period?
25
+ canceled? && Time.zone.now < ends_at
26
+ end
27
+
28
+ def stripe_paused?
29
+ false
30
+ end
31
+
32
+ def stripe_pause
33
+ raise NotImplementedError, "Stripe does not support pausing subscriptions"
30
34
  end
31
35
 
32
36
  def stripe_resume
37
+ unless on_grace_period?
38
+ raise StandardError, "You can only resume subscriptions within their grace period."
39
+ end
40
+
33
41
  subscription = processor_subscription
34
42
  subscription.plan = processor_plan
35
43
  subscription.trial_end = on_trial? ? trial_ends_at.to_i : "now"
36
44
  subscription.cancel_at_period_end = false
37
45
  subscription.save
38
46
  rescue ::Stripe::StripeError => e
39
- raise Error, e.message
47
+ raise Pay::Stripe::Error, e
40
48
  end
41
49
 
42
50
  def stripe_swap(plan)
@@ -48,7 +56,7 @@ module Pay
48
56
  subscription.quantity = quantity if quantity?
49
57
  subscription.save
50
58
  rescue ::Stripe::StripeError => e
51
- raise Error, e.message
59
+ raise Pay::Stripe::Error, e
52
60
  end
53
61
  end
54
62
  end
@@ -1,39 +1,38 @@
1
- require "stripe_event"
2
1
  Dir[File.join(__dir__, "webhooks", "**", "*.rb")].sort.each { |file| require file }
3
2
 
4
- StripeEvent.configure do |events|
3
+ Pay::Webhooks.configure do |events|
5
4
  # Listen to the charge event to make sure we get non-subscription
6
5
  # purchases as well. Invoice is only for subscriptions and manual creation
7
6
  # so it does not include individual charges.
8
- events.subscribe "charge.succeeded", Pay::Stripe::Webhooks::ChargeSucceeded.new
9
- events.subscribe "charge.refunded", Pay::Stripe::Webhooks::ChargeRefunded.new
7
+ events.subscribe "stripe.charge.succeeded", Pay::Stripe::Webhooks::ChargeSucceeded.new
8
+ events.subscribe "stripe.charge.refunded", Pay::Stripe::Webhooks::ChargeRefunded.new
10
9
 
11
10
  # Warn user of upcoming charges for their subscription. This is handy for
12
11
  # notifying annual users their subscription will renew shortly.
13
12
  # This probably should be ignored for monthly subscriptions.
14
- events.subscribe "invoice.upcoming", Pay::Stripe::Webhooks::SubscriptionRenewing.new
13
+ events.subscribe "stripe.invoice.upcoming", Pay::Stripe::Webhooks::SubscriptionRenewing.new
15
14
 
16
15
  # Payment action is required to process an invoice
17
- events.subscribe "invoice.payment_action_required", Pay::Stripe::Webhooks::PaymentActionRequired.new
16
+ events.subscribe "stripe.invoice.payment_action_required", Pay::Stripe::Webhooks::PaymentActionRequired.new
18
17
 
19
18
  # If a subscription is manually created on Stripe, we want to sync
20
- events.subscribe "customer.subscription.created", Pay::Stripe::Webhooks::SubscriptionCreated.new
19
+ events.subscribe "stripe.customer.subscription.created", Pay::Stripe::Webhooks::SubscriptionCreated.new
21
20
 
22
21
  # If the plan, quantity, or trial ending date is updated on Stripe, we want to sync
23
- events.subscribe "customer.subscription.updated", Pay::Stripe::Webhooks::SubscriptionUpdated.new
22
+ events.subscribe "stripe.customer.subscription.updated", Pay::Stripe::Webhooks::SubscriptionUpdated.new
24
23
 
25
24
  # When a customers subscription is canceled, we want to update our records
26
- events.subscribe "customer.subscription.deleted", Pay::Stripe::Webhooks::SubscriptionDeleted.new
25
+ events.subscribe "stripe.customer.subscription.deleted", Pay::Stripe::Webhooks::SubscriptionDeleted.new
27
26
 
28
27
  # Monitor changes for customer's default card changing
29
- events.subscribe "customer.updated", Pay::Stripe::Webhooks::CustomerUpdated.new
28
+ events.subscribe "stripe.customer.updated", Pay::Stripe::Webhooks::CustomerUpdated.new
30
29
 
31
30
  # If a customer was deleted in Stripe, their subscriptions should be cancelled
32
- events.subscribe "customer.deleted", Pay::Stripe::Webhooks::CustomerDeleted.new
31
+ events.subscribe "stripe.customer.deleted", Pay::Stripe::Webhooks::CustomerDeleted.new
33
32
 
34
33
  # If a customer's payment source was deleted in Stripe, we should update as well
35
- events.subscribe "payment_method.attached", Pay::Stripe::Webhooks::PaymentMethodUpdated.new
36
- events.subscribe "payment_method.updated", Pay::Stripe::Webhooks::PaymentMethodUpdated.new
37
- events.subscribe "payment_method.card_automatically_updated", Pay::Stripe::Webhooks::PaymentMethodUpdated.new
38
- events.subscribe "payment_method.detached", Pay::Stripe::Webhooks::PaymentMethodUpdated.new
34
+ events.subscribe "stripe.payment_method.attached", Pay::Stripe::Webhooks::PaymentMethodUpdated.new
35
+ events.subscribe "stripe.payment_method.updated", Pay::Stripe::Webhooks::PaymentMethodUpdated.new
36
+ events.subscribe "stripe.payment_method.card_automatically_updated", Pay::Stripe::Webhooks::PaymentMethodUpdated.new
37
+ events.subscribe "stripe.payment_method.detached", Pay::Stripe::Webhooks::PaymentMethodUpdated.new
39
38
  end
@@ -12,9 +12,9 @@ module Pay
12
12
  notify_user(charge.owner, charge)
13
13
  end
14
14
 
15
- def notify_user(user, charge)
15
+ def notify_user(billable, charge)
16
16
  if Pay.send_emails
17
- Pay::UserMailer.refund(user, charge).deliver_later
17
+ Pay::UserMailer.with(billable: billable, charge: charge).refund.deliver_later
18
18
  end
19
19
  end
20
20
  end
@@ -34,7 +34,7 @@ module Pay
34
34
 
35
35
  def notify_user(billable, charge)
36
36
  if Pay.send_emails && charge.respond_to?(:receipt)
37
- Pay::UserMailer.receipt(billable, charge).deliver_later
37
+ Pay::UserMailer.with(billable: billable, charge: charge).receipt.deliver_later
38
38
  end
39
39
  end
40
40
  end
@@ -17,7 +17,7 @@ module Pay
17
17
 
18
18
  def notify_user(billable, payment_intent_id, subscription)
19
19
  if Pay.send_emails
20
- Pay::UserMailer.payment_action_required(billable, payment_intent_id, subscription).deliver_later
20
+ Pay::UserMailer.with(billable: billable, payment_intent_id: payment_intent_id, subscription: subscription).payment_action_required.deliver_later
21
21
  end
22
22
  end
23
23
  end
@@ -18,10 +18,11 @@ module Pay
18
18
  return
19
19
  end
20
20
 
21
- subscription = Pay.subscription_model.new(name: "default", owner: owner, processor: :stripe, processor_id: object.id)
21
+ subscription = Pay.subscription_model.new(name: Pay.default_product_name, owner: owner, processor: :stripe, processor_id: object.id)
22
22
  end
23
23
 
24
24
  subscription.quantity = object.quantity
25
+ subscription.status = object.status
25
26
  subscription.processor_plan = object.plan.id
26
27
  subscription.trial_ends_at = Time.at(object.trial_end) if object.trial_end.present?
27
28
 
@@ -9,12 +9,13 @@ module Pay
9
9
  processor: :stripe,
10
10
  processor_id: event.data.object.subscription
11
11
  )
12
- notify_user(subscription.owner, subscription) if subscription.present?
12
+ date = Time.zone.at(event.data.object.next_payment_attempt)
13
+ notify_user(subscription.owner, subscription, date) if subscription.present?
13
14
  end
14
15
 
15
- def notify_user(user, subscription)
16
+ def notify_user(billable, subscription, date)
16
17
  if Pay.send_emails
17
- Pay::UserMailer.subscription_renewing(user, subscription).deliver_later
18
+ Pay::UserMailer.with(billable: billable, subscription: subscription, date: date).subscription_renewing.deliver_later
18
19
  end
19
20
  end
20
21
  end
data/lib/pay/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Pay
2
- VERSION = "2.3.1"
2
+ VERSION = "2.5.0"
3
3
  end
@@ -0,0 +1,61 @@
1
+ module Pay
2
+ module Webhooks
3
+ class Delegator
4
+ attr_reader :backend
5
+
6
+ def initialize
7
+ @backend = ActiveSupport::Notifications
8
+ end
9
+
10
+ # Configure DSL
11
+ def configure(&block)
12
+ raise ArgumentError, "must provide a block" unless block
13
+ block.arity.zero? ? instance_eval(&block) : yield(self)
14
+ end
15
+
16
+ # Subscribe to specific events
17
+ def subscribe(name, callable = nil, &block)
18
+ callable ||= block
19
+ backend.subscribe to_regexp(name), NotificationAdapter.new(callable)
20
+ end
21
+
22
+ # Listen to all events
23
+ def all(callable = nil, &block)
24
+ callable ||= block
25
+ subscribe nil, callable
26
+ end
27
+
28
+ # Unsubscribe
29
+ def unsubscribe(name)
30
+ backend.unsubscribe name
31
+ end
32
+
33
+ # Called to process an event
34
+ def instrument(event:, type:)
35
+ backend.instrument name_with_namespace(type), event
36
+ end
37
+
38
+ # Strips down to event data only
39
+ class NotificationAdapter
40
+ def initialize(subscriber)
41
+ @subscriber = subscriber
42
+ end
43
+
44
+ def call(*args)
45
+ payload = args.last
46
+ @subscriber.call(payload)
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def to_regexp(name)
53
+ %r{^#{Regexp.escape name_with_namespace(name)}}
54
+ end
55
+
56
+ def name_with_namespace(name, delimiter: ".")
57
+ [:pay, name].join(delimiter)
58
+ end
59
+ end
60
+ end
61
+ end