pay 2.2.0 → 2.4.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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +161 -11
  3. data/Rakefile +2 -4
  4. data/app/controllers/pay/payments_controller.rb +3 -0
  5. data/app/controllers/pay/webhooks/braintree_controller.rb +1 -1
  6. data/app/controllers/pay/webhooks/paddle_controller.rb +36 -0
  7. data/app/mailers/pay/user_mailer.rb +14 -35
  8. data/app/models/pay/application_record.rb +6 -1
  9. data/app/models/pay/charge.rb +7 -0
  10. data/app/models/pay/subscription.rb +27 -4
  11. data/app/views/pay/payments/show.html.erb +1 -1
  12. data/app/views/pay/user_mailer/payment_action_required.html.erb +1 -1
  13. data/app/views/pay/user_mailer/receipt.html.erb +6 -6
  14. data/app/views/pay/user_mailer/refund.html.erb +6 -6
  15. data/app/views/pay/user_mailer/subscription_renewing.html.erb +1 -1
  16. data/config/locales/en.yml +137 -0
  17. data/config/routes.rb +1 -0
  18. data/db/migrate/20200603134434_add_data_to_pay_models.rb +17 -0
  19. data/lib/generators/active_record/pay_generator.rb +1 -1
  20. data/lib/generators/pay/orm_helpers.rb +1 -2
  21. data/lib/pay.rb +9 -41
  22. data/lib/pay/billable.rb +16 -11
  23. data/lib/pay/braintree/billable.rb +25 -19
  24. data/lib/pay/braintree/charge.rb +7 -3
  25. data/lib/pay/braintree/subscription.rb +15 -5
  26. data/lib/pay/engine.rb +7 -0
  27. data/lib/pay/errors.rb +73 -0
  28. data/lib/pay/paddle.rb +38 -0
  29. data/lib/pay/paddle/billable.rb +95 -0
  30. data/lib/pay/paddle/charge.rb +39 -0
  31. data/lib/pay/paddle/subscription.rb +70 -0
  32. data/lib/pay/paddle/webhooks.rb +1 -0
  33. data/lib/pay/paddle/webhooks/signature_verifier.rb +115 -0
  34. data/lib/pay/paddle/webhooks/subscription_cancelled.rb +18 -0
  35. data/lib/pay/paddle/webhooks/subscription_created.rb +59 -0
  36. data/lib/pay/paddle/webhooks/subscription_payment_refunded.rb +21 -0
  37. data/lib/pay/paddle/webhooks/subscription_payment_succeeded.rb +43 -0
  38. data/lib/pay/paddle/webhooks/subscription_updated.rb +37 -0
  39. data/lib/pay/receipts.rb +6 -6
  40. data/lib/pay/stripe.rb +1 -1
  41. data/lib/pay/stripe/billable.rb +12 -6
  42. data/lib/pay/stripe/charge.rb +6 -2
  43. data/lib/pay/stripe/subscription.rb +15 -5
  44. data/lib/pay/stripe/webhooks/charge_refunded.rb +2 -2
  45. data/lib/pay/stripe/webhooks/charge_succeeded.rb +7 -7
  46. data/lib/pay/stripe/webhooks/payment_action_required.rb +7 -8
  47. data/lib/pay/stripe/webhooks/subscription_created.rb +1 -1
  48. data/lib/pay/stripe/webhooks/subscription_renewing.rb +4 -3
  49. data/lib/pay/version.rb +1 -1
  50. metadata +35 -43
@@ -0,0 +1,70 @@
1
+ module Pay
2
+ module Paddle
3
+ module Subscription
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ scope :paddle, -> { where(processor: :paddle) }
8
+
9
+ store_accessor :data, :paddle_update_url
10
+ store_accessor :data, :paddle_cancel_url
11
+ store_accessor :data, :paddle_paused_from
12
+ end
13
+
14
+ def paddle?
15
+ processor == "paddle"
16
+ end
17
+
18
+ def paddle_cancel
19
+ subscription = processor_subscription
20
+ PaddlePay::Subscription::User.cancel(processor_id)
21
+ if on_trial?
22
+ update(status: :canceled, ends_at: trial_ends_at)
23
+ else
24
+ update(status: :canceled, ends_at: Time.zone.parse(subscription.next_payment[:date]))
25
+ end
26
+ rescue ::PaddlePay::PaddlePayError => e
27
+ raise Pay::Paddle::Error, e
28
+ end
29
+
30
+ def paddle_cancel_now!
31
+ PaddlePay::Subscription::User.cancel(processor_id)
32
+ update(status: :canceled, ends_at: Time.zone.now)
33
+ rescue ::PaddlePay::PaddlePayError => e
34
+ raise Pay::Paddle::Error, e
35
+ end
36
+
37
+ def paddle_on_grace_period?
38
+ canceled? && Time.zone.now < ends_at || paused? && Time.zone.now < paddle_paused_from
39
+ end
40
+
41
+ def paddle_paused?
42
+ paddle_paused_from.present?
43
+ end
44
+
45
+ def paddle_pause
46
+ attributes = {pause: true}
47
+ response = PaddlePay::Subscription::User.update(processor_id, attributes)
48
+ update(paddle_paused_from: Time.zone.parse(response[:next_payment][:date]))
49
+ rescue ::PaddlePay::PaddlePayError => e
50
+ raise Pay::Paddle::Error, e
51
+ end
52
+
53
+ def paddle_resume
54
+ attributes = {pause: false}
55
+ PaddlePay::Subscription::User.update(processor_id, attributes)
56
+ update(status: :active, paddle_paused_from: nil)
57
+ rescue ::PaddlePay::PaddlePayError => e
58
+ raise Pay::Paddle::Error, e
59
+ end
60
+
61
+ def paddle_swap(plan)
62
+ attributes = {plan_id: plan, prorate: prorate}
63
+ attributes[:quantity] = quantity if quantity?
64
+ PaddlePay::Subscription::User.update(processor_id, attributes)
65
+ rescue ::PaddlePay::PaddlePayError => e
66
+ raise Pay::Paddle::Error, e
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1 @@
1
+ Dir[File.join(__dir__, "webhooks", "**", "*.rb")].sort.each { |file| require file }
@@ -0,0 +1,115 @@
1
+ require "base64"
2
+ require "json"
3
+ require "openssl"
4
+
5
+ module Pay
6
+ module Paddle
7
+ module Webhooks
8
+ class SignatureVerifier
9
+ def initialize(data)
10
+ @data = data
11
+ @public_key_base64 = Pay::Paddle.public_key_base64
12
+ end
13
+
14
+ def verify
15
+ data = @data
16
+ public_key = Base64.decode64(@public_key_base64) if @public_key_base64
17
+ return false unless data && data["p_signature"] && public_key
18
+
19
+ # 'data' represents all of the POST fields sent with the request.
20
+ # Get the p_signature parameter & base64 decode it.
21
+ signature = Base64.decode64(data["p_signature"])
22
+
23
+ # Remove the p_signature parameter
24
+ data.delete("p_signature")
25
+
26
+ # Ensure all the data fields are strings
27
+ data.each { |key, value| data[key] = String(value) }
28
+
29
+ # Sort the data
30
+ data_sorted = data.sort_by { |key, value| key }
31
+
32
+ # and serialize the fields
33
+ # serialization library is available here: https://github.com/jqr/php-serialize
34
+ data_serialized = serialize(data_sorted, true)
35
+
36
+ # verify the data
37
+ digest = OpenSSL::Digest.new("SHA1")
38
+ pub_key = OpenSSL::PKey::RSA.new(public_key)
39
+ pub_key.verify(digest, signature, data_serialized)
40
+ end
41
+
42
+ private
43
+
44
+ # https://github.com/jqr/php-serialize/blob/master/lib/php_serialize.rb
45
+ #
46
+ # Returns a string representing the argument in a form PHP.unserialize
47
+ # and PHP's unserialize() should both be able to load.
48
+ #
49
+ # string = PHP.serialize(mixed var[, bool assoc])
50
+ #
51
+ # Array, Hash, Fixnum, Float, True/FalseClass, NilClass, String and Struct
52
+ # are supported; as are objects which support the to_assoc method, which
53
+ # returns an array of the form [['attr_name', 'value']..]. Anything else
54
+ # will raise a TypeError.
55
+ #
56
+ # If 'assoc' is specified, Array's who's first element is a two value
57
+ # array will be assumed to be an associative array, and will be serialized
58
+ # as a PHP associative array rather than a multidimensional array.
59
+ def serialize(var, assoc = false)
60
+ s = ""
61
+ case var
62
+ when Array
63
+ s << "a:#{var.size}:{"
64
+ if assoc && var.first.is_a?(Array) && (var.first.size == 2)
65
+ var.each do |k, v|
66
+ s << serialize(k, assoc) << serialize(v, assoc)
67
+ end
68
+ else
69
+ var.each_with_index do |v, i|
70
+ s << "i:#{i};#{serialize(v, assoc)}"
71
+ end
72
+ end
73
+ s << "}"
74
+ when Hash
75
+ s << "a:#{var.size}:{"
76
+ var.each do |k, v|
77
+ s << "#{serialize(k, assoc)}#{serialize(v, assoc)}"
78
+ end
79
+ s << "}"
80
+ when Struct
81
+ # encode as Object with same name
82
+ s << "O:#{var.class.to_s.bytesize}:\"#{var.class.to_s.downcase}\":#{var.members.length}:{"
83
+ var.members.each do |member|
84
+ s << "#{serialize(member, assoc)}#{serialize(var[member], assoc)}"
85
+ end
86
+ s << "}"
87
+ when String, Symbol
88
+ s << "s:#{var.to_s.bytesize}:\"#{var}\";"
89
+ when Integer
90
+ s << "i:#{var};"
91
+ when Float
92
+ s << "d:#{var};"
93
+ when NilClass
94
+ s << "N;"
95
+ when FalseClass, TrueClass
96
+ s << "b:#{var ? 1 : 0};"
97
+ else
98
+ if var.respond_to?(:to_assoc)
99
+ v = var.to_assoc
100
+ # encode as Object with same name
101
+ s << "O:#{var.class.to_s.bytesize}:\"#{var.class.to_s.downcase}\":#{v.length}:{"
102
+ v.each do |k, v|
103
+ s << "#{serialize(k.to_s, assoc)}#{serialize(v, assoc)}"
104
+ end
105
+ s << "}"
106
+ else
107
+ raise TypeError, "Unable to serialize type #{var.class}"
108
+ end
109
+ end
110
+ s
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,18 @@
1
+ module Pay
2
+ module Paddle
3
+ module Webhooks
4
+ class SubscriptionCancelled
5
+ def initialize(data)
6
+ subscription = Pay.subscription_model.find_by(processor: :paddle, processor_id: data["subscription_id"])
7
+
8
+ # We couldn't find the subscription for some reason, maybe it's from another service
9
+ return if subscription.nil?
10
+
11
+ # User canceled subscriptions have an ends_at
12
+ # Automatically canceled subscriptions need this value set
13
+ subscription.update!(ends_at: Time.zone.parse(data["cancellation_effective_date"])) if subscription.ends_at.blank? && data["cancellation_effective_date"].present?
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,59 @@
1
+ module Pay
2
+ module Paddle
3
+ module Webhooks
4
+ class SubscriptionCreated
5
+ def initialize(data)
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"])
8
+
9
+ # Create the subscription in the database if we don't have it already
10
+ if subscription.nil?
11
+
12
+ # The customer could already be in the database
13
+ owner = Pay.find_billable(processor: :paddle, processor_id: data["user_id"])
14
+
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"])
18
+ end
19
+
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(", ")}")
22
+ return
23
+ end
24
+
25
+ subscription = Pay.subscription_model.new(owner: owner, name: Pay.default_product_name, processor: "paddle", processor_id: data["subscription_id"], status: :active)
26
+ end
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"
33
+
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?
36
+ subscription.trial_ends_at
37
+
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"])
41
+
42
+ # Subscription isn't marked to cancel at period end
43
+ end
44
+
45
+ subscription.save!
46
+ end
47
+
48
+ private
49
+
50
+ def owner_by_passtrough(passthrough, product_id)
51
+ passthrough_json = JSON.parse(passthrough)
52
+ GlobalID::Locator.locate_signed(passthrough_json["owner_sgid"])
53
+ rescue JSON::ParserError
54
+ nil
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,21 @@
1
+ module Pay
2
+ module Paddle
3
+ module Webhooks
4
+ class SubscriptionPaymentRefunded
5
+ def initialize(data)
6
+ charge = Pay.charge_model.find_by(processor: :paddle, processor_id: data["subscription_payment_id"])
7
+ return unless charge.present?
8
+
9
+ charge.update(amount_refunded: Integer(data["gross_refund"].to_f * 100))
10
+ notify_user(charge.owner, charge)
11
+ end
12
+
13
+ def notify_user(billable, charge)
14
+ if Pay.send_emails
15
+ Pay::UserMailer.with(billable: billable, charge: charge).refund.deliver_later
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,43 @@
1
+ module Pay
2
+ module Paddle
3
+ module Webhooks
4
+ class SubscriptionPaymentSucceeded
5
+ def initialize(data)
6
+ billable = Pay.find_billable(processor: :paddle, processor_id: data["user_id"])
7
+ return unless billable.present?
8
+ return if billable.charges.where(processor_id: data["subscription_payment_id"]).any?
9
+
10
+ charge = create_charge(billable, data)
11
+ notify_user(billable, charge)
12
+ end
13
+
14
+ def create_charge(user, data)
15
+ charge = user.charges.find_or_initialize_by(
16
+ processor: :paddle,
17
+ processor_id: data["subscription_payment_id"]
18
+ )
19
+
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: Time.zone.parse(data["event_time"])
25
+ }
26
+
27
+ payment_information = user.paddle_payment_information(data["subscription_id"])
28
+
29
+ charge.update(params.merge(payment_information))
30
+ user.update(payment_information)
31
+
32
+ charge
33
+ end
34
+
35
+ def notify_user(billable, charge)
36
+ if Pay.send_emails && charge.respond_to?(:receipt)
37
+ Pay::UserMailer.with(billable: billable, charge: charge).receipt.deliver_later
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,37 @@
1
+ module Pay
2
+ module Paddle
3
+ module Webhooks
4
+ class SubscriptionUpdated
5
+ def initialize(data)
6
+ subscription = Pay.subscription_model.find_by(processor: :paddle, processor_id: data["subscription_id"])
7
+
8
+ return if subscription.nil?
9
+
10
+ case data["status"]
11
+ when "deleted"
12
+ subscription.status = "canceled"
13
+ subscription.ends_at = Time.zone.parse(data["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(data["next_bill_date"])
17
+ when "active"
18
+ subscription.status = "active"
19
+ subscription.paddle_paused_from = Time.zone.parse(data["paused_from"]) if data["paused_from"].present?
20
+ else
21
+ subscription.status = data["status"]
22
+ end
23
+
24
+ subscription.quantity = data["new_quantity"]
25
+ subscription.processor_plan = data["subscription_plan_id"]
26
+ subscription.paddle_update_url = data["update_url"]
27
+ subscription.paddle_cancel_url = data["cancel_url"]
28
+
29
+ # If user was on trial, their subscription ends at the end of the trial
30
+ subscription.ends_at = subscription.trial_ends_at if subscription.on_trial?
31
+
32
+ subscription.save!
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -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
@@ -12,7 +12,7 @@ module Pay
12
12
 
13
13
  def setup
14
14
  ::Stripe.api_key = private_key
15
- ::Stripe.api_version = '2020-08-27'
15
+ ::Stripe.api_version = "2020-08-27"
16
16
  ::StripeEvent.signing_secret = signing_secret
17
17
 
18
18
  Pay.charge_model.include Pay::Stripe::Charge
@@ -1,6 +1,12 @@
1
1
  module Pay
2
2
  module Stripe
3
3
  module Billable
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ scope :stripe, -> { where(processor: :stripe) }
8
+ end
9
+
4
10
  # Handles Billable#customer
5
11
  #
6
12
  # Returns Stripe::Customer
@@ -11,7 +17,7 @@ module Pay
11
17
  create_stripe_customer
12
18
  end
13
19
  rescue ::Stripe::StripeError => e
14
- raise Error, e.message
20
+ raise Pay::Stripe::Error, e
15
21
  end
16
22
 
17
23
  def create_setup_intent
@@ -41,7 +47,7 @@ module Pay
41
47
  # Create a new charge object
42
48
  Stripe::Webhooks::ChargeSucceeded.new.create_charge(self, payment_intent.charges.first)
43
49
  rescue ::Stripe::StripeError => e
44
- raise Error, e.message
50
+ raise Pay::Stripe::Error, e
45
51
  end
46
52
 
47
53
  # Handles Billable#subscribe
@@ -74,7 +80,7 @@ module Pay
74
80
 
75
81
  subscription
76
82
  rescue ::Stripe::StripeError => e
77
- raise Error, e.message
83
+ raise Pay::Stripe::Error, e
78
84
  end
79
85
 
80
86
  # Handles Billable#update_card
@@ -91,13 +97,13 @@ module Pay
91
97
  update_stripe_card_on_file(payment_method.card)
92
98
  true
93
99
  rescue ::Stripe::StripeError => e
94
- raise Error, e.message
100
+ raise Pay::Stripe::Error, e
95
101
  end
96
102
 
97
103
  def update_stripe_email!
98
104
  customer = stripe_customer
99
105
  customer.email = email
100
- customer.description = customer_name
106
+ customer.name = customer_name
101
107
  customer.save
102
108
  end
103
109
 
@@ -137,7 +143,7 @@ module Pay
137
143
  private
138
144
 
139
145
  def create_stripe_customer
140
- customer = ::Stripe::Customer.create(email: email, description: customer_name)
146
+ customer = ::Stripe::Customer.create(email: email, name: customer_name)
141
147
  update(processor: "stripe", processor_id: customer.id)
142
148
 
143
149
  # Update the user's card on file if a token was passed in