pay 2.1.2 → 2.3.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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +168 -14
  3. data/Rakefile +15 -17
  4. data/app/controllers/pay/payments_controller.rb +3 -0
  5. data/app/controllers/pay/webhooks/paddle_controller.rb +36 -0
  6. data/app/mailers/pay/user_mailer.rb +2 -2
  7. data/app/models/pay/application_record.rb +6 -1
  8. data/app/models/pay/charge.rb +7 -0
  9. data/app/models/pay/subscription.rb +24 -3
  10. data/app/views/pay/payments/show.html.erb +1 -1
  11. data/config/routes.rb +1 -0
  12. data/db/migrate/20170205020145_create_pay_subscriptions.rb +2 -1
  13. data/db/migrate/20170727235816_create_pay_charges.rb +1 -0
  14. data/db/migrate/20200603134434_add_data_to_pay_models.rb +17 -0
  15. data/lib/generators/active_record/pay_generator.rb +1 -1
  16. data/lib/generators/pay/orm_helpers.rb +1 -2
  17. data/lib/pay.rb +11 -2
  18. data/lib/pay/billable.rb +7 -2
  19. data/lib/pay/braintree.rb +1 -1
  20. data/lib/pay/braintree/billable.rb +18 -4
  21. data/lib/pay/braintree/charge.rb +4 -0
  22. data/lib/pay/braintree/subscription.rb +6 -0
  23. data/lib/pay/engine.rb +7 -0
  24. data/lib/pay/paddle.rb +38 -0
  25. data/lib/pay/paddle/billable.rb +66 -0
  26. data/lib/pay/paddle/charge.rb +39 -0
  27. data/lib/pay/paddle/subscription.rb +59 -0
  28. data/lib/pay/paddle/webhooks.rb +1 -0
  29. data/lib/pay/paddle/webhooks/signature_verifier.rb +115 -0
  30. data/lib/pay/paddle/webhooks/subscription_cancelled.rb +18 -0
  31. data/lib/pay/paddle/webhooks/subscription_created.rb +59 -0
  32. data/lib/pay/paddle/webhooks/subscription_payment_refunded.rb +21 -0
  33. data/lib/pay/paddle/webhooks/subscription_payment_succeeded.rb +64 -0
  34. data/lib/pay/paddle/webhooks/subscription_updated.rb +34 -0
  35. data/lib/pay/stripe.rb +1 -0
  36. data/lib/pay/stripe/billable.rb +14 -5
  37. data/lib/pay/stripe/charge.rb +4 -0
  38. data/lib/pay/stripe/subscription.rb +7 -1
  39. data/lib/pay/stripe/webhooks/charge_succeeded.rb +7 -7
  40. data/lib/pay/stripe/webhooks/payment_action_required.rb +7 -8
  41. data/lib/pay/stripe/webhooks/subscription_created.rb +1 -1
  42. data/lib/pay/version.rb +1 -1
  43. metadata +63 -8
@@ -0,0 +1,39 @@
1
+ module Pay
2
+ module Paddle
3
+ module Charge
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ scope :paddle, -> { where(processor: :paddle) }
8
+
9
+ store_accessor :data, :paddle_receipt_url
10
+ end
11
+
12
+ def paddle?
13
+ processor == "paddle"
14
+ end
15
+
16
+ def paddle_charge
17
+ return unless owner.subscription
18
+ payments = PaddlePay::Subscription::Payment.list({subscription_id: owner.subscription.processor_id})
19
+ charges = payments.select { |p| p[:id].to_s == processor_id }
20
+ charges.try(:first)
21
+ rescue ::PaddlePay::PaddlePayError => e
22
+ raise Error, e.message
23
+ end
24
+
25
+ def paddle_refund!(amount_to_refund)
26
+ return unless owner.subscription
27
+ payments = PaddlePay::Subscription::Payment.list({subscription_id: owner.subscription.processor_id, is_paid: 1})
28
+ if payments.count > 0
29
+ PaddlePay::Subscription::Payment.refund(payments.last[:id], {amount: amount_to_refund})
30
+ update(amount_refunded: amount_to_refund)
31
+ else
32
+ raise Error, "Payment not found"
33
+ end
34
+ rescue ::PaddlePay::PaddlePayError => e
35
+ raise Error, e.message
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,59 @@
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
+ end
12
+
13
+ def paddle?
14
+ processor == "paddle"
15
+ end
16
+
17
+ def paddle_cancel
18
+ subscription = processor_subscription
19
+ PaddlePay::Subscription::User.cancel(processor_id)
20
+ if on_trial?
21
+ update(status: :canceled, ends_at: trial_ends_at)
22
+ else
23
+ update(status: :canceled, ends_at: DateTime.parse(subscription.next_payment[:date]))
24
+ end
25
+ rescue ::PaddlePay::PaddlePayError => e
26
+ raise Error, e.message
27
+ end
28
+
29
+ def paddle_cancel_now!
30
+ PaddlePay::Subscription::User.cancel(processor_id)
31
+ update(status: :canceled, ends_at: Time.zone.now)
32
+ rescue ::PaddlePay::PaddlePayError => e
33
+ raise Error, e.message
34
+ end
35
+
36
+ def paddle_pause
37
+ attributes = {pause: true}
38
+ response = PaddlePay::Subscription::User.update(processor_id, attributes)
39
+ update(status: :paused, ends_at: DateTime.parse(response[:next_payment][:date]))
40
+ end
41
+
42
+ def paddle_resume
43
+ attributes = {pause: false}
44
+ PaddlePay::Subscription::User.update(processor_id, attributes)
45
+ update(status: :active, ends_at: nil)
46
+ rescue ::PaddlePay::PaddlePayError => e
47
+ raise Error, e.message
48
+ end
49
+
50
+ def paddle_swap(plan)
51
+ attributes = {plan_id: plan, prorate: prorate}
52
+ attributes[:quantity] = quantity if quantity?
53
+ PaddlePay::Subscription::User.update(processor_id, attributes)
54
+ rescue ::PaddlePay::PaddlePayError => e
55
+ raise Error, e.message
56
+ end
57
+ end
58
+ end
59
+ 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: DateTime.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: "default", 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(user, charge)
14
+ if Pay.send_emails
15
+ Pay::UserMailer.refund(user, charge).deliver_later
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,64 @@
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: DateTime.parse(data["event_time"])
25
+ }.merge(payment_params(data["subscription_id"]))
26
+
27
+ charge.update(params)
28
+
29
+ charge
30
+ end
31
+
32
+ def notify_user(user, charge)
33
+ 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
+ {}
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,34 @@
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
+ 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"]
15
+
16
+ subscription.trial_ends_at = DateTime.parse(data["next_bill_date"]) if data["status"] == "trialing"
17
+
18
+ # 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
28
+
29
+ subscription.save!
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -12,6 +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
16
  ::StripeEvent.signing_secret = signing_secret
16
17
 
17
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
@@ -48,17 +54,20 @@ module Pay
48
54
  #
49
55
  # Returns Pay::Subscription
50
56
  def create_stripe_subscription(name, plan, options = {})
57
+ quantity = options.delete(:quantity) || 1
51
58
  opts = {
52
59
  expand: ["pending_setup_intent", "latest_invoice.payment_intent"],
53
- items: [plan: plan],
60
+ items: [plan: plan, quantity: quantity],
54
61
  off_session: true
55
62
  }.merge(options)
56
63
 
57
64
  # Inherit trial from plan unless trial override was specified
58
65
  opts[:trial_from_plan] = true unless opts[:trial_period_days]
59
66
 
60
- stripe_sub = customer.subscriptions.create(opts)
61
- subscription = create_subscription(stripe_sub, "stripe", name, plan, status: stripe_sub.status)
67
+ opts[:customer] = stripe_customer.id
68
+
69
+ stripe_sub = ::Stripe::Subscription.create(opts)
70
+ subscription = create_subscription(stripe_sub, "stripe", name, plan, status: stripe_sub.status, quantity: quantity)
62
71
 
63
72
  # No trial, card requires SCA
64
73
  if subscription.incomplete?
@@ -94,7 +103,7 @@ module Pay
94
103
  def update_stripe_email!
95
104
  customer = stripe_customer
96
105
  customer.email = email
97
- customer.description = customer_name
106
+ customer.name = customer_name
98
107
  customer.save
99
108
  end
100
109
 
@@ -134,7 +143,7 @@ module Pay
134
143
  private
135
144
 
136
145
  def create_stripe_customer
137
- customer = ::Stripe::Customer.create(email: email, description: customer_name)
146
+ customer = ::Stripe::Customer.create(email: email, name: customer_name)
138
147
  update(processor: "stripe", processor_id: customer.id)
139
148
 
140
149
  # Update the user's card on file if a token was passed in