pay 2.2.2 → 2.3.0

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.

Potentially problematic release.


This version of pay might be problematic. Click here for more details.

Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +137 -7
  3. data/Rakefile +2 -4
  4. data/app/controllers/pay/payments_controller.rb +2 -0
  5. data/app/controllers/pay/webhooks/paddle_controller.rb +36 -0
  6. data/app/models/pay/application_record.rb +6 -1
  7. data/app/models/pay/charge.rb +7 -0
  8. data/app/models/pay/subscription.rb +24 -3
  9. data/config/routes.rb +1 -0
  10. data/db/migrate/20200603134434_add_data_to_pay_models.rb +17 -0
  11. data/lib/pay.rb +3 -0
  12. data/lib/pay/billable.rb +5 -0
  13. data/lib/pay/braintree/billable.rb +10 -4
  14. data/lib/pay/braintree/charge.rb +4 -0
  15. data/lib/pay/braintree/subscription.rb +6 -0
  16. data/lib/pay/engine.rb +7 -0
  17. data/lib/pay/paddle.rb +38 -0
  18. data/lib/pay/paddle/billable.rb +66 -0
  19. data/lib/pay/paddle/charge.rb +39 -0
  20. data/lib/pay/paddle/subscription.rb +59 -0
  21. data/lib/pay/paddle/webhooks.rb +1 -0
  22. data/lib/pay/paddle/webhooks/signature_verifier.rb +115 -0
  23. data/lib/pay/paddle/webhooks/subscription_cancelled.rb +18 -0
  24. data/lib/pay/paddle/webhooks/subscription_created.rb +59 -0
  25. data/lib/pay/paddle/webhooks/subscription_payment_refunded.rb +21 -0
  26. data/lib/pay/paddle/webhooks/subscription_payment_succeeded.rb +64 -0
  27. data/lib/pay/paddle/webhooks/subscription_updated.rb +34 -0
  28. data/lib/pay/stripe/billable.rb +6 -0
  29. data/lib/pay/stripe/charge.rb +4 -0
  30. data/lib/pay/stripe/subscription.rb +6 -0
  31. data/lib/pay/stripe/webhooks/charge_succeeded.rb +7 -7
  32. data/lib/pay/stripe/webhooks/payment_action_required.rb +7 -8
  33. data/lib/pay/stripe/webhooks/subscription_created.rb +1 -1
  34. data/lib/pay/version.rb +1 -1
  35. metadata +59 -4
data/lib/pay.rb CHANGED
@@ -12,6 +12,9 @@ module Pay
12
12
  @@billable_class = "User"
13
13
  @@billable_table = @@billable_class.tableize
14
14
 
15
+ mattr_accessor :model_parent_class
16
+ @@model_parent_class = "ApplicationRecord"
17
+
15
18
  mattr_accessor :chargeable_class
16
19
  mattr_accessor :chargeable_table
17
20
  @@chargeable_class = "Pay::Charge"
@@ -19,6 +19,7 @@ module Pay
19
19
  include Pay::Billable::SyncEmail
20
20
  include Pay::Stripe::Billable if defined? ::Stripe
21
21
  include Pay::Braintree::Billable if defined? ::Braintree
22
+ include Pay::Paddle::Billable if defined? ::PaddlePay
22
23
 
23
24
  has_many :charges, class_name: Pay.chargeable_class, foreign_key: :owner_id, inverse_of: :owner, as: :owner
24
25
  has_many :subscriptions, class_name: Pay.subscription_class, foreign_key: :owner_id, inverse_of: :owner, as: :owner
@@ -118,6 +119,10 @@ module Pay
118
119
  braintree? && card_type == "PayPal"
119
120
  end
120
121
 
122
+ def paddle?
123
+ processor == "paddle"
124
+ end
125
+
121
126
  def has_incomplete_payment?(name: "default")
122
127
  subscription(name: name)&.has_incomplete_payment?
123
128
  end
@@ -1,6 +1,12 @@
1
1
  module Pay
2
2
  module Braintree
3
3
  module Billable
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ scope :braintree, -> { where(processor: :braintree) }
8
+ end
9
+
4
10
  # Handles Billable#customer
5
11
  #
6
12
  # Returns Braintree::Customer
@@ -35,7 +41,7 @@ module Pay
35
41
  # Returns a Pay::Charge
36
42
  def create_braintree_charge(amount, options = {})
37
43
  args = {
38
- amount: amount / 100.0,
44
+ amount: amount.to_i / 100.0,
39
45
  customer_id: customer.id,
40
46
  options: {submit_for_settlement: true}
41
47
  }.merge(options)
@@ -111,11 +117,11 @@ module Pay
111
117
  def braintree_trial_end_date(subscription)
112
118
  return unless subscription.trial_period
113
119
  # Braintree returns dates without time zones, so we'll assume they're UTC
114
- Time.parse(subscription.first_billing_date).end_of_day
120
+ subscription.first_billing_date.end_of_day
115
121
  end
116
122
 
117
123
  def update_subscriptions_to_payment_method(token)
118
- subscriptions.each do |subscription|
124
+ subscriptions.braintree.each do |subscription|
119
125
  if subscription.active?
120
126
  gateway.subscription.update(subscription.processor_id, {payment_method_token: token})
121
127
  end
@@ -175,7 +181,7 @@ module Pay
175
181
 
176
182
  def card_details_for_braintree_transaction(transaction)
177
183
  case transaction.payment_instrument_type
178
- when "credit_card", "samsung_pay_card", "masterpass_card", "samsung_pay_card", "visa_checkout_card"
184
+ when "credit_card", "samsung_pay_card", "masterpass_card", "visa_checkout_card"
179
185
  payment_method = transaction.send("#{transaction.payment_instrument_type}_details")
180
186
  {
181
187
  card_type: payment_method.card_type,
@@ -3,6 +3,10 @@ module Pay
3
3
  module Charge
4
4
  extend ActiveSupport::Concern
5
5
 
6
+ included do
7
+ scope :braintree, -> { where(processor: :braintree) }
8
+ end
9
+
6
10
  def braintree?
7
11
  processor == "braintree"
8
12
  end
@@ -1,6 +1,12 @@
1
1
  module Pay
2
2
  module Braintree
3
3
  module Subscription
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ scope :braintree, -> { where(processor: :braintree) }
8
+ end
9
+
4
10
  def braintree?
5
11
  processor == "braintree"
6
12
  end
@@ -11,6 +11,11 @@ begin
11
11
  require "stripe_event"
12
12
  rescue LoadError
13
13
  end
14
+
15
+ begin
16
+ require "paddle_pay"
17
+ rescue LoadError
18
+ end
14
19
  # rubocop:enable Lint/HandleExceptions
15
20
 
16
21
  module Pay
@@ -21,6 +26,7 @@ module Pay
21
26
  # Include processor backends
22
27
  require "pay/stripe" if defined? ::Stripe
23
28
  require "pay/braintree" if defined? ::Braintree
29
+ require "pay/paddle" if defined? ::PaddlePay
24
30
 
25
31
  if Pay.automount_routes
26
32
  app.routes.append do
@@ -32,6 +38,7 @@ module Pay
32
38
  config.to_prepare do
33
39
  Pay::Stripe.setup if defined? ::Stripe
34
40
  Pay::Braintree.setup if defined? ::Braintree
41
+ Pay::Paddle.setup if defined? ::PaddlePay
35
42
 
36
43
  Pay.charge_model.include Pay::Receipts if defined? ::Receipts::Receipt
37
44
  end
@@ -0,0 +1,38 @@
1
+ require "pay/env"
2
+ require "pay/paddle/billable"
3
+ require "pay/paddle/charge"
4
+ require "pay/paddle/subscription"
5
+ require "pay/paddle/webhooks"
6
+
7
+ module Pay
8
+ module Paddle
9
+ include Env
10
+
11
+ extend self
12
+
13
+ def setup
14
+ ::PaddlePay.config.vendor_id = vendor_id
15
+ ::PaddlePay.config.vendor_auth_code = vendor_auth_code
16
+
17
+ Pay.charge_model.include Pay::Paddle::Charge
18
+ Pay.subscription_model.include Pay::Paddle::Subscription
19
+ Pay.billable_models.each { |model| model.include Pay::Paddle::Billable }
20
+ end
21
+
22
+ def vendor_id
23
+ find_value_by_name(:paddle, :vendor_id)
24
+ end
25
+
26
+ def vendor_auth_code
27
+ find_value_by_name(:paddle, :vendor_auth_code)
28
+ end
29
+
30
+ def public_key_base64
31
+ find_value_by_name(:paddle, :public_key_base64)
32
+ end
33
+
34
+ def passthrough(owner:, **options)
35
+ options.merge(owner_sgid: owner.to_sgid.to_s).to_json
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,66 @@
1
+ module Pay
2
+ module Paddle
3
+ module Billable
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ scope :paddle, -> { where(processor: :paddle) }
8
+ end
9
+
10
+ def paddle_customer
11
+ # pass
12
+ end
13
+
14
+ def create_paddle_charge(amount, options = {})
15
+ return unless subscription.processor_id
16
+ raise Pay::Error, "A charge_name is required to create a one-time charge" if options[:charge_name].nil?
17
+ response = PaddlePay::Subscription::Charge.create(subscription.processor_id, amount.to_f / 100, options[:charge_name], options)
18
+ charge = charges.find_or_initialize_by(
19
+ processor: :paddle,
20
+ processor_id: response[:invoice_id]
21
+ )
22
+ charge.update(
23
+ amount: Integer(response[:amount].to_f * 100),
24
+ card_type: subscription.processor_subscription.payment_information[:payment_method],
25
+ paddle_receipt_url: response[:receipt_url],
26
+ created_at: DateTime.parse(response[:payment_date])
27
+ )
28
+ charge
29
+ rescue ::PaddlePay::PaddlePayError => e
30
+ raise Error, e.message
31
+ end
32
+
33
+ def create_paddle_subscription(name, plan, options = {})
34
+ # pass
35
+ end
36
+
37
+ def update_paddle_card(token)
38
+ # pass
39
+ end
40
+
41
+ def update_paddle_email!
42
+ # pass
43
+ end
44
+
45
+ def paddle_trial_end_date(subscription)
46
+ return unless subscription.state == "trialing"
47
+ DateTime.parse(subscription.next_payment[:date]).end_of_day
48
+ end
49
+
50
+ def paddle_subscription(subscription_id, options = {})
51
+ hash = PaddlePay::Subscription::User.list({subscription_id: subscription_id}, options).try(:first)
52
+ OpenStruct.new(hash)
53
+ rescue ::PaddlePay::PaddlePayError => e
54
+ raise Error, e.message
55
+ end
56
+
57
+ def paddle_invoice!(options = {})
58
+ # pass
59
+ end
60
+
61
+ def paddle_upcoming_invoice
62
+ # pass
63
+ end
64
+ end
65
+ end
66
+ end
@@ -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