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
@@ -1,5 +1,10 @@
1
1
  module Pay
2
- class ApplicationRecord < ActiveRecord::Base
2
+ class ApplicationRecord < Pay.model_parent_class.constantize
3
3
  self.abstract_class = true
4
+
5
+ def self.json_column?(name)
6
+ return unless connected? && table_exists?
7
+ [:json, :jsonb].include?(attribute_types[name].type)
8
+ end
4
9
  end
5
10
  end
@@ -2,6 +2,9 @@ module Pay
2
2
  class Charge < ApplicationRecord
3
3
  self.table_name = Pay.chargeable_table
4
4
 
5
+ # Only serialize for non-json columns
6
+ serialize :data unless json_column?("data")
7
+
5
8
  # Associations
6
9
  belongs_to :owner, polymorphic: true
7
10
 
@@ -39,5 +42,9 @@ module Pay
39
42
  def paypal?
40
43
  braintree? && card_type == "PayPal"
41
44
  end
45
+
46
+ def paddle?
47
+ processor == "paddle"
48
+ end
42
49
  end
43
50
  end
@@ -2,7 +2,10 @@ module Pay
2
2
  class Subscription < ApplicationRecord
3
3
  self.table_name = Pay.subscription_table
4
4
 
5
- STATUSES = %w[incomplete incomplete_expired trialing active past_due canceled unpaid]
5
+ STATUSES = %w[incomplete incomplete_expired trialing active past_due canceled unpaid paused]
6
+
7
+ # Only serialize for non-json columns
8
+ serialize :data unless json_column?("data")
6
9
 
7
10
  # Associations
8
11
  belongs_to :owner, polymorphic: true
@@ -66,6 +69,15 @@ module Pay
66
69
  past_due? || incomplete?
67
70
  end
68
71
 
72
+ def paused?
73
+ status == "paused"
74
+ end
75
+
76
+ def pause
77
+ return unless paddle?
78
+ send("#{processor}_pause")
79
+ end
80
+
69
81
  def cancel
70
82
  send("#{processor}_cancel")
71
83
  end
@@ -76,8 +88,17 @@ module Pay
76
88
 
77
89
  def resume
78
90
  unless on_grace_period?
79
- raise StandardError,
80
- "You can only resume subscriptions within their grace period."
91
+ unless paddle?
92
+ raise StandardError,
93
+ "You can only resume subscriptions within their grace period."
94
+ end
95
+ end
96
+
97
+ unless paused?
98
+ if paddle?
99
+ raise StandardError,
100
+ "You can only resume paused subscriptions."
101
+ end
81
102
  end
82
103
 
83
104
  send("#{processor}_resume")
@@ -51,7 +51,7 @@
51
51
  </div>
52
52
  <% end %>
53
53
 
54
- <%= link_to t("back"), root_path, class: "inline-block w-full px-4 py-3 bg-gray-200 hover:bg-gray-300 text-center text-gray-700 rounded-lg" %>
54
+ <%= link_to t("back"), @redirect_to, class: "inline-block w-full px-4 py-3 bg-gray-200 hover:bg-gray-300 text-center text-gray-700 rounded-lg" %>
55
55
  </div>
56
56
 
57
57
  <p class="text-center text-gray-500 text-sm">
@@ -4,4 +4,5 @@ Pay::Engine.routes.draw do
4
4
  resources :payments, only: [:show], module: :pay
5
5
  post "webhooks/stripe", to: "stripe_event/webhook#event"
6
6
  post "webhooks/braintree", to: "pay/webhooks/braintree#create"
7
+ post "webhooks/paddle", to: "pay/webhooks/paddle#create"
7
8
  end
@@ -1,7 +1,8 @@
1
1
  class CreatePaySubscriptions < ActiveRecord::Migration[4.2]
2
2
  def change
3
3
  create_table :pay_subscriptions do |t|
4
- t.references :owner
4
+ # Some Billable objects use string as ID, add `type: :string` if needed
5
+ t.references :owner, polymorphic: true
5
6
  t.string :name, null: false
6
7
  t.string :processor, null: false
7
8
  t.string :processor_id, null: false
@@ -1,6 +1,7 @@
1
1
  class CreatePayCharges < ActiveRecord::Migration[4.2]
2
2
  def change
3
3
  create_table :pay_charges do |t|
4
+ # Some Billable objects use string as ID, add `type: :string` if needed
4
5
  t.references :owner, polymorphic: true
5
6
  t.string :processor, null: false
6
7
  t.string :processor_id, null: false
@@ -0,0 +1,17 @@
1
+ class AddDataToPayModels < ActiveRecord::Migration[4.2]
2
+ def change
3
+ add_column :pay_subscriptions, :data, data_column_type
4
+ add_column :pay_charges, :data, data_column_type
5
+ end
6
+
7
+ def data_column_type
8
+ case ActiveRecord::Base.configurations.default_hash.dig("adapter")
9
+ when "mysql2"
10
+ :json
11
+ when "postgresql"
12
+ :jsonb
13
+ else
14
+ :text
15
+ end
16
+ end
17
+ end
@@ -26,7 +26,7 @@ module ActiveRecord
26
26
  end
27
27
 
28
28
  indent_depth = class_path.size - 1
29
- content = content.split("\n").map { |line| " " * indent_depth + line } .join("\n") << "\n"
29
+ content = content.split("\n").map { |line| " " * indent_depth + line }.join("\n") << "\n"
30
30
 
31
31
  inject_into_class(model_path, class_path.last, content) if model_exists?
32
32
  end
@@ -4,10 +4,9 @@ module Pay
4
4
  module Generators
5
5
  module OrmHelpers
6
6
  def model_contents
7
- buffer = <<-CONTENT
7
+ <<-CONTENT
8
8
  include Pay::Billable
9
9
  CONTENT
10
- buffer
11
10
  end
12
11
 
13
12
  private
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"
@@ -105,11 +108,17 @@ module Pay
105
108
  class BraintreeError < Error
106
109
  attr_reader :result
107
110
 
108
- def initialize(result)
111
+ def initialize(result = nil)
109
112
  @result = result
110
113
  end
111
114
  end
112
115
 
116
+ class BraintreeAuthorizationError < BraintreeError
117
+ def message
118
+ "Either the data you submitted is malformed and does not match the API or the API key you used may not be authorized to perform this action."
119
+ end
120
+ end
121
+
113
122
  class InvalidPaymentMethod < Error
114
123
  attr_reader :payment
115
124
 
@@ -118,7 +127,7 @@ module Pay
118
127
  end
119
128
 
120
129
  def message
121
- "This payment attempt failed beacuse of an invalid payment method."
130
+ "This payment attempt failed because of an invalid payment method."
122
131
  end
123
132
  end
124
133
 
@@ -19,9 +19,10 @@ 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
- has_many :charges, class_name: Pay.chargeable_class, foreign_key: :owner_id, inverse_of: :owner
24
- has_many :subscriptions, class_name: Pay.subscription_class, foreign_key: :owner_id, inverse_of: :owner
24
+ has_many :charges, class_name: Pay.chargeable_class, foreign_key: :owner_id, inverse_of: :owner, as: :owner
25
+ has_many :subscriptions, class_name: Pay.subscription_class, foreign_key: :owner_id, inverse_of: :owner, as: :owner
25
26
 
26
27
  attribute :plan, :string
27
28
  attribute :quantity, :integer
@@ -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
@@ -19,7 +19,7 @@ module Pay
19
19
 
20
20
  Pay.charge_model.include Pay::Braintree::Charge
21
21
  Pay.subscription_model.include Pay::Braintree::Subscription
22
- Pay.billable_models.each { |model| model.include Pay::Stripe::Billable }
22
+ Pay.billable_models.each { |model| model.include Pay::Braintree::Billable }
23
23
  end
24
24
 
25
25
  def public_key
@@ -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
@@ -24,6 +30,8 @@ module Pay
24
30
 
25
31
  result.customer
26
32
  end
33
+ rescue ::Braintree::AuthorizationError
34
+ raise BraintreeAuthorizationError
27
35
  rescue ::Braintree::BraintreeError => e
28
36
  raise BraintreeError, e.message
29
37
  end
@@ -33,7 +41,7 @@ module Pay
33
41
  # Returns a Pay::Charge
34
42
  def create_braintree_charge(amount, options = {})
35
43
  args = {
36
- amount: amount / 100.0,
44
+ amount: amount.to_i / 100.0,
37
45
  customer_id: customer.id,
38
46
  options: {submit_for_settlement: true}
39
47
  }.merge(options)
@@ -42,6 +50,8 @@ module Pay
42
50
  raise BraintreeError.new(result), result.message unless result.success?
43
51
 
44
52
  save_braintree_transaction(result.transaction)
53
+ rescue ::Braintree::AuthorizationError
54
+ raise BraintreeAuthorizationError
45
55
  rescue ::Braintree::BraintreeError => e
46
56
  raise BraintreeError, e.message
47
57
  end
@@ -67,6 +77,8 @@ module Pay
67
77
  raise BraintreeError.new(result), result.message unless result.success?
68
78
 
69
79
  create_subscription(result.subscription, "braintree", name, plan, status: :active)
80
+ rescue ::Braintree::AuthorizationError
81
+ raise BraintreeAuthorizationError
70
82
  rescue ::Braintree::BraintreeError => e
71
83
  raise BraintreeError, e.message
72
84
  end
@@ -88,6 +100,8 @@ module Pay
88
100
  update_braintree_card_on_file result.payment_method
89
101
  update_subscriptions_to_payment_method(result.payment_method.token)
90
102
  true
103
+ rescue ::Braintree::AuthorizationError
104
+ raise BraintreeAuthorizationError
91
105
  rescue ::Braintree::BraintreeError => e
92
106
  raise BraintreeError, e.message
93
107
  end
@@ -103,11 +117,11 @@ module Pay
103
117
  def braintree_trial_end_date(subscription)
104
118
  return unless subscription.trial_period
105
119
  # Braintree returns dates without time zones, so we'll assume they're UTC
106
- Time.parse(subscription.first_billing_date).end_of_day
120
+ subscription.first_billing_date.end_of_day
107
121
  end
108
122
 
109
123
  def update_subscriptions_to_payment_method(token)
110
- subscriptions.each do |subscription|
124
+ subscriptions.braintree.each do |subscription|
111
125
  if subscription.active?
112
126
  gateway.subscription.update(subscription.processor_id, {payment_method_token: token})
113
127
  end
@@ -167,7 +181,7 @@ module Pay
167
181
 
168
182
  def card_details_for_braintree_transaction(transaction)
169
183
  case transaction.payment_instrument_type
170
- 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"
171
185
  payment_method = transaction.send("#{transaction.payment_instrument_type}_details")
172
186
  {
173
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