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.
- checksums.yaml +4 -4
- data/README.md +137 -7
- data/Rakefile +2 -4
- data/app/controllers/pay/payments_controller.rb +2 -0
- data/app/controllers/pay/webhooks/paddle_controller.rb +36 -0
- data/app/models/pay/application_record.rb +6 -1
- data/app/models/pay/charge.rb +7 -0
- data/app/models/pay/subscription.rb +24 -3
- data/config/routes.rb +1 -0
- data/db/migrate/20200603134434_add_data_to_pay_models.rb +17 -0
- data/lib/pay.rb +3 -0
- data/lib/pay/billable.rb +5 -0
- data/lib/pay/braintree/billable.rb +10 -4
- data/lib/pay/braintree/charge.rb +4 -0
- data/lib/pay/braintree/subscription.rb +6 -0
- data/lib/pay/engine.rb +7 -0
- data/lib/pay/paddle.rb +38 -0
- data/lib/pay/paddle/billable.rb +66 -0
- data/lib/pay/paddle/charge.rb +39 -0
- data/lib/pay/paddle/subscription.rb +59 -0
- data/lib/pay/paddle/webhooks.rb +1 -0
- data/lib/pay/paddle/webhooks/signature_verifier.rb +115 -0
- data/lib/pay/paddle/webhooks/subscription_cancelled.rb +18 -0
- data/lib/pay/paddle/webhooks/subscription_created.rb +59 -0
- data/lib/pay/paddle/webhooks/subscription_payment_refunded.rb +21 -0
- data/lib/pay/paddle/webhooks/subscription_payment_succeeded.rb +64 -0
- data/lib/pay/paddle/webhooks/subscription_updated.rb +34 -0
- data/lib/pay/stripe/billable.rb +6 -0
- data/lib/pay/stripe/charge.rb +4 -0
- data/lib/pay/stripe/subscription.rb +6 -0
- data/lib/pay/stripe/webhooks/charge_succeeded.rb +7 -7
- data/lib/pay/stripe/webhooks/payment_action_required.rb +7 -8
- data/lib/pay/stripe/webhooks/subscription_created.rb +1 -1
- data/lib/pay/version.rb +1 -1
- 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"
|
data/lib/pay/billable.rb
CHANGED
@@ -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
|
-
|
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", "
|
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,
|
data/lib/pay/braintree/charge.rb
CHANGED
data/lib/pay/engine.rb
CHANGED
@@ -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
|
data/lib/pay/paddle.rb
ADDED
@@ -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
|